uikit-react-public 0.50.5 → 0.50.6
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 +2 -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 +5126 -5045
- 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} +2 -3
- 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
|
@@ -7,18 +7,23 @@ import {
|
|
|
7
7
|
useState,
|
|
8
8
|
} from 'react';
|
|
9
9
|
|
|
10
|
-
import { css } from '@emotion/css';
|
|
10
|
+
import { css, cx } from '@emotion/css';
|
|
11
11
|
import useTheme from '../../theme/useTheme';
|
|
12
12
|
import useMediaQuery from '../../hooks/useMediaQuery';
|
|
13
13
|
import Select, { NonClearableSelectProps } from '../Select';
|
|
14
14
|
import ToggleGroupContext from './ToggleGroupContext';
|
|
15
|
-
import
|
|
15
|
+
import ToggleGroupItem, {
|
|
16
|
+
ToggleGroupItemProps,
|
|
17
|
+
} from './subcomponents/ToggleGroupItem';
|
|
18
|
+
|
|
19
|
+
export const NAME = 'ucl-uikit-toggle-group';
|
|
16
20
|
|
|
17
21
|
export interface ToggleGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
18
22
|
children: React.ReactNode;
|
|
19
23
|
value?: string;
|
|
20
24
|
defaultValue?: string;
|
|
21
25
|
onValueChange?: (value: string) => void;
|
|
26
|
+
testId?: string;
|
|
22
27
|
selectProps?: Omit<
|
|
23
28
|
NonClearableSelectProps<string>,
|
|
24
29
|
'options' | 'value' | 'onValueChange'
|
|
@@ -27,7 +32,14 @@ export interface ToggleGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
27
32
|
|
|
28
33
|
const MOBILE_SELECT_ITEM_THRESHOLD = 2;
|
|
29
34
|
|
|
30
|
-
const NAVIGATION_KEYS = [
|
|
35
|
+
const NAVIGATION_KEYS = [
|
|
36
|
+
'ArrowRight',
|
|
37
|
+
'ArrowDown',
|
|
38
|
+
'ArrowLeft',
|
|
39
|
+
'ArrowUp',
|
|
40
|
+
'Home',
|
|
41
|
+
'End',
|
|
42
|
+
];
|
|
31
43
|
|
|
32
44
|
const getEnabledItems = (container: HTMLElement) =>
|
|
33
45
|
Array.from(
|
|
@@ -38,13 +50,13 @@ const getEnabledItems = (container: HTMLElement) =>
|
|
|
38
50
|
|
|
39
51
|
const getItemsFromChildren = (
|
|
40
52
|
children: React.ReactNode
|
|
41
|
-
): React.ReactElement<
|
|
53
|
+
): React.ReactElement<ToggleGroupItemProps>[] =>
|
|
42
54
|
Children.toArray(children).flatMap((child) => {
|
|
43
55
|
if (!isValidElement<{ children?: React.ReactNode }>(child)) {
|
|
44
56
|
return [];
|
|
45
57
|
}
|
|
46
|
-
if (child.type ===
|
|
47
|
-
return [child as React.ReactElement<
|
|
58
|
+
if (child.type === ToggleGroupItem) {
|
|
59
|
+
return [child as React.ReactElement<ToggleGroupItemProps>];
|
|
48
60
|
}
|
|
49
61
|
return getItemsFromChildren(child.props.children);
|
|
50
62
|
});
|
|
@@ -55,6 +67,10 @@ const ToggleGroup = ({
|
|
|
55
67
|
defaultValue,
|
|
56
68
|
onValueChange,
|
|
57
69
|
selectProps,
|
|
70
|
+
className,
|
|
71
|
+
onKeyDown,
|
|
72
|
+
testId = NAME,
|
|
73
|
+
...htmlAttributes
|
|
58
74
|
}: ToggleGroupProps) => {
|
|
59
75
|
const [theme] = useTheme();
|
|
60
76
|
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
@@ -63,6 +79,14 @@ const ToggleGroup = ({
|
|
|
63
79
|
const isMobile = !useMediaQuery(`(min-width: ${theme.breakpoints.tablet}px)`);
|
|
64
80
|
const items = getItemsFromChildren(children);
|
|
65
81
|
const name = useId();
|
|
82
|
+
const hasSublabel = items.some((item) => item.props.sublabel);
|
|
83
|
+
const firstEnabledValue = items.find((item) => !item.props.disabled)?.props
|
|
84
|
+
.value;
|
|
85
|
+
const tabbableValue = items.some(
|
|
86
|
+
(item) => item.props.value === selectedValue && !item.props.disabled
|
|
87
|
+
)
|
|
88
|
+
? selectedValue
|
|
89
|
+
: firstEnabledValue;
|
|
66
90
|
|
|
67
91
|
const setValue = (nextValue: string) => {
|
|
68
92
|
if (!isControlled) {
|
|
@@ -73,6 +97,12 @@ const ToggleGroup = ({
|
|
|
73
97
|
};
|
|
74
98
|
|
|
75
99
|
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
100
|
+
onKeyDown?.(event);
|
|
101
|
+
|
|
102
|
+
if (event.defaultPrevented) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
76
106
|
if (!NAVIGATION_KEYS.includes(event.key)) {
|
|
77
107
|
return;
|
|
78
108
|
}
|
|
@@ -91,8 +121,10 @@ const ToggleGroup = ({
|
|
|
91
121
|
const nextIndex =
|
|
92
122
|
{
|
|
93
123
|
ArrowRight: (currentIndex + 1) % enabledItems.length,
|
|
124
|
+
ArrowDown: (currentIndex + 1) % enabledItems.length,
|
|
94
125
|
ArrowLeft:
|
|
95
126
|
(currentIndex - 1 + enabledItems.length) % enabledItems.length,
|
|
127
|
+
ArrowUp: (currentIndex - 1 + enabledItems.length) % enabledItems.length,
|
|
96
128
|
Home: 0,
|
|
97
129
|
End: enabledItems.length - 1,
|
|
98
130
|
}[event.key] ?? currentIndex;
|
|
@@ -104,7 +136,6 @@ const ToggleGroup = ({
|
|
|
104
136
|
|
|
105
137
|
const toggleGroupStyle = css`
|
|
106
138
|
display: flex;
|
|
107
|
-
gap: -1px;
|
|
108
139
|
padding: ${theme.padding.p4};
|
|
109
140
|
background-color: ${theme.colour.fill.subtle};
|
|
110
141
|
border-radius: ${theme.radius.r4};
|
|
@@ -114,6 +145,8 @@ const ToggleGroup = ({
|
|
|
114
145
|
return (
|
|
115
146
|
<Select
|
|
116
147
|
{...selectProps}
|
|
148
|
+
className={cx(NAME, selectProps?.className, className)}
|
|
149
|
+
testId={testId}
|
|
117
150
|
options={items.map((item) => ({
|
|
118
151
|
label: item.props.label,
|
|
119
152
|
value: item.props.value,
|
|
@@ -126,10 +159,18 @@ const ToggleGroup = ({
|
|
|
126
159
|
|
|
127
160
|
return (
|
|
128
161
|
<ToggleGroupContext.Provider
|
|
129
|
-
value={{
|
|
162
|
+
value={{
|
|
163
|
+
value: selectedValue,
|
|
164
|
+
setValue,
|
|
165
|
+
name,
|
|
166
|
+
tabbableValue,
|
|
167
|
+
hasSublabel,
|
|
168
|
+
}}
|
|
130
169
|
>
|
|
131
170
|
<div
|
|
132
|
-
|
|
171
|
+
{...htmlAttributes}
|
|
172
|
+
className={cx(NAME, toggleGroupStyle, className)}
|
|
173
|
+
data-testid={testId}
|
|
133
174
|
role='radiogroup'
|
|
134
175
|
onKeyDown={handleKeyDown}
|
|
135
176
|
>
|
|
@@ -139,6 +180,6 @@ const ToggleGroup = ({
|
|
|
139
180
|
);
|
|
140
181
|
};
|
|
141
182
|
|
|
142
|
-
ToggleGroup.Item =
|
|
183
|
+
ToggleGroup.Item = ToggleGroupItem;
|
|
143
184
|
|
|
144
185
|
export default ToggleGroup;
|
|
@@ -4,6 +4,8 @@ export interface ToggleGroupContextType {
|
|
|
4
4
|
value: string | undefined;
|
|
5
5
|
setValue: (value: string) => void;
|
|
6
6
|
name: string;
|
|
7
|
+
tabbableValue: string | undefined;
|
|
8
|
+
hasSublabel: boolean;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
const ToggleGroupContext = createContext<ToggleGroupContextType | undefined>(
|
|
@@ -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,11 +1,10 @@
|
|
|
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
|
},
|
|
@@ -17,7 +16,7 @@ const meta = {
|
|
|
17
16
|
</ToggleGroup>
|
|
18
17
|
),
|
|
19
18
|
],
|
|
20
|
-
} satisfies Meta<typeof Item>;
|
|
19
|
+
} satisfies Meta<typeof ToggleGroup.Item>;
|
|
21
20
|
|
|
22
21
|
export default meta;
|
|
23
22
|
|
|
@@ -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;
|