@basic-ui/core 0.0.29 → 0.0.33

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 (130) hide show
  1. package/build/cjs/index.js +90 -71
  2. package/build/cjs/index.js.map +1 -1
  3. package/build/esm/FocusLock/useFocusLock.js +21 -7
  4. package/build/esm/FocusLock/useFocusLock.js.map +1 -1
  5. package/build/esm/Menu/MenuList.js +7 -5
  6. package/build/esm/Menu/MenuList.js.map +1 -1
  7. package/build/esm/Tooltip/Tooltip.d.ts +1 -0
  8. package/build/esm/Tooltip/Tooltip.js +10 -3
  9. package/build/esm/Tooltip/Tooltip.js.map +1 -1
  10. package/build/esm/Tooltip/stateMachine.d.ts +17 -19
  11. package/build/esm/Tooltip/stateMachine.js +45 -49
  12. package/build/esm/Tooltip/stateMachine.js.map +1 -1
  13. package/build/esm/Tooltip/useTooltip.js +9 -9
  14. package/build/esm/Tooltip/useTooltip.js.map +1 -1
  15. package/build/tsconfig.tsbuildinfo +384 -89
  16. package/package.json +5 -5
  17. package/src/Accordion/Accordion.story.tsx +72 -0
  18. package/src/Accordion/Accordion.tsx +51 -0
  19. package/src/Accordion/AccordionBody.tsx +53 -0
  20. package/src/Accordion/AccordionHeader.tsx +165 -0
  21. package/src/Accordion/AccordionItem.tsx +43 -0
  22. package/src/Accordion/context.ts +35 -0
  23. package/src/Accordion/index.ts +4 -0
  24. package/src/Accordion/scopeQuery.ts +7 -0
  25. package/src/Accordion/styles.css +21 -0
  26. package/src/CheckBox/CheckBox.tsx +41 -0
  27. package/src/CheckBox/index.ts +1 -0
  28. package/src/ComboBox/ComboBox.story.tsx +118 -0
  29. package/src/ComboBox/Combobox.tsx +153 -0
  30. package/src/ComboBox/ComboboxButton.tsx +60 -0
  31. package/src/ComboBox/ComboboxInput.tsx +178 -0
  32. package/src/ComboBox/ComboboxLabel.tsx +32 -0
  33. package/src/ComboBox/ComboboxList.tsx +47 -0
  34. package/src/ComboBox/ComboboxOption.tsx +107 -0
  35. package/src/ComboBox/ComboboxPopover.tsx +58 -0
  36. package/src/ComboBox/cities.ts +23194 -0
  37. package/src/ComboBox/context.ts +33 -0
  38. package/src/ComboBox/hooks.tsx +428 -0
  39. package/src/ComboBox/index.ts +8 -0
  40. package/src/ComboBox/makeHash.ts +19 -0
  41. package/src/ComboBox/scopeQuery.ts +6 -0
  42. package/src/ComboBox/styles.css +30 -0
  43. package/src/FocusLock/FocusLock.tsx +59 -0
  44. package/src/FocusLock/index.ts +1 -0
  45. package/src/FocusLock/tabUtils.ts +28 -0
  46. package/src/FocusLock/useFocusLock.ts +61 -0
  47. package/src/List/List.story.tsx +17 -0
  48. package/src/List/List.tsx +17 -0
  49. package/src/List/ListItem.tsx +23 -0
  50. package/src/List/context.ts +19 -0
  51. package/src/List/index.ts +2 -0
  52. package/src/Menu/.gitkeep +0 -0
  53. package/src/Menu/Menu.story.tsx +158 -0
  54. package/src/Menu/Menu.tsx +60 -0
  55. package/src/Menu/MenuButton.tsx +83 -0
  56. package/src/Menu/MenuItem.tsx +83 -0
  57. package/src/Menu/MenuList.tsx +201 -0
  58. package/src/Menu/MenuPopover.tsx +25 -0
  59. package/src/Menu/context.ts +32 -0
  60. package/src/Menu/index.ts +5 -0
  61. package/src/Menu/scope.ts +7 -0
  62. package/src/Menu/styles.css +42 -0
  63. package/src/Modal/Modal.story.tsx +242 -0
  64. package/src/Modal/Modal.tsx +42 -0
  65. package/src/Modal/ModalBackdrop.tsx +72 -0
  66. package/src/Modal/NavDrawer.story.tsx +157 -0
  67. package/src/Modal/index.ts +2 -0
  68. package/src/Modal/styles.css +46 -0
  69. package/src/Popover/.gitkeep +0 -0
  70. package/src/Popper/Popper.story.tsx +267 -0
  71. package/src/Popper/Popper.tsx +149 -0
  72. package/src/Popper/PopperArrow.tsx +36 -0
  73. package/src/Popper/context.ts +9 -0
  74. package/src/Popper/index.ts +3 -0
  75. package/src/Popper/styles.css +60 -0
  76. package/src/Portal/Portal.tsx +20 -0
  77. package/src/Portal/index.ts +1 -0
  78. package/src/RadioButton/RadioButton.story.tsx +73 -0
  79. package/src/RadioButton/RadioButton.tsx +48 -0
  80. package/src/RadioButton/RadioGroup.tsx +56 -0
  81. package/src/RadioButton/context.ts +19 -0
  82. package/src/RadioButton/index.ts +2 -0
  83. package/src/SkipNav/SkipNav.tsx +16 -0
  84. package/src/SkipNav/index.tsx +1 -0
  85. package/src/Spinner/Spinner.story.tsx +30 -0
  86. package/src/Spinner/Spinner.tsx +112 -0
  87. package/src/Spinner/SpinnerButton.tsx +48 -0
  88. package/src/Spinner/context.ts +21 -0
  89. package/src/Spinner/index.ts +2 -0
  90. package/src/Spinner/styles.css +23 -0
  91. package/src/Tabs/Tab.story.tsx +78 -0
  92. package/src/Tabs/Tab.tsx +131 -0
  93. package/src/Tabs/TabList.tsx +63 -0
  94. package/src/Tabs/TabPanel.tsx +52 -0
  95. package/src/Tabs/TabPanels.tsx +30 -0
  96. package/src/Tabs/Tabs.tsx +47 -0
  97. package/src/Tabs/context.ts +30 -0
  98. package/src/Tabs/index.tsx +5 -0
  99. package/src/Tabs/scopeQuery.ts +6 -0
  100. package/src/Tabs/styles.css +0 -0
  101. package/src/Tooltip/.gitkeep +0 -0
  102. package/src/Tooltip/Tooltip.story.tsx +59 -0
  103. package/src/Tooltip/Tooltip.tsx +48 -0
  104. package/src/Tooltip/index.ts +1 -0
  105. package/src/Tooltip/stateMachine.ts +196 -0
  106. package/src/Tooltip/styles.css +17 -0
  107. package/src/Tooltip/useTooltip.ts +128 -0
  108. package/src/hooks/index.ts +14 -0
  109. package/src/hooks/useAutoFocus.ts +13 -0
  110. package/src/hooks/useChildrenCounter.ts +50 -0
  111. package/src/hooks/useControlledState.ts +37 -0
  112. package/src/hooks/useFocusReturn.ts +23 -0
  113. package/src/hooks/useFocusState.ts +28 -0
  114. package/src/hooks/useGestureHandlers.ts +217 -0
  115. package/src/hooks/useId.ts +18 -0
  116. package/src/hooks/useMeasure.ts +33 -0
  117. package/src/hooks/useOnClickOutside.ts +32 -0
  118. package/src/hooks/useOnKeyDown.ts +18 -0
  119. package/src/hooks/useReducerMachine.ts +59 -0
  120. package/src/hooks/useRemoveBodyScroll.ts +37 -0
  121. package/src/hooks/useScope.ts +51 -0
  122. package/src/hooks/useThrottle.ts +19 -0
  123. package/src/index.ts +19 -0
  124. package/src/utils/assignRef.ts +27 -0
  125. package/src/utils/clamp.ts +3 -0
  126. package/src/utils/createSubscription.ts +16 -0
  127. package/src/utils/getCircularIndex.ts +7 -0
  128. package/src/utils/index.ts +4 -0
  129. package/src/utils/rubberBandClamp.ts +25 -0
  130. package/src/utils/wrapEvent.ts +20 -0
@@ -0,0 +1,33 @@
1
+ import { createContext, useContext } from 'react';
2
+ import { ActionTypes, StateTypes, ReducerState } from './hooks';
3
+ import { SelectEventHandler } from './Combobox';
4
+ import { Scope } from '../hooks';
5
+
6
+ export interface ComboBoxContextProps {
7
+ data: Omit<ReducerState, 'state'>;
8
+ inputRef: React.MutableRefObject<HTMLInputElement | null>;
9
+ popoverRef: React.MutableRefObject<HTMLDivElement | null>;
10
+ buttonRef: React.MutableRefObject<HTMLButtonElement | null>;
11
+ onSelect?: SelectEventHandler;
12
+ optionsRef: React.MutableRefObject<{
13
+ [itemId: string]: {
14
+ value: string | unknown;
15
+ text: string;
16
+ };
17
+ }>;
18
+ listScope: Scope<HTMLElement>;
19
+ state: StateTypes;
20
+ transition: (action: ActionTypes, payload?: any) => void;
21
+ listboxIdRef: React.MutableRefObject<string | undefined>;
22
+ labelIdRef: React.MutableRefObject<string | undefined>;
23
+ autocompletePropRef: React.MutableRefObject<boolean>;
24
+ persistSelectionRef: React.MutableRefObject<boolean>;
25
+ clearOnEscapeRef: React.MutableRefObject<boolean>;
26
+ isVisible: boolean;
27
+ openOnFocus: boolean;
28
+ selectOnBlur: boolean;
29
+ }
30
+
31
+ const comboboxContext = createContext<ComboBoxContextProps>(null as any);
32
+ export const { Provider: ComboBoxProvider } = comboboxContext;
33
+ export const useComboBoxContext = () => useContext(comboboxContext);
@@ -0,0 +1,428 @@
1
+ /* eslint-disable @typescript-eslint/no-use-before-define */
2
+ /* eslint-disable default-case */
3
+ import { useEffect } from 'react';
4
+ import {
5
+ StateChart as GenericStateChart,
6
+ StateMachineState,
7
+ } from '../hooks/useReducerMachine';
8
+ import { getCircularIndex } from '../utils/getCircularIndex';
9
+ import { useComboBoxContext } from './context';
10
+ import { scopeQuery } from './scopeQuery';
11
+
12
+ ////////////////////////////////////////////////////////////////////////////////
13
+ // States
14
+
15
+ // Nothing going on, waiting for the user to type or use the arrow keys
16
+ export const IDLE = 'IDLE';
17
+
18
+ // The component is suggesting options as the user types
19
+ const SUGGESTING = 'SUGGESTING';
20
+
21
+ // The user is using the keyboard to navigate the list, not typing
22
+ export const NAVIGATING = 'NAVIGATING';
23
+
24
+ export type StateTypes = typeof IDLE | typeof SUGGESTING | typeof NAVIGATING;
25
+
26
+ ////////////////////////////////////////////////////////////////////////////////
27
+ // Actions:
28
+
29
+ // Used to sync the state with controlled state, right after mounting
30
+ export const INIT = 'INIT';
31
+
32
+ // User cleared the value w/ backspace, but input still has focus
33
+ export const CLEAR = 'CLEAR';
34
+
35
+ // User cleared the value w/ backspace, but input still has focus
36
+ export const CLEAR_SELECTION = 'CLEAR_SELECTION';
37
+
38
+ // User is typing
39
+ export const CHANGE = 'CHANGE';
40
+
41
+ // User is navigating w/ the keyboard
42
+ export const NAVIGATE = 'NAVIGATE';
43
+
44
+ // User can be navigating with keyboard and then click instead, we want the
45
+ // value from the click, not the current nav item
46
+ const SELECT_WITH_KEYBOARD = 'SELECT_WITH_KEYBOARD';
47
+ export const SELECT_WITH_CLICK = 'SELECT_WITH_CLICK';
48
+
49
+ // Pretty self-explanatory, user can hit escape or blur to close the popover
50
+ const ESCAPE = 'ESCAPE';
51
+ const BLUR = 'BLUR';
52
+
53
+ export const FOCUS = 'FOCUS';
54
+
55
+ export const OPEN_WITH_BUTTON = 'OPEN_WITH_BUTTON';
56
+
57
+ export const CLOSE_WITH_BUTTON = 'CLOSE_WITH_BUTTON';
58
+
59
+ export type ActionTypes =
60
+ | typeof CLEAR
61
+ | typeof CLEAR_SELECTION
62
+ | typeof CHANGE
63
+ | typeof INIT
64
+ | typeof NAVIGATE
65
+ | typeof SELECT_WITH_KEYBOARD
66
+ | typeof SELECT_WITH_CLICK
67
+ | typeof ESCAPE
68
+ | typeof BLUR
69
+ | typeof FOCUS
70
+ | typeof OPEN_WITH_BUTTON
71
+ | typeof CLOSE_WITH_BUTTON;
72
+
73
+ ////////////////////////////////////////////////////////////////////////////////
74
+ export const stateChart: GenericStateChart<StateTypes, ActionTypes> = {
75
+ initial: IDLE,
76
+ states: {
77
+ [IDLE]: {
78
+ on: {
79
+ [BLUR]: IDLE,
80
+ [CLEAR]: IDLE,
81
+ [INIT]: IDLE,
82
+ [CLEAR_SELECTION]: IDLE,
83
+ [CHANGE]: SUGGESTING,
84
+ [FOCUS]: SUGGESTING,
85
+ [NAVIGATE]: NAVIGATING,
86
+ [OPEN_WITH_BUTTON]: SUGGESTING,
87
+ },
88
+ },
89
+ [SUGGESTING]: {
90
+ on: {
91
+ [CHANGE]: SUGGESTING,
92
+ [FOCUS]: SUGGESTING,
93
+ [INIT]: SUGGESTING,
94
+ [NAVIGATE]: NAVIGATING,
95
+ [CLEAR]: IDLE,
96
+ [CLEAR_SELECTION]: SUGGESTING,
97
+ [ESCAPE]: IDLE,
98
+ [BLUR]: IDLE,
99
+ [SELECT_WITH_CLICK]: IDLE,
100
+ [CLOSE_WITH_BUTTON]: IDLE,
101
+ },
102
+ },
103
+ [NAVIGATING]: {
104
+ on: {
105
+ [CHANGE]: SUGGESTING,
106
+ [FOCUS]: SUGGESTING,
107
+ [INIT]: NAVIGATING,
108
+ [CLEAR]: IDLE,
109
+ [CLEAR_SELECTION]: NAVIGATING,
110
+ [BLUR]: IDLE,
111
+ [ESCAPE]: IDLE,
112
+ [NAVIGATE]: NAVIGATING,
113
+ [SELECT_WITH_KEYBOARD]: IDLE,
114
+ [SELECT_WITH_CLICK]: IDLE,
115
+ [CLOSE_WITH_BUTTON]: IDLE,
116
+ },
117
+ },
118
+ },
119
+ };
120
+
121
+ export interface ReducerState
122
+ extends StateMachineState<StateTypes, ActionTypes> {
123
+ item: string;
124
+ navigationItem: string;
125
+ text: string;
126
+ }
127
+
128
+ interface ActionObject {
129
+ type: ActionTypes;
130
+ state: StateTypes;
131
+ nextState: StateTypes;
132
+ [rest: string]: any;
133
+ }
134
+
135
+ export function comboboxReducer(
136
+ data: Readonly<ReducerState>,
137
+ action: ActionObject
138
+ ): ReducerState {
139
+ const nextState = {
140
+ ...data,
141
+ state: action.nextState,
142
+ lastActionType: action.type,
143
+ };
144
+
145
+ switch (action.type) {
146
+ case INIT:
147
+ case CHANGE:
148
+ return {
149
+ ...nextState,
150
+ text: action.text,
151
+ navigationItem: '',
152
+ item: '',
153
+ };
154
+ case NAVIGATE:
155
+ case OPEN_WITH_BUTTON:
156
+ if (action.persistSelection) {
157
+ return {
158
+ ...nextState,
159
+ navigationItem: data.item,
160
+ };
161
+ }
162
+
163
+ return {
164
+ ...nextState,
165
+ navigationItem: action.item,
166
+ };
167
+ case CLEAR_SELECTION:
168
+ return {
169
+ ...nextState,
170
+ navigationItem: '',
171
+ };
172
+ case CLEAR:
173
+ return {
174
+ ...nextState,
175
+ text: '',
176
+ navigationItem: '',
177
+ item: '',
178
+ };
179
+ case BLUR:
180
+ return {
181
+ ...nextState,
182
+ text: action.text,
183
+ navigationItem: '',
184
+ item: action.item,
185
+ };
186
+ case CLOSE_WITH_BUTTON:
187
+ case ESCAPE:
188
+ return {
189
+ ...nextState,
190
+ navigationItem: '',
191
+ item: '',
192
+ };
193
+ case SELECT_WITH_CLICK:
194
+ case SELECT_WITH_KEYBOARD:
195
+ return {
196
+ ...nextState,
197
+ text: action.text,
198
+ item: action.item,
199
+ navigationItem: '',
200
+ };
201
+ case FOCUS:
202
+ return {
203
+ ...nextState,
204
+ navigationItem: action.item,
205
+ };
206
+
207
+ default:
208
+ throw new Error(`Unknown action ${action.type}`);
209
+ }
210
+ }
211
+
212
+ const visibleStates = [SUGGESTING, NAVIGATING];
213
+ export const isVisible = (state: any) => visibleStates.indexOf(state) >= 0;
214
+
215
+ ////////////////////////////////////////////////////////////////////////////////
216
+ // The rest is all implementation details
217
+
218
+ // Move focus back to the input if we start navigating w/ the
219
+ // keyboard after focus has moved to any focusable content in
220
+ // the popup.
221
+ export function useFocusManagement(
222
+ lastActionType: ActionTypes,
223
+ inputRef: React.MutableRefObject<HTMLInputElement | null>
224
+ ) {
225
+ // useEffect so that the cursor goes to the end of the input instead
226
+ // of awkwardly at the beginning, unclear to me why ...
227
+ useEffect(() => {
228
+ if (
229
+ lastActionType === NAVIGATE ||
230
+ lastActionType === ESCAPE ||
231
+ lastActionType === SELECT_WITH_CLICK ||
232
+ lastActionType === OPEN_WITH_BUTTON
233
+ ) {
234
+ inputRef.current && inputRef.current.focus();
235
+ }
236
+ });
237
+ }
238
+
239
+ function getNextItem(
240
+ currentItem: string,
241
+ incr: number,
242
+ optionsItems: HTMLElement[],
243
+ autocomplete: boolean
244
+ ): string {
245
+ const index =
246
+ currentItem === ''
247
+ ? -1
248
+ : optionsItems.findIndex((n) => String(n.id) === currentItem);
249
+
250
+ const optionsLen = optionsItems.length;
251
+
252
+ // Nothing selected, either go to start, or end
253
+ if (index < 0) {
254
+ if (incr > 0) {
255
+ // Go to start
256
+ return optionsItems[0].id;
257
+ } else {
258
+ // Go to end
259
+ return optionsItems[optionsLen - 1].id;
260
+ }
261
+ } else if (autocomplete) {
262
+ const nextIndex = index + incr;
263
+
264
+ if (nextIndex < 0 || nextIndex >= optionsLen) {
265
+ // Next is outside the bounds of list, return nothing selected
266
+ return '';
267
+ }
268
+ }
269
+
270
+ // I'm sure it won't be null, we already check optionsLen above
271
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
272
+ return optionsItems[getCircularIndex(index + incr, optionsLen)!].id;
273
+ }
274
+
275
+ // We want the same events when the input or the popup have focus (HOW COOL ARE
276
+ // HOOKS BTW?) This is probably the hairiest piece but it's not bad.
277
+ export function useKeyDown() {
278
+ const {
279
+ data: { text, navigationItem },
280
+ onSelect,
281
+ optionsRef,
282
+ inputRef,
283
+ state,
284
+ transition,
285
+ autocompletePropRef,
286
+ clearOnEscapeRef,
287
+ persistSelectionRef,
288
+ listScope,
289
+ } = useComboBoxContext();
290
+
291
+ return function handleKeyDown(event: React.KeyboardEvent<any>) {
292
+ const optionNodes = listScope.current.queryAllNodes(scopeQuery);
293
+
294
+ switch (event.key) {
295
+ case 'ArrowUp':
296
+ case 'ArrowDown': {
297
+ // Don't scroll the page
298
+ event.preventDefault();
299
+
300
+ const optionsLen = optionNodes.length;
301
+
302
+ // If the developer didn't render any options, there's no point in
303
+ // trying to navigate--but seriously what the heck? Give us some
304
+ // options fam.
305
+ if (optionsLen === 0) {
306
+ return;
307
+ }
308
+
309
+ if (state === IDLE) {
310
+ // Opening a closed list
311
+ transition(NAVIGATE, {
312
+ persistSelection: persistSelectionRef.current,
313
+ });
314
+ } else {
315
+ // ArrowUp decreases index, ArrowDown increases
316
+ const incr = event.key === 'ArrowUp' ? -1 : 1;
317
+
318
+ // When autocompletting, we'll not cycle through the list directly
319
+ const autocomplete = autocompletePropRef.current;
320
+
321
+ // Get next selected item
322
+ const nextItem = getNextItem(
323
+ navigationItem,
324
+ incr,
325
+ optionNodes,
326
+ autocomplete
327
+ );
328
+
329
+ const value =
330
+ nextItem !== '' ? optionsRef.current[nextItem].text : null;
331
+
332
+ transition(NAVIGATE, { value, item: nextItem });
333
+ }
334
+ break;
335
+ }
336
+ case 'Escape': {
337
+ if (state !== IDLE) {
338
+ transition(ESCAPE);
339
+ } else if (state === IDLE && text !== '') {
340
+ if (!inputRef.current || !clearOnEscapeRef.current) {
341
+ break;
342
+ }
343
+
344
+ // emulate a inputRef change event, might not work in future versions of React
345
+ const lastValue = inputRef.current.value;
346
+ inputRef.current.value = '';
347
+
348
+ const tracker = (inputRef.current as any)._valueTracker;
349
+ if (tracker) {
350
+ tracker.setValue(lastValue);
351
+ }
352
+
353
+ const event = new Event('change', { bubbles: true });
354
+ inputRef.current.dispatchEvent(event);
355
+ }
356
+ break;
357
+ }
358
+ case 'Enter': {
359
+ if (state === NAVIGATING && navigationItem !== '') {
360
+ const {
361
+ value: navigationValue,
362
+ text: navigationText,
363
+ } = optionsRef.current[navigationItem];
364
+
365
+ // don't want to submit forms
366
+ event.preventDefault();
367
+ onSelect && onSelect(navigationText, navigationItem, navigationValue);
368
+ transition(SELECT_WITH_KEYBOARD, {
369
+ text: navigationText,
370
+ item: navigationItem,
371
+ });
372
+ }
373
+ break;
374
+ }
375
+ }
376
+ };
377
+ }
378
+
379
+ export function useBlur() {
380
+ const {
381
+ data: { navigationItem, text: stateText },
382
+ transition,
383
+ optionsRef,
384
+ popoverRef,
385
+ inputRef,
386
+ buttonRef,
387
+ onSelect,
388
+ selectOnBlur, // not implemented yet
389
+ } = useComboBoxContext();
390
+
391
+ return function handleBlur() {
392
+ requestAnimationFrame(() => {
393
+ // we on want to close only if focus rests outside the combobox
394
+ if (
395
+ document.activeElement !== inputRef.current &&
396
+ document.activeElement !== buttonRef.current &&
397
+ popoverRef.current
398
+ ) {
399
+ if (popoverRef.current.contains(document.activeElement)) {
400
+ // focus landed inside the combobox, keep it open
401
+ // in the future, we can make it not close, select, or anything
402
+ // this way we can have like... checkboxes available in the
403
+ // menu item, etc.
404
+ } else {
405
+ // focus landed outside the combobox, close it.
406
+ if (!selectOnBlur || navigationItem === '') {
407
+ // we don't wanna select on blur, or navigationIndex is invalid
408
+ transition(BLUR, { text: stateText, item: '' });
409
+ } else {
410
+ // select the currently selected item
411
+ const {
412
+ value: navigationValue,
413
+ text: navigationText,
414
+ } = optionsRef.current[navigationItem];
415
+
416
+ onSelect &&
417
+ onSelect(navigationText, navigationItem, navigationValue);
418
+
419
+ transition(BLUR, {
420
+ text: navigationText,
421
+ item: navigationItem,
422
+ });
423
+ }
424
+ }
425
+ }
426
+ });
427
+ };
428
+ }
@@ -0,0 +1,8 @@
1
+ export * from './Combobox';
2
+ export * from './ComboboxButton';
3
+ export * from './ComboboxInput';
4
+ export * from './ComboboxLabel';
5
+ export * from './ComboboxList';
6
+ export * from './ComboboxOption';
7
+ export * from './ComboboxPopover';
8
+ export * from './context';
@@ -0,0 +1,19 @@
1
+ // We don't want to track the active descendant with indexes because nothing is
2
+ // more annoying in a combobox than having it change values RIGHT AS YOU HIT
3
+ // ENTER. That only happens if you use the index as your data, rather than
4
+ // *your data as your data*. We use this to generate a unique ID based on the
5
+ // value of each item. This function is short, sweet, and good enough™ (I also
6
+ // don't know how it works, tbqh)
7
+ // https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript
8
+ export function makeHash(str: string) {
9
+ let hash = 0;
10
+ if (str.length === 0) {
11
+ return hash;
12
+ }
13
+ for (let i = 0; i < str.length; i++) {
14
+ const char = str.charCodeAt(i);
15
+ hash = (hash << 5) - hash + char;
16
+ hash = hash & hash;
17
+ }
18
+ return hash;
19
+ }
@@ -0,0 +1,6 @@
1
+ export function scopeQuery(
2
+ nodeType: string,
3
+ props: Record<string, unknown>
4
+ ): boolean {
5
+ return props['data-reach-combobox-option'] === '';
6
+ }
@@ -0,0 +1,30 @@
1
+ [data-reach-combobox] {
2
+ position: relative;
3
+ }
4
+
5
+ [data-reach-combobox-input] {
6
+ width: 400px;
7
+ font-size: 100%;
8
+ padding: 0.33rem;
9
+ }
10
+
11
+ [data-reach-combobox-popover] {
12
+ position: absolute;
13
+ top: 100%;
14
+ left: 0px;
15
+ width: auto;
16
+ box-shadow: 0px 2px 6px hsla(0, 0%, 0%, 0.15);
17
+ border: none;
18
+ }
19
+
20
+ [data-reach-combobox-list] {
21
+ padding: 0 8px;
22
+ }
23
+
24
+ [data-reach-combobox-option] {
25
+ list-style: none;
26
+ }
27
+
28
+ [data-reach-combobox-option][data-highlighted] {
29
+ background-color: #f1f2f4;
30
+ }
@@ -0,0 +1,59 @@
1
+ import { useRef } from 'react';
2
+ import type * as React from 'react';
3
+ import { useFocusLock } from './useFocusLock';
4
+
5
+ export interface FocusLockProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ as?: React.ElementType<any>;
7
+ innerAs?: React.ElementType<any>;
8
+ children?: React.ReactNode;
9
+ childRef: React.MutableRefObject<HTMLElement | null>;
10
+ enabled?: boolean;
11
+ }
12
+
13
+ export const FocusLock: React.FC<FocusLockProps> = function FocusLock(props) {
14
+ const {
15
+ as: Comp = 'div',
16
+ childRef,
17
+ enabled = false,
18
+ style = {},
19
+ children,
20
+ ...otherProps
21
+ } = props;
22
+ const lockStartRef = useRef<HTMLElement>(null);
23
+ const lockEndRef = useRef<HTMLElement>(null);
24
+
25
+ useFocusLock(childRef, { enabled, lockStartRef, lockEndRef });
26
+
27
+ const lockStyle = {
28
+ width: 1,
29
+ height: 0,
30
+ padding: 0,
31
+ overflow: 'hidden',
32
+ position: 'fixed',
33
+ top: 1,
34
+ left: 1,
35
+ ...style,
36
+ };
37
+
38
+ return (
39
+ <>
40
+ <Comp
41
+ ref={lockStartRef}
42
+ data-focus-lock-start=""
43
+ aria-hidden={true}
44
+ tabIndex={0}
45
+ style={lockStyle}
46
+ {...otherProps}
47
+ />
48
+ {children}
49
+ <Comp
50
+ ref={lockEndRef}
51
+ data-focus-lock-end=""
52
+ aria-hidden={true}
53
+ tabIndex={0}
54
+ style={lockStyle}
55
+ {...otherProps}
56
+ />
57
+ </>
58
+ );
59
+ };
@@ -0,0 +1 @@
1
+ export * from './FocusLock';
@@ -0,0 +1,28 @@
1
+ export const tabblable = [
2
+ 'button:enabled:not([readonly])',
3
+ 'select:enabled:not([readonly])',
4
+ 'textarea:enabled:not([readonly])',
5
+ 'input:enabled:not([readonly])',
6
+
7
+ 'a[href]',
8
+ 'area[href]',
9
+
10
+ 'iframe',
11
+ 'object',
12
+ 'embed',
13
+
14
+ '[tabindex]',
15
+ '[contenteditable]',
16
+ '[autofocus]',
17
+ ].join(',');
18
+
19
+ /* This is naive and will not consider tabIndex */
20
+ export const getTabblableNodes = (
21
+ parentNode: HTMLElement | null
22
+ ): HTMLElement[] => {
23
+ if (!parentNode) {
24
+ return [];
25
+ }
26
+
27
+ return Array.from(parentNode.querySelectorAll(tabblable));
28
+ };
@@ -0,0 +1,61 @@
1
+ import { useEffect } from 'react';
2
+ import { getTabblableNodes } from './tabUtils';
3
+
4
+ export interface FocusLockOptions {
5
+ enabled: boolean;
6
+ lockStartRef: React.MutableRefObject<HTMLElement | null>;
7
+ lockEndRef: React.MutableRefObject<HTMLElement | null>;
8
+ }
9
+
10
+ const focusLockStack: HTMLElement[] = [];
11
+ export function useFocusLock(
12
+ ref: React.MutableRefObject<HTMLElement | null>,
13
+ opts: FocusLockOptions
14
+ ) {
15
+ const { enabled = true, lockStartRef, lockEndRef } = opts;
16
+
17
+ useEffect(() => {
18
+ const rootEl = ref.current;
19
+ if (enabled && rootEl) {
20
+ focusLockStack.push(rootEl);
21
+
22
+ const listener = (event: FocusEvent) => {
23
+ const isActiveFocusLock =
24
+ focusLockStack[focusLockStack.length - 1] === rootEl;
25
+ if (!isActiveFocusLock) {
26
+ // Not the currently focused lock. Forget about it.
27
+ return;
28
+ }
29
+
30
+ if (event.target === lockEndRef.current) {
31
+ rootEl.focus();
32
+ } else if (event.target === lockStartRef.current) {
33
+ const nodes = getTabblableNodes(rootEl);
34
+
35
+ if (nodes.length > 0) {
36
+ const nodeToFocus = nodes.length - 1;
37
+ nodes[nodeToFocus].focus();
38
+ } else {
39
+ rootEl.focus();
40
+ }
41
+ } else if (
42
+ document !== event.target &&
43
+ rootEl !== event.target &&
44
+ !rootEl.contains(event.target as any)
45
+ ) {
46
+ event.preventDefault();
47
+ rootEl.focus();
48
+ }
49
+ };
50
+
51
+ document.addEventListener('focusin', listener);
52
+ return () => {
53
+ document.removeEventListener('focusin', listener);
54
+
55
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
56
+ focusLockStack.pop();
57
+ };
58
+ }
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ }, [enabled]);
61
+ }
@@ -0,0 +1,17 @@
1
+ import { List, ListItem } from './';
2
+ import { storiesOf } from '@storybook/react';
3
+ // import './styles.css';
4
+
5
+ const Example = () => {
6
+ return (
7
+ <List>
8
+ <ListItem>Item 1</ListItem>
9
+ <ListItem>Item 2</ListItem>
10
+ <ListItem>Item 3</ListItem>
11
+ </List>
12
+ );
13
+ };
14
+
15
+ const stories = storiesOf('Components/List', module);
16
+
17
+ stories.add('controlled', () => <Example />);