@basic-ui/core 0.0.31 → 0.0.34
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 +113 -71
- package/build/cjs/index.js.map +1 -1
- package/build/esm/FocusLock/useFocusLock.js +21 -7
- package/build/esm/FocusLock/useFocusLock.js.map +1 -1
- package/build/esm/Tooltip/stateMachine.d.ts +17 -19
- package/build/esm/Tooltip/stateMachine.js +45 -49
- package/build/esm/Tooltip/stateMachine.js.map +1 -1
- package/build/esm/Tooltip/useTooltip.js +9 -9
- package/build/esm/Tooltip/useTooltip.js.map +1 -1
- package/build/esm/hooks/useGestureHandlers.d.ts +2 -0
- package/build/esm/hooks/useGestureHandlers.js +39 -7
- package/build/esm/hooks/useGestureHandlers.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +383 -88
- package/package.json +6 -6
- package/src/Accordion/Accordion.story.tsx +72 -0
- package/src/Accordion/Accordion.tsx +51 -0
- package/src/Accordion/AccordionBody.tsx +53 -0
- package/src/Accordion/AccordionHeader.tsx +165 -0
- package/src/Accordion/AccordionItem.tsx +43 -0
- package/src/Accordion/context.ts +35 -0
- package/src/Accordion/index.ts +4 -0
- package/src/Accordion/scopeQuery.ts +7 -0
- package/src/Accordion/styles.css +21 -0
- package/src/CheckBox/CheckBox.tsx +41 -0
- package/src/CheckBox/index.ts +1 -0
- package/src/ComboBox/ComboBox.story.tsx +118 -0
- package/src/ComboBox/Combobox.tsx +153 -0
- package/src/ComboBox/ComboboxButton.tsx +60 -0
- package/src/ComboBox/ComboboxInput.tsx +178 -0
- package/src/ComboBox/ComboboxLabel.tsx +32 -0
- package/src/ComboBox/ComboboxList.tsx +47 -0
- package/src/ComboBox/ComboboxOption.tsx +107 -0
- package/src/ComboBox/ComboboxPopover.tsx +58 -0
- package/src/ComboBox/cities.ts +23194 -0
- package/src/ComboBox/context.ts +33 -0
- package/src/ComboBox/hooks.tsx +428 -0
- package/src/ComboBox/index.ts +8 -0
- package/src/ComboBox/makeHash.ts +19 -0
- package/src/ComboBox/scopeQuery.ts +6 -0
- package/src/ComboBox/styles.css +30 -0
- package/src/FocusLock/FocusLock.tsx +59 -0
- package/src/FocusLock/index.ts +1 -0
- package/src/FocusLock/tabUtils.ts +28 -0
- package/src/FocusLock/useFocusLock.ts +61 -0
- package/src/List/List.story.tsx +17 -0
- package/src/List/List.tsx +17 -0
- package/src/List/ListItem.tsx +23 -0
- package/src/List/context.ts +19 -0
- package/src/List/index.ts +2 -0
- package/src/Menu/.gitkeep +0 -0
- package/src/Menu/Menu.story.tsx +158 -0
- package/src/Menu/Menu.tsx +60 -0
- package/src/Menu/MenuButton.tsx +83 -0
- package/src/Menu/MenuItem.tsx +83 -0
- package/src/Menu/MenuList.tsx +201 -0
- package/src/Menu/MenuPopover.tsx +25 -0
- package/src/Menu/context.ts +32 -0
- package/src/Menu/index.ts +5 -0
- package/src/Menu/scope.ts +7 -0
- package/src/Menu/styles.css +42 -0
- package/src/Modal/Modal.story.tsx +242 -0
- package/src/Modal/Modal.tsx +42 -0
- package/src/Modal/ModalBackdrop.tsx +72 -0
- package/src/Modal/NavDrawer.story.tsx +157 -0
- package/src/Modal/index.ts +2 -0
- package/src/Modal/styles.css +46 -0
- package/src/Popover/.gitkeep +0 -0
- package/src/Popper/Popper.story.tsx +267 -0
- package/src/Popper/Popper.tsx +149 -0
- package/src/Popper/PopperArrow.tsx +36 -0
- package/src/Popper/context.ts +9 -0
- package/src/Popper/index.ts +3 -0
- package/src/Popper/styles.css +60 -0
- package/src/Portal/Portal.tsx +20 -0
- package/src/Portal/index.ts +1 -0
- package/src/RadioButton/RadioButton.story.tsx +73 -0
- package/src/RadioButton/RadioButton.tsx +48 -0
- package/src/RadioButton/RadioGroup.tsx +56 -0
- package/src/RadioButton/context.ts +19 -0
- package/src/RadioButton/index.ts +2 -0
- package/src/SkipNav/SkipNav.tsx +16 -0
- package/src/SkipNav/index.tsx +1 -0
- package/src/Spinner/Spinner.story.tsx +30 -0
- package/src/Spinner/Spinner.tsx +112 -0
- package/src/Spinner/SpinnerButton.tsx +48 -0
- package/src/Spinner/context.ts +21 -0
- package/src/Spinner/index.ts +2 -0
- package/src/Spinner/styles.css +23 -0
- package/src/Tabs/Tab.story.tsx +78 -0
- package/src/Tabs/Tab.tsx +131 -0
- package/src/Tabs/TabList.tsx +63 -0
- package/src/Tabs/TabPanel.tsx +52 -0
- package/src/Tabs/TabPanels.tsx +30 -0
- package/src/Tabs/Tabs.tsx +47 -0
- package/src/Tabs/context.ts +30 -0
- package/src/Tabs/index.tsx +5 -0
- package/src/Tabs/scopeQuery.ts +6 -0
- package/src/Tabs/styles.css +0 -0
- package/src/Tooltip/.gitkeep +0 -0
- package/src/Tooltip/Tooltip.story.tsx +59 -0
- package/src/Tooltip/Tooltip.tsx +48 -0
- package/src/Tooltip/index.ts +1 -0
- package/src/Tooltip/stateMachine.ts +196 -0
- package/src/Tooltip/styles.css +17 -0
- package/src/Tooltip/useTooltip.ts +128 -0
- package/src/hooks/index.ts +14 -0
- package/src/hooks/useAutoFocus.ts +13 -0
- package/src/hooks/useChildrenCounter.ts +50 -0
- package/src/hooks/useControlledState.ts +37 -0
- package/src/hooks/useFocusReturn.ts +23 -0
- package/src/hooks/useFocusState.ts +28 -0
- package/src/hooks/useGestureHandlers.ts +253 -0
- package/src/hooks/useId.ts +18 -0
- package/src/hooks/useMeasure.ts +33 -0
- package/src/hooks/useOnClickOutside.ts +32 -0
- package/src/hooks/useOnKeyDown.ts +18 -0
- package/src/hooks/useReducerMachine.ts +59 -0
- package/src/hooks/useRemoveBodyScroll.ts +37 -0
- package/src/hooks/useScope.ts +51 -0
- package/src/hooks/useThrottle.ts +19 -0
- package/src/index.ts +19 -0
- package/src/utils/assignRef.ts +27 -0
- package/src/utils/clamp.ts +3 -0
- package/src/utils/createSubscription.ts +16 -0
- package/src/utils/getCircularIndex.ts +7 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/rubberBandClamp.ts +25 -0
- package/src/utils/wrapEvent.ts +20 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
import { wrapEvent, CustomEventHandler } from '../utils';
|
|
3
|
+
|
|
4
|
+
export function useControlledState<
|
|
5
|
+
V,
|
|
6
|
+
E extends React.SyntheticEvent<any>,
|
|
7
|
+
H extends unknown[]
|
|
8
|
+
>(
|
|
9
|
+
valueProp: V | undefined,
|
|
10
|
+
onChangeProp: CustomEventHandler<E, H> | undefined,
|
|
11
|
+
defaultValue: V,
|
|
12
|
+
defaultOnChange: (
|
|
13
|
+
setValue: React.Dispatch<React.SetStateAction<V>>
|
|
14
|
+
) => CustomEventHandler<E, H>
|
|
15
|
+
): [
|
|
16
|
+
V,
|
|
17
|
+
CustomEventHandler<E, H> | undefined,
|
|
18
|
+
React.Dispatch<React.SetStateAction<V>>
|
|
19
|
+
] {
|
|
20
|
+
const isControlled = useRef(valueProp !== undefined);
|
|
21
|
+
const [valueState, setValueState] = useState<V>(defaultValue);
|
|
22
|
+
|
|
23
|
+
if (isControlled.current) {
|
|
24
|
+
if (valueProp === undefined) {
|
|
25
|
+
console.warn('Trying to change from controlled to uncontrolled.');
|
|
26
|
+
}
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
28
|
+
return [valueProp!, onChangeProp, setValueState];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
33
|
+
valueState!,
|
|
34
|
+
wrapEvent(onChangeProp, defaultOnChange(setValueState)),
|
|
35
|
+
setValueState,
|
|
36
|
+
];
|
|
37
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useFocusReturn(open: boolean) {
|
|
4
|
+
const previousFocusRef = useRef<Element | null>(null);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (open) {
|
|
8
|
+
// once opened, keep track of the element that triggered
|
|
9
|
+
// the Modal opening
|
|
10
|
+
previousFocusRef.current = document.activeElement;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return () => {
|
|
14
|
+
// on unmount, return focus to that element
|
|
15
|
+
const previousFocus = previousFocusRef.current;
|
|
16
|
+
requestAnimationFrame(() => {
|
|
17
|
+
if (previousFocus) {
|
|
18
|
+
(previousFocus as HTMLElement).focus();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
}, [open]);
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { wrapEvent } from '../utils/wrapEvent';
|
|
3
|
+
|
|
4
|
+
export function useFocusState<T>(
|
|
5
|
+
props: {
|
|
6
|
+
onFocus?: React.FocusEventHandler<T>;
|
|
7
|
+
onBlur?: React.FocusEventHandler<T>;
|
|
8
|
+
} = {}
|
|
9
|
+
) {
|
|
10
|
+
const { onFocus, onBlur } = props;
|
|
11
|
+
const [hasFocus, setHasFocus] = useState(false);
|
|
12
|
+
|
|
13
|
+
const handleFocus = () => {
|
|
14
|
+
setHasFocus(true);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const handleBlur = () => {
|
|
18
|
+
setHasFocus(false);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
bind: {
|
|
23
|
+
onFocus: wrapEvent(onFocus, handleFocus),
|
|
24
|
+
onBlur: wrapEvent(onBlur, handleBlur),
|
|
25
|
+
},
|
|
26
|
+
hasFocus,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
|
|
4
|
+
export interface GestureHandlersState {
|
|
5
|
+
target: null | EventTarget;
|
|
6
|
+
x: number;
|
|
7
|
+
xDelta: number;
|
|
8
|
+
xDeltaPercent: number;
|
|
9
|
+
xInitial: number;
|
|
10
|
+
xPrev: number;
|
|
11
|
+
xVelocity: number;
|
|
12
|
+
y: number;
|
|
13
|
+
yDelta: number;
|
|
14
|
+
yDeltaPercent: number;
|
|
15
|
+
yInitial: number;
|
|
16
|
+
yPrev: number;
|
|
17
|
+
yVelocity: number;
|
|
18
|
+
startTime: number;
|
|
19
|
+
down: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type SetStateFunc<S> = (
|
|
23
|
+
state: (prevState: Readonly<S>) => S,
|
|
24
|
+
callback?: () => void
|
|
25
|
+
) => void;
|
|
26
|
+
|
|
27
|
+
export interface GestureHandlersReturn {
|
|
28
|
+
onMouseDown: (e: React.MouseEvent<HTMLElement>) => void;
|
|
29
|
+
onTouchStart: (e: React.TouchEvent<HTMLElement>) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface GestureHandlerOptions {
|
|
33
|
+
ensureTargetIsContainer?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const initialGestureHandlersState: GestureHandlersState = {
|
|
37
|
+
target: null,
|
|
38
|
+
x: 0,
|
|
39
|
+
xDelta: 0,
|
|
40
|
+
xDeltaPercent: 0,
|
|
41
|
+
xInitial: 0,
|
|
42
|
+
xPrev: 0,
|
|
43
|
+
xVelocity: 0,
|
|
44
|
+
y: 0,
|
|
45
|
+
yDelta: 0,
|
|
46
|
+
yDeltaPercent: 0,
|
|
47
|
+
yInitial: 0,
|
|
48
|
+
yPrev: 0,
|
|
49
|
+
yVelocity: 0,
|
|
50
|
+
startTime: 0,
|
|
51
|
+
down: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const FRAMERATE_CONST = 1000 / 60; // 60 fps
|
|
55
|
+
const VELOCITY_DEPR_FACTOR = FRAMERATE_CONST * 2;
|
|
56
|
+
|
|
57
|
+
export function gestureHandlers(
|
|
58
|
+
set: SetStateFunc<GestureHandlersState>,
|
|
59
|
+
containerRef?: React.MutableRefObject<HTMLElement | null>,
|
|
60
|
+
options: GestureHandlerOptions = {}
|
|
61
|
+
): GestureHandlersReturn {
|
|
62
|
+
const { ensureTargetIsContainer = false } = options;
|
|
63
|
+
|
|
64
|
+
// Common handlers
|
|
65
|
+
const handleUp = () => {
|
|
66
|
+
set((state: GestureHandlersState) => {
|
|
67
|
+
const deltaTime = Date.now() - state.startTime;
|
|
68
|
+
const xDelta = state.x - state.xInitial;
|
|
69
|
+
const yDelta = state.y - state.yInitial;
|
|
70
|
+
const xVelocity = calcVelocity(xDelta, deltaTime, state.xVelocity);
|
|
71
|
+
const yVelocity = calcVelocity(yDelta, deltaTime, state.yVelocity);
|
|
72
|
+
const newState: GestureHandlersState = {
|
|
73
|
+
...state,
|
|
74
|
+
xVelocity,
|
|
75
|
+
yVelocity,
|
|
76
|
+
target: null,
|
|
77
|
+
down: false,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return newState;
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleDown = (e: MouseEvent) => {
|
|
85
|
+
const { target, pageX, pageY } = e;
|
|
86
|
+
|
|
87
|
+
set((state: GestureHandlersState) => {
|
|
88
|
+
const newState = {
|
|
89
|
+
...state,
|
|
90
|
+
target,
|
|
91
|
+
x: pageX,
|
|
92
|
+
xDelta: 0,
|
|
93
|
+
xDeltaPercent: 0,
|
|
94
|
+
xVelocity: 0,
|
|
95
|
+
xInitial: pageX,
|
|
96
|
+
xPrev: pageX,
|
|
97
|
+
y: pageY,
|
|
98
|
+
yDelta: 0,
|
|
99
|
+
yDeltaPercent: 0,
|
|
100
|
+
yVelocity: 0,
|
|
101
|
+
yInitial: pageY,
|
|
102
|
+
yPrev: pageY,
|
|
103
|
+
startTime: Date.now(),
|
|
104
|
+
down: true,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return newState;
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function calcVelocity(
|
|
112
|
+
deltaSpace: number,
|
|
113
|
+
deltaTime: number,
|
|
114
|
+
prevVelocity: number
|
|
115
|
+
) {
|
|
116
|
+
if (deltaTime < 1) {
|
|
117
|
+
deltaTime = 1;
|
|
118
|
+
}
|
|
119
|
+
const speed = deltaSpace / deltaTime;
|
|
120
|
+
const depr = 0.5 + Math.min(deltaTime / VELOCITY_DEPR_FACTOR, 0.5);
|
|
121
|
+
return speed * depr + prevVelocity * (1 - depr);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function handleMove(e: MouseEvent) {
|
|
125
|
+
const { pageX, pageY } = e;
|
|
126
|
+
if (e.cancelable) {
|
|
127
|
+
// prevent drag & drop behaviour from browser
|
|
128
|
+
e.preventDefault && e.preventDefault();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
set((state: GestureHandlersState) => {
|
|
132
|
+
const target =
|
|
133
|
+
(containerRef && containerRef.current) || (e as any).target;
|
|
134
|
+
|
|
135
|
+
const deltaTime = Date.now() - state.startTime;
|
|
136
|
+
|
|
137
|
+
const width = target ? target.offsetWidth : NaN;
|
|
138
|
+
const xDelta = pageX - state.xInitial;
|
|
139
|
+
const xDeltaPercent = (xDelta * 100) / width;
|
|
140
|
+
const xVelocity = calcVelocity(xDelta, deltaTime, state.xVelocity);
|
|
141
|
+
|
|
142
|
+
const height = target ? target.offsetHeight : NaN;
|
|
143
|
+
const yDelta = pageY - state.yInitial;
|
|
144
|
+
const yDeltaPercent = (yDelta * 100) / height;
|
|
145
|
+
const yVelocity = calcVelocity(yDelta, deltaTime, state.yVelocity);
|
|
146
|
+
|
|
147
|
+
const newState = {
|
|
148
|
+
...state,
|
|
149
|
+
xDelta,
|
|
150
|
+
xDeltaPercent,
|
|
151
|
+
x: pageX,
|
|
152
|
+
xPrev: state.x,
|
|
153
|
+
xVelocity,
|
|
154
|
+
yDelta,
|
|
155
|
+
yDeltaPercent,
|
|
156
|
+
y: pageY,
|
|
157
|
+
yPrev: state.y,
|
|
158
|
+
yVelocity,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return newState;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Touch handlers
|
|
166
|
+
|
|
167
|
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
168
|
+
function handleTouchMove(e: TouchEvent) {
|
|
169
|
+
if (e.cancelable) {
|
|
170
|
+
// prevent drag & drop behaviour from browser
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
}
|
|
173
|
+
handleMove(e.touches.item(0) as any);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function handleTouchStart(e: React.TouchEvent<HTMLElement>) {
|
|
177
|
+
// making sure we're not dragging the element when more than one finger press the screen
|
|
178
|
+
const { touches } = e;
|
|
179
|
+
if (touches.length > 1) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (
|
|
184
|
+
ensureTargetIsContainer &&
|
|
185
|
+
containerRef &&
|
|
186
|
+
e.target !== containerRef.current
|
|
187
|
+
) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
window.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
192
|
+
window.addEventListener('touchend', handleTouchEnd);
|
|
193
|
+
window.addEventListener('touchcancel', handleTouchEnd);
|
|
194
|
+
handleDown(e.touches.item(0) as any);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function handleTouchEnd() {
|
|
198
|
+
window.removeEventListener('touchmove', handleTouchMove);
|
|
199
|
+
window.removeEventListener('touchend', handleTouchEnd);
|
|
200
|
+
window.removeEventListener('touchcancel', handleTouchEnd);
|
|
201
|
+
handleUp();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Mouse handlers
|
|
205
|
+
function handleMouseDown(e: React.MouseEvent<HTMLElement>) {
|
|
206
|
+
if (
|
|
207
|
+
ensureTargetIsContainer &&
|
|
208
|
+
containerRef &&
|
|
209
|
+
e.target !== containerRef.current
|
|
210
|
+
) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (e.button === 0) {
|
|
215
|
+
window.addEventListener('mousemove', handleMove);
|
|
216
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
217
|
+
handleDown(e as any);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function handleMouseUp() {
|
|
222
|
+
window.removeEventListener('mousemove', handleMove);
|
|
223
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
224
|
+
handleUp();
|
|
225
|
+
}
|
|
226
|
+
/* eslint-enable @typescript-eslint/no-use-before-define */
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
onMouseDown: handleMouseDown,
|
|
230
|
+
onTouchStart: handleTouchStart,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export const useGestureHandlers = (
|
|
235
|
+
containerRef: React.MutableRefObject<HTMLElement | null> | undefined,
|
|
236
|
+
onGesture: (e: GestureHandlersState) => void,
|
|
237
|
+
options: GestureHandlerOptions = {}
|
|
238
|
+
) => {
|
|
239
|
+
const state = useRef({ ...initialGestureHandlersState });
|
|
240
|
+
|
|
241
|
+
const set = (
|
|
242
|
+
cb: (prevState: GestureHandlersState) => GestureHandlersState
|
|
243
|
+
) => {
|
|
244
|
+
state.current = cb(state.current);
|
|
245
|
+
onGesture && onGesture(state.current);
|
|
246
|
+
|
|
247
|
+
return state.current;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const handlers = gestureHandlers(set, containerRef, options);
|
|
251
|
+
|
|
252
|
+
return { state: state.current, handlers };
|
|
253
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
// Could use UUID but if we hit 9,007,199,254,740,991 unique components over
|
|
4
|
+
// the lifetime of the app before it gets reloaded, I mean ... come on.
|
|
5
|
+
// I don't even know what xillion that is.
|
|
6
|
+
// /me googles
|
|
7
|
+
// Oh duh, quadrillion. Nine quadrillion components. I think we're okay.
|
|
8
|
+
let id = 0;
|
|
9
|
+
const genId = () => `${++id}`;
|
|
10
|
+
|
|
11
|
+
export function useId(): string | undefined;
|
|
12
|
+
export function useId(preferredId: string): string;
|
|
13
|
+
export function useId(preferredId: string | undefined): string | undefined;
|
|
14
|
+
export function useId(preferredId?: string): string | undefined {
|
|
15
|
+
const [id, setId] = useState<string | undefined>(preferredId);
|
|
16
|
+
useEffect(() => setId(genId()), []);
|
|
17
|
+
return id;
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useRef, useState, useEffect } from 'react';
|
|
2
|
+
import ResizeObserver from 'resize-observer-polyfill';
|
|
3
|
+
|
|
4
|
+
export function useMeasure(ref: React.MutableRefObject<HTMLElement | null>) {
|
|
5
|
+
const ro = useRef<ResizeObserver | null>(null);
|
|
6
|
+
const [bounds, setBounds] = useState({
|
|
7
|
+
left: 0,
|
|
8
|
+
top: 0,
|
|
9
|
+
width: 0,
|
|
10
|
+
height: 0,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (ro.current === null) {
|
|
15
|
+
ro.current = new ResizeObserver((entries: any) => {
|
|
16
|
+
const entry = entries[0];
|
|
17
|
+
setBounds(entry.contentRect);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (ref.current) {
|
|
22
|
+
ro.current.observe(ref.current);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
if (ro.current) {
|
|
27
|
+
ro.current.disconnect();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}, [ref]);
|
|
31
|
+
|
|
32
|
+
return bounds;
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect, useCallback } from 'react';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
|
|
4
|
+
export function useOnClickOutside(
|
|
5
|
+
ref: React.RefObject<any>,
|
|
6
|
+
handler: (e: PointerEvent) => void,
|
|
7
|
+
active = true
|
|
8
|
+
) {
|
|
9
|
+
const listener = useCallback(
|
|
10
|
+
(event: PointerEvent) => {
|
|
11
|
+
// Do nothing if clicking ref's element or descendent elements
|
|
12
|
+
if (!ref.current || ref.current.contains(event.target)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
handler && handler(event);
|
|
17
|
+
},
|
|
18
|
+
[ref, handler]
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (active) {
|
|
23
|
+
document.addEventListener('pointerup', listener);
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
document.removeEventListener('pointerup', listener);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return;
|
|
31
|
+
}, [listener, active]);
|
|
32
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useOnKeyDown(
|
|
4
|
+
handler: (e: KeyboardEvent) => void,
|
|
5
|
+
active = true
|
|
6
|
+
) {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (active) {
|
|
9
|
+
window.addEventListener('keydown', handler);
|
|
10
|
+
|
|
11
|
+
return () => {
|
|
12
|
+
window.removeEventListener('keydown', handler);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return;
|
|
17
|
+
}, [active, handler]);
|
|
18
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useReducer, Reducer } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface StateChart<STypes extends string, ATypes extends string> {
|
|
4
|
+
initial: STypes;
|
|
5
|
+
states: {
|
|
6
|
+
[state in STypes]?: {
|
|
7
|
+
enter?: (prevState: STypes, payload: any) => void;
|
|
8
|
+
leave?: (nextState: STypes, payload: any) => void;
|
|
9
|
+
on: { [action in ATypes]?: STypes };
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface StateMachineState<STypes, ATypes> {
|
|
15
|
+
state: STypes;
|
|
16
|
+
lastActionType: ATypes;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StateMachineAction<STypes, ATypes> {
|
|
20
|
+
type: ATypes;
|
|
21
|
+
nextState: STypes;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type TransitionType<ATypes extends string> = (
|
|
25
|
+
action: ATypes,
|
|
26
|
+
payload: any
|
|
27
|
+
) => void;
|
|
28
|
+
|
|
29
|
+
// This manages transitions between states with a built in reducer to manage
|
|
30
|
+
// the data that goes with those transitions.
|
|
31
|
+
export function useReducerMachine<
|
|
32
|
+
S extends StateMachineState<STypes, ATypes>,
|
|
33
|
+
A extends StateMachineAction<STypes, ATypes>,
|
|
34
|
+
STypes extends string,
|
|
35
|
+
ATypes extends string
|
|
36
|
+
>(
|
|
37
|
+
chart: StateChart<STypes, ATypes>,
|
|
38
|
+
reducer: Reducer<S, A>,
|
|
39
|
+
initialData: S
|
|
40
|
+
): [STypes, Omit<S, 'state'>, TransitionType<ATypes>] {
|
|
41
|
+
const [reducerState, dispatch] = useReducer(reducer, initialData);
|
|
42
|
+
const { state, ...data } = reducerState;
|
|
43
|
+
|
|
44
|
+
const transition: TransitionType<ATypes> = (action, payload = {}) => {
|
|
45
|
+
const currentState = chart.states[state];
|
|
46
|
+
if (!currentState) {
|
|
47
|
+
throw new Error(`Unknown currentState "${state}"`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const nextState: STypes | undefined = currentState.on[action];
|
|
51
|
+
if (!nextState) {
|
|
52
|
+
throw new Error(`Unknown action "${action}" for state "${state}"`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
dispatch({ type: action, nextState, ...payload } as any);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return [state, data, transition];
|
|
59
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useRemoveBodyScroll(open: boolean) {
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
if (open) {
|
|
6
|
+
const locksCount = ((window as any).__SCROLL_BODY_COUNT__ || 0) + 1;
|
|
7
|
+
|
|
8
|
+
// calculate scrollbar width if mounting the first scroll lock
|
|
9
|
+
let scrollBarWidth = 0;
|
|
10
|
+
if (locksCount === 1) {
|
|
11
|
+
scrollBarWidth = window.innerWidth - document.body.clientWidth;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
document.body.style.overflow = 'hidden';
|
|
15
|
+
if (scrollBarWidth > 0) {
|
|
16
|
+
document.body.style.paddingRight = `${scrollBarWidth}px`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// save current lock count in global object
|
|
20
|
+
(window as any).__SCROLL_BODY_COUNT__ = locksCount;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return () => {
|
|
24
|
+
if (open) {
|
|
25
|
+
const locksCount = (window as any).__SCROLL_BODY_COUNT__ - 1;
|
|
26
|
+
|
|
27
|
+
if (locksCount === 0) {
|
|
28
|
+
document.body.style.overflow = '';
|
|
29
|
+
document.body.style.paddingRight = '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// save current lock count in global object
|
|
33
|
+
(window as any).__SCROLL_BODY_COUNT__ = locksCount;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}, [open]);
|
|
37
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useRef, MutableRefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
export type ScopeMatcherFn = (
|
|
4
|
+
nodeType: string,
|
|
5
|
+
props: {
|
|
6
|
+
[key: string]: string;
|
|
7
|
+
},
|
|
8
|
+
instance: Element
|
|
9
|
+
) => boolean;
|
|
10
|
+
|
|
11
|
+
export type Scope<T extends HTMLElement> = MutableRefObject<{
|
|
12
|
+
queryAllNodes: (matcherFn: ScopeMatcherFn) => T[];
|
|
13
|
+
}>;
|
|
14
|
+
|
|
15
|
+
export function getScope<T extends HTMLElement, R extends HTMLElement>(
|
|
16
|
+
rootRef: MutableRefObject<R | undefined | null>
|
|
17
|
+
) {
|
|
18
|
+
const queryAllNodes = (matcherFn: ScopeMatcherFn) => {
|
|
19
|
+
if (!rootRef.current) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const allNodes = rootRef.current.querySelectorAll('*');
|
|
24
|
+
|
|
25
|
+
const filtered: T[] = [];
|
|
26
|
+
allNodes.forEach((node) => {
|
|
27
|
+
const props = {};
|
|
28
|
+
const { attributes } = node;
|
|
29
|
+
for (let i = 0; i < attributes.length; i++) {
|
|
30
|
+
const attr = attributes[i];
|
|
31
|
+
props[attr.name] = attr.value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (matcherFn(node.tagName.toLowerCase(), props, node)) {
|
|
35
|
+
filtered.push(node as T);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return filtered;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return { queryAllNodes };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function useScope<T extends HTMLElement, R extends HTMLElement>(
|
|
46
|
+
rootRef: MutableRefObject<R | undefined | null>
|
|
47
|
+
): Scope<T> {
|
|
48
|
+
const scope: Scope<T> = useRef(getScope(rootRef));
|
|
49
|
+
|
|
50
|
+
return scope;
|
|
51
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useThrottle<T>(value: T, limit: number) {
|
|
4
|
+
const [throttledValue, setThrottledValue] = useState(value);
|
|
5
|
+
const lastRan = useRef(Date.now());
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const handler = setTimeout(() => {
|
|
9
|
+
setThrottledValue(value);
|
|
10
|
+
lastRan.current = Date.now();
|
|
11
|
+
}, limit - (Date.now() - lastRan.current));
|
|
12
|
+
|
|
13
|
+
return () => {
|
|
14
|
+
clearTimeout(handler);
|
|
15
|
+
};
|
|
16
|
+
}, [value, limit]);
|
|
17
|
+
|
|
18
|
+
return throttledValue;
|
|
19
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export * from './Accordion';
|
|
3
|
+
export * from './CheckBox';
|
|
4
|
+
export * from './ComboBox';
|
|
5
|
+
export * from './FocusLock';
|
|
6
|
+
export * from './Menu';
|
|
7
|
+
export * from './Modal';
|
|
8
|
+
export * from './Popper';
|
|
9
|
+
export * from './Portal';
|
|
10
|
+
export * from './RadioButton';
|
|
11
|
+
export * from './Spinner';
|
|
12
|
+
export * from './Tabs';
|
|
13
|
+
export * from './Tooltip';
|
|
14
|
+
|
|
15
|
+
// General React utilities
|
|
16
|
+
export * from './utils';
|
|
17
|
+
|
|
18
|
+
// Hooks
|
|
19
|
+
export * from './hooks';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { LegacyRef, MutableRefObject, Ref, RefCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
export function assignRef<T>(
|
|
4
|
+
ref: LegacyRef<T> | MutableRefObject<T> | Ref<T> | null | undefined,
|
|
5
|
+
value: T
|
|
6
|
+
) {
|
|
7
|
+
if (ref == null) return;
|
|
8
|
+
if (typeof ref === 'function') {
|
|
9
|
+
ref(value);
|
|
10
|
+
} else {
|
|
11
|
+
try {
|
|
12
|
+
(ref as MutableRefObject<T>).current = value;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
throw new Error(`Cannot assign value "${value}" to ref "${ref}"`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function assignMultipleRefs<T>(
|
|
20
|
+
...refs: (LegacyRef<T> | MutableRefObject<T> | Ref<T> | null | undefined)[]
|
|
21
|
+
): RefCallback<T> {
|
|
22
|
+
return (node: T | null) => {
|
|
23
|
+
refs.forEach((ref) => {
|
|
24
|
+
assignRef(ref, node);
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function createSubscription() {
|
|
2
|
+
const subscriptions: (() => void)[] = [];
|
|
3
|
+
|
|
4
|
+
function subscribe(fn: () => void) {
|
|
5
|
+
subscriptions.push(fn);
|
|
6
|
+
return () => {
|
|
7
|
+
subscriptions.splice(subscriptions.indexOf(fn), 1);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function notify() {
|
|
12
|
+
subscriptions.forEach((fn) => fn());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return { subscribe, notify };
|
|
16
|
+
}
|