@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.
Files changed (214) hide show
  1. package/build/cjs/index.js.map +1 -1
  2. package/build/esm/Accordion/AccordionBody.d.ts.map +1 -1
  3. package/build/esm/Accordion/AccordionBody.js +6 -26
  4. package/build/esm/Accordion/AccordionBody.js.map +1 -1
  5. package/build/esm/Accordion/AccordionHeader.d.ts.map +1 -1
  6. package/build/esm/Accordion/AccordionHeader.js +21 -69
  7. package/build/esm/Accordion/AccordionHeader.js.map +1 -1
  8. package/build/esm/Accordion/AccordionItem.d.ts.map +1 -1
  9. package/build/esm/Accordion/AccordionItem.js +31 -18
  10. package/build/esm/Accordion/AccordionItem.js.map +1 -1
  11. package/build/esm/Accordion/context.d.ts +0 -8
  12. package/build/esm/Accordion/context.d.ts.map +1 -1
  13. package/build/esm/Accordion/context.js +0 -11
  14. package/build/esm/Accordion/context.js.map +1 -1
  15. package/build/esm/Accordion/scopeQuery.d.ts +1 -0
  16. package/build/esm/Accordion/scopeQuery.d.ts.map +1 -1
  17. package/build/esm/Accordion/scopeQuery.js +3 -0
  18. package/build/esm/Accordion/scopeQuery.js.map +1 -1
  19. package/build/esm/Collapsible/Collapsible.d.ts +13 -0
  20. package/build/esm/Collapsible/Collapsible.d.ts.map +1 -0
  21. package/build/esm/Collapsible/Collapsible.js +53 -0
  22. package/build/esm/Collapsible/Collapsible.js.map +1 -0
  23. package/build/esm/Collapsible/CollapsiblePanel.d.ts +10 -0
  24. package/build/esm/Collapsible/CollapsiblePanel.d.ts.map +1 -0
  25. package/build/esm/Collapsible/CollapsiblePanel.js +85 -0
  26. package/build/esm/Collapsible/CollapsiblePanel.js.map +1 -0
  27. package/build/esm/Collapsible/CollapsibleTrigger.d.ts +11 -0
  28. package/build/esm/Collapsible/CollapsibleTrigger.d.ts.map +1 -0
  29. package/build/esm/Collapsible/CollapsibleTrigger.js +51 -0
  30. package/build/esm/Collapsible/CollapsibleTrigger.js.map +1 -0
  31. package/build/esm/Collapsible/context.d.ts +16 -0
  32. package/build/esm/Collapsible/context.d.ts.map +1 -0
  33. package/build/esm/Collapsible/context.js +11 -0
  34. package/build/esm/Collapsible/context.js.map +1 -0
  35. package/build/esm/Collapsible/index.d.ts +4 -0
  36. package/build/esm/Collapsible/index.d.ts.map +1 -0
  37. package/build/esm/Collapsible/index.js +4 -0
  38. package/build/esm/Collapsible/index.js.map +1 -0
  39. package/build/esm/Menu/Menu.d.ts +3 -2
  40. package/build/esm/Menu/Menu.d.ts.map +1 -1
  41. package/build/esm/Menu/Menu.js +64 -4
  42. package/build/esm/Menu/Menu.js.map +1 -1
  43. package/build/esm/Menu/MenuButton.d.ts.map +1 -1
  44. package/build/esm/Menu/MenuButton.js +85 -8
  45. package/build/esm/Menu/MenuButton.js.map +1 -1
  46. package/build/esm/Menu/MenuItem.d.ts.map +1 -1
  47. package/build/esm/Menu/MenuItem.js +16 -4
  48. package/build/esm/Menu/MenuItem.js.map +1 -1
  49. package/build/esm/Menu/MenuList.d.ts.map +1 -1
  50. package/build/esm/Menu/MenuList.js +47 -12
  51. package/build/esm/Menu/MenuList.js.map +1 -1
  52. package/build/esm/Menu/MenuPopover.d.ts.map +1 -1
  53. package/build/esm/Menu/MenuPopover.js +12 -1
  54. package/build/esm/Menu/MenuPopover.js.map +1 -1
  55. package/build/esm/Menu/MenuSubmenuTrigger.d.ts +8 -0
  56. package/build/esm/Menu/MenuSubmenuTrigger.d.ts.map +1 -0
  57. package/build/esm/Menu/MenuSubmenuTrigger.js +131 -0
  58. package/build/esm/Menu/MenuSubmenuTrigger.js.map +1 -0
  59. package/build/esm/Menu/context.d.ts +13 -3
  60. package/build/esm/Menu/context.d.ts.map +1 -1
  61. package/build/esm/Menu/context.js +1 -0
  62. package/build/esm/Menu/context.js.map +1 -1
  63. package/build/esm/Menu/index.d.ts +3 -0
  64. package/build/esm/Menu/index.d.ts.map +1 -1
  65. package/build/esm/Menu/index.js +2 -0
  66. package/build/esm/Menu/index.js.map +1 -1
  67. package/build/esm/Menu/scope.d.ts +1 -0
  68. package/build/esm/Menu/scope.d.ts.map +1 -1
  69. package/build/esm/Menu/scope.js +2 -1
  70. package/build/esm/Menu/scope.js.map +1 -1
  71. package/build/esm/MenuBar/MenuBar.d.ts +11 -0
  72. package/build/esm/MenuBar/MenuBar.d.ts.map +1 -0
  73. package/build/esm/MenuBar/MenuBar.js +153 -0
  74. package/build/esm/MenuBar/MenuBar.js.map +1 -0
  75. package/build/esm/MenuBar/context.d.ts +29 -0
  76. package/build/esm/MenuBar/context.d.ts.map +1 -0
  77. package/build/esm/MenuBar/context.js +7 -0
  78. package/build/esm/MenuBar/context.js.map +1 -0
  79. package/build/esm/MenuBar/index.d.ts +2 -0
  80. package/build/esm/MenuBar/index.d.ts.map +1 -0
  81. package/build/esm/MenuBar/index.js +2 -0
  82. package/build/esm/MenuBar/index.js.map +1 -0
  83. package/build/esm/Slider/Slider.d.ts +47 -1
  84. package/build/esm/Slider/Slider.d.ts.map +1 -1
  85. package/build/esm/Slider/Slider.js +91 -5
  86. package/build/esm/Slider/Slider.js.map +1 -1
  87. package/build/esm/ToggleGroup/ToggleGroup.d.ts +40 -0
  88. package/build/esm/ToggleGroup/ToggleGroup.d.ts.map +1 -0
  89. package/build/esm/ToggleGroup/ToggleGroup.js +113 -0
  90. package/build/esm/ToggleGroup/ToggleGroup.js.map +1 -0
  91. package/build/esm/ToggleGroup/ToggleGroupContext.d.ts +10 -0
  92. package/build/esm/ToggleGroup/ToggleGroupContext.d.ts.map +1 -0
  93. package/build/esm/ToggleGroup/ToggleGroupContext.js +6 -0
  94. package/build/esm/ToggleGroup/ToggleGroupContext.js.map +1 -0
  95. package/build/esm/ToggleGroup/index.d.ts +3 -0
  96. package/build/esm/ToggleGroup/index.d.ts.map +1 -0
  97. package/build/esm/ToggleGroup/index.js +3 -0
  98. package/build/esm/ToggleGroup/index.js.map +1 -0
  99. package/build/esm/Tree/Tree.d.ts +3 -0
  100. package/build/esm/Tree/Tree.d.ts.map +1 -0
  101. package/build/esm/Tree/Tree.js +730 -0
  102. package/build/esm/Tree/Tree.js.map +1 -0
  103. package/build/esm/Tree/TreeHeader.d.ts +3 -0
  104. package/build/esm/Tree/TreeHeader.d.ts.map +1 -0
  105. package/build/esm/Tree/TreeHeader.js +5 -0
  106. package/build/esm/Tree/TreeHeader.js.map +1 -0
  107. package/build/esm/Tree/TreeItem.d.ts +3 -0
  108. package/build/esm/Tree/TreeItem.d.ts.map +1 -0
  109. package/build/esm/Tree/TreeItem.js +5 -0
  110. package/build/esm/Tree/TreeItem.js.map +1 -0
  111. package/build/esm/Tree/TreeItemContent.d.ts +3 -0
  112. package/build/esm/Tree/TreeItemContent.d.ts.map +1 -0
  113. package/build/esm/Tree/TreeItemContent.js +69 -0
  114. package/build/esm/Tree/TreeItemContent.js.map +1 -0
  115. package/build/esm/Tree/TreeSection.d.ts +3 -0
  116. package/build/esm/Tree/TreeSection.d.ts.map +1 -0
  117. package/build/esm/Tree/TreeSection.js +5 -0
  118. package/build/esm/Tree/TreeSection.js.map +1 -0
  119. package/build/esm/Tree/collection.d.ts +18 -0
  120. package/build/esm/Tree/collection.d.ts.map +1 -0
  121. package/build/esm/Tree/collection.js +252 -0
  122. package/build/esm/Tree/collection.js.map +1 -0
  123. package/build/esm/Tree/context.d.ts +3 -0
  124. package/build/esm/Tree/context.d.ts.map +1 -0
  125. package/build/esm/Tree/context.js +3 -0
  126. package/build/esm/Tree/context.js.map +1 -0
  127. package/build/esm/Tree/index.d.ts +8 -0
  128. package/build/esm/Tree/index.d.ts.map +1 -0
  129. package/build/esm/Tree/index.js +7 -0
  130. package/build/esm/Tree/index.js.map +1 -0
  131. package/build/esm/Tree/types.d.ts +128 -0
  132. package/build/esm/Tree/types.d.ts.map +1 -0
  133. package/build/esm/Tree/types.js +2 -0
  134. package/build/esm/Tree/types.js.map +1 -0
  135. package/build/esm/hooks/index.d.ts +1 -0
  136. package/build/esm/hooks/index.d.ts.map +1 -1
  137. package/build/esm/hooks/index.js +1 -0
  138. package/build/esm/hooks/index.js.map +1 -1
  139. package/build/esm/hooks/useTransitionStatus.d.ts +7 -0
  140. package/build/esm/hooks/useTransitionStatus.d.ts.map +1 -0
  141. package/build/esm/hooks/useTransitionStatus.js +48 -0
  142. package/build/esm/hooks/useTransitionStatus.js.map +1 -0
  143. package/build/esm/index.d.ts +5 -0
  144. package/build/esm/index.d.ts.map +1 -1
  145. package/build/esm/index.js +5 -0
  146. package/build/esm/index.js.map +1 -1
  147. package/build/esm/toggle/Toggle.d.ts +28 -0
  148. package/build/esm/toggle/Toggle.d.ts.map +1 -0
  149. package/build/esm/toggle/Toggle.js +55 -0
  150. package/build/esm/toggle/Toggle.js.map +1 -0
  151. package/build/esm/toggle/index.d.ts +2 -0
  152. package/build/esm/toggle/index.d.ts.map +1 -0
  153. package/build/esm/toggle/index.js +2 -0
  154. package/build/esm/toggle/index.js.map +1 -0
  155. package/build/esm/utils/assign-ref.d.ts +3 -3
  156. package/build/esm/utils/assign-ref.d.ts.map +1 -1
  157. package/build/esm/utils/assign-ref.js +1 -1
  158. package/build/esm/utils/assign-ref.js.map +1 -1
  159. package/build/tsconfig-build.tsbuildinfo +1 -1
  160. package/build/tsconfig.tsbuildinfo +1 -1
  161. package/package.json +7 -4
  162. package/src/Accordion/AccordionBody.tsx +6 -35
  163. package/src/Accordion/AccordionHeader.tsx +29 -103
  164. package/src/Accordion/AccordionItem.tsx +40 -29
  165. package/src/Accordion/context.ts +0 -18
  166. package/src/Accordion/scopeQuery.ts +4 -0
  167. package/src/Collapsible/Collapsible.story.tsx +153 -0
  168. package/src/Collapsible/Collapsible.tsx +79 -0
  169. package/src/Collapsible/CollapsiblePanel.tsx +103 -0
  170. package/src/Collapsible/CollapsibleTrigger.tsx +60 -0
  171. package/src/Collapsible/context.ts +28 -0
  172. package/src/Collapsible/index.ts +3 -0
  173. package/src/Menu/Menu.story.tsx +70 -1
  174. package/src/Menu/Menu.tsx +141 -65
  175. package/src/Menu/MenuButton.tsx +115 -9
  176. package/src/Menu/MenuItem.tsx +20 -3
  177. package/src/Menu/MenuList.tsx +50 -13
  178. package/src/Menu/MenuPopover.tsx +12 -2
  179. package/src/Menu/MenuSubmenuTrigger.tsx +167 -0
  180. package/src/Menu/context.ts +20 -10
  181. package/src/Menu/index.ts +3 -0
  182. package/src/Menu/scope.ts +4 -1
  183. package/src/Menu/styles.css +57 -22
  184. package/src/MenuBar/MenuBar.story.tsx +92 -0
  185. package/src/MenuBar/MenuBar.tsx +236 -0
  186. package/src/MenuBar/context.ts +46 -0
  187. package/src/MenuBar/index.ts +1 -0
  188. package/src/MenuBar/styles.css +78 -0
  189. package/src/Slider/Slider.story.tsx +1 -1
  190. package/src/Slider/Slider.tsx +145 -8
  191. package/src/Toggle/Toggle.story.tsx +42 -0
  192. package/src/Toggle/Toggle.tsx +95 -0
  193. package/src/Toggle/index.ts +1 -0
  194. package/src/Toggle/styles.css +39 -0
  195. package/src/ToggleGroup/ToggleGroup.story.tsx +86 -0
  196. package/src/ToggleGroup/ToggleGroup.tsx +185 -0
  197. package/src/ToggleGroup/ToggleGroupContext.ts +17 -0
  198. package/src/ToggleGroup/index.ts +2 -0
  199. package/src/ToggleGroup/styles.css +66 -0
  200. package/src/Tree/Tree.story.tsx +221 -0
  201. package/src/Tree/Tree.tsx +1081 -0
  202. package/src/Tree/TreeHeader.tsx +9 -0
  203. package/src/Tree/TreeItem.tsx +9 -0
  204. package/src/Tree/TreeItemContent.tsx +91 -0
  205. package/src/Tree/TreeSection.tsx +9 -0
  206. package/src/Tree/collection.tsx +371 -0
  207. package/src/Tree/context.ts +6 -0
  208. package/src/Tree/index.ts +7 -0
  209. package/src/Tree/styles.css +135 -0
  210. package/src/Tree/types.ts +161 -0
  211. package/src/hooks/index.ts +1 -0
  212. package/src/hooks/useTransitionStatus.ts +65 -0
  213. package/src/index.ts +5 -0
  214. 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,2 @@
1
+ export * from './ToggleGroup';
2
+ export * from './ToggleGroupContext';
@@ -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
+ }