@celestia-island/hikari 0.55.135 → 0.55.137
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/HkDraggableList.scss +12 -0
- package/src/components/HkDraggableList.styles.test.ts +64 -0
- package/src/components/HkDraggableList.test.tsx +182 -0
- package/src/components/HkDraggableList.tsx +16 -1
- package/src/components/HkSelect.test.tsx +92 -0
- package/src/components/HkSelect.tsx +22 -1
package/package.json
CHANGED
|
@@ -71,6 +71,18 @@
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/* A list where every row is locked can never reorder, so the handle lane
|
|
75
|
+
* has nothing to offer — collapse it instead of reserving an empty gutter
|
|
76
|
+
* that reads as "missing grips" (user report 2026-10-11: chest's 2FA
|
|
77
|
+
* factors list). Mixed lists are unaffected: they keep the lane on every
|
|
78
|
+
* row (locked ones render it hidden via [data-hidden]) so the grip column
|
|
79
|
+
* stays aligned. */
|
|
80
|
+
.hk-draggable-list[data-all-locked] {
|
|
81
|
+
.hk-draggable-list-handle {
|
|
82
|
+
display: none;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
.hk-draggable-list-content {
|
|
75
87
|
flex: 1;
|
|
76
88
|
min-width: 0;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
import { compile } from "sass";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sheet contract for the locked-row handle lane (DOM twin:
|
|
9
|
+
* HkDraggableList.test.tsx).
|
|
10
|
+
*
|
|
11
|
+
* - a locked row HIDES its grip but keeps the 20px lane reserved
|
|
12
|
+
* (`visibility: hidden` under [data-hidden]) — mixed lists keep the
|
|
13
|
+
* grip column aligned;
|
|
14
|
+
* - a list whose every row is locked (root [data-all-locked]) can never
|
|
15
|
+
* reorder, so the lane collapses outright (`display: none`) instead of
|
|
16
|
+
* reserving an empty gutter that reads as "missing grips" (user report
|
|
17
|
+
* 2026-10-11 on chest's 2FA factors list).
|
|
18
|
+
*
|
|
19
|
+
* Assertions run on the COMPILED sheet, not the source text, so a
|
|
20
|
+
* declaration cannot hide behind SCSS nesting, and comments are stripped
|
|
21
|
+
* first so a commented-out guard cannot keep a hollow test green.
|
|
22
|
+
*/
|
|
23
|
+
const componentDir = resolve(dirname(fileURLToPath(import.meta.url)));
|
|
24
|
+
|
|
25
|
+
function listCss(): string {
|
|
26
|
+
const file = resolve(componentDir, "HkDraggableList.scss");
|
|
27
|
+
return compile(file, { style: "expanded", loadPaths: [componentDir] }).css.replace(
|
|
28
|
+
/\/\*[\s\S]*?\*\//g,
|
|
29
|
+
"",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ruleBody(css: string, selector: string): string | null {
|
|
34
|
+
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
35
|
+
const m = css.match(new RegExp(`${escaped}\\s*\\{([^}]*)\\}`));
|
|
36
|
+
return m ? m[1]! : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("HkDraggableList handle-lane sheet", () => {
|
|
40
|
+
const css = listCss();
|
|
41
|
+
|
|
42
|
+
it("compiles", () => {
|
|
43
|
+
expect(css.length).toBeGreaterThan(0);
|
|
44
|
+
// Sanity anchors so a renamed file cannot hollow this guard.
|
|
45
|
+
expect(css).toContain(".hk-draggable-list-item");
|
|
46
|
+
expect(css).toContain(".hk-draggable-list-handle");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("keeps a locked row's lane reserved — hidden, not removed", () => {
|
|
50
|
+
const body = ruleBody(css, ".hk-draggable-list-handle[data-hidden]");
|
|
51
|
+
expect(body, "the [data-hidden] rule must exist").not.toBeNull();
|
|
52
|
+
expect(body).toContain("visibility: hidden");
|
|
53
|
+
expect(body).not.toContain("display: none");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("collapses the lane entirely when every row is locked", () => {
|
|
57
|
+
const body = ruleBody(
|
|
58
|
+
css,
|
|
59
|
+
".hk-draggable-list[data-all-locked] .hk-draggable-list-handle",
|
|
60
|
+
);
|
|
61
|
+
expect(body, "the [data-all-locked] collapse rule must exist").not.toBeNull();
|
|
62
|
+
expect(body).toContain("display: none");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createApp, h, nextTick, ref } from "vue";
|
|
3
|
+
|
|
4
|
+
import HDraggableList from "./HkDraggableList";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DOM contract for the locked-row handle lane.
|
|
8
|
+
*
|
|
9
|
+
* Every row renders a 20px handle lane so the grip column stays aligned
|
|
10
|
+
* across draggable and locked rows; a locked row hides its grip
|
|
11
|
+
* (`data-hidden` → visibility: hidden — reserved, not removed). When
|
|
12
|
+
* EVERY row is locked the list can never reorder at all, and an
|
|
13
|
+
* all-hidden lane reads as "missing grips" rather than "locked" (user
|
|
14
|
+
* report 2026-10-11 on chest's 2FA factors list): the root then carries
|
|
15
|
+
* `data-all-locked` and the sheet collapses the lane entirely (the sheet
|
|
16
|
+
* twin of this contract: HkDraggableList.styles.test.ts). A custom
|
|
17
|
+
* `handle` slot rides the same lane, so it collapses with it.
|
|
18
|
+
*
|
|
19
|
+
* (Repo test convention: raw createApp + document queries, no
|
|
20
|
+
* @vue/test-utils.)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
while (mounts.length) {
|
|
27
|
+
const { app, container } = mounts.pop()!;
|
|
28
|
+
app.unmount();
|
|
29
|
+
container.remove();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function mountList(items: string[], lockedKeys: string[]): HTMLElement {
|
|
34
|
+
const container = document.createElement("div");
|
|
35
|
+
document.body.appendChild(container);
|
|
36
|
+
const app = createApp({
|
|
37
|
+
render: () =>
|
|
38
|
+
h(
|
|
39
|
+
HDraggableList,
|
|
40
|
+
{ items: items.map((key) => ({ key })), lockedKeys },
|
|
41
|
+
{
|
|
42
|
+
item: ({ item }: { item: { key: string } }) =>
|
|
43
|
+
h("span", { class: "row-body" }, item.key),
|
|
44
|
+
},
|
|
45
|
+
),
|
|
46
|
+
});
|
|
47
|
+
app.mount(container);
|
|
48
|
+
mounts.push({ app, container });
|
|
49
|
+
return container.querySelector(".hk-draggable-list") as HTMLElement;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handlesOf(root: HTMLElement): HTMLElement[] {
|
|
53
|
+
return Array.from(root.querySelectorAll(".hk-draggable-list-handle"));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("HkDraggableList locked-lane contract", () => {
|
|
57
|
+
it("hides a locked row's grip but keeps the lane in mixed lists", () => {
|
|
58
|
+
const root = mountList(["a", "b"], ["b"]);
|
|
59
|
+
|
|
60
|
+
expect(root.hasAttribute("data-all-locked")).toBe(false);
|
|
61
|
+
const handles = handlesOf(root);
|
|
62
|
+
expect(handles).toHaveLength(2);
|
|
63
|
+
expect(handles[0]!.hasAttribute("data-hidden")).toBe(false);
|
|
64
|
+
expect(handles[1]!.hasAttribute("data-hidden")).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("marks an all-locked list so the sheet can collapse the lane", () => {
|
|
68
|
+
const root = mountList(["a", "b"], ["a", "b"]);
|
|
69
|
+
|
|
70
|
+
expect(root.hasAttribute("data-all-locked")).toBe(true);
|
|
71
|
+
// Handles stay in the DOM (consumers count them — chest's 2FA
|
|
72
|
+
// contract test does); the collapse is presentational, carried by
|
|
73
|
+
// the sheet, so the marker is the load-bearing part here.
|
|
74
|
+
const handles = handlesOf(root);
|
|
75
|
+
expect(handles).toHaveLength(2);
|
|
76
|
+
expect(handles.every((handle) => handle.hasAttribute("data-hidden"))).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("flips the marker reactively as the last draggable row locks", async () => {
|
|
80
|
+
const lockedKeys = ref<string[]>([]);
|
|
81
|
+
const container = document.createElement("div");
|
|
82
|
+
document.body.appendChild(container);
|
|
83
|
+
const app = createApp({
|
|
84
|
+
render: () =>
|
|
85
|
+
h(
|
|
86
|
+
HDraggableList,
|
|
87
|
+
{ items: [{ key: "a" }, { key: "b" }], lockedKeys: lockedKeys.value },
|
|
88
|
+
{
|
|
89
|
+
item: ({ item }: { item: { key: string } }) =>
|
|
90
|
+
h("span", { class: "row-body" }, item.key),
|
|
91
|
+
},
|
|
92
|
+
),
|
|
93
|
+
});
|
|
94
|
+
app.mount(container);
|
|
95
|
+
mounts.push({ app, container });
|
|
96
|
+
const root = container.querySelector(".hk-draggable-list") as HTMLElement;
|
|
97
|
+
|
|
98
|
+
expect(root.hasAttribute("data-all-locked")).toBe(false);
|
|
99
|
+
|
|
100
|
+
lockedKeys.value = ["a", "b"];
|
|
101
|
+
await nextTick();
|
|
102
|
+
|
|
103
|
+
expect(root.hasAttribute("data-all-locked")).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("collapses a custom handle slot with the lane in an all-locked list", () => {
|
|
107
|
+
// The handle slot renders INSIDE the lane span, so the collapse rule
|
|
108
|
+
// hides custom grips the same as the default six-dot svg.
|
|
109
|
+
const container = document.createElement("div");
|
|
110
|
+
document.body.appendChild(container);
|
|
111
|
+
const app = createApp({
|
|
112
|
+
render: () =>
|
|
113
|
+
h(
|
|
114
|
+
HDraggableList,
|
|
115
|
+
{ items: [{ key: "a" }], lockedKeys: ["a"] },
|
|
116
|
+
{
|
|
117
|
+
item: ({ item }: { item: { key: string } }) =>
|
|
118
|
+
h("span", { class: "row-body" }, item.key),
|
|
119
|
+
handle: () => h("span", { class: "custom-grip" }, "≡"),
|
|
120
|
+
},
|
|
121
|
+
),
|
|
122
|
+
});
|
|
123
|
+
app.mount(container);
|
|
124
|
+
mounts.push({ app, container });
|
|
125
|
+
|
|
126
|
+
const grip = container.querySelector(".custom-grip") as HTMLElement;
|
|
127
|
+
expect(grip).not.toBeNull();
|
|
128
|
+
// The custom grip rides the lane span that the sheet display:nones.
|
|
129
|
+
expect(grip.closest(".hk-draggable-list-handle")).not.toBeNull();
|
|
130
|
+
expect(grip.closest(".hk-draggable-list")).not.toBeNull();
|
|
131
|
+
expect(
|
|
132
|
+
(grip.closest(".hk-draggable-list") as HTMLElement).hasAttribute("data-all-locked"),
|
|
133
|
+
).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("carries no marker on an empty list", () => {
|
|
137
|
+
const root = mountList([], []);
|
|
138
|
+
|
|
139
|
+
expect(root.hasAttribute("data-all-locked")).toBe(false);
|
|
140
|
+
expect(handlesOf(root)).toHaveLength(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("ignores locked keys that match no row — membership, not counts", () => {
|
|
144
|
+
// Guards against a length-arithmetic refactor
|
|
145
|
+
// (lockedKeys.length >= items.length): TWO phantom keys vs two rows
|
|
146
|
+
// satisfy the count yet lock nothing — the list stays draggable and
|
|
147
|
+
// the lane must stay.
|
|
148
|
+
const phantomOnly = mountList(["a", "b"], ["zzz", "yyy"]);
|
|
149
|
+
expect(phantomOnly.hasAttribute("data-all-locked")).toBe(false);
|
|
150
|
+
|
|
151
|
+
// A superset still marks: every real row IS locked.
|
|
152
|
+
const superset = mountList(["a", "b"], ["zzz", "a", "b"]);
|
|
153
|
+
expect(superset.hasAttribute("data-all-locked")).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("flips the marker reactively as rows are added around a locked one", async () => {
|
|
157
|
+
const items = ref<{ key: string }[]>([{ key: "a" }]);
|
|
158
|
+
const container = document.createElement("div");
|
|
159
|
+
document.body.appendChild(container);
|
|
160
|
+
const app = createApp({
|
|
161
|
+
render: () =>
|
|
162
|
+
h(
|
|
163
|
+
HDraggableList,
|
|
164
|
+
{ items: items.value, lockedKeys: ["a"] },
|
|
165
|
+
{
|
|
166
|
+
item: ({ item }: { item: { key: string } }) =>
|
|
167
|
+
h("span", { class: "row-body" }, item.key),
|
|
168
|
+
},
|
|
169
|
+
),
|
|
170
|
+
});
|
|
171
|
+
app.mount(container);
|
|
172
|
+
mounts.push({ app, container });
|
|
173
|
+
const root = container.querySelector(".hk-draggable-list") as HTMLElement;
|
|
174
|
+
|
|
175
|
+
expect(root.hasAttribute("data-all-locked")).toBe(true);
|
|
176
|
+
|
|
177
|
+
items.value = [{ key: "a" }, { key: "b" }];
|
|
178
|
+
await nextTick();
|
|
179
|
+
|
|
180
|
+
expect(root.hasAttribute("data-all-locked")).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -22,6 +22,10 @@ export default defineComponent({
|
|
|
22
22
|
name: "HkDraggableList",
|
|
23
23
|
props: {
|
|
24
24
|
items: { type: Array as PropType<DragListItem[]>, required: true },
|
|
25
|
+
/** Row keys that may not be dragged. A locked row hides its grip but
|
|
26
|
+
* keeps the handle lane reserved ([data-hidden]); when EVERY row is
|
|
27
|
+
* locked the root carries data-all-locked and the sheet collapses
|
|
28
|
+
* the lane entirely — the list cannot reorder at all. */
|
|
25
29
|
lockedKeys: { type: Array as PropType<string[]>, default: () => [] },
|
|
26
30
|
},
|
|
27
31
|
emits: {
|
|
@@ -198,8 +202,19 @@ export default defineComponent({
|
|
|
198
202
|
return () => {
|
|
199
203
|
void tick.value;
|
|
200
204
|
const s = drag.value;
|
|
205
|
+
// A list whose every row is locked can never reorder (chest's 2FA
|
|
206
|
+
// factor list is the all-locked case), and an all-hidden lane reads
|
|
207
|
+
// as "missing grips" rather than "locked" (user report 2026-10-11).
|
|
208
|
+
// Mark the root so the sheet can collapse the lane outright; mixed
|
|
209
|
+
// lists keep it on every row for grip-column alignment.
|
|
210
|
+
const allLocked =
|
|
211
|
+
props.items.length > 0 && props.items.every((item) => isLocked(item.key));
|
|
201
212
|
return (
|
|
202
|
-
<div
|
|
213
|
+
<div
|
|
214
|
+
class="hk-draggable-list"
|
|
215
|
+
data-dragging={s ? "" : undefined}
|
|
216
|
+
data-all-locked={allLocked || undefined}
|
|
217
|
+
>
|
|
203
218
|
{props.items.map((item, index) => {
|
|
204
219
|
const locked = isLocked(item.key);
|
|
205
220
|
const isSource = s != null && s.fromIndex === index;
|
|
@@ -85,4 +85,96 @@ describe("HSelect mobile sheet", () => {
|
|
|
85
85
|
|
|
86
86
|
app.unmount();
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
it("consumes Escape while the panel is open so hosting dialogs stay put", async () => {
|
|
90
|
+
// The trigger commonly renders inside a dialog that closes on
|
|
91
|
+
// bubbled Escape (HkModal/HkDrawer). One press must close ONLY the
|
|
92
|
+
// dropdown; with the dropdown already closed the key bubbles on so
|
|
93
|
+
// the dialog's own Escape semantics are untouched.
|
|
94
|
+
setViewportWidth(1280);
|
|
95
|
+
const host = document.createElement("div");
|
|
96
|
+
document.body.appendChild(host);
|
|
97
|
+
const dialogKeydowns: KeyboardEvent[] = [];
|
|
98
|
+
host.addEventListener("keydown", (ev) => dialogKeydowns.push(ev as KeyboardEvent));
|
|
99
|
+
|
|
100
|
+
const app = createApp({
|
|
101
|
+
render: () =>
|
|
102
|
+
h("div", { onKeydown: () => {} }, [
|
|
103
|
+
h(HSelect, {
|
|
104
|
+
modelValue: "",
|
|
105
|
+
"onUpdate:modelValue": () => {},
|
|
106
|
+
options: [
|
|
107
|
+
{ value: "a", label: "Option A" },
|
|
108
|
+
{ value: "b", label: "Option B" },
|
|
109
|
+
],
|
|
110
|
+
}),
|
|
111
|
+
]),
|
|
112
|
+
});
|
|
113
|
+
app.mount(host);
|
|
114
|
+
await nextTick();
|
|
115
|
+
|
|
116
|
+
const trigger = host.querySelector<HTMLButtonElement>(".hk-select-trigger")!;
|
|
117
|
+
|
|
118
|
+
// Panel open: Escape closes it AND is consumed.
|
|
119
|
+
trigger.click();
|
|
120
|
+
await nextTick();
|
|
121
|
+
const escapeOpen = new KeyboardEvent("keydown", {
|
|
122
|
+
key: "Escape", bubbles: true, cancelable: true,
|
|
123
|
+
});
|
|
124
|
+
trigger.dispatchEvent(escapeOpen);
|
|
125
|
+
await nextTick();
|
|
126
|
+
expect(escapeOpen.defaultPrevented).toBe(true);
|
|
127
|
+
expect(dialogKeydowns).toHaveLength(0);
|
|
128
|
+
|
|
129
|
+
// Panel closed: the same key bubbles to the hosting dialog.
|
|
130
|
+
const escapeClosed = new KeyboardEvent("keydown", {
|
|
131
|
+
key: "Escape", bubbles: true, cancelable: true,
|
|
132
|
+
});
|
|
133
|
+
trigger.dispatchEvent(escapeClosed);
|
|
134
|
+
await nextTick();
|
|
135
|
+
expect(escapeClosed.defaultPrevented).toBe(false);
|
|
136
|
+
expect(dialogKeydowns).toHaveLength(1);
|
|
137
|
+
|
|
138
|
+
app.unmount();
|
|
139
|
+
host.remove();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("titles the opened surface from panelTitle without a visible label", async () => {
|
|
143
|
+
// Consumers whose visible label lives outside the field (a titled
|
|
144
|
+
// row) still need the mobile sheet header / popout a11y name; an
|
|
145
|
+
// untitled surface registers an unnamed blocking layer on phones.
|
|
146
|
+
setViewportWidth(375);
|
|
147
|
+
const el = document.createElement("div");
|
|
148
|
+
document.body.appendChild(el);
|
|
149
|
+
const app = createApp({
|
|
150
|
+
render: () =>
|
|
151
|
+
h(HSelect, {
|
|
152
|
+
modelValue: "",
|
|
153
|
+
"onUpdate:modelValue": () => {},
|
|
154
|
+
panelTitle: "What this screen shows",
|
|
155
|
+
options: [
|
|
156
|
+
{ value: "a", label: "Option A" },
|
|
157
|
+
{ value: "b", label: "Option B" },
|
|
158
|
+
],
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
app.mount(el);
|
|
162
|
+
await nextTick();
|
|
163
|
+
|
|
164
|
+
// No visible field label rendered above the trigger.
|
|
165
|
+
expect(document.querySelector(".hk-select-label")).toBeNull();
|
|
166
|
+
|
|
167
|
+
const trigger = document.querySelector<HTMLButtonElement>(".hk-select-trigger")!;
|
|
168
|
+
trigger.click();
|
|
169
|
+
await nextTick();
|
|
170
|
+
|
|
171
|
+
const title = document.querySelector<HTMLElement>(".hk-select-sheet-title");
|
|
172
|
+
expect(title?.textContent?.trim()).toBe("What this screen shows");
|
|
173
|
+
// The sheet panel is the a11y-named dialog for the surface.
|
|
174
|
+
const panel = document.querySelector<HTMLElement>(".hk-select-sheet-panel");
|
|
175
|
+
expect(panel?.getAttribute("aria-label")).toBe("What this screen shows");
|
|
176
|
+
|
|
177
|
+
app.unmount();
|
|
178
|
+
el.remove();
|
|
179
|
+
});
|
|
88
180
|
});
|
|
@@ -30,6 +30,15 @@ export default defineComponent({
|
|
|
30
30
|
props: {
|
|
31
31
|
modelValue: { type: String, default: "" },
|
|
32
32
|
label: { type: String, default: undefined },
|
|
33
|
+
/**
|
|
34
|
+
* Title for the OPENED surface only (mobile sheet header, desktop
|
|
35
|
+
* popout's a11y name, popup-manager breadcrumb layer) — without
|
|
36
|
+
* rendering the visible field `label`. Consumers whose visible label
|
|
37
|
+
* lives elsewhere (a titled row around the field) pass this so the
|
|
38
|
+
* surface is not registered untitled; resolution order is
|
|
39
|
+
* panelTitle, then `label`, then empty (the historic behaviour).
|
|
40
|
+
*/
|
|
41
|
+
panelTitle: { type: String, default: undefined },
|
|
33
42
|
placeholder: { type: String, default: "" },
|
|
34
43
|
error: { type: String, default: undefined },
|
|
35
44
|
disabled: { type: Boolean, default: false },
|
|
@@ -100,6 +109,18 @@ export default defineComponent({
|
|
|
100
109
|
normalizedOptions.value.length;
|
|
101
110
|
}
|
|
102
111
|
} else if (e.key === "Escape") {
|
|
112
|
+
// Consume the key ONLY when it actually closed something: the
|
|
113
|
+
// trigger commonly renders inside a dialog surface (HkModal and
|
|
114
|
+
// HkDrawer close on bubbled Escape), and one press must mean one
|
|
115
|
+
// thing — close the dropdown, not the dropdown AND the hosting
|
|
116
|
+
// dialog. The panel surface itself stops propagation for the
|
|
117
|
+
// same reason (HkSelectPanel's onSurfaceEscape); with the panel
|
|
118
|
+
// closed the key bubbles on as before, so the hosting dialog's
|
|
119
|
+
// own Escape semantics are untouched.
|
|
120
|
+
if (isOpen.value) {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
e.stopPropagation();
|
|
123
|
+
}
|
|
103
124
|
isOpen.value = false;
|
|
104
125
|
}
|
|
105
126
|
}
|
|
@@ -182,7 +203,7 @@ export default defineComponent({
|
|
|
182
203
|
open={isOpen.value}
|
|
183
204
|
onUpdate:open={(v: boolean) => { isOpen.value = v; }}
|
|
184
205
|
anchorRef={triggerRef.value ?? null}
|
|
185
|
-
title={props.label ?? ""}
|
|
206
|
+
title={props.panelTitle ?? props.label ?? ""}
|
|
186
207
|
placement="bottom-start"
|
|
187
208
|
offset={4}
|
|
188
209
|
onKeydown={onPopoutKeydown}
|