uikit-react-public 0.49.3 → 0.49.5

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 (30) hide show
  1. package/dist/components/Select/index.d.ts +1 -1
  2. package/dist/components/ToggleGroup/ToggleGroup.d.ts +15 -0
  3. package/dist/components/ToggleGroup/ToggleGroup.stories.d.ts +18 -0
  4. package/dist/components/ToggleGroup/ToggleGroupContext.d.ts +8 -0
  5. package/dist/components/ToggleGroup/__tests__/ToggleGroup.test.d.ts +1 -0
  6. package/dist/components/ToggleGroup/index.d.ts +4 -0
  7. package/dist/components/ToggleGroup/subcomponents/Item.d.ts +11 -0
  8. package/dist/components/ToggleGroup/subcomponents/Item.stories.d.ts +331 -0
  9. package/dist/components/ToggleGroup/subcomponents/__tests__/Item.test.d.ts +1 -0
  10. package/dist/components/index.d.ts +2 -0
  11. package/dist/index.js +7108 -6874
  12. package/lib/components/ConfidentialDisplayField/Documentation.mdx +1 -1
  13. package/lib/components/ConfidentialDisplayField/__tests__/ConfidentialDisplayField.test.tsx +5 -5
  14. package/lib/components/ConfidentialInput/Documentation.mdx +1 -1
  15. package/lib/components/ConfidentialInput/__tests__/ConfidentialInput.test.tsx +11 -11
  16. package/lib/components/Icon/svgImports.ts +4 -2
  17. package/lib/components/Select/index.ts +5 -1
  18. package/lib/components/ToggleGroup/ToggleGroup.mdx +31 -0
  19. package/lib/components/ToggleGroup/ToggleGroup.stories.tsx +120 -0
  20. package/lib/components/ToggleGroup/ToggleGroup.tsx +144 -0
  21. package/lib/components/ToggleGroup/ToggleGroupContext.tsx +25 -0
  22. package/lib/components/ToggleGroup/__tests__/ToggleGroup.test.tsx +468 -0
  23. package/lib/components/ToggleGroup/index.ts +4 -0
  24. package/lib/components/ToggleGroup/subcomponents/Item.stories.tsx +63 -0
  25. package/lib/components/ToggleGroup/subcomponents/Item.tsx +233 -0
  26. package/lib/components/ToggleGroup/subcomponents/__tests__/Item.test.tsx +195 -0
  27. package/lib/components/common/formatConfidentialValue.ts +1 -1
  28. package/lib/components/index.ts +3 -0
  29. package/package.json +3 -3
  30. package/dist/components/Icon/svgImports.d.ts +0 -8
@@ -14,7 +14,7 @@ import {
14
14
 
15
15
  `ConfidentialDisplayField` extends `DisplayField` with the same simple mask
16
16
  format used by `ConfidentialInput`. `#` reveals a value character, `*` masks it
17
- with a `•`, and other characters are displayed as separators.
17
+ with a `●`, and other characters are displayed as separators.
18
18
 
19
19
  <Source
20
20
  code={`<ConfidentialDisplayField
@@ -23,7 +23,7 @@ describe('ConfidentialDisplayField', () => {
23
23
 
24
24
  expect(
25
25
  screen.getByTestId('ucl-uikit-confidential-display-field')
26
- ).toHaveTextContent('••••••78');
26
+ ).toHaveTextContent('●●●●●●78');
27
27
  });
28
28
 
29
29
  test('applies a function mask', () => {
@@ -34,7 +34,7 @@ describe('ConfidentialDisplayField', () => {
34
34
 
35
35
  expect(
36
36
  screen.getByTestId('ucl-uikit-confidential-display-field')
37
- ).toHaveTextContent('•••• ending in 78');
37
+ ).toHaveTextContent('●●●● ending in 78');
38
38
  });
39
39
 
40
40
  test('does not add a visibility control by default', () => {
@@ -69,7 +69,7 @@ describe('ConfidentialDisplayField', () => {
69
69
  await user.click(
70
70
  screen.getByRole('button', { name: 'Hide confidential value' })
71
71
  );
72
- expect(output).toHaveTextContent('••••••78');
72
+ expect(output).toHaveTextContent('●●●●●●78');
73
73
  });
74
74
 
75
75
  test('formats both masked and visible values', async () => {
@@ -82,7 +82,7 @@ describe('ConfidentialDisplayField', () => {
82
82
  });
83
83
 
84
84
  const output = screen.getByTestId('ucl-uikit-confidential-display-field');
85
- expect(output).toHaveTextContent('••-••-72');
85
+ expect(output).toHaveTextContent('●●-●●-72');
86
86
 
87
87
  await user.click(
88
88
  screen.getByRole('button', { name: 'Show confidential value' })
@@ -139,6 +139,6 @@ describe('ConfidentialDisplayField', () => {
139
139
 
140
140
  expect(
141
141
  screen.getByTestId('ucl-uikit-confidential-display-field')
142
- ).toHaveTextContent('••••••78');
142
+ ).toHaveTextContent('●●●●●●78');
143
143
  });
144
144
  });
@@ -20,7 +20,7 @@ change events, and form submission while displaying a masked value.
20
20
  ## Usage
21
21
 
22
22
  The string mask is a simple template. `#` reveals the corresponding value
23
- character, `*` masks it with a `•`, and all other characters are displayed as
23
+ character, `*` masks it with a `●`, and all other characters are displayed as
24
24
  separators. Asterisks returned by a formatter function are also displayed as
25
25
  bullets. Focusing the input temporarily reveals the original value for editing,
26
26
  and blurring restores the configured mask. When `showable` is `true`, the Eye
@@ -27,7 +27,7 @@ describe('ConfidentialInput', () => {
27
27
  );
28
28
  expect(
29
29
  screen.getByTestId('ucl-uikit-confidential-input-mask')
30
- ).toHaveTextContent('1234••••');
30
+ ).toHaveTextContent('1234●●●●');
31
31
  });
32
32
 
33
33
  test('supports separators and masks characters beyond the template', () => {
@@ -39,7 +39,7 @@ describe('ConfidentialInput', () => {
39
39
 
40
40
  expect(
41
41
  screen.getByTestId('ucl-uikit-confidential-input-mask')
42
- ).toHaveTextContent('••-••-72•');
42
+ ).toHaveTextContent('●●-●●-72●');
43
43
  });
44
44
 
45
45
  test('applies a function mask', () => {
@@ -51,7 +51,7 @@ describe('ConfidentialInput', () => {
51
51
 
52
52
  expect(
53
53
  screen.getByTestId('ucl-uikit-confidential-input-mask')
54
- ).toHaveTextContent('•••• ending in 78');
54
+ ).toHaveTextContent('●●●● ending in 78');
55
55
  });
56
56
 
57
57
  test('does not add a visibility control by default', () => {
@@ -120,7 +120,7 @@ describe('ConfidentialInput', () => {
120
120
 
121
121
  expect(
122
122
  screen.getByTestId('ucl-uikit-confidential-input-mask')
123
- ).toHaveTextContent('••••');
123
+ ).toHaveTextContent('●●●●');
124
124
  expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({}));
125
125
  expect(onChange.mock.lastCall?.[0].target.value).toBe('1234');
126
126
  });
@@ -189,7 +189,7 @@ describe('ConfidentialInput', () => {
189
189
  ) as HTMLInputElement;
190
190
  expect(
191
191
  screen.getByTestId('ucl-uikit-confidential-input-mask')
192
- ).toHaveTextContent('••-••-72');
192
+ ).toHaveTextContent('●●-●●-72');
193
193
 
194
194
  await user.click(input);
195
195
 
@@ -203,7 +203,7 @@ describe('ConfidentialInput', () => {
203
203
 
204
204
  expect(
205
205
  screen.getByTestId('ucl-uikit-confidential-input-mask')
206
- ).toHaveTextContent('••-••-72');
206
+ ).toHaveTextContent('●●-●●-72');
207
207
  });
208
208
 
209
209
  test('uses native password masking when visibleWhenFocused is false', async () => {
@@ -219,7 +219,7 @@ describe('ConfidentialInput', () => {
219
219
  ) as HTMLInputElement;
220
220
  expect(
221
221
  screen.getByTestId('ucl-uikit-confidential-input-mask')
222
- ).toHaveTextContent('••-••-72');
222
+ ).toHaveTextContent('●●-●●-72');
223
223
 
224
224
  await user.click(input);
225
225
 
@@ -234,7 +234,7 @@ describe('ConfidentialInput', () => {
234
234
 
235
235
  expect(
236
236
  screen.getByTestId('ucl-uikit-confidential-input-mask')
237
- ).toHaveTextContent('••-••-72');
237
+ ).toHaveTextContent('●●-●●-72');
238
238
  });
239
239
 
240
240
  test('shows the raw value while editing a showable value', async () => {
@@ -250,7 +250,7 @@ describe('ConfidentialInput', () => {
250
250
  ) as HTMLInputElement;
251
251
  expect(
252
252
  screen.getByTestId('ucl-uikit-confidential-input-mask')
253
- ).toHaveTextContent('••-••-72');
253
+ ).toHaveTextContent('●●-●●-72');
254
254
 
255
255
  await user.click(input);
256
256
 
@@ -268,7 +268,7 @@ describe('ConfidentialInput', () => {
268
268
 
269
269
  expect(
270
270
  screen.getByTestId('ucl-uikit-confidential-input-mask')
271
- ).toHaveTextContent('••-••-72');
271
+ ).toHaveTextContent('●●-●●-72');
272
272
  });
273
273
 
274
274
  test('supports a right icon alongside the visibility control', () => {
@@ -301,7 +301,7 @@ describe('ConfidentialInput', () => {
301
301
 
302
302
  expect(
303
303
  screen.getByTestId('ucl-uikit-confidential-input-mask')
304
- ).toHaveTextContent('••-••-72');
304
+ ).toHaveTextContent('●●-●●-72');
305
305
 
306
306
  await user.click(
307
307
  screen.getByRole('button', { name: 'Show confidential value' })
@@ -321,7 +321,9 @@ interface SVGRProps {
321
321
 
322
322
  type IconComponent = (props: SVGProps<SVGSVGElement> & SVGRProps) => ReactNode;
323
323
 
324
- const iconSvgs: Record<string, IconComponent> = {
324
+ // `satisfies` keeps literal key names (needed for icon name autocomplete/type-checking)
325
+ // while still validating every value matches IconComponent.
326
+ const iconSvgs = {
325
327
  Activity,
326
328
  Airplay,
327
329
  AlertCircle,
@@ -618,6 +620,6 @@ const iconSvgs: Record<string, IconComponent> = {
618
620
  Zap,
619
621
  ZoomIn,
620
622
  ZoomOut,
621
- };
623
+ } satisfies Record<string, IconComponent>;
622
624
 
623
625
  export default iconSvgs;
@@ -1,2 +1,6 @@
1
1
  export { default } from './Select';
2
- export type { SelectProps, OptionData } from './Select.types';
2
+ export type {
3
+ SelectProps,
4
+ NonClearableSelectProps,
5
+ OptionData,
6
+ } from './Select.types';
@@ -0,0 +1,31 @@
1
+ import * as ToggleGroupStories from "./ToggleGroup.stories";
2
+ import { Meta, Title, Subtitle, Canvas, Controls } from "@storybook/addon-docs/blocks";
3
+
4
+ <Meta of={ToggleGroupStories} />
5
+ <Title />
6
+ <Subtitle>A mutually-exclusive horizontal bar of radio buttons.</Subtitle>
7
+
8
+ Use a `<ToggleGroup>` when you want the user to switch between two or more views, modes, or states that sit side by side (e.g. list / grid, day / week / month).
9
+
10
+ <Canvas of={ToggleGroupStories.Default} sourceState="hidden" />
11
+
12
+ ## ToggleGroup.Item components
13
+
14
+ ### Icons
15
+ Add any `<Icon>` to the left of the `<Item>`.
16
+
17
+ <Canvas of={ToggleGroupStories.WithIcons} />
18
+
19
+ ### Sublabels
20
+ Add a `sublabel` to the `<Item>`.
21
+
22
+ <Canvas of={ToggleGroupStories.WithSublabels} />
23
+
24
+ ### Disabled
25
+ Disable a given `<Item>`.
26
+
27
+ <Canvas of={ToggleGroupStories.Disabled} />
28
+
29
+ ## Props
30
+
31
+ <Controls of={ToggleGroupStories.Default} />
@@ -0,0 +1,120 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+
3
+ import ToggleGroup from './ToggleGroup';
4
+
5
+ const meta = {
6
+ title: 'Components/ToggleGroup',
7
+ component: ToggleGroup,
8
+ parameters: {
9
+ layout: 'centered',
10
+ },
11
+ tags: ['autodocs'],
12
+ } satisfies Meta<typeof ToggleGroup>;
13
+
14
+ export default meta;
15
+
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const Default: Story = {
19
+ args: {
20
+ defaultValue: 'option-1',
21
+ children: (
22
+ <>
23
+ <ToggleGroup.Item
24
+ value='option-1'
25
+ label='Option 1'
26
+ />
27
+ <ToggleGroup.Item
28
+ value='option-2'
29
+ label='Option 2'
30
+ />
31
+ <ToggleGroup.Item
32
+ value='option-3'
33
+ label='Option 3'
34
+ />
35
+ </>
36
+ ),
37
+ },
38
+ };
39
+
40
+ export const WithIcons: Story = {
41
+ args: {
42
+ defaultValue: 'option-1',
43
+ children: (
44
+ <>
45
+ <ToggleGroup.Item
46
+ value='option-1'
47
+ label='Option 1'
48
+ icon='BookOpen'
49
+ />
50
+ <ToggleGroup.Item
51
+ value='option-2'
52
+ label='Option 2'
53
+ icon='Calendar'
54
+ />
55
+ <ToggleGroup.Item
56
+ value='option-3'
57
+ label='Option 3'
58
+ icon='Star'
59
+ />
60
+ <ToggleGroup.Item
61
+ value='option-4'
62
+ label='Option 4'
63
+ icon='User'
64
+ />
65
+ </>
66
+ ),
67
+ },
68
+ };
69
+
70
+ export const WithSublabels: Story = {
71
+ args: {
72
+ defaultValue: 'option-1',
73
+ children: (
74
+ <>
75
+ <ToggleGroup.Item
76
+ value='option-1'
77
+ label='Option 1'
78
+ sublabel='Sublabel 1'
79
+ />
80
+ <ToggleGroup.Item
81
+ value='option-2'
82
+ label='Option 2'
83
+ sublabel='Sublabel 2'
84
+ icon='User'
85
+ />
86
+ <ToggleGroup.Item
87
+ value='option-3'
88
+ label='Option 3'
89
+ sublabel='Sublabel 3'
90
+ />
91
+ </>
92
+ ),
93
+ },
94
+ };
95
+
96
+ export const Disabled: Story = {
97
+ args: {
98
+ defaultValue: 'option-1',
99
+ children: (
100
+ <>
101
+ <ToggleGroup.Item
102
+ value='option-1'
103
+ label='Option 1'
104
+ />
105
+ <ToggleGroup.Item
106
+ value='option-2'
107
+ label='Option 2'
108
+ icon='Calendar'
109
+ disabled
110
+ />
111
+ <ToggleGroup.Item
112
+ value='option-3'
113
+ label='Option 3'
114
+ sublabel='Sublabel 3'
115
+ disabled
116
+ />
117
+ </>
118
+ ),
119
+ },
120
+ };
@@ -0,0 +1,144 @@
1
+ import type React from 'react';
2
+ import {
3
+ Children,
4
+ isValidElement,
5
+ KeyboardEvent,
6
+ useId,
7
+ useState,
8
+ } from 'react';
9
+
10
+ import { css } from '@emotion/css';
11
+ import useTheme from '../../theme/useTheme';
12
+ import useMediaQuery from '../../hooks/useMediaQuery';
13
+ import Select, { NonClearableSelectProps } from '../Select';
14
+ import ToggleGroupContext from './ToggleGroupContext';
15
+ import Item, { ItemProps } from './subcomponents/Item';
16
+
17
+ export interface ToggleGroupProps extends React.HTMLAttributes<HTMLDivElement> {
18
+ children: React.ReactNode;
19
+ value?: string;
20
+ defaultValue?: string;
21
+ onValueChange?: (value: string) => void;
22
+ selectProps?: Omit<
23
+ NonClearableSelectProps<string>,
24
+ 'options' | 'value' | 'onValueChange'
25
+ >;
26
+ }
27
+
28
+ const MOBILE_SELECT_ITEM_THRESHOLD = 2;
29
+
30
+ const NAVIGATION_KEYS = ['ArrowRight', 'ArrowLeft', 'Home', 'End'];
31
+
32
+ const getEnabledItems = (container: HTMLElement) =>
33
+ Array.from(
34
+ container.querySelectorAll<HTMLInputElement>(
35
+ '[role="radio"]:not(:disabled)'
36
+ )
37
+ );
38
+
39
+ const getItemsFromChildren = (
40
+ children: React.ReactNode
41
+ ): React.ReactElement<ItemProps>[] =>
42
+ Children.toArray(children).flatMap((child) => {
43
+ if (!isValidElement<{ children?: React.ReactNode }>(child)) {
44
+ return [];
45
+ }
46
+ if (child.type === Item) {
47
+ return [child as React.ReactElement<ItemProps>];
48
+ }
49
+ return getItemsFromChildren(child.props.children);
50
+ });
51
+
52
+ const ToggleGroup = ({
53
+ children,
54
+ value,
55
+ defaultValue,
56
+ onValueChange,
57
+ selectProps,
58
+ }: ToggleGroupProps) => {
59
+ const [theme] = useTheme();
60
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
61
+ const isControlled = value !== undefined;
62
+ const selectedValue = value ?? uncontrolledValue;
63
+ const isMobile = !useMediaQuery(`(min-width: ${theme.breakpoints.tablet}px)`);
64
+ const items = getItemsFromChildren(children);
65
+ const name = useId();
66
+
67
+ const setValue = (nextValue: string) => {
68
+ if (!isControlled) {
69
+ setUncontrolledValue(nextValue);
70
+ }
71
+
72
+ onValueChange?.(nextValue);
73
+ };
74
+
75
+ const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
76
+ if (!NAVIGATION_KEYS.includes(event.key)) {
77
+ return;
78
+ }
79
+
80
+ const enabledItems = getEnabledItems(event.currentTarget);
81
+ const currentIndex = enabledItems.indexOf(
82
+ document.activeElement as HTMLInputElement
83
+ );
84
+
85
+ if (currentIndex === -1) {
86
+ return;
87
+ }
88
+
89
+ event.preventDefault();
90
+
91
+ const nextIndex =
92
+ {
93
+ ArrowRight: (currentIndex + 1) % enabledItems.length,
94
+ ArrowLeft:
95
+ (currentIndex - 1 + enabledItems.length) % enabledItems.length,
96
+ Home: 0,
97
+ End: enabledItems.length - 1,
98
+ }[event.key] ?? currentIndex;
99
+
100
+ const nextItem = enabledItems[nextIndex];
101
+ nextItem.focus();
102
+ setValue(nextItem.dataset.uclToggleGroupValue as string);
103
+ };
104
+
105
+ const toggleGroupStyle = css`
106
+ display: flex;
107
+ gap: -1px;
108
+ padding: ${theme.padding.p4};
109
+ background-color: ${theme.colour.fill.subtle};
110
+ border-radius: ${theme.radius.r4};
111
+ `;
112
+
113
+ if (isMobile && items.length > MOBILE_SELECT_ITEM_THRESHOLD) {
114
+ return (
115
+ <Select
116
+ {...selectProps}
117
+ options={items.map((item) => ({
118
+ label: item.props.label,
119
+ value: item.props.value,
120
+ }))}
121
+ value={selectedValue}
122
+ onValueChange={setValue}
123
+ />
124
+ );
125
+ }
126
+
127
+ return (
128
+ <ToggleGroupContext.Provider
129
+ value={{ value: selectedValue, setValue, name }}
130
+ >
131
+ <div
132
+ className={toggleGroupStyle}
133
+ role='radiogroup'
134
+ onKeyDown={handleKeyDown}
135
+ >
136
+ {children}
137
+ </div>
138
+ </ToggleGroupContext.Provider>
139
+ );
140
+ };
141
+
142
+ ToggleGroup.Item = Item;
143
+
144
+ export default ToggleGroup;
@@ -0,0 +1,25 @@
1
+ import { createContext, use } from 'react';
2
+
3
+ export interface ToggleGroupContextType {
4
+ value: string | undefined;
5
+ setValue: (value: string) => void;
6
+ name: string;
7
+ }
8
+
9
+ const ToggleGroupContext = createContext<ToggleGroupContextType | undefined>(
10
+ undefined
11
+ );
12
+
13
+ export const useToggleGroupContext = () => {
14
+ const context = use(ToggleGroupContext);
15
+
16
+ if (!context) {
17
+ throw new Error(
18
+ 'ToggleGroupItem must be used within a ToggleGroup component'
19
+ );
20
+ }
21
+
22
+ return context;
23
+ };
24
+
25
+ export default ToggleGroupContext;