uikit-react-public 0.49.4 → 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.
- package/dist/components/Select/index.d.ts +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.d.ts +15 -0
- package/dist/components/ToggleGroup/ToggleGroup.stories.d.ts +18 -0
- package/dist/components/ToggleGroup/ToggleGroupContext.d.ts +8 -0
- package/dist/components/ToggleGroup/__tests__/ToggleGroup.test.d.ts +1 -0
- package/dist/components/ToggleGroup/index.d.ts +4 -0
- package/dist/components/ToggleGroup/subcomponents/Item.d.ts +11 -0
- package/dist/components/ToggleGroup/subcomponents/Item.stories.d.ts +331 -0
- package/dist/components/ToggleGroup/subcomponents/__tests__/Item.test.d.ts +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/index.js +7108 -6874
- package/lib/components/Icon/svgImports.ts +4 -2
- package/lib/components/Select/index.ts +5 -1
- package/lib/components/ToggleGroup/ToggleGroup.mdx +31 -0
- package/lib/components/ToggleGroup/ToggleGroup.stories.tsx +120 -0
- package/lib/components/ToggleGroup/ToggleGroup.tsx +144 -0
- package/lib/components/ToggleGroup/ToggleGroupContext.tsx +25 -0
- package/lib/components/ToggleGroup/__tests__/ToggleGroup.test.tsx +468 -0
- package/lib/components/ToggleGroup/index.ts +4 -0
- package/lib/components/ToggleGroup/subcomponents/Item.stories.tsx +63 -0
- package/lib/components/ToggleGroup/subcomponents/Item.tsx +233 -0
- package/lib/components/ToggleGroup/subcomponents/__tests__/Item.test.tsx +195 -0
- package/lib/components/index.ts +3 -0
- package/package.json +3 -3
- package/dist/components/Icon/svgImports.d.ts +0 -8
|
@@ -321,7 +321,9 @@ interface SVGRProps {
|
|
|
321
321
|
|
|
322
322
|
type IconComponent = (props: SVGProps<SVGSVGElement> & SVGRProps) => ReactNode;
|
|
323
323
|
|
|
324
|
-
|
|
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;
|
|
@@ -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;
|