uikit-react-public 0.50.5 → 0.50.7
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.
- package/dist/components/ToggleGroup/ToggleGroup.d.ts +5 -3
- package/dist/components/ToggleGroup/ToggleGroup.stories.d.ts +7 -2
- package/dist/components/ToggleGroup/ToggleGroupContext.d.ts +2 -0
- package/dist/components/ToggleGroup/index.d.ts +1 -2
- package/dist/components/ToggleGroup/subcomponents/ToggleGroupItem.d.ts +13 -0
- package/dist/components/ToggleGroup/subcomponents/{Item.stories.d.ts → ToggleGroupItem.stories.d.ts} +2 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/index.js +5138 -5050
- package/lib/components/Accordion/Accordion.Heading.tsx +1 -1
- package/lib/components/Accordion/Accordion.Panel.tsx +1 -1
- package/lib/components/Accordion/__tests__/Accordion.test.tsx +21 -0
- package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +2 -2
- package/lib/components/Badge/Badge.tsx +3 -1
- package/lib/components/Badge/__tests__/Badge.test.tsx +10 -2
- package/lib/components/Badge/__tests__/__snapshots__/Badge.test.tsx.snap +9 -9
- package/lib/components/Calendar/__tests__/__snapshots__/Calendar.test.tsx.snap +4 -4
- package/lib/components/Callout/Callout.tsx +1 -1
- package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +6 -6
- package/lib/components/Datepicker/subcomponents/DatepickerInput.tsx +1 -0
- package/lib/components/Datepicker/subcomponents/VisibleField.tsx +2 -1
- package/lib/components/NativeDatepicker/NativeDatepicker.tsx +1 -0
- package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +1 -1
- package/lib/components/Select/subcomponents/CustomSelect.tsx +1 -0
- package/lib/components/Select/subcomponents/NativeSelect.tsx +1 -0
- package/lib/components/ToggleGroup/ToggleGroup.stories.tsx +3 -0
- package/lib/components/ToggleGroup/ToggleGroup.tsx +51 -10
- package/lib/components/ToggleGroup/ToggleGroupContext.tsx +2 -0
- package/lib/components/ToggleGroup/__tests__/ToggleGroup.test.tsx +105 -6
- package/lib/components/ToggleGroup/index.ts +1 -2
- package/lib/components/ToggleGroup/subcomponents/{Item.stories.tsx → ToggleGroupItem.stories.tsx} +6 -4
- package/lib/components/ToggleGroup/subcomponents/{Item.tsx → ToggleGroupItem.tsx} +126 -52
- package/lib/components/ToggleGroup/subcomponents/__tests__/{Item.test.tsx → ToggleGroupItem.test.tsx} +50 -6
- package/lib/components/index.ts +1 -1
- package/package.json +1 -1
- package/dist/components/ToggleGroup/subcomponents/Item.d.ts +0 -11
- /package/dist/components/ToggleGroup/subcomponents/__tests__/{Item.test.d.ts → ToggleGroupItem.test.d.ts} +0 -0
|
@@ -2,9 +2,10 @@ import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
|
2
2
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
4
|
import ToggleGroup from '../ToggleGroup';
|
|
5
|
-
import ToggleGroupItem from '../subcomponents/Item';
|
|
6
5
|
import { ThemeContextProvider } from '../../../theme/useTheme';
|
|
7
6
|
|
|
7
|
+
const ToggleGroupItem = ToggleGroup.Item;
|
|
8
|
+
|
|
8
9
|
const mockMatchMedia = (matchesDesktop: boolean) => {
|
|
9
10
|
globalThis.matchMedia = vi.fn().mockImplementation((query) => ({
|
|
10
11
|
matches: query.includes('min-width') ? matchesDesktop : false,
|
|
@@ -44,7 +45,10 @@ describe('ToggleGroup', () => {
|
|
|
44
45
|
</ToggleGroup>
|
|
45
46
|
);
|
|
46
47
|
|
|
47
|
-
expect(screen.getByRole('radiogroup')).
|
|
48
|
+
expect(screen.getByRole('radiogroup')).toHaveClass(
|
|
49
|
+
'ucl-uikit-toggle-group'
|
|
50
|
+
);
|
|
51
|
+
expect(screen.getByTestId('ucl-uikit-toggle-group')).toBeInTheDocument();
|
|
48
52
|
expect(screen.getByRole('radio', { name: 'Option 1' })).toHaveAttribute(
|
|
49
53
|
'aria-checked',
|
|
50
54
|
'true'
|
|
@@ -187,6 +191,90 @@ describe('ToggleGroup', () => {
|
|
|
187
191
|
expect(option3).toHaveAttribute('aria-checked', 'true');
|
|
188
192
|
});
|
|
189
193
|
|
|
194
|
+
test('moves focus and selection with vertical arrow keys', () => {
|
|
195
|
+
wrap(
|
|
196
|
+
<ToggleGroup defaultValue='option-1'>
|
|
197
|
+
<ToggleGroupItem
|
|
198
|
+
value='option-1'
|
|
199
|
+
label='Option 1'
|
|
200
|
+
/>
|
|
201
|
+
<ToggleGroupItem
|
|
202
|
+
value='option-2'
|
|
203
|
+
label='Option 2'
|
|
204
|
+
/>
|
|
205
|
+
</ToggleGroup>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const option1 = screen.getByRole('radio', { name: 'Option 1' });
|
|
209
|
+
const option2 = screen.getByRole('radio', { name: 'Option 2' });
|
|
210
|
+
|
|
211
|
+
option1.focus();
|
|
212
|
+
fireEvent.keyDown(screen.getByRole('radiogroup'), { key: 'ArrowDown' });
|
|
213
|
+
|
|
214
|
+
expect(option2).toHaveFocus();
|
|
215
|
+
expect(option2).toHaveAttribute('aria-checked', 'true');
|
|
216
|
+
|
|
217
|
+
fireEvent.keyDown(screen.getByRole('radiogroup'), { key: 'ArrowUp' });
|
|
218
|
+
|
|
219
|
+
expect(option1).toHaveFocus();
|
|
220
|
+
expect(option1).toHaveAttribute('aria-checked', 'true');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('makes the first enabled item tabbable when no item is selected', () => {
|
|
224
|
+
wrap(
|
|
225
|
+
<ToggleGroup>
|
|
226
|
+
<ToggleGroupItem
|
|
227
|
+
value='option-1'
|
|
228
|
+
label='Option 1'
|
|
229
|
+
disabled
|
|
230
|
+
/>
|
|
231
|
+
<ToggleGroupItem
|
|
232
|
+
value='option-2'
|
|
233
|
+
label='Option 2'
|
|
234
|
+
/>
|
|
235
|
+
</ToggleGroup>
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(screen.getByRole('radio', { name: 'Option 1' })).toHaveAttribute(
|
|
239
|
+
'tabindex',
|
|
240
|
+
'-1'
|
|
241
|
+
);
|
|
242
|
+
expect(screen.getByRole('radio', { name: 'Option 2' })).toHaveAttribute(
|
|
243
|
+
'tabindex',
|
|
244
|
+
'0'
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('forwards HTML attributes and composes onKeyDown', () => {
|
|
249
|
+
const onKeyDown = vi.fn();
|
|
250
|
+
|
|
251
|
+
wrap(
|
|
252
|
+
<ToggleGroup
|
|
253
|
+
aria-label='View'
|
|
254
|
+
className='custom-toggle-group'
|
|
255
|
+
defaultValue='option-1'
|
|
256
|
+
onKeyDown={onKeyDown}
|
|
257
|
+
testId='toggle-group'
|
|
258
|
+
>
|
|
259
|
+
<ToggleGroupItem
|
|
260
|
+
value='option-1'
|
|
261
|
+
label='Option 1'
|
|
262
|
+
/>
|
|
263
|
+
<ToggleGroupItem
|
|
264
|
+
value='option-2'
|
|
265
|
+
label='Option 2'
|
|
266
|
+
/>
|
|
267
|
+
</ToggleGroup>
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const group = screen.getByTestId('toggle-group');
|
|
271
|
+
fireEvent.keyDown(group, { key: 'a' });
|
|
272
|
+
|
|
273
|
+
expect(group).toHaveAccessibleName('View');
|
|
274
|
+
expect(group).toHaveClass('ucl-uikit-toggle-group', 'custom-toggle-group');
|
|
275
|
+
expect(onKeyDown).toHaveBeenCalledOnce();
|
|
276
|
+
});
|
|
277
|
+
|
|
190
278
|
test('skips disabled items when navigating with arrow keys', () => {
|
|
191
279
|
wrap(
|
|
192
280
|
<ToggleGroup defaultValue='option-1'>
|
|
@@ -272,7 +360,11 @@ describe('ToggleGroup mobile select fallback', () => {
|
|
|
272
360
|
|
|
273
361
|
test('renders a Select instead of the radiogroup once item count exceeds the threshold', () => {
|
|
274
362
|
wrap(
|
|
275
|
-
<ToggleGroup
|
|
363
|
+
<ToggleGroup
|
|
364
|
+
className='custom-toggle-group'
|
|
365
|
+
defaultValue='option-1'
|
|
366
|
+
testId='responsive-toggle-group'
|
|
367
|
+
>
|
|
276
368
|
<ToggleGroupItem
|
|
277
369
|
value='option-1'
|
|
278
370
|
label='Option 1'
|
|
@@ -289,7 +381,14 @@ describe('ToggleGroup mobile select fallback', () => {
|
|
|
289
381
|
);
|
|
290
382
|
|
|
291
383
|
expect(screen.queryByRole('radiogroup')).not.toBeInTheDocument();
|
|
292
|
-
expect(screen.getByRole('combobox')).
|
|
384
|
+
expect(screen.getByRole('combobox')).toHaveAttribute(
|
|
385
|
+
'data-testid',
|
|
386
|
+
'responsive-toggle-group'
|
|
387
|
+
);
|
|
388
|
+
expect(screen.getByRole('combobox')).toHaveClass(
|
|
389
|
+
'ucl-uikit-toggle-group',
|
|
390
|
+
'custom-toggle-group'
|
|
391
|
+
);
|
|
293
392
|
});
|
|
294
393
|
|
|
295
394
|
test('counts items nested in fragments toward the mobile select threshold', async () => {
|
|
@@ -418,7 +517,7 @@ describe('ToggleGroup mobile select fallback', () => {
|
|
|
418
517
|
);
|
|
419
518
|
|
|
420
519
|
const select = screen.getByTestId(
|
|
421
|
-
'ucl-uikit-
|
|
520
|
+
'ucl-uikit-toggle-group'
|
|
422
521
|
) as HTMLSelectElement;
|
|
423
522
|
|
|
424
523
|
expect(select.tagName).toBe('SELECT');
|
|
@@ -458,7 +557,7 @@ describe('ToggleGroup mobile select fallback', () => {
|
|
|
458
557
|
</ToggleGroup>
|
|
459
558
|
);
|
|
460
559
|
|
|
461
|
-
fireEvent.change(screen.getByTestId('ucl-uikit-
|
|
560
|
+
fireEvent.change(screen.getByTestId('ucl-uikit-toggle-group'), {
|
|
462
561
|
target: { value: 'option-2' },
|
|
463
562
|
});
|
|
464
563
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export { default } from './ToggleGroup';
|
|
2
2
|
export type { ToggleGroupProps } from './ToggleGroup';
|
|
3
|
-
export {
|
|
4
|
-
export type { ItemProps } from './subcomponents/Item';
|
|
3
|
+
export type { ToggleGroupItemProps } from './subcomponents/ToggleGroupItem';
|
package/lib/components/ToggleGroup/subcomponents/{Item.stories.tsx → ToggleGroupItem.stories.tsx}
RENAMED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
2
|
|
|
3
3
|
import ToggleGroup from '../ToggleGroup';
|
|
4
|
-
import Item from './Item';
|
|
5
4
|
|
|
6
5
|
const meta = {
|
|
7
6
|
title: 'Components/ToggleGroup/Item',
|
|
8
|
-
component: Item,
|
|
7
|
+
component: ToggleGroup.Item,
|
|
9
8
|
parameters: {
|
|
10
9
|
layout: 'centered',
|
|
11
10
|
},
|
|
12
11
|
tags: ['autodocs'],
|
|
13
12
|
decorators: [
|
|
14
13
|
(Story) => (
|
|
15
|
-
<ToggleGroup
|
|
14
|
+
<ToggleGroup
|
|
15
|
+
defaultValue='option-1'
|
|
16
|
+
style={{ width: 507 }}
|
|
17
|
+
>
|
|
16
18
|
<Story />
|
|
17
19
|
</ToggleGroup>
|
|
18
20
|
),
|
|
19
21
|
],
|
|
20
|
-
} satisfies Meta<typeof Item>;
|
|
22
|
+
} satisfies Meta<typeof ToggleGroup.Item>;
|
|
21
23
|
|
|
22
24
|
export default meta;
|
|
23
25
|
|
|
@@ -1,37 +1,60 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import type React from 'react';
|
|
2
2
|
import { css, cx } from '@emotion/css';
|
|
3
3
|
|
|
4
4
|
import useTheme from '../../../theme/useTheme';
|
|
5
|
-
import Text from '../../Text';
|
|
6
5
|
import Icon from '../../Icon';
|
|
7
6
|
import type { SpecificIconsType } from '../../Icon/Icon';
|
|
7
|
+
import Text from '../../Text';
|
|
8
8
|
import { useToggleGroupContext } from '../ToggleGroupContext';
|
|
9
9
|
|
|
10
|
-
export
|
|
10
|
+
export const NAME = 'ucl-uikit-toggle-group__item';
|
|
11
|
+
|
|
12
|
+
const LABEL_CLASS_NAME = `${NAME}-label`;
|
|
13
|
+
|
|
14
|
+
export interface ToggleGroupItemProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
11
15
|
value: string;
|
|
12
16
|
label: string;
|
|
13
17
|
icon?: keyof SpecificIconsType;
|
|
14
18
|
disabled?: boolean;
|
|
15
19
|
sublabel?: string;
|
|
20
|
+
testId?: string;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
const
|
|
23
|
+
const ToggleGroupItem = ({
|
|
19
24
|
value,
|
|
20
25
|
label,
|
|
21
26
|
sublabel,
|
|
22
27
|
icon,
|
|
23
28
|
disabled = false,
|
|
24
|
-
|
|
29
|
+
className,
|
|
30
|
+
onChange,
|
|
31
|
+
testId = NAME,
|
|
32
|
+
...inputProps
|
|
33
|
+
}: ToggleGroupItemProps) => {
|
|
25
34
|
const [theme] = useTheme();
|
|
26
|
-
const {
|
|
35
|
+
const {
|
|
36
|
+
value: selectedValue,
|
|
37
|
+
setValue,
|
|
38
|
+
name,
|
|
39
|
+
tabbableValue,
|
|
40
|
+
hasSublabel,
|
|
41
|
+
} = useToggleGroupContext();
|
|
27
42
|
const selected = selectedValue === value;
|
|
43
|
+
const sublabelLineHeight =
|
|
44
|
+
theme.typography.body.xs.fontSize *
|
|
45
|
+
(theme.typography.body.xs.lineHeight / 100);
|
|
28
46
|
|
|
29
47
|
const toggleGroupItemContainerStyle = css`
|
|
30
48
|
box-sizing: border-box;
|
|
31
49
|
position: relative;
|
|
32
50
|
display: flex;
|
|
33
|
-
flex: 1 0
|
|
34
|
-
|
|
51
|
+
flex: 1 1 0;
|
|
52
|
+
min-width: max-content;
|
|
53
|
+
height: ${
|
|
54
|
+
hasSublabel
|
|
55
|
+
? `calc(${theme.height.h40} + ${theme.padding.p8})`
|
|
56
|
+
: theme.height.h40
|
|
57
|
+
};
|
|
35
58
|
padding: ${theme.padding.p8};
|
|
36
59
|
align-items: center;
|
|
37
60
|
justify-content: center;
|
|
@@ -55,6 +78,10 @@ const Item = ({
|
|
|
55
78
|
:hover {
|
|
56
79
|
background-color: ${theme.colour.fill.brandSubtle};
|
|
57
80
|
}
|
|
81
|
+
|
|
82
|
+
:hover .${LABEL_CLASS_NAME} {
|
|
83
|
+
font-weight: ${theme.typography.body.smSemibold.fontWeight};
|
|
84
|
+
}
|
|
58
85
|
`
|
|
59
86
|
}
|
|
60
87
|
|
|
@@ -82,6 +109,17 @@ const Item = ({
|
|
|
82
109
|
gap: ${theme.padding.p8};
|
|
83
110
|
`;
|
|
84
111
|
|
|
112
|
+
const toggleGroupItemContentStyle = css`
|
|
113
|
+
box-sizing: border-box;
|
|
114
|
+
position: absolute;
|
|
115
|
+
inset: 0;
|
|
116
|
+
display: flex;
|
|
117
|
+
padding: ${theme.padding.p8};
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: ${sublabel ? 'space-between' : 'center'};
|
|
120
|
+
gap: ${theme.padding.p8};
|
|
121
|
+
`;
|
|
122
|
+
|
|
85
123
|
const toggleGroupItemSizerStyle = cx(
|
|
86
124
|
flexRowStyle,
|
|
87
125
|
css`
|
|
@@ -89,24 +127,8 @@ const Item = ({
|
|
|
89
127
|
`
|
|
90
128
|
);
|
|
91
129
|
|
|
92
|
-
const toggleGroupItemContentStyle = cx(
|
|
93
|
-
flexRowStyle,
|
|
94
|
-
css`
|
|
95
|
-
box-sizing: border-box;
|
|
96
|
-
position: absolute;
|
|
97
|
-
inset: 0;
|
|
98
|
-
justify-content: ${sublabel && icon ? 'space-between' : 'center'};
|
|
99
|
-
padding: ${theme.padding.p8};
|
|
100
|
-
`
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
const toggleGroupItemLabelBaseStyle = css`
|
|
104
|
-
text-align: ${sublabel && icon ? 'left' : 'center'};
|
|
105
|
-
`;
|
|
106
|
-
|
|
107
130
|
const toggleGroupItemLabelSelectedStyle = css`
|
|
108
131
|
color: ${theme.colour.text.brand};
|
|
109
|
-
font-weight: ${theme.typography.body.smSemibold.fontWeight};
|
|
110
132
|
`;
|
|
111
133
|
|
|
112
134
|
const toggleGroupItemLabelDisabledStyle = css`
|
|
@@ -114,7 +136,6 @@ const Item = ({
|
|
|
114
136
|
`;
|
|
115
137
|
|
|
116
138
|
const toggleGroupItemLabelStyle = cx(
|
|
117
|
-
toggleGroupItemLabelBaseStyle,
|
|
118
139
|
selected && !disabled && toggleGroupItemLabelSelectedStyle,
|
|
119
140
|
disabled && toggleGroupItemLabelDisabledStyle
|
|
120
141
|
);
|
|
@@ -140,8 +161,31 @@ const Item = ({
|
|
|
140
161
|
);
|
|
141
162
|
|
|
142
163
|
const toggleGroupItemTextStyle = css`
|
|
164
|
+
position: relative;
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-direction: column;
|
|
167
|
+
align-items: flex-start;
|
|
168
|
+
padding-bottom: ${sublabel ? `${sublabelLineHeight}px` : 0};
|
|
169
|
+
text-align: left;
|
|
170
|
+
`;
|
|
171
|
+
|
|
172
|
+
const toggleGroupItemReservedTextStyle = css`
|
|
143
173
|
display: flex;
|
|
144
174
|
flex-direction: column;
|
|
175
|
+
align-items: flex-start;
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
const toggleGroupItemLabelSizerStyle = css`
|
|
179
|
+
display: inline-grid;
|
|
180
|
+
white-space: nowrap;
|
|
181
|
+
|
|
182
|
+
> * {
|
|
183
|
+
grid-area: 1 / 1;
|
|
184
|
+
}
|
|
185
|
+
`;
|
|
186
|
+
|
|
187
|
+
const toggleGroupItemReservedLabelStyle = css`
|
|
188
|
+
visibility: hidden;
|
|
145
189
|
`;
|
|
146
190
|
|
|
147
191
|
const toggleGroupItemSublabelBaseStyle = css`
|
|
@@ -157,32 +201,58 @@ const Item = ({
|
|
|
157
201
|
disabled && toggleGroupItemSublabelDisabledStyle
|
|
158
202
|
);
|
|
159
203
|
|
|
160
|
-
const
|
|
204
|
+
const toggleGroupItemPositionedSublabelStyle = css`
|
|
205
|
+
position: absolute;
|
|
206
|
+
bottom: 0;
|
|
207
|
+
left: 0;
|
|
208
|
+
white-space: nowrap;
|
|
209
|
+
`;
|
|
161
210
|
|
|
162
211
|
const toggleGroupItemCheckIconStyle = css`
|
|
163
212
|
color: ${theme.colour.icon.brand};
|
|
213
|
+
visibility: ${selected ? 'visible' : 'hidden'};
|
|
164
214
|
`;
|
|
165
215
|
|
|
166
216
|
const IconComponent = icon ? Icon[icon] : undefined;
|
|
167
217
|
|
|
168
|
-
const
|
|
169
|
-
<div className={
|
|
218
|
+
const renderLabelContent = (reserved = false) => (
|
|
219
|
+
<div className={flexRowStyle}>
|
|
170
220
|
{IconComponent && (
|
|
171
221
|
<span className={toggleGroupItemIconStyle}>
|
|
172
222
|
<IconComponent size={16} />
|
|
173
223
|
</span>
|
|
174
224
|
)}
|
|
175
|
-
<div
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
225
|
+
<div
|
|
226
|
+
className={
|
|
227
|
+
reserved ? toggleGroupItemReservedTextStyle : toggleGroupItemTextStyle
|
|
228
|
+
}
|
|
229
|
+
>
|
|
230
|
+
{reserved ? (
|
|
231
|
+
<Text level='sm-semibold'>{label}</Text>
|
|
232
|
+
) : (
|
|
233
|
+
<span className={toggleGroupItemLabelSizerStyle}>
|
|
234
|
+
<Text
|
|
235
|
+
level={selected && !disabled ? 'sm-semibold' : 'sm'}
|
|
236
|
+
className={cx(LABEL_CLASS_NAME, toggleGroupItemLabelStyle)}
|
|
237
|
+
>
|
|
238
|
+
{label}
|
|
239
|
+
</Text>
|
|
240
|
+
<Text
|
|
241
|
+
aria-hidden
|
|
242
|
+
level='sm-semibold'
|
|
243
|
+
className={toggleGroupItemReservedLabelStyle}
|
|
244
|
+
>
|
|
245
|
+
{label}
|
|
246
|
+
</Text>
|
|
247
|
+
</span>
|
|
248
|
+
)}
|
|
182
249
|
{sublabel && (
|
|
183
250
|
<Text
|
|
184
251
|
level='xs'
|
|
185
|
-
className={
|
|
252
|
+
className={cx(
|
|
253
|
+
toggleGroupItemSublabelStyle,
|
|
254
|
+
!reserved && toggleGroupItemPositionedSublabelStyle
|
|
255
|
+
)}
|
|
186
256
|
>
|
|
187
257
|
{sublabel}
|
|
188
258
|
</Text>
|
|
@@ -191,43 +261,47 @@ const Item = ({
|
|
|
191
261
|
</div>
|
|
192
262
|
);
|
|
193
263
|
|
|
264
|
+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
265
|
+
if (disabled) return;
|
|
266
|
+
|
|
267
|
+
setValue(value);
|
|
268
|
+
onChange?.(event);
|
|
269
|
+
};
|
|
270
|
+
|
|
194
271
|
return (
|
|
195
272
|
<label className={toggleGroupItemContainerStyle}>
|
|
196
273
|
<input
|
|
274
|
+
{...inputProps}
|
|
197
275
|
type='radio'
|
|
198
|
-
className={toggleGroupItemInputStyle}
|
|
276
|
+
className={cx(NAME, toggleGroupItemInputStyle, className)}
|
|
199
277
|
role='radio'
|
|
200
278
|
aria-checked={selected}
|
|
201
279
|
checked={selected}
|
|
202
280
|
disabled={disabled}
|
|
203
281
|
name={name}
|
|
204
282
|
value={value}
|
|
205
|
-
tabIndex={
|
|
283
|
+
tabIndex={tabbableValue === value ? 0 : -1}
|
|
206
284
|
data-ucl-toggle-group-value={value}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
setValue(value);
|
|
210
|
-
}}
|
|
285
|
+
data-testid={testId}
|
|
286
|
+
onChange={handleChange}
|
|
211
287
|
/>
|
|
212
288
|
<div
|
|
213
289
|
aria-hidden
|
|
214
290
|
className={toggleGroupItemSizerStyle}
|
|
215
291
|
>
|
|
216
|
-
{
|
|
292
|
+
{renderLabelContent(true)}
|
|
217
293
|
<Icon.Check size={16} />
|
|
218
294
|
</div>
|
|
219
295
|
<div className={toggleGroupItemContentStyle}>
|
|
220
|
-
{
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
/>
|
|
227
|
-
)}
|
|
296
|
+
{renderLabelContent()}
|
|
297
|
+
<Icon.Check
|
|
298
|
+
aria-hidden
|
|
299
|
+
className={toggleGroupItemCheckIconStyle}
|
|
300
|
+
size={16}
|
|
301
|
+
/>
|
|
228
302
|
</div>
|
|
229
303
|
</label>
|
|
230
304
|
);
|
|
231
305
|
};
|
|
232
306
|
|
|
233
|
-
export default
|
|
307
|
+
export default ToggleGroupItem;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test, vi } from 'vitest';
|
|
2
2
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
3
|
import ToggleGroup from '../../ToggleGroup';
|
|
4
|
-
import ToggleGroupItem from '../Item';
|
|
5
4
|
import { ThemeContextProvider } from '../../../../theme/useTheme';
|
|
6
5
|
|
|
6
|
+
const ToggleGroupItem = ToggleGroup.Item;
|
|
7
|
+
|
|
7
8
|
const wrap = (ui: React.ReactElement) =>
|
|
8
9
|
render(<ThemeContextProvider>{ui}</ThemeContextProvider>);
|
|
9
10
|
|
|
@@ -20,8 +21,10 @@ describe('ToggleGroupItem', () => {
|
|
|
20
21
|
|
|
21
22
|
const item = screen.getByRole('radio', { name: 'Option 1' });
|
|
22
23
|
|
|
24
|
+
expect(item).toHaveAttribute('data-testid', 'ucl-uikit-toggle-group__item');
|
|
25
|
+
expect(item).toHaveClass('ucl-uikit-toggle-group__item');
|
|
23
26
|
expect(item).toHaveAttribute('aria-checked', 'false');
|
|
24
|
-
expect(item).toHaveAttribute('tabindex', '
|
|
27
|
+
expect(item).toHaveAttribute('tabindex', '0');
|
|
25
28
|
});
|
|
26
29
|
|
|
27
30
|
test('reflects selection state from the surrounding ToggleGroup', () => {
|
|
@@ -54,6 +57,7 @@ describe('ToggleGroupItem', () => {
|
|
|
54
57
|
|
|
55
58
|
test('calls setValue via the ToggleGroup on click', () => {
|
|
56
59
|
const onValueChange = vi.fn();
|
|
60
|
+
const onChange = vi.fn();
|
|
57
61
|
|
|
58
62
|
wrap(
|
|
59
63
|
<ToggleGroup
|
|
@@ -67,13 +71,23 @@ describe('ToggleGroupItem', () => {
|
|
|
67
71
|
<ToggleGroupItem
|
|
68
72
|
value='option-2'
|
|
69
73
|
label='Option 2'
|
|
74
|
+
className='custom-toggle-group-item'
|
|
75
|
+
onChange={onChange}
|
|
76
|
+
testId='option-2'
|
|
70
77
|
/>
|
|
71
78
|
</ToggleGroup>
|
|
72
79
|
);
|
|
73
80
|
|
|
74
|
-
|
|
81
|
+
const option2 = screen.getByRole('radio', { name: 'Option 2' });
|
|
82
|
+
fireEvent.click(option2);
|
|
75
83
|
|
|
84
|
+
expect(option2).toHaveAttribute('data-testid', 'option-2');
|
|
85
|
+
expect(option2).toHaveClass(
|
|
86
|
+
'ucl-uikit-toggle-group__item',
|
|
87
|
+
'custom-toggle-group-item'
|
|
88
|
+
);
|
|
76
89
|
expect(onValueChange).toHaveBeenCalledWith('option-2');
|
|
90
|
+
expect(onChange).toHaveBeenCalledOnce();
|
|
77
91
|
});
|
|
78
92
|
|
|
79
93
|
test('does not select or fire onValueChange when disabled', () => {
|
|
@@ -168,7 +182,7 @@ describe('ToggleGroupItem', () => {
|
|
|
168
182
|
).toBeInTheDocument();
|
|
169
183
|
});
|
|
170
184
|
|
|
171
|
-
test('
|
|
185
|
+
test('reserves checkmark space without showing it on unselected items', () => {
|
|
172
186
|
wrap(
|
|
173
187
|
<ToggleGroup defaultValue='option-1'>
|
|
174
188
|
<ToggleGroupItem
|
|
@@ -189,7 +203,37 @@ describe('ToggleGroupItem', () => {
|
|
|
189
203
|
.getByRole('radio', { name: 'Option 2' })
|
|
190
204
|
.closest('label');
|
|
191
205
|
|
|
192
|
-
|
|
193
|
-
|
|
206
|
+
const selectedCheckmark = selected!.lastElementChild!.querySelector('svg')!;
|
|
207
|
+
const unselectedCheckmark =
|
|
208
|
+
unselected!.lastElementChild!.querySelector('svg')!;
|
|
209
|
+
|
|
210
|
+
expect(selected!.querySelectorAll('svg')).toHaveLength(2);
|
|
211
|
+
expect(unselected!.querySelectorAll('svg')).toHaveLength(2);
|
|
212
|
+
expect(selectedCheckmark).toHaveStyle({ visibility: 'visible' });
|
|
213
|
+
expect(unselectedCheckmark).toHaveStyle({ visibility: 'hidden' });
|
|
214
|
+
expect(selected!.lastElementChild).toHaveStyle({
|
|
215
|
+
justifyContent: 'center',
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('aligns sublabel content between the item edges', () => {
|
|
220
|
+
wrap(
|
|
221
|
+
<ToggleGroup defaultValue='option-1'>
|
|
222
|
+
<ToggleGroupItem
|
|
223
|
+
value='option-1'
|
|
224
|
+
label='Option 1'
|
|
225
|
+
sublabel='Sublabel 1'
|
|
226
|
+
icon='BookOpen'
|
|
227
|
+
/>
|
|
228
|
+
</ToggleGroup>
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const item = screen
|
|
232
|
+
.getByRole('radio', { name: 'Option 1 Sublabel 1' })
|
|
233
|
+
.closest('label');
|
|
234
|
+
|
|
235
|
+
expect(item!.lastElementChild).toHaveStyle({
|
|
236
|
+
justifyContent: 'space-between',
|
|
237
|
+
});
|
|
194
238
|
});
|
|
195
239
|
});
|
package/lib/components/index.ts
CHANGED
|
@@ -142,7 +142,7 @@ export { default as Toggle } from './Toggle';
|
|
|
142
142
|
export type { ToggleProps } from './Toggle';
|
|
143
143
|
|
|
144
144
|
export { default as ToggleGroup } from './ToggleGroup';
|
|
145
|
-
export type {
|
|
145
|
+
export type { ToggleGroupItemProps, ToggleGroupProps } from './ToggleGroup';
|
|
146
146
|
|
|
147
147
|
export { default as Chip } from './Chip';
|
|
148
148
|
export type { ChipProps } from './Chip';
|
package/package.json
CHANGED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { InputHTMLAttributes } from 'react';
|
|
2
|
-
import { SpecificIconsType } from '../../Icon/Icon';
|
|
3
|
-
export interface ItemProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
4
|
-
value: string;
|
|
5
|
-
label: string;
|
|
6
|
-
icon?: keyof SpecificIconsType;
|
|
7
|
-
disabled?: boolean;
|
|
8
|
-
sublabel?: string;
|
|
9
|
-
}
|
|
10
|
-
declare const Item: ({ value, label, sublabel, icon, disabled, }: ItemProps) => import("react").JSX.Element;
|
|
11
|
-
export default Item;
|
|
File without changes
|