@octanejs/base-ui 0.1.1 → 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/README.md +7 -1
- package/package.json +12 -4
- package/src/alert-dialog.ts +47 -0
- package/src/checkbox.ts +7 -16
- package/src/dialog.ts +859 -0
- package/src/field.ts +4 -30
- package/src/index.ts +3 -0
- package/src/number-field.ts +8 -45
- package/src/popover.ts +1205 -0
- package/src/radio.ts +3 -24
- package/src/slider.ts +4 -5
- package/src/switch.ts +4 -30
- package/src/utils/InternalBackdrop.ts +28 -0
- package/src/utils/adaptiveOriginMiddleware.ts +82 -0
- package/src/utils/closePart.ts +61 -0
- package/src/utils/constants.ts +9 -0
- package/src/utils/createChangeEventDetails.ts +7 -0
- package/src/utils/dom.ts +9 -0
- package/src/utils/empty.ts +7 -0
- package/src/utils/floating/FloatingFocusManager.ts +833 -0
- package/src/utils/floating/FloatingPortal.ts +268 -0
- package/src/utils/floating/FloatingRootStore.ts +127 -0
- package/src/utils/floating/FloatingTree.ts +70 -0
- package/src/utils/floating/FloatingTreeStore.ts +22 -0
- package/src/utils/floating/FocusGuard.ts +34 -0
- package/src/utils/floating/composite.ts +20 -0
- package/src/utils/floating/constants.ts +8 -0
- package/src/utils/floating/createAttribute.ts +4 -0
- package/src/utils/floating/createEventEmitter.ts +20 -0
- package/src/utils/floating/element.ts +54 -0
- package/src/utils/floating/enqueueFocus.ts +38 -0
- package/src/utils/floating/event.ts +53 -0
- package/src/utils/floating/getEmptyRootContext.ts +19 -0
- package/src/utils/floating/markOthers.ts +197 -0
- package/src/utils/floating/nodes.ts +31 -0
- package/src/utils/floating/tabbable.ts +260 -0
- package/src/utils/floating/types.ts +57 -0
- package/src/utils/floating/useClick.ts +174 -0
- package/src/utils/floating/useDismiss.ts +641 -0
- package/src/utils/floating/useFloating.ts +198 -0
- package/src/utils/floating/useFloatingRootContext.ts +81 -0
- package/src/utils/floating/useHoverFloatingInteraction.ts +12 -0
- package/src/utils/floating/useHoverReferenceInteraction.ts +17 -0
- package/src/utils/floating/useSyncedFloatingRootContext.ts +93 -0
- package/src/utils/getDisabledMountTransitionStyles.ts +12 -0
- package/src/utils/hideMiddleware.ts +22 -0
- package/src/utils/inertValue.ts +5 -0
- package/src/utils/mergeCleanups.ts +13 -0
- package/src/utils/platform.ts +46 -2
- package/src/utils/popupStateMapping.ts +48 -0
- package/src/utils/popups/index.ts +4 -0
- package/src/utils/popups/popupStoreUtils.ts +484 -0
- package/src/utils/popups/popupTriggerMap.ts +62 -0
- package/src/utils/popups/store.ts +147 -0
- package/src/utils/popups/useTriggerFocusGuards.ts +73 -0
- package/src/utils/store/ReactStore.ts +168 -0
- package/src/utils/store/Store.ts +84 -0
- package/src/utils/store/createSelector.ts +66 -0
- package/src/utils/store/useStore.ts +49 -0
- package/src/utils/useAnchorPositioning.ts +581 -0
- package/src/utils/useAnchoredPopupScrollLock.ts +50 -0
- package/src/utils/useAnimationFrame.ts +28 -5
- package/src/utils/useEnhancedClickHandler.ts +47 -0
- package/src/utils/useOnFirstRender.ts +11 -0
- package/src/utils/useOpenInteractionType.ts +32 -0
- package/src/utils/usePositioner.ts +48 -0
- package/src/utils/useScrollLock.ts +253 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/floating-ui-react/utils/event.ts (v1.6.0) — the subset
|
|
2
|
+
// used so far. octane events are native, so these operate on native events directly.
|
|
3
|
+
import { platform } from '../platform';
|
|
4
|
+
|
|
5
|
+
export function isClickLikeEvent(event: Event): boolean {
|
|
6
|
+
const type = event.type;
|
|
7
|
+
return type === 'click' || type === 'mousedown' || type === 'keydown' || type === 'keyup';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isMouseLikePointerType(pointerType: string | undefined, strict?: boolean): boolean {
|
|
11
|
+
const values: Array<string | undefined> = ['mouse', 'pen'];
|
|
12
|
+
if (!strict) {
|
|
13
|
+
values.push('', undefined);
|
|
14
|
+
}
|
|
15
|
+
return values.includes(pointerType);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// octane events are native, so this is effectively always false for handler `event`s (a native
|
|
19
|
+
// event has no `.nativeEvent`) — ported for faithfulness so `isReactEvent(e) ? e.nativeEvent : e`
|
|
20
|
+
// resolves to the native event.
|
|
21
|
+
export function isReactEvent(event: any): boolean {
|
|
22
|
+
return event != null && 'nativeEvent' in event;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isVirtualClick(event: MouseEvent | PointerEvent): boolean {
|
|
26
|
+
if ((event as PointerEvent).pointerType === '' && event.isTrusted) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (platform.os.android && (event as PointerEvent).pointerType) {
|
|
30
|
+
return event.type === 'click' && event.buttons === 1;
|
|
31
|
+
}
|
|
32
|
+
return event.detail === 0 && !(event as PointerEvent).pointerType;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isVirtualPointerEvent(event: PointerEvent): boolean {
|
|
36
|
+
if (platform.env.jsdom) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return (
|
|
40
|
+
(!platform.os.android && event.width === 0 && event.height === 0) ||
|
|
41
|
+
(platform.os.android &&
|
|
42
|
+
event.width === 1 &&
|
|
43
|
+
event.height === 1 &&
|
|
44
|
+
event.pressure === 0 &&
|
|
45
|
+
event.detail === 0 &&
|
|
46
|
+
event.pointerType === 'mouse') ||
|
|
47
|
+
(event.width < 1 &&
|
|
48
|
+
event.height < 1 &&
|
|
49
|
+
event.pressure === 0 &&
|
|
50
|
+
event.detail === 0 &&
|
|
51
|
+
event.pointerType === 'touch')
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/floating-ui-react/utils/getEmptyRootContext.ts.
|
|
2
|
+
// The inert root context a popup store starts with before its real one is created.
|
|
3
|
+
import { PopupTriggerMap } from '../popups/popupTriggerMap';
|
|
4
|
+
import { FloatingRootStore } from './FloatingRootStore';
|
|
5
|
+
import type { FloatingRootContext } from './types';
|
|
6
|
+
|
|
7
|
+
export function getEmptyRootContext(): FloatingRootContext {
|
|
8
|
+
return new FloatingRootStore({
|
|
9
|
+
open: false,
|
|
10
|
+
transitionStatus: undefined,
|
|
11
|
+
floatingElement: null,
|
|
12
|
+
referenceElement: null,
|
|
13
|
+
triggerElements: new PopupTriggerMap(),
|
|
14
|
+
floatingId: undefined,
|
|
15
|
+
syncOnly: false,
|
|
16
|
+
nested: false,
|
|
17
|
+
onOpenChange: undefined,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/floating-ui-react/utils/markOthers.ts (v1.6.0).
|
|
2
|
+
// Ref-counted `aria-hidden`/`inert` + `data-base-ui-inert` marker application to everything outside
|
|
3
|
+
// the modal popup (an aria-hidden fork). Pure DOM — only import paths change.
|
|
4
|
+
import { getNodeName, isShadowRoot } from '@floating-ui/utils/dom';
|
|
5
|
+
import { ownerDocument } from '../owner';
|
|
6
|
+
|
|
7
|
+
type Undo = () => void;
|
|
8
|
+
|
|
9
|
+
interface MarkOthersOptions {
|
|
10
|
+
ariaHidden?: boolean | undefined;
|
|
11
|
+
inert?: boolean | undefined;
|
|
12
|
+
mark?: boolean | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const counters = {
|
|
16
|
+
inert: new WeakMap<Element, number>(),
|
|
17
|
+
'aria-hidden': new WeakMap<Element, number>(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const markerName = 'data-base-ui-inert';
|
|
21
|
+
type ControlAttribute = keyof typeof counters;
|
|
22
|
+
|
|
23
|
+
const uncontrolledElementsSets: Record<ControlAttribute, WeakSet<Element>> = {
|
|
24
|
+
inert: new WeakSet<Element>(),
|
|
25
|
+
'aria-hidden': new WeakSet<Element>(),
|
|
26
|
+
};
|
|
27
|
+
let markerCounterMap = new WeakMap<Element, number>();
|
|
28
|
+
let lockCount = 0;
|
|
29
|
+
|
|
30
|
+
function getUncontrolledElementsSet(controlAttribute: ControlAttribute) {
|
|
31
|
+
return uncontrolledElementsSets[controlAttribute];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function unwrapHost(node: Node | null): Element | null {
|
|
35
|
+
if (!node) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return isShadowRoot(node) ? node.host : unwrapHost(node.parentNode);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const correctElements = (parent: HTMLElement, targets: Element[]): Element[] =>
|
|
42
|
+
targets
|
|
43
|
+
.map((target) => {
|
|
44
|
+
if (parent.contains(target)) {
|
|
45
|
+
return target;
|
|
46
|
+
}
|
|
47
|
+
const correctedTarget = unwrapHost(target);
|
|
48
|
+
if (correctedTarget && parent.contains(correctedTarget)) {
|
|
49
|
+
return correctedTarget;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
})
|
|
53
|
+
.filter((x): x is Element => x != null);
|
|
54
|
+
|
|
55
|
+
const buildKeepSet = (targets: Element[]): Set<Node> => {
|
|
56
|
+
const keep = new Set<Node>();
|
|
57
|
+
targets.forEach((target) => {
|
|
58
|
+
let node: Node | null = target;
|
|
59
|
+
while (node && !keep.has(node)) {
|
|
60
|
+
keep.add(node);
|
|
61
|
+
node = node.parentNode;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return keep;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const collectOutsideElements = (
|
|
68
|
+
root: HTMLElement,
|
|
69
|
+
keepElements: Set<Node>,
|
|
70
|
+
stopElements: Set<Node>,
|
|
71
|
+
): Element[] => {
|
|
72
|
+
const outside: Element[] = [];
|
|
73
|
+
const walk = (parent: Element | null) => {
|
|
74
|
+
if (!parent || stopElements.has(parent)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
Array.from(parent.children).forEach((node: Element) => {
|
|
78
|
+
if (getNodeName(node) === 'script') {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (keepElements.has(node)) {
|
|
82
|
+
walk(node);
|
|
83
|
+
} else {
|
|
84
|
+
outside.push(node);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
walk(root);
|
|
89
|
+
return outside;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function applyAttributeToOthers(
|
|
93
|
+
uncorrectedAvoidElements: Element[],
|
|
94
|
+
body: HTMLElement,
|
|
95
|
+
ariaHidden: boolean,
|
|
96
|
+
inert: boolean,
|
|
97
|
+
{ mark = true }: MarkOthersOptions,
|
|
98
|
+
): Undo {
|
|
99
|
+
let controlAttribute: ControlAttribute | null = null;
|
|
100
|
+
if (inert) {
|
|
101
|
+
controlAttribute = 'inert';
|
|
102
|
+
} else if (ariaHidden) {
|
|
103
|
+
controlAttribute = 'aria-hidden';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let counterMap: WeakMap<Element, number> | null = null;
|
|
107
|
+
let uncontrolledElementsSet: WeakSet<Element> | null = null;
|
|
108
|
+
const avoidElements = correctElements(body, uncorrectedAvoidElements);
|
|
109
|
+
const markerTargets = mark
|
|
110
|
+
? collectOutsideElements(body, buildKeepSet(avoidElements), new Set<Node>(avoidElements))
|
|
111
|
+
: [];
|
|
112
|
+
const hiddenElements: Element[] = [];
|
|
113
|
+
const markedElements: Element[] = [];
|
|
114
|
+
|
|
115
|
+
if (controlAttribute) {
|
|
116
|
+
const map = counters[controlAttribute];
|
|
117
|
+
const currentUncontrolledElementsSet = getUncontrolledElementsSet(controlAttribute);
|
|
118
|
+
uncontrolledElementsSet = currentUncontrolledElementsSet;
|
|
119
|
+
counterMap = map;
|
|
120
|
+
const ariaLiveElements = correctElements(
|
|
121
|
+
body,
|
|
122
|
+
Array.from(body.querySelectorAll('[aria-live]')),
|
|
123
|
+
);
|
|
124
|
+
const controlElements = avoidElements.concat(ariaLiveElements);
|
|
125
|
+
const controlTargets = collectOutsideElements(
|
|
126
|
+
body,
|
|
127
|
+
buildKeepSet(controlElements),
|
|
128
|
+
new Set<Node>(controlElements),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
controlTargets.forEach((node) => {
|
|
132
|
+
const attribute = node.getAttribute(controlAttribute!);
|
|
133
|
+
const alreadyHidden = attribute !== null && attribute !== 'false';
|
|
134
|
+
const counterValue = (map.get(node) || 0) + 1;
|
|
135
|
+
map.set(node, counterValue);
|
|
136
|
+
hiddenElements.push(node);
|
|
137
|
+
if (counterValue === 1 && alreadyHidden) {
|
|
138
|
+
currentUncontrolledElementsSet.add(node);
|
|
139
|
+
}
|
|
140
|
+
if (!alreadyHidden) {
|
|
141
|
+
node.setAttribute(controlAttribute!, controlAttribute === 'inert' ? '' : 'true');
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (mark) {
|
|
147
|
+
markerTargets.forEach((node) => {
|
|
148
|
+
const markerValue = (markerCounterMap.get(node) || 0) + 1;
|
|
149
|
+
markerCounterMap.set(node, markerValue);
|
|
150
|
+
markedElements.push(node);
|
|
151
|
+
if (markerValue === 1) {
|
|
152
|
+
node.setAttribute(markerName, '');
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
lockCount += 1;
|
|
158
|
+
|
|
159
|
+
return () => {
|
|
160
|
+
if (counterMap) {
|
|
161
|
+
hiddenElements.forEach((element) => {
|
|
162
|
+
const currentCounterValue = counterMap!.get(element) || 0;
|
|
163
|
+
const counterValue = currentCounterValue - 1;
|
|
164
|
+
counterMap!.set(element, counterValue);
|
|
165
|
+
if (!counterValue) {
|
|
166
|
+
if (!uncontrolledElementsSet?.has(element) && controlAttribute) {
|
|
167
|
+
element.removeAttribute(controlAttribute);
|
|
168
|
+
}
|
|
169
|
+
uncontrolledElementsSet?.delete(element);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (mark) {
|
|
174
|
+
markedElements.forEach((element) => {
|
|
175
|
+
const markerValue = (markerCounterMap.get(element) || 0) - 1;
|
|
176
|
+
markerCounterMap.set(element, markerValue);
|
|
177
|
+
if (!markerValue) {
|
|
178
|
+
element.removeAttribute(markerName);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
lockCount -= 1;
|
|
183
|
+
if (!lockCount) {
|
|
184
|
+
counters.inert = new WeakMap();
|
|
185
|
+
counters['aria-hidden'] = new WeakMap();
|
|
186
|
+
uncontrolledElementsSets.inert = new WeakSet();
|
|
187
|
+
uncontrolledElementsSets['aria-hidden'] = new WeakSet();
|
|
188
|
+
markerCounterMap = new WeakMap();
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function markOthers(avoidElements: Element[], options: MarkOthersOptions = {}): Undo {
|
|
194
|
+
const { ariaHidden = false, inert = false, mark = true } = options;
|
|
195
|
+
const body = ownerDocument(avoidElements[0]).body;
|
|
196
|
+
return applyAttributeToOthers(avoidElements, body, ariaHidden, inert, { mark });
|
|
197
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/floating-ui-react/utils/nodes.ts (the subset used).
|
|
2
|
+
// Walks the FloatingTree's flat node list to collect a node's (optionally open) descendants.
|
|
3
|
+
import type { FloatingNodeType } from './types';
|
|
4
|
+
|
|
5
|
+
export function getNodeChildren(
|
|
6
|
+
nodes: Array<FloatingNodeType>,
|
|
7
|
+
id: string | undefined,
|
|
8
|
+
onlyOpenChildren = true,
|
|
9
|
+
): Array<FloatingNodeType> {
|
|
10
|
+
const directChildren = nodes.filter((node) => node.parentId === id);
|
|
11
|
+
return directChildren.flatMap((child) => [
|
|
12
|
+
...(!onlyOpenChildren || child.context?.open ? [child] : []),
|
|
13
|
+
...getNodeChildren(nodes, child.id, onlyOpenChildren),
|
|
14
|
+
]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getNodeAncestors(
|
|
18
|
+
nodes: Array<FloatingNodeType>,
|
|
19
|
+
id: string | undefined,
|
|
20
|
+
): Array<FloatingNodeType> {
|
|
21
|
+
let allAncestors: Array<FloatingNodeType> = [];
|
|
22
|
+
let currentParentId = nodes.find((node) => node.id === id)?.parentId;
|
|
23
|
+
while (currentParentId) {
|
|
24
|
+
const currentNode = nodes.find((node) => node.id === currentParentId);
|
|
25
|
+
currentParentId = currentNode?.parentId;
|
|
26
|
+
if (currentNode) {
|
|
27
|
+
allAncestors = allAncestors.concat(currentNode);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return allAncestors;
|
|
31
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/floating-ui-react/utils/tabbable.ts (v1.6.0).
|
|
2
|
+
// The DOM tab-order walk the FocusManager + portal use (candidate collection, radio-group handling,
|
|
3
|
+
// visibility/inert filtering, next/previous-tabbable, enable/disableFocusInside). Pure — only import
|
|
4
|
+
// paths change; `React.FocusEvent` is a native FocusEvent in octane.
|
|
5
|
+
import { getComputedStyle, getNodeName, isHTMLElement, isShadowRoot } from '@floating-ui/utils/dom';
|
|
6
|
+
import { ownerDocument } from '../owner';
|
|
7
|
+
import { activeElement, contains } from './element';
|
|
8
|
+
import { isElementVisible } from './composite';
|
|
9
|
+
|
|
10
|
+
export type FocusableElement = HTMLElement | SVGElement;
|
|
11
|
+
|
|
12
|
+
const CANDIDATE_SELECTOR =
|
|
13
|
+
'a[href],button,input,select,textarea,summary,details,iframe,object,embed,[tabindex],[contenteditable]:not([contenteditable="false"]),audio[controls],video[controls]';
|
|
14
|
+
|
|
15
|
+
function getParentElement(element: Element) {
|
|
16
|
+
const assignedSlot = (element as Element & { assignedSlot?: HTMLSlotElement | null | undefined })
|
|
17
|
+
.assignedSlot;
|
|
18
|
+
if (assignedSlot) {
|
|
19
|
+
return assignedSlot;
|
|
20
|
+
}
|
|
21
|
+
if (element.parentElement) {
|
|
22
|
+
return element.parentElement;
|
|
23
|
+
}
|
|
24
|
+
const rootNode = element.getRootNode();
|
|
25
|
+
return isShadowRoot(rootNode) ? rootNode.host : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getDetailsSummary(details: Element) {
|
|
29
|
+
for (const child of Array.from(details.children)) {
|
|
30
|
+
if (getNodeName(child) === 'summary') {
|
|
31
|
+
return child;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isWithinOpenDetailsSummary(element: Element, details: Element) {
|
|
38
|
+
const summary = getDetailsSummary(details);
|
|
39
|
+
return !!summary && (element === summary || contains(summary, element));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isFocusableCandidate(element: Element | null): element is FocusableElement {
|
|
43
|
+
const nodeName = element ? getNodeName(element) : '';
|
|
44
|
+
return (
|
|
45
|
+
element != null &&
|
|
46
|
+
element.matches(CANDIDATE_SELECTOR) &&
|
|
47
|
+
(nodeName !== 'summary' ||
|
|
48
|
+
(element.parentElement != null &&
|
|
49
|
+
getNodeName(element.parentElement) === 'details' &&
|
|
50
|
+
getDetailsSummary(element.parentElement) === element)) &&
|
|
51
|
+
(nodeName !== 'details' || getDetailsSummary(element) == null) &&
|
|
52
|
+
(nodeName !== 'input' || (element as HTMLInputElement).type !== 'hidden')
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isFocusableElement(element: Element | null): element is FocusableElement {
|
|
57
|
+
if (!isFocusableCandidate(element) || !element.isConnected || element.matches(':disabled')) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
for (let current: Element | null = element; current; current = getParentElement(current)) {
|
|
61
|
+
const isAncestor = current !== element;
|
|
62
|
+
const isSlot = getNodeName(current) === 'slot';
|
|
63
|
+
if (current.hasAttribute('inert')) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
(isAncestor &&
|
|
68
|
+
getNodeName(current) === 'details' &&
|
|
69
|
+
!(current as HTMLDetailsElement).open &&
|
|
70
|
+
!isWithinOpenDetailsSummary(element, current)) ||
|
|
71
|
+
current.hasAttribute('hidden') ||
|
|
72
|
+
(!isSlot && !isVisibleInTabbableTree(current, isAncestor))
|
|
73
|
+
) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isVisibleInTabbableTree(element: Element, isAncestor: boolean) {
|
|
81
|
+
const styles = getComputedStyle(element);
|
|
82
|
+
if (!isAncestor) {
|
|
83
|
+
return isElementVisible(element, styles);
|
|
84
|
+
}
|
|
85
|
+
return styles.display !== 'none';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getTabIndex(element: FocusableElement) {
|
|
89
|
+
const tabIndex = element.tabIndex;
|
|
90
|
+
if (tabIndex < 0) {
|
|
91
|
+
const nodeName = getNodeName(element);
|
|
92
|
+
if (
|
|
93
|
+
nodeName === 'details' ||
|
|
94
|
+
nodeName === 'audio' ||
|
|
95
|
+
nodeName === 'video' ||
|
|
96
|
+
(isHTMLElement(element) && element.isContentEditable)
|
|
97
|
+
) {
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return tabIndex;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getNamedRadioInput(element: FocusableElement) {
|
|
105
|
+
if (getNodeName(element) !== 'input') {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const input = element as HTMLInputElement;
|
|
109
|
+
return input.type === 'radio' && input.name !== '' ? input : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isTabbableRadio(element: FocusableElement, candidates: FocusableElement[]) {
|
|
113
|
+
const input = getNamedRadioInput(element);
|
|
114
|
+
if (!input) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
const checkedRadio = candidates.find((candidate) => {
|
|
118
|
+
const radio = getNamedRadioInput(candidate);
|
|
119
|
+
return radio?.name === input.name && radio.form === input.form && radio.checked;
|
|
120
|
+
});
|
|
121
|
+
if (checkedRadio) {
|
|
122
|
+
return checkedRadio === input;
|
|
123
|
+
}
|
|
124
|
+
return (
|
|
125
|
+
candidates.find((candidate) => {
|
|
126
|
+
const radio = getNamedRadioInput(candidate);
|
|
127
|
+
return radio?.name === input.name && radio.form === input.form;
|
|
128
|
+
}) === input
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getComposedChildren(container: ParentNode): Element[] {
|
|
133
|
+
if (isHTMLElement(container) && getNodeName(container) === 'slot') {
|
|
134
|
+
const assignedElements = (container as unknown as HTMLSlotElement).assignedElements({
|
|
135
|
+
flatten: true,
|
|
136
|
+
});
|
|
137
|
+
if (assignedElements.length > 0) {
|
|
138
|
+
return assignedElements;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (isHTMLElement(container) && (container as unknown as HTMLElement).shadowRoot) {
|
|
142
|
+
return Array.from((container as unknown as HTMLElement).shadowRoot!.children);
|
|
143
|
+
}
|
|
144
|
+
return Array.from(container.children);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function appendCandidates(container: ParentNode, list: FocusableElement[]) {
|
|
148
|
+
getComposedChildren(container).forEach((child) => {
|
|
149
|
+
if (isFocusableCandidate(child)) {
|
|
150
|
+
list.push(child);
|
|
151
|
+
}
|
|
152
|
+
appendCandidates(child, list);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function appendMatchingElements(container: ParentNode, selector: string, list: HTMLElement[]) {
|
|
157
|
+
getComposedChildren(container).forEach((child) => {
|
|
158
|
+
if (isHTMLElement(child) && child.matches(selector)) {
|
|
159
|
+
list.push(child);
|
|
160
|
+
}
|
|
161
|
+
appendMatchingElements(child, selector, list);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function isTabbable(element: Element | null) {
|
|
166
|
+
return isFocusableElement(element) && getTabIndex(element) >= 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function focusable(container: Element) {
|
|
170
|
+
const candidates: FocusableElement[] = [];
|
|
171
|
+
appendCandidates(container, candidates);
|
|
172
|
+
return candidates.filter(isFocusableElement);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function tabbable(container: Element) {
|
|
176
|
+
const candidates = focusable(container);
|
|
177
|
+
return candidates.filter(
|
|
178
|
+
(element) => getTabIndex(element) >= 0 && isTabbableRadio(element, candidates),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getTabbableIn(container: HTMLElement, dir: 1 | -1): FocusableElement | undefined {
|
|
183
|
+
const list = tabbable(container);
|
|
184
|
+
const len = list.length;
|
|
185
|
+
if (len === 0) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const active = activeElement(ownerDocument(container)) as FocusableElement;
|
|
189
|
+
const index = list.indexOf(active);
|
|
190
|
+
const nextIndex = index === -1 ? (dir === 1 ? 0 : len - 1) : index + dir;
|
|
191
|
+
return list[nextIndex];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function getNextTabbable(referenceElement: Element | null): FocusableElement | null {
|
|
195
|
+
return (
|
|
196
|
+
getTabbableIn(ownerDocument(referenceElement).body, 1) || (referenceElement as FocusableElement)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getPreviousTabbable(referenceElement: Element | null): FocusableElement | null {
|
|
201
|
+
return (
|
|
202
|
+
getTabbableIn(ownerDocument(referenceElement).body, -1) ||
|
|
203
|
+
(referenceElement as FocusableElement)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getTabbableNearElement(referenceElement: Element | null, dir: 1 | -1) {
|
|
208
|
+
if (!referenceElement) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const list = tabbable(ownerDocument(referenceElement).body);
|
|
212
|
+
const elementCount = list.length;
|
|
213
|
+
if (elementCount === 0) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const index = list.indexOf(referenceElement as FocusableElement);
|
|
217
|
+
if (index === -1) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const nextIndex = (index + dir + elementCount) % elementCount;
|
|
221
|
+
return list[nextIndex];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function getTabbableAfterElement(referenceElement: Element | null): FocusableElement | null {
|
|
225
|
+
return getTabbableNearElement(referenceElement, 1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function getTabbableBeforeElement(
|
|
229
|
+
referenceElement: Element | null,
|
|
230
|
+
): FocusableElement | null {
|
|
231
|
+
return getTabbableNearElement(referenceElement, -1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function isOutsideEvent(event: FocusEvent, container?: Element) {
|
|
235
|
+
const containerElement = container || (event.currentTarget as Element);
|
|
236
|
+
const relatedTarget = event.relatedTarget as HTMLElement | null;
|
|
237
|
+
return !relatedTarget || !contains(containerElement, relatedTarget);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function disableFocusInside(container: HTMLElement) {
|
|
241
|
+
const tabbableElements = tabbable(container);
|
|
242
|
+
tabbableElements.forEach((element) => {
|
|
243
|
+
(element as HTMLElement).dataset.tabindex = element.getAttribute('tabindex') || '';
|
|
244
|
+
element.setAttribute('tabindex', '-1');
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function enableFocusInside(container: HTMLElement) {
|
|
249
|
+
const elements: HTMLElement[] = [];
|
|
250
|
+
appendMatchingElements(container, '[data-tabindex]', elements);
|
|
251
|
+
elements.forEach((element) => {
|
|
252
|
+
const tabindex = element.dataset.tabindex;
|
|
253
|
+
delete element.dataset.tabindex;
|
|
254
|
+
if (tabindex) {
|
|
255
|
+
element.setAttribute('tabindex', tabindex);
|
|
256
|
+
} else {
|
|
257
|
+
element.removeAttribute('tabindex');
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/floating-ui-react/types.ts (v1.6.0) — the subset the
|
|
2
|
+
// Base UI overlays use. `FloatingRootContext` IS the store (`FloatingRootStore`); a `FloatingContext`
|
|
3
|
+
// wraps it with the positioning data. Interaction hooks return `ElementProps` prop bags.
|
|
4
|
+
import type { FloatingRootStore } from './FloatingRootStore';
|
|
5
|
+
import type { FloatingTreeStore } from './FloatingTreeStore';
|
|
6
|
+
|
|
7
|
+
export type FloatingTreeType = FloatingTreeStore;
|
|
8
|
+
|
|
9
|
+
export interface VirtualElement {
|
|
10
|
+
getBoundingClientRect(): DOMRect;
|
|
11
|
+
contextElement?: Element;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ReferenceType = Element | VirtualElement;
|
|
15
|
+
|
|
16
|
+
export interface FloatingEvents {
|
|
17
|
+
emit(event: string, data?: any): void;
|
|
18
|
+
on(event: string, listener: (data: any) => void): void;
|
|
19
|
+
off(event: string, listener: (data: any) => void): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ContextData {
|
|
23
|
+
openEvent?: Event;
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type FloatingRootContext = FloatingRootStore;
|
|
28
|
+
|
|
29
|
+
export interface FloatingContext {
|
|
30
|
+
rootStore: FloatingRootContext;
|
|
31
|
+
open: boolean;
|
|
32
|
+
onOpenChange: (open: boolean, eventDetails: any) => void;
|
|
33
|
+
nodeId: string | undefined;
|
|
34
|
+
dataRef: { current: ContextData };
|
|
35
|
+
events: FloatingEvents;
|
|
36
|
+
elements: {
|
|
37
|
+
reference: ReferenceType | null;
|
|
38
|
+
floating: HTMLElement | null;
|
|
39
|
+
domReference: Element | null;
|
|
40
|
+
};
|
|
41
|
+
refs: { setPositionReference(node: ReferenceType | null): void };
|
|
42
|
+
[key: string]: any;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface FloatingNodeType {
|
|
46
|
+
id: string | undefined;
|
|
47
|
+
parentId: string | null;
|
|
48
|
+
context?: FloatingContext | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ElementProps {
|
|
52
|
+
reference?: Record<string, any>;
|
|
53
|
+
floating?: Record<string, any>;
|
|
54
|
+
item?: Record<string, any> | ((props: any) => Record<string, any>);
|
|
55
|
+
// Base UI's store-connected interaction hooks also expose the trigger prop bag.
|
|
56
|
+
trigger?: Record<string, any>;
|
|
57
|
+
}
|