uikit-react-public 0.29.6 → 0.30.0

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 (136) 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/Dialog.stories.d.ts +1 -1
  17. package/dist/components/Divider/Divider.stories.d.ts +1 -1
  18. package/dist/components/Dropdown/Dropdown.stories.d.ts +1 -1
  19. package/dist/components/FeedbackDialog/FeedbackDialog.stories.d.ts +1 -1
  20. package/dist/components/Field/Field.stories.d.ts +1 -1
  21. package/dist/components/FileInput/FileInput.stories.d.ts +1 -1
  22. package/dist/components/Footer/Footer.stories.d.ts +1 -1
  23. package/dist/components/Header/Header.stories.d.ts +1 -1
  24. package/dist/components/Heading/Heading.stories.d.ts +1 -1
  25. package/dist/components/Icon/Icon.stories.d.ts +1 -1
  26. package/dist/components/IconButton/IconButton.stories.d.ts +1 -1
  27. package/dist/components/Input/Input.stories.d.ts +1 -1
  28. package/dist/components/Label/Label.stories.d.ts +1 -1
  29. package/dist/components/Layout/Layout.stories.d.ts +1 -1
  30. package/dist/components/Link/Link.stories.d.ts +1 -1
  31. package/dist/components/Main/Main.stories.d.ts +1 -1
  32. package/dist/components/Modal/Modal.stories.d.ts +1 -1
  33. package/dist/components/NativeDatepicker/NativeDatepicker.stories.d.ts +1 -1
  34. package/dist/components/Overlay/Overlay.stories.d.ts +2 -2
  35. package/dist/components/Pagination/Pagination.stories.d.ts +1 -1
  36. package/dist/components/Paragraph/Paragraph.stories.d.ts +1 -1
  37. package/dist/components/Radio/Radio.stories.d.ts +1 -1
  38. package/dist/components/Search/Search.stories.d.ts +1 -1
  39. package/dist/components/Select/Select.stories.d.ts +1 -1
  40. package/dist/components/Snackbar/Snackbar.stories.d.ts +1 -1
  41. package/dist/components/Spinner/Spinner.stories.d.ts +1 -1
  42. package/dist/components/StandaloneLink/StandaloneLink.stories.d.ts +1 -1
  43. package/dist/components/Table/Table.stories.d.ts +1 -1
  44. package/dist/components/Table/subcomponents/Cell/Cell.stories.d.ts +2 -2
  45. package/dist/components/Table/subcomponents/HeadCell/HeadCell.stories.d.ts +2 -2
  46. package/dist/components/Tabs/Tab.d.ts +11 -5
  47. package/dist/components/Tabs/TabContext.d.ts +14 -8
  48. package/dist/components/Tabs/Tabs.d.ts +25 -8
  49. package/dist/components/Tabs/Tabs.stories.d.ts +5 -9
  50. package/dist/components/Tabs/TabsList.d.ts +9 -0
  51. package/dist/components/Tabs/TabsPanel.d.ts +10 -0
  52. package/dist/components/Tabs/index.d.ts +2 -1
  53. package/dist/components/Textarea/Textarea.stories.d.ts +1 -1
  54. package/dist/components/Timepicker/Timepicker.stories.d.ts +1 -1
  55. package/dist/components/Toggle/Toggle.stories.d.ts +1 -1
  56. package/dist/components/Tooltip/Tooltip.stories.d.ts +1 -1
  57. package/dist/components/UclLogo/UclLogo.stories.d.ts +1 -1
  58. package/dist/components/WeekPicker/WeekPicker.stories.d.ts +1 -1
  59. package/dist/components/index.d.ts +1 -1
  60. package/dist/index.js +4143 -3890
  61. package/lib/Welcome.mdx +1 -1
  62. package/lib/components/Accordion/Accordion.stories.tsx +1 -1
  63. package/lib/components/Alert/Alert.mdx +1 -1
  64. package/lib/components/Alert/Alert.stories.tsx +1 -1
  65. package/lib/components/AppHeader/AppHeader.stories.tsx +1 -1
  66. package/lib/components/AppMenu/AppMenu.stories.tsx +1 -1
  67. package/lib/components/Avatar/Avatar.mdx +1 -1
  68. package/lib/components/Avatar/Avatar.stories.tsx +1 -1
  69. package/lib/components/Badge/Badge.stories.tsx +1 -1
  70. package/lib/components/BaseCheckbox/BaseCheckbox.stories.tsx +2 -2
  71. package/lib/components/Blanket/Blanket.stories.tsx +1 -1
  72. package/lib/components/Breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  73. package/lib/components/Button/Button.mdx +1 -1
  74. package/lib/components/Button/Button.stories.tsx +2 -2
  75. package/lib/components/Calendar/Calendar.stories.tsx +2 -2
  76. package/lib/components/Calendar/subcomponents/Day.stories.tsx +1 -1
  77. package/lib/components/Checkbox/Checkbox.stories.tsx +2 -2
  78. package/lib/components/Chip/Chip.stories.tsx +2 -2
  79. package/lib/components/Datepicker/Datepicker.stories.tsx +2 -2
  80. package/lib/components/Dialog/Dialog.stories.tsx +1 -1
  81. package/lib/components/Divider/Divider.stories.tsx +1 -1
  82. package/lib/components/Dropdown/Dropdown.stories.tsx +1 -1
  83. package/lib/components/FeedbackDialog/FeedbackDialog.stories.tsx +1 -1
  84. package/lib/components/Field/Field.stories.tsx +2 -2
  85. package/lib/components/FileInput/FileInput.stories.tsx +1 -1
  86. package/lib/components/Footer/Footer.stories.tsx +1 -1
  87. package/lib/components/Header/Header.mdx +1 -1
  88. package/lib/components/Header/Header.stories.tsx +1 -1
  89. package/lib/components/Heading/Documentation.mdx +1 -1
  90. package/lib/components/Heading/Heading.stories.tsx +1 -1
  91. package/lib/components/Icon/Icon.stories.tsx +1 -1
  92. package/lib/components/IconButton/IconButton.stories.tsx +1 -1
  93. package/lib/components/Input/Documentation.mdx +1 -1
  94. package/lib/components/Input/Input.stories.tsx +1 -1
  95. package/lib/components/Label/Label.stories.tsx +1 -1
  96. package/lib/components/Layout/Layout.stories.tsx +1 -1
  97. package/lib/components/Link/Link.stories.tsx +1 -1
  98. package/lib/components/Main/Main.stories.tsx +1 -1
  99. package/lib/components/Modal/Modal.stories.tsx +1 -1
  100. package/lib/components/NativeDatepicker/NativeDatepicker.stories.tsx +2 -2
  101. package/lib/components/Overlay/Overlay.stories.tsx +1 -1
  102. package/lib/components/Overlay/Overlay.tsx +1 -4
  103. package/lib/components/Pagination/Pagination.stories.tsx +1 -1
  104. package/lib/components/Paragraph/Paragraph.stories.tsx +1 -1
  105. package/lib/components/Radio/Radio.stories.tsx +2 -2
  106. package/lib/components/Search/Search.stories.tsx +1 -1
  107. package/lib/components/Select/Select.mdx +1 -1
  108. package/lib/components/Select/Select.stories.tsx +2 -2
  109. package/lib/components/Select/Select.types.ts +1 -2
  110. package/lib/components/Snackbar/Snackbar.stories.tsx +1 -1
  111. package/lib/components/Spinner/Spinner.stories.tsx +1 -1
  112. package/lib/components/StandaloneLink/StandaloneLink.stories.tsx +1 -1
  113. package/lib/components/Table/Table.stories.tsx +1 -1
  114. package/lib/components/Table/subcomponents/Cell/Cell.stories.tsx +2 -2
  115. package/lib/components/Table/subcomponents/HeadCell/HeadCell.stories.tsx +1 -1
  116. package/lib/components/Tabs/Tab.tsx +209 -36
  117. package/lib/components/Tabs/TabContext.tsx +20 -7
  118. package/lib/components/Tabs/Tabs.stories.tsx +87 -68
  119. package/lib/components/Tabs/Tabs.tsx +129 -37
  120. package/lib/components/Tabs/TabsList.tsx +134 -0
  121. package/lib/components/Tabs/TabsPanel.tsx +55 -0
  122. package/lib/components/Tabs/__tests__/Tabs.test.tsx +173 -105
  123. package/lib/components/Tabs/index.ts +8 -1
  124. package/lib/components/Textarea/Textarea.stories.tsx +1 -1
  125. package/lib/components/Timepicker/Timepicker.stories.tsx +1 -1
  126. package/lib/components/Toggle/Documentation.mdx +1 -1
  127. package/lib/components/Toggle/Toggle.stories.tsx +1 -1
  128. package/lib/components/Tooltip/Tooltip.stories.tsx +1 -1
  129. package/lib/components/UclLogo/UclLogo.stories.tsx +1 -1
  130. package/lib/components/WeekPicker/WeekPicker.stories.tsx +2 -2
  131. package/lib/components/common/Common.mdx +1 -1
  132. package/lib/components/index.ts +7 -1
  133. package/lib/theme/Icons.mdx +1 -1
  134. package/lib/theme/Typography.mdx +1 -1
  135. package/package.json +8 -11
  136. package/lib/components/Tabs/__tests__/__snapshots__/Tabs.test.tsx.snap +0 -185
@@ -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);
@@ -1,122 +1,190 @@
1
- import { describe, expect, test } from 'vitest';
2
- import { render } from '@testing-library/react';
1
+ import { describe, expect, test, vi } from 'vitest';
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
3
  import Tabs from '../Tabs';
4
4
  import { ThemeContextProvider } from '../../../theme/useTheme';
5
5
 
6
+ const wrap = (ui: React.ReactElement) =>
7
+ render(<ThemeContextProvider>{ui}</ThemeContextProvider>);
8
+
6
9
  describe('Tabs', () => {
7
- // Snapshot tests
10
+ test('renders a tablist with selectable button tabs', () => {
11
+ const onValueChange = vi.fn();
8
12
 
9
- test('snapshot: Minimal props', () => {
10
- const renderResult = render(
11
- <ThemeContextProvider>
12
- <Tabs activeTab='1'>
13
- <Tabs.Tab
14
- label='Tab Item 1'
15
- value='1'
16
- />
17
- <Tabs.Tab
18
- label='Tab Item 2'
19
- value='2'
20
- />
21
- <Tabs.Tab
22
- label='Tab Item 3'
23
- value='3'
24
- />
25
- <Tabs.Tab
26
- label='Tab Item 4'
27
- value='4'
28
- />
29
- </Tabs>
30
- </ThemeContextProvider>
13
+ wrap(
14
+ <Tabs
15
+ defaultValue='overview'
16
+ onValueChange={onValueChange}
17
+ >
18
+ <Tabs.List aria-label='Course sections'>
19
+ <Tabs.Tab value='overview'>Overview</Tabs.Tab>
20
+ <Tabs.Tab value='modules'>Modules</Tabs.Tab>
21
+ </Tabs.List>
22
+ </Tabs>
23
+ );
24
+
25
+ expect(
26
+ screen.getByRole('tablist', { name: 'Course sections' })
27
+ ).toBeInTheDocument();
28
+ expect(screen.getByRole('tab', { name: 'Overview' })).toHaveAttribute(
29
+ 'aria-selected',
30
+ 'true'
31
+ );
32
+ expect(screen.getByRole('tab', { name: 'Overview' })).not.toHaveAttribute(
33
+ 'aria-controls'
34
+ );
35
+
36
+ fireEvent.click(screen.getByRole('tab', { name: 'Modules' }));
37
+
38
+ expect(onValueChange).toHaveBeenCalledWith('modules');
39
+ expect(screen.getByRole('tab', { name: 'Modules' })).toHaveAttribute(
40
+ 'aria-selected',
41
+ 'true'
31
42
  );
32
- expect(renderResult.container.firstChild).toMatchSnapshot();
33
43
  });
34
44
 
35
- test('snapshot: Custom test id', () => {
36
- const renderResult = render(
37
- <ThemeContextProvider>
38
- <Tabs
39
- activeTab='1'
40
- data-testid='custom-test-id'
41
- >
42
- <Tabs.Tab
43
- label='Tab Item 1'
44
- value='1'
45
- />
46
- <Tabs.Tab
47
- label='Tab Item 2'
48
- value='2'
49
- />
50
- <Tabs.Tab
51
- label='Tab Item 3'
52
- value='3'
53
- />
54
- <Tabs.Tab
55
- label='Tab Item 4'
56
- value='4'
57
- />
58
- </Tabs>
59
- </ThemeContextProvider>
45
+ test('supports panels linked to their tabs', () => {
46
+ wrap(
47
+ <Tabs defaultValue='overview'>
48
+ <Tabs.List>
49
+ <Tabs.Tab value='overview'>Overview</Tabs.Tab>
50
+ <Tabs.Tab value='modules'>Modules</Tabs.Tab>
51
+ </Tabs.List>
52
+ <Tabs.Panel value='overview'>Overview content</Tabs.Panel>
53
+ <Tabs.Panel value='modules'>Modules content</Tabs.Panel>
54
+ </Tabs>
55
+ );
56
+
57
+ const overviewTab = screen.getByRole('tab', { name: 'Overview' });
58
+ const overviewPanel = screen.getByRole('tabpanel', {
59
+ name: 'Overview',
60
+ });
61
+
62
+ expect(overviewTab).toHaveAttribute(
63
+ 'aria-controls',
64
+ overviewPanel.getAttribute('id')
60
65
  );
61
- expect(renderResult.container.firstChild).toMatchSnapshot();
66
+ expect(screen.queryByText('Modules content')).not.toBeInTheDocument();
62
67
  });
63
68
 
64
- test('snapshot: Auto width', () => {
65
- const renderResult = render(
66
- <ThemeContextProvider>
67
- <Tabs
68
- activeTab='1'
69
- fullWidth={false}
70
- >
71
- <Tabs.Tab
72
- label='Tab Item 1'
73
- value='1'
74
- />
75
- <Tabs.Tab
76
- label='Tab Item 2'
77
- value='2'
78
- />
79
- <Tabs.Tab
80
- label='Tab Item 3'
81
- value='3'
82
- />
83
- <Tabs.Tab
84
- label='A longer tab item showing auto width'
85
- value='4'
86
- />
87
- </Tabs>
88
- </ThemeContextProvider>
69
+ test('supports asChild link tabs', () => {
70
+ wrap(
71
+ <Tabs value='overview'>
72
+ <Tabs.List>
73
+ <Tabs.Tab
74
+ value='overview'
75
+ asChild
76
+ >
77
+ <a href='/overview'>Overview</a>
78
+ </Tabs.Tab>
79
+ <Tabs.Tab
80
+ value='modules'
81
+ asChild
82
+ >
83
+ <a href='/modules'>Modules</a>
84
+ </Tabs.Tab>
85
+ </Tabs.List>
86
+ </Tabs>
89
87
  );
90
- expect(renderResult.container.firstChild).toMatchSnapshot();
88
+
89
+ const overview = screen.getByRole('tab', { name: 'Overview' });
90
+
91
+ expect(overview.tagName).toBe('A');
92
+ expect(overview).toHaveAttribute('href', '/overview');
93
+ expect(overview).toHaveAttribute('aria-selected', 'true');
91
94
  });
92
95
 
93
- test('snapshot: Counter', () => {
94
- const renderResult = render(
95
- <ThemeContextProvider>
96
- <Tabs activeTab='1'>
97
- <Tabs.Tab
98
- counter={12}
99
- label='Tab Item 1'
100
- value='1'
101
- />
102
- <Tabs.Tab
103
- counter={6}
104
- label='Tab Item 2'
105
- value='2'
106
- />
107
- <Tabs.Tab
108
- counter={9}
109
- label='Tab Item 3'
110
- value='3'
111
- />
112
- <Tabs.Tab
113
- counter={0}
114
- label='Tab Item 4'
115
- value='4'
116
- />
117
- </Tabs>
118
- </ThemeContextProvider>
96
+ test('renders counts as plain tab text', () => {
97
+ wrap(
98
+ <Tabs defaultValue='overview'>
99
+ <Tabs.List>
100
+ <Tabs.Tab
101
+ count={12}
102
+ value='overview'
103
+ >
104
+ Overview
105
+ </Tabs.Tab>
106
+ </Tabs.List>
107
+ </Tabs>
108
+ );
109
+
110
+ expect(screen.getByRole('tab', { name: 'Overview 12' })).toHaveTextContent(
111
+ 'Overview 12'
112
+ );
113
+ expect(screen.queryByRole('tab', { name: 'Overview (12)' })).toBe(null);
114
+ });
115
+
116
+ test('moves focus with arrow keys and activates automatically', () => {
117
+ wrap(
118
+ <Tabs defaultValue='overview'>
119
+ <Tabs.List>
120
+ <Tabs.Tab value='overview'>Overview</Tabs.Tab>
121
+ <Tabs.Tab value='modules'>Modules</Tabs.Tab>
122
+ <Tabs.Tab value='people'>People</Tabs.Tab>
123
+ </Tabs.List>
124
+ </Tabs>
125
+ );
126
+
127
+ const overview = screen.getByRole('tab', { name: 'Overview' });
128
+ const modules = screen.getByRole('tab', { name: 'Modules' });
129
+
130
+ overview.focus();
131
+ fireEvent.keyDown(screen.getByRole('tablist'), { key: 'ArrowRight' });
132
+
133
+ expect(modules).toHaveFocus();
134
+ expect(modules).toHaveAttribute('aria-selected', 'true');
135
+ });
136
+
137
+ test('keeps deprecated activeTab/onChange/label props working', () => {
138
+ const onChange = vi.fn();
139
+
140
+ wrap(
141
+ <Tabs
142
+ activeTab='overview'
143
+ onChange={onChange}
144
+ >
145
+ <Tabs.Tab
146
+ label='Overview'
147
+ value='overview'
148
+ />
149
+ <Tabs.Tab
150
+ label='Modules'
151
+ value='modules'
152
+ />
153
+ </Tabs>
154
+ );
155
+
156
+ fireEvent.click(screen.getByRole('tab', { name: 'Modules' }));
157
+
158
+ expect(screen.getByRole('tablist')).toHaveClass('ucl-uikit-tabs__list');
159
+ expect(onChange).toHaveBeenCalledWith('modules');
160
+ });
161
+
162
+ test('does not activate disabled tabs', () => {
163
+ const onValueChange = vi.fn();
164
+
165
+ wrap(
166
+ <Tabs
167
+ defaultValue='overview'
168
+ onValueChange={onValueChange}
169
+ >
170
+ <Tabs.List>
171
+ <Tabs.Tab value='overview'>Overview</Tabs.Tab>
172
+ <Tabs.Tab
173
+ value='modules'
174
+ disabled
175
+ >
176
+ Modules
177
+ </Tabs.Tab>
178
+ </Tabs.List>
179
+ </Tabs>
180
+ );
181
+
182
+ fireEvent.click(screen.getByRole('tab', { name: 'Modules' }));
183
+
184
+ expect(onValueChange).not.toHaveBeenCalled();
185
+ expect(screen.getByRole('tab', { name: 'Overview' })).toHaveAttribute(
186
+ 'aria-selected',
187
+ 'true'
119
188
  );
120
- expect(renderResult.container.firstChild).toMatchSnapshot();
121
189
  });
122
190
  });