@basic-ui/core 0.0.60 → 0.0.62
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/build/cjs/index.js.map +1 -1
- package/build/esm/Accordion/AccordionBody.d.ts.map +1 -1
- package/build/esm/Accordion/AccordionBody.js +6 -26
- package/build/esm/Accordion/AccordionBody.js.map +1 -1
- package/build/esm/Accordion/AccordionHeader.d.ts.map +1 -1
- package/build/esm/Accordion/AccordionHeader.js +21 -69
- package/build/esm/Accordion/AccordionHeader.js.map +1 -1
- package/build/esm/Accordion/AccordionItem.d.ts.map +1 -1
- package/build/esm/Accordion/AccordionItem.js +31 -18
- package/build/esm/Accordion/AccordionItem.js.map +1 -1
- package/build/esm/Accordion/context.d.ts +0 -8
- package/build/esm/Accordion/context.d.ts.map +1 -1
- package/build/esm/Accordion/context.js +0 -11
- package/build/esm/Accordion/context.js.map +1 -1
- package/build/esm/Accordion/scopeQuery.d.ts +1 -0
- package/build/esm/Accordion/scopeQuery.d.ts.map +1 -1
- package/build/esm/Accordion/scopeQuery.js +3 -0
- package/build/esm/Accordion/scopeQuery.js.map +1 -1
- package/build/esm/Collapsible/Collapsible.d.ts +13 -0
- package/build/esm/Collapsible/Collapsible.d.ts.map +1 -0
- package/build/esm/Collapsible/Collapsible.js +53 -0
- package/build/esm/Collapsible/Collapsible.js.map +1 -0
- package/build/esm/Collapsible/CollapsiblePanel.d.ts +10 -0
- package/build/esm/Collapsible/CollapsiblePanel.d.ts.map +1 -0
- package/build/esm/Collapsible/CollapsiblePanel.js +85 -0
- package/build/esm/Collapsible/CollapsiblePanel.js.map +1 -0
- package/build/esm/Collapsible/CollapsibleTrigger.d.ts +11 -0
- package/build/esm/Collapsible/CollapsibleTrigger.d.ts.map +1 -0
- package/build/esm/Collapsible/CollapsibleTrigger.js +51 -0
- package/build/esm/Collapsible/CollapsibleTrigger.js.map +1 -0
- package/build/esm/Collapsible/context.d.ts +16 -0
- package/build/esm/Collapsible/context.d.ts.map +1 -0
- package/build/esm/Collapsible/context.js +11 -0
- package/build/esm/Collapsible/context.js.map +1 -0
- package/build/esm/Collapsible/index.d.ts +4 -0
- package/build/esm/Collapsible/index.d.ts.map +1 -0
- package/build/esm/Collapsible/index.js +4 -0
- package/build/esm/Collapsible/index.js.map +1 -0
- package/build/esm/Menu/Menu.d.ts +3 -2
- package/build/esm/Menu/Menu.d.ts.map +1 -1
- package/build/esm/Menu/Menu.js +64 -4
- package/build/esm/Menu/Menu.js.map +1 -1
- package/build/esm/Menu/MenuButton.d.ts.map +1 -1
- package/build/esm/Menu/MenuButton.js +85 -8
- package/build/esm/Menu/MenuButton.js.map +1 -1
- package/build/esm/Menu/MenuItem.d.ts.map +1 -1
- package/build/esm/Menu/MenuItem.js +16 -4
- package/build/esm/Menu/MenuItem.js.map +1 -1
- package/build/esm/Menu/MenuList.d.ts.map +1 -1
- package/build/esm/Menu/MenuList.js +47 -12
- package/build/esm/Menu/MenuList.js.map +1 -1
- package/build/esm/Menu/MenuPopover.d.ts.map +1 -1
- package/build/esm/Menu/MenuPopover.js +12 -1
- package/build/esm/Menu/MenuPopover.js.map +1 -1
- package/build/esm/Menu/MenuSubmenuTrigger.d.ts +8 -0
- package/build/esm/Menu/MenuSubmenuTrigger.d.ts.map +1 -0
- package/build/esm/Menu/MenuSubmenuTrigger.js +131 -0
- package/build/esm/Menu/MenuSubmenuTrigger.js.map +1 -0
- package/build/esm/Menu/context.d.ts +13 -3
- package/build/esm/Menu/context.d.ts.map +1 -1
- package/build/esm/Menu/context.js +1 -0
- package/build/esm/Menu/context.js.map +1 -1
- package/build/esm/Menu/index.d.ts +3 -0
- package/build/esm/Menu/index.d.ts.map +1 -1
- package/build/esm/Menu/index.js +2 -0
- package/build/esm/Menu/index.js.map +1 -1
- package/build/esm/Menu/scope.d.ts +1 -0
- package/build/esm/Menu/scope.d.ts.map +1 -1
- package/build/esm/Menu/scope.js +2 -1
- package/build/esm/Menu/scope.js.map +1 -1
- package/build/esm/MenuBar/MenuBar.d.ts +11 -0
- package/build/esm/MenuBar/MenuBar.d.ts.map +1 -0
- package/build/esm/MenuBar/MenuBar.js +153 -0
- package/build/esm/MenuBar/MenuBar.js.map +1 -0
- package/build/esm/MenuBar/context.d.ts +29 -0
- package/build/esm/MenuBar/context.d.ts.map +1 -0
- package/build/esm/MenuBar/context.js +7 -0
- package/build/esm/MenuBar/context.js.map +1 -0
- package/build/esm/MenuBar/index.d.ts +2 -0
- package/build/esm/MenuBar/index.d.ts.map +1 -0
- package/build/esm/MenuBar/index.js +2 -0
- package/build/esm/MenuBar/index.js.map +1 -0
- package/build/esm/Slider/Slider.d.ts +47 -1
- package/build/esm/Slider/Slider.d.ts.map +1 -1
- package/build/esm/Slider/Slider.js +91 -5
- package/build/esm/Slider/Slider.js.map +1 -1
- package/build/esm/ToggleGroup/ToggleGroup.d.ts +40 -0
- package/build/esm/ToggleGroup/ToggleGroup.d.ts.map +1 -0
- package/build/esm/ToggleGroup/ToggleGroup.js +113 -0
- package/build/esm/ToggleGroup/ToggleGroup.js.map +1 -0
- package/build/esm/ToggleGroup/ToggleGroupContext.d.ts +10 -0
- package/build/esm/ToggleGroup/ToggleGroupContext.d.ts.map +1 -0
- package/build/esm/ToggleGroup/ToggleGroupContext.js +6 -0
- package/build/esm/ToggleGroup/ToggleGroupContext.js.map +1 -0
- package/build/esm/ToggleGroup/index.d.ts +3 -0
- package/build/esm/ToggleGroup/index.d.ts.map +1 -0
- package/build/esm/ToggleGroup/index.js +3 -0
- package/build/esm/ToggleGroup/index.js.map +1 -0
- package/build/esm/Tree/Tree.d.ts +3 -0
- package/build/esm/Tree/Tree.d.ts.map +1 -0
- package/build/esm/Tree/Tree.js +730 -0
- package/build/esm/Tree/Tree.js.map +1 -0
- package/build/esm/Tree/TreeHeader.d.ts +3 -0
- package/build/esm/Tree/TreeHeader.d.ts.map +1 -0
- package/build/esm/Tree/TreeHeader.js +5 -0
- package/build/esm/Tree/TreeHeader.js.map +1 -0
- package/build/esm/Tree/TreeItem.d.ts +3 -0
- package/build/esm/Tree/TreeItem.d.ts.map +1 -0
- package/build/esm/Tree/TreeItem.js +5 -0
- package/build/esm/Tree/TreeItem.js.map +1 -0
- package/build/esm/Tree/TreeItemContent.d.ts +3 -0
- package/build/esm/Tree/TreeItemContent.d.ts.map +1 -0
- package/build/esm/Tree/TreeItemContent.js +69 -0
- package/build/esm/Tree/TreeItemContent.js.map +1 -0
- package/build/esm/Tree/TreeSection.d.ts +3 -0
- package/build/esm/Tree/TreeSection.d.ts.map +1 -0
- package/build/esm/Tree/TreeSection.js +5 -0
- package/build/esm/Tree/TreeSection.js.map +1 -0
- package/build/esm/Tree/collection.d.ts +18 -0
- package/build/esm/Tree/collection.d.ts.map +1 -0
- package/build/esm/Tree/collection.js +252 -0
- package/build/esm/Tree/collection.js.map +1 -0
- package/build/esm/Tree/context.d.ts +3 -0
- package/build/esm/Tree/context.d.ts.map +1 -0
- package/build/esm/Tree/context.js +3 -0
- package/build/esm/Tree/context.js.map +1 -0
- package/build/esm/Tree/index.d.ts +8 -0
- package/build/esm/Tree/index.d.ts.map +1 -0
- package/build/esm/Tree/index.js +7 -0
- package/build/esm/Tree/index.js.map +1 -0
- package/build/esm/Tree/types.d.ts +128 -0
- package/build/esm/Tree/types.d.ts.map +1 -0
- package/build/esm/Tree/types.js +2 -0
- package/build/esm/Tree/types.js.map +1 -0
- package/build/esm/hooks/index.d.ts +1 -0
- package/build/esm/hooks/index.d.ts.map +1 -1
- package/build/esm/hooks/index.js +1 -0
- package/build/esm/hooks/index.js.map +1 -1
- package/build/esm/hooks/useTransitionStatus.d.ts +7 -0
- package/build/esm/hooks/useTransitionStatus.d.ts.map +1 -0
- package/build/esm/hooks/useTransitionStatus.js +48 -0
- package/build/esm/hooks/useTransitionStatus.js.map +1 -0
- package/build/esm/index.d.ts +5 -0
- package/build/esm/index.d.ts.map +1 -1
- package/build/esm/index.js +5 -0
- package/build/esm/index.js.map +1 -1
- package/build/esm/toggle/Toggle.d.ts +28 -0
- package/build/esm/toggle/Toggle.d.ts.map +1 -0
- package/build/esm/toggle/Toggle.js +55 -0
- package/build/esm/toggle/Toggle.js.map +1 -0
- package/build/esm/toggle/index.d.ts +2 -0
- package/build/esm/toggle/index.d.ts.map +1 -0
- package/build/esm/toggle/index.js +2 -0
- package/build/esm/toggle/index.js.map +1 -0
- package/build/esm/utils/assign-ref.d.ts +3 -3
- package/build/esm/utils/assign-ref.d.ts.map +1 -1
- package/build/esm/utils/assign-ref.js +1 -1
- package/build/esm/utils/assign-ref.js.map +1 -1
- package/build/tsconfig-build.tsbuildinfo +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -4
- package/src/Accordion/AccordionBody.tsx +6 -35
- package/src/Accordion/AccordionHeader.tsx +29 -103
- package/src/Accordion/AccordionItem.tsx +40 -29
- package/src/Accordion/context.ts +0 -18
- package/src/Accordion/scopeQuery.ts +4 -0
- package/src/Collapsible/Collapsible.story.tsx +153 -0
- package/src/Collapsible/Collapsible.tsx +79 -0
- package/src/Collapsible/CollapsiblePanel.tsx +103 -0
- package/src/Collapsible/CollapsibleTrigger.tsx +60 -0
- package/src/Collapsible/context.ts +28 -0
- package/src/Collapsible/index.ts +3 -0
- package/src/Menu/Menu.story.tsx +70 -1
- package/src/Menu/Menu.tsx +141 -65
- package/src/Menu/MenuButton.tsx +115 -9
- package/src/Menu/MenuItem.tsx +20 -3
- package/src/Menu/MenuList.tsx +50 -13
- package/src/Menu/MenuPopover.tsx +12 -2
- package/src/Menu/MenuSubmenuTrigger.tsx +167 -0
- package/src/Menu/context.ts +20 -10
- package/src/Menu/index.ts +3 -0
- package/src/Menu/scope.ts +4 -1
- package/src/Menu/styles.css +57 -22
- package/src/MenuBar/MenuBar.story.tsx +92 -0
- package/src/MenuBar/MenuBar.tsx +236 -0
- package/src/MenuBar/context.ts +46 -0
- package/src/MenuBar/index.ts +1 -0
- package/src/MenuBar/styles.css +78 -0
- package/src/Slider/Slider.story.tsx +1 -1
- package/src/Slider/Slider.tsx +145 -8
- package/src/Toggle/Toggle.story.tsx +42 -0
- package/src/Toggle/Toggle.tsx +95 -0
- package/src/Toggle/index.ts +1 -0
- package/src/Toggle/styles.css +39 -0
- package/src/ToggleGroup/ToggleGroup.story.tsx +86 -0
- package/src/ToggleGroup/ToggleGroup.tsx +185 -0
- package/src/ToggleGroup/ToggleGroupContext.ts +17 -0
- package/src/ToggleGroup/index.ts +2 -0
- package/src/ToggleGroup/styles.css +66 -0
- package/src/Tree/Tree.story.tsx +221 -0
- package/src/Tree/Tree.tsx +1081 -0
- package/src/Tree/TreeHeader.tsx +9 -0
- package/src/Tree/TreeItem.tsx +9 -0
- package/src/Tree/TreeItemContent.tsx +91 -0
- package/src/Tree/TreeSection.tsx +9 -0
- package/src/Tree/collection.tsx +371 -0
- package/src/Tree/context.ts +6 -0
- package/src/Tree/index.ts +7 -0
- package/src/Tree/styles.css +135 -0
- package/src/Tree/types.ts +161 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useTransitionStatus.ts +65 -0
- package/src/index.ts +5 -0
- package/src/utils/assign-ref.ts +4 -4
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ElementType, ReactNode, ButtonHTMLAttributes } from 'react';
|
|
2
|
+
import { forwardRef, useId, useState, Fragment } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useToggleGroupContext } from '../ToggleGroup/ToggleGroupContext';
|
|
5
|
+
|
|
6
|
+
export interface ToggleProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
7
|
+
as?: ElementType<any>;
|
|
8
|
+
innerAs?: ElementType<any>;
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* Whether the toggle button is currently pressed.
|
|
12
|
+
* This is the controlled counterpart of `defaultPressed`.
|
|
13
|
+
*/
|
|
14
|
+
pressed?: boolean | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the toggle button is currently pressed.
|
|
17
|
+
* This is the uncontrolled counterpart of `pressed`.
|
|
18
|
+
* @default false
|
|
19
|
+
*/
|
|
20
|
+
defaultPressed?: boolean | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Callback fired when the pressed state is changed.
|
|
23
|
+
*/
|
|
24
|
+
onPressedChange?: ((pressed: boolean) => void) | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* A unique string that identifies the toggle when used
|
|
27
|
+
* inside a toggle group.
|
|
28
|
+
*/
|
|
29
|
+
value?: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Toggle = forwardRef<HTMLButtonElement, ToggleProps>(
|
|
33
|
+
function Toggle(props, forwardedRef) {
|
|
34
|
+
const {
|
|
35
|
+
as: Comp = 'button',
|
|
36
|
+
innerAs,
|
|
37
|
+
defaultPressed = false,
|
|
38
|
+
disabled = false,
|
|
39
|
+
onPressedChange: onPressedChangeProp,
|
|
40
|
+
pressed: pressedProp,
|
|
41
|
+
value: valueProp,
|
|
42
|
+
onClick: onClickProp,
|
|
43
|
+
...otherProps
|
|
44
|
+
} = props;
|
|
45
|
+
|
|
46
|
+
const useIdValue = useId();
|
|
47
|
+
const value = valueProp || useIdValue;
|
|
48
|
+
const groupContext = useToggleGroupContext();
|
|
49
|
+
const groupValue = groupContext?.value ?? [];
|
|
50
|
+
const isControlled = pressedProp !== undefined;
|
|
51
|
+
const [pressedState, setPressedState] = useState(defaultPressed);
|
|
52
|
+
|
|
53
|
+
const isDisabled = disabled || groupContext?.disabled || false;
|
|
54
|
+
|
|
55
|
+
// When in a group, the pressed state is derived from the group's value array
|
|
56
|
+
// When not in a group, use the passed pressed prop if controlled, otherwise internal state
|
|
57
|
+
const pressed = groupContext
|
|
58
|
+
? groupValue.includes(value)
|
|
59
|
+
: isControlled
|
|
60
|
+
? pressedProp
|
|
61
|
+
: pressedState;
|
|
62
|
+
|
|
63
|
+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
64
|
+
onClickProp?.(e);
|
|
65
|
+
|
|
66
|
+
if (isDisabled) return;
|
|
67
|
+
|
|
68
|
+
const nextPressed = !pressed;
|
|
69
|
+
|
|
70
|
+
onPressedChangeProp?.(nextPressed);
|
|
71
|
+
|
|
72
|
+
if (groupContext) {
|
|
73
|
+
groupContext.onValueChange?.(value, nextPressed);
|
|
74
|
+
} else if (!isControlled) {
|
|
75
|
+
setPressedState(nextPressed);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Comp
|
|
81
|
+
{...(Comp !== Fragment
|
|
82
|
+
? { as: innerAs, ref: forwardedRef }
|
|
83
|
+
: { ref: forwardedRef })}
|
|
84
|
+
type="button"
|
|
85
|
+
disabled={isDisabled}
|
|
86
|
+
aria-pressed={pressed}
|
|
87
|
+
onClick={handleClick}
|
|
88
|
+
data-toggle=""
|
|
89
|
+
data-pressed={pressed ? '' : undefined}
|
|
90
|
+
data-disabled={isDisabled ? '' : undefined}
|
|
91
|
+
{...otherProps}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Toggle';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
.toggle-demo {
|
|
2
|
+
padding: 20px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.toggle-demo button {
|
|
6
|
+
padding: 8px 16px;
|
|
7
|
+
border: 1px solid #ccc;
|
|
8
|
+
border-radius: 4px;
|
|
9
|
+
background-color: #f5f5f5;
|
|
10
|
+
cursor: pointer;
|
|
11
|
+
font-size: 14px;
|
|
12
|
+
font-weight: 500;
|
|
13
|
+
transition: all 150ms ease-in-out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.toggle-demo button:hover:not(:disabled) {
|
|
17
|
+
border-color: #999;
|
|
18
|
+
background-color: #efefef;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.toggle-demo button[data-pressed] {
|
|
22
|
+
background-color: #0066cc;
|
|
23
|
+
color: white;
|
|
24
|
+
border-color: #0052a3;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.toggle-demo button[data-pressed]:hover {
|
|
28
|
+
background-color: #0052a3;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.toggle-demo button[data-disabled] {
|
|
32
|
+
opacity: 0.5;
|
|
33
|
+
cursor: not-allowed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.toggle-demo button:focus-visible {
|
|
37
|
+
outline: 2px solid #0066cc;
|
|
38
|
+
outline-offset: 2px;
|
|
39
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ToggleGroup } from './ToggleGroup';
|
|
3
|
+
import { Toggle } from '../Toggle/Toggle';
|
|
4
|
+
import './styles.css';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'components/ToggleGroup',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const SingleSelection = () => (
|
|
11
|
+
<div className="toggle-group-demo">
|
|
12
|
+
<ToggleGroup defaultValue={['left']}>
|
|
13
|
+
<Toggle value="left">Left</Toggle>
|
|
14
|
+
<Toggle value="center">Center</Toggle>
|
|
15
|
+
<Toggle value="right">Right</Toggle>
|
|
16
|
+
</ToggleGroup>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export const MultipleSelection = () => (
|
|
21
|
+
<div className="toggle-group-demo">
|
|
22
|
+
<ToggleGroup multiple defaultValue={['bold']}>
|
|
23
|
+
<Toggle value="bold">Bold</Toggle>
|
|
24
|
+
<Toggle value="italic">Italic</Toggle>
|
|
25
|
+
<Toggle value="underline">Underline</Toggle>
|
|
26
|
+
</ToggleGroup>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const Controlled = () => {
|
|
31
|
+
const [value, setValue] = React.useState<string[]>(['sm']);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="toggle-group-demo">
|
|
35
|
+
<ToggleGroup value={value} onValueChange={setValue}>
|
|
36
|
+
<Toggle value="xs">XS</Toggle>
|
|
37
|
+
<Toggle value="sm">SM</Toggle>
|
|
38
|
+
<Toggle value="md">MD</Toggle>
|
|
39
|
+
<Toggle value="lg">LG</Toggle>
|
|
40
|
+
</ToggleGroup>
|
|
41
|
+
<p>Selected: {value.join(', ') || 'None'}</p>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Disabled = () => (
|
|
47
|
+
<div className="toggle-group-demo">
|
|
48
|
+
<ToggleGroup disabled defaultValue={['center']}>
|
|
49
|
+
<Toggle value="left">Left</Toggle>
|
|
50
|
+
<Toggle value="center">Center</Toggle>
|
|
51
|
+
<Toggle value="right">Right</Toggle>
|
|
52
|
+
</ToggleGroup>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export const Vertical = () => (
|
|
57
|
+
<div className="toggle-group-demo">
|
|
58
|
+
<ToggleGroup orientation="vertical" defaultValue={['all']}>
|
|
59
|
+
<Toggle value="all">All</Toggle>
|
|
60
|
+
<Toggle value="unread">Unread</Toggle>
|
|
61
|
+
<Toggle value="flagged">Flagged</Toggle>
|
|
62
|
+
</ToggleGroup>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
export const RequiredSelection = () => {
|
|
67
|
+
const [value, setValue] = React.useState<string[]>(['list']);
|
|
68
|
+
|
|
69
|
+
const handleValueChange = (newValue: string[]) => {
|
|
70
|
+
// Prevent deselecting all items - always keep at least one selected
|
|
71
|
+
if (newValue.length > 0) {
|
|
72
|
+
setValue(newValue);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="toggle-group-demo">
|
|
78
|
+
<ToggleGroup value={value} onValueChange={handleValueChange}>
|
|
79
|
+
<Toggle value="list">List</Toggle>
|
|
80
|
+
<Toggle value="grid">Grid</Toggle>
|
|
81
|
+
<Toggle value="compact">Compact</Toggle>
|
|
82
|
+
</ToggleGroup>
|
|
83
|
+
<p>Selected view: {value[0]} (at least one must be selected)</p>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { ElementType, ReactNode, HTMLAttributes, KeyboardEvent } from 'react';
|
|
2
|
+
import { forwardRef, useCallback, Fragment, useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useScope } from '../hooks';
|
|
5
|
+
import { getCircularIndex } from '../utils';
|
|
6
|
+
import { ToggleGroupContext } from './ToggleGroupContext';
|
|
7
|
+
|
|
8
|
+
export interface ToggleGroupProps extends HTMLAttributes<HTMLDivElement> {
|
|
9
|
+
as?: ElementType<any>;
|
|
10
|
+
innerAs?: ElementType<any>;
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* The pressed state of the toggle group represented by an array of
|
|
14
|
+
* the values of all pressed toggle buttons.
|
|
15
|
+
* This is the controlled counterpart of `defaultValue`.
|
|
16
|
+
*/
|
|
17
|
+
value?: readonly string[] | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* The pressed state of the toggle group represented by an array of
|
|
20
|
+
* the values of all pressed toggle buttons.
|
|
21
|
+
* This is the uncontrolled counterpart of `value`.
|
|
22
|
+
*/
|
|
23
|
+
defaultValue?: readonly string[] | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Callback fired when the pressed states of the toggle group changes.
|
|
26
|
+
*/
|
|
27
|
+
onValueChange?: ((value: string[]) => void) | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Whether the toggle group should ignore user interaction.
|
|
30
|
+
* @default false
|
|
31
|
+
*/
|
|
32
|
+
disabled?: boolean | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* @default 'horizontal'
|
|
35
|
+
*/
|
|
36
|
+
orientation?: 'horizontal' | 'vertical' | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* When `false` only one item in the group can be pressed. If any item in
|
|
39
|
+
* the group becomes pressed, the others will become unpressed.
|
|
40
|
+
* When `true` multiple items can be pressed.
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
43
|
+
multiple?: boolean | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const ToggleGroup = forwardRef<HTMLDivElement, ToggleGroupProps>(
|
|
47
|
+
function ToggleGroup(props, forwardedRef) {
|
|
48
|
+
const {
|
|
49
|
+
as: Comp = 'div',
|
|
50
|
+
innerAs,
|
|
51
|
+
defaultValue = [],
|
|
52
|
+
disabled = false,
|
|
53
|
+
onValueChange: onValueChangeProp,
|
|
54
|
+
orientation = 'horizontal',
|
|
55
|
+
multiple = false,
|
|
56
|
+
value: valueProp,
|
|
57
|
+
children,
|
|
58
|
+
onKeyDown: onKeyDownProp,
|
|
59
|
+
...otherProps
|
|
60
|
+
} = props;
|
|
61
|
+
|
|
62
|
+
const groupRef = useRef<HTMLDivElement>(null);
|
|
63
|
+
const scope = useScope<HTMLButtonElement, HTMLDivElement>(groupRef);
|
|
64
|
+
|
|
65
|
+
const [valueState, setValueState] = useState<string[]>(() => defaultValue as string[]);
|
|
66
|
+
const groupValue = (valueProp !== undefined ? valueProp : valueState) as string[];
|
|
67
|
+
|
|
68
|
+
const getAllButtons = useCallback(
|
|
69
|
+
() =>
|
|
70
|
+
scope.current.queryAllNodes(
|
|
71
|
+
(type, props) => type === 'button' && props['data-toggle'] !== undefined
|
|
72
|
+
),
|
|
73
|
+
[scope]
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const handleValueChange = useCallback(
|
|
77
|
+
(value: string, pressed: boolean) => {
|
|
78
|
+
let newGroupValue: string[];
|
|
79
|
+
|
|
80
|
+
if (multiple) {
|
|
81
|
+
newGroupValue = groupValue.slice();
|
|
82
|
+
if (pressed) {
|
|
83
|
+
if (!newGroupValue.includes(value)) {
|
|
84
|
+
newGroupValue.push(value);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
const index = newGroupValue.indexOf(value);
|
|
88
|
+
if (index > -1) {
|
|
89
|
+
newGroupValue.splice(index, 1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
newGroupValue = pressed ? [value] : [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setValueState(newGroupValue);
|
|
97
|
+
onValueChangeProp?.(newGroupValue);
|
|
98
|
+
},
|
|
99
|
+
[groupValue, multiple, setValueState, onValueChangeProp]
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
103
|
+
onKeyDownProp?.(e as any);
|
|
104
|
+
|
|
105
|
+
if (disabled) return;
|
|
106
|
+
|
|
107
|
+
const isHorizontal = orientation === 'horizontal';
|
|
108
|
+
const isArrowKey = (key: string) =>
|
|
109
|
+
(isHorizontal && ['ArrowLeft', 'ArrowRight'].includes(key)) ||
|
|
110
|
+
(!isHorizontal && ['ArrowUp', 'ArrowDown'].includes(key));
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
!isArrowKey(e.key) &&
|
|
114
|
+
e.key !== 'Home' &&
|
|
115
|
+
e.key !== 'End'
|
|
116
|
+
) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
|
|
122
|
+
const allButtons = getAllButtons();
|
|
123
|
+
if (allButtons.length === 0) return;
|
|
124
|
+
|
|
125
|
+
const currentButton = e.target as HTMLElement;
|
|
126
|
+
const currentIndex = allButtons.findIndex((btn) => btn === currentButton);
|
|
127
|
+
|
|
128
|
+
let nextIndex = currentIndex >= 0 ? currentIndex : 0;
|
|
129
|
+
|
|
130
|
+
switch (e.key) {
|
|
131
|
+
case 'ArrowLeft':
|
|
132
|
+
case 'ArrowUp':
|
|
133
|
+
nextIndex = getCircularIndex(nextIndex - 1, allButtons.length) ?? nextIndex;
|
|
134
|
+
break;
|
|
135
|
+
case 'ArrowRight':
|
|
136
|
+
case 'ArrowDown':
|
|
137
|
+
nextIndex = getCircularIndex(nextIndex + 1, allButtons.length) ?? nextIndex;
|
|
138
|
+
break;
|
|
139
|
+
case 'Home':
|
|
140
|
+
nextIndex = 0;
|
|
141
|
+
break;
|
|
142
|
+
case 'End':
|
|
143
|
+
nextIndex = allButtons.length - 1;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
allButtons[nextIndex].focus();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const contextValue = {
|
|
151
|
+
value: groupValue,
|
|
152
|
+
onValueChange: handleValueChange,
|
|
153
|
+
disabled,
|
|
154
|
+
multiple,
|
|
155
|
+
orientation,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const handleRef = (el: HTMLDivElement | null) => {
|
|
159
|
+
groupRef.current = el;
|
|
160
|
+
if (forwardedRef) {
|
|
161
|
+
if (typeof forwardedRef === 'function') {
|
|
162
|
+
forwardedRef(el);
|
|
163
|
+
} else {
|
|
164
|
+
forwardedRef.current = el;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<ToggleGroupContext.Provider value={contextValue}>
|
|
171
|
+
<Comp
|
|
172
|
+
{...(Comp !== Fragment ? { as: innerAs, ref: handleRef } : { ref: handleRef })}
|
|
173
|
+
role="group"
|
|
174
|
+
data-disabled={disabled ? '' : undefined}
|
|
175
|
+
data-orientation={orientation}
|
|
176
|
+
data-multiple={multiple ? '' : undefined}
|
|
177
|
+
onKeyDown={handleKeyDown}
|
|
178
|
+
{...otherProps}
|
|
179
|
+
>
|
|
180
|
+
{children}
|
|
181
|
+
</Comp>
|
|
182
|
+
</ToggleGroupContext.Provider>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ToggleGroupContextProps {
|
|
4
|
+
value: string[];
|
|
5
|
+
onValueChange?: (value: string, pressed: boolean) => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
multiple?: boolean;
|
|
8
|
+
orientation?: 'horizontal' | 'vertical';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ToggleGroupContext = createContext<ToggleGroupContextProps | undefined>(
|
|
12
|
+
undefined
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export function useToggleGroupContext() {
|
|
16
|
+
return useContext(ToggleGroupContext);
|
|
17
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
.toggle-group-demo {
|
|
2
|
+
padding: 20px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.toggle-group-demo [role="group"] {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
gap: 0;
|
|
8
|
+
border-radius: 4px;
|
|
9
|
+
border: 1px solid #ccc;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.toggle-group-demo [data-orientation="vertical"] {
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.toggle-group-demo [role="group"] button {
|
|
18
|
+
padding: 8px 16px;
|
|
19
|
+
border: none;
|
|
20
|
+
border-right: 1px solid #ccc;
|
|
21
|
+
background-color: #f5f5f5;
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
font-size: 14px;
|
|
24
|
+
font-weight: 500;
|
|
25
|
+
transition: all 150ms ease-in-out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.toggle-group-demo [data-orientation="vertical"] button {
|
|
29
|
+
border-right: none;
|
|
30
|
+
border-bottom: 1px solid #ccc;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.toggle-group-demo [role="group"] button:last-child {
|
|
34
|
+
border-right: none;
|
|
35
|
+
border-bottom: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.toggle-group-demo [role="group"] button:hover:not(:disabled) {
|
|
39
|
+
background-color: #efefef;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.toggle-group-demo [role="group"] button[data-pressed] {
|
|
43
|
+
background-color: #0066cc;
|
|
44
|
+
color: white;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.toggle-group-demo [role="group"] button[data-pressed]:hover {
|
|
48
|
+
background-color: #0052a3;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.toggle-group-demo [role="group"][data-disabled] button {
|
|
52
|
+
opacity: 0.5;
|
|
53
|
+
cursor: not-allowed;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.toggle-group-demo [role="group"] button:focus-visible {
|
|
57
|
+
outline: 2px solid #0066cc;
|
|
58
|
+
outline-offset: -2px;
|
|
59
|
+
z-index: 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.toggle-group-demo p {
|
|
63
|
+
margin-top: 16px;
|
|
64
|
+
font-size: 14px;
|
|
65
|
+
color: #666;
|
|
66
|
+
}
|