@antadesign/anta 0.3.5 → 0.3.6
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/dist/anta_helpers.d.ts +10 -0
- package/dist/anta_helpers.js +8 -0
- package/dist/components/Dialog.d.ts +92 -0
- package/dist/components/Dialog.js +71 -0
- package/dist/components/Input.d.ts +3 -0
- package/dist/components/InputAutocomplete.d.ts +69 -0
- package/dist/components/InputAutocomplete.js +95 -0
- package/dist/components/Menu.d.ts +4 -1
- package/dist/components/Menu.js +2 -1
- package/dist/components/MenuItem.d.ts +9 -1
- package/dist/components/MenuItem.js +2 -1
- package/dist/components/Select.d.ts +5 -2
- package/dist/components/Select.js +11 -28
- package/dist/components/SelectFaceted.d.ts +7 -1
- package/dist/components/SelectFaceted.js +12 -11
- package/dist/components/select-options.d.ts +14 -0
- package/dist/components/select-options.js +31 -0
- package/dist/elements/a-dialog.css +1 -0
- package/dist/elements/a-dialog.d.ts +26 -0
- package/dist/elements/a-dialog.js +386 -0
- package/dist/elements/a-expander.js +2 -2
- package/dist/elements/a-input-time.js +2 -1
- package/dist/elements/a-menu.d.ts +5 -2
- package/dist/elements/a-menu.js +31 -6
- package/dist/elements/a-tooltip.d.ts +17 -0
- package/dist/elements/a-tooltip.js +28 -0
- package/dist/elements/index.d.ts +1 -0
- package/dist/elements/index.js +3 -0
- package/dist/general_types.d.ts +45 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/jsx-runtime.d.ts +2 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "@antadesign/anta/jsx-runtime";
|
|
2
2
|
import { useState, useMemo } from "../jsx-runtime";
|
|
3
|
-
import { nativeStateChange } from "../anta_helpers";
|
|
3
|
+
import { nativeStateChange, ISOLATE_HINT } from "../anta_helpers";
|
|
4
|
+
import { normalizeOpt, matchQueryRegex, matchesQuery } from "./select-options";
|
|
4
5
|
import { Button } from "./Button";
|
|
5
6
|
import { Menu } from "./Menu";
|
|
6
7
|
import { MenuItem } from "./MenuItem";
|
|
@@ -8,8 +9,8 @@ import { MenuGroup } from "./MenuGroup";
|
|
|
8
9
|
import { MenuSeparator } from "./MenuSeparator";
|
|
9
10
|
import { Tag } from "./Tag";
|
|
10
11
|
import { Input } from "./Input";
|
|
12
|
+
import { Tooltip } from "./Tooltip";
|
|
11
13
|
import styles from "./SelectFaceted.module.css";
|
|
12
|
-
const normalizeOpt = (o) => typeof o === "string" ? { value: o, label: o } : o;
|
|
13
14
|
const leavesOf = (items) => {
|
|
14
15
|
const out = [];
|
|
15
16
|
for (const it of items) {
|
|
@@ -22,13 +23,7 @@ const leavesOf = (items) => {
|
|
|
22
23
|
return out;
|
|
23
24
|
};
|
|
24
25
|
const isEmpty = (v) => v == null || v === "" || Array.isArray(v) && v.length === 0;
|
|
25
|
-
const
|
|
26
|
-
const defaultMatch = (o, query) => {
|
|
27
|
-
const q = query.trim();
|
|
28
|
-
if (!q) return true;
|
|
29
|
-
const re = new RegExp(q.split(/\s+/).map(escapeRe).join("\\s+"), "i");
|
|
30
|
-
return [o.value, o.label, o.hint].some((v) => typeof v === "string" && re.test(v));
|
|
31
|
-
};
|
|
26
|
+
const defaultMatch = (o, query) => matchesQuery(o, matchQueryRegex(query));
|
|
32
27
|
const SelectFaceted = (props) => {
|
|
33
28
|
const {
|
|
34
29
|
facets,
|
|
@@ -40,6 +35,7 @@ const SelectFaceted = (props) => {
|
|
|
40
35
|
size,
|
|
41
36
|
priority = "secondary",
|
|
42
37
|
disabled,
|
|
38
|
+
toneSelected,
|
|
43
39
|
searchable,
|
|
44
40
|
searchPlaceholder = "Filter\u2026",
|
|
45
41
|
clearable = true,
|
|
@@ -125,6 +121,7 @@ const SelectFaceted = (props) => {
|
|
|
125
121
|
label: opt.label ?? opt.value,
|
|
126
122
|
hint: opt.hint,
|
|
127
123
|
tone: opt.tone,
|
|
124
|
+
toneSelected,
|
|
128
125
|
disabled: opt.disabled,
|
|
129
126
|
"data-menu-open": ""
|
|
130
127
|
};
|
|
@@ -142,13 +139,17 @@ const SelectFaceted = (props) => {
|
|
|
142
139
|
);
|
|
143
140
|
}
|
|
144
141
|
const arr = current[facet.key] ?? [];
|
|
142
|
+
const isolable = facet.selectAll !== false && !opt.disabled;
|
|
143
|
+
const tip = opt.tooltip ?? (isolable ? ISOLATE_HINT : void 0);
|
|
144
|
+
const hintOnly = opt.tooltip == null && isolable;
|
|
145
145
|
return /* @__PURE__ */ jsx(
|
|
146
146
|
MenuItem,
|
|
147
147
|
{
|
|
148
148
|
...shared,
|
|
149
149
|
selectionIndicator: "checkbox",
|
|
150
150
|
selected: arr.includes(opt.value),
|
|
151
|
-
onSelect: () => setFacet(facet, arr.includes(opt.value) ? arr.filter((v) => v !== opt.value) : [...arr, opt.value])
|
|
151
|
+
onSelect: (e) => isolable && e?.altKey ? setFacet(facet, [opt.value]) : setFacet(facet, arr.includes(opt.value) ? arr.filter((v) => v !== opt.value) : [...arr, opt.value]),
|
|
152
|
+
children: tip && /* @__PURE__ */ jsx(Tooltip, { ...hintOnly ? { follow: true, delay: 700 } : {}, children: tip })
|
|
152
153
|
},
|
|
153
154
|
`${keyPrefix}${opt.value}`
|
|
154
155
|
);
|
|
@@ -161,7 +162,7 @@ const SelectFaceted = (props) => {
|
|
|
161
162
|
const allOn = enabled.length > 0 && enabled.every((v) => arr.includes(v));
|
|
162
163
|
const someOn = enabled.some((v) => arr.includes(v));
|
|
163
164
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
164
|
-
facet.selectAll && leaves.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
165
|
+
facet.selectAll !== false && leaves.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
165
166
|
/* @__PURE__ */ jsx(
|
|
166
167
|
MenuItem,
|
|
167
168
|
{
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SelectOption } from './Select';
|
|
2
|
+
/** Coerce a bare string option to `{ value, label }`; pass an object through. */
|
|
3
|
+
export declare const normalizeOpt: (o: string | SelectOption) => SelectOption;
|
|
4
|
+
/** The built-in filter regex: case-insensitive, with each run of whitespace in the
|
|
5
|
+
* query matching any gap (`\s+`). Returns null for an empty query — the signal to
|
|
6
|
+
* match everything and to skip match-highlighting. */
|
|
7
|
+
export declare function matchQueryRegex(query: string): RegExp | null;
|
|
8
|
+
/** Whether an option matches the built-in query regex, testing value / label /
|
|
9
|
+
* hint. A null regex (empty query) matches everything. */
|
|
10
|
+
export declare const matchesQuery: (o: SelectOption, queryRe: RegExp | null) => boolean;
|
|
11
|
+
/** Bold the matched substring(s) of `text` for display (built-in matcher only).
|
|
12
|
+
* A null regex returns the text unchanged. The regex is re-created global so
|
|
13
|
+
* `exec` walks every match; the zero-width guard stops an empty match looping. */
|
|
14
|
+
export declare const highlight: (text: string, queryRe: RegExp | null) => React.ReactNode;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx } from "@antadesign/anta/jsx-runtime";
|
|
2
|
+
const normalizeOpt = (o) => typeof o === "string" ? { value: o, label: o } : o;
|
|
3
|
+
function matchQueryRegex(query) {
|
|
4
|
+
const q = query.trim();
|
|
5
|
+
if (!q) return null;
|
|
6
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7
|
+
return new RegExp(q.split(/\s+/).map(escape).join("\\s+"), "i");
|
|
8
|
+
}
|
|
9
|
+
const matchesQuery = (o, queryRe) => !queryRe || [o.value, o.label ?? "", o.hint ?? ""].some((s) => queryRe.test(s));
|
|
10
|
+
const highlight = (text, queryRe) => {
|
|
11
|
+
if (!queryRe) return text;
|
|
12
|
+
const re = new RegExp(queryRe.source, "gi");
|
|
13
|
+
const out = [];
|
|
14
|
+
let last = 0;
|
|
15
|
+
let m;
|
|
16
|
+
while ((m = re.exec(text)) !== null) {
|
|
17
|
+
if (m.index > last) out.push(text.slice(last, m.index));
|
|
18
|
+
out.push(/* @__PURE__ */ jsx("b", { children: m[0] }, out.length));
|
|
19
|
+
last = m.index + m[0].length;
|
|
20
|
+
if (m[0].length === 0) re.lastIndex++;
|
|
21
|
+
}
|
|
22
|
+
if (out.length === 0) return text;
|
|
23
|
+
if (last < text.length) out.push(text.slice(last));
|
|
24
|
+
return out;
|
|
25
|
+
};
|
|
26
|
+
export {
|
|
27
|
+
highlight,
|
|
28
|
+
matchQueryRegex,
|
|
29
|
+
matchesQuery,
|
|
30
|
+
normalizeOpt
|
|
31
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer anta{a-dialog{--dialog-bg: var(--bg-1);--dialog-text: var(--text-2);--dialog-border: color-mix(in oklch, black 9%, transparent);--dialog-radius: 10px;--dialog-shadow: inset 0 0 0 1px var(--dialog-border), 0 1px 3px color-mix(in oklch, black 10%, transparent), 0 12px 32px color-mix(in oklch, black 22%, transparent), 0 28px 64px color-mix(in oklch, black 14%, transparent);--dialog-overlay: color-mix(in oklch, black 42%, transparent);display:contents}a-dialog[round]{--dialog-round: attr(round type(<length>), var(--dialog-radius, 10px))}a-dialog:not(:defined){display:none}.dark a-dialog{--dialog-border: color-mix(in oklch, white 14%, transparent);--dialog-shadow: inset 0 0 0 1px var(--dialog-border), 0 1px 3px color-mix(in oklch, black 40%, transparent), 0 12px 32px color-mix(in oklch, black 55%, transparent), 0 28px 64px color-mix(in oklch, black 40%, transparent);--dialog-overlay: color-mix(in oklch, black 62%, transparent)}}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { HTMLElementBase } from '../anta_helpers';
|
|
2
|
+
import './a-dialog.css';
|
|
3
|
+
export declare class ADialogElement extends HTMLElementBase {
|
|
4
|
+
#private;
|
|
5
|
+
static observedAttributes: string[];
|
|
6
|
+
private dialog;
|
|
7
|
+
private headerSlot;
|
|
8
|
+
private footerSlot;
|
|
9
|
+
private closeSlot;
|
|
10
|
+
constructor();
|
|
11
|
+
connectedCallback(): void;
|
|
12
|
+
disconnectedCallback(): void;
|
|
13
|
+
attributeChangedCallback(name: string): void;
|
|
14
|
+
/** Public imperative API — mirrors `<dialog>`. `show()` requests open,
|
|
15
|
+
* `close()` requests close; both honor the state contract. */
|
|
16
|
+
show(): void;
|
|
17
|
+
close(): void;
|
|
18
|
+
get open(): boolean;
|
|
19
|
+
/** Announce a requested OPEN (cancelable, before applying), then — uncontrolled
|
|
20
|
+
* and un-vetoed — apply it. Controlled: never self-apply. */
|
|
21
|
+
requestOpen(): void;
|
|
22
|
+
/** Announce a requested CLOSE (cancelable, before applying), then — uncontrolled
|
|
23
|
+
* and un-vetoed — apply it. Controlled: never self-apply. */
|
|
24
|
+
requestClose(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function register_a_dialog(): void;
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { HTMLElementBase, parseOpenState } from "../anta_helpers";
|
|
2
|
+
import "./a-dialog.css";
|
|
3
|
+
const CLOSE_TRIGGER = "closerequest";
|
|
4
|
+
const ANIM_MS = 200;
|
|
5
|
+
const SHADOW_STYLE = `
|
|
6
|
+
:host { display: contents; }
|
|
7
|
+
|
|
8
|
+
dialog {
|
|
9
|
+
--_dur: ${ANIM_MS}ms;
|
|
10
|
+
--_r: var(--dialog-round, var(--dialog-radius, 10px));
|
|
11
|
+
|
|
12
|
+
position: fixed;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
padding: 0;
|
|
15
|
+
border: none;
|
|
16
|
+
max-width: calc(100vw - 2 * var(--dialog-margin, 32px));
|
|
17
|
+
max-height: calc(100dvh - 2 * var(--dialog-margin, 32px));
|
|
18
|
+
width: var(--dialog-width, 480px);
|
|
19
|
+
height: var(--dialog-height, fit-content);
|
|
20
|
+
overflow: visible;
|
|
21
|
+
color: var(--dialog-text, inherit);
|
|
22
|
+
background: var(--dialog-bg, #fff);
|
|
23
|
+
border-radius: var(--_r);
|
|
24
|
+
box-shadow: var(--dialog-shadow, inset 0 0 0 1px color-mix(in oklch, black 8%, transparent), 0 10px 38px color-mix(in oklch, black 28%, transparent));
|
|
25
|
+
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
dialog[open] {
|
|
30
|
+
display: flex;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
dialog:focus, dialog:focus-visible { outline: none; }
|
|
34
|
+
|
|
35
|
+
dialog::backdrop {
|
|
36
|
+
background: var(--dialog-overlay, color-mix(in oklch, black 40%, transparent));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:host { --_enter-from: scale(0.97); }
|
|
40
|
+
:host([position="left"]) { --_enter-from: translateX(-100%); }
|
|
41
|
+
:host([position="right"]) { --_enter-from: translateX(100%); }
|
|
42
|
+
:host([position="top"]) { --_enter-from: translateY(-100%); }
|
|
43
|
+
:host([position="bottom"]) { --_enter-from: translateY(100%); }
|
|
44
|
+
|
|
45
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
46
|
+
dialog {
|
|
47
|
+
opacity: 0;
|
|
48
|
+
transform: var(--_enter-from);
|
|
49
|
+
transition:
|
|
50
|
+
opacity var(--_dur) ease,
|
|
51
|
+
transform var(--_dur) ease,
|
|
52
|
+
overlay var(--_dur) ease allow-discrete,
|
|
53
|
+
display var(--_dur) ease allow-discrete;
|
|
54
|
+
}
|
|
55
|
+
dialog[open] {
|
|
56
|
+
opacity: 1;
|
|
57
|
+
transform: none;
|
|
58
|
+
}
|
|
59
|
+
@starting-style {
|
|
60
|
+
dialog[open] { opacity: 0; transform: var(--_enter-from); }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
dialog::backdrop {
|
|
64
|
+
background: transparent;
|
|
65
|
+
transition:
|
|
66
|
+
background var(--_dur) ease,
|
|
67
|
+
overlay var(--_dur) ease allow-discrete,
|
|
68
|
+
display var(--_dur) ease allow-discrete;
|
|
69
|
+
}
|
|
70
|
+
dialog[open]::backdrop { background: var(--dialog-overlay, color-mix(in oklch, black 40%, transparent)); }
|
|
71
|
+
@starting-style {
|
|
72
|
+
dialog[open]::backdrop { background: transparent; }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
:host([position="left"]) dialog,
|
|
77
|
+
:host([position="right"]) dialog {
|
|
78
|
+
width: var(--dialog-width, 380px);
|
|
79
|
+
max-width: calc(100vw - var(--dialog-margin, 32px));
|
|
80
|
+
height: 100dvh;
|
|
81
|
+
max-height: 100dvh;
|
|
82
|
+
border-radius: 0;
|
|
83
|
+
}
|
|
84
|
+
:host([position="left"]) dialog { margin: 0 auto 0 0; }
|
|
85
|
+
:host([position="right"]) dialog { margin: 0 0 0 auto; }
|
|
86
|
+
|
|
87
|
+
:host([position="top"]) dialog,
|
|
88
|
+
:host([position="bottom"]) dialog {
|
|
89
|
+
width: 100vw;
|
|
90
|
+
max-width: 100vw;
|
|
91
|
+
height: var(--dialog-height, fit-content);
|
|
92
|
+
max-height: calc(100dvh - var(--dialog-margin, 32px));
|
|
93
|
+
border-radius: 0;
|
|
94
|
+
}
|
|
95
|
+
:host([position="top"]) dialog { margin: 0 auto auto auto; }
|
|
96
|
+
:host([position="bottom"]) dialog { margin: auto auto 0 auto; }
|
|
97
|
+
|
|
98
|
+
:host([position="fullscreen"]) dialog {
|
|
99
|
+
width: 100vw;
|
|
100
|
+
max-width: 100vw;
|
|
101
|
+
height: 100dvh;
|
|
102
|
+
max-height: 100dvh;
|
|
103
|
+
margin: 0;
|
|
104
|
+
border-radius: 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
:host([round]) dialog { border-radius: var(--_r); }
|
|
108
|
+
:host([round="top"]) dialog { border-radius: var(--_r) var(--_r) 0 0; }
|
|
109
|
+
:host([round="bottom"]) dialog { border-radius: 0 0 var(--_r) var(--_r); }
|
|
110
|
+
:host([round="left"]) dialog { border-radius: var(--_r) 0 0 var(--_r); }
|
|
111
|
+
:host([round="right"]) dialog { border-radius: 0 var(--_r) var(--_r) 0; }
|
|
112
|
+
|
|
113
|
+
slot[name="header"] {
|
|
114
|
+
display: none;
|
|
115
|
+
flex: 0 0 auto;
|
|
116
|
+
padding: var(--dialog-header-padding, 18px 20px 8px);
|
|
117
|
+
font-size: 17px;
|
|
118
|
+
font-weight: 560;
|
|
119
|
+
line-height: 24px;
|
|
120
|
+
}
|
|
121
|
+
slot[name="header"].has-content { display: block; }
|
|
122
|
+
dialog.has-close slot[name="header"] { padding-inline-end: 52px; }
|
|
123
|
+
|
|
124
|
+
slot[part="body"] {
|
|
125
|
+
display: block;
|
|
126
|
+
flex: 1 1 auto;
|
|
127
|
+
min-height: 0;
|
|
128
|
+
overflow: auto;
|
|
129
|
+
padding: var(--dialog-body-padding, 8px 20px 12px);
|
|
130
|
+
}
|
|
131
|
+
/* With a close button but no header, the body is the top zone, so it takes the
|
|
132
|
+
\u2715 clearance instead of the header (which is display:none when empty). */
|
|
133
|
+
dialog.has-close slot[name="header"]:not(.has-content) ~ slot[part="body"] {
|
|
134
|
+
padding-inline-end: 52px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
slot[name="footer"] {
|
|
138
|
+
display: none;
|
|
139
|
+
flex: 0 0 auto;
|
|
140
|
+
padding: var(--dialog-footer-padding, 8px 20px 18px);
|
|
141
|
+
}
|
|
142
|
+
slot[name="footer"].has-content {
|
|
143
|
+
display: flex;
|
|
144
|
+
gap: 8px;
|
|
145
|
+
justify-content: flex-end;
|
|
146
|
+
align-items: center;
|
|
147
|
+
flex-wrap: wrap;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
slot[name="close"] { display: none; }
|
|
151
|
+
slot[name="close"].has-content {
|
|
152
|
+
display: block;
|
|
153
|
+
position: absolute;
|
|
154
|
+
inset-block-start: 10px;
|
|
155
|
+
inset-inline-end: 10px;
|
|
156
|
+
z-index: 1;
|
|
157
|
+
}
|
|
158
|
+
`;
|
|
159
|
+
class ADialogElement extends HTMLElementBase {
|
|
160
|
+
static observedAttributes = ["state", "name"];
|
|
161
|
+
dialog;
|
|
162
|
+
headerSlot;
|
|
163
|
+
footerSlot;
|
|
164
|
+
closeSlot;
|
|
165
|
+
// Counts `dialog.close()` calls WE initiate whose async `close` event is still
|
|
166
|
+
// pending, and is decremented as each arrives. The native `close` event fires
|
|
167
|
+
// from a queued task, so a synchronous guard can't span it, and a boolean
|
|
168
|
+
// can't span a BURST (two re-parents in one tick queue two closes; the first
|
|
169
|
+
// event would clear the flag and the second would be misread as out-of-band).
|
|
170
|
+
// The counter lets the `close` handler tell our own closes (ignore) from an
|
|
171
|
+
// out-of-band one (`<form method="dialog">`, a consumer calling the shadow
|
|
172
|
+
// dialog's close(), bfcache), which it must reconcile.
|
|
173
|
+
#programmaticCloses = 0;
|
|
174
|
+
// The intended (uncontrolled) open state, held in JS so it survives a
|
|
175
|
+
// disconnect → reconnect (a DOM move / re-parent). disconnectedCallback
|
|
176
|
+
// force-closes the native dialog to free the top layer, but must not lose
|
|
177
|
+
// whether the user had it open — otherwise a re-parented open dialog would
|
|
178
|
+
// vanish (trigger-opened) or a re-parented closed one would pop back (a stale
|
|
179
|
+
// `default-state="open"` re-seeding). Only ever reflects genuine open/close
|
|
180
|
+
// intent, never the disconnect teardown.
|
|
181
|
+
#wantOpen = false;
|
|
182
|
+
// default-state is read ONCE (the first connect), never again — re-seeding on
|
|
183
|
+
// every connect would reopen a dialog the user has since closed.
|
|
184
|
+
#seeded = false;
|
|
185
|
+
// Whether the current pointer press began on the backdrop (the dialog element
|
|
186
|
+
// itself), so a press-inside → release-on-backdrop drag (text selection) is
|
|
187
|
+
// NOT read as a backdrop dismiss.
|
|
188
|
+
#pressedBackdrop = false;
|
|
189
|
+
// The document click listener wired for the `name`-trigger convenience, kept so
|
|
190
|
+
// it can be torn down (and re-wired when `name` / controlled-ness changes).
|
|
191
|
+
#triggerListener;
|
|
192
|
+
constructor() {
|
|
193
|
+
super();
|
|
194
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
195
|
+
const style = document.createElement("style");
|
|
196
|
+
style.textContent = SHADOW_STYLE;
|
|
197
|
+
this.dialog = document.createElement("dialog");
|
|
198
|
+
this.dialog.setAttribute("part", "dialog");
|
|
199
|
+
this.dialog.tabIndex = -1;
|
|
200
|
+
this.closeSlot = document.createElement("slot");
|
|
201
|
+
this.closeSlot.name = "close";
|
|
202
|
+
this.closeSlot.setAttribute("part", "close");
|
|
203
|
+
this.headerSlot = document.createElement("slot");
|
|
204
|
+
this.headerSlot.name = "header";
|
|
205
|
+
this.headerSlot.setAttribute("part", "header");
|
|
206
|
+
const bodySlot = document.createElement("slot");
|
|
207
|
+
bodySlot.setAttribute("part", "body");
|
|
208
|
+
this.footerSlot = document.createElement("slot");
|
|
209
|
+
this.footerSlot.name = "footer";
|
|
210
|
+
this.footerSlot.setAttribute("part", "footer");
|
|
211
|
+
for (const slot of [this.headerSlot, this.footerSlot, this.closeSlot]) {
|
|
212
|
+
slot.addEventListener("slotchange", () => {
|
|
213
|
+
const has = slot.assignedNodes().length > 0;
|
|
214
|
+
slot.classList.toggle("has-content", has);
|
|
215
|
+
if (slot === this.closeSlot) this.dialog.classList.toggle("has-close", has);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
this.headerSlot.addEventListener("slotchange", () => this.#syncAccessibleName());
|
|
219
|
+
this.dialog.append(this.headerSlot, bodySlot, this.footerSlot, this.closeSlot);
|
|
220
|
+
shadow.append(style, this.dialog);
|
|
221
|
+
this.dialog.addEventListener("cancel", (e) => {
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
if (!this.#persistent) this.requestClose();
|
|
224
|
+
});
|
|
225
|
+
this.dialog.addEventListener("pointerdown", (e) => {
|
|
226
|
+
this.#pressedBackdrop = e.target === this.dialog;
|
|
227
|
+
});
|
|
228
|
+
this.dialog.addEventListener("click", (e) => {
|
|
229
|
+
if (this.#persistent) return;
|
|
230
|
+
if (this.#pressedBackdrop && e.target === this.dialog) this.requestClose();
|
|
231
|
+
});
|
|
232
|
+
this.dialog.addEventListener("close", () => {
|
|
233
|
+
if (this.#programmaticCloses > 0) {
|
|
234
|
+
this.#programmaticCloses--;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (this.#controlled) this.#reflect(parseOpenState(this.getAttribute("state")));
|
|
238
|
+
else this.#wantOpen = false;
|
|
239
|
+
});
|
|
240
|
+
this.addEventListener(CLOSE_TRIGGER, () => this.requestClose());
|
|
241
|
+
}
|
|
242
|
+
connectedCallback() {
|
|
243
|
+
if (this.#controlled) {
|
|
244
|
+
this.#reflect(parseOpenState(this.getAttribute("state")));
|
|
245
|
+
} else {
|
|
246
|
+
if (!this.#seeded) {
|
|
247
|
+
if (parseOpenState(this.getAttribute("default-state")) === "open") this.#wantOpen = true;
|
|
248
|
+
this.#seeded = true;
|
|
249
|
+
}
|
|
250
|
+
if (this.#wantOpen) this.#open();
|
|
251
|
+
}
|
|
252
|
+
this.#syncTriggerListener();
|
|
253
|
+
this.#syncAccessibleName();
|
|
254
|
+
}
|
|
255
|
+
disconnectedCallback() {
|
|
256
|
+
this.#teardownTriggerListener();
|
|
257
|
+
if (this.dialog.open) {
|
|
258
|
+
this.#programmaticCloses++;
|
|
259
|
+
this.dialog.close();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
attributeChangedCallback(name) {
|
|
263
|
+
if (name === "state") {
|
|
264
|
+
if (this.#controlled) this.#reflect(parseOpenState(this.getAttribute("state")));
|
|
265
|
+
this.#syncTriggerListener();
|
|
266
|
+
} else if (name === "name") {
|
|
267
|
+
this.#syncTriggerListener();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/** Controlled mode: the `state` attribute is present and owns the open state. */
|
|
271
|
+
get #controlled() {
|
|
272
|
+
return this.hasAttribute("state");
|
|
273
|
+
}
|
|
274
|
+
/** Light-dismiss (Esc + backdrop) disabled — the dialog closes only via
|
|
275
|
+
* explicit controls (close button / programmatic). */
|
|
276
|
+
get #persistent() {
|
|
277
|
+
return this.hasAttribute("persistent");
|
|
278
|
+
}
|
|
279
|
+
/** The currently *applied* open state (read from the native dialog — the
|
|
280
|
+
* source of truth for what's painted). */
|
|
281
|
+
get #current() {
|
|
282
|
+
return this.dialog.open ? "open" : "closed";
|
|
283
|
+
}
|
|
284
|
+
/** Public imperative API — mirrors `<dialog>`. `show()` requests open,
|
|
285
|
+
* `close()` requests close; both honor the state contract. */
|
|
286
|
+
show() {
|
|
287
|
+
this.requestOpen();
|
|
288
|
+
}
|
|
289
|
+
close() {
|
|
290
|
+
this.requestClose();
|
|
291
|
+
}
|
|
292
|
+
get open() {
|
|
293
|
+
return this.dialog.open;
|
|
294
|
+
}
|
|
295
|
+
/** Announce a requested OPEN (cancelable, before applying), then — uncontrolled
|
|
296
|
+
* and un-vetoed — apply it. Controlled: never self-apply. */
|
|
297
|
+
requestOpen() {
|
|
298
|
+
if (this.#current === "open") return;
|
|
299
|
+
const ok = this.#announce("open");
|
|
300
|
+
if (this.#controlled) return;
|
|
301
|
+
if (ok) this.#open();
|
|
302
|
+
}
|
|
303
|
+
/** Announce a requested CLOSE (cancelable, before applying), then — uncontrolled
|
|
304
|
+
* and un-vetoed — apply it. Controlled: never self-apply. */
|
|
305
|
+
requestClose() {
|
|
306
|
+
if (this.#current === "closed") return;
|
|
307
|
+
const ok = this.#announce("closed");
|
|
308
|
+
if (this.#controlled) return;
|
|
309
|
+
if (ok) this.#close();
|
|
310
|
+
}
|
|
311
|
+
/** Dispatch the cancelable `statechange` for a requested transition. Returns
|
|
312
|
+
* false if a handler vetoed it. */
|
|
313
|
+
#announce(next) {
|
|
314
|
+
return this.dispatchEvent(
|
|
315
|
+
new CustomEvent("statechange", {
|
|
316
|
+
cancelable: true,
|
|
317
|
+
detail: { next, prev: this.#current }
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
/** Reflect a controlled `state` onto the native dialog (idempotent). No
|
|
322
|
+
* `statechange` — this is the consumer's own command, not a user gesture. */
|
|
323
|
+
#reflect(state) {
|
|
324
|
+
if (state === "open") this.#open();
|
|
325
|
+
else this.#close();
|
|
326
|
+
}
|
|
327
|
+
/** Raw open: show the modal dialog. Records the open intent (survives a
|
|
328
|
+
* disconnect). Guarded against a not-connected / already-open native element
|
|
329
|
+
* (both throw / no-op). */
|
|
330
|
+
#open() {
|
|
331
|
+
this.#wantOpen = true;
|
|
332
|
+
if (this.dialog.open || !this.isConnected) return;
|
|
333
|
+
this.dialog.showModal();
|
|
334
|
+
if (!this.querySelector("[autofocus]")) this.dialog.focus();
|
|
335
|
+
}
|
|
336
|
+
/** Raw close. Records the closed intent and counts the pending `close` event as
|
|
337
|
+
* ours so the listener doesn't reconcile it. (The disconnect teardown closes
|
|
338
|
+
* the native dialog directly, NOT through here, to preserve `#wantOpen`.) */
|
|
339
|
+
#close() {
|
|
340
|
+
this.#wantOpen = false;
|
|
341
|
+
if (!this.dialog.open) return;
|
|
342
|
+
this.#programmaticCloses++;
|
|
343
|
+
this.dialog.close();
|
|
344
|
+
}
|
|
345
|
+
/** Publish the header text as the shadow dialog's accessible name (a-input's
|
|
346
|
+
* label-mirror pattern). Shadow-internal — not a host / light-DOM write. */
|
|
347
|
+
#syncAccessibleName() {
|
|
348
|
+
const text = this.headerSlot.assignedNodes().map((n) => n.textContent ?? "").join(" ").trim();
|
|
349
|
+
if (text) this.dialog.setAttribute("aria-label", text);
|
|
350
|
+
else this.dialog.removeAttribute("aria-label");
|
|
351
|
+
}
|
|
352
|
+
// --- `name`-trigger convenience (uncontrolled only) ---
|
|
353
|
+
#syncTriggerListener() {
|
|
354
|
+
const wanted = !this.#controlled && !!this.getAttribute("name");
|
|
355
|
+
if (wanted && !this.#triggerListener) this.#wireTriggerListener();
|
|
356
|
+
else if (!wanted && this.#triggerListener) this.#teardownTriggerListener();
|
|
357
|
+
}
|
|
358
|
+
#wireTriggerListener() {
|
|
359
|
+
this.#triggerListener = (e) => {
|
|
360
|
+
const name = this.getAttribute("name");
|
|
361
|
+
if (!name) return;
|
|
362
|
+
const t = e.target;
|
|
363
|
+
if (!(t instanceof Element)) return;
|
|
364
|
+
if (t.closest(`[data-dialog-open="${CSS.escape(name)}"]`)) this.requestOpen();
|
|
365
|
+
else if (t.closest(`[data-dialog-close="${CSS.escape(name)}"]`)) this.requestClose();
|
|
366
|
+
};
|
|
367
|
+
this.doc.addEventListener("click", this.#triggerListener);
|
|
368
|
+
}
|
|
369
|
+
#teardownTriggerListener() {
|
|
370
|
+
if (this.#triggerListener) {
|
|
371
|
+
this.doc.removeEventListener("click", this.#triggerListener);
|
|
372
|
+
this.#triggerListener = void 0;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function register_a_dialog() {
|
|
377
|
+
if (typeof customElements === "undefined") return;
|
|
378
|
+
if (!customElements.get("a-dialog")) {
|
|
379
|
+
customElements.define("a-dialog", ADialogElement);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
register_a_dialog();
|
|
383
|
+
export {
|
|
384
|
+
ADialogElement,
|
|
385
|
+
register_a_dialog
|
|
386
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HTMLElementBase } from "../anta_helpers";
|
|
1
|
+
import { HTMLElementBase, parseOpenState } from "../anta_helpers";
|
|
2
2
|
import "./a-expander.css";
|
|
3
3
|
const ANIM_MS = 200;
|
|
4
4
|
const SUMMARY_TYPE_SCALE = {
|
|
@@ -139,7 +139,7 @@ const SHADOW_STYLE = `
|
|
|
139
139
|
transition-behavior: allow-discrete;
|
|
140
140
|
}
|
|
141
141
|
`;
|
|
142
|
-
const parseState =
|
|
142
|
+
const parseState = parseOpenState;
|
|
143
143
|
class AExpanderElement extends HTMLElementBase {
|
|
144
144
|
static observedAttributes = ["state", "disabled", "round"];
|
|
145
145
|
summary;
|
|
@@ -94,6 +94,7 @@ const SHADOW_STYLE = `
|
|
|
94
94
|
padding-inline: 1px;
|
|
95
95
|
border-radius: 2px;
|
|
96
96
|
text-align: center;
|
|
97
|
+
white-space: nowrap;
|
|
97
98
|
outline: none;
|
|
98
99
|
cursor: default;
|
|
99
100
|
font-variant-numeric: tabular-nums;
|
|
@@ -102,7 +103,7 @@ const SHADOW_STYLE = `
|
|
|
102
103
|
.seg[inputmode="numeric"] {
|
|
103
104
|
/* Fixed to two tabular digits so the box never reflows: neither the narrower
|
|
104
105
|
placeholder dashes nor a mid-entry single digit changes the field width. */
|
|
105
|
-
width: calc(2ch +
|
|
106
|
+
width: calc(2ch + 4px);
|
|
106
107
|
}
|
|
107
108
|
.seg:focus {
|
|
108
109
|
background: var(--input-time-seg-focus-bg);
|
|
@@ -110,8 +110,11 @@ export declare class AMenuElement extends HTMLElementBase {
|
|
|
110
110
|
* the seat target — the cursor lands on the first real option — but they stay
|
|
111
111
|
* arrow-reachable. */
|
|
112
112
|
private resetActive;
|
|
113
|
-
/** Combobox arrow /
|
|
114
|
-
* key (so `handleKey` stops).
|
|
113
|
+
/** Combobox arrow / PageUp / PageDown / Enter handling; returns true if it
|
|
114
|
+
* consumed the key (so `handleKey` stops). Home / End are deliberately NOT
|
|
115
|
+
* handled: focus is in a text field, so they must reach it for caret movement
|
|
116
|
+
* (line start/end); PageUp / PageDown jump the option list to first / last
|
|
117
|
+
* instead. Any other key falls through to the input (typing). */
|
|
115
118
|
private handleComboKey;
|
|
116
119
|
/** Public imperative API. Routes through the same intent path as the
|
|
117
120
|
* triggers, so it emits `statechange` and respects controlled mode. */
|
package/dist/elements/a-menu.js
CHANGED
|
@@ -451,11 +451,22 @@ class AMenuElement extends HTMLElementBase {
|
|
|
451
451
|
el.style.setProperty("--fade-bottom", bottom ? "14px" : "0px");
|
|
452
452
|
}
|
|
453
453
|
/* ------------------------------ combobox mode ------------------------------ */
|
|
454
|
-
/**
|
|
454
|
+
/** Combobox-anchor mode: the trigger anchor ITSELF is the filter field (it
|
|
455
|
+
* carries `[data-menu-search]`), as in `InputAutocomplete` where an editable
|
|
456
|
+
* `<a-input>` is the always-visible field the menu hangs under. null for the
|
|
457
|
+
* normal case — `Select`/`SelectFaceted` slot the field as a child, so their
|
|
458
|
+
* anchor never carries the marker and this stays inert for them. */
|
|
459
|
+
get #comboAnchor() {
|
|
460
|
+
const a = this.triggerAnchor;
|
|
461
|
+
return a?.matches("[data-menu-search]") ? a : null;
|
|
462
|
+
}
|
|
463
|
+
/** The filter field of THIS menu, or null. A slotted child (never a submenu's)
|
|
464
|
+
* wins; else the anchor when it's the field (combobox-anchor mode). Its
|
|
455
465
|
* presence switches the menu to the combobox keyboard. */
|
|
456
466
|
get #searchField() {
|
|
457
467
|
const el = this.querySelector("[data-menu-search]");
|
|
458
|
-
|
|
468
|
+
if (el && el.closest("a-menu") === this) return el;
|
|
469
|
+
return this.#comboAnchor;
|
|
459
470
|
}
|
|
460
471
|
/** Move the combobox cursor. Sets the item's `active` **property** (off-DOM
|
|
461
472
|
* `:state(active)`, no attribute churn) for the highlight, and REPORTS the
|
|
@@ -481,11 +492,15 @@ class AMenuElement extends HTMLElementBase {
|
|
|
481
492
|
resetActive() {
|
|
482
493
|
const q = this.#searchField?.value;
|
|
483
494
|
if (!q || !q.trim()) return this.setActive(null);
|
|
495
|
+
if (this.#comboAnchor) return this.setActive(null);
|
|
484
496
|
const items = this.focusableItems();
|
|
485
497
|
this.setActive(items.find((it) => !it.hasAttribute("data-menu-skip-active")) ?? items[0] ?? null);
|
|
486
498
|
}
|
|
487
|
-
/** Combobox arrow /
|
|
488
|
-
* key (so `handleKey` stops).
|
|
499
|
+
/** Combobox arrow / PageUp / PageDown / Enter handling; returns true if it
|
|
500
|
+
* consumed the key (so `handleKey` stops). Home / End are deliberately NOT
|
|
501
|
+
* handled: focus is in a text field, so they must reach it for caret movement
|
|
502
|
+
* (line start/end); PageUp / PageDown jump the option list to first / last
|
|
503
|
+
* instead. Any other key falls through to the input (typing). */
|
|
489
504
|
handleComboKey(e) {
|
|
490
505
|
const items = this.focusableItems();
|
|
491
506
|
const idx = this.activeItem ? items.indexOf(this.activeItem) : -1;
|
|
@@ -498,11 +513,11 @@ class AMenuElement extends HTMLElementBase {
|
|
|
498
513
|
e.preventDefault();
|
|
499
514
|
this.setActive(items[idx <= 0 ? items.length - 1 : idx - 1] ?? null);
|
|
500
515
|
return true;
|
|
501
|
-
case "
|
|
516
|
+
case "PageUp":
|
|
502
517
|
e.preventDefault();
|
|
503
518
|
this.setActive(items[0] ?? null);
|
|
504
519
|
return true;
|
|
505
|
-
case "
|
|
520
|
+
case "PageDown":
|
|
506
521
|
e.preventDefault();
|
|
507
522
|
this.setActive(items[items.length - 1] ?? null);
|
|
508
523
|
return true;
|
|
@@ -848,6 +863,10 @@ class AMenuElement extends HTMLElementBase {
|
|
|
848
863
|
return;
|
|
849
864
|
}
|
|
850
865
|
if (e.key === "Tab") {
|
|
866
|
+
if (this.#comboAnchor) {
|
|
867
|
+
this.requestClose(e);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
851
870
|
const f = this.focusables();
|
|
852
871
|
if (!f.length) return;
|
|
853
872
|
e.preventDefault();
|
|
@@ -969,6 +988,12 @@ class AMenuElement extends HTMLElementBase {
|
|
|
969
988
|
anchor.removeEventListener("contextmenu", onContext);
|
|
970
989
|
this.listening = false;
|
|
971
990
|
};
|
|
991
|
+
} else if (this.#comboAnchor) {
|
|
992
|
+
this.teardown = () => {
|
|
993
|
+
this.listening = false;
|
|
994
|
+
};
|
|
995
|
+
this.listening = true;
|
|
996
|
+
return;
|
|
972
997
|
} else {
|
|
973
998
|
const onClick = (e) => {
|
|
974
999
|
const viaKeyboard = e.detail === 0;
|