@mendylanda/ui 0.1.0 → 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 +10 -0
- package/dist/class-names.js +1 -1
- package/dist/customization.d.ts +3 -1
- package/dist/customization.js +3 -2
- package/dist/filters/filter-bar.d.ts +4 -1
- package/dist/filters/filter-bar.js +134 -57
- package/dist/filters/filter-collection.d.ts +28 -0
- package/dist/filters/filter-collection.js +191 -0
- package/dist/filters/filter-date-editor.d.ts +3 -1
- package/dist/filters/filter-date-editor.js +5 -5
- package/dist/filters/filter-definition.d.ts +13 -0
- package/dist/filters/filter-menu-focus.d.ts +11 -0
- package/dist/filters/filter-menu-focus.js +91 -0
- package/dist/filters/filter-menu-panel.d.ts +3 -1
- package/dist/filters/filter-menu-panel.js +153 -108
- package/dist/filters/filters.d.ts +4 -2
- package/dist/filters/filters.js +26 -5
- package/dist/filters/use-filter-options.js +7 -5
- package/dist/filters/use-menu-placement.d.ts +14 -0
- package/dist/filters/use-menu-placement.js +77 -0
- package/dist/filters/use-menu-pointer.d.ts +10 -0
- package/dist/filters/use-menu-pointer.js +64 -0
- package/dist/filters/use-value-draft.d.ts +9 -2
- package/dist/filters/use-value-draft.js +20 -5
- package/dist/primitives/calendar.js +2 -2
- package/dist/primitives/dropdown-menu.js +2 -2
- package/dist/styles.css +259 -91
- package/package.json +3 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode, Ref } from "react";
|
|
2
|
+
export interface CollectionHandle {
|
|
3
|
+
focus(key: string): void;
|
|
4
|
+
focusFirst(): void;
|
|
5
|
+
}
|
|
6
|
+
interface RowProps {
|
|
7
|
+
ref(node: HTMLElement | null): void;
|
|
8
|
+
style?: CSSProperties;
|
|
9
|
+
tabIndex: number;
|
|
10
|
+
"data-index": number;
|
|
11
|
+
"data-collection-key": string;
|
|
12
|
+
}
|
|
13
|
+
interface CollectionItem {
|
|
14
|
+
key: string;
|
|
15
|
+
label: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** A single keyboard model for both ordinary and measured, virtualized collections. */
|
|
19
|
+
export declare function FilterCollection<T extends CollectionItem>({ items, children, className, role, label, collectionRef, initialKey, }: {
|
|
20
|
+
items: T[];
|
|
21
|
+
children(item: T, index: number, props: RowProps): ReactNode;
|
|
22
|
+
className?: string;
|
|
23
|
+
role: "menu" | "group";
|
|
24
|
+
label: string;
|
|
25
|
+
collectionRef?: Ref<CollectionHandle>;
|
|
26
|
+
initialKey?: string;
|
|
27
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
4
|
+
import { defaultRangeExtractor, useVirtualizer } from "@tanstack/react-virtual";
|
|
5
|
+
import { cn } from "../utils.js";
|
|
6
|
+
/** A single keyboard model for both ordinary and measured, virtualized collections. */
|
|
7
|
+
export function FilterCollection({ items, children, className, role, label, collectionRef, initialKey, }) {
|
|
8
|
+
const scroll = useRef(null);
|
|
9
|
+
const nodes = useRef(new Map());
|
|
10
|
+
const pending = useRef(null);
|
|
11
|
+
const ownsFocus = useRef(false);
|
|
12
|
+
const previousIndex = useRef(0);
|
|
13
|
+
const [focused, setFocused] = useState(initialKey ?? items.find((item) => !item.disabled)?.key);
|
|
14
|
+
const typeahead = useRef({ text: "", at: 0 });
|
|
15
|
+
const virtual = items.length > 100;
|
|
16
|
+
const keys = useMemo(() => new Map(items.map((item, index) => [item.key, index])), [items]);
|
|
17
|
+
const focusedIndex = focused === undefined ? undefined : keys.get(focused);
|
|
18
|
+
const rowEstimate = (() => {
|
|
19
|
+
const rem = typeof document === "undefined"
|
|
20
|
+
? 16
|
|
21
|
+
: parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
22
|
+
return (rem *
|
|
23
|
+
(typeof matchMedia !== "undefined" &&
|
|
24
|
+
matchMedia("(pointer: fine) and (min-width: 640px)").matches
|
|
25
|
+
? 2
|
|
26
|
+
: 2.5));
|
|
27
|
+
})();
|
|
28
|
+
const virtualizer = useVirtualizer({
|
|
29
|
+
count: items.length,
|
|
30
|
+
enabled: virtual,
|
|
31
|
+
getScrollElement: () => scroll.current,
|
|
32
|
+
estimateSize: () => rowEstimate,
|
|
33
|
+
getItemKey: useCallback((index) => items[index].key, [items]),
|
|
34
|
+
overscan: 5,
|
|
35
|
+
initialOffset: () => (initialKey === undefined ? 0 : (keys.get(initialKey) ?? 0) * rowEstimate),
|
|
36
|
+
rangeExtractor: useCallback((range) => {
|
|
37
|
+
const visible = defaultRangeExtractor(range);
|
|
38
|
+
// Keep DOM focus alive when a wheel or touch scroll moves it outside the viewport.
|
|
39
|
+
return focusedIndex === undefined
|
|
40
|
+
? visible
|
|
41
|
+
: [...new Set([...visible, focusedIndex])].sort((a, b) => a - b);
|
|
42
|
+
}, [focusedIndex]),
|
|
43
|
+
});
|
|
44
|
+
function focus(key) {
|
|
45
|
+
const index = keys.get(key);
|
|
46
|
+
if (index === undefined || items[index]?.disabled)
|
|
47
|
+
return;
|
|
48
|
+
pending.current = key;
|
|
49
|
+
setFocused(key);
|
|
50
|
+
if (virtual)
|
|
51
|
+
virtualizer.scrollToIndex(index, { align: "auto" });
|
|
52
|
+
const node = nodes.current.get(key);
|
|
53
|
+
if (node) {
|
|
54
|
+
node.focus({ preventScroll: virtual });
|
|
55
|
+
pending.current = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
useImperativeHandle(collectionRef, () => ({
|
|
59
|
+
focus,
|
|
60
|
+
focusFirst() {
|
|
61
|
+
const first = items.find((item) => !item.disabled);
|
|
62
|
+
if (first)
|
|
63
|
+
focus(first.key);
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
useLayoutEffect(() => {
|
|
67
|
+
if (pending.current !== null && !keys.has(pending.current))
|
|
68
|
+
pending.current = null;
|
|
69
|
+
if (focusedIndex === undefined || items[focusedIndex]?.disabled) {
|
|
70
|
+
const replacement = items.slice(previousIndex.current).find((item) => !item.disabled) ??
|
|
71
|
+
items
|
|
72
|
+
.slice(0, previousIndex.current)
|
|
73
|
+
.reverse()
|
|
74
|
+
.find((item) => !item.disabled);
|
|
75
|
+
setFocused(replacement?.key);
|
|
76
|
+
if (!replacement)
|
|
77
|
+
previousIndex.current = 0;
|
|
78
|
+
const active = document.activeElement;
|
|
79
|
+
if (ownsFocus.current &&
|
|
80
|
+
(active === document.body || (active && scroll.current?.contains(active)))) {
|
|
81
|
+
if (replacement)
|
|
82
|
+
focus(replacement.key);
|
|
83
|
+
else
|
|
84
|
+
scroll.current?.focus({ preventScroll: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
previousIndex.current = focusedIndex;
|
|
89
|
+
// Enabling virtualization can briefly have no measured range. Restore a
|
|
90
|
+
// still-valid focused item as soon as its row mounts again.
|
|
91
|
+
if (ownsFocus.current && document.activeElement === document.body)
|
|
92
|
+
focus(items[focusedIndex].key);
|
|
93
|
+
}
|
|
94
|
+
const key = pending.current;
|
|
95
|
+
const node = key === null ? undefined : nodes.current.get(key);
|
|
96
|
+
if (!node)
|
|
97
|
+
return;
|
|
98
|
+
node.focus({ preventScroll: virtual });
|
|
99
|
+
const index = keys.get(key);
|
|
100
|
+
if (virtual && index !== undefined)
|
|
101
|
+
virtualizer.scrollToIndex(index, { align: "auto" });
|
|
102
|
+
pending.current = null;
|
|
103
|
+
});
|
|
104
|
+
function render(index, start) {
|
|
105
|
+
const item = items[index];
|
|
106
|
+
return children(item, index, {
|
|
107
|
+
"data-index": index,
|
|
108
|
+
tabIndex: item.key === focused && !item.disabled ? 0 : -1,
|
|
109
|
+
"data-collection-key": item.key,
|
|
110
|
+
ref(node) {
|
|
111
|
+
if (node) {
|
|
112
|
+
nodes.current.set(item.key, node);
|
|
113
|
+
if (virtual)
|
|
114
|
+
virtualizer.measureElement(node);
|
|
115
|
+
}
|
|
116
|
+
else
|
|
117
|
+
nodes.current.delete(item.key);
|
|
118
|
+
},
|
|
119
|
+
style: virtual
|
|
120
|
+
? {
|
|
121
|
+
position: "absolute",
|
|
122
|
+
top: 0,
|
|
123
|
+
left: 0,
|
|
124
|
+
width: "100%",
|
|
125
|
+
transform: `translateY(${start}px)`,
|
|
126
|
+
}
|
|
127
|
+
: undefined,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return (_jsx("div", { ref: scroll, tabIndex: -1, role: role, "aria-label": label, "data-filter-collection": "", "data-virtual": virtual, className: cn("mui-410da8dfa8ac mui-2368e909f3a5 mui-998780cdef87 mui-1dee6e3ec67d", className), onBlurCapture: (event) => {
|
|
131
|
+
if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget))
|
|
132
|
+
ownsFocus.current = false;
|
|
133
|
+
}, onFocusCapture: (event) => {
|
|
134
|
+
ownsFocus.current = true;
|
|
135
|
+
const key = event.target.closest("[data-collection-key]")
|
|
136
|
+
?.dataset.collectionKey;
|
|
137
|
+
if (key !== undefined && keys.has(key))
|
|
138
|
+
setFocused(key);
|
|
139
|
+
}, onKeyDownCapture: (event) => {
|
|
140
|
+
if (event.nativeEvent.isComposing || event.ctrlKey || event.metaKey || event.altKey)
|
|
141
|
+
return;
|
|
142
|
+
if (event.target.closest("input, textarea, select, [contenteditable=true]"))
|
|
143
|
+
return;
|
|
144
|
+
const key = event.target.closest("[data-collection-key]")
|
|
145
|
+
?.dataset.collectionKey;
|
|
146
|
+
if (key === undefined || !keys.has(key))
|
|
147
|
+
return;
|
|
148
|
+
const enabled = items.filter((item) => !item.disabled);
|
|
149
|
+
const index = enabled.findIndex((item) => item.key === key);
|
|
150
|
+
let next;
|
|
151
|
+
const page = Math.max(1, Math.floor((scroll.current?.clientHeight ?? 320) / rowEstimate) - 1);
|
|
152
|
+
switch (event.key) {
|
|
153
|
+
case "Home":
|
|
154
|
+
next = enabled[0];
|
|
155
|
+
break;
|
|
156
|
+
case "End":
|
|
157
|
+
next = enabled.at(-1);
|
|
158
|
+
break;
|
|
159
|
+
case "ArrowDown":
|
|
160
|
+
next = enabled[(index + 1) % enabled.length];
|
|
161
|
+
break;
|
|
162
|
+
case "ArrowUp":
|
|
163
|
+
next = enabled[(index - 1 + enabled.length) % enabled.length];
|
|
164
|
+
break;
|
|
165
|
+
case "PageDown":
|
|
166
|
+
next = enabled[Math.min(index + page, enabled.length - 1)];
|
|
167
|
+
break;
|
|
168
|
+
case "PageUp":
|
|
169
|
+
next = enabled[Math.max(index - page, 0)];
|
|
170
|
+
break;
|
|
171
|
+
default: {
|
|
172
|
+
if (event.key.length !== 1 || event.key === " ")
|
|
173
|
+
return;
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
const text = now - typeahead.current.at < 600 ? typeahead.current.text + event.key : event.key;
|
|
176
|
+
typeahead.current = { text, at: now };
|
|
177
|
+
const term = [...text].every((char) => char === text[0]) ? text[0] : text;
|
|
178
|
+
const order = [...enabled.slice(index + 1), ...enabled.slice(0, index + 1)];
|
|
179
|
+
next = order.find((item) => item.label.toLocaleLowerCase().startsWith(term.toLocaleLowerCase()));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
event.stopPropagation();
|
|
184
|
+
if (next)
|
|
185
|
+
focus(next.key);
|
|
186
|
+
}, children: _jsx("div", { role: "presentation", style: virtual
|
|
187
|
+
? { height: virtualizer.getTotalSize(), position: "relative", width: "100%" }
|
|
188
|
+
: undefined, children: virtual
|
|
189
|
+
? virtualizer.getVirtualItems().map((row) => render(row.index, row.start))
|
|
190
|
+
: items.map((_, index) => render(index)) }) }));
|
|
191
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { RuntimeField } from "./filter-definition.js";
|
|
2
|
-
export declare function FilterDateEditor({ field, value, disabled, apply, error, autoFocus, showLabel, }: {
|
|
2
|
+
export declare function FilterDateEditor({ field, value, disabled, apply, error, draftKey, autoFocus, showLabel, showClear, }: {
|
|
3
|
+
draftKey?: string;
|
|
3
4
|
autoFocus?: boolean;
|
|
4
5
|
showLabel?: boolean;
|
|
6
|
+
showClear?: boolean;
|
|
5
7
|
field: RuntimeField;
|
|
6
8
|
value: unknown;
|
|
7
9
|
disabled?: boolean;
|
|
@@ -12,20 +12,20 @@ function dateString(value) {
|
|
|
12
12
|
return null;
|
|
13
13
|
return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, "0")}-${String(value.getDate()).padStart(2, "0")}`;
|
|
14
14
|
}
|
|
15
|
-
export function FilterDateEditor({ field, value, disabled, apply, error, autoFocus = true, showLabel = true, }) {
|
|
16
|
-
const [range, setRange] = useValueDraft(value, (current) => current ? { from: calendarDate(current.from), to: calendarDate(current.to) } : undefined);
|
|
15
|
+
export function FilterDateEditor({ field, value, disabled, apply, error, draftKey, autoFocus = true, showLabel = true, showClear = true, }) {
|
|
16
|
+
const [range, setRange] = useValueDraft(value, (current) => current ? { from: calendarDate(current.from), to: calendarDate(current.to) } : undefined, draftKey);
|
|
17
17
|
return (_jsxs("div", { "data-mendy-ui": "", onKeyDown: (event) => {
|
|
18
18
|
// Calendar arrows navigate days. Escape still dismisses the surrounding editor.
|
|
19
19
|
if (event.key !== "Escape")
|
|
20
20
|
event.stopPropagation();
|
|
21
|
-
}, children: [showLabel && _jsx("p", { className: "mui-0f9e6672913a mui-0a702d97ed7d mui-c74ab393b96d mui-daaac3fbf55e", children: field.label }), _jsx(Calendar, { mode: "range", autoFocus: autoFocus, selected: range, onSelect: (next) => {
|
|
21
|
+
}, children: [showLabel && _jsx("p", { className: "mui-0f9e6672913a mui-0a702d97ed7d mui-c74ab393b96d mui-daaac3fbf55e", children: field.label }), _jsx(Calendar, { className: "mui-1752f11474a6 mui-82c2977beb8e mui-d78a8526410d", mode: "range", autoFocus: autoFocus, selected: range, onSelect: (next) => {
|
|
22
22
|
const selected = next ? { from: next.from, to: next.to ?? next.from } : undefined;
|
|
23
23
|
setRange(selected);
|
|
24
24
|
apply(selected
|
|
25
25
|
? { from: dateString(selected.from), to: dateString(selected.to) }
|
|
26
26
|
: field.clearValue, false);
|
|
27
|
-
}, defaultMonth: range?.from ?? range?.to, disabled: disabled }), _jsxs("div", { "data-mendy-ui": "", className: "mui-267171770524 mui-894c3815a57f mui-094f5333853b", children: [_jsx("p", { className: "mui-3992de70b033 mui-35f35c41d134", children: "Pick a day, or two dates for a range." }), error && (_jsx("p", { role: "alert", className: "mui-c74ab393b96d mui-887b9502d5c7", children: error })), _jsx("div", { "data-mendy-ui": "", className: "mui-222f930b8752 mui-074569488cca", children: _jsx(Button, { type: "button", size: "sm", variant: "ghost", disabled: disabled, onClick: () => {
|
|
27
|
+
}, defaultMonth: range?.from ?? range?.to, disabled: disabled }), _jsxs("div", { "data-mendy-ui": "", className: "mui-267171770524 mui-894c3815a57f mui-094f5333853b", children: [_jsx("p", { className: "mui-3992de70b033 mui-35f35c41d134", children: "Pick a day, or two dates for a range." }), error && (_jsx("p", { role: "alert", className: "mui-c74ab393b96d mui-887b9502d5c7", children: error })), showClear && (_jsx("div", { "data-mendy-ui": "", className: "mui-222f930b8752 mui-074569488cca", children: _jsx(Button, { type: "button", size: "sm", variant: "ghost", className: "mui-958e2cfcf5b6 mui-e7e01cc7f4df", disabled: disabled || !range, onClick: () => {
|
|
28
28
|
setRange(undefined);
|
|
29
29
|
apply(field.clearValue);
|
|
30
|
-
}, children: "Clear date" }) })] })] }));
|
|
30
|
+
}, children: "Clear date" }) }))] })] }));
|
|
31
31
|
}
|
|
@@ -56,6 +56,9 @@ export interface ExternalOptions<T> {
|
|
|
56
56
|
}
|
|
57
57
|
export declare function externalOptions<T>(source: Omit<ExternalOptions<T>, "kind">): ExternalOptions<T>;
|
|
58
58
|
export interface EditorContext<V> {
|
|
59
|
+
/** False while the menu previews an editor. The menu focuses it on click or Enter. */
|
|
60
|
+
autoFocus: boolean;
|
|
61
|
+
location: "menu" | "chip" | "inline";
|
|
59
62
|
value: V;
|
|
60
63
|
setValue(value: V): void;
|
|
61
64
|
close(): void;
|
|
@@ -79,6 +82,11 @@ export interface FieldConfig<V> {
|
|
|
79
82
|
loading?: boolean;
|
|
80
83
|
disabled?: boolean;
|
|
81
84
|
};
|
|
85
|
+
/** Hide a redundant chip label, or use a shorter visible label. Accessible names stay intact. */
|
|
86
|
+
chipLabel?: boolean | string;
|
|
87
|
+
/** Place this editor beside its label inside a grouped menu. */
|
|
88
|
+
menuLayout?: "stack" | "inline";
|
|
89
|
+
editorPadding?: "default" | "none";
|
|
82
90
|
summary?: SummaryPolicy;
|
|
83
91
|
renderSummary?: (value: V, choices: readonly Choice[]) => ReactNode;
|
|
84
92
|
renderEditor?: (context: EditorContext<V>) => ReactNode;
|
|
@@ -133,6 +141,11 @@ export interface RuntimeField {
|
|
|
133
141
|
loading?: boolean;
|
|
134
142
|
disabled?: boolean;
|
|
135
143
|
};
|
|
144
|
+
/** Hide a redundant chip label, or use a shorter visible label. Accessible names stay intact. */
|
|
145
|
+
chipLabel?: boolean | string;
|
|
146
|
+
/** Place this editor beside its label inside a grouped menu. */
|
|
147
|
+
menuLayout?: "stack" | "inline";
|
|
148
|
+
editorPadding?: "default" | "none";
|
|
136
149
|
summary?: SummaryPolicy;
|
|
137
150
|
renderSummary?: (value: unknown, choices: readonly Choice[]) => ReactNode;
|
|
138
151
|
renderEditor?: (context: EditorContext<unknown>) => ReactNode;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { KeyboardEvent } from "react";
|
|
2
|
+
export declare function focusMenuEditor(root: HTMLDivElement | null): void;
|
|
3
|
+
export declare function handleMenuTab(event: KeyboardEvent<HTMLDivElement>, { trigger, editor, onClose, hasSelection, }: {
|
|
4
|
+
trigger: HTMLButtonElement | null;
|
|
5
|
+
editor: HTMLDivElement | null;
|
|
6
|
+
onClose(): void;
|
|
7
|
+
hasSelection: boolean;
|
|
8
|
+
}): void;
|
|
9
|
+
/** Return from editor controls without consuming text, grid, or tree navigation keys. */
|
|
10
|
+
export declare function handleMenuReturn(event: KeyboardEvent<HTMLDivElement>, back: () => void): void;
|
|
11
|
+
export declare function preserveOutsideFocus(event: Event): void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
function visible(element) {
|
|
2
|
+
return (!element.matches(":disabled, [data-disabled], [aria-disabled=true]") &&
|
|
3
|
+
!element.closest("[hidden], [inert]") &&
|
|
4
|
+
element.getClientRects().length > 0 &&
|
|
5
|
+
getComputedStyle(element).visibility !== "hidden");
|
|
6
|
+
}
|
|
7
|
+
export function focusMenuEditor(root) {
|
|
8
|
+
if (!root)
|
|
9
|
+
return;
|
|
10
|
+
const priorities = [
|
|
11
|
+
"input, textarea",
|
|
12
|
+
'[role="grid"] button[tabindex="0"]',
|
|
13
|
+
'[role^="menuitem"][aria-checked="true"]',
|
|
14
|
+
'[role^="menuitem"]',
|
|
15
|
+
'button, [tabindex="0"]',
|
|
16
|
+
];
|
|
17
|
+
for (const selector of priorities) {
|
|
18
|
+
const target = [...root.querySelectorAll(selector)].find((element) => !element.matches(':disabled, [data-disabled], [aria-disabled="true"]') &&
|
|
19
|
+
element.getClientRects().length > 0);
|
|
20
|
+
if (target) {
|
|
21
|
+
target.focus();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
root.focus();
|
|
26
|
+
}
|
|
27
|
+
export function handleMenuTab(event, { trigger, editor, onClose, hasSelection, }) {
|
|
28
|
+
if (event.key !== "Tab" ||
|
|
29
|
+
event.altKey ||
|
|
30
|
+
event.ctrlKey ||
|
|
31
|
+
event.metaKey ||
|
|
32
|
+
!event.currentTarget.contains(event.target))
|
|
33
|
+
return;
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
event.stopPropagation();
|
|
36
|
+
const root = event.currentTarget;
|
|
37
|
+
const target = event.target;
|
|
38
|
+
const selector = "button, input, textarea, select, a[href], [tabindex]";
|
|
39
|
+
const menuStops = new Set();
|
|
40
|
+
root.querySelectorAll('[role="menu"]').forEach((menu) => {
|
|
41
|
+
const choices = [...menu.querySelectorAll('[role^="menuitem"]')].filter(visible);
|
|
42
|
+
const current = choices.find((item) => item === document.activeElement) ??
|
|
43
|
+
choices.find((item) => item.getAttribute("aria-checked") === "true") ??
|
|
44
|
+
choices[0];
|
|
45
|
+
if (current)
|
|
46
|
+
menuStops.add(current);
|
|
47
|
+
});
|
|
48
|
+
const stops = [...root.querySelectorAll(selector)].filter((element) => visible(element) &&
|
|
49
|
+
(element.matches('[role^="menuitem"]') ? menuStops.has(element) : element.tabIndex >= 0));
|
|
50
|
+
const index = stops.indexOf(target);
|
|
51
|
+
const next = index < 0 ? undefined : stops[index + (event.shiftKey ? -1 : 1)];
|
|
52
|
+
if (target.closest('[aria-label="Filter types"]') &&
|
|
53
|
+
!event.shiftKey &&
|
|
54
|
+
hasSelection &&
|
|
55
|
+
(!next || editor?.contains(next)))
|
|
56
|
+
focusMenuEditor(editor);
|
|
57
|
+
else if (next)
|
|
58
|
+
next.focus();
|
|
59
|
+
else {
|
|
60
|
+
const button = trigger;
|
|
61
|
+
const outside = [...document.querySelectorAll(selector)].filter((element) => visible(element) &&
|
|
62
|
+
element.tabIndex >= 0 &&
|
|
63
|
+
!root.contains(element) &&
|
|
64
|
+
!element.hasAttribute("data-radix-focus-guard"));
|
|
65
|
+
const destination = event.shiftKey ? button : (outside[outside.indexOf(button) + 1] ?? button);
|
|
66
|
+
destination?.focus();
|
|
67
|
+
onClose();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Return from editor controls without consuming text, grid, or tree navigation keys. */
|
|
71
|
+
export function handleMenuReturn(event, back) {
|
|
72
|
+
const target = event.target;
|
|
73
|
+
const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
|
|
74
|
+
if (event.key !== (rtl ? "ArrowRight" : "ArrowLeft"))
|
|
75
|
+
return;
|
|
76
|
+
if (!(target instanceof HTMLElement) || target.isContentEditable)
|
|
77
|
+
return;
|
|
78
|
+
if (target.closest('input, textarea, select, [role="grid"], [role="slider"], [role="spinbutton"], [role="combobox"], [role="tablist"], [role="tree"], [role="listbox"]'))
|
|
79
|
+
return;
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
event.stopPropagation();
|
|
82
|
+
back();
|
|
83
|
+
}
|
|
84
|
+
export function preserveOutsideFocus(event) {
|
|
85
|
+
const focused = document.activeElement;
|
|
86
|
+
if (event.target instanceof HTMLElement &&
|
|
87
|
+
focused &&
|
|
88
|
+
focused !== document.body &&
|
|
89
|
+
!event.target.contains(focused))
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
}
|
|
@@ -6,6 +6,7 @@ export interface FilterMenuSection {
|
|
|
6
6
|
icon?: ReactNode;
|
|
7
7
|
disabled: boolean;
|
|
8
8
|
active: boolean;
|
|
9
|
+
separatorBefore?: boolean;
|
|
9
10
|
clear?: () => void;
|
|
10
11
|
content: ReactNode;
|
|
11
12
|
}
|
|
@@ -13,9 +14,10 @@ interface FilterMenuPanelProps {
|
|
|
13
14
|
sections: FilterMenuSection[];
|
|
14
15
|
selectedId: string | null;
|
|
15
16
|
onSelect(id: string | null): void;
|
|
17
|
+
onClose(): void;
|
|
16
18
|
anchor?: RefObject<HTMLDivElement | null>;
|
|
17
19
|
trigger: RefObject<HTMLButtonElement | null>;
|
|
18
20
|
}
|
|
19
21
|
/** One dialog contains the filter list and its editor, with a single-panel layout on phones. */
|
|
20
|
-
export declare function FilterMenuPanel({ sections, selectedId, onSelect, anchor, trigger, }: FilterMenuPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export declare function FilterMenuPanel({ sections, selectedId, onSelect, onClose, anchor, trigger, }: FilterMenuPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
21
23
|
export {};
|