@octanejs/base-ui 0.1.14 → 0.1.16
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/package.json +5 -5
- package/src/alert-dialog.ts +2 -0
- package/src/button.ts +63 -0
- package/src/context-menu.ts +317 -0
- package/src/csp-provider.ts +37 -0
- package/src/dialog.ts +89 -22
- package/src/direction-provider.ts +32 -0
- package/src/index.ts +10 -0
- package/src/menu.ts +3387 -0
- package/src/menubar.ts +160 -0
- package/src/popover.ts +99 -25
- package/src/preview-card.ts +812 -0
- package/src/tooltip.ts +1053 -0
- package/src/unstable-use-media-query.ts +101 -0
- package/src/utils/CSPContext.ts +21 -0
- package/src/utils/ContextMenuRootContext.ts +44 -0
- package/src/utils/FloatingPortalLite.ts +29 -0
- package/src/utils/MenubarContext.ts +38 -0
- package/src/utils/RequestQueue.ts +132 -0
- package/src/utils/TimeoutManager.ts +32 -0
- package/src/utils/composite/grid-utils.ts +437 -0
- package/src/utils/composite/gridNavigation.ts +127 -0
- package/src/utils/composite/list-utils.ts +56 -28
- package/src/utils/composite/useCompositeRoot.ts +11 -13
- package/src/utils/constants.ts +22 -0
- package/src/utils/createChangeEventDetails.ts +4 -0
- package/src/utils/findRootOwnerId.ts +19 -0
- package/src/utils/floating/FloatingDelayGroup.ts +280 -0
- package/src/utils/floating/element.ts +36 -2
- package/src/utils/floating/gridNavigation.ts +54 -0
- package/src/utils/floating/safePolygon.ts +448 -0
- package/src/utils/floating/useClientPoint.ts +276 -0
- package/src/utils/floating/useFocus.ts +248 -0
- package/src/utils/floating/useHoverFloatingInteraction.ts +301 -11
- package/src/utils/floating/useHoverInteractionSharedState.ts +139 -0
- package/src/utils/floating/useHoverReferenceInteraction.ts +507 -14
- package/src/utils/floating/useHoverShared.ts +78 -0
- package/src/utils/floating/useListNavigation.ts +1018 -0
- package/src/utils/floating/useTypeahead.ts +287 -0
- package/src/utils/getCssDimensions.ts +28 -0
- package/src/utils/getPseudoElementBounds.ts +54 -0
- package/src/utils/platform.ts +10 -0
- package/src/utils/popups/index.ts +6 -0
- package/src/utils/popups/inlineRect.ts +289 -0
- package/src/utils/useMixedToggleClickHandler.ts +77 -0
- package/src/utils/useOpenInteractionType.ts +37 -1
- package/src/utils/usePopupAutoResize.ts +250 -0
- package/src/utils/usePopupViewport.ts +375 -0
- package/src/utils/usePreviousValue.ts +25 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/base-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -41,19 +41,19 @@
|
|
|
41
41
|
"@floating-ui/utils": "^0.2.12",
|
|
42
42
|
"aria-hidden": "^1.2.6",
|
|
43
43
|
"tabbable": "^6.5.0",
|
|
44
|
-
"@octanejs/floating-ui": "0.1.
|
|
44
|
+
"@octanejs/floating-ui": "0.1.17"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"octane": "0.1.
|
|
47
|
+
"octane": "0.1.18"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@base-ui/react": "^1.6.0",
|
|
51
|
-
"@tsrx/react": "^0.2.
|
|
51
|
+
"@tsrx/react": "^0.2.54",
|
|
52
52
|
"esbuild": "^0.28.1",
|
|
53
53
|
"react": "^19.2.7",
|
|
54
54
|
"react-dom": "^19.2.7",
|
|
55
55
|
"vitest": "^4.1.10",
|
|
56
|
-
"octane": "0.1.
|
|
56
|
+
"octane": "0.1.18"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"test": "vitest run"
|
package/src/alert-dialog.ts
CHANGED
|
@@ -40,8 +40,10 @@ export const AlertDialog = {
|
|
|
40
40
|
Portal: Dialog.Portal,
|
|
41
41
|
Backdrop: Dialog.Backdrop,
|
|
42
42
|
Popup: Dialog.Popup,
|
|
43
|
+
Viewport: Dialog.Viewport,
|
|
43
44
|
Title: Dialog.Title,
|
|
44
45
|
Description: Dialog.Description,
|
|
45
46
|
Close: Dialog.Close,
|
|
46
47
|
createHandle: createAlertDialogHandle,
|
|
48
|
+
Handle: AlertDialogHandle,
|
|
47
49
|
};
|
package/src/button.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/button/Button.tsx (v1.6.0). A button that can be
|
|
2
|
+
// used to trigger actions. Renders a `<button>` element.
|
|
3
|
+
import { S, subSlot } from './internal';
|
|
4
|
+
import { useButton } from './utils/useButton';
|
|
5
|
+
import { useRenderElement } from './utils/useRenderElement';
|
|
6
|
+
|
|
7
|
+
export interface ButtonState {
|
|
8
|
+
/** Whether the button should ignore user interaction. */
|
|
9
|
+
disabled: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ButtonProps {
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Whether the button should be focusable when disabled.
|
|
16
|
+
* @default false
|
|
17
|
+
*/
|
|
18
|
+
focusableWhenDisabled?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Whether the component renders a native `<button>` element when replacing it via the
|
|
21
|
+
* `render` prop.
|
|
22
|
+
* @default true
|
|
23
|
+
*/
|
|
24
|
+
nativeButton?: boolean;
|
|
25
|
+
className?: string | ((state: ButtonState) => string | undefined);
|
|
26
|
+
render?: import('./utils/useRenderElement').RenderProp<ButtonState>;
|
|
27
|
+
ref?: any;
|
|
28
|
+
[key: string]: any;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// octane: forwardRef → ref-as-prop.
|
|
32
|
+
export function Button(props: ButtonProps): any {
|
|
33
|
+
const slot = S('Button');
|
|
34
|
+
const {
|
|
35
|
+
render,
|
|
36
|
+
className,
|
|
37
|
+
disabled = false,
|
|
38
|
+
focusableWhenDisabled = false,
|
|
39
|
+
nativeButton = true,
|
|
40
|
+
style,
|
|
41
|
+
ref,
|
|
42
|
+
...elementProps
|
|
43
|
+
} = props;
|
|
44
|
+
|
|
45
|
+
const { getButtonProps, buttonRef } = useButton(
|
|
46
|
+
{ disabled, focusableWhenDisabled, native: nativeButton },
|
|
47
|
+
subSlot(slot, 'btn'),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const state: ButtonState = { disabled };
|
|
51
|
+
|
|
52
|
+
return useRenderElement(
|
|
53
|
+
'button',
|
|
54
|
+
{ className, render, style },
|
|
55
|
+
{ state, ref: [ref, buttonRef], props: [elementProps, getButtonProps] },
|
|
56
|
+
subSlot(slot, 're'),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export namespace Button {
|
|
61
|
+
export type Props = ButtonProps;
|
|
62
|
+
export type State = ButtonState;
|
|
63
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/context-menu/ (v1.6.0) — Phase 3f STAGE 4.
|
|
2
|
+
//
|
|
3
|
+
// A `ContextMenu` is a `Menu.Root` opened by right-click or long-press at the pointer, anchored to a
|
|
4
|
+
// synthetic zero-size rect at the cursor instead of to a trigger element. `ContextMenu.Root`
|
|
5
|
+
// provides the `ContextMenuRootContext` that `Menu` has been branching on since stage 1
|
|
6
|
+
// (`parent.type === 'context-menu'`): a modal focus manager, `mousedown`-grace-period outside-press
|
|
7
|
+
// dismissal, `fixed` positioning, `start` alignment, and the item `mouseup` guards that stop the
|
|
8
|
+
// gesture which OPENED the menu from immediately activating an item under the cursor.
|
|
9
|
+
//
|
|
10
|
+
// `ContextMenuRootContext` itself lives in `./utils/ContextMenuRootContext` — it landed as a
|
|
11
|
+
// context-only module in stage 1 so `Menu`'s parent-union branches could be written verbatim ahead
|
|
12
|
+
// of this component.
|
|
13
|
+
//
|
|
14
|
+
// Every part other than `Root` and `Trigger` is `Menu`'s own, re-exported through this namespace
|
|
15
|
+
// exactly as upstream's `context-menu/index.parts.ts` does.
|
|
16
|
+
//
|
|
17
|
+
// octane adaptations: `createElement`, not JSX; `forwardRef` → ref-as-prop; per-call-site `S('…')`
|
|
18
|
+
// slots with `subSlot`; native events, so `event.nativeEvent` collapses to `event`; upstream's
|
|
19
|
+
// `React.TouchEvent`/`React.MouseEvent` are the native types here.
|
|
20
|
+
import { createElement, useState, useRef, useEffect, useMemo, useId } from 'octane';
|
|
21
|
+
|
|
22
|
+
import { S, subSlot } from './internal';
|
|
23
|
+
import { Menu, MenuRootContext, useMenuRootContext } from './menu';
|
|
24
|
+
import type { MenuRootContextValue } from './menu';
|
|
25
|
+
import { ContextMenuRootContext, useContextMenuRootContext } from './utils/ContextMenuRootContext';
|
|
26
|
+
import type { ContextMenuRootContextValue } from './utils/ContextMenuRootContext';
|
|
27
|
+
import { useRenderElement } from './utils/useRenderElement';
|
|
28
|
+
import { useTimeout } from './utils/useTimeout';
|
|
29
|
+
import { addEventListener } from './utils/addEventListener';
|
|
30
|
+
import { ownerDocument } from './utils/owner';
|
|
31
|
+
import { contains } from './utils/contains';
|
|
32
|
+
import { getTarget, stopEvent } from './utils/composite/list-utils';
|
|
33
|
+
import { createChangeEventDetails, REASONS } from './utils/createChangeEventDetails';
|
|
34
|
+
import { pressableTriggerOpenStateMapping } from './utils/popupStateMapping';
|
|
35
|
+
import { findRootOwnerId } from './utils/findRootOwnerId';
|
|
36
|
+
|
|
37
|
+
const LONG_PRESS_DELAY = 500;
|
|
38
|
+
|
|
39
|
+
// --- Root ---------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A component that creates a context menu activated by right clicking or long pressing.
|
|
43
|
+
* Doesn't render its own HTML element.
|
|
44
|
+
*/
|
|
45
|
+
function ContextMenuRoot(props: any): any {
|
|
46
|
+
const slot = S('ContextMenuRoot');
|
|
47
|
+
const [anchor, setAnchor] = useState<ContextMenuRootContextValue['anchor']>(
|
|
48
|
+
{
|
|
49
|
+
getBoundingClientRect() {
|
|
50
|
+
return DOMRect.fromRect({ width: 0, height: 0, x: 0, y: 0 });
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
subSlot(slot, 'anchor'),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const backdropRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'backdrop'));
|
|
57
|
+
const internalBackdropRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'ibackdrop'));
|
|
58
|
+
const actionsRef = useRef<ContextMenuRootContextValue['actionsRef']['current']>(
|
|
59
|
+
null,
|
|
60
|
+
subSlot(slot, 'actions'),
|
|
61
|
+
);
|
|
62
|
+
const positionerRef = useRef<HTMLElement | null>(null, subSlot(slot, 'positioner'));
|
|
63
|
+
const allowMouseUpTriggerRef = useRef(true, subSlot(slot, 'amut'));
|
|
64
|
+
const initialCursorPointRef = useRef<{ x: number; y: number } | null>(
|
|
65
|
+
null,
|
|
66
|
+
subSlot(slot, 'cursor'),
|
|
67
|
+
);
|
|
68
|
+
// Raw `useId` (no `base-ui-` prefix), matching Base UI's `@base-ui/utils/useId`.
|
|
69
|
+
const id = useId(subSlot(slot, 'id'));
|
|
70
|
+
|
|
71
|
+
const contextValue: ContextMenuRootContextValue = useMemo(
|
|
72
|
+
() => ({
|
|
73
|
+
anchor,
|
|
74
|
+
setAnchor,
|
|
75
|
+
actionsRef,
|
|
76
|
+
backdropRef,
|
|
77
|
+
internalBackdropRef,
|
|
78
|
+
positionerRef,
|
|
79
|
+
allowMouseUpTriggerRef,
|
|
80
|
+
initialCursorPointRef,
|
|
81
|
+
rootId: id,
|
|
82
|
+
}),
|
|
83
|
+
[anchor, id],
|
|
84
|
+
subSlot(slot, 'ctx'),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// The inner `MenuRootContext.Provider value={undefined}` is load-bearing, not defensive: it is
|
|
88
|
+
// how `MenuRoot` tells "this Menu IS the context menu" apart from "this Menu is nested inside a
|
|
89
|
+
// ContextMenu.Trigger" (the `contextMenuContext && !parentMenuRootContext` guard).
|
|
90
|
+
return createElement(ContextMenuRootContext.Provider, {
|
|
91
|
+
value: contextValue,
|
|
92
|
+
children: createElement(MenuRootContext.Provider, {
|
|
93
|
+
value: undefined,
|
|
94
|
+
children: createElement(Menu.Root, props),
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Trigger ------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* An area that opens the menu on right click or long press.
|
|
103
|
+
* Renders a `<div>` element.
|
|
104
|
+
*/
|
|
105
|
+
function ContextMenuTrigger(componentProps: any): any {
|
|
106
|
+
const slot = S('ContextMenuTrigger');
|
|
107
|
+
const { render, className, style, ref: forwardedRef, ...elementProps } = componentProps;
|
|
108
|
+
|
|
109
|
+
const {
|
|
110
|
+
setAnchor,
|
|
111
|
+
actionsRef,
|
|
112
|
+
internalBackdropRef,
|
|
113
|
+
backdropRef,
|
|
114
|
+
positionerRef,
|
|
115
|
+
allowMouseUpTriggerRef,
|
|
116
|
+
initialCursorPointRef,
|
|
117
|
+
rootId,
|
|
118
|
+
} = useContextMenuRootContext(false);
|
|
119
|
+
|
|
120
|
+
const { store } = useMenuRootContext() as MenuRootContextValue;
|
|
121
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
122
|
+
const disabled = store.useState('disabled', subSlot(slot, 'disabled'));
|
|
123
|
+
|
|
124
|
+
const triggerRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'trigger'));
|
|
125
|
+
const touchPositionRef = useRef<{ x: number; y: number } | null>(null, subSlot(slot, 'touch'));
|
|
126
|
+
const longPressTimeout = useTimeout(subSlot(slot, 'lp'));
|
|
127
|
+
const allowMouseUpTimeout = useTimeout(subSlot(slot, 'amu'));
|
|
128
|
+
const allowMouseUpRef = useRef(false, subSlot(slot, 'amur'));
|
|
129
|
+
|
|
130
|
+
function handleLongPress(x: number, y: number, event: MouseEvent | TouchEvent) {
|
|
131
|
+
const isTouchEvent = event.type.startsWith('touch');
|
|
132
|
+
|
|
133
|
+
initialCursorPointRef.current = { x, y };
|
|
134
|
+
|
|
135
|
+
setAnchor({
|
|
136
|
+
getBoundingClientRect() {
|
|
137
|
+
return DOMRect.fromRect({
|
|
138
|
+
width: isTouchEvent ? 10 : 0,
|
|
139
|
+
height: isTouchEvent ? 10 : 0,
|
|
140
|
+
x,
|
|
141
|
+
y,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
allowMouseUpRef.current = false;
|
|
147
|
+
actionsRef.current?.setOpen(true, createChangeEventDetails(REASONS.triggerPress, event));
|
|
148
|
+
|
|
149
|
+
allowMouseUpTimeout.start(LONG_PRESS_DELAY, () => {
|
|
150
|
+
allowMouseUpRef.current = true;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function handleContextMenu(event: MouseEvent) {
|
|
155
|
+
if (disabled) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
allowMouseUpTriggerRef.current = true;
|
|
159
|
+
stopEvent(event);
|
|
160
|
+
// octane dispatches NATIVE events, so upstream's `event.nativeEvent` collapses to `event`.
|
|
161
|
+
handleLongPress(event.clientX, event.clientY, event);
|
|
162
|
+
const doc = ownerDocument(triggerRef.current);
|
|
163
|
+
|
|
164
|
+
addEventListener(
|
|
165
|
+
doc,
|
|
166
|
+
'mouseup',
|
|
167
|
+
(mouseEvent: Event) => {
|
|
168
|
+
allowMouseUpTriggerRef.current = false;
|
|
169
|
+
|
|
170
|
+
if (!allowMouseUpRef.current) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
allowMouseUpTimeout.clear();
|
|
175
|
+
allowMouseUpRef.current = false;
|
|
176
|
+
|
|
177
|
+
const mouseUpTarget = getTarget(mouseEvent) as Element | null;
|
|
178
|
+
|
|
179
|
+
if (contains(positionerRef.current, mouseUpTarget)) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (rootId && mouseUpTarget && findRootOwnerId(mouseUpTarget) === rootId) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
actionsRef.current?.setOpen(
|
|
188
|
+
false,
|
|
189
|
+
createChangeEventDetails(REASONS.cancelOpen, mouseEvent),
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
{ once: true },
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function handleTouchStart(event: TouchEvent) {
|
|
197
|
+
if (disabled) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
allowMouseUpTriggerRef.current = false;
|
|
201
|
+
if (event.touches.length === 1) {
|
|
202
|
+
event.stopPropagation();
|
|
203
|
+
const touch = event.touches[0];
|
|
204
|
+
touchPositionRef.current = { x: touch.clientX, y: touch.clientY };
|
|
205
|
+
longPressTimeout.start(LONG_PRESS_DELAY, () => {
|
|
206
|
+
if (touchPositionRef.current) {
|
|
207
|
+
handleLongPress(touchPositionRef.current.x, touchPositionRef.current.y, event);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function handleTouchMove(event: TouchEvent) {
|
|
214
|
+
if (longPressTimeout.isStarted() && touchPositionRef.current && event.touches.length === 1) {
|
|
215
|
+
const touch = event.touches[0];
|
|
216
|
+
const moveThreshold = 10;
|
|
217
|
+
|
|
218
|
+
const deltaX = Math.abs(touch.clientX - touchPositionRef.current.x);
|
|
219
|
+
const deltaY = Math.abs(touch.clientY - touchPositionRef.current.y);
|
|
220
|
+
|
|
221
|
+
if (deltaX > moveThreshold || deltaY > moveThreshold) {
|
|
222
|
+
longPressTimeout.clear();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function handleTouchEnd() {
|
|
228
|
+
longPressTimeout.clear();
|
|
229
|
+
touchPositionRef.current = null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
useEffect(
|
|
233
|
+
() => {
|
|
234
|
+
function handleDocumentContextMenu(event: Event) {
|
|
235
|
+
if (disabled) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const target = getTarget(event);
|
|
240
|
+
const targetElement = target as HTMLElement | null;
|
|
241
|
+
if (
|
|
242
|
+
contains(triggerRef.current, targetElement) ||
|
|
243
|
+
contains(internalBackdropRef.current, targetElement) ||
|
|
244
|
+
contains(backdropRef.current, targetElement)
|
|
245
|
+
) {
|
|
246
|
+
event.preventDefault();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const doc = ownerDocument(triggerRef.current);
|
|
251
|
+
return addEventListener(doc, 'contextmenu', handleDocumentContextMenu);
|
|
252
|
+
},
|
|
253
|
+
[backdropRef, disabled, internalBackdropRef],
|
|
254
|
+
subSlot(slot, 'e:ctxmenu'),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const state: ContextMenuTriggerState = {
|
|
258
|
+
open,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
return useRenderElement(
|
|
262
|
+
'div',
|
|
263
|
+
componentProps,
|
|
264
|
+
{
|
|
265
|
+
state,
|
|
266
|
+
ref: [triggerRef, forwardedRef],
|
|
267
|
+
props: [
|
|
268
|
+
{
|
|
269
|
+
onContextMenu: handleContextMenu,
|
|
270
|
+
onTouchStart: handleTouchStart,
|
|
271
|
+
onTouchMove: handleTouchMove,
|
|
272
|
+
onTouchEnd: handleTouchEnd,
|
|
273
|
+
onTouchCancel: handleTouchEnd,
|
|
274
|
+
style: {
|
|
275
|
+
WebkitTouchCallout: 'none',
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
elementProps,
|
|
279
|
+
],
|
|
280
|
+
stateAttributesMapping: pressableTriggerOpenStateMapping,
|
|
281
|
+
},
|
|
282
|
+
subSlot(slot, 're'),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export interface ContextMenuTriggerState {
|
|
287
|
+
/** Whether the context menu is currently open. */
|
|
288
|
+
open: boolean;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// --- Namespace ----------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Mirrors upstream's `context-menu/index.parts.ts`: `Root` and `Trigger` are ContextMenu's own,
|
|
295
|
+
* everything else is `Menu`'s part re-exported here.
|
|
296
|
+
*/
|
|
297
|
+
export const ContextMenu = {
|
|
298
|
+
Root: ContextMenuRoot,
|
|
299
|
+
Trigger: ContextMenuTrigger,
|
|
300
|
+
Backdrop: Menu.Backdrop,
|
|
301
|
+
Portal: Menu.Portal,
|
|
302
|
+
Positioner: Menu.Positioner,
|
|
303
|
+
Popup: Menu.Popup,
|
|
304
|
+
Arrow: Menu.Arrow,
|
|
305
|
+
Group: Menu.Group,
|
|
306
|
+
GroupLabel: Menu.GroupLabel,
|
|
307
|
+
Item: Menu.Item,
|
|
308
|
+
CheckboxItem: Menu.CheckboxItem,
|
|
309
|
+
CheckboxItemIndicator: Menu.CheckboxItemIndicator,
|
|
310
|
+
LinkItem: Menu.LinkItem,
|
|
311
|
+
RadioGroup: Menu.RadioGroup,
|
|
312
|
+
RadioItem: Menu.RadioItem,
|
|
313
|
+
RadioItemIndicator: Menu.RadioItemIndicator,
|
|
314
|
+
SubmenuRoot: Menu.SubmenuRoot,
|
|
315
|
+
SubmenuTrigger: Menu.SubmenuTrigger,
|
|
316
|
+
Separator: Menu.Separator,
|
|
317
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/csp-provider/CSPProvider.tsx (v1.6.0). Provides a
|
|
2
|
+
// default Content Security Policy configuration for Base UI components that require inline
|
|
3
|
+
// `<style>` or `<script>` tags.
|
|
4
|
+
import { createElement, useMemo } from 'octane';
|
|
5
|
+
|
|
6
|
+
import { S, subSlot } from './internal';
|
|
7
|
+
import { CSPContext } from './utils/CSPContext';
|
|
8
|
+
import type { CSPContextValue } from './utils/CSPContext';
|
|
9
|
+
|
|
10
|
+
export interface CSPProviderProps {
|
|
11
|
+
children?: any;
|
|
12
|
+
/** The nonce value to apply to inline `<style>` and `<script>` tags. */
|
|
13
|
+
nonce?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Whether inline `<style>` elements created by Base UI components should not be rendered.
|
|
16
|
+
* Instead, components must specify the CSS styles via custom class names or other methods.
|
|
17
|
+
* @default false
|
|
18
|
+
*/
|
|
19
|
+
disableStyleElements?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function CSPProvider(props: CSPProviderProps): any {
|
|
23
|
+
const slot = S('CSPProvider');
|
|
24
|
+
const { children, nonce, disableStyleElements } = props;
|
|
25
|
+
|
|
26
|
+
const contextValue: CSPContextValue = useMemo(
|
|
27
|
+
() => ({ nonce, disableStyleElements }),
|
|
28
|
+
[nonce, disableStyleElements],
|
|
29
|
+
subSlot(slot, 'ctx'),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return createElement(CSPContext.Provider, { value: contextValue, children });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export namespace CSPProvider {
|
|
36
|
+
export type Props = CSPProviderProps;
|
|
37
|
+
}
|
package/src/dialog.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import {
|
|
10
10
|
createContext,
|
|
11
11
|
createElement,
|
|
12
|
+
Fragment,
|
|
12
13
|
useContext,
|
|
13
14
|
useMemo,
|
|
14
15
|
useRef,
|
|
@@ -33,7 +34,11 @@ import { useDismiss } from './utils/floating/useDismiss';
|
|
|
33
34
|
import { useScrollLock } from './utils/useScrollLock';
|
|
34
35
|
import { useOpenChangeComplete } from './utils/useOpenChangeComplete';
|
|
35
36
|
import { useOpenMethodTriggerProps } from './utils/useOpenInteractionType';
|
|
36
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
triggerOpenStateMapping,
|
|
39
|
+
popupStateMapping,
|
|
40
|
+
CommonPopupDataAttributes,
|
|
41
|
+
} from './utils/popupStateMapping';
|
|
37
42
|
import { transitionStatusMapping } from './utils/useTransitionStatus';
|
|
38
43
|
import type { StateAttributesMapping } from './utils/getStateAttributesProps';
|
|
39
44
|
import { EMPTY_OBJECT } from './utils/empty';
|
|
@@ -404,14 +409,7 @@ function DialogInteractions(props: any): any {
|
|
|
404
409
|
subSlot(slot, 'pip'),
|
|
405
410
|
);
|
|
406
411
|
|
|
407
|
-
|
|
408
|
-
return props.children ?? null;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Passthrough used when the interactions aren't mounted, to keep the Provider's children a stable
|
|
412
|
-
// element-descriptor shape (see the note at the `content` call site).
|
|
413
|
-
function DialogChildren(props: any): any {
|
|
414
|
-
return props.children ?? null;
|
|
412
|
+
return null;
|
|
415
413
|
}
|
|
416
414
|
|
|
417
415
|
export function useRenderDialogRoot<Payload>(
|
|
@@ -482,19 +480,19 @@ export function useRenderDialogRoot<Payload>(
|
|
|
482
480
|
const resolvedChildren =
|
|
483
481
|
typeof children === 'function' && !isChildrenBlock(children) ? children({ payload }) : children;
|
|
484
482
|
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
children: resolvedChildren,
|
|
497
|
-
|
|
483
|
+
// `DialogInteractions` is a headless SIBLING of the children, exactly as Base UI renders it, so
|
|
484
|
+
// toggling it never disturbs them.
|
|
485
|
+
const content = [
|
|
486
|
+
shouldRenderInteractions
|
|
487
|
+
? createElement(DialogInteractions, {
|
|
488
|
+
key: 'interactions',
|
|
489
|
+
store,
|
|
490
|
+
parentContext: parentDialogRootContext?.store.context,
|
|
491
|
+
isDrawer,
|
|
492
|
+
})
|
|
493
|
+
: null,
|
|
494
|
+
createElement(Fragment, { key: 'children', children: resolvedChildren }),
|
|
495
|
+
];
|
|
498
496
|
|
|
499
497
|
return createElement(IsDrawerContext.Provider, {
|
|
500
498
|
value: false,
|
|
@@ -844,6 +842,73 @@ function DialogClose(componentProps: any): any {
|
|
|
844
842
|
);
|
|
845
843
|
}
|
|
846
844
|
|
|
845
|
+
// --- Viewport ----------------------------------------------------------------
|
|
846
|
+
|
|
847
|
+
export const DialogViewportDataAttributes = {
|
|
848
|
+
open: CommonPopupDataAttributes.open,
|
|
849
|
+
closed: CommonPopupDataAttributes.closed,
|
|
850
|
+
startingStyle: CommonPopupDataAttributes.startingStyle,
|
|
851
|
+
endingStyle: CommonPopupDataAttributes.endingStyle,
|
|
852
|
+
nested: 'data-nested',
|
|
853
|
+
nestedDialogOpen: 'data-nested-dialog-open',
|
|
854
|
+
} as const;
|
|
855
|
+
|
|
856
|
+
const viewportStateAttributesMapping: StateAttributesMapping<any> = {
|
|
857
|
+
...(popupStateMapping as StateAttributesMapping<any>),
|
|
858
|
+
...(transitionStatusMapping as StateAttributesMapping<any>),
|
|
859
|
+
nested(value: boolean) {
|
|
860
|
+
return value ? { [DialogViewportDataAttributes.nested]: '' } : null;
|
|
861
|
+
},
|
|
862
|
+
nestedDialogOpen(value: boolean) {
|
|
863
|
+
return value ? { [DialogViewportDataAttributes.nestedDialogOpen]: '' } : null;
|
|
864
|
+
},
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
// A positioning container for the dialog popup that can be made scrollable.
|
|
868
|
+
function DialogViewport(componentProps: any): any {
|
|
869
|
+
const slot = S('DialogViewport');
|
|
870
|
+
const { render, className, style, children, ref, ...elementProps } = componentProps;
|
|
871
|
+
|
|
872
|
+
const keepMounted = useDialogPortalContext();
|
|
873
|
+
const { store } = useDialogRootContext() as DialogRootContextValue;
|
|
874
|
+
|
|
875
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
876
|
+
const nested = store.useState('nested', subSlot(slot, 'nested'));
|
|
877
|
+
const transitionStatus = store.useState('transitionStatus', subSlot(slot, 'ts'));
|
|
878
|
+
const nestedOpenDialogCount = store.useState('nestedOpenDialogCount', subSlot(slot, 'nodc'));
|
|
879
|
+
const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
|
|
880
|
+
|
|
881
|
+
const setViewportElement = store.useStateSetter('viewportElement', subSlot(slot, 'sve'));
|
|
882
|
+
|
|
883
|
+
const state = {
|
|
884
|
+
open,
|
|
885
|
+
nested,
|
|
886
|
+
transitionStatus,
|
|
887
|
+
nestedDialogOpen: nestedOpenDialogCount > 0,
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
return useRenderElement(
|
|
891
|
+
'div',
|
|
892
|
+
{ render, className, style },
|
|
893
|
+
{
|
|
894
|
+
enabled: keepMounted || mounted,
|
|
895
|
+
state,
|
|
896
|
+
ref: [ref, setViewportElement],
|
|
897
|
+
stateAttributesMapping: viewportStateAttributesMapping,
|
|
898
|
+
props: [
|
|
899
|
+
{
|
|
900
|
+
role: 'presentation',
|
|
901
|
+
hidden: !mounted,
|
|
902
|
+
style: { pointerEvents: !open ? 'none' : undefined },
|
|
903
|
+
children,
|
|
904
|
+
},
|
|
905
|
+
elementProps,
|
|
906
|
+
],
|
|
907
|
+
},
|
|
908
|
+
subSlot(slot, 're'),
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
847
912
|
// --- Namespace ---------------------------------------------------------------
|
|
848
913
|
|
|
849
914
|
export const Dialog = {
|
|
@@ -852,8 +917,10 @@ export const Dialog = {
|
|
|
852
917
|
Portal: DialogPortal,
|
|
853
918
|
Backdrop: DialogBackdrop,
|
|
854
919
|
Popup: DialogPopup,
|
|
920
|
+
Viewport: DialogViewport,
|
|
855
921
|
Title: DialogTitle,
|
|
856
922
|
Description: DialogDescription,
|
|
857
923
|
Close: DialogClose,
|
|
858
924
|
createHandle: createDialogHandle,
|
|
925
|
+
Handle: DialogHandle,
|
|
859
926
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/direction-provider/DirectionProvider.tsx (v1.6.0).
|
|
2
|
+
// Enables RTL behavior for Base UI components.
|
|
3
|
+
//
|
|
4
|
+
// octane adaptation: `utils/DirectionContext` carries the `TextDirection` string DIRECTLY
|
|
5
|
+
// rather than Base UI's `{ direction }` wrapper object, so the provider needs no `useMemo`
|
|
6
|
+
// to stay referentially stable. The context value is never observable in the DOM, and every
|
|
7
|
+
// consumer reads it through `useDirection()`.
|
|
8
|
+
import { createElement } from 'octane';
|
|
9
|
+
|
|
10
|
+
import { DirectionContext, useDirection } from './utils/DirectionContext';
|
|
11
|
+
import type { TextDirection } from './utils/DirectionContext';
|
|
12
|
+
|
|
13
|
+
export type { TextDirection };
|
|
14
|
+
export { useDirection };
|
|
15
|
+
|
|
16
|
+
export interface DirectionProviderProps {
|
|
17
|
+
children?: any;
|
|
18
|
+
/**
|
|
19
|
+
* The reading direction of the text.
|
|
20
|
+
* @default 'ltr'
|
|
21
|
+
*/
|
|
22
|
+
direction?: TextDirection;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function DirectionProvider(props: DirectionProviderProps): any {
|
|
26
|
+
const { direction = 'ltr' } = props;
|
|
27
|
+
return createElement(DirectionContext.Provider, { value: direction, children: props.children });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export namespace DirectionProvider {
|
|
31
|
+
export type Props = DirectionProviderProps;
|
|
32
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,19 +5,29 @@
|
|
|
5
5
|
export { Separator } from './separator';
|
|
6
6
|
export { AlertDialog } from './alert-dialog';
|
|
7
7
|
export { Avatar } from './avatar';
|
|
8
|
+
export { Button } from './button';
|
|
9
|
+
export { CSPProvider } from './csp-provider';
|
|
10
|
+
export { DirectionProvider, useDirection } from './direction-provider';
|
|
11
|
+
export type { TextDirection } from './direction-provider';
|
|
12
|
+
export { useMediaQuery } from './unstable-use-media-query';
|
|
8
13
|
export { Checkbox } from './checkbox';
|
|
14
|
+
export { ContextMenu } from './context-menu';
|
|
9
15
|
export { CheckboxGroup } from './checkbox-group';
|
|
10
16
|
export { Dialog } from './dialog';
|
|
11
17
|
export { Field } from './field';
|
|
12
18
|
export { Fieldset } from './fieldset';
|
|
13
19
|
export { Form } from './form';
|
|
14
20
|
export { Input } from './input';
|
|
21
|
+
export { Menu } from './menu';
|
|
22
|
+
export { Menubar } from './menubar';
|
|
15
23
|
export { Meter } from './meter';
|
|
16
24
|
export { NumberField } from './number-field';
|
|
17
25
|
export { Progress } from './progress';
|
|
18
26
|
export { Radio } from './radio';
|
|
19
27
|
export { RadioGroup } from './radio-group';
|
|
20
28
|
export { Popover } from './popover';
|
|
29
|
+
export { PreviewCard } from './preview-card';
|
|
30
|
+
export { Tooltip } from './tooltip';
|
|
21
31
|
export { Slider } from './slider';
|
|
22
32
|
export { Switch } from './switch';
|
|
23
33
|
export { Toggle } from './toggle';
|