You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Tabs.tsx 3.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import React, { useCallback, useEffect, useState } from 'react';
  2. /**
  3. * The type of the React {@code Component} props of {@link Tabs}.
  4. */
  5. interface IProps {
  6. /**
  7. * Accessibility label for the tabs container.
  8. *
  9. */
  10. accessibilityLabel: string;
  11. /**
  12. * Tabs information.
  13. */
  14. tabs: {
  15. content: any;
  16. id: string;
  17. label: string;
  18. }[];
  19. }
  20. /**
  21. * A React component that implements tabs.
  22. *
  23. * @returns {ReactElement} The component.
  24. */
  25. const Tabs = ({ accessibilityLabel, tabs }: IProps) => {
  26. const [ current, setCurrent ] = useState(0);
  27. const onClick = useCallback(index => (event: React.MouseEvent) => {
  28. event.preventDefault();
  29. setCurrent(index);
  30. }, []);
  31. const onKeyDown = useCallback(index => (event: React.KeyboardEvent) => {
  32. let newIndex = null;
  33. if (event.key === 'ArrowLeft') {
  34. event.preventDefault();
  35. newIndex = index === 0 ? tabs.length - 1 : index - 1;
  36. }
  37. if (event.key === 'ArrowRight') {
  38. event.preventDefault();
  39. newIndex = index === tabs.length - 1 ? 0 : index + 1;
  40. }
  41. if (newIndex !== null) {
  42. setCurrent(newIndex);
  43. }
  44. }, [ tabs ]);
  45. useEffect(() => {
  46. // this test is needed to make sure the effect is triggered because of user actually changing tab
  47. if (document.activeElement?.getAttribute('role') === 'tab') {
  48. // @ts-ignore
  49. document.querySelector(`#${`${tabs[current].id}-tab`}`)?.focus();
  50. }
  51. }, [ current, tabs ]);
  52. return (
  53. <div className = 'tab-container'>
  54. { tabs.length > 1
  55. ? (
  56. <>
  57. <div
  58. aria-label = { accessibilityLabel }
  59. className = 'tab-buttons'
  60. role = 'tablist'>
  61. {tabs.map((tab, index) => (
  62. <button
  63. aria-controls = { `${tab.id}-panel` }
  64. aria-selected = { current === index ? 'true' : 'false' }
  65. id = { `${tab.id}-tab` }
  66. key = { tab.id }
  67. onClick = { onClick(index) }
  68. onKeyDown = { onKeyDown(index) }
  69. role = 'tab'
  70. tabIndex = { current === index ? undefined : -1 }>
  71. {tab.label}
  72. </button>
  73. ))}
  74. </div>
  75. {tabs.map((tab, index) => (
  76. <div
  77. aria-labelledby = { `${tab.id}-tab` }
  78. className = { current === index ? 'tab-content' : 'hide' }
  79. id = { `${tab.id}-panel` }
  80. key = { tab.id }
  81. role = 'tabpanel'
  82. tabIndex = { 0 }>
  83. {tab.content}
  84. </div>
  85. ))}
  86. </>
  87. )
  88. : (
  89. <>
  90. <h2 className = 'sr-only'>{accessibilityLabel}</h2>
  91. <div className = 'tab-content'>{tabs[0].content}</div>
  92. </>
  93. )
  94. }
  95. </div>
  96. );
  97. };
  98. export default Tabs;