@octanejs/radix 0.1.2
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/LICENSE +21 -0
- package/README.md +100 -0
- package/package.json +45 -0
- package/src/AccessibleIcon.ts +26 -0
- package/src/Accordion.ts +282 -0
- package/src/AlertDialog.ts +110 -0
- package/src/Arrow.ts +21 -0
- package/src/AspectRatio.ts +34 -0
- package/src/Avatar.ts +152 -0
- package/src/Checkbox.ts +325 -0
- package/src/Collapsible.ts +170 -0
- package/src/ContextMenu.ts +303 -0
- package/src/Dialog.ts +308 -0
- package/src/DismissableLayer.ts +413 -0
- package/src/DropdownMenu.ts +275 -0
- package/src/FocusScope.ts +266 -0
- package/src/Form.ts +621 -0
- package/src/HoverCard.ts +318 -0
- package/src/Label.ts +25 -0
- package/src/Menu.ts +1057 -0
- package/src/Menubar.ts +572 -0
- package/src/NavigationMenu.ts +1236 -0
- package/src/OneTimePasswordField.ts +977 -0
- package/src/PasswordToggleField.ts +495 -0
- package/src/Popover.ts +354 -0
- package/src/Popper.ts +401 -0
- package/src/Portal.ts +19 -0
- package/src/Presence.ts +225 -0
- package/src/Primitive.ts +53 -0
- package/src/Progress.ts +92 -0
- package/src/Radio.ts +262 -0
- package/src/RadioGroup.ts +212 -0
- package/src/RovingFocusGroup.ts +259 -0
- package/src/ScrollArea.ts +966 -0
- package/src/Select.ts +1901 -0
- package/src/Separator.ts +36 -0
- package/src/Slider.ts +745 -0
- package/src/Slot.ts +105 -0
- package/src/Switch.ts +278 -0
- package/src/Tabs.ts +177 -0
- package/src/Toast.ts +923 -0
- package/src/Toggle.ts +31 -0
- package/src/ToggleGroup.ts +157 -0
- package/src/Toolbar.ts +130 -0
- package/src/Tooltip.ts +669 -0
- package/src/VisuallyHidden.ts +29 -0
- package/src/collection.ts +97 -0
- package/src/compose-event-handlers.ts +15 -0
- package/src/compose-refs.ts +53 -0
- package/src/context.ts +109 -0
- package/src/direction.ts +19 -0
- package/src/focus-guards.ts +56 -0
- package/src/index.ts +54 -0
- package/src/internal.ts +42 -0
- package/src/scroll-lock.ts +57 -0
- package/src/use-callback-ref.ts +28 -0
- package/src/use-effect-event.ts +36 -0
- package/src/use-is-hydrated.ts +23 -0
- package/src/use-previous.ts +28 -0
- package/src/use-size.ts +57 -0
- package/src/useControllableState.ts +78 -0
- package/src/useId.ts +15 -0
package/src/HoverCard.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// Ported from @radix-ui/react-hover-card (source:
|
|
2
|
+
// .radix-primitives/packages/react/hover-card/src/hover-card.tsx). A link-preview card
|
|
3
|
+
// for sighted pointer users: Trigger (an anchor, doubling as the Popper anchor) opens
|
|
4
|
+
// after `openDelay` on pointer-enter/focus and closes after `closeDelay` on
|
|
5
|
+
// pointer-leave/blur — unless the user is selecting text in the card or has the pointer
|
|
6
|
+
// down on it. Content composes DismissableLayer over Popper.Content, contains text
|
|
7
|
+
// selection to the card while selecting, and removes its tabbables from the tab order.
|
|
8
|
+
import { createElement, useCallback, useEffect, useRef, useState } from 'octane';
|
|
9
|
+
|
|
10
|
+
import { composeEventHandlers } from './compose-event-handlers';
|
|
11
|
+
import { useComposedRefs } from './compose-refs';
|
|
12
|
+
import { createContextScope } from './context';
|
|
13
|
+
import { DismissableLayer } from './DismissableLayer';
|
|
14
|
+
import { S, subSlot } from './internal';
|
|
15
|
+
import * as PopperPrimitive from './Popper';
|
|
16
|
+
import { createPopperScope } from './Popper';
|
|
17
|
+
import { Portal as PortalPrimitive } from './Portal';
|
|
18
|
+
import { Presence } from './Presence';
|
|
19
|
+
import { Primitive } from './Primitive';
|
|
20
|
+
import { useControllableState } from './useControllableState';
|
|
21
|
+
|
|
22
|
+
let originalBodyUserSelect: string;
|
|
23
|
+
|
|
24
|
+
const HOVERCARD_NAME = 'HoverCard';
|
|
25
|
+
|
|
26
|
+
const [createHoverCardContext, createHoverCardScope] = createContextScope(HOVERCARD_NAME, [
|
|
27
|
+
createPopperScope,
|
|
28
|
+
]);
|
|
29
|
+
export { createHoverCardScope };
|
|
30
|
+
const usePopperScope = createPopperScope();
|
|
31
|
+
|
|
32
|
+
interface HoverCardContextValue {
|
|
33
|
+
open: boolean;
|
|
34
|
+
onOpenChange(open: boolean): void;
|
|
35
|
+
onOpen(): void;
|
|
36
|
+
onClose(): void;
|
|
37
|
+
onDismiss(): void;
|
|
38
|
+
hasSelectionRef: { current: boolean };
|
|
39
|
+
isPointerDownOnContentRef: { current: boolean };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const [HoverCardProvider, useHoverCardContext] =
|
|
43
|
+
createHoverCardContext<HoverCardContextValue>(HOVERCARD_NAME);
|
|
44
|
+
|
|
45
|
+
export function Root(props: any): any {
|
|
46
|
+
const slot = S('HoverCard.Root');
|
|
47
|
+
const {
|
|
48
|
+
__scopeHoverCard,
|
|
49
|
+
children,
|
|
50
|
+
open: openProp,
|
|
51
|
+
defaultOpen,
|
|
52
|
+
onOpenChange,
|
|
53
|
+
openDelay = 700,
|
|
54
|
+
closeDelay = 300,
|
|
55
|
+
} = props ?? {};
|
|
56
|
+
const popperScope = usePopperScope(__scopeHoverCard, subSlot(slot, 'popper'));
|
|
57
|
+
const openTimerRef = useRef(0, subSlot(slot, 'openTimer'));
|
|
58
|
+
const closeTimerRef = useRef(0, subSlot(slot, 'closeTimer'));
|
|
59
|
+
const hasSelectionRef = useRef(false, subSlot(slot, 'selection'));
|
|
60
|
+
const isPointerDownOnContentRef = useRef(false, subSlot(slot, 'pointerDown'));
|
|
61
|
+
|
|
62
|
+
const [open, setOpen] = useControllableState<boolean>(
|
|
63
|
+
{ prop: openProp, defaultProp: defaultOpen ?? false, onChange: onOpenChange },
|
|
64
|
+
subSlot(slot, 'open'),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const handleOpen = useCallback(
|
|
68
|
+
() => {
|
|
69
|
+
clearTimeout(closeTimerRef.current);
|
|
70
|
+
openTimerRef.current = window.setTimeout(() => setOpen(true), openDelay);
|
|
71
|
+
},
|
|
72
|
+
[openDelay, setOpen],
|
|
73
|
+
subSlot(slot, 'handleOpen'),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const handleClose = useCallback(
|
|
77
|
+
() => {
|
|
78
|
+
clearTimeout(openTimerRef.current);
|
|
79
|
+
if (!hasSelectionRef.current && !isPointerDownOnContentRef.current) {
|
|
80
|
+
closeTimerRef.current = window.setTimeout(() => setOpen(false), closeDelay);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
[closeDelay, setOpen],
|
|
84
|
+
subSlot(slot, 'handleClose'),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const handleDismiss = useCallback(() => setOpen(false), [setOpen], subSlot(slot, 'dismiss'));
|
|
88
|
+
|
|
89
|
+
// cleanup any queued state updates on unmount
|
|
90
|
+
useEffect(
|
|
91
|
+
() => {
|
|
92
|
+
return () => {
|
|
93
|
+
clearTimeout(openTimerRef.current);
|
|
94
|
+
clearTimeout(closeTimerRef.current);
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
[],
|
|
98
|
+
subSlot(slot, 'e:cleanup'),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return createElement(HoverCardProvider, {
|
|
102
|
+
scope: __scopeHoverCard,
|
|
103
|
+
open,
|
|
104
|
+
onOpenChange: setOpen,
|
|
105
|
+
onOpen: handleOpen,
|
|
106
|
+
onClose: handleClose,
|
|
107
|
+
onDismiss: handleDismiss,
|
|
108
|
+
hasSelectionRef,
|
|
109
|
+
isPointerDownOnContentRef,
|
|
110
|
+
children: createElement(PopperPrimitive.Root, { ...popperScope, children }),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function Trigger(props: any): any {
|
|
115
|
+
const slot = S('HoverCard.Trigger');
|
|
116
|
+
const { __scopeHoverCard, ...triggerProps } = props ?? {};
|
|
117
|
+
const context = useHoverCardContext('HoverCardTrigger', __scopeHoverCard);
|
|
118
|
+
const popperScope = usePopperScope(__scopeHoverCard, subSlot(slot, 'popper'));
|
|
119
|
+
return createElement(PopperPrimitive.Anchor, {
|
|
120
|
+
asChild: true,
|
|
121
|
+
...popperScope,
|
|
122
|
+
children: createElement(Primitive.a, {
|
|
123
|
+
'data-state': context.open ? 'open' : 'closed',
|
|
124
|
+
...triggerProps,
|
|
125
|
+
onPointerEnter: composeEventHandlers(props?.onPointerEnter, excludeTouch(context.onOpen)),
|
|
126
|
+
onPointerLeave: composeEventHandlers(props?.onPointerLeave, excludeTouch(context.onClose)),
|
|
127
|
+
onFocus: composeEventHandlers(props?.onFocus, context.onOpen),
|
|
128
|
+
onBlur: composeEventHandlers(props?.onBlur, context.onClose),
|
|
129
|
+
// prevent focus event on touch devices
|
|
130
|
+
onTouchStart: composeEventHandlers(props?.onTouchStart, (event: Event) =>
|
|
131
|
+
event.preventDefault(),
|
|
132
|
+
),
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const [PortalProvider, usePortalContext] = createHoverCardContext<{ forceMount?: boolean }>(
|
|
138
|
+
'HoverCardPortal',
|
|
139
|
+
{ forceMount: undefined },
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Mounts its children into `container` (default document.body) through `Presence`.
|
|
144
|
+
* octane children convention: pass children at a prop/value position
|
|
145
|
+
* (`children={[<Content/>]}`); a function child is portal'd as a single unit.
|
|
146
|
+
*/
|
|
147
|
+
export function Portal(props: any): any {
|
|
148
|
+
const { __scopeHoverCard, forceMount, children, container } = props ?? {};
|
|
149
|
+
const context = useHoverCardContext('HoverCardPortal', __scopeHoverCard);
|
|
150
|
+
return createElement(PortalProvider, {
|
|
151
|
+
scope: __scopeHoverCard,
|
|
152
|
+
forceMount,
|
|
153
|
+
children: createElement(Presence, {
|
|
154
|
+
present: forceMount || context.open,
|
|
155
|
+
children: createElement(PortalPrimitive, {
|
|
156
|
+
asChild: typeof children !== 'function',
|
|
157
|
+
container,
|
|
158
|
+
children,
|
|
159
|
+
}),
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function Content(props: any): any {
|
|
165
|
+
const portalContext = usePortalContext('HoverCardContent', props?.__scopeHoverCard);
|
|
166
|
+
const { forceMount = portalContext.forceMount, ...contentProps } = props ?? {};
|
|
167
|
+
const context = useHoverCardContext('HoverCardContent', props?.__scopeHoverCard);
|
|
168
|
+
return createElement(Presence, {
|
|
169
|
+
present: forceMount || context.open,
|
|
170
|
+
children: createElement(ContentImpl, {
|
|
171
|
+
'data-state': context.open ? 'open' : 'closed',
|
|
172
|
+
...contentProps,
|
|
173
|
+
onPointerEnter: composeEventHandlers(props?.onPointerEnter, excludeTouch(context.onOpen)),
|
|
174
|
+
onPointerLeave: composeEventHandlers(props?.onPointerLeave, excludeTouch(context.onClose)),
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function ContentImpl(props: any): any {
|
|
180
|
+
const slot = S('HoverCard.ContentImpl');
|
|
181
|
+
const {
|
|
182
|
+
__scopeHoverCard,
|
|
183
|
+
ref: forwardedRef,
|
|
184
|
+
onEscapeKeyDown,
|
|
185
|
+
onPointerDownOutside,
|
|
186
|
+
onFocusOutside,
|
|
187
|
+
onInteractOutside,
|
|
188
|
+
...contentProps
|
|
189
|
+
} = props;
|
|
190
|
+
const context = useHoverCardContext('HoverCardContent', __scopeHoverCard);
|
|
191
|
+
const popperScope = usePopperScope(__scopeHoverCard, subSlot(slot, 'popper'));
|
|
192
|
+
const ref = useRef<HTMLElement | null>(null, subSlot(slot, 'ref'));
|
|
193
|
+
const composedRefs = useComposedRefs(forwardedRef, ref, subSlot(slot, 'refs'));
|
|
194
|
+
const [containSelection, setContainSelection] = useState(false, subSlot(slot, 'contain'));
|
|
195
|
+
|
|
196
|
+
useEffect(
|
|
197
|
+
() => {
|
|
198
|
+
if (containSelection) {
|
|
199
|
+
const body = document.body;
|
|
200
|
+
// Safari requires prefix
|
|
201
|
+
originalBodyUserSelect = body.style.userSelect || (body.style as any).webkitUserSelect;
|
|
202
|
+
body.style.userSelect = 'none';
|
|
203
|
+
(body.style as any).webkitUserSelect = 'none';
|
|
204
|
+
return () => {
|
|
205
|
+
body.style.userSelect = originalBodyUserSelect;
|
|
206
|
+
(body.style as any).webkitUserSelect = originalBodyUserSelect;
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
[containSelection],
|
|
211
|
+
subSlot(slot, 'e:selection'),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
useEffect(
|
|
215
|
+
() => {
|
|
216
|
+
if (ref.current) {
|
|
217
|
+
const handlePointerUp = (): void => {
|
|
218
|
+
setContainSelection(false);
|
|
219
|
+
context.isPointerDownOnContentRef.current = false;
|
|
220
|
+
// Delay a frame to ensure we always access the latest selection
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
const hasSelection = document.getSelection()?.toString() !== '';
|
|
223
|
+
if (hasSelection) context.hasSelectionRef.current = true;
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
document.addEventListener('pointerup', handlePointerUp);
|
|
227
|
+
return () => {
|
|
228
|
+
document.removeEventListener('pointerup', handlePointerUp);
|
|
229
|
+
context.hasSelectionRef.current = false;
|
|
230
|
+
context.isPointerDownOnContentRef.current = false;
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
[context.isPointerDownOnContentRef, context.hasSelectionRef],
|
|
235
|
+
subSlot(slot, 'e:pointerup'),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Remove the card's tabbables from the tab order (runs after every render —
|
|
239
|
+
// no deps, matching Radix).
|
|
240
|
+
useEffect(
|
|
241
|
+
() => {
|
|
242
|
+
if (ref.current) {
|
|
243
|
+
const tabbables = getTabbableNodes(ref.current);
|
|
244
|
+
tabbables.forEach((tabbable) => tabbable.setAttribute('tabindex', '-1'));
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
undefined,
|
|
248
|
+
subSlot(slot, 'e:tabbables'),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
return createElement(DismissableLayer, {
|
|
252
|
+
asChild: true,
|
|
253
|
+
disableOutsidePointerEvents: false,
|
|
254
|
+
onInteractOutside,
|
|
255
|
+
onEscapeKeyDown,
|
|
256
|
+
onPointerDownOutside,
|
|
257
|
+
onFocusOutside: composeEventHandlers(onFocusOutside, (event: Event) => {
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
}),
|
|
260
|
+
onDismiss: context.onDismiss,
|
|
261
|
+
children: createElement(PopperPrimitive.Content, {
|
|
262
|
+
...popperScope,
|
|
263
|
+
...contentProps,
|
|
264
|
+
onPointerDown: composeEventHandlers(contentProps.onPointerDown, (event: PointerEvent) => {
|
|
265
|
+
// Contain selection to current layer
|
|
266
|
+
if ((event.currentTarget as HTMLElement).contains(event.target as HTMLElement)) {
|
|
267
|
+
setContainSelection(true);
|
|
268
|
+
}
|
|
269
|
+
context.hasSelectionRef.current = false;
|
|
270
|
+
context.isPointerDownOnContentRef.current = true;
|
|
271
|
+
}),
|
|
272
|
+
ref: composedRefs,
|
|
273
|
+
style: {
|
|
274
|
+
...contentProps.style,
|
|
275
|
+
userSelect: containSelection ? 'text' : undefined,
|
|
276
|
+
// Safari requires prefix
|
|
277
|
+
WebkitUserSelect: containSelection ? 'text' : undefined,
|
|
278
|
+
// re-namespace exposed content custom properties
|
|
279
|
+
'--radix-hover-card-content-transform-origin': 'var(--radix-popper-transform-origin)',
|
|
280
|
+
'--radix-hover-card-content-available-width': 'var(--radix-popper-available-width)',
|
|
281
|
+
'--radix-hover-card-content-available-height': 'var(--radix-popper-available-height)',
|
|
282
|
+
'--radix-hover-card-trigger-width': 'var(--radix-popper-anchor-width)',
|
|
283
|
+
'--radix-hover-card-trigger-height': 'var(--radix-popper-anchor-height)',
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function Arrow(props: any): any {
|
|
290
|
+
const slot = S('HoverCard.Arrow');
|
|
291
|
+
const { __scopeHoverCard, ...arrowProps } = props ?? {};
|
|
292
|
+
const popperScope = usePopperScope(__scopeHoverCard, subSlot(slot, 'popper'));
|
|
293
|
+
return createElement(PopperPrimitive.Arrow, { ...popperScope, ...arrowProps });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function excludeTouch(eventHandler: () => void): (event: PointerEvent) => void {
|
|
297
|
+
return (event: PointerEvent) => (event.pointerType === 'touch' ? undefined : eventHandler());
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Returns a list of nodes that can be in the tab sequence.
|
|
302
|
+
* @see: https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
|
|
303
|
+
*/
|
|
304
|
+
function getTabbableNodes(container: HTMLElement): HTMLElement[] {
|
|
305
|
+
const nodes: HTMLElement[] = [];
|
|
306
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
|
|
307
|
+
acceptNode: (node: any) => {
|
|
308
|
+
// `.tabIndex` is not the same as the `tabindex` attribute. It works on the
|
|
309
|
+
// runtime's understanding of tabbability, so this automatically accounts
|
|
310
|
+
// for any kind of element that could be tabbed to.
|
|
311
|
+
return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
while (walker.nextNode()) nodes.push(walker.currentNode as HTMLElement);
|
|
315
|
+
return nodes;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export { Root as HoverCard, Trigger as HoverCardTrigger, Content as HoverCardContent };
|
package/src/Label.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Ported from @radix-ui/react-label (source:
|
|
2
|
+
// .radix-primitives/packages/react/label/src/label.tsx). A `<label>` that suppresses text
|
|
3
|
+
// selection on double-click. Faithful ordering: presses on a form control INSIDE the
|
|
4
|
+
// label return early — before the user's own `onMouseDown` runs. octane events are
|
|
5
|
+
// native + delegated, so `event.detail` / `event.defaultPrevented` are the real DOM flags.
|
|
6
|
+
import { createElement } from 'octane';
|
|
7
|
+
|
|
8
|
+
import { Primitive } from './Primitive';
|
|
9
|
+
|
|
10
|
+
export function Label(props: any): any {
|
|
11
|
+
return createElement(Primitive.label, {
|
|
12
|
+
...props,
|
|
13
|
+
onMouseDown: (event: MouseEvent) => {
|
|
14
|
+
// Only prevent text selection if clicking inside the label itself.
|
|
15
|
+
const target = event.target as HTMLElement;
|
|
16
|
+
if (target.closest('button, input, select, textarea')) return;
|
|
17
|
+
|
|
18
|
+
props?.onMouseDown?.(event);
|
|
19
|
+
// Prevent text selection when double clicking label.
|
|
20
|
+
if (!event.defaultPrevented && event.detail > 1) event.preventDefault();
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { Label as Root };
|