@celestia-island/hikari 0.55.294 → 0.55.295
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
CHANGED
|
@@ -546,3 +546,118 @@ describe("HkModal chrome / content split", () => {
|
|
|
546
546
|
});
|
|
547
547
|
});
|
|
548
548
|
});
|
|
549
|
+
|
|
550
|
+
describe("HkModal Escape precedence", () => {
|
|
551
|
+
it("lets a nested control own Escape while a bare Escape still closes", async () => {
|
|
552
|
+
const container = document.createElement("div");
|
|
553
|
+
document.body.appendChild(container);
|
|
554
|
+
containers.push(container);
|
|
555
|
+
|
|
556
|
+
const open = ref(true);
|
|
557
|
+
const emitted: boolean[] = [];
|
|
558
|
+
const Wrapper = defineComponent({
|
|
559
|
+
setup() {
|
|
560
|
+
return () =>
|
|
561
|
+
h(HkModal, {
|
|
562
|
+
modelValue: open.value,
|
|
563
|
+
closable: true,
|
|
564
|
+
title: "Escape precedence",
|
|
565
|
+
"onUpdate:modelValue": (v: boolean) => { emitted.push(v); open.value = v; },
|
|
566
|
+
}, {
|
|
567
|
+
default: () => h("div", {
|
|
568
|
+
class: "escape-owner",
|
|
569
|
+
onKeydown: (e: KeyboardEvent) => {
|
|
570
|
+
if (e.key === "Escape") e.preventDefault();
|
|
571
|
+
},
|
|
572
|
+
}, "nested control"),
|
|
573
|
+
});
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
const app = createApp(Wrapper);
|
|
577
|
+
mounts.push(app);
|
|
578
|
+
app.mount(container);
|
|
579
|
+
|
|
580
|
+
await vi.waitFor(() => {
|
|
581
|
+
expect(document.body.querySelector(".escape-owner")).not.toBeNull();
|
|
582
|
+
});
|
|
583
|
+
const owner = document.body.querySelector<HTMLElement>(".escape-owner")!;
|
|
584
|
+
|
|
585
|
+
// The nested control consumes Escape: the window must stay open.
|
|
586
|
+
owner.dispatchEvent(
|
|
587
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }),
|
|
588
|
+
);
|
|
589
|
+
await nextTick();
|
|
590
|
+
expect(emitted, "a consumed Escape must not close the window").toEqual([]);
|
|
591
|
+
expect(open.value).toBe(true);
|
|
592
|
+
|
|
593
|
+
// A bare Escape (nothing below consumed it) still closes.
|
|
594
|
+
const content = document.body.querySelector<HTMLElement>(".hk-modal-content")!;
|
|
595
|
+
content.dispatchEvent(
|
|
596
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }),
|
|
597
|
+
);
|
|
598
|
+
await nextTick();
|
|
599
|
+
expect(emitted).toEqual([false]);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("keeps a consumed Escape from reaching page-level Escape listeners", async () => {
|
|
603
|
+
// One press must mean one action: before the precedence rule a closable
|
|
604
|
+
// window swallowed every Escape at its root, so a handled press travelling
|
|
605
|
+
// on would hand the same key to a page-level listener (a revealed strip, a
|
|
606
|
+
// pointer-reorder cancel) while the nested control also acted on it.
|
|
607
|
+
const container = document.createElement("div");
|
|
608
|
+
document.body.appendChild(container);
|
|
609
|
+
containers.push(container);
|
|
610
|
+
const open = ref(true);
|
|
611
|
+
const emitted: boolean[] = [];
|
|
612
|
+
const Wrapper = defineComponent({
|
|
613
|
+
setup() {
|
|
614
|
+
return () =>
|
|
615
|
+
h(HkModal, {
|
|
616
|
+
modelValue: open.value,
|
|
617
|
+
closable: true,
|
|
618
|
+
title: "Escape propagation",
|
|
619
|
+
"onUpdate:modelValue": (v: boolean) => { emitted.push(v); open.value = v; },
|
|
620
|
+
}, {
|
|
621
|
+
default: () => h("div", {
|
|
622
|
+
class: "escape-owner",
|
|
623
|
+
onKeydown: (e: KeyboardEvent) => {
|
|
624
|
+
if (e.key === "Escape") e.preventDefault();
|
|
625
|
+
},
|
|
626
|
+
}, "nested control"),
|
|
627
|
+
});
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
const app = createApp(Wrapper);
|
|
631
|
+
mounts.push(app);
|
|
632
|
+
app.mount(container);
|
|
633
|
+
await vi.waitFor(() => {
|
|
634
|
+
expect(document.body.querySelector(".escape-owner")).not.toBeNull();
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
const pageHits: string[] = [];
|
|
638
|
+
const pageListener = (e: KeyboardEvent) => {
|
|
639
|
+
if (e.key === "Escape") pageHits.push("escape");
|
|
640
|
+
};
|
|
641
|
+
document.addEventListener("keydown", pageListener);
|
|
642
|
+
try {
|
|
643
|
+
const owner = document.body.querySelector<HTMLElement>(".escape-owner")!;
|
|
644
|
+
owner.dispatchEvent(
|
|
645
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }),
|
|
646
|
+
);
|
|
647
|
+
await nextTick();
|
|
648
|
+
expect(emitted, "a consumed Escape must not close the window").toEqual([]);
|
|
649
|
+
expect(pageHits, "a consumed Escape must not reach page-level listeners").toEqual([]);
|
|
650
|
+
|
|
651
|
+
// A bare Escape closes AND is still swallowed (the pre-existing rule).
|
|
652
|
+
const content = document.body.querySelector<HTMLElement>(".hk-modal-content")!;
|
|
653
|
+
content.dispatchEvent(
|
|
654
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }),
|
|
655
|
+
);
|
|
656
|
+
await nextTick();
|
|
657
|
+
expect(emitted).toEqual([false]);
|
|
658
|
+
expect(pageHits, "a closing Escape is swallowed by the window").toEqual([]);
|
|
659
|
+
} finally {
|
|
660
|
+
document.removeEventListener("keydown", pageListener);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
});
|
|
@@ -489,6 +489,23 @@ export default defineComponent({
|
|
|
489
489
|
|
|
490
490
|
function onKeydown(e: KeyboardEvent) {
|
|
491
491
|
if (e.key === "Escape") {
|
|
492
|
+
// Cross-component precedence: a nested control that already consumed
|
|
493
|
+
// Escape (the range ribbon's "Escape resets the range" is the live
|
|
494
|
+
// case) owns the key, so the window must not close underneath it.
|
|
495
|
+
// `defaultPrevented` is the DOM-canonical "I handled this" signal for
|
|
496
|
+
// inline, preventDefault-only consumers — the package's pickers and
|
|
497
|
+
// panels instead stopPropagation (or teleport out of this subtree)
|
|
498
|
+
// and therefore never reach this handler either way.
|
|
499
|
+
//
|
|
500
|
+
// Keep swallowing the press here (`stopPropagation`) like a bare
|
|
501
|
+
// Escape does: before this precedence rule, a closable window consumed
|
|
502
|
+
// every Escape at this root, so letting a handled one travel on would
|
|
503
|
+
// hand the same press to page-level Escape listeners (a revealed strip,
|
|
504
|
+
// a pointer-reorder cancel) and produce one-press-two-actions.
|
|
505
|
+
if (e.defaultPrevented) {
|
|
506
|
+
if (props.closable) e.stopPropagation();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
492
509
|
if (props.closable) {
|
|
493
510
|
e.preventDefault();
|
|
494
511
|
e.stopPropagation();
|