@mdbase-dev/ui 0.1.0-beta.126 → 0.1.0-beta.147
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 +159 -0
- package/css/collections.css +978 -0
- package/css/controls.css +26 -1
- package/css/screens.css +44 -2
- package/dist/AppSwitcher.js +187 -33
- package/dist/Checkbox.d.ts +14 -0
- package/dist/Checkbox.js +32 -0
- package/dist/CommandPalette.d.ts +4 -2
- package/dist/CommandPalette.js +275 -103
- package/dist/Dialog.js +72 -25
- package/dist/Feedback.js +763 -229
- package/dist/FeedbackMarkup.d.ts +1 -1
- package/dist/FeedbackMarkup.js +326 -125
- package/dist/FeedbackVerification.js +115 -68
- package/dist/SaveNotice.js +71 -28
- package/dist/Select.js +183 -135
- package/dist/SelectList.js +59 -14
- package/dist/ThemeSelect.js +55 -4
- package/dist/apps.d.ts +1 -0
- package/dist/apps.js +50 -29
- package/dist/brand.js +251 -68
- package/dist/codemirror.js +116 -87
- package/dist/collections/CollectionGate.d.ts +52 -0
- package/dist/collections/CollectionGate.js +456 -0
- package/dist/collections/CollectionSaveWarning.d.ts +10 -0
- package/dist/collections/CollectionSaveWarning.js +28 -0
- package/dist/collections/CollectionSwitcher.d.ts +26 -0
- package/dist/collections/CollectionSwitcher.js +579 -0
- package/dist/collections/SetupReview.d.ts +9 -0
- package/dist/collections/SetupReview.js +175 -0
- package/dist/collections/copy.d.ts +100 -0
- package/dist/collections/copy.js +937 -0
- package/dist/collections/dialogs.d.ts +27 -0
- package/dist/collections/dialogs.js +169 -0
- package/dist/collections/model.d.ts +146 -0
- package/dist/collections/model.js +337 -0
- package/dist/collections/mount.d.ts +18 -0
- package/dist/collections/mount.js +56 -0
- package/dist/collections/parts.d.ts +86 -0
- package/dist/collections/parts.js +439 -0
- package/dist/collections/testing.d.ts +19 -0
- package/dist/collections/testing.js +39 -0
- package/dist/collections/use-collection-ui.d.ts +140 -0
- package/dist/collections/use-collection-ui.js +236 -0
- package/dist/collections.d.ts +19 -0
- package/dist/collections.js +13 -0
- package/dist/feedback-data.d.ts +13 -12
- package/dist/feedback-data.js +365 -168
- package/dist/flock.d.ts +63 -0
- package/dist/flock.js +140 -0
- package/dist/i18n.d.ts +6 -0
- package/dist/i18n.js +71 -0
- package/dist/locales/de/messages.json +1 -0
- package/dist/locales/en/messages.json +1 -0
- package/dist/locales/es/messages.json +1 -0
- package/dist/locales/fr/messages.json +1 -0
- package/dist/locales/it/messages.json +1 -0
- package/dist/locales/ja/messages.json +1 -0
- package/dist/locales/ko/messages.json +1 -0
- package/dist/locales/pt/messages.json +1 -0
- package/dist/locales/ru/messages.json +1 -0
- package/dist/locales/zh/messages.json +1 -0
- package/dist/mark-activity.js +82 -75
- package/dist/permission-selection.js +27 -17
- package/dist/popover.js +138 -130
- package/dist/screens.d.ts +20 -3
- package/dist/screens.js +275 -6
- package/dist/select-model.js +34 -28
- package/dist/text-diff.d.ts +1 -0
- package/dist/text-diff.js +106 -74
- package/dist/theme.js +34 -40
- package/package.json +52 -7
|
@@ -1,76 +1,123 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { i18n as _i18n } from "@lingui/core";
|
|
3
|
+
import { useLocaleRevision } from "./i18n.js";
|
|
2
4
|
import { useEffect, useRef, useState } from "react";
|
|
3
5
|
let turnstileScript;
|
|
4
|
-
export function FeedbackVerification({
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
6
|
+
export function FeedbackVerification({
|
|
7
|
+
siteKey,
|
|
8
|
+
onToken
|
|
9
|
+
}) {
|
|
10
|
+
useLocaleRevision();
|
|
11
|
+
const container = useRef(null);
|
|
12
|
+
const callback = useRef(onToken);
|
|
13
|
+
const [error, setError] = useState("");
|
|
14
|
+
const [attempt, setAttempt] = useState(0);
|
|
15
|
+
callback.current = onToken;
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
callback.current("");
|
|
18
|
+
setError("");
|
|
19
|
+
let active = true;
|
|
20
|
+
let widgetId;
|
|
21
|
+
void loadTurnstile().then(() => {
|
|
22
|
+
if (!active || !container.current || !window.turnstile) return;
|
|
23
|
+
widgetId = window.turnstile.render(container.current, {
|
|
24
|
+
sitekey: siteKey,
|
|
25
|
+
callback: token => {
|
|
26
|
+
if (active) {
|
|
27
|
+
setError("");
|
|
28
|
+
callback.current(token);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"expired-callback": () => {
|
|
32
|
+
if (active) callback.current("");
|
|
33
|
+
},
|
|
34
|
+
"error-callback": () => {
|
|
35
|
+
if (active) {
|
|
36
|
+
callback.current("");
|
|
37
|
+
setError(_i18n._(
|
|
38
|
+
/** i18n */
|
|
39
|
+
{
|
|
40
|
+
id: "kC9O29",
|
|
41
|
+
message: "Verification could not be completed. Try again."
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
action: "feedback",
|
|
46
|
+
theme: "auto"
|
|
47
|
+
});
|
|
48
|
+
}).catch(() => {
|
|
49
|
+
if (active) setError(_i18n._(
|
|
50
|
+
/** i18n */
|
|
51
|
+
{
|
|
52
|
+
id: "gvuZjZ",
|
|
53
|
+
message: "Verification could not be loaded. Check your connection and try again."
|
|
54
|
+
}));
|
|
55
|
+
});
|
|
56
|
+
return () => {
|
|
57
|
+
active = false;
|
|
58
|
+
if (widgetId && window.turnstile) window.turnstile.remove(widgetId);
|
|
59
|
+
};
|
|
60
|
+
}, [siteKey, attempt]);
|
|
61
|
+
return _jsxs("div", {
|
|
62
|
+
className: "mdbase-feedback-verification",
|
|
63
|
+
children: [_jsx("div", {
|
|
64
|
+
ref: container
|
|
65
|
+
}), error && _jsxs(_Fragment, {
|
|
66
|
+
children: [_jsx("p", {
|
|
67
|
+
className: "mdbase-feedback-error",
|
|
68
|
+
role: "alert",
|
|
69
|
+
children: error
|
|
70
|
+
}), _jsx("button", {
|
|
71
|
+
type: "button",
|
|
72
|
+
onClick: () => setAttempt(value => value + 1),
|
|
73
|
+
children: _i18n._(
|
|
74
|
+
/** i18n */
|
|
75
|
+
{
|
|
76
|
+
id: "ak4q13",
|
|
77
|
+
message: "Retry verification"
|
|
78
|
+
})
|
|
79
|
+
})]
|
|
80
|
+
})]
|
|
81
|
+
});
|
|
44
82
|
}
|
|
45
83
|
function loadTurnstile() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
document.head.append(script);
|
|
68
|
-
}
|
|
84
|
+
if (window.turnstile) return Promise.resolve();
|
|
85
|
+
if (turnstileScript) return turnstileScript;
|
|
86
|
+
turnstileScript = new Promise((resolve, reject) => {
|
|
87
|
+
const existing = document.querySelector('script[data-mdbase-turnstile="true"]');
|
|
88
|
+
const script = existing ?? document.createElement("script");
|
|
89
|
+
const cleanup = () => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
script.removeEventListener("load", loaded);
|
|
92
|
+
script.removeEventListener("error", failed);
|
|
93
|
+
};
|
|
94
|
+
const loaded = () => {
|
|
95
|
+
cleanup();
|
|
96
|
+
if (window.turnstile) resolve();else reject(new Error("Turnstile API unavailable"));
|
|
97
|
+
};
|
|
98
|
+
const failed = () => {
|
|
99
|
+
cleanup();
|
|
100
|
+
reject(new Error("Turnstile failed to load"));
|
|
101
|
+
};
|
|
102
|
+
const timer = setTimeout(failed, 15_000);
|
|
103
|
+
script.addEventListener("load", loaded, {
|
|
104
|
+
once: true
|
|
69
105
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
turnstileScript = undefined;
|
|
73
|
-
throw error;
|
|
106
|
+
script.addEventListener("error", failed, {
|
|
107
|
+
once: true
|
|
74
108
|
});
|
|
75
|
-
|
|
109
|
+
if (!existing) {
|
|
110
|
+
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
|
|
111
|
+
script.async = true;
|
|
112
|
+
script.defer = true;
|
|
113
|
+
script.dataset.mdbaseTurnstile = "true";
|
|
114
|
+
document.head.append(script);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
turnstileScript = turnstileScript.catch(error => {
|
|
118
|
+
document.querySelector('script[data-mdbase-turnstile="true"]')?.remove();
|
|
119
|
+
turnstileScript = undefined;
|
|
120
|
+
throw error;
|
|
121
|
+
});
|
|
122
|
+
return turnstileScript;
|
|
76
123
|
}
|
package/dist/SaveNotice.js
CHANGED
|
@@ -1,21 +1,49 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { i18n as _i18n } from "@lingui/core";
|
|
3
|
+
import { useLocaleRevision } from "./i18n.js";
|
|
2
4
|
import { useEffect, useRef } from "react";
|
|
3
5
|
import { holdMdbaseMarkBusy, signalMdbaseMark } from "./mark-activity.js";
|
|
4
6
|
/** A save shorter than this never sets the mark moving. */
|
|
5
7
|
const SLOW_SAVE_MS = 600;
|
|
6
8
|
/** The mark's reaction when a notice changes from `before` to `tone`; none when it first appears. */
|
|
7
9
|
export function saveToneSignal(before, tone) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return "error";
|
|
12
|
-
return tone === "saved" && (before === "saving" || before === "pending") ? "saved" : null;
|
|
10
|
+
if (before === null || before === tone) return null;
|
|
11
|
+
if (tone === "attention") return "error";
|
|
12
|
+
return tone === "saved" && (before === "saving" || before === "pending") ? "saved" : null;
|
|
13
13
|
}
|
|
14
14
|
const defaultLabels = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
get saved() {
|
|
16
|
+
return _i18n._(
|
|
17
|
+
/** i18n */
|
|
18
|
+
{
|
|
19
|
+
id: "idD8Ev",
|
|
20
|
+
message: "Saved"
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
get saving() {
|
|
24
|
+
return _i18n._(
|
|
25
|
+
/** i18n */
|
|
26
|
+
{
|
|
27
|
+
id: "uF9ruK",
|
|
28
|
+
message: "Saving"
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
get pending() {
|
|
32
|
+
return _i18n._(
|
|
33
|
+
/** i18n */
|
|
34
|
+
{
|
|
35
|
+
id: "P74TOi",
|
|
36
|
+
message: "Unsaved"
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
get attention() {
|
|
40
|
+
return _i18n._(
|
|
41
|
+
/** i18n */
|
|
42
|
+
{
|
|
43
|
+
id: "fAUSZy",
|
|
44
|
+
message: "Needs attention"
|
|
45
|
+
});
|
|
46
|
+
}
|
|
19
47
|
};
|
|
20
48
|
/**
|
|
21
49
|
* The minimal save notice every mdbase app shows beside what it edits. Put it inside an
|
|
@@ -25,23 +53,38 @@ const defaultLabels = {
|
|
|
25
53
|
* The notice also drives the app's mark: a save that completes plays `saved`, a newly
|
|
26
54
|
* raised problem plays `error`, and a slow save bounces the mark until it ends.
|
|
27
55
|
*/
|
|
28
|
-
export function SaveNotice({
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
return
|
|
56
|
+
export function SaveNotice({
|
|
57
|
+
tone,
|
|
58
|
+
label = defaultLabels[tone],
|
|
59
|
+
className
|
|
60
|
+
}) {
|
|
61
|
+
useLocaleRevision();
|
|
62
|
+
const previous = useRef(null);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const kind = saveToneSignal(previous.current, tone);
|
|
65
|
+
previous.current = tone;
|
|
66
|
+
if (kind) signalMdbaseMark(kind);
|
|
67
|
+
}, [tone]);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (tone !== "saving") return;
|
|
70
|
+
let release;
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
release = holdMdbaseMarkBusy("bounce");
|
|
73
|
+
}, SLOW_SAVE_MS);
|
|
74
|
+
return () => {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
release?.();
|
|
77
|
+
};
|
|
78
|
+
}, [tone]);
|
|
79
|
+
return _jsxs("span", {
|
|
80
|
+
className: ["mdbase-save-notice", `is-${tone}`, tone === "saved" && "mdbase-settle", className].filter(Boolean).join(" "),
|
|
81
|
+
"aria-live": "polite",
|
|
82
|
+
children: [tone === "saved" && _jsx("svg", {
|
|
83
|
+
viewBox: "0 0 16 16",
|
|
84
|
+
"aria-hidden": "true",
|
|
85
|
+
children: _jsx("path", {
|
|
86
|
+
d: "m3.5 8.5 3 3 6-7"
|
|
87
|
+
})
|
|
88
|
+
}), label]
|
|
89
|
+
});
|
|
47
90
|
}
|
package/dist/Select.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useId, useLayoutEffect, useRef, useState
|
|
3
|
-
import { edgeOption, extendTypeahead, flattenOptions, stepOption, typeaheadOption
|
|
2
|
+
import { useId, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { edgeOption, extendTypeahead, flattenOptions, stepOption, typeaheadOption } from "./select-model.js";
|
|
4
4
|
import { SelectList } from "./SelectList.js";
|
|
5
5
|
import { useSelectPopover } from "./popover.js";
|
|
6
6
|
/**
|
|
@@ -9,150 +9,198 @@ import { useSelectPopover } from "./popover.js";
|
|
|
9
9
|
* panels, menus) never cut it off, while it stays in the DOM beside its trigger: a click in
|
|
10
10
|
* the list still counts as inside any menu that contains the select.
|
|
11
11
|
*/
|
|
12
|
-
export function Select({
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
export function Select({
|
|
13
|
+
value,
|
|
14
|
+
options: items,
|
|
15
|
+
onChange,
|
|
16
|
+
placeholder,
|
|
17
|
+
className,
|
|
18
|
+
disabled,
|
|
19
|
+
onKeyDown,
|
|
20
|
+
onClick,
|
|
21
|
+
ref,
|
|
22
|
+
...trigger
|
|
23
|
+
}) {
|
|
24
|
+
const id = useId();
|
|
25
|
+
const {
|
|
26
|
+
"aria-label": ariaLabel,
|
|
27
|
+
"aria-labelledby": ariaLabelledBy
|
|
28
|
+
} = trigger;
|
|
29
|
+
const triggerRef = useRef(null);
|
|
30
|
+
const listRef = useRef(null);
|
|
31
|
+
const options = flattenOptions(items);
|
|
32
|
+
const selected = options.findIndex(option => option.value === value);
|
|
33
|
+
const state = useSelectState(options, selected, value, onChange, disabled, triggerRef);
|
|
34
|
+
useSelectPopover(state.open, triggerRef, listRef, () => state.close(false));
|
|
35
|
+
useLayoutEffect(() => {
|
|
36
|
+
if (state.open && listRef.current) {
|
|
37
|
+
revealOption(listRef.current, state.active);
|
|
38
|
+
}
|
|
39
|
+
}, [state.active, state.open]);
|
|
40
|
+
const label = selected >= 0 ? options[selected]?.label : placeholder;
|
|
41
|
+
return _jsxs(_Fragment, {
|
|
42
|
+
children: [_jsxs("button", {
|
|
43
|
+
...trigger,
|
|
44
|
+
ref: node => {
|
|
45
|
+
triggerRef.current = node;
|
|
46
|
+
if (typeof ref === "function") ref(node);else if (ref) ref.current = node;
|
|
47
|
+
},
|
|
48
|
+
type: "button",
|
|
49
|
+
role: "combobox",
|
|
50
|
+
className: ["mdbase-select", className].filter(Boolean).join(" "),
|
|
51
|
+
"data-value": value,
|
|
52
|
+
disabled: disabled,
|
|
53
|
+
"aria-haspopup": "listbox",
|
|
54
|
+
"aria-expanded": state.open,
|
|
55
|
+
"aria-controls": `${id}-list`,
|
|
56
|
+
onClick: event => {
|
|
57
|
+
onClick?.(event);
|
|
58
|
+
if (!event.defaultPrevented) {
|
|
59
|
+
state.toggle();
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
onKeyDown: event => {
|
|
63
|
+
onKeyDown?.(event);
|
|
64
|
+
if (!event.defaultPrevented) {
|
|
65
|
+
state.onTriggerKey(event);
|
|
23
66
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
67
|
+
},
|
|
68
|
+
children: [_jsx("span", {
|
|
69
|
+
className: selected >= 0 ? "mdbase-select-value" : "mdbase-select-value is-empty",
|
|
70
|
+
children: label ?? ""
|
|
71
|
+
}), _jsx("svg", {
|
|
72
|
+
className: "mdbase-select-chevron",
|
|
73
|
+
viewBox: "0 0 24 24",
|
|
74
|
+
"aria-hidden": "true",
|
|
75
|
+
children: _jsx("path", {
|
|
76
|
+
d: "m7 10 5 5 5-5"
|
|
77
|
+
})
|
|
78
|
+
})]
|
|
79
|
+
}), _jsx(SelectList, {
|
|
80
|
+
id: id,
|
|
81
|
+
listRef: listRef,
|
|
82
|
+
items: items,
|
|
83
|
+
selected: selected,
|
|
84
|
+
active: state.active,
|
|
85
|
+
open: state.open,
|
|
86
|
+
label: ariaLabel,
|
|
87
|
+
labelledBy: ariaLabelledBy,
|
|
88
|
+
onActivate: state.setActive,
|
|
89
|
+
onChoose: state.choose,
|
|
90
|
+
onKeyDown: state.onListKey
|
|
91
|
+
})]
|
|
92
|
+
});
|
|
43
93
|
}
|
|
44
94
|
function useSelectState(options, selected, value, onChange, disabled, triggerRef) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
};
|
|
95
|
+
const [open, setOpen] = useState(false);
|
|
96
|
+
const [active, setActive] = useState(-1);
|
|
97
|
+
const typed = useRef({
|
|
98
|
+
text: "",
|
|
99
|
+
at: 0
|
|
100
|
+
});
|
|
101
|
+
const typeahead = (event, from) => {
|
|
102
|
+
typed.current = extendTypeahead(typed.current, event.key, event.timeStamp);
|
|
103
|
+
return typeaheadOption(options, typed.current.text, from);
|
|
104
|
+
};
|
|
105
|
+
const show = index => {
|
|
106
|
+
if (!disabled && options.length) {
|
|
107
|
+
setActive(index);
|
|
108
|
+
setOpen(true);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const close = refocus => {
|
|
112
|
+
setOpen(false);
|
|
113
|
+
if (refocus) {
|
|
114
|
+
triggerRef.current?.focus();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const commit = index => {
|
|
118
|
+
const option = options[index];
|
|
119
|
+
if (option && !option.disabled && option.value !== value) {
|
|
120
|
+
onChange(option.value);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const choose = index => {
|
|
124
|
+
if (options[index] && !options[index].disabled) {
|
|
125
|
+
close(true);
|
|
126
|
+
commit(index);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const initial = key => selected >= 0 ? selected : edgeOption(options, key === "ArrowUp" ? "last" : "first");
|
|
130
|
+
const onTriggerKey = event => {
|
|
131
|
+
if (disabled) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (["ArrowDown", "ArrowUp", "Enter", " "].includes(event.key)) {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
show(initial(event.key));
|
|
137
|
+
} else if (isPrintable(event)) {
|
|
138
|
+
// Like a native select, typing on the closed control changes the value directly.
|
|
139
|
+
commit(typeahead(event, selected));
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const moves = {
|
|
143
|
+
ArrowDown: () => stepOption(options, active, 1),
|
|
144
|
+
ArrowUp: () => stepOption(options, active, -1),
|
|
145
|
+
Home: () => edgeOption(options, "first"),
|
|
146
|
+
End: () => edgeOption(options, "last"),
|
|
147
|
+
PageDown: () => stepPage(options, active, 1),
|
|
148
|
+
PageUp: () => stepPage(options, active, -1)
|
|
149
|
+
};
|
|
150
|
+
const onListKey = event => {
|
|
151
|
+
event.stopPropagation();
|
|
152
|
+
const move = moves[event.key];
|
|
153
|
+
if (move) {
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
setActive(move());
|
|
156
|
+
} else if (event.key === "Enter" || event.key === " ") {
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
choose(active);
|
|
159
|
+
} else if (event.key === "Escape" || event.key === "Tab") {
|
|
160
|
+
if (event.key === "Escape") {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
}
|
|
163
|
+
close(event.key === "Escape");
|
|
164
|
+
} else if (isPrintable(event)) {
|
|
165
|
+
const match = typeahead(event, active);
|
|
166
|
+
setActive(match >= 0 ? match : active);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
open,
|
|
171
|
+
active,
|
|
172
|
+
setActive,
|
|
173
|
+
close,
|
|
174
|
+
choose,
|
|
175
|
+
toggle: () => open ? close(false) : show(initial("ArrowDown")),
|
|
176
|
+
onTriggerKey,
|
|
177
|
+
onListKey
|
|
178
|
+
};
|
|
130
179
|
}
|
|
131
180
|
function stepPage(options, from, direction) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
181
|
+
let index = from;
|
|
182
|
+
for (let step = 0; step < 8; step += 1) {
|
|
183
|
+
index = stepOption(options, index, direction);
|
|
184
|
+
}
|
|
185
|
+
return index;
|
|
137
186
|
}
|
|
138
187
|
/**
|
|
139
188
|
* Scrolls only the list to show an option. scrollIntoView would also scroll the panels the
|
|
140
189
|
* list sits inside in the DOM, which the open list treats as the page moving and closes.
|
|
141
190
|
*/
|
|
142
191
|
function revealOption(list, index) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
192
|
+
const option = list.querySelector(`[data-index="${String(index)}"]`);
|
|
193
|
+
if (!option) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const top = option.offsetTop;
|
|
197
|
+
const bottom = top + option.offsetHeight;
|
|
198
|
+
if (top < list.scrollTop) {
|
|
199
|
+
list.scrollTop = top;
|
|
200
|
+
} else if (bottom > list.scrollTop + list.clientHeight) {
|
|
201
|
+
list.scrollTop = bottom - list.clientHeight;
|
|
202
|
+
}
|
|
155
203
|
}
|
|
156
204
|
function isPrintable(event) {
|
|
157
|
-
|
|
205
|
+
return event.key.length === 1 && event.key !== " " && !event.ctrlKey && !event.metaKey && !event.altKey;
|
|
158
206
|
}
|