uikit-react-public 0.29.6 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/dist/components/Accordion/Accordion.stories.d.ts +1 -1
  2. package/dist/components/Alert/Alert.stories.d.ts +1 -1
  3. package/dist/components/AppHeader/AppHeader.stories.d.ts +1 -1
  4. package/dist/components/AppMenu/AppMenu.stories.d.ts +1 -1
  5. package/dist/components/Avatar/Avatar.stories.d.ts +1 -1
  6. package/dist/components/Badge/Badge.stories.d.ts +1 -1
  7. package/dist/components/BaseCheckbox/BaseCheckbox.stories.d.ts +1 -1
  8. package/dist/components/Blanket/Blanket.stories.d.ts +1 -1
  9. package/dist/components/Breadcrumbs/Breadcrumbs.stories.d.ts +1 -1
  10. package/dist/components/Button/Button.stories.d.ts +1 -1
  11. package/dist/components/Calendar/Calendar.stories.d.ts +1 -1
  12. package/dist/components/Calendar/subcomponents/Day.stories.d.ts +1 -1
  13. package/dist/components/Checkbox/Checkbox.stories.d.ts +1 -1
  14. package/dist/components/Chip/Chip.stories.d.ts +1 -1
  15. package/dist/components/Datepicker/Datepicker.stories.d.ts +1 -1
  16. package/dist/components/Dialog/BaseDialog.d.ts +1 -1
  17. package/dist/components/Dialog/Dialog.d.ts +1 -1
  18. package/dist/components/Dialog/Dialog.stories.d.ts +2 -2
  19. package/dist/components/Divider/Divider.stories.d.ts +1 -1
  20. package/dist/components/Dropdown/Dropdown.stories.d.ts +1 -1
  21. package/dist/components/FeedbackDialog/FeedbackDialog.stories.d.ts +1 -1
  22. package/dist/components/Field/Field.stories.d.ts +1 -1
  23. package/dist/components/FileInput/FileInput.stories.d.ts +1 -1
  24. package/dist/components/Footer/Footer.stories.d.ts +1 -1
  25. package/dist/components/Header/Header.stories.d.ts +1 -1
  26. package/dist/components/Heading/Heading.stories.d.ts +1 -1
  27. package/dist/components/Icon/Icon.stories.d.ts +1 -1
  28. package/dist/components/IconButton/IconButton.stories.d.ts +1 -1
  29. package/dist/components/Input/Input.stories.d.ts +1 -1
  30. package/dist/components/Label/Label.stories.d.ts +1 -1
  31. package/dist/components/Layout/Layout.stories.d.ts +1 -1
  32. package/dist/components/Link/Link.stories.d.ts +1 -1
  33. package/dist/components/Main/Main.stories.d.ts +1 -1
  34. package/dist/components/Modal/Modal.stories.d.ts +1 -1
  35. package/dist/components/NativeDatepicker/NativeDatepicker.stories.d.ts +1 -1
  36. package/dist/components/Overlay/Overlay.stories.d.ts +2 -2
  37. package/dist/components/Pagination/Pagination.stories.d.ts +1 -1
  38. package/dist/components/Paragraph/Paragraph.stories.d.ts +1 -1
  39. package/dist/components/Radio/Radio.stories.d.ts +1 -1
  40. package/dist/components/Search/Search.stories.d.ts +1 -1
  41. package/dist/components/Select/Select.stories.d.ts +1 -1
  42. package/dist/components/Snackbar/Snackbar.stories.d.ts +1 -1
  43. package/dist/components/Spinner/Spinner.stories.d.ts +1 -1
  44. package/dist/components/StandaloneLink/StandaloneLink.stories.d.ts +1 -1
  45. package/dist/components/Table/Table.stories.d.ts +1 -1
  46. package/dist/components/Table/subcomponents/Cell/Cell.stories.d.ts +2 -2
  47. package/dist/components/Table/subcomponents/HeadCell/HeadCell.stories.d.ts +2 -2
  48. package/dist/components/Tabs/Tab.d.ts +11 -5
  49. package/dist/components/Tabs/TabContext.d.ts +14 -8
  50. package/dist/components/Tabs/Tabs.d.ts +25 -8
  51. package/dist/components/Tabs/Tabs.stories.d.ts +5 -9
  52. package/dist/components/Tabs/TabsList.d.ts +9 -0
  53. package/dist/components/Tabs/TabsPanel.d.ts +10 -0
  54. package/dist/components/Tabs/index.d.ts +2 -1
  55. package/dist/components/Textarea/Textarea.stories.d.ts +1 -1
  56. package/dist/components/Timepicker/Timepicker.stories.d.ts +1 -1
  57. package/dist/components/Toggle/Toggle.stories.d.ts +1 -1
  58. package/dist/components/Tooltip/Tooltip.stories.d.ts +1 -1
  59. package/dist/components/UclLogo/UclLogo.stories.d.ts +1 -1
  60. package/dist/components/WeekPicker/WeekPicker.stories.d.ts +1 -1
  61. package/dist/components/index.d.ts +1 -1
  62. package/dist/index.js +4392 -4123
  63. package/dist/utils/announce.d.ts +2 -1
  64. package/lib/Welcome.mdx +1 -1
  65. package/lib/components/Accordion/Accordion.stories.tsx +1 -1
  66. package/lib/components/Alert/Alert.mdx +1 -1
  67. package/lib/components/Alert/Alert.stories.tsx +1 -1
  68. package/lib/components/AppHeader/AppHeader.stories.tsx +1 -1
  69. package/lib/components/AppMenu/AppMenu.stories.tsx +1 -1
  70. package/lib/components/Avatar/Avatar.mdx +1 -1
  71. package/lib/components/Avatar/Avatar.stories.tsx +1 -1
  72. package/lib/components/Badge/Badge.stories.tsx +1 -1
  73. package/lib/components/BaseCheckbox/BaseCheckbox.stories.tsx +2 -2
  74. package/lib/components/Blanket/Blanket.stories.tsx +1 -1
  75. package/lib/components/Breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  76. package/lib/components/Button/Button.mdx +1 -1
  77. package/lib/components/Button/Button.stories.tsx +2 -2
  78. package/lib/components/Calendar/Calendar.stories.tsx +2 -2
  79. package/lib/components/Calendar/subcomponents/Day.stories.tsx +1 -1
  80. package/lib/components/Checkbox/Checkbox.stories.tsx +2 -2
  81. package/lib/components/Chip/Chip.stories.tsx +2 -2
  82. package/lib/components/Datepicker/Datepicker.stories.tsx +2 -2
  83. package/lib/components/Dialog/BaseDialog.tsx +180 -161
  84. package/lib/components/Dialog/Dialog.stories.tsx +1 -1
  85. package/lib/components/Dialog/Dialog.tsx +15 -11
  86. package/lib/components/Divider/Divider.stories.tsx +1 -1
  87. package/lib/components/Dropdown/Dropdown.stories.tsx +1 -1
  88. package/lib/components/FeedbackDialog/FeedbackDialog.stories.tsx +1 -1
  89. package/lib/components/Field/Field.stories.tsx +2 -2
  90. package/lib/components/FileInput/FileInput.stories.tsx +1 -1
  91. package/lib/components/Footer/Footer.stories.tsx +1 -1
  92. package/lib/components/Header/Header.mdx +1 -1
  93. package/lib/components/Header/Header.stories.tsx +1 -1
  94. package/lib/components/Heading/Documentation.mdx +1 -1
  95. package/lib/components/Heading/Heading.stories.tsx +1 -1
  96. package/lib/components/Icon/Icon.stories.tsx +1 -1
  97. package/lib/components/IconButton/IconButton.stories.tsx +1 -1
  98. package/lib/components/Input/Documentation.mdx +1 -1
  99. package/lib/components/Input/Input.stories.tsx +1 -1
  100. package/lib/components/Label/Label.stories.tsx +1 -1
  101. package/lib/components/Layout/Layout.stories.tsx +1 -1
  102. package/lib/components/Link/Link.stories.tsx +1 -1
  103. package/lib/components/Main/Main.stories.tsx +1 -1
  104. package/lib/components/Modal/Modal.stories.tsx +1 -1
  105. package/lib/components/NativeDatepicker/NativeDatepicker.stories.tsx +2 -2
  106. package/lib/components/Overlay/Overlay.stories.tsx +1 -1
  107. package/lib/components/Overlay/Overlay.tsx +1 -4
  108. package/lib/components/Pagination/Pagination.stories.tsx +1 -1
  109. package/lib/components/Paragraph/Paragraph.stories.tsx +1 -1
  110. package/lib/components/Radio/Radio.stories.tsx +2 -2
  111. package/lib/components/Search/Search.stories.tsx +1 -1
  112. package/lib/components/Select/Select.mdx +1 -1
  113. package/lib/components/Select/Select.stories.tsx +2 -2
  114. package/lib/components/Select/Select.types.ts +1 -2
  115. package/lib/components/Snackbar/Snackbar.stories.tsx +1 -1
  116. package/lib/components/Spinner/Spinner.stories.tsx +1 -1
  117. package/lib/components/StandaloneLink/StandaloneLink.stories.tsx +1 -1
  118. package/lib/components/Table/Table.stories.tsx +1 -1
  119. package/lib/components/Table/subcomponents/Cell/Cell.stories.tsx +2 -2
  120. package/lib/components/Table/subcomponents/HeadCell/HeadCell.stories.tsx +1 -1
  121. package/lib/components/Tabs/Tab.tsx +209 -36
  122. package/lib/components/Tabs/TabContext.tsx +20 -7
  123. package/lib/components/Tabs/Tabs.stories.tsx +87 -68
  124. package/lib/components/Tabs/Tabs.tsx +129 -37
  125. package/lib/components/Tabs/TabsList.tsx +134 -0
  126. package/lib/components/Tabs/TabsPanel.tsx +55 -0
  127. package/lib/components/Tabs/__tests__/Tabs.test.tsx +173 -105
  128. package/lib/components/Tabs/index.ts +8 -1
  129. package/lib/components/Textarea/Textarea.stories.tsx +1 -1
  130. package/lib/components/Timepicker/Timepicker.stories.tsx +1 -1
  131. package/lib/components/Toggle/Documentation.mdx +1 -1
  132. package/lib/components/Toggle/Toggle.stories.tsx +1 -1
  133. package/lib/components/Tooltip/Tooltip.stories.tsx +1 -1
  134. package/lib/components/UclLogo/UclLogo.stories.tsx +1 -1
  135. package/lib/components/WeekPicker/WeekPicker.stories.tsx +2 -2
  136. package/lib/components/common/Common.mdx +1 -1
  137. package/lib/components/index.ts +7 -1
  138. package/lib/theme/Icons.mdx +1 -1
  139. package/lib/theme/Typography.mdx +1 -1
  140. package/lib/utils/__tests__/announce.test.ts +53 -0
  141. package/lib/utils/announce.ts +33 -10
  142. package/package.json +8 -11
  143. package/lib/components/Tabs/__tests__/__snapshots__/Tabs.test.tsx.snap +0 -185
@@ -1,4 +1,5 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
3
  import Tabs from './Tabs';
3
4
 
4
5
  const meta = {
@@ -7,89 +8,107 @@ const meta = {
7
8
  parameters: {
8
9
  layout: 'padded',
9
10
  },
10
- args: {
11
- activeTab: '',
12
- },
13
11
  } satisfies Meta<typeof Tabs>;
14
12
 
15
13
  export default meta;
16
14
  type Story = StoryObj<typeof meta>;
17
15
 
18
- export const FullWidth: Story = {
19
- name: 'Full width',
16
+ export const Controlled: Story = {
17
+ render: () => {
18
+ const [value, setValue] = useState('overview');
19
+
20
+ return (
21
+ <Tabs
22
+ value={value}
23
+ onValueChange={setValue}
24
+ >
25
+ <Tabs.List fullWidth>
26
+ <Tabs.Tab value='overview'>Overview</Tabs.Tab>
27
+ <Tabs.Tab value='modules'>Modules</Tabs.Tab>
28
+ <Tabs.Tab value='people'>People</Tabs.Tab>
29
+ <Tabs.Tab value='settings'>Settings</Tabs.Tab>
30
+ </Tabs.List>
31
+ <Tabs.Panel value='overview'>Overview content</Tabs.Panel>
32
+ <Tabs.Panel value='modules'>Modules content</Tabs.Panel>
33
+ <Tabs.Panel value='people'>People content</Tabs.Panel>
34
+ <Tabs.Panel value='settings'>Settings content</Tabs.Panel>
35
+ </Tabs>
36
+ );
37
+ },
38
+ };
39
+
40
+ export const AutoWidth: Story = {
41
+ name: 'Auto width',
20
42
  render: () => (
21
- <Tabs activeTab='1'>
22
- <Tabs.Tab
23
- label='Tab Item 1'
24
- value='1'
25
- />
26
- <Tabs.Tab
27
- label='Tab Item 2'
28
- value='2'
29
- />
30
- <Tabs.Tab
31
- label='Tab Item 3'
32
- value='3'
33
- />
34
- <Tabs.Tab
35
- label='Tab Item 4'
36
- value='4'
37
- />
43
+ <Tabs defaultValue='overview'>
44
+ <Tabs.List>
45
+ <Tabs.Tab value='overview'>Overview</Tabs.Tab>
46
+ <Tabs.Tab value='modules'>Modules</Tabs.Tab>
47
+ <Tabs.Tab value='people'>People</Tabs.Tab>
48
+ <Tabs.Tab value='long-label'>
49
+ A longer tab item showing auto width
50
+ </Tabs.Tab>
51
+ </Tabs.List>
38
52
  </Tabs>
39
53
  ),
40
54
  };
41
55
 
42
- export const AutoWidth: Story = {
43
- name: 'Auto width',
56
+ export const Count: Story = {
44
57
  render: () => (
45
- <Tabs
46
- activeTab='1'
47
- fullWidth={false}
48
- >
49
- <Tabs.Tab
50
- label='Tab Item 1'
51
- value='1'
52
- />
53
- <Tabs.Tab
54
- label='Tab Item 2'
55
- value='2'
56
- />
57
- <Tabs.Tab
58
- label='Tab Item 3'
59
- value='3'
60
- />
61
- <Tabs.Tab
62
- label='A longer tab item showing auto width'
63
- value='4'
64
- />
58
+ <Tabs defaultValue='overview'>
59
+ <Tabs.List fullWidth>
60
+ <Tabs.Tab
61
+ count={12}
62
+ value='overview'
63
+ >
64
+ Overview
65
+ </Tabs.Tab>
66
+ <Tabs.Tab
67
+ count={6}
68
+ value='modules'
69
+ >
70
+ Modules
71
+ </Tabs.Tab>
72
+ <Tabs.Tab
73
+ count={9}
74
+ value='people'
75
+ >
76
+ People
77
+ </Tabs.Tab>
78
+ <Tabs.Tab
79
+ count={0}
80
+ value='settings'
81
+ >
82
+ Settings
83
+ </Tabs.Tab>
84
+ </Tabs.List>
65
85
  </Tabs>
66
86
  ),
67
87
  };
68
88
 
69
- export const Counter: Story = {
70
- name: 'Counter',
89
+ export const Links: Story = {
71
90
  render: () => (
72
- <Tabs activeTab='1'>
73
- <Tabs.Tab
74
- counter={12}
75
- label='Tab Item 1'
76
- value='1'
77
- />
78
- <Tabs.Tab
79
- counter={6}
80
- label='Tab Item 2'
81
- value='2'
82
- />
83
- <Tabs.Tab
84
- counter={9}
85
- label='Tab Item 3'
86
- value='3'
87
- />
88
- <Tabs.Tab
89
- counter={0}
90
- label='Tab Item 4'
91
- value='4'
92
- />
91
+ <Tabs value='overview'>
92
+ <Tabs.List>
93
+ <Tabs.Tab
94
+ value='overview'
95
+ asChild
96
+ >
97
+ <a href='#overview'>Overview</a>
98
+ </Tabs.Tab>
99
+ <Tabs.Tab
100
+ value='modules'
101
+ asChild
102
+ >
103
+ <a href='#modules'>Modules</a>
104
+ </Tabs.Tab>
105
+ <Tabs.Tab
106
+ value='people'
107
+ asChild
108
+ >
109
+ <a href='#people'>People</a>
110
+ </Tabs.Tab>
111
+ </Tabs.List>
93
112
  </Tabs>
94
113
  ),
95
114
  };
@@ -1,34 +1,91 @@
1
- import { memo, HTMLAttributes } from 'react';
2
- import { css, cx } from '@emotion/css';
1
+ import {
2
+ Children,
3
+ HTMLAttributes,
4
+ NamedExoticComponent,
5
+ ReactNode,
6
+ isValidElement,
7
+ memo,
8
+ useId,
9
+ useState,
10
+ } from 'react';
11
+ import { cx } from '@emotion/css';
3
12
  import { useTheme } from '../../theme';
4
- import TabContext from './TabContext';
5
- import Tab from './Tab';
13
+ import TabContext, { TabsActivationMode, TabsOrientation } from './TabContext';
14
+ import Tab, { TabProps } from './Tab';
15
+ import TabsList, { TabsListProps } from './TabsList';
16
+ import TabsPanel, { TabsPanelProps } from './TabsPanel';
6
17
  import marginsStyle, { MarginProps } from '../common/marginsStyle';
7
18
 
8
19
  export const NAME = 'ucl-uikit-tabs';
9
20
 
10
21
  export interface TabsBaseProps extends Omit<
11
22
  HTMLAttributes<HTMLDivElement>,
12
- 'onChange'
23
+ 'defaultValue' | 'onChange'
13
24
  > {
14
- activeTab: string;
25
+ value?: string;
26
+ defaultValue?: string;
27
+ onValueChange?: (value: string) => void;
28
+ activationMode?: TabsActivationMode;
29
+ orientation?: TabsOrientation;
30
+ testId?: string;
31
+ ref?: React.Ref<HTMLDivElement>;
32
+ /** @deprecated Use value instead. */
33
+ activeTab?: string;
34
+ /** @deprecated Use onValueChange instead. */
15
35
  onChange?: (value: string) => void;
36
+ /** @deprecated Prefer setting fullWidth on Tabs.List. */
16
37
  fullWidth?: boolean;
17
- testId?: string;
38
+ /** @deprecated Tabs now uses theme.colour.border.brand by default. */
18
39
  color?: string;
19
- ref?: React.RefObject<HTMLDivElement>;
20
40
  }
21
41
 
22
42
  export type TabsProps = TabsBaseProps & MarginProps;
23
43
 
24
- type TabsComponent = React.FC<TabsProps> & {
44
+ export interface TabsComponent extends NamedExoticComponent<TabsProps> {
45
+ List: typeof TabsList;
25
46
  Tab: typeof Tab;
47
+ Panel: typeof TabsPanel;
48
+ /** @deprecated Use Tabs.Tab instead. */
49
+ Trigger: typeof Tab;
50
+ }
51
+
52
+ const hasTabsList = (children: React.ReactNode) =>
53
+ Children.toArray(children).some(
54
+ (child) => isValidElement(child) && child.type === TabsList
55
+ );
56
+
57
+ const getPanelValues = (children: ReactNode): Set<string> => {
58
+ const values = new Set<string>();
59
+
60
+ Children.toArray(children).forEach((child) => {
61
+ if (!isValidElement<{ value?: string; children?: ReactNode }>(child)) {
62
+ return;
63
+ }
64
+
65
+ if (child.type === TabsPanel && child.props.value) {
66
+ values.add(child.props.value);
67
+ return;
68
+ }
69
+
70
+ if (child.props.children) {
71
+ getPanelValues(child.props.children).forEach((panelValue) => {
72
+ values.add(panelValue);
73
+ });
74
+ }
75
+ });
76
+
77
+ return values;
26
78
  };
27
79
 
28
80
  const TabsBase = ({
81
+ value,
82
+ defaultValue,
83
+ onValueChange,
84
+ activationMode = 'automatic',
85
+ orientation = 'horizontal',
29
86
  activeTab,
30
87
  onChange,
31
- fullWidth = true,
88
+ fullWidth,
32
89
  color,
33
90
  testId = NAME,
34
91
  className,
@@ -37,42 +94,77 @@ const TabsBase = ({
37
94
  ...props
38
95
  }: TabsProps) => {
39
96
  const [theme] = useTheme();
97
+ void color;
98
+ const generatedId = useId();
99
+ const [uncontrolledValue, setUncontrolledValue] = useState(
100
+ defaultValue ?? activeTab
101
+ );
102
+ const containsList = hasTabsList(children);
103
+ const panelValues = getPanelValues(children);
104
+ const isControlled = value !== undefined || activeTab !== undefined;
105
+ const selectedValue = value ?? activeTab ?? uncontrolledValue;
106
+ const shouldFill = fullWidth ?? !containsList;
107
+
108
+ const handleValueChange = (nextValue: string) => {
109
+ if (!isControlled) {
110
+ setUncontrolledValue(nextValue);
111
+ }
40
112
 
41
- const handleTabChange = (value: string) => {
42
- if (onChange) onChange(value);
113
+ onValueChange?.(nextValue);
114
+ onChange?.(nextValue);
43
115
  };
44
116
 
45
- const baseStyle = css`
46
- display: flex;
47
- border-bottom: 1px solid ${theme.colour.border.subtle};
48
- `;
117
+ const style = cx(NAME, marginsStyle(props, theme), className);
49
118
 
50
- const style = cx(NAME, baseStyle, marginsStyle(props, theme), className);
119
+ const contextValue = {
120
+ value: selectedValue,
121
+ setValue: handleValueChange,
122
+ baseId: `ucl-uikit-tabs-${generatedId}`,
123
+ activationMode,
124
+ orientation,
125
+ fullWidth: shouldFill,
126
+ panelValues,
127
+ };
51
128
 
52
129
  return (
53
- <TabContext.Provider
54
- value={{
55
- activeTab,
56
- setActiveTab: handleTabChange,
57
- onChange,
58
- fullWidth,
59
- color,
60
- }}
61
- >
62
- <div
63
- ref={ref}
64
- className={style}
65
- data-testid={testId}
66
- role='tablist'
67
- {...props}
68
- >
69
- {children}
70
- </div>
130
+ <TabContext.Provider value={contextValue}>
131
+ {containsList ? (
132
+ <div
133
+ ref={ref}
134
+ className={style}
135
+ data-testid={testId}
136
+ {...props}
137
+ >
138
+ {children}
139
+ </div>
140
+ ) : (
141
+ <TabsList
142
+ ref={ref}
143
+ className={style}
144
+ data-testid={testId}
145
+ fullWidth={shouldFill}
146
+ {...props}
147
+ >
148
+ {children}
149
+ </TabsList>
150
+ )}
71
151
  </TabContext.Provider>
72
152
  );
73
153
  };
74
154
 
75
- const Tabs = memo(TabsBase) as unknown as TabsComponent;
76
- Tabs.Tab = Tab;
155
+ const Tabs = Object.assign(memo(TabsBase), {
156
+ List: TabsList,
157
+ Tab,
158
+ Panel: TabsPanel,
159
+ Trigger: Tab,
160
+ }) as TabsComponent;
161
+
162
+ export type {
163
+ TabProps,
164
+ TabsActivationMode,
165
+ TabsListProps,
166
+ TabsOrientation,
167
+ TabsPanelProps,
168
+ };
77
169
 
78
170
  export default Tabs;
@@ -0,0 +1,134 @@
1
+ import { HTMLAttributes, KeyboardEvent, memo } from 'react';
2
+ import { css, cx } from '@emotion/css';
3
+ import { useTheme } from '../../theme';
4
+ import { useTabContext } from './TabContext';
5
+
6
+ export const NAME = 'ucl-uikit-tabs__list';
7
+
8
+ export interface TabsListProps extends HTMLAttributes<HTMLDivElement> {
9
+ fullWidth?: boolean;
10
+ testId?: string;
11
+ ref?: React.Ref<HTMLDivElement>;
12
+ }
13
+
14
+ const getEnabledTabs = (list: HTMLElement) =>
15
+ Array.from(
16
+ list.querySelectorAll<HTMLElement>(
17
+ '[role="tab"]:not([aria-disabled="true"])'
18
+ )
19
+ );
20
+
21
+ const TabsList = ({
22
+ fullWidth,
23
+ testId = NAME,
24
+ className,
25
+ children,
26
+ onKeyDown,
27
+ ref,
28
+ ...props
29
+ }: TabsListProps) => {
30
+ const [theme] = useTheme();
31
+ const {
32
+ orientation,
33
+ activationMode,
34
+ fullWidth: contextFullWidth,
35
+ setValue,
36
+ } = useTabContext();
37
+ const shouldFill = fullWidth ?? contextFullWidth;
38
+
39
+ const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
40
+ onKeyDown?.(event);
41
+
42
+ if (event.defaultPrevented) {
43
+ return;
44
+ }
45
+
46
+ const keyMap =
47
+ orientation === 'vertical'
48
+ ? { next: 'ArrowDown', previous: 'ArrowUp' }
49
+ : { next: 'ArrowRight', previous: 'ArrowLeft' };
50
+
51
+ const isNavigationKey = [
52
+ keyMap.next,
53
+ keyMap.previous,
54
+ 'Home',
55
+ 'End',
56
+ ].includes(event.key);
57
+
58
+ if (!isNavigationKey) {
59
+ return;
60
+ }
61
+
62
+ const tabs = getEnabledTabs(event.currentTarget);
63
+ const currentIndex = tabs.indexOf(document.activeElement as HTMLElement);
64
+
65
+ if (currentIndex === -1) {
66
+ return;
67
+ }
68
+
69
+ event.preventDefault();
70
+
71
+ const nextIndex =
72
+ {
73
+ [keyMap.next]: (currentIndex + 1) % tabs.length,
74
+ [keyMap.previous]: (currentIndex - 1 + tabs.length) % tabs.length,
75
+ Home: 0,
76
+ End: tabs.length - 1,
77
+ }[event.key] ?? currentIndex;
78
+
79
+ const nextTab = tabs[nextIndex];
80
+ nextTab.focus();
81
+
82
+ if (activationMode === 'automatic') {
83
+ const nextValue = nextTab.dataset.uclTabsValue;
84
+
85
+ if (nextValue) {
86
+ setValue(nextValue);
87
+ }
88
+ }
89
+ };
90
+
91
+ const baseStyle = css`
92
+ display: flex;
93
+ align-items: stretch;
94
+ overflow-x: auto;
95
+ padding: 4px;
96
+ border-bottom: ${theme.border.b1} solid ${theme.colour.border.subtle};
97
+ background-color: transparent;
98
+ `;
99
+
100
+ const verticalStyle = css`
101
+ flex-direction: column;
102
+ overflow: visible;
103
+ border-bottom: 0;
104
+ border-right: ${theme.border.b1} solid ${theme.colour.border.subtle};
105
+ `;
106
+
107
+ const fullWidthStyle = css`
108
+ width: 100%;
109
+ `;
110
+
111
+ const style = cx(
112
+ NAME,
113
+ baseStyle,
114
+ orientation === 'vertical' && verticalStyle,
115
+ shouldFill && fullWidthStyle,
116
+ className
117
+ );
118
+
119
+ return (
120
+ <div
121
+ ref={ref}
122
+ className={style}
123
+ data-testid={testId}
124
+ role='tablist'
125
+ aria-orientation={orientation}
126
+ onKeyDown={handleKeyDown}
127
+ {...props}
128
+ >
129
+ {children}
130
+ </div>
131
+ );
132
+ };
133
+
134
+ export default memo(TabsList);
@@ -0,0 +1,55 @@
1
+ import { HTMLAttributes, memo } from 'react';
2
+ import { css, cx } from '@emotion/css';
3
+ import { useTheme } from '../../theme';
4
+ import { getTabId, getTabPanelId, useTabContext } from './TabContext';
5
+
6
+ export const NAME = 'ucl-uikit-tabs__panel';
7
+
8
+ export interface TabsPanelProps extends HTMLAttributes<HTMLDivElement> {
9
+ value: string;
10
+ forceMount?: boolean;
11
+ testId?: string;
12
+ ref?: React.Ref<HTMLDivElement>;
13
+ }
14
+
15
+ const TabsPanel = ({
16
+ value,
17
+ forceMount = false,
18
+ testId = NAME,
19
+ className,
20
+ children,
21
+ ref,
22
+ ...props
23
+ }: TabsPanelProps) => {
24
+ const [theme] = useTheme();
25
+ const { value: selectedValue, baseId } = useTabContext();
26
+ const isSelected = selectedValue === value;
27
+
28
+ if (!forceMount && !isSelected) {
29
+ return null;
30
+ }
31
+
32
+ const baseStyle = css`
33
+ color: ${theme.colour.text.default};
34
+ outline: none;
35
+ `;
36
+
37
+ const style = cx(NAME, baseStyle, className);
38
+
39
+ return (
40
+ <div
41
+ ref={ref}
42
+ className={style}
43
+ data-testid={testId}
44
+ role='tabpanel'
45
+ id={getTabPanelId(baseId, value)}
46
+ aria-labelledby={getTabId(baseId, value)}
47
+ hidden={!isSelected}
48
+ {...props}
49
+ >
50
+ {children}
51
+ </div>
52
+ );
53
+ };
54
+
55
+ export default memo(TabsPanel);