@basic-ui/core 0.0.60 → 0.0.61

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,236 @@
1
+ import type { ElementType, HTMLAttributes } from 'react';
2
+ import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
3
+
4
+ import type {
5
+ MenuBarContextProps,
6
+ MenuBarOrientation,
7
+ MenuBarRegisteredMenu,
8
+ } from './context';
9
+ import { MenuBarProvider } from './context';
10
+
11
+ export interface MenuBarProps extends HTMLAttributes<HTMLDivElement> {
12
+ as?: ElementType<any>;
13
+ innerAs?: ElementType<any>;
14
+ disabled?: boolean;
15
+ loopFocus?: boolean;
16
+ orientation?: MenuBarOrientation;
17
+ }
18
+
19
+ type RegisteredButton = {
20
+ id: string;
21
+ element: HTMLElement;
22
+ disabled: boolean;
23
+ };
24
+
25
+ export const MenuBar = forwardRef<HTMLDivElement, MenuBarProps>(
26
+ function MenuBar(props, forwardedRef) {
27
+ const {
28
+ as: Comp = 'div',
29
+ innerAs,
30
+ disabled = false,
31
+ loopFocus = true,
32
+ orientation = 'horizontal',
33
+ ...otherProps
34
+ } = props;
35
+
36
+ const menusRef = useRef(new Map<string, MenuBarRegisteredMenu>());
37
+ const buttonsRef = useRef(new Map<string, RegisteredButton>());
38
+ const [activeMenuId, setActiveMenuId] = useState<string | null>(null);
39
+ const [openMenuIdState, setOpenMenuIdState] = useState<string | null>(null);
40
+
41
+ const getEnabledButtons = useCallback(() => {
42
+ if (disabled) {
43
+ return [];
44
+ }
45
+
46
+ return Array.from(buttonsRef.current.values()).filter(
47
+ (button) => !button.disabled && button.element.isConnected
48
+ );
49
+ }, [disabled]);
50
+
51
+ const syncActiveMenuId = useCallback(() => {
52
+ setActiveMenuId((currentId) => {
53
+ const current = currentId ? buttonsRef.current.get(currentId) : null;
54
+ if (current && !current.disabled && current.element.isConnected) {
55
+ return currentId;
56
+ }
57
+
58
+ return getEnabledButtons()[0]?.id ?? null;
59
+ });
60
+ }, [getEnabledButtons]);
61
+
62
+ const registerButton = useCallback<MenuBarContextProps['registerButton']>(
63
+ (id, element, buttonDisabled) => {
64
+ if (element) {
65
+ buttonsRef.current.set(id, {
66
+ id,
67
+ element,
68
+ disabled: buttonDisabled,
69
+ });
70
+ } else {
71
+ buttonsRef.current.delete(id);
72
+ }
73
+
74
+ syncActiveMenuId();
75
+ },
76
+ [syncActiveMenuId]
77
+ );
78
+
79
+ const registerMenu = useCallback<MenuBarContextProps['registerMenu']>(
80
+ (menu) => {
81
+ menusRef.current.set(menu.id, menu);
82
+
83
+ return () => {
84
+ menusRef.current.delete(menu.id);
85
+ };
86
+ },
87
+ []
88
+ );
89
+
90
+ const notifyMenuOpenChange = useCallback<
91
+ MenuBarContextProps['notifyMenuOpenChange']
92
+ >((id, isOpen) => {
93
+ setOpenMenuIdState((currentId) => {
94
+ if (isOpen) {
95
+ return id;
96
+ }
97
+
98
+ return currentId === id ? null : currentId;
99
+ });
100
+ }, []);
101
+
102
+ const focusMenu = useCallback((id: string) => {
103
+ const button = buttonsRef.current.get(id);
104
+ if (!button || button.disabled) {
105
+ return;
106
+ }
107
+
108
+ setActiveMenuId(id);
109
+ button.element.focus({ preventScroll: true });
110
+ }, []);
111
+
112
+ const openMenu = useCallback<MenuBarContextProps['openMenu']>(
113
+ (id, e, focusKey = null) => {
114
+ if (disabled) {
115
+ return;
116
+ }
117
+
118
+ if (openMenuIdState && openMenuIdState !== id) {
119
+ menusRef.current.get(openMenuIdState)?.onOpenChange(e, false);
120
+ }
121
+
122
+ const menu = menusRef.current.get(id);
123
+ if (!menu) {
124
+ return;
125
+ }
126
+
127
+ menu.openWithArrowKeyRef.current = focusKey;
128
+ menu.onOpenChange(e, true);
129
+ setActiveMenuId(id);
130
+ setOpenMenuIdState(id);
131
+ },
132
+ [disabled, openMenuIdState]
133
+ );
134
+
135
+ const closeMenu = useCallback<MenuBarContextProps['closeMenu']>((id, e) => {
136
+ menusRef.current.get(id)?.onOpenChange(e, false);
137
+ setOpenMenuIdState((currentId) => (currentId === id ? null : currentId));
138
+ }, []);
139
+
140
+ const moveFocus = useCallback<MenuBarContextProps['moveFocus']>(
141
+ (id, delta, e, options) => {
142
+ const buttons = getEnabledButtons();
143
+ if (buttons.length === 0) {
144
+ return;
145
+ }
146
+
147
+ const currentIndex = Math.max(
148
+ 0,
149
+ buttons.findIndex((button) => button.id === id)
150
+ );
151
+ let nextIndex = currentIndex + delta;
152
+
153
+ if (loopFocus) {
154
+ nextIndex = (nextIndex + buttons.length) % buttons.length;
155
+ } else {
156
+ nextIndex = Math.max(0, Math.min(nextIndex, buttons.length - 1));
157
+ }
158
+
159
+ const nextButton = buttons[nextIndex];
160
+ if (!nextButton || nextButton.id === id) {
161
+ return;
162
+ }
163
+
164
+ focusMenu(nextButton.id);
165
+
166
+ if (options?.open) {
167
+ openMenu(nextButton.id, e, options.focusKey ?? 'ArrowDown');
168
+ }
169
+ },
170
+ [focusMenu, getEnabledButtons, loopFocus, openMenu]
171
+ );
172
+
173
+ const focusFirstMenu = useCallback(() => {
174
+ const firstButton = getEnabledButtons()[0];
175
+ if (firstButton) {
176
+ focusMenu(firstButton.id);
177
+ }
178
+ }, [focusMenu, getEnabledButtons]);
179
+
180
+ const focusLastMenu = useCallback(() => {
181
+ const buttons = getEnabledButtons();
182
+ const lastButton = buttons[buttons.length - 1];
183
+ if (lastButton) {
184
+ focusMenu(lastButton.id);
185
+ }
186
+ }, [focusMenu, getEnabledButtons]);
187
+
188
+ const value = useMemo<MenuBarContextProps>(
189
+ () => ({
190
+ activeMenuId,
191
+ disabled,
192
+ openMenuId: openMenuIdState,
193
+ orientation,
194
+ closeMenu,
195
+ focusFirstMenu,
196
+ focusLastMenu,
197
+ moveFocus,
198
+ notifyMenuOpenChange,
199
+ openMenu,
200
+ registerButton,
201
+ registerMenu,
202
+ setActiveMenuId,
203
+ }),
204
+ [
205
+ activeMenuId,
206
+ closeMenu,
207
+ disabled,
208
+ focusFirstMenu,
209
+ focusLastMenu,
210
+ moveFocus,
211
+ notifyMenuOpenChange,
212
+ openMenu,
213
+ openMenuIdState,
214
+ orientation,
215
+ registerButton,
216
+ registerMenu,
217
+ ]
218
+ );
219
+
220
+ return (
221
+ <MenuBarProvider value={value}>
222
+ <Comp
223
+ ref={forwardedRef}
224
+ as={innerAs}
225
+ role="menubar"
226
+ aria-orientation={orientation}
227
+ data-menubar=""
228
+ data-orientation={orientation}
229
+ data-disabled={disabled ? '' : undefined}
230
+ data-has-submenu-open={openMenuIdState ? '' : undefined}
231
+ {...otherProps}
232
+ />
233
+ </MenuBarProvider>
234
+ );
235
+ }
236
+ );
@@ -0,0 +1,46 @@
1
+ import type { MutableRefObject } from 'react';
2
+ import { createContext, useContext } from 'react';
3
+
4
+ import type { MenuTriggerEvent } from '../Menu/context';
5
+
6
+ export type MenuBarOrientation = 'horizontal' | 'vertical';
7
+
8
+ export interface MenuBarRegisteredMenu {
9
+ id: string;
10
+ openWithArrowKeyRef: MutableRefObject<string | null>;
11
+ onOpenChange: (e: MenuTriggerEvent, isOpen: boolean) => void;
12
+ }
13
+
14
+ export interface MenuBarContextProps {
15
+ activeMenuId: string | null;
16
+ disabled: boolean;
17
+ openMenuId: string | null;
18
+ orientation: MenuBarOrientation;
19
+ closeMenu: (id: string, e: MenuTriggerEvent) => void;
20
+ focusFirstMenu: () => void;
21
+ focusLastMenu: () => void;
22
+ moveFocus: (
23
+ id: string,
24
+ delta: 1 | -1,
25
+ e: MenuTriggerEvent,
26
+ options?: { open?: boolean; focusKey?: 'ArrowUp' | 'ArrowDown' | null }
27
+ ) => void;
28
+ notifyMenuOpenChange: (id: string, isOpen: boolean) => void;
29
+ openMenu: (
30
+ id: string,
31
+ e: MenuTriggerEvent,
32
+ focusKey?: 'ArrowUp' | 'ArrowDown' | null
33
+ ) => void;
34
+ registerButton: (
35
+ id: string,
36
+ element: HTMLElement | null,
37
+ disabled: boolean
38
+ ) => void;
39
+ registerMenu: (menu: MenuBarRegisteredMenu) => () => void;
40
+ setActiveMenuId: (id: string | null) => void;
41
+ }
42
+
43
+ const menuBarContext = createContext<MenuBarContextProps | null>(null);
44
+
45
+ export const { Provider: MenuBarProvider } = menuBarContext;
46
+ export const useOptionalMenuBarContext = () => useContext(menuBarContext);
@@ -0,0 +1 @@
1
+ export * from './MenuBar';
@@ -0,0 +1,78 @@
1
+ .MenuBarStory-frame {
2
+ min-height: 360px;
3
+ padding: 48px;
4
+ font-family: sans-serif;
5
+ }
6
+
7
+ .MenuBarStory-frame [data-menubar] {
8
+ display: flex;
9
+ align-items: center;
10
+ width: max-content;
11
+ border: 1px solid #d8d8d8;
12
+ background: white;
13
+ }
14
+
15
+ .MenuBarStory-frame [data-menubar-menu-button] {
16
+ box-sizing: border-box;
17
+ height: 2rem;
18
+ margin: 0;
19
+ padding: 0 0.75rem;
20
+ border: 0;
21
+ background: transparent;
22
+ color: #1f1f1f;
23
+ font: inherit;
24
+ cursor: default;
25
+ user-select: none;
26
+ }
27
+
28
+ .MenuBarStory-frame [data-menubar-menu-button]:hover:not([disabled]),
29
+ .MenuBarStory-frame [data-menubar-menu-button]:focus-visible,
30
+ .MenuBarStory-frame [data-menubar-menu-button][aria-expanded='true'] {
31
+ background: #f0f0f0;
32
+ }
33
+
34
+ .MenuBarStory-frame [data-popper-placement] {
35
+ outline: 0;
36
+ }
37
+
38
+ .MenuBarStory-frame [data-menu-list] {
39
+ min-width: 180px;
40
+ margin: 0;
41
+ padding: 4px 0;
42
+ border: 1px solid #d8d8d8;
43
+ background: white;
44
+ color: #1f1f1f;
45
+ box-shadow: 4px 4px 0 rgb(0 0 0 / 10%);
46
+ list-style: none;
47
+ }
48
+
49
+ .MenuBarStory-frame [data-menu-item],
50
+ .MenuBarStory-frame [data-menu-submenu-trigger] {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 1rem;
54
+ padding: 0.5rem 1rem;
55
+ outline: 0;
56
+ cursor: default;
57
+ user-select: none;
58
+ }
59
+
60
+ .MenuBarStory-frame [data-menu-submenu-trigger] {
61
+ padding-right: 0.5rem;
62
+ }
63
+
64
+ .MenuBarStory-frame [data-menu-submenu-trigger] > span:first-child {
65
+ margin-right: auto;
66
+ }
67
+
68
+ .MenuBarStory-frame [data-menu-item][data-highlighted],
69
+ .MenuBarStory-frame [data-menu-submenu-trigger][data-highlighted] {
70
+ background: #1f1f1f;
71
+ color: white;
72
+ }
73
+
74
+ .MenuBarStory-frame [role='separator'] {
75
+ height: 1px;
76
+ margin: 4px;
77
+ background: #d8d8d8;
78
+ }
@@ -7,7 +7,7 @@ export default { title: 'components/Slider' };
7
7
  export function Basic() {
8
8
  return (
9
9
  <div>
10
- <Slider id="gee-whiz">
10
+ <Slider id="gee-whiz" resetValue={50}>
11
11
  <SliderMarker value={10}>
12
12
  <span>10</span>
13
13
  </SliderMarker>
@@ -49,8 +49,9 @@ const noop = () => {
49
49
  type SliderOrientation = 'horizontal' | 'vertical';
50
50
  type SliderHandleAlignment = 'center' | 'contain';
51
51
 
52
- const [SliderProvider, useSliderContext] =
53
- createContext<ISliderContext>('Slider');
52
+ const _sliderCtx = createContext<ISliderContext>('Slider');
53
+ const SliderProvider = _sliderCtx[0];
54
+ export const useSliderContext = _sliderCtx[1];
54
55
 
55
56
  ////////////////////////////////////////////////////////////////////////////////
56
57
 
@@ -92,6 +93,10 @@ interface SliderProps {
92
93
  * @see Docs https://reach.tech/slider#slider-defaultvalue
93
94
  */
94
95
  defaultValue?: number;
96
+ /**
97
+ * Sets the value that the filled range starts from. Defaults to `min`.
98
+ */
99
+ fillOffset?: number;
95
100
  /**
96
101
  * @see Docs https://reach.tech/slider#slider-disabled
97
102
  */
@@ -102,6 +107,11 @@ interface SliderProps {
102
107
  * @see Docs https://reach.tech/slider#slider-value
103
108
  */
104
109
  value?: number;
110
+ /**
111
+ * When set, double clicking or double tapping the slider handle will reset
112
+ * the value.
113
+ */
114
+ resetValue?: number;
105
115
  /**
106
116
  * A function used to set a human-readable name for the slider.
107
117
  *
@@ -209,6 +219,8 @@ const SliderInput = forwardRef(function SliderInput(
209
219
  innerAs,
210
220
  defaultValue,
211
221
  disabled = false,
222
+ fillOffset,
223
+ resetValue,
212
224
  value: controlledValue,
213
225
  getAriaLabel,
214
226
  getAriaValueText,
@@ -257,6 +269,16 @@ const SliderInput = forwardRef(function SliderInput(
257
269
  );
258
270
  const value = clamp(_value, min, max);
259
271
  const trackPercent = valueToPercent(value, min, max);
272
+ const hasFillOffset = fillOffset !== undefined;
273
+ const fillOffsetPercent = valueToPercent(fillOffset ?? min, min, max);
274
+ const fillStartPercent = Math.min(trackPercent, fillOffsetPercent);
275
+ const fillEndPercent = Math.max(trackPercent, fillOffsetPercent);
276
+ const fillDirection =
277
+ !hasFillOffset || trackPercent === fillOffsetPercent
278
+ ? 'none'
279
+ : trackPercent > fillOffsetPercent
280
+ ? 'right'
281
+ : 'left';
260
282
  const isVertical = orientation === 'vertical';
261
283
 
262
284
  const handleSize = isVertical
@@ -343,7 +365,10 @@ const SliderInput = forwardRef(function SliderInput(
343
365
  ? getAriaValueText(value)
344
366
  : ariaValueTextProp;
345
367
 
346
- const rangeStyle = { [isVertical ? 'height' : 'width']: `${trackPercent}%` };
368
+ const rangeStyle = {
369
+ [isVertical ? 'bottom' : 'left']: `${fillStartPercent}%`,
370
+ [isVertical ? 'height' : 'width']: `${fillEndPercent - fillStartPercent}%`,
371
+ };
347
372
 
348
373
  // Slide events!
349
374
  // We will try to use pointer events if they are supported to leverage
@@ -577,8 +602,14 @@ const SliderInput = forwardRef(function SliderInput(
577
602
  handlePosition={handlePosition}
578
603
  handleRef={handleRef}
579
604
  hasFocus={hasFocus}
605
+ fillDirection={fillDirection}
606
+ fillEndPercent={fillEndPercent}
607
+ fillStartPercent={fillStartPercent}
580
608
  onKeyDown={onKeyDown}
581
609
  setHasFocus={setHasFocus}
610
+ resetValue={
611
+ resetValue === undefined ? undefined : clamp(resetValue, min, max)
612
+ }
582
613
  sliderId={id}
583
614
  sliderMax={max}
584
615
  sliderMin={min}
@@ -609,6 +640,10 @@ const SliderInput = forwardRef(function SliderInput(
609
640
  min,
610
641
  value,
611
642
  ariaValueText,
643
+ fillEndPercent,
644
+ fillOffsetPercent,
645
+ fillStartPercent,
646
+ valuePercent: trackPercent,
612
647
  })
613
648
  : children}
614
649
  {name && (
@@ -700,7 +735,8 @@ const SliderRangeImpl = forwardRef(function SliderRange(
700
735
  { as: Comp = 'div', innerAs, children, style = {}, ...props },
701
736
  forwardedRef
702
737
  ) {
703
- const { disabled, orientation, rangeStyle } = useSliderContext('SliderRange');
738
+ const { disabled, fillDirection, orientation, rangeStyle } =
739
+ useSliderContext('SliderRange');
704
740
  return (
705
741
  <Comp
706
742
  ref={forwardedRef}
@@ -710,6 +746,7 @@ const SliderRangeImpl = forwardRef(function SliderRange(
710
746
  {...props}
711
747
  data-reach-slider-range=""
712
748
  data-disabled={disabled ? '' : undefined}
749
+ data-fill-direction={fillDirection}
713
750
  data-orientation={orientation}
714
751
  />
715
752
  );
@@ -762,12 +799,92 @@ const SliderHandleImpl = forwardRef(function SliderHandle(
762
799
  isVertical,
763
800
  handleKeyDown,
764
801
  orientation,
802
+ resetValue,
765
803
  setHasFocus,
766
804
  sliderMin,
767
805
  sliderMax,
806
+ updateValue,
768
807
  value,
769
808
  } = useSliderContext('SliderHandle');
770
809
 
810
+ const lastTouchEndRef = useRef<{
811
+ time: number;
812
+ x: number;
813
+ y: number;
814
+ } | null>(null);
815
+ const resetHandleValue = useStableLayoutCallback(
816
+ (event: MouseEvent | TouchEvent) => {
817
+ if (!disabled && resetValue !== undefined) {
818
+ updateValue(event, resetValue);
819
+ }
820
+ }
821
+ );
822
+
823
+ const handleResetTouchEnd = useStableLayoutCallback((event: TouchEvent) => {
824
+ if (disabled || resetValue === undefined) {
825
+ return;
826
+ }
827
+
828
+ const touch = event.changedTouches[0];
829
+ if (!touch) {
830
+ return;
831
+ }
832
+
833
+ const now = event.timeStamp;
834
+ const lastTouchEnd = lastTouchEndRef.current;
835
+
836
+ if (
837
+ lastTouchEnd &&
838
+ now - lastTouchEnd.time <= 300 &&
839
+ Math.abs(touch.clientX - lastTouchEnd.x) <= 20 &&
840
+ Math.abs(touch.clientY - lastTouchEnd.y) <= 20
841
+ ) {
842
+ event.stopPropagation();
843
+ event.stopImmediatePropagation();
844
+ lastTouchEndRef.current = null;
845
+ updateValue(event, resetValue);
846
+ event.preventDefault();
847
+ return;
848
+ }
849
+
850
+ lastTouchEndRef.current = {
851
+ time: now,
852
+ x: touch.clientX,
853
+ y: touch.clientY,
854
+ };
855
+ });
856
+
857
+ useEffect(() => {
858
+ const handle = handleRef.current;
859
+ if (!handle) {
860
+ return noop;
861
+ }
862
+
863
+ const mouseDownListener = (event: MouseEvent) => {
864
+ if (event.detail < 2) {
865
+ return;
866
+ }
867
+
868
+ event.stopPropagation();
869
+ event.stopImmediatePropagation();
870
+ resetHandleValue(event);
871
+ event.preventDefault();
872
+ };
873
+ const doubleClickListener = (event: MouseEvent) => {
874
+ resetHandleValue(event);
875
+ };
876
+
877
+ handle.addEventListener('mousedown', mouseDownListener);
878
+ handle.addEventListener('dblclick', doubleClickListener);
879
+ handle.addEventListener('touchend', handleResetTouchEnd);
880
+
881
+ return () => {
882
+ handle.removeEventListener('mousedown', mouseDownListener);
883
+ handle.removeEventListener('dblclick', doubleClickListener);
884
+ handle.removeEventListener('touchend', handleResetTouchEnd);
885
+ };
886
+ }, [handleRef, handleResetTouchEnd, resetHandleValue]);
887
+
771
888
  return (
772
889
  <Comp
773
890
  // @ts-ignore
@@ -860,6 +977,8 @@ const SliderMarkerImpl = forwardRef(function SliderMarker(
860
977
  sliderMin,
861
978
  sliderMax,
862
979
  value: sliderValue,
980
+ fillEndPercent,
981
+ fillStartPercent,
863
982
  } = useSliderContext('SliderMarker');
864
983
 
865
984
  const inRange = value >= sliderMin && value <= sliderMax;
@@ -868,13 +987,18 @@ const SliderMarkerImpl = forwardRef(function SliderMarker(
868
987
  sliderMin,
869
988
  sliderMax
870
989
  )}%`;
990
+ const markerPercent = valueToPercent(value, sliderMin, sliderMax);
991
+ const fillState =
992
+ markerPercent >= fillStartPercent && markerPercent <= fillEndPercent
993
+ ? 'in-range'
994
+ : 'out-of-range';
871
995
 
872
996
  const state =
873
997
  value < sliderValue
874
998
  ? 'under-value'
875
999
  : value === sliderValue
876
- ? 'at-value'
877
- : 'over-value';
1000
+ ? 'at-value'
1001
+ : 'over-value';
878
1002
 
879
1003
  return inRange ? (
880
1004
  <Comp
@@ -891,6 +1015,7 @@ const SliderMarkerImpl = forwardRef(function SliderMarker(
891
1015
  {...props}
892
1016
  data-reach-slider-marker=""
893
1017
  data-disabled={disabled ? '' : undefined}
1018
+ data-fill-state={fillState}
894
1019
  data-orientation={orientation}
895
1020
  data-state={state}
896
1021
  data-value={value}
@@ -1041,7 +1166,11 @@ function useDimensions(ref: React.RefObject<HTMLElement | null>) {
1041
1166
  }
1042
1167
 
1043
1168
  function valueToPercent(value: number, min: number, max: number) {
1044
- return ((value - min) * 100) / (max - min);
1169
+ if (max === min) {
1170
+ return 0;
1171
+ }
1172
+
1173
+ return Math.min(100, Math.max(0, ((value - min) * 100) / (max - min)));
1045
1174
  }
1046
1175
 
1047
1176
  ////////////////////////////////////////////////////////////////////////////////
@@ -1052,7 +1181,7 @@ type HandleRef = React.RefObject<HTMLDivElement | null>;
1052
1181
  type SliderRef = React.RefObject<HTMLDivElement | null>;
1053
1182
  type TouchIdRef = React.MutableRefObject<number | undefined>;
1054
1183
 
1055
- type SomePointerEvent = TouchEvent | MouseEvent;
1184
+ type SomePointerEvent = TouchEvent | MouseEvent | PointerEvent;
1056
1185
 
1057
1186
  interface ISliderContext {
1058
1187
  ariaLabel: string | undefined;
@@ -1065,8 +1194,12 @@ interface ISliderContext {
1065
1194
  handlePosition: string;
1066
1195
  handleRef: HandleRef;
1067
1196
  hasFocus: boolean;
1197
+ fillDirection: 'left' | 'none' | 'right';
1198
+ fillEndPercent: number;
1199
+ fillStartPercent: number;
1068
1200
  onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
1069
1201
  handleKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void;
1202
+ resetValue: number | undefined;
1070
1203
  setHasFocus: React.Dispatch<React.SetStateAction<boolean>>;
1071
1204
  sliderId: string | undefined;
1072
1205
  sliderMax: number;
@@ -1086,9 +1219,13 @@ type SliderChildrenRender = (props: {
1086
1219
  hasFocus?: boolean;
1087
1220
  id?: string | undefined;
1088
1221
  sliderId?: string | undefined;
1222
+ fillEndPercent?: number;
1223
+ fillOffsetPercent?: number;
1224
+ fillStartPercent?: number;
1089
1225
  max?: number;
1090
1226
  min?: number;
1091
1227
  value?: number;
1228
+ valuePercent?: number;
1092
1229
  }) => ReactElement;
1093
1230
 
1094
1231
  ////////////////////////////////////////////////////////////////////////////////
@@ -0,0 +1,42 @@
1
+ import * as React from 'react';
2
+ import { Toggle } from './Toggle';
3
+ import './styles.css';
4
+
5
+ export default {
6
+ title: 'components/Toggle',
7
+ };
8
+
9
+ export const Basic = () => (
10
+ <div className="toggle-demo">
11
+ <Toggle>Toggle me</Toggle>
12
+ </div>
13
+ );
14
+
15
+ export const Disabled = () => (
16
+ <div className="toggle-demo">
17
+ <Toggle disabled>Disabled Toggle</Toggle>
18
+ </div>
19
+ );
20
+
21
+ export const Controlled = () => {
22
+ const [pressed, setPressed] = React.useState(false);
23
+
24
+ return (
25
+ <div className="toggle-demo">
26
+ <Toggle pressed={pressed} onPressedChange={setPressed}>
27
+ {pressed ? 'ON' : 'OFF'}
28
+ </Toggle>
29
+ <p>State: {pressed ? 'Pressed' : 'Not Pressed'}</p>
30
+ </div>
31
+ );
32
+ };
33
+
34
+ export const Multiple = () => (
35
+ <div className="toggle-demo">
36
+ <div style={{ display: 'flex', gap: 8 }}>
37
+ <Toggle>Bold</Toggle>
38
+ <Toggle>Italic</Toggle>
39
+ <Toggle>Underline</Toggle>
40
+ </div>
41
+ </div>
42
+ );