@octanejs/base-ui 0.1.15 → 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/context-menu.ts +317 -0
- package/src/index.ts +3 -0
- package/src/menu.ts +3387 -0
- package/src/menubar.ts +160 -0
- package/src/popover.ts +7 -3
- package/src/utils/ContextMenuRootContext.ts +44 -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 +3 -0
- package/src/utils/findRootOwnerId.ts +19 -0
- package/src/utils/floating/gridNavigation.ts +54 -0
- package/src/utils/floating/useListNavigation.ts +1018 -0
- package/src/utils/floating/useTypeahead.ts +287 -0
- package/src/utils/getPseudoElementBounds.ts +54 -0
- package/src/utils/useMixedToggleClickHandler.ts +77 -0
- package/src/utils/useOpenInteractionType.ts +37 -1
|
@@ -141,14 +141,9 @@ export function useCompositeRoot(...args: any[]): {
|
|
|
141
141
|
|
|
142
142
|
if (activeIndex !== -1) {
|
|
143
143
|
onHighlightedIndexChange(activeIndex);
|
|
144
|
-
} else if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const firstEnabledIndex = findNonDisabledListIndex(
|
|
148
|
-
{ current: sortedElements },
|
|
149
|
-
{ disabledIndices },
|
|
150
|
-
);
|
|
151
|
-
if (!isIndexOutOfListBounds({ current: sortedElements }, firstEnabledIndex)) {
|
|
144
|
+
} else if (isListIndexDisabled(sortedElements, highlightedIndex, disabledIndices)) {
|
|
145
|
+
const firstEnabledIndex = findNonDisabledListIndex(sortedElements, { disabledIndices });
|
|
146
|
+
if (!isIndexOutOfListBounds(sortedElements, firstEnabledIndex)) {
|
|
152
147
|
onHighlightedIndexChange(firstEnabledIndex);
|
|
153
148
|
}
|
|
154
149
|
}
|
|
@@ -168,9 +163,9 @@ export function useCompositeRoot(...args: any[]): {
|
|
|
168
163
|
return;
|
|
169
164
|
}
|
|
170
165
|
const elements = elementsRef.current;
|
|
171
|
-
if (isListIndexDisabled(
|
|
172
|
-
const firstEnabledIndex = findNonDisabledListIndex(
|
|
173
|
-
if (!isIndexOutOfListBounds(
|
|
166
|
+
if (isListIndexDisabled(elements, highlightedIndex, disabledIndices)) {
|
|
167
|
+
const firstEnabledIndex = findNonDisabledListIndex(elements, { disabledIndices });
|
|
168
|
+
if (!isIndexOutOfListBounds(elements, firstEnabledIndex)) {
|
|
174
169
|
onHighlightedIndexChange(firstEnabledIndex);
|
|
175
170
|
}
|
|
176
171
|
}
|
|
@@ -294,7 +289,7 @@ export function useCompositeRoot(...args: any[]): {
|
|
|
294
289
|
nextIndex = onLoop(event, highlightedIndex, nextIndex, elementsRef);
|
|
295
290
|
}
|
|
296
291
|
} else {
|
|
297
|
-
nextIndex = findNonDisabledListIndex(elementsRef, {
|
|
292
|
+
nextIndex = findNonDisabledListIndex(elementsRef.current, {
|
|
298
293
|
startingIndex: nextIndex,
|
|
299
294
|
decrement: backwardKeys.includes(event.key),
|
|
300
295
|
disabledIndices,
|
|
@@ -302,7 +297,10 @@ export function useCompositeRoot(...args: any[]): {
|
|
|
302
297
|
}
|
|
303
298
|
}
|
|
304
299
|
|
|
305
|
-
if (
|
|
300
|
+
if (
|
|
301
|
+
nextIndex !== highlightedIndex &&
|
|
302
|
+
!isIndexOutOfListBounds(elementsRef.current, nextIndex)
|
|
303
|
+
) {
|
|
306
304
|
if (stopEventPropagation) {
|
|
307
305
|
event.stopPropagation();
|
|
308
306
|
}
|
package/src/utils/constants.ts
CHANGED
|
@@ -2,8 +2,30 @@
|
|
|
2
2
|
// overlay/positioner layer needs. `POPUP_COLLISION_AVOIDANCE` is the default collision config
|
|
3
3
|
// popups pass to `useAnchorPositioning`; `DISABLED_TRANSITIONS_STYLE` disables transitions while
|
|
4
4
|
// a popup is in the `starting` transition phase (so the mount frame doesn't animate).
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Used by regular popups that usually aren't scrollable and are allowed to
|
|
8
|
+
* freely flip to any axis of placement.
|
|
9
|
+
*/
|
|
5
10
|
export const POPUP_COLLISION_AVOIDANCE = {
|
|
6
11
|
fallbackAxisSide: 'end',
|
|
7
12
|
} as const;
|
|
8
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Used for dropdowns that usually strictly prefer top/bottom placements and
|
|
16
|
+
* use `var(--available-height)` to limit their height.
|
|
17
|
+
*/
|
|
18
|
+
export const DROPDOWN_COLLISION_AVOIDANCE = {
|
|
19
|
+
fallbackAxisSide: 'none',
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
9
22
|
export const DISABLED_TRANSITIONS_STYLE = { style: { transition: 'none' } } as const;
|
|
23
|
+
|
|
24
|
+
/** How long a typeahead buffer survives without a keystroke before it resets. */
|
|
25
|
+
export const TYPEAHEAD_RESET_MS = 500;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* How long after a hover-open a click is still treated as "patient" (and therefore does not
|
|
29
|
+
* immediately close the popup it just opened).
|
|
30
|
+
*/
|
|
31
|
+
export const PATIENT_CLICK_THRESHOLD = 500;
|
|
@@ -28,8 +28,11 @@ export const REASONS = {
|
|
|
28
28
|
outsidePress: 'outside-press',
|
|
29
29
|
closePress: 'close-press',
|
|
30
30
|
focusOut: 'focus-out',
|
|
31
|
+
listNavigation: 'list-navigation',
|
|
31
32
|
escapeKey: 'escape-key',
|
|
32
33
|
imperativeAction: 'imperative-action',
|
|
34
|
+
cancelOpen: 'cancel-open',
|
|
35
|
+
siblingOpen: 'sibling-open',
|
|
33
36
|
} as const;
|
|
34
37
|
|
|
35
38
|
export type BaseUIEventReason = (typeof REASONS)[keyof typeof REASONS];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/menu/utils/findRootOwnerId.ts (v1.6.0). Pure —
|
|
2
|
+
// no octane adaptations needed.
|
|
3
|
+
//
|
|
4
|
+
// Walks up from a node to the nearest element carrying `data-rootownerid` (stamped on
|
|
5
|
+
// `Menu.Popup`), so a mouseup landing in ANOTHER popup of the same menu root can be told apart
|
|
6
|
+
// from a mouseup that landed outside the menu tree entirely.
|
|
7
|
+
import { getParentNode, isHTMLElement, isLastTraversableNode } from '@floating-ui/utils/dom';
|
|
8
|
+
|
|
9
|
+
export function findRootOwnerId(node: Node): string | undefined {
|
|
10
|
+
if (isHTMLElement(node) && node.hasAttribute('data-rootownerid')) {
|
|
11
|
+
return node.getAttribute('data-rootownerid') ?? undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (isLastTraversableNode(node)) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return findRootOwnerId(getParentNode(node));
|
|
19
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/floating-ui-react/hooks/gridNavigation.ts (v1.6.0). The
|
|
2
|
+
// grid navigator `useListNavigation` injects for its own `grid` option — distinct from
|
|
3
|
+
// `../composite/gridNavigation`, which builds the navigator `useCompositeRoot` takes.
|
|
4
|
+
//
|
|
5
|
+
// octane adaptations: the event is the NATIVE `KeyboardEvent`, and `React.RefObject<T>` is a plain
|
|
6
|
+
// `{ current: T }`. Not a hook, so no slot. Upstream's `utils/composite` module is split in this
|
|
7
|
+
// binding: the list algebra lives in `../composite/list-utils`, the grid algorithm in
|
|
8
|
+
// `../composite/grid-utils`.
|
|
9
|
+
import { getGridNavigatedIndex } from '../composite/grid-utils';
|
|
10
|
+
import { isIndexOutOfListBounds, type DisabledIndices } from '../composite/list-utils';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Positional arguments are deliberate: property names of an options object
|
|
14
|
+
* don't minify, and the signature is locked to the caller via `typeof` on the
|
|
15
|
+
* `grid` option of `useListNavigation`.
|
|
16
|
+
*
|
|
17
|
+
* The injected grid navigator only ever operates on a uniform 1x1 grid (sizes are
|
|
18
|
+
* always `1x1` and packing is never dense), so the cell-map machinery that supports
|
|
19
|
+
* multi-cell items collapses to an identity transform over the item list. Calling
|
|
20
|
+
* `getGridNavigatedIndex` directly keeps the cell-map helpers
|
|
21
|
+
* (`createGridCellMap`/`getGridCellIndexOfCorner`/`getGridCellIndices`) out of
|
|
22
|
+
* grid-combobox bundles.
|
|
23
|
+
*/
|
|
24
|
+
export function gridNavigation(
|
|
25
|
+
event: KeyboardEvent,
|
|
26
|
+
prevIndex: number,
|
|
27
|
+
listRef: { current: Array<HTMLElement | null> },
|
|
28
|
+
orientation: 'horizontal' | 'vertical' | 'both',
|
|
29
|
+
loopFocus: boolean,
|
|
30
|
+
rtl: boolean,
|
|
31
|
+
disabledIndices: DisabledIndices | undefined,
|
|
32
|
+
minIndex: number,
|
|
33
|
+
maxIndex: number,
|
|
34
|
+
cols = 2,
|
|
35
|
+
): number | undefined {
|
|
36
|
+
const nextIndex = getGridNavigatedIndex(listRef.current, {
|
|
37
|
+
event,
|
|
38
|
+
orientation,
|
|
39
|
+
loopFocus,
|
|
40
|
+
rtl,
|
|
41
|
+
cols,
|
|
42
|
+
disabledIndices,
|
|
43
|
+
minIndex,
|
|
44
|
+
maxIndex,
|
|
45
|
+
// An out-of-range previous index falls back to the first enabled item.
|
|
46
|
+
prevIndex: prevIndex > maxIndex ? minIndex : prevIndex,
|
|
47
|
+
stopEvent: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// `getGridNavigatedIndex` can return an out-of-bounds sentinel (e.g. `-1` when there is no
|
|
51
|
+
// previous item to move from); surface that as `undefined` so the caller treats it as
|
|
52
|
+
// "no navigation" rather than highlighting index `-1`.
|
|
53
|
+
return isIndexOutOfListBounds(listRef.current, nextIndex) ? undefined : nextIndex;
|
|
54
|
+
}
|