@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/Popover.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// Ported from @radix-ui/react-popover (source:
|
|
2
|
+
// .radix-primitives/packages/react/popover/src/popover.tsx). A popper-positioned,
|
|
3
|
+
// dismissable panel: Trigger doubles as the Popper anchor (unless a custom Anchor is
|
|
4
|
+
// rendered), Portal + Presence mount Content into document.body, and Content composes
|
|
5
|
+
// FocusScope (trap/loop when modal) + DismissableLayer (Escape / outside interactions)
|
|
6
|
+
// on top of Popper.Content. Modal content hides the rest of the app from ATs
|
|
7
|
+
// (`aria-hidden`'s hideOthers) and locks body scroll (see scroll-lock.ts for the
|
|
8
|
+
// react-remove-scroll divergence note).
|
|
9
|
+
import { createElement, useCallback, useEffect, useRef, useState } from 'octane';
|
|
10
|
+
import { hideOthers } from 'aria-hidden';
|
|
11
|
+
|
|
12
|
+
import { composeEventHandlers } from './compose-event-handlers';
|
|
13
|
+
import { useComposedRefs } from './compose-refs';
|
|
14
|
+
import { createContextScope } from './context';
|
|
15
|
+
import { DismissableLayer } from './DismissableLayer';
|
|
16
|
+
import { FocusScope } from './FocusScope';
|
|
17
|
+
import { useFocusGuards } from './focus-guards';
|
|
18
|
+
import { S, subSlot } from './internal';
|
|
19
|
+
import * as PopperPrimitive from './Popper';
|
|
20
|
+
import { createPopperScope } from './Popper';
|
|
21
|
+
import { Portal as PortalPrimitive } from './Portal';
|
|
22
|
+
import { Presence } from './Presence';
|
|
23
|
+
import { Primitive } from './Primitive';
|
|
24
|
+
import { useScrollLock } from './scroll-lock';
|
|
25
|
+
import { useControllableState } from './useControllableState';
|
|
26
|
+
import { useId } from './useId';
|
|
27
|
+
|
|
28
|
+
const POPOVER_NAME = 'Popover';
|
|
29
|
+
|
|
30
|
+
const [createPopoverContext, createPopoverScope] = createContextScope(POPOVER_NAME, [
|
|
31
|
+
createPopperScope,
|
|
32
|
+
]);
|
|
33
|
+
export { createPopoverScope };
|
|
34
|
+
const usePopperScope = createPopperScope();
|
|
35
|
+
|
|
36
|
+
interface PopoverContextValue {
|
|
37
|
+
triggerRef: { current: HTMLElement | null };
|
|
38
|
+
contentId: string;
|
|
39
|
+
open: boolean;
|
|
40
|
+
onOpenChange(open: boolean): void;
|
|
41
|
+
onOpenToggle(): void;
|
|
42
|
+
hasCustomAnchor: boolean;
|
|
43
|
+
onCustomAnchorAdd(): void;
|
|
44
|
+
onCustomAnchorRemove(): void;
|
|
45
|
+
modal: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const [PopoverProvider, usePopoverContext] =
|
|
49
|
+
createPopoverContext<PopoverContextValue>(POPOVER_NAME);
|
|
50
|
+
|
|
51
|
+
export function Root(props: any): any {
|
|
52
|
+
const slot = S('Popover.Root');
|
|
53
|
+
const {
|
|
54
|
+
__scopePopover,
|
|
55
|
+
children,
|
|
56
|
+
open: openProp,
|
|
57
|
+
defaultOpen,
|
|
58
|
+
onOpenChange,
|
|
59
|
+
modal = false,
|
|
60
|
+
} = props ?? {};
|
|
61
|
+
const popperScope = usePopperScope(__scopePopover, subSlot(slot, 'popper'));
|
|
62
|
+
const triggerRef = useRef<HTMLElement | null>(null, subSlot(slot, 'trigger'));
|
|
63
|
+
const [hasCustomAnchor, setHasCustomAnchor] = useState(false, subSlot(slot, 'anchor'));
|
|
64
|
+
const [open, setOpen] = useControllableState<boolean>(
|
|
65
|
+
{ prop: openProp, defaultProp: defaultOpen ?? false, onChange: onOpenChange },
|
|
66
|
+
subSlot(slot, 'open'),
|
|
67
|
+
);
|
|
68
|
+
return createElement(PopperPrimitive.Root, {
|
|
69
|
+
...popperScope,
|
|
70
|
+
children: createElement(PopoverProvider, {
|
|
71
|
+
scope: __scopePopover,
|
|
72
|
+
contentId: useId(subSlot(slot, 'contentId')),
|
|
73
|
+
triggerRef,
|
|
74
|
+
open,
|
|
75
|
+
onOpenChange: setOpen,
|
|
76
|
+
onOpenToggle: useCallback(
|
|
77
|
+
() => setOpen((prevOpen) => !prevOpen),
|
|
78
|
+
[setOpen],
|
|
79
|
+
subSlot(slot, 'toggle'),
|
|
80
|
+
),
|
|
81
|
+
hasCustomAnchor,
|
|
82
|
+
onCustomAnchorAdd: useCallback(() => setHasCustomAnchor(true), [], subSlot(slot, 'add')),
|
|
83
|
+
onCustomAnchorRemove: useCallback(
|
|
84
|
+
() => setHasCustomAnchor(false),
|
|
85
|
+
[],
|
|
86
|
+
subSlot(slot, 'remove'),
|
|
87
|
+
),
|
|
88
|
+
modal,
|
|
89
|
+
children,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function Anchor(props: any): any {
|
|
95
|
+
const slot = S('Popover.Anchor');
|
|
96
|
+
const { __scopePopover, ...anchorProps } = props ?? {};
|
|
97
|
+
const context = usePopoverContext('PopoverAnchor', __scopePopover);
|
|
98
|
+
const popperScope = usePopperScope(__scopePopover, subSlot(slot, 'popper'));
|
|
99
|
+
const { onCustomAnchorAdd, onCustomAnchorRemove } = context;
|
|
100
|
+
|
|
101
|
+
useEffect(
|
|
102
|
+
() => {
|
|
103
|
+
onCustomAnchorAdd();
|
|
104
|
+
return () => onCustomAnchorRemove();
|
|
105
|
+
},
|
|
106
|
+
[onCustomAnchorAdd, onCustomAnchorRemove],
|
|
107
|
+
subSlot(slot, 'e:custom'),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return createElement(PopperPrimitive.Anchor, { ...popperScope, ...anchorProps });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function Trigger(props: any): any {
|
|
114
|
+
const slot = S('Popover.Trigger');
|
|
115
|
+
const { __scopePopover, ref: forwardedRef, ...triggerProps } = props ?? {};
|
|
116
|
+
const context = usePopoverContext('PopoverTrigger', __scopePopover);
|
|
117
|
+
const popperScope = usePopperScope(__scopePopover, subSlot(slot, 'popper'));
|
|
118
|
+
const composedTriggerRef = useComposedRefs(
|
|
119
|
+
forwardedRef,
|
|
120
|
+
context.triggerRef,
|
|
121
|
+
subSlot(slot, 'refs'),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const trigger = createElement(Primitive.button, {
|
|
125
|
+
type: 'button',
|
|
126
|
+
'aria-haspopup': 'dialog',
|
|
127
|
+
'aria-expanded': context.open,
|
|
128
|
+
'aria-controls': context.open ? context.contentId : undefined,
|
|
129
|
+
'data-state': getState(context.open),
|
|
130
|
+
...triggerProps,
|
|
131
|
+
ref: composedTriggerRef,
|
|
132
|
+
onClick: composeEventHandlers(props?.onClick, context.onOpenToggle),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return context.hasCustomAnchor
|
|
136
|
+
? trigger
|
|
137
|
+
: createElement(PopperPrimitive.Anchor, { asChild: true, ...popperScope, children: trigger });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const [PortalProvider, usePortalContext] = createPopoverContext<{ forceMount?: boolean }>(
|
|
141
|
+
'PopoverPortal',
|
|
142
|
+
{ forceMount: undefined },
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Mounts its children into `container` (default document.body) through `Presence`.
|
|
147
|
+
* Unlike Dialog.Portal (which maps each child into its own Presence), Popover's
|
|
148
|
+
* source wraps ALL children in a single Presence. octane children convention: pass
|
|
149
|
+
* children at a prop/value position (`children={[<Content/>]}`); a function child
|
|
150
|
+
* is portal'd as a single unit.
|
|
151
|
+
*/
|
|
152
|
+
export function Portal(props: any): any {
|
|
153
|
+
const { __scopePopover, forceMount, children, container } = props ?? {};
|
|
154
|
+
const context = usePopoverContext('PopoverPortal', __scopePopover);
|
|
155
|
+
return createElement(PortalProvider, {
|
|
156
|
+
scope: __scopePopover,
|
|
157
|
+
forceMount,
|
|
158
|
+
children: createElement(Presence, {
|
|
159
|
+
present: forceMount || context.open,
|
|
160
|
+
children: createElement(PortalPrimitive, {
|
|
161
|
+
asChild: typeof children !== 'function',
|
|
162
|
+
container,
|
|
163
|
+
children,
|
|
164
|
+
}),
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function Content(props: any): any {
|
|
170
|
+
const portalContext = usePortalContext('PopoverContent', props?.__scopePopover);
|
|
171
|
+
const { forceMount = portalContext.forceMount, ...contentProps } = props ?? {};
|
|
172
|
+
const context = usePopoverContext('PopoverContent', props?.__scopePopover);
|
|
173
|
+
return createElement(Presence, {
|
|
174
|
+
present: forceMount || context.open,
|
|
175
|
+
children: context.modal
|
|
176
|
+
? createElement(ContentModal, contentProps)
|
|
177
|
+
: createElement(ContentNonModal, contentProps),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function ContentModal(props: any): any {
|
|
182
|
+
const slot = S('Popover.ContentModal');
|
|
183
|
+
const { ref: forwardedRef, ...rest } = props;
|
|
184
|
+
const context = usePopoverContext('PopoverContent', props.__scopePopover);
|
|
185
|
+
const contentRef = useRef<HTMLElement | null>(null, subSlot(slot, 'ref'));
|
|
186
|
+
const composedRefs = useComposedRefs(forwardedRef, contentRef, subSlot(slot, 'refs'));
|
|
187
|
+
const isRightClickOutsideRef = useRef(false, subSlot(slot, 'rightClick'));
|
|
188
|
+
|
|
189
|
+
// aria-hide everything except the content (better supported equivalent to aria-modal).
|
|
190
|
+
useEffect(
|
|
191
|
+
() => {
|
|
192
|
+
const content = contentRef.current;
|
|
193
|
+
if (content) return hideOthers(content);
|
|
194
|
+
},
|
|
195
|
+
[],
|
|
196
|
+
subSlot(slot, 'e:hide'),
|
|
197
|
+
);
|
|
198
|
+
// Radix wraps modal content in `react-remove-scroll` (as a Slot — no wrapper DOM); the
|
|
199
|
+
// octane equivalent is the useScrollLock hook (see scroll-lock.ts).
|
|
200
|
+
useScrollLock(true, subSlot(slot, 'lock'));
|
|
201
|
+
|
|
202
|
+
return createElement(ContentImpl, {
|
|
203
|
+
...rest,
|
|
204
|
+
__scopePopover: props.__scopePopover,
|
|
205
|
+
ref: composedRefs,
|
|
206
|
+
// We make sure we're not trapping once it's been closed
|
|
207
|
+
// (closed !== unmounted when animating out).
|
|
208
|
+
trapFocus: context.open,
|
|
209
|
+
disableOutsidePointerEvents: true,
|
|
210
|
+
onCloseAutoFocus: composeEventHandlers(props.onCloseAutoFocus, (event: Event) => {
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
if (!isRightClickOutsideRef.current) context.triggerRef.current?.focus();
|
|
213
|
+
}),
|
|
214
|
+
onPointerDownOutside: composeEventHandlers(
|
|
215
|
+
props.onPointerDownOutside,
|
|
216
|
+
(event: any) => {
|
|
217
|
+
const originalEvent = event.detail.originalEvent;
|
|
218
|
+
const ctrlLeftClick = originalEvent.button === 0 && originalEvent.ctrlKey === true;
|
|
219
|
+
const isRightClick = originalEvent.button === 2 || ctrlLeftClick;
|
|
220
|
+
isRightClickOutsideRef.current = isRightClick;
|
|
221
|
+
},
|
|
222
|
+
{ checkForDefaultPrevented: false },
|
|
223
|
+
),
|
|
224
|
+
// When focus is trapped, a `focusout` event may still happen —
|
|
225
|
+
// make sure we don't trigger our `onDismiss` in such case.
|
|
226
|
+
onFocusOutside: composeEventHandlers(
|
|
227
|
+
props.onFocusOutside,
|
|
228
|
+
(event: Event) => event.preventDefault(),
|
|
229
|
+
{ checkForDefaultPrevented: false },
|
|
230
|
+
),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function ContentNonModal(props: any): any {
|
|
235
|
+
const slot = S('Popover.ContentNonModal');
|
|
236
|
+
const context = usePopoverContext('PopoverContent', props.__scopePopover);
|
|
237
|
+
const hasInteractedOutsideRef = useRef(false, subSlot(slot, 'interacted'));
|
|
238
|
+
const hasPointerDownOutsideRef = useRef(false, subSlot(slot, 'pointer'));
|
|
239
|
+
|
|
240
|
+
return createElement(ContentImpl, {
|
|
241
|
+
...props,
|
|
242
|
+
trapFocus: false,
|
|
243
|
+
disableOutsidePointerEvents: false,
|
|
244
|
+
onCloseAutoFocus: (event: Event) => {
|
|
245
|
+
props.onCloseAutoFocus?.(event);
|
|
246
|
+
if (!event.defaultPrevented) {
|
|
247
|
+
if (!hasInteractedOutsideRef.current) context.triggerRef.current?.focus();
|
|
248
|
+
// Always prevent auto focus because we either focus manually or want user agent focus.
|
|
249
|
+
event.preventDefault();
|
|
250
|
+
}
|
|
251
|
+
hasInteractedOutsideRef.current = false;
|
|
252
|
+
hasPointerDownOutsideRef.current = false;
|
|
253
|
+
},
|
|
254
|
+
onInteractOutside: (event: any) => {
|
|
255
|
+
props.onInteractOutside?.(event);
|
|
256
|
+
if (!event.defaultPrevented) {
|
|
257
|
+
hasInteractedOutsideRef.current = true;
|
|
258
|
+
if (event.detail.originalEvent.type === 'pointerdown') {
|
|
259
|
+
hasPointerDownOutsideRef.current = true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Prevent dismissing when clicking the trigger. As the trigger is already
|
|
263
|
+
// setup to close, without doing so it would close and immediately open.
|
|
264
|
+
const target = event.target as HTMLElement;
|
|
265
|
+
const targetIsTrigger = context.triggerRef.current?.contains(target);
|
|
266
|
+
if (targetIsTrigger) event.preventDefault();
|
|
267
|
+
// On Safari, if the trigger is inside a container with tabIndex={0}, clicking the
|
|
268
|
+
// trigger gives a pointer-down outside on it and then a focus outside on the
|
|
269
|
+
// container — ignore any focus outside after a pointer down outside.
|
|
270
|
+
if (event.detail.originalEvent.type === 'focusin' && hasPointerDownOutsideRef.current) {
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function ContentImpl(props: any): any {
|
|
278
|
+
const slot = S('Popover.ContentImpl');
|
|
279
|
+
const {
|
|
280
|
+
__scopePopover,
|
|
281
|
+
trapFocus,
|
|
282
|
+
onOpenAutoFocus,
|
|
283
|
+
onCloseAutoFocus,
|
|
284
|
+
disableOutsidePointerEvents,
|
|
285
|
+
onEscapeKeyDown,
|
|
286
|
+
onPointerDownOutside,
|
|
287
|
+
onFocusOutside,
|
|
288
|
+
onInteractOutside,
|
|
289
|
+
...contentProps
|
|
290
|
+
} = props;
|
|
291
|
+
const context = usePopoverContext('PopoverContent', __scopePopover);
|
|
292
|
+
const popperScope = usePopperScope(__scopePopover, subSlot(slot, 'popper'));
|
|
293
|
+
|
|
294
|
+
// Make sure the whole tree has focus guards as our `Popover` may be
|
|
295
|
+
// the last element in the DOM (because of the `Portal`).
|
|
296
|
+
useFocusGuards(subSlot(slot, 'guards'));
|
|
297
|
+
|
|
298
|
+
return createElement(FocusScope, {
|
|
299
|
+
asChild: true,
|
|
300
|
+
loop: true,
|
|
301
|
+
trapped: trapFocus,
|
|
302
|
+
onMountAutoFocus: onOpenAutoFocus,
|
|
303
|
+
onUnmountAutoFocus: onCloseAutoFocus,
|
|
304
|
+
children: createElement(DismissableLayer, {
|
|
305
|
+
asChild: true,
|
|
306
|
+
disableOutsidePointerEvents,
|
|
307
|
+
onInteractOutside,
|
|
308
|
+
onEscapeKeyDown,
|
|
309
|
+
onPointerDownOutside,
|
|
310
|
+
onFocusOutside,
|
|
311
|
+
onDismiss: () => context.onOpenChange(false),
|
|
312
|
+
deferPointerDownOutside: true,
|
|
313
|
+
children: createElement(PopperPrimitive.Content, {
|
|
314
|
+
'data-state': getState(context.open),
|
|
315
|
+
role: 'dialog',
|
|
316
|
+
id: context.contentId,
|
|
317
|
+
...popperScope,
|
|
318
|
+
...contentProps,
|
|
319
|
+
style: {
|
|
320
|
+
...contentProps.style,
|
|
321
|
+
// re-namespace exposed content custom properties
|
|
322
|
+
'--radix-popover-content-transform-origin': 'var(--radix-popper-transform-origin)',
|
|
323
|
+
'--radix-popover-content-available-width': 'var(--radix-popper-available-width)',
|
|
324
|
+
'--radix-popover-content-available-height': 'var(--radix-popper-available-height)',
|
|
325
|
+
'--radix-popover-trigger-width': 'var(--radix-popper-anchor-width)',
|
|
326
|
+
'--radix-popover-trigger-height': 'var(--radix-popper-anchor-height)',
|
|
327
|
+
},
|
|
328
|
+
}),
|
|
329
|
+
}),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function Close(props: any): any {
|
|
334
|
+
const { __scopePopover, ...closeProps } = props ?? {};
|
|
335
|
+
const context = usePopoverContext('PopoverClose', __scopePopover);
|
|
336
|
+
return createElement(Primitive.button, {
|
|
337
|
+
type: 'button',
|
|
338
|
+
...closeProps,
|
|
339
|
+
onClick: composeEventHandlers(props?.onClick, () => context.onOpenChange(false)),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function Arrow(props: any): any {
|
|
344
|
+
const slot = S('Popover.Arrow');
|
|
345
|
+
const { __scopePopover, ...arrowProps } = props ?? {};
|
|
346
|
+
const popperScope = usePopperScope(__scopePopover, subSlot(slot, 'popper'));
|
|
347
|
+
return createElement(PopperPrimitive.Arrow, { ...popperScope, ...arrowProps });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function getState(open?: boolean): 'open' | 'closed' {
|
|
351
|
+
return open ? 'open' : 'closed';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export { Root as Popover, Anchor as PopoverAnchor, Trigger as PopoverTrigger };
|