foundation.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  2. import { get, noop } from 'lodash';
  3. export interface TabsAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  4. collectPane: () => void;
  5. collectActiveKey: () => void;
  6. notifyTabClick: (activeKey: string, event: any) => void;
  7. notifyChange: (activeKey: string) => void;
  8. setNewActiveKey: (activeKey: string) => void;
  9. getDefaultActiveKeyFromChildren: () => string;
  10. notifyTabDelete: (tabKey: string) => void;
  11. }
  12. class TabsFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<TabsAdapter<P, S>, P, S> {
  13. constructor(adapter: TabsAdapter<P, S>) {
  14. super({ ...adapter });
  15. }
  16. init(): void {
  17. this._adapter.collectPane();
  18. }
  19. destroy = noop;
  20. _notifyChange(activeKey: string): void {
  21. const { activeKey: stateActiveKey } = this.getStates();
  22. if (stateActiveKey !== activeKey) {
  23. this._adapter.notifyChange(activeKey);
  24. }
  25. }
  26. handleTabClick(activeKey: string, event: any): void {
  27. const isControlledComponent = this._isInProps('activeKey');
  28. if (isControlledComponent) {
  29. this._notifyChange(activeKey);
  30. } else {
  31. this._notifyChange(activeKey);
  32. this.handleNewActiveKey(activeKey);
  33. }
  34. this._adapter.notifyTabClick(activeKey, event);
  35. }
  36. handleNewActiveKey(activeKey: string): void {
  37. const { activeKey: stateActiveKey } = this.getStates();
  38. if (stateActiveKey !== activeKey) {
  39. this._adapter.setNewActiveKey(activeKey);
  40. }
  41. }
  42. getDefaultActiveKey(): string {
  43. let activeKey;
  44. const props = this.getProps();
  45. if ('activeKey' in props) {
  46. activeKey = props.activeKey;
  47. } else if ('defaultActiveKey' in props) {
  48. activeKey = props.defaultActiveKey;
  49. } else {
  50. activeKey = this._adapter.getDefaultActiveKeyFromChildren();
  51. }
  52. return activeKey;
  53. }
  54. handleTabListChange(): void {
  55. this._adapter.collectPane();
  56. }
  57. handleTabPanesChange(): void {
  58. this._adapter.collectPane();
  59. this._adapter.collectActiveKey();
  60. }
  61. handleTabDelete(tabKey: string): void {
  62. this._adapter.notifyTabDelete(tabKey);
  63. }
  64. handlePrevent = (event: any) => {
  65. event.stopPropagation();
  66. event.preventDefault();
  67. }
  68. handleKeyDown = (event: any, itemKey: string, closable: boolean) => {
  69. const tabs = [...event.target.parentNode.childNodes].filter(item => {
  70. return get(item, 'attributes.data-tabkey.value', '').includes('semiTab') && get(item, 'attributes.aria-disabled.value', '') !== "true";
  71. });
  72. switch (event.key) {
  73. case "ArrowLeft":
  74. case "ArrowRight":
  75. case "ArrowUp":
  76. case "ArrowDown":
  77. this.determineOrientation(event, tabs);
  78. break;
  79. case "Backspace":
  80. case "Delete":
  81. this.handleDeleteKeyDown(event, tabs, itemKey, closable);
  82. break;
  83. case "Enter":
  84. case " ":
  85. this.handleTabClick(itemKey, event);
  86. this.handlePrevent(event);
  87. break;
  88. case "Home":
  89. tabs[0].focus(); // focus first tab
  90. this.handlePrevent(event);
  91. break;
  92. case "End":
  93. tabs[tabs.length - 1].focus(); // focus last tab
  94. this.handlePrevent(event);
  95. break;
  96. }
  97. }
  98. determineOrientation(event: any, tabs: HTMLElement[]): void {
  99. const { tabPosition } = this.getProps();
  100. const isVertical = tabPosition === 'left';
  101. if (isVertical) {
  102. if (event.key === "ArrowUp" || event.key === "ArrowDown") {
  103. this.switchTabOnArrowPress(event, tabs);
  104. this.handlePrevent(event);
  105. }
  106. } else {
  107. if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
  108. this.switchTabOnArrowPress(event, tabs);
  109. this.handlePrevent(event);
  110. }
  111. }
  112. }
  113. handleDeleteKeyDown(event:any, tabs: HTMLElement[], itemKey: string, closable: boolean): void {
  114. if (closable) {
  115. this.handleTabDelete(itemKey);
  116. const index = tabs.indexOf(event.target);
  117. // Move focus to next element after deletion
  118. // If the element is the last removable tab, focus to its previous tab
  119. if (tabs.length !== 1 ){
  120. tabs[index + 1 >= tabs.length ? index - 1 : index + 1].focus();
  121. }
  122. }
  123. }
  124. switchTabOnArrowPress(event: any, tabs: HTMLElement[]): void {
  125. const index = tabs.indexOf(event.target);
  126. const direction = {
  127. "ArrowLeft": -1,
  128. "ArrowUp": -1,
  129. "ArrowRight": 1,
  130. "ArrowDown": 1,
  131. };
  132. if (direction[event.key]) {
  133. if (index !== undefined) {
  134. if (tabs[index + direction[event.key]]) {
  135. tabs[index+ direction[event.key]].focus();
  136. } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
  137. tabs[tabs.length - 1].focus(); // focus last tab
  138. } else if (event.key === "ArrowRight" || event.key == "ArrowDown") {
  139. tabs[0].focus(); // focus first tab
  140. }
  141. }
  142. }
  143. }
  144. }
  145. export default TabsFoundation;