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.
@@ -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;