uikit-react-public 0.49.4 → 0.49.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.
Files changed (33) hide show
  1. package/dist/components/Overlay/Overlay.d.ts +2 -2
  2. package/dist/components/Overlay/Overlay.stories.d.ts +13 -1
  3. package/dist/components/Select/index.d.ts +1 -1
  4. package/dist/components/Select/subcomponents/Panel.d.ts +1 -2
  5. package/dist/components/ToggleGroup/ToggleGroup.d.ts +15 -0
  6. package/dist/components/ToggleGroup/ToggleGroup.stories.d.ts +18 -0
  7. package/dist/components/ToggleGroup/ToggleGroupContext.d.ts +8 -0
  8. package/dist/components/ToggleGroup/__tests__/ToggleGroup.test.d.ts +1 -0
  9. package/dist/components/ToggleGroup/index.d.ts +4 -0
  10. package/dist/components/ToggleGroup/subcomponents/Item.d.ts +11 -0
  11. package/dist/components/ToggleGroup/subcomponents/Item.stories.d.ts +331 -0
  12. package/dist/components/ToggleGroup/subcomponents/__tests__/Item.test.d.ts +1 -0
  13. package/dist/components/index.d.ts +2 -0
  14. package/dist/index.js +8351 -8121
  15. package/lib/components/Icon/svgImports.ts +4 -2
  16. package/lib/components/Overlay/Overlay.tsx +9 -3
  17. package/lib/components/Select/Select.mdx +1 -0
  18. package/lib/components/Select/__tests__/Select.test.tsx +137 -4
  19. package/lib/components/Select/index.ts +5 -1
  20. package/lib/components/Select/subcomponents/CustomSelect.tsx +42 -43
  21. package/lib/components/Select/subcomponents/Panel.tsx +2 -8
  22. package/lib/components/ToggleGroup/ToggleGroup.mdx +31 -0
  23. package/lib/components/ToggleGroup/ToggleGroup.stories.tsx +120 -0
  24. package/lib/components/ToggleGroup/ToggleGroup.tsx +144 -0
  25. package/lib/components/ToggleGroup/ToggleGroupContext.tsx +25 -0
  26. package/lib/components/ToggleGroup/__tests__/ToggleGroup.test.tsx +468 -0
  27. package/lib/components/ToggleGroup/index.ts +4 -0
  28. package/lib/components/ToggleGroup/subcomponents/Item.stories.tsx +63 -0
  29. package/lib/components/ToggleGroup/subcomponents/Item.tsx +233 -0
  30. package/lib/components/ToggleGroup/subcomponents/__tests__/Item.test.tsx +195 -0
  31. package/lib/components/index.ts +3 -0
  32. package/package.json +3 -3
  33. package/dist/components/Icon/svgImports.d.ts +0 -8
@@ -0,0 +1,233 @@
1
+ import type { InputHTMLAttributes } from 'react';
2
+ import { css, cx } from '@emotion/css';
3
+
4
+ import useTheme from '../../../theme/useTheme';
5
+ import Text from '../../Text';
6
+ import Icon from '../../Icon';
7
+ import type { SpecificIconsType } from '../../Icon/Icon';
8
+ import { useToggleGroupContext } from '../ToggleGroupContext';
9
+
10
+ export interface ItemProps extends InputHTMLAttributes<HTMLInputElement> {
11
+ value: string;
12
+ label: string;
13
+ icon?: keyof SpecificIconsType;
14
+ disabled?: boolean;
15
+ sublabel?: string;
16
+ }
17
+
18
+ const Item = ({
19
+ value,
20
+ label,
21
+ sublabel,
22
+ icon,
23
+ disabled = false,
24
+ }: ItemProps) => {
25
+ const [theme] = useTheme();
26
+ const { value: selectedValue, setValue, name } = useToggleGroupContext();
27
+ const selected = selectedValue === value;
28
+
29
+ const toggleGroupItemContainerStyle = css`
30
+ box-sizing: border-box;
31
+ position: relative;
32
+ display: flex;
33
+ flex: 1 0 auto;
34
+ height: ${theme.height.h40};
35
+ padding: ${theme.padding.p8};
36
+ align-items: center;
37
+ justify-content: center;
38
+ border-radius: ${theme.radius.r4};
39
+ cursor: ${disabled ? 'not-allowed' : 'pointer'};
40
+ transition:
41
+ background-color 150ms ease-in-out,
42
+ box-shadow 150ms ease-in-out;
43
+
44
+ background-color: ${
45
+ disabled
46
+ ? theme.colour.fill.disabled
47
+ : selected
48
+ ? theme.colour.fill.default
49
+ : theme.colour.fill.subtle
50
+ };
51
+
52
+ ${
53
+ !disabled &&
54
+ `
55
+ :hover {
56
+ background-color: ${theme.colour.fill.brandSubtle};
57
+ }
58
+ `
59
+ }
60
+
61
+ &:has(input:focus-visible) {
62
+ box-shadow: ${theme.boxShadow.focus};
63
+ z-index: 1;
64
+ }
65
+ `;
66
+
67
+ const toggleGroupItemInputStyle = css`
68
+ position: absolute;
69
+ inset: 0;
70
+ width: 100%;
71
+ height: 100%;
72
+ margin: 0;
73
+ padding: 0;
74
+ border: 0;
75
+ opacity: 0;
76
+ cursor: inherit;
77
+ `;
78
+
79
+ const flexRowStyle = css`
80
+ display: flex;
81
+ align-items: center;
82
+ gap: ${theme.padding.p8};
83
+ `;
84
+
85
+ const toggleGroupItemSizerStyle = cx(
86
+ flexRowStyle,
87
+ css`
88
+ visibility: hidden;
89
+ `
90
+ );
91
+
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
+ const toggleGroupItemLabelSelectedStyle = css`
108
+ color: ${theme.colour.text.brand};
109
+ font-weight: ${theme.typography.body.smSemibold.fontWeight};
110
+ `;
111
+
112
+ const toggleGroupItemLabelDisabledStyle = css`
113
+ color: ${theme.colour.text.disabled};
114
+ `;
115
+
116
+ const toggleGroupItemLabelStyle = cx(
117
+ toggleGroupItemLabelBaseStyle,
118
+ selected && !disabled && toggleGroupItemLabelSelectedStyle,
119
+ disabled && toggleGroupItemLabelDisabledStyle
120
+ );
121
+
122
+ const toggleGroupItemIconBaseStyle = css`
123
+ display: flex;
124
+ align-items: center;
125
+ color: ${theme.colour.icon.default};
126
+ `;
127
+
128
+ const toggleGroupItemIconSelectedStyle = css`
129
+ color: ${theme.colour.icon.brand};
130
+ `;
131
+
132
+ const toggleGroupItemIconDisabledStyle = css`
133
+ color: ${theme.colour.icon.disabled};
134
+ `;
135
+
136
+ const toggleGroupItemIconStyle = cx(
137
+ toggleGroupItemIconBaseStyle,
138
+ selected && !disabled && toggleGroupItemIconSelectedStyle,
139
+ disabled && toggleGroupItemIconDisabledStyle
140
+ );
141
+
142
+ const toggleGroupItemTextStyle = css`
143
+ display: flex;
144
+ flex-direction: column;
145
+ `;
146
+
147
+ const toggleGroupItemSublabelBaseStyle = css`
148
+ color: ${theme.colour.text.secondary};
149
+ `;
150
+
151
+ const toggleGroupItemSublabelDisabledStyle = css`
152
+ color: ${theme.colour.text.disabled};
153
+ `;
154
+
155
+ const toggleGroupItemSublabelStyle = cx(
156
+ toggleGroupItemSublabelBaseStyle,
157
+ disabled && toggleGroupItemSublabelDisabledStyle
158
+ );
159
+
160
+ const toggleGroupItemLabelGroupStyle = flexRowStyle;
161
+
162
+ const toggleGroupItemCheckIconStyle = css`
163
+ color: ${theme.colour.icon.brand};
164
+ `;
165
+
166
+ const IconComponent = icon ? Icon[icon] : undefined;
167
+
168
+ const labelContent = (
169
+ <div className={toggleGroupItemLabelGroupStyle}>
170
+ {IconComponent && (
171
+ <span className={toggleGroupItemIconStyle}>
172
+ <IconComponent size={16} />
173
+ </span>
174
+ )}
175
+ <div className={toggleGroupItemTextStyle}>
176
+ <Text
177
+ level='sm'
178
+ className={toggleGroupItemLabelStyle}
179
+ >
180
+ {label}
181
+ </Text>
182
+ {sublabel && (
183
+ <Text
184
+ level='xs'
185
+ className={toggleGroupItemSublabelStyle}
186
+ >
187
+ {sublabel}
188
+ </Text>
189
+ )}
190
+ </div>
191
+ </div>
192
+ );
193
+
194
+ return (
195
+ <label className={toggleGroupItemContainerStyle}>
196
+ <input
197
+ type='radio'
198
+ className={toggleGroupItemInputStyle}
199
+ role='radio'
200
+ aria-checked={selected}
201
+ checked={selected}
202
+ disabled={disabled}
203
+ name={name}
204
+ value={value}
205
+ tabIndex={selected ? 0 : -1}
206
+ data-ucl-toggle-group-value={value}
207
+ onChange={() => {
208
+ if (disabled) return;
209
+ setValue(value);
210
+ }}
211
+ />
212
+ <div
213
+ aria-hidden
214
+ className={toggleGroupItemSizerStyle}
215
+ >
216
+ {labelContent}
217
+ <Icon.Check size={16} />
218
+ </div>
219
+ <div className={toggleGroupItemContentStyle}>
220
+ {labelContent}
221
+ {selected && (
222
+ <Icon.Check
223
+ aria-hidden
224
+ className={toggleGroupItemCheckIconStyle}
225
+ size={16}
226
+ />
227
+ )}
228
+ </div>
229
+ </label>
230
+ );
231
+ };
232
+
233
+ export default Item;
@@ -0,0 +1,195 @@
1
+ import { describe, expect, test, vi } from 'vitest';
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import ToggleGroup from '../../ToggleGroup';
4
+ import ToggleGroupItem from '../Item';
5
+ import { ThemeContextProvider } from '../../../../theme/useTheme';
6
+
7
+ const wrap = (ui: React.ReactElement) =>
8
+ render(<ThemeContextProvider>{ui}</ThemeContextProvider>);
9
+
10
+ describe('ToggleGroupItem', () => {
11
+ test('renders as an unchecked radio by default', () => {
12
+ wrap(
13
+ <ToggleGroup>
14
+ <ToggleGroupItem
15
+ value='option-1'
16
+ label='Option 1'
17
+ />
18
+ </ToggleGroup>
19
+ );
20
+
21
+ const item = screen.getByRole('radio', { name: 'Option 1' });
22
+
23
+ expect(item).toHaveAttribute('aria-checked', 'false');
24
+ expect(item).toHaveAttribute('tabindex', '-1');
25
+ });
26
+
27
+ test('reflects selection state from the surrounding ToggleGroup', () => {
28
+ wrap(
29
+ <ToggleGroup defaultValue='option-1'>
30
+ <ToggleGroupItem
31
+ value='option-1'
32
+ label='Option 1'
33
+ />
34
+ <ToggleGroupItem
35
+ value='option-2'
36
+ label='Option 2'
37
+ />
38
+ </ToggleGroup>
39
+ );
40
+
41
+ expect(screen.getByRole('radio', { name: 'Option 1' })).toHaveAttribute(
42
+ 'aria-checked',
43
+ 'true'
44
+ );
45
+ expect(screen.getByRole('radio', { name: 'Option 1' })).toHaveAttribute(
46
+ 'tabindex',
47
+ '0'
48
+ );
49
+ expect(screen.getByRole('radio', { name: 'Option 2' })).toHaveAttribute(
50
+ 'tabindex',
51
+ '-1'
52
+ );
53
+ });
54
+
55
+ test('calls setValue via the ToggleGroup on click', () => {
56
+ const onValueChange = vi.fn();
57
+
58
+ wrap(
59
+ <ToggleGroup
60
+ defaultValue='option-1'
61
+ onValueChange={onValueChange}
62
+ >
63
+ <ToggleGroupItem
64
+ value='option-1'
65
+ label='Option 1'
66
+ />
67
+ <ToggleGroupItem
68
+ value='option-2'
69
+ label='Option 2'
70
+ />
71
+ </ToggleGroup>
72
+ );
73
+
74
+ fireEvent.click(screen.getByRole('radio', { name: 'Option 2' }));
75
+
76
+ expect(onValueChange).toHaveBeenCalledWith('option-2');
77
+ });
78
+
79
+ test('does not select or fire onValueChange when disabled', () => {
80
+ const onValueChange = vi.fn();
81
+
82
+ wrap(
83
+ <ToggleGroup
84
+ defaultValue='option-1'
85
+ onValueChange={onValueChange}
86
+ >
87
+ <ToggleGroupItem
88
+ value='option-1'
89
+ label='Option 1'
90
+ />
91
+ <ToggleGroupItem
92
+ value='option-2'
93
+ label='Option 2'
94
+ disabled
95
+ />
96
+ </ToggleGroup>
97
+ );
98
+
99
+ const disabledItem = screen.getByRole('radio', { name: 'Option 2' });
100
+
101
+ expect(disabledItem).toBeDisabled();
102
+
103
+ fireEvent.click(disabledItem);
104
+
105
+ expect(onValueChange).not.toHaveBeenCalled();
106
+ expect(disabledItem).toHaveAttribute('aria-checked', 'false');
107
+ });
108
+
109
+ test('throws when rendered outside a ToggleGroup', () => {
110
+ const consoleError = vi
111
+ .spyOn(console, 'error')
112
+ .mockImplementation(() => {});
113
+
114
+ expect(() =>
115
+ render(
116
+ <ThemeContextProvider>
117
+ <ToggleGroupItem
118
+ value='option-1'
119
+ label='Option 1'
120
+ />
121
+ </ThemeContextProvider>
122
+ )
123
+ ).toThrow('ToggleGroupItem must be used within a ToggleGroup component');
124
+
125
+ consoleError.mockRestore();
126
+ });
127
+
128
+ test('renders the specified icon next to the label', () => {
129
+ wrap(
130
+ <ToggleGroup defaultValue='option-1'>
131
+ <ToggleGroupItem
132
+ value='option-1'
133
+ label='Option 1'
134
+ icon='BookOpen'
135
+ />
136
+ <ToggleGroupItem
137
+ value='option-2'
138
+ label='Option 2'
139
+ />
140
+ </ToggleGroup>
141
+ );
142
+
143
+ const withIcon = screen
144
+ .getByRole('radio', { name: 'Option 1' })
145
+ .closest('label');
146
+ const withoutIcon = screen
147
+ .getByRole('radio', { name: 'Option 2' })
148
+ .closest('label');
149
+
150
+ expect(withIcon!.querySelectorAll('svg').length).toBeGreaterThan(
151
+ withoutIcon!.querySelectorAll('svg').length
152
+ );
153
+ });
154
+
155
+ test('includes the sublabel in the accessible name', () => {
156
+ wrap(
157
+ <ToggleGroup defaultValue='option-1'>
158
+ <ToggleGroupItem
159
+ value='option-1'
160
+ label='Option 1'
161
+ sublabel='Sublabel 1'
162
+ />
163
+ </ToggleGroup>
164
+ );
165
+
166
+ expect(
167
+ screen.getByRole('radio', { name: 'Option 1 Sublabel 1' })
168
+ ).toBeInTheDocument();
169
+ });
170
+
171
+ test('only shows a visible checkmark once selected, without changing when it is reserved', () => {
172
+ wrap(
173
+ <ToggleGroup defaultValue='option-1'>
174
+ <ToggleGroupItem
175
+ value='option-1'
176
+ label='Option 1'
177
+ />
178
+ <ToggleGroupItem
179
+ value='option-2'
180
+ label='Option 2'
181
+ />
182
+ </ToggleGroup>
183
+ );
184
+
185
+ const selected = screen
186
+ .getByRole('radio', { name: 'Option 1' })
187
+ .closest('label');
188
+ const unselected = screen
189
+ .getByRole('radio', { name: 'Option 2' })
190
+ .closest('label');
191
+
192
+ expect(selected!.querySelectorAll('svg').length).toBe(2);
193
+ expect(unselected!.querySelectorAll('svg').length).toBe(1);
194
+ });
195
+ });
@@ -133,6 +133,9 @@ export type { CheckboxProps, LabelledCheckboxProps } from './Checkbox';
133
133
  export { default as Toggle } from './Toggle';
134
134
  export type { ToggleProps } from './Toggle';
135
135
 
136
+ export { default as ToggleGroup } from './ToggleGroup';
137
+ export type { ToggleGroupProps, ItemProps } from './ToggleGroup';
138
+
136
139
  export { default as Chip } from './Chip';
137
140
  export type { ChipProps } from './Chip';
138
141
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uikit-react-public",
3
3
  "private": false,
4
4
  "license": "UNLICENSED",
5
- "version": "0.49.4",
5
+ "version": "0.49.6",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
@@ -69,7 +69,7 @@
69
69
  "@typescript-eslint/parser": "^8.26.1",
70
70
  "@vitejs/plugin-basic-ssl": "^2.0.0",
71
71
  "@vitejs/plugin-react": "^4.3.4",
72
- "@vitest/coverage-v8": "^4.1.8",
72
+ "@vitest/coverage-v8": "^4.1.11",
73
73
  "eslint": "^9.22.0",
74
74
  "eslint-plugin-babel": "^5.3.1",
75
75
  "eslint-plugin-ft-flow": "^3.0.11",
@@ -94,7 +94,7 @@
94
94
  "typescript-eslint": "^8.26.1",
95
95
  "vite": "^6.2.1",
96
96
  "vite-plugin-dts": "^4.5.4",
97
- "vitest": "^4.1.8",
97
+ "vitest": "^4.1.11",
98
98
  "wouter": "^3.6.0"
99
99
  }
100
100
  }
@@ -1,8 +0,0 @@
1
- import { ReactNode, SVGProps } from 'react';
2
- interface SVGRProps {
3
- title?: string;
4
- titleId?: string;
5
- }
6
- type IconComponent = (props: SVGProps<SVGSVGElement> & SVGRProps) => ReactNode;
7
- declare const iconSvgs: Record<string, IconComponent>;
8
- export default iconSvgs;