foundation.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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 { preventScroll } = this.getProps();
  70. const tabs = [...event.target.parentNode.childNodes].filter(item => {
  71. return get(item, 'attributes.data-tabkey.value', '').includes('semiTab') && get(item, 'attributes.aria-disabled.value', '') !== "true";
  72. });
  73. switch (event.key) {
  74. case "ArrowLeft":
  75. case "ArrowRight":
  76. case "ArrowUp":
  77. case "ArrowDown":
  78. this.determineOrientation(event, tabs);
  79. break;
  80. case "Backspace":
  81. case "Delete":
  82. this.handleDeleteKeyDown(event, tabs, itemKey, closable);
  83. break;
  84. case "Enter":
  85. case " ":
  86. this.handleTabClick(itemKey, event);
  87. this.handlePrevent(event);
  88. break;
  89. case "Home":
  90. tabs[0].focus({ preventScroll }); // focus first tab
  91. this.handlePrevent(event);
  92. break;
  93. case "End":
  94. tabs[tabs.length - 1].focus({ preventScroll }); // focus last tab
  95. this.handlePrevent(event);
  96. break;
  97. }
  98. }
  99. determineOrientation(event: any, tabs: HTMLElement[]): void {
  100. const { tabPosition } = this.getProps();
  101. const isVertical = tabPosition === 'left';
  102. if (isVertical) {
  103. if (event.key === "ArrowUp" || event.key === "ArrowDown") {
  104. this.switchTabOnArrowPress(event, tabs);
  105. this.handlePrevent(event);
  106. }
  107. } else {
  108. if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
  109. this.switchTabOnArrowPress(event, tabs);
  110. this.handlePrevent(event);
  111. }
  112. }
  113. }
  114. handleDeleteKeyDown(event:any, tabs: HTMLElement[], itemKey: string, closable: boolean): void {
  115. const { preventScroll } = this.getProps();
  116. if (closable) {
  117. this.handleTabDelete(itemKey);
  118. const index = tabs.indexOf(event.target);
  119. // Move focus to next element after deletion
  120. // If the element is the last removable tab, focus to its previous tab
  121. if (tabs.length !== 1 ){
  122. tabs[index + 1 >= tabs.length ? index - 1 : index + 1].focus({ preventScroll });
  123. }
  124. }
  125. }
  126. switchTabOnArrowPress(event: any, tabs: HTMLElement[]): void {
  127. const { preventScroll } = this.getProps();
  128. const index = tabs.indexOf(event.target);
  129. const direction = {
  130. "ArrowLeft": -1,
  131. "ArrowUp": -1,
  132. "ArrowRight": 1,
  133. "ArrowDown": 1,
  134. };
  135. if (direction[event.key]) {
  136. if (index !== undefined) {
  137. if (tabs[index + direction[event.key]]) {
  138. tabs[index+ direction[event.key]].focus({ preventScroll });
  139. } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
  140. tabs[tabs.length - 1].focus({ preventScroll }); // focus last tab
  141. } else if (event.key === "ArrowRight" || event.key == "ArrowDown") {
  142. tabs[0].focus({ preventScroll }); // focus first tab
  143. }
  144. }
  145. }
  146. }
  147. }
  148. export default TabsFoundation;