@glob-ui/hooks 0.0.0-stage → 0.2.0
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 +8 -2
- package/dist/index.d.mts +111 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +346 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +326 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +65 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kevin Martínez E
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @glob-ui/hooks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Internal hooks used by GlobUI components.
|
|
4
|
+
|
|
5
|
+
> **Internal API.** This package is used by the other GlobUI packages. Its exports can change in any release, so depend on the component packages instead.
|
|
6
|
+
|
|
7
|
+
## License
|
|
8
|
+
|
|
9
|
+
MIT. See [LICENSE](./LICENSE).
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as _floating_ui_react_dom from '@floating-ui/react-dom';
|
|
2
|
+
import { Placement } from '@floating-ui/react-dom';
|
|
3
|
+
export { Placement } from '@floating-ui/react-dom';
|
|
4
|
+
import * as react from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* State that is controlled when `prop` is defined and internal otherwise.
|
|
8
|
+
* `onChange` fires only when the value actually changes.
|
|
9
|
+
*/
|
|
10
|
+
declare function useControllableState<T>(prop: T | undefined, defaultProp: T, onChange?: (value: T) => void): readonly [T, (next: T) => void];
|
|
11
|
+
|
|
12
|
+
interface UseFloatingPanelOptions {
|
|
13
|
+
open: boolean;
|
|
14
|
+
onOpenChange: (open: boolean) => void;
|
|
15
|
+
placement?: Placement;
|
|
16
|
+
/** Gap between trigger and panel, in px. */
|
|
17
|
+
offset?: number;
|
|
18
|
+
/** Arrow element, positioned by the arrow middleware. */
|
|
19
|
+
arrow?: HTMLElement | null;
|
|
20
|
+
/** Constrain the panel to the space left in the viewport (max-height/max-width). */
|
|
21
|
+
fitViewport?: boolean;
|
|
22
|
+
/** Runs right after the panel is shown. Use it to move focus. */
|
|
23
|
+
onShow?: (panel: HTMLElement) => void;
|
|
24
|
+
/**
|
|
25
|
+
* When the browser closes the panel (light dismiss, Esc), return focus to the trigger
|
|
26
|
+
* if focus was inside the panel or lost. A click elsewhere keeps its own focus.
|
|
27
|
+
*/
|
|
28
|
+
restoreFocus?: boolean;
|
|
29
|
+
/** Element that gets focus back, when it is not the trigger (e.g. the trigger is a wrapper). */
|
|
30
|
+
restoreFocusTo?: () => HTMLElement | null;
|
|
31
|
+
/** Make the panel at least as wide as the trigger (listboxes). */
|
|
32
|
+
matchTriggerWidth?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Positions a `popover` element next to a trigger and keeps it in sync with `open`.
|
|
36
|
+
* The panel element must have a `popover` attribute ("auto" or "manual").
|
|
37
|
+
*/
|
|
38
|
+
declare function useFloatingPanel({ open, onOpenChange, placement, offset, arrow, fitViewport, onShow, restoreFocus, restoreFocusTo, matchTriggerWidth, }: UseFloatingPanelOptions): {
|
|
39
|
+
trigger: HTMLElement | null;
|
|
40
|
+
setTrigger: react.Dispatch<react.SetStateAction<HTMLElement | null>>;
|
|
41
|
+
panel: HTMLElement | null;
|
|
42
|
+
setPanel: react.Dispatch<react.SetStateAction<HTMLElement | null>>;
|
|
43
|
+
floatingStyles: React.CSSProperties;
|
|
44
|
+
isPositioned: boolean;
|
|
45
|
+
placement: Placement;
|
|
46
|
+
middlewareData: _floating_ui_react_dom.MiddlewareData;
|
|
47
|
+
triggerHandlers: {
|
|
48
|
+
onPointerDown: () => void;
|
|
49
|
+
onClick: () => void;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
interface UseRovingFocusOptions {
|
|
54
|
+
/** Which arrows move focus. "both" accepts all four. */
|
|
55
|
+
orientation?: "horizontal" | "vertical" | "both";
|
|
56
|
+
/** Wrap from last to first and back. */
|
|
57
|
+
loop?: boolean;
|
|
58
|
+
/** Typing printable characters jumps to the next item whose text starts with them. */
|
|
59
|
+
typeahead?: boolean;
|
|
60
|
+
/** Items to move between. Disabled ones are excluded by the default selector. */
|
|
61
|
+
itemSelector?: string;
|
|
62
|
+
/** Runs after an item receives focus (e.g. to activate a tab). */
|
|
63
|
+
onNavigate?: (item: HTMLElement) => void;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Keyboard focus movement inside a composite widget (tablist, menu, accordion headers).
|
|
67
|
+
* Spread `rootProps` on the container. Only items whose closest `[data-roving-root]` is this
|
|
68
|
+
* container are used, so nested widgets do not mix.
|
|
69
|
+
*/
|
|
70
|
+
declare function useRovingFocus({ orientation, loop, typeahead, itemSelector, onNavigate, }?: UseRovingFocusOptions): {
|
|
71
|
+
rootProps: {
|
|
72
|
+
"data-roving-root": string;
|
|
73
|
+
onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
74
|
+
};
|
|
75
|
+
onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
interface UseListboxOptions<T> {
|
|
79
|
+
/** Base id; option ids are derived from it. */
|
|
80
|
+
id: string;
|
|
81
|
+
/** Options currently shown (already filtered), in display order. */
|
|
82
|
+
items: T[];
|
|
83
|
+
getKey: (item: T) => string;
|
|
84
|
+
/** Text matched by typeahead. */
|
|
85
|
+
getText: (item: T) => string;
|
|
86
|
+
isDisabled?: (item: T) => boolean;
|
|
87
|
+
open: boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Active-option state for a listbox driven by `aria-activedescendant` (focus stays on the
|
|
91
|
+
* combobox or search input). Handles arrows, Home/End, PageUp/PageDown and typeahead, skips
|
|
92
|
+
* disabled options, and keeps the active option scrolled into view.
|
|
93
|
+
*/
|
|
94
|
+
declare function useListbox<T>({ id, items, getKey, getText, isDisabled, open }: UseListboxOptions<T>): {
|
|
95
|
+
activeKey: string | null;
|
|
96
|
+
activeItem: T | undefined;
|
|
97
|
+
setActiveKey: react.Dispatch<react.SetStateAction<string | null>>;
|
|
98
|
+
move: (to: "next" | "prev" | "first" | "last" | "pageDown" | "pageUp") => void;
|
|
99
|
+
typeahead: (char: string) => void;
|
|
100
|
+
navigate: (e: React.KeyboardEvent, { homeEnd, typeahead: allowTypeahead }?: {
|
|
101
|
+
homeEnd?: boolean | undefined;
|
|
102
|
+
typeahead?: boolean | undefined;
|
|
103
|
+
}) => boolean;
|
|
104
|
+
optionId: (key: string) => string;
|
|
105
|
+
/** Value for `aria-activedescendant` on the element that has focus. */
|
|
106
|
+
activeDescendant: string | undefined;
|
|
107
|
+
};
|
|
108
|
+
/** A single printable character without Ctrl/Meta/Alt (Space excluded). */
|
|
109
|
+
declare function isPrintable(e: React.KeyboardEvent): boolean;
|
|
110
|
+
|
|
111
|
+
export { type UseFloatingPanelOptions, type UseListboxOptions, type UseRovingFocusOptions, isPrintable, useControllableState, useFloatingPanel, useListbox, useRovingFocus };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as _floating_ui_react_dom from '@floating-ui/react-dom';
|
|
2
|
+
import { Placement } from '@floating-ui/react-dom';
|
|
3
|
+
export { Placement } from '@floating-ui/react-dom';
|
|
4
|
+
import * as react from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* State that is controlled when `prop` is defined and internal otherwise.
|
|
8
|
+
* `onChange` fires only when the value actually changes.
|
|
9
|
+
*/
|
|
10
|
+
declare function useControllableState<T>(prop: T | undefined, defaultProp: T, onChange?: (value: T) => void): readonly [T, (next: T) => void];
|
|
11
|
+
|
|
12
|
+
interface UseFloatingPanelOptions {
|
|
13
|
+
open: boolean;
|
|
14
|
+
onOpenChange: (open: boolean) => void;
|
|
15
|
+
placement?: Placement;
|
|
16
|
+
/** Gap between trigger and panel, in px. */
|
|
17
|
+
offset?: number;
|
|
18
|
+
/** Arrow element, positioned by the arrow middleware. */
|
|
19
|
+
arrow?: HTMLElement | null;
|
|
20
|
+
/** Constrain the panel to the space left in the viewport (max-height/max-width). */
|
|
21
|
+
fitViewport?: boolean;
|
|
22
|
+
/** Runs right after the panel is shown. Use it to move focus. */
|
|
23
|
+
onShow?: (panel: HTMLElement) => void;
|
|
24
|
+
/**
|
|
25
|
+
* When the browser closes the panel (light dismiss, Esc), return focus to the trigger
|
|
26
|
+
* if focus was inside the panel or lost. A click elsewhere keeps its own focus.
|
|
27
|
+
*/
|
|
28
|
+
restoreFocus?: boolean;
|
|
29
|
+
/** Element that gets focus back, when it is not the trigger (e.g. the trigger is a wrapper). */
|
|
30
|
+
restoreFocusTo?: () => HTMLElement | null;
|
|
31
|
+
/** Make the panel at least as wide as the trigger (listboxes). */
|
|
32
|
+
matchTriggerWidth?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Positions a `popover` element next to a trigger and keeps it in sync with `open`.
|
|
36
|
+
* The panel element must have a `popover` attribute ("auto" or "manual").
|
|
37
|
+
*/
|
|
38
|
+
declare function useFloatingPanel({ open, onOpenChange, placement, offset, arrow, fitViewport, onShow, restoreFocus, restoreFocusTo, matchTriggerWidth, }: UseFloatingPanelOptions): {
|
|
39
|
+
trigger: HTMLElement | null;
|
|
40
|
+
setTrigger: react.Dispatch<react.SetStateAction<HTMLElement | null>>;
|
|
41
|
+
panel: HTMLElement | null;
|
|
42
|
+
setPanel: react.Dispatch<react.SetStateAction<HTMLElement | null>>;
|
|
43
|
+
floatingStyles: React.CSSProperties;
|
|
44
|
+
isPositioned: boolean;
|
|
45
|
+
placement: Placement;
|
|
46
|
+
middlewareData: _floating_ui_react_dom.MiddlewareData;
|
|
47
|
+
triggerHandlers: {
|
|
48
|
+
onPointerDown: () => void;
|
|
49
|
+
onClick: () => void;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
interface UseRovingFocusOptions {
|
|
54
|
+
/** Which arrows move focus. "both" accepts all four. */
|
|
55
|
+
orientation?: "horizontal" | "vertical" | "both";
|
|
56
|
+
/** Wrap from last to first and back. */
|
|
57
|
+
loop?: boolean;
|
|
58
|
+
/** Typing printable characters jumps to the next item whose text starts with them. */
|
|
59
|
+
typeahead?: boolean;
|
|
60
|
+
/** Items to move between. Disabled ones are excluded by the default selector. */
|
|
61
|
+
itemSelector?: string;
|
|
62
|
+
/** Runs after an item receives focus (e.g. to activate a tab). */
|
|
63
|
+
onNavigate?: (item: HTMLElement) => void;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Keyboard focus movement inside a composite widget (tablist, menu, accordion headers).
|
|
67
|
+
* Spread `rootProps` on the container. Only items whose closest `[data-roving-root]` is this
|
|
68
|
+
* container are used, so nested widgets do not mix.
|
|
69
|
+
*/
|
|
70
|
+
declare function useRovingFocus({ orientation, loop, typeahead, itemSelector, onNavigate, }?: UseRovingFocusOptions): {
|
|
71
|
+
rootProps: {
|
|
72
|
+
"data-roving-root": string;
|
|
73
|
+
onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
74
|
+
};
|
|
75
|
+
onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
interface UseListboxOptions<T> {
|
|
79
|
+
/** Base id; option ids are derived from it. */
|
|
80
|
+
id: string;
|
|
81
|
+
/** Options currently shown (already filtered), in display order. */
|
|
82
|
+
items: T[];
|
|
83
|
+
getKey: (item: T) => string;
|
|
84
|
+
/** Text matched by typeahead. */
|
|
85
|
+
getText: (item: T) => string;
|
|
86
|
+
isDisabled?: (item: T) => boolean;
|
|
87
|
+
open: boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Active-option state for a listbox driven by `aria-activedescendant` (focus stays on the
|
|
91
|
+
* combobox or search input). Handles arrows, Home/End, PageUp/PageDown and typeahead, skips
|
|
92
|
+
* disabled options, and keeps the active option scrolled into view.
|
|
93
|
+
*/
|
|
94
|
+
declare function useListbox<T>({ id, items, getKey, getText, isDisabled, open }: UseListboxOptions<T>): {
|
|
95
|
+
activeKey: string | null;
|
|
96
|
+
activeItem: T | undefined;
|
|
97
|
+
setActiveKey: react.Dispatch<react.SetStateAction<string | null>>;
|
|
98
|
+
move: (to: "next" | "prev" | "first" | "last" | "pageDown" | "pageUp") => void;
|
|
99
|
+
typeahead: (char: string) => void;
|
|
100
|
+
navigate: (e: React.KeyboardEvent, { homeEnd, typeahead: allowTypeahead }?: {
|
|
101
|
+
homeEnd?: boolean | undefined;
|
|
102
|
+
typeahead?: boolean | undefined;
|
|
103
|
+
}) => boolean;
|
|
104
|
+
optionId: (key: string) => string;
|
|
105
|
+
/** Value for `aria-activedescendant` on the element that has focus. */
|
|
106
|
+
activeDescendant: string | undefined;
|
|
107
|
+
};
|
|
108
|
+
/** A single printable character without Ctrl/Meta/Alt (Space excluded). */
|
|
109
|
+
declare function isPrintable(e: React.KeyboardEvent): boolean;
|
|
110
|
+
|
|
111
|
+
export { type UseFloatingPanelOptions, type UseListboxOptions, type UseRovingFocusOptions, isPrintable, useControllableState, useFloatingPanel, useListbox, useRovingFocus };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __defProps = Object.defineProperties;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
11
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
12
|
+
var __spreadValues = (a, b) => {
|
|
13
|
+
for (var prop in b || (b = {}))
|
|
14
|
+
if (__hasOwnProp.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
if (__getOwnPropSymbols)
|
|
17
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
18
|
+
if (__propIsEnum.call(b, prop))
|
|
19
|
+
__defNormalProp(a, prop, b[prop]);
|
|
20
|
+
}
|
|
21
|
+
return a;
|
|
22
|
+
};
|
|
23
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
24
|
+
var __export = (target, all) => {
|
|
25
|
+
for (var name in all)
|
|
26
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
27
|
+
};
|
|
28
|
+
var __copyProps = (to, from, except, desc) => {
|
|
29
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
30
|
+
for (let key of __getOwnPropNames(from))
|
|
31
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
32
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
33
|
+
}
|
|
34
|
+
return to;
|
|
35
|
+
};
|
|
36
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
|
+
|
|
38
|
+
// src/index.ts
|
|
39
|
+
var index_exports = {};
|
|
40
|
+
__export(index_exports, {
|
|
41
|
+
isPrintable: () => isPrintable,
|
|
42
|
+
useControllableState: () => useControllableState,
|
|
43
|
+
useFloatingPanel: () => useFloatingPanel,
|
|
44
|
+
useListbox: () => useListbox,
|
|
45
|
+
useRovingFocus: () => useRovingFocus
|
|
46
|
+
});
|
|
47
|
+
module.exports = __toCommonJS(index_exports);
|
|
48
|
+
|
|
49
|
+
// src/use-controllable-state.ts
|
|
50
|
+
var import_react = require("react");
|
|
51
|
+
function useControllableState(prop, defaultProp, onChange) {
|
|
52
|
+
const [internal, setInternal] = (0, import_react.useState)(defaultProp);
|
|
53
|
+
const isControlled = prop !== void 0;
|
|
54
|
+
const value = isControlled ? prop : internal;
|
|
55
|
+
const setValue = (0, import_react.useCallback)(
|
|
56
|
+
(next) => {
|
|
57
|
+
if (!isControlled) setInternal(next);
|
|
58
|
+
if (next !== value) onChange == null ? void 0 : onChange(next);
|
|
59
|
+
},
|
|
60
|
+
[isControlled, value, onChange]
|
|
61
|
+
);
|
|
62
|
+
return [value, setValue];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/use-floating-panel.ts
|
|
66
|
+
var import_react2 = require("react");
|
|
67
|
+
var import_react_dom = require("@floating-ui/react-dom");
|
|
68
|
+
function isPopoverOpen(el) {
|
|
69
|
+
try {
|
|
70
|
+
return el.matches(":popover-open");
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function useFloatingPanel({
|
|
76
|
+
open,
|
|
77
|
+
onOpenChange,
|
|
78
|
+
placement = "bottom",
|
|
79
|
+
offset = 8,
|
|
80
|
+
arrow = null,
|
|
81
|
+
fitViewport = true,
|
|
82
|
+
onShow,
|
|
83
|
+
restoreFocus = true,
|
|
84
|
+
restoreFocusTo,
|
|
85
|
+
matchTriggerWidth = false
|
|
86
|
+
}) {
|
|
87
|
+
const [trigger, setTrigger] = (0, import_react2.useState)(null);
|
|
88
|
+
const [panel, setPanel] = (0, import_react2.useState)(null);
|
|
89
|
+
const onShowRef = (0, import_react2.useRef)(onShow);
|
|
90
|
+
const onOpenChangeRef = (0, import_react2.useRef)(onOpenChange);
|
|
91
|
+
const restoreFocusToRef = (0, import_react2.useRef)(restoreFocusTo);
|
|
92
|
+
(0, import_react2.useEffect)(() => {
|
|
93
|
+
onShowRef.current = onShow;
|
|
94
|
+
onOpenChangeRef.current = onOpenChange;
|
|
95
|
+
restoreFocusToRef.current = restoreFocusTo;
|
|
96
|
+
});
|
|
97
|
+
const { floatingStyles, update, isPositioned, placement: finalPlacement, middlewareData } = (0, import_react_dom.useFloating)({
|
|
98
|
+
open,
|
|
99
|
+
placement,
|
|
100
|
+
// Panels live in the top layer, outside any overflow:hidden ancestor.
|
|
101
|
+
strategy: "fixed",
|
|
102
|
+
elements: { reference: trigger, floating: panel },
|
|
103
|
+
middleware: [
|
|
104
|
+
(0, import_react_dom.offset)(offset),
|
|
105
|
+
(0, import_react_dom.flip)({ padding: 8 }),
|
|
106
|
+
(0, import_react_dom.shift)({ padding: 8 }),
|
|
107
|
+
...fitViewport ? [
|
|
108
|
+
(0, import_react_dom.size)({
|
|
109
|
+
padding: 8,
|
|
110
|
+
apply({ availableHeight, availableWidth, elements, rects }) {
|
|
111
|
+
Object.assign(elements.floating.style, __spreadValues({
|
|
112
|
+
maxHeight: `${Math.max(0, availableHeight)}px`,
|
|
113
|
+
maxWidth: `${Math.max(0, availableWidth)}px`
|
|
114
|
+
}, matchTriggerWidth ? { minWidth: `${rects.reference.width}px` } : {}));
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
] : [],
|
|
118
|
+
...arrow ? [(0, import_react_dom.arrow)({ element: arrow, padding: 6 })] : []
|
|
119
|
+
]
|
|
120
|
+
});
|
|
121
|
+
(0, import_react2.useEffect)(() => {
|
|
122
|
+
var _a;
|
|
123
|
+
if (!panel || typeof panel.showPopover !== "function") return;
|
|
124
|
+
if (!open) {
|
|
125
|
+
if (isPopoverOpen(panel)) panel.hidePopover();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!isPopoverOpen(panel)) panel.showPopover();
|
|
129
|
+
(_a = onShowRef.current) == null ? void 0 : _a.call(onShowRef, panel);
|
|
130
|
+
return trigger ? (0, import_react_dom.autoUpdate)(trigger, panel, update) : void 0;
|
|
131
|
+
}, [open, panel, trigger, update]);
|
|
132
|
+
(0, import_react2.useEffect)(() => {
|
|
133
|
+
if (!panel) return;
|
|
134
|
+
const onToggle = (e) => {
|
|
135
|
+
var _a, _b, _c;
|
|
136
|
+
if (e.newState !== "closed") return;
|
|
137
|
+
onOpenChangeRef.current(false);
|
|
138
|
+
if (!restoreFocus) return;
|
|
139
|
+
const active = document.activeElement;
|
|
140
|
+
if (!active || active === document.body || panel.contains(active)) {
|
|
141
|
+
;
|
|
142
|
+
(_c = (_b = (_a = restoreFocusToRef.current) == null ? void 0 : _a.call(restoreFocusToRef)) != null ? _b : trigger) == null ? void 0 : _c.focus();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
panel.addEventListener("toggle", onToggle);
|
|
146
|
+
return () => panel.removeEventListener("toggle", onToggle);
|
|
147
|
+
}, [panel, trigger, restoreFocus]);
|
|
148
|
+
const openAtPointerDown = (0, import_react2.useRef)(null);
|
|
149
|
+
const triggerHandlers = {
|
|
150
|
+
onPointerDown: () => {
|
|
151
|
+
openAtPointerDown.current = open;
|
|
152
|
+
},
|
|
153
|
+
onClick: () => {
|
|
154
|
+
var _a;
|
|
155
|
+
const wasOpen = (_a = openAtPointerDown.current) != null ? _a : open;
|
|
156
|
+
openAtPointerDown.current = null;
|
|
157
|
+
onOpenChange(!wasOpen);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
trigger,
|
|
162
|
+
setTrigger,
|
|
163
|
+
panel,
|
|
164
|
+
setPanel,
|
|
165
|
+
// opacity (not visibility) until positioned: the panel must be focusable right after showPopover().
|
|
166
|
+
floatingStyles: isPositioned ? floatingStyles : __spreadProps(__spreadValues({}, floatingStyles), { opacity: 0 }),
|
|
167
|
+
isPositioned,
|
|
168
|
+
placement: finalPlacement,
|
|
169
|
+
middlewareData,
|
|
170
|
+
triggerHandlers
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/use-roving-focus.ts
|
|
175
|
+
var import_react3 = require("react");
|
|
176
|
+
var DEFAULT_SELECTOR = '[data-roving-item]:not([disabled]):not([aria-disabled="true"])';
|
|
177
|
+
var TYPEAHEAD_RESET_MS = 500;
|
|
178
|
+
function useRovingFocus({
|
|
179
|
+
orientation = "vertical",
|
|
180
|
+
loop = true,
|
|
181
|
+
typeahead = false,
|
|
182
|
+
itemSelector = DEFAULT_SELECTOR,
|
|
183
|
+
onNavigate
|
|
184
|
+
} = {}) {
|
|
185
|
+
const buffer = (0, import_react3.useRef)("");
|
|
186
|
+
const resetTimer = (0, import_react3.useRef)(void 0);
|
|
187
|
+
function getItems(root) {
|
|
188
|
+
return Array.from(root.querySelectorAll(itemSelector)).filter(
|
|
189
|
+
(item) => item.closest("[data-roving-root]") === root
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
function focusItem(item) {
|
|
193
|
+
if (!item) return;
|
|
194
|
+
item.focus();
|
|
195
|
+
onNavigate == null ? void 0 : onNavigate(item);
|
|
196
|
+
}
|
|
197
|
+
function onKeyDown(e) {
|
|
198
|
+
if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
199
|
+
const root = e.currentTarget;
|
|
200
|
+
const items = getItems(root);
|
|
201
|
+
if (items.length === 0) return;
|
|
202
|
+
const index = items.indexOf(document.activeElement);
|
|
203
|
+
const prevKeys = orientation === "horizontal" ? ["ArrowLeft"] : orientation === "vertical" ? ["ArrowUp"] : ["ArrowLeft", "ArrowUp"];
|
|
204
|
+
const nextKeys = orientation === "horizontal" ? ["ArrowRight"] : orientation === "vertical" ? ["ArrowDown"] : ["ArrowRight", "ArrowDown"];
|
|
205
|
+
let target;
|
|
206
|
+
if (e.key === "Home") target = items[0];
|
|
207
|
+
else if (e.key === "End") target = items[items.length - 1];
|
|
208
|
+
else if (nextKeys.includes(e.key)) {
|
|
209
|
+
if (index === -1) target = items[0];
|
|
210
|
+
else if (index < items.length - 1) target = items[index + 1];
|
|
211
|
+
else if (loop) target = items[0];
|
|
212
|
+
} else if (prevKeys.includes(e.key)) {
|
|
213
|
+
if (index === -1) target = items[items.length - 1];
|
|
214
|
+
else if (index > 0) target = items[index - 1];
|
|
215
|
+
else if (loop) target = items[items.length - 1];
|
|
216
|
+
} else if (typeahead && e.key.length === 1 && e.key !== " ") {
|
|
217
|
+
target = findByTypeahead(items, index, e.key);
|
|
218
|
+
if (!target) return;
|
|
219
|
+
} else {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
focusItem(target);
|
|
224
|
+
}
|
|
225
|
+
function findByTypeahead(items, index, key) {
|
|
226
|
+
clearTimeout(resetTimer.current);
|
|
227
|
+
resetTimer.current = setTimeout(() => buffer.current = "", TYPEAHEAD_RESET_MS);
|
|
228
|
+
buffer.current += key.toLowerCase();
|
|
229
|
+
const search = buffer.current;
|
|
230
|
+
const isRepeat = search.length > 1 && search.split("").every((c) => c === search[0]);
|
|
231
|
+
const query = isRepeat ? search[0] : search;
|
|
232
|
+
const start = isRepeat || search.length === 1 ? index + 1 : Math.max(index, 0);
|
|
233
|
+
const text = (item) => {
|
|
234
|
+
var _a, _b;
|
|
235
|
+
return ((_b = (_a = item.dataset.textValue) != null ? _a : item.textContent) != null ? _b : "").trim().toLowerCase();
|
|
236
|
+
};
|
|
237
|
+
for (let i = 0; i < items.length; i++) {
|
|
238
|
+
const item = items[(start + i) % items.length];
|
|
239
|
+
if (text(item).startsWith(query)) return item;
|
|
240
|
+
}
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
rootProps: { "data-roving-root": "", onKeyDown },
|
|
245
|
+
onKeyDown
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/use-listbox.ts
|
|
250
|
+
var import_react4 = require("react");
|
|
251
|
+
var TYPEAHEAD_RESET_MS2 = 500;
|
|
252
|
+
var PAGE_SIZE = 10;
|
|
253
|
+
function useListbox({ id, items, getKey, getText, isDisabled, open }) {
|
|
254
|
+
var _a;
|
|
255
|
+
const [activeKeyState, setActiveKey] = (0, import_react4.useState)(null);
|
|
256
|
+
const buffer = (0, import_react4.useRef)("");
|
|
257
|
+
const resetTimer = (0, import_react4.useRef)(void 0);
|
|
258
|
+
const enabled = items.filter((item) => !(isDisabled == null ? void 0 : isDisabled(item)));
|
|
259
|
+
const activeItem = open ? (_a = enabled.find((item) => getKey(item) === activeKeyState)) != null ? _a : enabled[0] : void 0;
|
|
260
|
+
const activeKey = activeItem === void 0 ? null : getKey(activeItem);
|
|
261
|
+
const optionId = (key) => `${id}-option-${encodeURIComponent(key)}`;
|
|
262
|
+
function move(to) {
|
|
263
|
+
if (enabled.length === 0) return;
|
|
264
|
+
const index = activeItem === void 0 ? -1 : enabled.indexOf(activeItem);
|
|
265
|
+
const last = enabled.length - 1;
|
|
266
|
+
const next = {
|
|
267
|
+
next: index < 0 ? 0 : Math.min(index + 1, last),
|
|
268
|
+
prev: index < 0 ? last : Math.max(index - 1, 0),
|
|
269
|
+
first: 0,
|
|
270
|
+
last,
|
|
271
|
+
pageDown: Math.min(Math.max(index, 0) + PAGE_SIZE, last),
|
|
272
|
+
pageUp: Math.max(index - PAGE_SIZE, 0)
|
|
273
|
+
}[to];
|
|
274
|
+
setActiveKey(getKey(enabled[next]));
|
|
275
|
+
}
|
|
276
|
+
function typeahead(char) {
|
|
277
|
+
clearTimeout(resetTimer.current);
|
|
278
|
+
resetTimer.current = setTimeout(() => buffer.current = "", TYPEAHEAD_RESET_MS2);
|
|
279
|
+
buffer.current += char.toLowerCase();
|
|
280
|
+
const search = buffer.current;
|
|
281
|
+
const isRepeat = search.length > 1 && search.split("").every((c) => c === search[0]);
|
|
282
|
+
const query = isRepeat ? search[0] : search;
|
|
283
|
+
const index = activeItem === void 0 ? -1 : enabled.indexOf(activeItem);
|
|
284
|
+
const start = isRepeat || search.length === 1 ? index + 1 : Math.max(index, 0);
|
|
285
|
+
for (let i = 0; i < enabled.length; i++) {
|
|
286
|
+
const item = enabled[(start + i) % enabled.length];
|
|
287
|
+
if (getText(item).toLowerCase().startsWith(query)) {
|
|
288
|
+
setActiveKey(getKey(item));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function navigate(e, { homeEnd = true, typeahead: allowTypeahead = false } = {}) {
|
|
294
|
+
const keys = __spreadValues({
|
|
295
|
+
ArrowDown: "next",
|
|
296
|
+
ArrowUp: "prev",
|
|
297
|
+
PageDown: "pageDown",
|
|
298
|
+
PageUp: "pageUp"
|
|
299
|
+
}, homeEnd ? { Home: "first", End: "last" } : {});
|
|
300
|
+
const target = keys[e.key];
|
|
301
|
+
if (target && !e.altKey) {
|
|
302
|
+
e.preventDefault();
|
|
303
|
+
move(target);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
if (allowTypeahead && isPrintable(e)) {
|
|
307
|
+
e.preventDefault();
|
|
308
|
+
typeahead(e.key);
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
(0, import_react4.useEffect)(() => {
|
|
314
|
+
if (!open || activeKey === null) return;
|
|
315
|
+
const option = document.getElementById(optionId(activeKey));
|
|
316
|
+
const list = option == null ? void 0 : option.closest('[role="listbox"]');
|
|
317
|
+
if (!option || !list) return;
|
|
318
|
+
const top = option.offsetTop;
|
|
319
|
+
const bottom = top + option.offsetHeight;
|
|
320
|
+
if (top < list.scrollTop) list.scrollTop = top;
|
|
321
|
+
else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight;
|
|
322
|
+
}, [open, activeKey, id]);
|
|
323
|
+
return {
|
|
324
|
+
activeKey,
|
|
325
|
+
activeItem,
|
|
326
|
+
setActiveKey,
|
|
327
|
+
move,
|
|
328
|
+
typeahead,
|
|
329
|
+
navigate,
|
|
330
|
+
optionId,
|
|
331
|
+
/** Value for `aria-activedescendant` on the element that has focus. */
|
|
332
|
+
activeDescendant: activeKey === null ? void 0 : optionId(activeKey)
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function isPrintable(e) {
|
|
336
|
+
return e.key.length === 1 && e.key !== " " && !e.ctrlKey && !e.metaKey && !e.altKey;
|
|
337
|
+
}
|
|
338
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
339
|
+
0 && (module.exports = {
|
|
340
|
+
isPrintable,
|
|
341
|
+
useControllableState,
|
|
342
|
+
useFloatingPanel,
|
|
343
|
+
useListbox,
|
|
344
|
+
useRovingFocus
|
|
345
|
+
});
|
|
346
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/use-controllable-state.ts","../src/use-floating-panel.ts","../src/use-roving-focus.ts","../src/use-listbox.ts"],"sourcesContent":["export { useControllableState } from \"./use-controllable-state\"\nexport { useFloatingPanel } from \"./use-floating-panel\"\nexport type { UseFloatingPanelOptions, Placement } from \"./use-floating-panel\"\nexport { useRovingFocus } from \"./use-roving-focus\"\nexport type { UseRovingFocusOptions } from \"./use-roving-focus\"\nexport { useListbox, isPrintable } from \"./use-listbox\"\nexport type { UseListboxOptions } from \"./use-listbox\"\n","import { useCallback, useState } from \"react\"\n\n/**\n * State that is controlled when `prop` is defined and internal otherwise.\n * `onChange` fires only when the value actually changes.\n */\nexport function useControllableState<T>(\n prop: T | undefined,\n defaultProp: T,\n onChange?: (value: T) => void\n): readonly [T, (next: T) => void] {\n const [internal, setInternal] = useState(defaultProp)\n const isControlled = prop !== undefined\n const value = isControlled ? prop : internal\n const setValue = useCallback(\n (next: T) => {\n if (!isControlled) setInternal(next)\n if (next !== value) onChange?.(next)\n },\n [isControlled, value, onChange]\n )\n return [value, setValue] as const\n}\n","import { useEffect, useRef, useState } from \"react\"\nimport {\n arrow as arrowMiddleware,\n autoUpdate,\n flip,\n offset as offsetMiddleware,\n shift,\n size,\n useFloating,\n type Placement,\n} from \"@floating-ui/react-dom\"\n\nfunction isPopoverOpen(el: HTMLElement) {\n try {\n return el.matches(\":popover-open\")\n } catch {\n return false\n }\n}\n\nexport interface UseFloatingPanelOptions {\n open: boolean\n onOpenChange: (open: boolean) => void\n placement?: Placement\n /** Gap between trigger and panel, in px. */\n offset?: number\n /** Arrow element, positioned by the arrow middleware. */\n arrow?: HTMLElement | null\n /** Constrain the panel to the space left in the viewport (max-height/max-width). */\n fitViewport?: boolean\n /** Runs right after the panel is shown. Use it to move focus. */\n onShow?: (panel: HTMLElement) => void\n /**\n * When the browser closes the panel (light dismiss, Esc), return focus to the trigger\n * if focus was inside the panel or lost. A click elsewhere keeps its own focus.\n */\n restoreFocus?: boolean\n /** Element that gets focus back, when it is not the trigger (e.g. the trigger is a wrapper). */\n restoreFocusTo?: () => HTMLElement | null\n /** Make the panel at least as wide as the trigger (listboxes). */\n matchTriggerWidth?: boolean\n}\n\n/**\n * Positions a `popover` element next to a trigger and keeps it in sync with `open`.\n * The panel element must have a `popover` attribute (\"auto\" or \"manual\").\n */\nexport function useFloatingPanel({\n open,\n onOpenChange,\n placement = \"bottom\",\n offset = 8,\n arrow = null,\n fitViewport = true,\n onShow,\n restoreFocus = true,\n restoreFocusTo,\n matchTriggerWidth = false,\n}: UseFloatingPanelOptions) {\n const [trigger, setTrigger] = useState<HTMLElement | null>(null)\n const [panel, setPanel] = useState<HTMLElement | null>(null)\n\n // Latest callbacks without re-running the effects.\n const onShowRef = useRef(onShow)\n const onOpenChangeRef = useRef(onOpenChange)\n const restoreFocusToRef = useRef(restoreFocusTo)\n useEffect(() => {\n onShowRef.current = onShow\n onOpenChangeRef.current = onOpenChange\n restoreFocusToRef.current = restoreFocusTo\n })\n\n const { floatingStyles, update, isPositioned, placement: finalPlacement, middlewareData } = useFloating({\n open,\n placement,\n // Panels live in the top layer, outside any overflow:hidden ancestor.\n strategy: \"fixed\",\n elements: { reference: trigger, floating: panel },\n middleware: [\n offsetMiddleware(offset),\n flip({ padding: 8 }),\n shift({ padding: 8 }),\n ...(fitViewport\n ? [\n size({\n padding: 8,\n apply({ availableHeight, availableWidth, elements, rects }) {\n Object.assign(elements.floating.style, {\n maxHeight: `${Math.max(0, availableHeight)}px`,\n maxWidth: `${Math.max(0, availableWidth)}px`,\n ...(matchTriggerWidth ? { minWidth: `${rects.reference.width}px` } : {}),\n })\n },\n }),\n ]\n : []),\n ...(arrow ? [arrowMiddleware({ element: arrow, padding: 6 })] : []),\n ],\n })\n\n // Show/hide the native popover; track the trigger position only while open.\n useEffect(() => {\n if (!panel || typeof panel.showPopover !== \"function\") return\n if (!open) {\n if (isPopoverOpen(panel)) panel.hidePopover()\n return\n }\n if (!isPopoverOpen(panel)) panel.showPopover()\n onShowRef.current?.(panel)\n return trigger ? autoUpdate(trigger, panel, update) : undefined\n }, [open, panel, trigger, update])\n\n // Mirror browser-initiated closes (light dismiss, Esc) into state.\n useEffect(() => {\n if (!panel) return\n const onToggle = (e: Event) => {\n if ((e as ToggleEvent).newState !== \"closed\") return\n onOpenChangeRef.current(false)\n if (!restoreFocus) return\n const active = document.activeElement\n if (!active || active === document.body || panel.contains(active)) {\n ;(restoreFocusToRef.current?.() ?? trigger)?.focus()\n }\n }\n panel.addEventListener(\"toggle\", onToggle)\n return () => panel.removeEventListener(\"toggle\", onToggle)\n }, [panel, trigger, restoreFocus])\n\n // Light dismiss closes the panel on pointerup, before the trigger's click runs. Remember the\n // state at pointerdown so clicking the trigger closes the panel instead of reopening it.\n const openAtPointerDown = useRef<boolean | null>(null)\n const triggerHandlers = {\n onPointerDown: () => {\n openAtPointerDown.current = open\n },\n onClick: () => {\n const wasOpen = openAtPointerDown.current ?? open\n openAtPointerDown.current = null\n onOpenChange(!wasOpen)\n },\n }\n\n return {\n trigger,\n setTrigger,\n panel,\n setPanel,\n // opacity (not visibility) until positioned: the panel must be focusable right after showPopover().\n floatingStyles: (isPositioned ? floatingStyles : { ...floatingStyles, opacity: 0 }) as React.CSSProperties,\n isPositioned,\n placement: finalPlacement,\n middlewareData,\n triggerHandlers,\n }\n}\n\nexport type { Placement }\n","import { useRef } from \"react\"\n\nexport interface UseRovingFocusOptions {\n /** Which arrows move focus. \"both\" accepts all four. */\n orientation?: \"horizontal\" | \"vertical\" | \"both\"\n /** Wrap from last to first and back. */\n loop?: boolean\n /** Typing printable characters jumps to the next item whose text starts with them. */\n typeahead?: boolean\n /** Items to move between. Disabled ones are excluded by the default selector. */\n itemSelector?: string\n /** Runs after an item receives focus (e.g. to activate a tab). */\n onNavigate?: (item: HTMLElement) => void\n}\n\nconst DEFAULT_SELECTOR = '[data-roving-item]:not([disabled]):not([aria-disabled=\"true\"])'\nconst TYPEAHEAD_RESET_MS = 500\n\n/**\n * Keyboard focus movement inside a composite widget (tablist, menu, accordion headers).\n * Spread `rootProps` on the container. Only items whose closest `[data-roving-root]` is this\n * container are used, so nested widgets do not mix.\n */\nexport function useRovingFocus({\n orientation = \"vertical\",\n loop = true,\n typeahead = false,\n itemSelector = DEFAULT_SELECTOR,\n onNavigate,\n}: UseRovingFocusOptions = {}) {\n const buffer = useRef(\"\")\n const resetTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n\n function getItems(root: HTMLElement) {\n return Array.from(root.querySelectorAll<HTMLElement>(itemSelector)).filter(\n (item) => item.closest(\"[data-roving-root]\") === root\n )\n }\n\n function focusItem(item: HTMLElement | undefined) {\n if (!item) return\n item.focus()\n onNavigate?.(item)\n }\n\n function onKeyDown(e: React.KeyboardEvent<HTMLElement>) {\n if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return\n const root = e.currentTarget\n const items = getItems(root)\n if (items.length === 0) return\n const index = items.indexOf(document.activeElement as HTMLElement)\n\n const prevKeys = orientation === \"horizontal\" ? [\"ArrowLeft\"] : orientation === \"vertical\" ? [\"ArrowUp\"] : [\"ArrowLeft\", \"ArrowUp\"]\n const nextKeys = orientation === \"horizontal\" ? [\"ArrowRight\"] : orientation === \"vertical\" ? [\"ArrowDown\"] : [\"ArrowRight\", \"ArrowDown\"]\n\n let target: HTMLElement | undefined\n if (e.key === \"Home\") target = items[0]\n else if (e.key === \"End\") target = items[items.length - 1]\n else if (nextKeys.includes(e.key)) {\n if (index === -1) target = items[0]\n else if (index < items.length - 1) target = items[index + 1]\n else if (loop) target = items[0]\n } else if (prevKeys.includes(e.key)) {\n if (index === -1) target = items[items.length - 1]\n else if (index > 0) target = items[index - 1]\n else if (loop) target = items[items.length - 1]\n } else if (typeahead && e.key.length === 1 && e.key !== \" \") {\n target = findByTypeahead(items, index, e.key)\n if (!target) return\n } else {\n return\n }\n\n e.preventDefault()\n focusItem(target)\n }\n\n function findByTypeahead(items: HTMLElement[], index: number, key: string) {\n clearTimeout(resetTimer.current)\n resetTimer.current = setTimeout(() => (buffer.current = \"\"), TYPEAHEAD_RESET_MS)\n buffer.current += key.toLowerCase()\n\n const search = buffer.current\n // Repeating one letter cycles through items starting with it.\n const isRepeat = search.length > 1 && search.split(\"\").every((c) => c === search[0])\n const query = isRepeat ? search[0] : search\n const start = isRepeat || search.length === 1 ? index + 1 : Math.max(index, 0)\n const text = (item: HTMLElement) => (item.dataset.textValue ?? item.textContent ?? \"\").trim().toLowerCase()\n\n for (let i = 0; i < items.length; i++) {\n const item = items[(start + i) % items.length]\n if (text(item).startsWith(query)) return item\n }\n return undefined\n }\n\n return {\n rootProps: { \"data-roving-root\": \"\", onKeyDown },\n onKeyDown,\n }\n}\n","import { useEffect, useRef, useState } from \"react\"\n\nexport interface UseListboxOptions<T> {\n /** Base id; option ids are derived from it. */\n id: string\n /** Options currently shown (already filtered), in display order. */\n items: T[]\n getKey: (item: T) => string\n /** Text matched by typeahead. */\n getText: (item: T) => string\n isDisabled?: (item: T) => boolean\n open: boolean\n}\n\nconst TYPEAHEAD_RESET_MS = 500\nconst PAGE_SIZE = 10\n\n/**\n * Active-option state for a listbox driven by `aria-activedescendant` (focus stays on the\n * combobox or search input). Handles arrows, Home/End, PageUp/PageDown and typeahead, skips\n * disabled options, and keeps the active option scrolled into view.\n */\nexport function useListbox<T>({ id, items, getKey, getText, isDisabled, open }: UseListboxOptions<T>) {\n const [activeKeyState, setActiveKey] = useState<string | null>(null)\n const buffer = useRef(\"\")\n const resetTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n\n const enabled = items.filter((item) => !isDisabled?.(item))\n // If the stored key is filtered out (or unset), the first enabled option is active.\n const activeItem = open ? (enabled.find((item) => getKey(item) === activeKeyState) ?? enabled[0]) : undefined\n const activeKey = activeItem === undefined ? null : getKey(activeItem)\n const optionId = (key: string) => `${id}-option-${encodeURIComponent(key)}`\n\n function move(to: \"next\" | \"prev\" | \"first\" | \"last\" | \"pageDown\" | \"pageUp\") {\n if (enabled.length === 0) return\n const index = activeItem === undefined ? -1 : enabled.indexOf(activeItem)\n const last = enabled.length - 1\n const next = {\n next: index < 0 ? 0 : Math.min(index + 1, last),\n prev: index < 0 ? last : Math.max(index - 1, 0),\n first: 0,\n last,\n pageDown: Math.min(Math.max(index, 0) + PAGE_SIZE, last),\n pageUp: Math.max(index - PAGE_SIZE, 0),\n }[to]\n setActiveKey(getKey(enabled[next]))\n }\n\n /** Jumps to the next option starting with the typed text. Repeating one letter cycles. */\n function typeahead(char: string) {\n clearTimeout(resetTimer.current)\n resetTimer.current = setTimeout(() => (buffer.current = \"\"), TYPEAHEAD_RESET_MS)\n buffer.current += char.toLowerCase()\n\n const search = buffer.current\n const isRepeat = search.length > 1 && search.split(\"\").every((c) => c === search[0])\n const query = isRepeat ? search[0] : search\n const index = activeItem === undefined ? -1 : enabled.indexOf(activeItem)\n const start = isRepeat || search.length === 1 ? index + 1 : Math.max(index, 0)\n for (let i = 0; i < enabled.length; i++) {\n const item = enabled[(start + i) % enabled.length]\n if (getText(item).toLowerCase().startsWith(query)) {\n setActiveKey(getKey(item))\n return\n }\n }\n }\n\n /**\n * Handles navigation keys. Returns true when the key was used (and default prevented).\n * Pass `homeEnd: false` in text inputs, where Home/End move the caret.\n */\n function navigate(e: React.KeyboardEvent, { homeEnd = true, typeahead: allowTypeahead = false } = {}) {\n const keys: Record<string, Parameters<typeof move>[0]> = {\n ArrowDown: \"next\",\n ArrowUp: \"prev\",\n PageDown: \"pageDown\",\n PageUp: \"pageUp\",\n ...(homeEnd ? { Home: \"first\", End: \"last\" } : {}),\n }\n const target = keys[e.key]\n if (target && !e.altKey) {\n e.preventDefault()\n move(target)\n return true\n }\n if (allowTypeahead && isPrintable(e)) {\n e.preventDefault()\n typeahead(e.key)\n return true\n }\n return false\n }\n\n // Scroll the list itself (not the page) so the active option is visible.\n useEffect(() => {\n if (!open || activeKey === null) return\n const option = document.getElementById(optionId(activeKey))\n const list = option?.closest<HTMLElement>('[role=\"listbox\"]')\n if (!option || !list) return\n const top = option.offsetTop\n const bottom = top + option.offsetHeight\n if (top < list.scrollTop) list.scrollTop = top\n else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight\n // optionId only depends on id.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, activeKey, id])\n\n return {\n activeKey,\n activeItem,\n setActiveKey,\n move,\n typeahead,\n navigate,\n optionId,\n /** Value for `aria-activedescendant` on the element that has focus. */\n activeDescendant: activeKey === null ? undefined : optionId(activeKey),\n }\n}\n\n/** A single printable character without Ctrl/Meta/Alt (Space excluded). */\nexport function isPrintable(e: React.KeyboardEvent) {\n return e.key.length === 1 && e.key !== \" \" && !e.ctrlKey && !e.metaKey && !e.altKey\n}\n"],"mappings":"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","names":["import_react","offsetMiddleware","arrowMiddleware","import_react","import_react","TYPEAHEAD_RESET_MS"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __defProps = Object.defineProperties;
|
|
4
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
5
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __spreadValues = (a, b) => {
|
|
10
|
+
for (var prop in b || (b = {}))
|
|
11
|
+
if (__hasOwnProp.call(b, prop))
|
|
12
|
+
__defNormalProp(a, prop, b[prop]);
|
|
13
|
+
if (__getOwnPropSymbols)
|
|
14
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
15
|
+
if (__propIsEnum.call(b, prop))
|
|
16
|
+
__defNormalProp(a, prop, b[prop]);
|
|
17
|
+
}
|
|
18
|
+
return a;
|
|
19
|
+
};
|
|
20
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
21
|
+
|
|
22
|
+
// src/use-controllable-state.ts
|
|
23
|
+
import { useCallback, useState } from "react";
|
|
24
|
+
function useControllableState(prop, defaultProp, onChange) {
|
|
25
|
+
const [internal, setInternal] = useState(defaultProp);
|
|
26
|
+
const isControlled = prop !== void 0;
|
|
27
|
+
const value = isControlled ? prop : internal;
|
|
28
|
+
const setValue = useCallback(
|
|
29
|
+
(next) => {
|
|
30
|
+
if (!isControlled) setInternal(next);
|
|
31
|
+
if (next !== value) onChange == null ? void 0 : onChange(next);
|
|
32
|
+
},
|
|
33
|
+
[isControlled, value, onChange]
|
|
34
|
+
);
|
|
35
|
+
return [value, setValue];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/use-floating-panel.ts
|
|
39
|
+
import { useEffect, useRef, useState as useState2 } from "react";
|
|
40
|
+
import {
|
|
41
|
+
arrow as arrowMiddleware,
|
|
42
|
+
autoUpdate,
|
|
43
|
+
flip,
|
|
44
|
+
offset as offsetMiddleware,
|
|
45
|
+
shift,
|
|
46
|
+
size,
|
|
47
|
+
useFloating
|
|
48
|
+
} from "@floating-ui/react-dom";
|
|
49
|
+
function isPopoverOpen(el) {
|
|
50
|
+
try {
|
|
51
|
+
return el.matches(":popover-open");
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function useFloatingPanel({
|
|
57
|
+
open,
|
|
58
|
+
onOpenChange,
|
|
59
|
+
placement = "bottom",
|
|
60
|
+
offset = 8,
|
|
61
|
+
arrow = null,
|
|
62
|
+
fitViewport = true,
|
|
63
|
+
onShow,
|
|
64
|
+
restoreFocus = true,
|
|
65
|
+
restoreFocusTo,
|
|
66
|
+
matchTriggerWidth = false
|
|
67
|
+
}) {
|
|
68
|
+
const [trigger, setTrigger] = useState2(null);
|
|
69
|
+
const [panel, setPanel] = useState2(null);
|
|
70
|
+
const onShowRef = useRef(onShow);
|
|
71
|
+
const onOpenChangeRef = useRef(onOpenChange);
|
|
72
|
+
const restoreFocusToRef = useRef(restoreFocusTo);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
onShowRef.current = onShow;
|
|
75
|
+
onOpenChangeRef.current = onOpenChange;
|
|
76
|
+
restoreFocusToRef.current = restoreFocusTo;
|
|
77
|
+
});
|
|
78
|
+
const { floatingStyles, update, isPositioned, placement: finalPlacement, middlewareData } = useFloating({
|
|
79
|
+
open,
|
|
80
|
+
placement,
|
|
81
|
+
// Panels live in the top layer, outside any overflow:hidden ancestor.
|
|
82
|
+
strategy: "fixed",
|
|
83
|
+
elements: { reference: trigger, floating: panel },
|
|
84
|
+
middleware: [
|
|
85
|
+
offsetMiddleware(offset),
|
|
86
|
+
flip({ padding: 8 }),
|
|
87
|
+
shift({ padding: 8 }),
|
|
88
|
+
...fitViewport ? [
|
|
89
|
+
size({
|
|
90
|
+
padding: 8,
|
|
91
|
+
apply({ availableHeight, availableWidth, elements, rects }) {
|
|
92
|
+
Object.assign(elements.floating.style, __spreadValues({
|
|
93
|
+
maxHeight: `${Math.max(0, availableHeight)}px`,
|
|
94
|
+
maxWidth: `${Math.max(0, availableWidth)}px`
|
|
95
|
+
}, matchTriggerWidth ? { minWidth: `${rects.reference.width}px` } : {}));
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
] : [],
|
|
99
|
+
...arrow ? [arrowMiddleware({ element: arrow, padding: 6 })] : []
|
|
100
|
+
]
|
|
101
|
+
});
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
var _a;
|
|
104
|
+
if (!panel || typeof panel.showPopover !== "function") return;
|
|
105
|
+
if (!open) {
|
|
106
|
+
if (isPopoverOpen(panel)) panel.hidePopover();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!isPopoverOpen(panel)) panel.showPopover();
|
|
110
|
+
(_a = onShowRef.current) == null ? void 0 : _a.call(onShowRef, panel);
|
|
111
|
+
return trigger ? autoUpdate(trigger, panel, update) : void 0;
|
|
112
|
+
}, [open, panel, trigger, update]);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!panel) return;
|
|
115
|
+
const onToggle = (e) => {
|
|
116
|
+
var _a, _b, _c;
|
|
117
|
+
if (e.newState !== "closed") return;
|
|
118
|
+
onOpenChangeRef.current(false);
|
|
119
|
+
if (!restoreFocus) return;
|
|
120
|
+
const active = document.activeElement;
|
|
121
|
+
if (!active || active === document.body || panel.contains(active)) {
|
|
122
|
+
;
|
|
123
|
+
(_c = (_b = (_a = restoreFocusToRef.current) == null ? void 0 : _a.call(restoreFocusToRef)) != null ? _b : trigger) == null ? void 0 : _c.focus();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
panel.addEventListener("toggle", onToggle);
|
|
127
|
+
return () => panel.removeEventListener("toggle", onToggle);
|
|
128
|
+
}, [panel, trigger, restoreFocus]);
|
|
129
|
+
const openAtPointerDown = useRef(null);
|
|
130
|
+
const triggerHandlers = {
|
|
131
|
+
onPointerDown: () => {
|
|
132
|
+
openAtPointerDown.current = open;
|
|
133
|
+
},
|
|
134
|
+
onClick: () => {
|
|
135
|
+
var _a;
|
|
136
|
+
const wasOpen = (_a = openAtPointerDown.current) != null ? _a : open;
|
|
137
|
+
openAtPointerDown.current = null;
|
|
138
|
+
onOpenChange(!wasOpen);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
trigger,
|
|
143
|
+
setTrigger,
|
|
144
|
+
panel,
|
|
145
|
+
setPanel,
|
|
146
|
+
// opacity (not visibility) until positioned: the panel must be focusable right after showPopover().
|
|
147
|
+
floatingStyles: isPositioned ? floatingStyles : __spreadProps(__spreadValues({}, floatingStyles), { opacity: 0 }),
|
|
148
|
+
isPositioned,
|
|
149
|
+
placement: finalPlacement,
|
|
150
|
+
middlewareData,
|
|
151
|
+
triggerHandlers
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/use-roving-focus.ts
|
|
156
|
+
import { useRef as useRef2 } from "react";
|
|
157
|
+
var DEFAULT_SELECTOR = '[data-roving-item]:not([disabled]):not([aria-disabled="true"])';
|
|
158
|
+
var TYPEAHEAD_RESET_MS = 500;
|
|
159
|
+
function useRovingFocus({
|
|
160
|
+
orientation = "vertical",
|
|
161
|
+
loop = true,
|
|
162
|
+
typeahead = false,
|
|
163
|
+
itemSelector = DEFAULT_SELECTOR,
|
|
164
|
+
onNavigate
|
|
165
|
+
} = {}) {
|
|
166
|
+
const buffer = useRef2("");
|
|
167
|
+
const resetTimer = useRef2(void 0);
|
|
168
|
+
function getItems(root) {
|
|
169
|
+
return Array.from(root.querySelectorAll(itemSelector)).filter(
|
|
170
|
+
(item) => item.closest("[data-roving-root]") === root
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
function focusItem(item) {
|
|
174
|
+
if (!item) return;
|
|
175
|
+
item.focus();
|
|
176
|
+
onNavigate == null ? void 0 : onNavigate(item);
|
|
177
|
+
}
|
|
178
|
+
function onKeyDown(e) {
|
|
179
|
+
if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
180
|
+
const root = e.currentTarget;
|
|
181
|
+
const items = getItems(root);
|
|
182
|
+
if (items.length === 0) return;
|
|
183
|
+
const index = items.indexOf(document.activeElement);
|
|
184
|
+
const prevKeys = orientation === "horizontal" ? ["ArrowLeft"] : orientation === "vertical" ? ["ArrowUp"] : ["ArrowLeft", "ArrowUp"];
|
|
185
|
+
const nextKeys = orientation === "horizontal" ? ["ArrowRight"] : orientation === "vertical" ? ["ArrowDown"] : ["ArrowRight", "ArrowDown"];
|
|
186
|
+
let target;
|
|
187
|
+
if (e.key === "Home") target = items[0];
|
|
188
|
+
else if (e.key === "End") target = items[items.length - 1];
|
|
189
|
+
else if (nextKeys.includes(e.key)) {
|
|
190
|
+
if (index === -1) target = items[0];
|
|
191
|
+
else if (index < items.length - 1) target = items[index + 1];
|
|
192
|
+
else if (loop) target = items[0];
|
|
193
|
+
} else if (prevKeys.includes(e.key)) {
|
|
194
|
+
if (index === -1) target = items[items.length - 1];
|
|
195
|
+
else if (index > 0) target = items[index - 1];
|
|
196
|
+
else if (loop) target = items[items.length - 1];
|
|
197
|
+
} else if (typeahead && e.key.length === 1 && e.key !== " ") {
|
|
198
|
+
target = findByTypeahead(items, index, e.key);
|
|
199
|
+
if (!target) return;
|
|
200
|
+
} else {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
focusItem(target);
|
|
205
|
+
}
|
|
206
|
+
function findByTypeahead(items, index, key) {
|
|
207
|
+
clearTimeout(resetTimer.current);
|
|
208
|
+
resetTimer.current = setTimeout(() => buffer.current = "", TYPEAHEAD_RESET_MS);
|
|
209
|
+
buffer.current += key.toLowerCase();
|
|
210
|
+
const search = buffer.current;
|
|
211
|
+
const isRepeat = search.length > 1 && search.split("").every((c) => c === search[0]);
|
|
212
|
+
const query = isRepeat ? search[0] : search;
|
|
213
|
+
const start = isRepeat || search.length === 1 ? index + 1 : Math.max(index, 0);
|
|
214
|
+
const text = (item) => {
|
|
215
|
+
var _a, _b;
|
|
216
|
+
return ((_b = (_a = item.dataset.textValue) != null ? _a : item.textContent) != null ? _b : "").trim().toLowerCase();
|
|
217
|
+
};
|
|
218
|
+
for (let i = 0; i < items.length; i++) {
|
|
219
|
+
const item = items[(start + i) % items.length];
|
|
220
|
+
if (text(item).startsWith(query)) return item;
|
|
221
|
+
}
|
|
222
|
+
return void 0;
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
rootProps: { "data-roving-root": "", onKeyDown },
|
|
226
|
+
onKeyDown
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/use-listbox.ts
|
|
231
|
+
import { useEffect as useEffect2, useRef as useRef3, useState as useState3 } from "react";
|
|
232
|
+
var TYPEAHEAD_RESET_MS2 = 500;
|
|
233
|
+
var PAGE_SIZE = 10;
|
|
234
|
+
function useListbox({ id, items, getKey, getText, isDisabled, open }) {
|
|
235
|
+
var _a;
|
|
236
|
+
const [activeKeyState, setActiveKey] = useState3(null);
|
|
237
|
+
const buffer = useRef3("");
|
|
238
|
+
const resetTimer = useRef3(void 0);
|
|
239
|
+
const enabled = items.filter((item) => !(isDisabled == null ? void 0 : isDisabled(item)));
|
|
240
|
+
const activeItem = open ? (_a = enabled.find((item) => getKey(item) === activeKeyState)) != null ? _a : enabled[0] : void 0;
|
|
241
|
+
const activeKey = activeItem === void 0 ? null : getKey(activeItem);
|
|
242
|
+
const optionId = (key) => `${id}-option-${encodeURIComponent(key)}`;
|
|
243
|
+
function move(to) {
|
|
244
|
+
if (enabled.length === 0) return;
|
|
245
|
+
const index = activeItem === void 0 ? -1 : enabled.indexOf(activeItem);
|
|
246
|
+
const last = enabled.length - 1;
|
|
247
|
+
const next = {
|
|
248
|
+
next: index < 0 ? 0 : Math.min(index + 1, last),
|
|
249
|
+
prev: index < 0 ? last : Math.max(index - 1, 0),
|
|
250
|
+
first: 0,
|
|
251
|
+
last,
|
|
252
|
+
pageDown: Math.min(Math.max(index, 0) + PAGE_SIZE, last),
|
|
253
|
+
pageUp: Math.max(index - PAGE_SIZE, 0)
|
|
254
|
+
}[to];
|
|
255
|
+
setActiveKey(getKey(enabled[next]));
|
|
256
|
+
}
|
|
257
|
+
function typeahead(char) {
|
|
258
|
+
clearTimeout(resetTimer.current);
|
|
259
|
+
resetTimer.current = setTimeout(() => buffer.current = "", TYPEAHEAD_RESET_MS2);
|
|
260
|
+
buffer.current += char.toLowerCase();
|
|
261
|
+
const search = buffer.current;
|
|
262
|
+
const isRepeat = search.length > 1 && search.split("").every((c) => c === search[0]);
|
|
263
|
+
const query = isRepeat ? search[0] : search;
|
|
264
|
+
const index = activeItem === void 0 ? -1 : enabled.indexOf(activeItem);
|
|
265
|
+
const start = isRepeat || search.length === 1 ? index + 1 : Math.max(index, 0);
|
|
266
|
+
for (let i = 0; i < enabled.length; i++) {
|
|
267
|
+
const item = enabled[(start + i) % enabled.length];
|
|
268
|
+
if (getText(item).toLowerCase().startsWith(query)) {
|
|
269
|
+
setActiveKey(getKey(item));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function navigate(e, { homeEnd = true, typeahead: allowTypeahead = false } = {}) {
|
|
275
|
+
const keys = __spreadValues({
|
|
276
|
+
ArrowDown: "next",
|
|
277
|
+
ArrowUp: "prev",
|
|
278
|
+
PageDown: "pageDown",
|
|
279
|
+
PageUp: "pageUp"
|
|
280
|
+
}, homeEnd ? { Home: "first", End: "last" } : {});
|
|
281
|
+
const target = keys[e.key];
|
|
282
|
+
if (target && !e.altKey) {
|
|
283
|
+
e.preventDefault();
|
|
284
|
+
move(target);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
if (allowTypeahead && isPrintable(e)) {
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
typeahead(e.key);
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
useEffect2(() => {
|
|
295
|
+
if (!open || activeKey === null) return;
|
|
296
|
+
const option = document.getElementById(optionId(activeKey));
|
|
297
|
+
const list = option == null ? void 0 : option.closest('[role="listbox"]');
|
|
298
|
+
if (!option || !list) return;
|
|
299
|
+
const top = option.offsetTop;
|
|
300
|
+
const bottom = top + option.offsetHeight;
|
|
301
|
+
if (top < list.scrollTop) list.scrollTop = top;
|
|
302
|
+
else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight;
|
|
303
|
+
}, [open, activeKey, id]);
|
|
304
|
+
return {
|
|
305
|
+
activeKey,
|
|
306
|
+
activeItem,
|
|
307
|
+
setActiveKey,
|
|
308
|
+
move,
|
|
309
|
+
typeahead,
|
|
310
|
+
navigate,
|
|
311
|
+
optionId,
|
|
312
|
+
/** Value for `aria-activedescendant` on the element that has focus. */
|
|
313
|
+
activeDescendant: activeKey === null ? void 0 : optionId(activeKey)
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function isPrintable(e) {
|
|
317
|
+
return e.key.length === 1 && e.key !== " " && !e.ctrlKey && !e.metaKey && !e.altKey;
|
|
318
|
+
}
|
|
319
|
+
export {
|
|
320
|
+
isPrintable,
|
|
321
|
+
useControllableState,
|
|
322
|
+
useFloatingPanel,
|
|
323
|
+
useListbox,
|
|
324
|
+
useRovingFocus
|
|
325
|
+
};
|
|
326
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/use-controllable-state.ts","../src/use-floating-panel.ts","../src/use-roving-focus.ts","../src/use-listbox.ts"],"sourcesContent":["import { useCallback, useState } from \"react\"\n\n/**\n * State that is controlled when `prop` is defined and internal otherwise.\n * `onChange` fires only when the value actually changes.\n */\nexport function useControllableState<T>(\n prop: T | undefined,\n defaultProp: T,\n onChange?: (value: T) => void\n): readonly [T, (next: T) => void] {\n const [internal, setInternal] = useState(defaultProp)\n const isControlled = prop !== undefined\n const value = isControlled ? prop : internal\n const setValue = useCallback(\n (next: T) => {\n if (!isControlled) setInternal(next)\n if (next !== value) onChange?.(next)\n },\n [isControlled, value, onChange]\n )\n return [value, setValue] as const\n}\n","import { useEffect, useRef, useState } from \"react\"\nimport {\n arrow as arrowMiddleware,\n autoUpdate,\n flip,\n offset as offsetMiddleware,\n shift,\n size,\n useFloating,\n type Placement,\n} from \"@floating-ui/react-dom\"\n\nfunction isPopoverOpen(el: HTMLElement) {\n try {\n return el.matches(\":popover-open\")\n } catch {\n return false\n }\n}\n\nexport interface UseFloatingPanelOptions {\n open: boolean\n onOpenChange: (open: boolean) => void\n placement?: Placement\n /** Gap between trigger and panel, in px. */\n offset?: number\n /** Arrow element, positioned by the arrow middleware. */\n arrow?: HTMLElement | null\n /** Constrain the panel to the space left in the viewport (max-height/max-width). */\n fitViewport?: boolean\n /** Runs right after the panel is shown. Use it to move focus. */\n onShow?: (panel: HTMLElement) => void\n /**\n * When the browser closes the panel (light dismiss, Esc), return focus to the trigger\n * if focus was inside the panel or lost. A click elsewhere keeps its own focus.\n */\n restoreFocus?: boolean\n /** Element that gets focus back, when it is not the trigger (e.g. the trigger is a wrapper). */\n restoreFocusTo?: () => HTMLElement | null\n /** Make the panel at least as wide as the trigger (listboxes). */\n matchTriggerWidth?: boolean\n}\n\n/**\n * Positions a `popover` element next to a trigger and keeps it in sync with `open`.\n * The panel element must have a `popover` attribute (\"auto\" or \"manual\").\n */\nexport function useFloatingPanel({\n open,\n onOpenChange,\n placement = \"bottom\",\n offset = 8,\n arrow = null,\n fitViewport = true,\n onShow,\n restoreFocus = true,\n restoreFocusTo,\n matchTriggerWidth = false,\n}: UseFloatingPanelOptions) {\n const [trigger, setTrigger] = useState<HTMLElement | null>(null)\n const [panel, setPanel] = useState<HTMLElement | null>(null)\n\n // Latest callbacks without re-running the effects.\n const onShowRef = useRef(onShow)\n const onOpenChangeRef = useRef(onOpenChange)\n const restoreFocusToRef = useRef(restoreFocusTo)\n useEffect(() => {\n onShowRef.current = onShow\n onOpenChangeRef.current = onOpenChange\n restoreFocusToRef.current = restoreFocusTo\n })\n\n const { floatingStyles, update, isPositioned, placement: finalPlacement, middlewareData } = useFloating({\n open,\n placement,\n // Panels live in the top layer, outside any overflow:hidden ancestor.\n strategy: \"fixed\",\n elements: { reference: trigger, floating: panel },\n middleware: [\n offsetMiddleware(offset),\n flip({ padding: 8 }),\n shift({ padding: 8 }),\n ...(fitViewport\n ? [\n size({\n padding: 8,\n apply({ availableHeight, availableWidth, elements, rects }) {\n Object.assign(elements.floating.style, {\n maxHeight: `${Math.max(0, availableHeight)}px`,\n maxWidth: `${Math.max(0, availableWidth)}px`,\n ...(matchTriggerWidth ? { minWidth: `${rects.reference.width}px` } : {}),\n })\n },\n }),\n ]\n : []),\n ...(arrow ? [arrowMiddleware({ element: arrow, padding: 6 })] : []),\n ],\n })\n\n // Show/hide the native popover; track the trigger position only while open.\n useEffect(() => {\n if (!panel || typeof panel.showPopover !== \"function\") return\n if (!open) {\n if (isPopoverOpen(panel)) panel.hidePopover()\n return\n }\n if (!isPopoverOpen(panel)) panel.showPopover()\n onShowRef.current?.(panel)\n return trigger ? autoUpdate(trigger, panel, update) : undefined\n }, [open, panel, trigger, update])\n\n // Mirror browser-initiated closes (light dismiss, Esc) into state.\n useEffect(() => {\n if (!panel) return\n const onToggle = (e: Event) => {\n if ((e as ToggleEvent).newState !== \"closed\") return\n onOpenChangeRef.current(false)\n if (!restoreFocus) return\n const active = document.activeElement\n if (!active || active === document.body || panel.contains(active)) {\n ;(restoreFocusToRef.current?.() ?? trigger)?.focus()\n }\n }\n panel.addEventListener(\"toggle\", onToggle)\n return () => panel.removeEventListener(\"toggle\", onToggle)\n }, [panel, trigger, restoreFocus])\n\n // Light dismiss closes the panel on pointerup, before the trigger's click runs. Remember the\n // state at pointerdown so clicking the trigger closes the panel instead of reopening it.\n const openAtPointerDown = useRef<boolean | null>(null)\n const triggerHandlers = {\n onPointerDown: () => {\n openAtPointerDown.current = open\n },\n onClick: () => {\n const wasOpen = openAtPointerDown.current ?? open\n openAtPointerDown.current = null\n onOpenChange(!wasOpen)\n },\n }\n\n return {\n trigger,\n setTrigger,\n panel,\n setPanel,\n // opacity (not visibility) until positioned: the panel must be focusable right after showPopover().\n floatingStyles: (isPositioned ? floatingStyles : { ...floatingStyles, opacity: 0 }) as React.CSSProperties,\n isPositioned,\n placement: finalPlacement,\n middlewareData,\n triggerHandlers,\n }\n}\n\nexport type { Placement }\n","import { useRef } from \"react\"\n\nexport interface UseRovingFocusOptions {\n /** Which arrows move focus. \"both\" accepts all four. */\n orientation?: \"horizontal\" | \"vertical\" | \"both\"\n /** Wrap from last to first and back. */\n loop?: boolean\n /** Typing printable characters jumps to the next item whose text starts with them. */\n typeahead?: boolean\n /** Items to move between. Disabled ones are excluded by the default selector. */\n itemSelector?: string\n /** Runs after an item receives focus (e.g. to activate a tab). */\n onNavigate?: (item: HTMLElement) => void\n}\n\nconst DEFAULT_SELECTOR = '[data-roving-item]:not([disabled]):not([aria-disabled=\"true\"])'\nconst TYPEAHEAD_RESET_MS = 500\n\n/**\n * Keyboard focus movement inside a composite widget (tablist, menu, accordion headers).\n * Spread `rootProps` on the container. Only items whose closest `[data-roving-root]` is this\n * container are used, so nested widgets do not mix.\n */\nexport function useRovingFocus({\n orientation = \"vertical\",\n loop = true,\n typeahead = false,\n itemSelector = DEFAULT_SELECTOR,\n onNavigate,\n}: UseRovingFocusOptions = {}) {\n const buffer = useRef(\"\")\n const resetTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n\n function getItems(root: HTMLElement) {\n return Array.from(root.querySelectorAll<HTMLElement>(itemSelector)).filter(\n (item) => item.closest(\"[data-roving-root]\") === root\n )\n }\n\n function focusItem(item: HTMLElement | undefined) {\n if (!item) return\n item.focus()\n onNavigate?.(item)\n }\n\n function onKeyDown(e: React.KeyboardEvent<HTMLElement>) {\n if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return\n const root = e.currentTarget\n const items = getItems(root)\n if (items.length === 0) return\n const index = items.indexOf(document.activeElement as HTMLElement)\n\n const prevKeys = orientation === \"horizontal\" ? [\"ArrowLeft\"] : orientation === \"vertical\" ? [\"ArrowUp\"] : [\"ArrowLeft\", \"ArrowUp\"]\n const nextKeys = orientation === \"horizontal\" ? [\"ArrowRight\"] : orientation === \"vertical\" ? [\"ArrowDown\"] : [\"ArrowRight\", \"ArrowDown\"]\n\n let target: HTMLElement | undefined\n if (e.key === \"Home\") target = items[0]\n else if (e.key === \"End\") target = items[items.length - 1]\n else if (nextKeys.includes(e.key)) {\n if (index === -1) target = items[0]\n else if (index < items.length - 1) target = items[index + 1]\n else if (loop) target = items[0]\n } else if (prevKeys.includes(e.key)) {\n if (index === -1) target = items[items.length - 1]\n else if (index > 0) target = items[index - 1]\n else if (loop) target = items[items.length - 1]\n } else if (typeahead && e.key.length === 1 && e.key !== \" \") {\n target = findByTypeahead(items, index, e.key)\n if (!target) return\n } else {\n return\n }\n\n e.preventDefault()\n focusItem(target)\n }\n\n function findByTypeahead(items: HTMLElement[], index: number, key: string) {\n clearTimeout(resetTimer.current)\n resetTimer.current = setTimeout(() => (buffer.current = \"\"), TYPEAHEAD_RESET_MS)\n buffer.current += key.toLowerCase()\n\n const search = buffer.current\n // Repeating one letter cycles through items starting with it.\n const isRepeat = search.length > 1 && search.split(\"\").every((c) => c === search[0])\n const query = isRepeat ? search[0] : search\n const start = isRepeat || search.length === 1 ? index + 1 : Math.max(index, 0)\n const text = (item: HTMLElement) => (item.dataset.textValue ?? item.textContent ?? \"\").trim().toLowerCase()\n\n for (let i = 0; i < items.length; i++) {\n const item = items[(start + i) % items.length]\n if (text(item).startsWith(query)) return item\n }\n return undefined\n }\n\n return {\n rootProps: { \"data-roving-root\": \"\", onKeyDown },\n onKeyDown,\n }\n}\n","import { useEffect, useRef, useState } from \"react\"\n\nexport interface UseListboxOptions<T> {\n /** Base id; option ids are derived from it. */\n id: string\n /** Options currently shown (already filtered), in display order. */\n items: T[]\n getKey: (item: T) => string\n /** Text matched by typeahead. */\n getText: (item: T) => string\n isDisabled?: (item: T) => boolean\n open: boolean\n}\n\nconst TYPEAHEAD_RESET_MS = 500\nconst PAGE_SIZE = 10\n\n/**\n * Active-option state for a listbox driven by `aria-activedescendant` (focus stays on the\n * combobox or search input). Handles arrows, Home/End, PageUp/PageDown and typeahead, skips\n * disabled options, and keeps the active option scrolled into view.\n */\nexport function useListbox<T>({ id, items, getKey, getText, isDisabled, open }: UseListboxOptions<T>) {\n const [activeKeyState, setActiveKey] = useState<string | null>(null)\n const buffer = useRef(\"\")\n const resetTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n\n const enabled = items.filter((item) => !isDisabled?.(item))\n // If the stored key is filtered out (or unset), the first enabled option is active.\n const activeItem = open ? (enabled.find((item) => getKey(item) === activeKeyState) ?? enabled[0]) : undefined\n const activeKey = activeItem === undefined ? null : getKey(activeItem)\n const optionId = (key: string) => `${id}-option-${encodeURIComponent(key)}`\n\n function move(to: \"next\" | \"prev\" | \"first\" | \"last\" | \"pageDown\" | \"pageUp\") {\n if (enabled.length === 0) return\n const index = activeItem === undefined ? -1 : enabled.indexOf(activeItem)\n const last = enabled.length - 1\n const next = {\n next: index < 0 ? 0 : Math.min(index + 1, last),\n prev: index < 0 ? last : Math.max(index - 1, 0),\n first: 0,\n last,\n pageDown: Math.min(Math.max(index, 0) + PAGE_SIZE, last),\n pageUp: Math.max(index - PAGE_SIZE, 0),\n }[to]\n setActiveKey(getKey(enabled[next]))\n }\n\n /** Jumps to the next option starting with the typed text. Repeating one letter cycles. */\n function typeahead(char: string) {\n clearTimeout(resetTimer.current)\n resetTimer.current = setTimeout(() => (buffer.current = \"\"), TYPEAHEAD_RESET_MS)\n buffer.current += char.toLowerCase()\n\n const search = buffer.current\n const isRepeat = search.length > 1 && search.split(\"\").every((c) => c === search[0])\n const query = isRepeat ? search[0] : search\n const index = activeItem === undefined ? -1 : enabled.indexOf(activeItem)\n const start = isRepeat || search.length === 1 ? index + 1 : Math.max(index, 0)\n for (let i = 0; i < enabled.length; i++) {\n const item = enabled[(start + i) % enabled.length]\n if (getText(item).toLowerCase().startsWith(query)) {\n setActiveKey(getKey(item))\n return\n }\n }\n }\n\n /**\n * Handles navigation keys. Returns true when the key was used (and default prevented).\n * Pass `homeEnd: false` in text inputs, where Home/End move the caret.\n */\n function navigate(e: React.KeyboardEvent, { homeEnd = true, typeahead: allowTypeahead = false } = {}) {\n const keys: Record<string, Parameters<typeof move>[0]> = {\n ArrowDown: \"next\",\n ArrowUp: \"prev\",\n PageDown: \"pageDown\",\n PageUp: \"pageUp\",\n ...(homeEnd ? { Home: \"first\", End: \"last\" } : {}),\n }\n const target = keys[e.key]\n if (target && !e.altKey) {\n e.preventDefault()\n move(target)\n return true\n }\n if (allowTypeahead && isPrintable(e)) {\n e.preventDefault()\n typeahead(e.key)\n return true\n }\n return false\n }\n\n // Scroll the list itself (not the page) so the active option is visible.\n useEffect(() => {\n if (!open || activeKey === null) return\n const option = document.getElementById(optionId(activeKey))\n const list = option?.closest<HTMLElement>('[role=\"listbox\"]')\n if (!option || !list) return\n const top = option.offsetTop\n const bottom = top + option.offsetHeight\n if (top < list.scrollTop) list.scrollTop = top\n else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight\n // optionId only depends on id.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, activeKey, id])\n\n return {\n activeKey,\n activeItem,\n setActiveKey,\n move,\n typeahead,\n navigate,\n optionId,\n /** Value for `aria-activedescendant` on the element that has focus. */\n activeDescendant: activeKey === null ? undefined : optionId(activeKey),\n }\n}\n\n/** A single printable character without Ctrl/Meta/Alt (Space excluded). */\nexport function isPrintable(e: React.KeyboardEvent) {\n return e.key.length === 1 && e.key !== \" \" && !e.ctrlKey && !e.metaKey && !e.altKey\n}\n"],"mappings":"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","names":["useState","useState","useRef","useEffect","useRef","useState","TYPEAHEAD_RESET_MS","useState","useRef","useEffect"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glob-ui/hooks",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"module": "./dist/index.mjs",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": {
|
|
10
|
+
"types": "./dist/index.d.mts",
|
|
11
|
+
"default": "./dist/index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"require": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": "^19",
|
|
27
|
+
"react-dom": "^19"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@floating-ui/react-dom": "^2.1.9"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"tsup": "^8.5.1"
|
|
34
|
+
},
|
|
35
|
+
"description": "Internal hooks used by GlobUI components.",
|
|
36
|
+
"keywords": [
|
|
37
|
+
"react",
|
|
38
|
+
"glob-ui",
|
|
39
|
+
"components",
|
|
40
|
+
"accessible",
|
|
41
|
+
"tailwind",
|
|
42
|
+
"hooks"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"author": "Kevin Martínez E",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/kevin-martineze/GlobUI.git",
|
|
49
|
+
"directory": "packages/hooks"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/kevin-martineze/GlobUI#readme",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/kevin-martineze/GlobUI/issues"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public",
|
|
57
|
+
"provenance": true
|
|
58
|
+
},
|
|
59
|
+
"sideEffects": false,
|
|
60
|
+
"type": "commonjs",
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=18.18"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsup",
|
|
66
|
+
"dev": "tsup --watch --no-clean"
|
|
67
|
+
}
|
|
6
68
|
}
|