@celestia-island/hikari 0.55.125 → 0.55.126
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
|
@@ -161,7 +161,18 @@
|
|
|
161
161
|
padding: var(--space-4, 4px);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
// The group is a role="group" WRAPPER carrying the accessible name in
|
|
165
|
+
// aria-label; the visible heading text is the inner label band (hidden
|
|
166
|
+
// from the a11y tree — the wrapper already announced the name).
|
|
164
167
|
.hk-tpl-group {
|
|
168
|
+
// Flex like the sibling rows list, so the parent's row gap keeps
|
|
169
|
+
// applying INSIDE the wrapper (a bare block would drop it to 0).
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
gap: 2px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.hk-tpl-group-label {
|
|
165
176
|
padding: var(--space-6, 6px) var(--space-8, 8px) var(--space-2, 2px);
|
|
166
177
|
font-size: var(--text-2xs, 0.625rem);
|
|
167
178
|
font-weight: 600;
|
|
@@ -92,6 +92,14 @@ describe("HkTemplateField stylesheet contract", () => {
|
|
|
92
92
|
}
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
it("keeps the heading band on the label class, not the group wrapper", () => {
|
|
96
|
+
// The wrapper is a bare layout box; carrying the band's chrome
|
|
97
|
+
// (padding/font-weight) there would pad every row run.
|
|
98
|
+
expect(css).toMatch(/\.hk-tpl-group-label\s*\{[^}]*font-weight:\s*600/s);
|
|
99
|
+
expect(css).not.toMatch(/\.hk-tpl-group\s*\{[^}]*font-weight/s);
|
|
100
|
+
expect(css).not.toMatch(/\.hk-tpl-group\s*\{[^}]*padding/s);
|
|
101
|
+
});
|
|
102
|
+
|
|
95
103
|
it("keeps group headings free of text-transform (the host owns casing)", () => {
|
|
96
104
|
// R2 M13: re-adding `text-transform: uppercase` used to stay green.
|
|
97
105
|
expect(css).not.toMatch(/\.hk-tpl-group[^{]*\{[^}]*text-transform/s);
|
|
@@ -415,30 +415,44 @@ describe("HkTemplateField grouped vocabulary", () => {
|
|
|
415
415
|
{ name: "sha256_email", group: "email" },
|
|
416
416
|
];
|
|
417
417
|
|
|
418
|
-
it("renders one
|
|
418
|
+
it("renders one labeled group wrapper per run in the panel", async () => {
|
|
419
419
|
const el = mountWithTokens(GROUPED);
|
|
420
420
|
await nextTick();
|
|
421
421
|
await openTrigger(el, "{{ ");
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
//
|
|
425
|
-
expect(
|
|
422
|
+
const groups = Array.from(document.body.querySelectorAll<HTMLElement>(".hk-tpl-group"));
|
|
423
|
+
// The accessible name rides on the wrapper (announced on entry);
|
|
424
|
+
// the visible text lives in the inner label band.
|
|
425
|
+
expect(groups.map((g) => g.getAttribute("aria-label"))).toEqual(["identity", "email"]);
|
|
426
|
+
expect(groups.every((g) => g.getAttribute("role") === "group")).toBe(true);
|
|
427
|
+
const labels = Array.from(document.body.querySelectorAll<HTMLElement>(".hk-tpl-group-label"));
|
|
428
|
+
expect(labels.map((l) => l.textContent)).toEqual(["identity", "email"]);
|
|
429
|
+
// The visible band is hidden from the a11y tree — the wrapper's
|
|
430
|
+
// aria-label already announced the name (no double reading).
|
|
431
|
+
expect(labels.every((l) => l.getAttribute("aria-hidden") === "true")).toBe(true);
|
|
432
|
+
// Rows ride INSIDE their group wrapper.
|
|
433
|
+
expect(groups[0]!.querySelectorAll(".hk-tpl-row")).toHaveLength(2);
|
|
434
|
+
expect(groups[1]!.querySelectorAll(".hk-tpl-row")).toHaveLength(2);
|
|
426
435
|
});
|
|
427
436
|
|
|
428
437
|
it("maps group keys through groupLabel", async () => {
|
|
429
438
|
const el = mountWithTokens(GROUPED, { groupLabel: (g) => g.toUpperCase() });
|
|
430
439
|
await nextTick();
|
|
431
440
|
await openTrigger(el, "{{ ");
|
|
432
|
-
const
|
|
433
|
-
expect(
|
|
441
|
+
const groups = Array.from(document.body.querySelectorAll<HTMLElement>(".hk-tpl-group"));
|
|
442
|
+
expect(groups.map((g) => g.getAttribute("aria-label"))).toEqual(["IDENTITY", "EMAIL"]);
|
|
443
|
+
expect(groups.map((g) => g.querySelector(".hk-tpl-group-label")?.textContent)).toEqual([
|
|
444
|
+
"IDENTITY",
|
|
445
|
+
"EMAIL",
|
|
446
|
+
]);
|
|
434
447
|
});
|
|
435
448
|
|
|
436
449
|
it("filters groups out with their members (no stray heading)", async () => {
|
|
437
450
|
const el = mountWithTokens(GROUPED);
|
|
438
451
|
await nextTick();
|
|
439
452
|
await openTrigger(el, "{{ md5");
|
|
440
|
-
const
|
|
441
|
-
expect(
|
|
453
|
+
const groups = Array.from(document.body.querySelectorAll<HTMLElement>(".hk-tpl-group"));
|
|
454
|
+
expect(groups.map((g) => g.getAttribute("aria-label"))).toEqual(["email"]);
|
|
455
|
+
expect(groups[0]!.querySelectorAll(".hk-tpl-row")).toHaveLength(1);
|
|
442
456
|
});
|
|
443
457
|
|
|
444
458
|
it("ungrouped vocabularies render exactly as before (no headings)", async () => {
|
|
@@ -454,10 +468,10 @@ describe("HkTemplateField grouped vocabulary", () => {
|
|
|
454
468
|
await nextTick();
|
|
455
469
|
editable(el).querySelector<HTMLElement>(".hk-tpl-chip")!.click();
|
|
456
470
|
await nextTick();
|
|
457
|
-
const
|
|
471
|
+
const groups = Array.from(
|
|
458
472
|
document.body.querySelectorAll<HTMLElement>(".hk-tpl-editor .hk-tpl-group"),
|
|
459
473
|
);
|
|
460
|
-
expect(
|
|
474
|
+
expect(groups.map((g) => g.getAttribute("aria-label"))).toEqual(["identity", "email"]);
|
|
461
475
|
});
|
|
462
476
|
});
|
|
463
477
|
|
|
@@ -647,10 +661,53 @@ describe("HkTemplateField partially-grouped vocabulary", () => {
|
|
|
647
661
|
]);
|
|
648
662
|
await nextTick();
|
|
649
663
|
await openTrigger(el, "{{ ");
|
|
650
|
-
const
|
|
651
|
-
// Two
|
|
652
|
-
//
|
|
653
|
-
|
|
664
|
+
const groups = Array.from(document.body.querySelectorAll<HTMLElement>(".hk-tpl-group"));
|
|
665
|
+
// Two wrappers for two group runs; the ungrouped tokens ride inside
|
|
666
|
+
// the open run (see the groupLabel prop doc) — free_a/b inside
|
|
667
|
+
// identity, free_c inside email.
|
|
668
|
+
expect(groups.map((g) => g.getAttribute("aria-label"))).toEqual(["identity", "email"]);
|
|
669
|
+
expect(groups[0]!.querySelectorAll(".hk-tpl-row")).toHaveLength(3);
|
|
670
|
+
expect(groups[1]!.querySelectorAll(".hk-tpl-row")).toHaveLength(2);
|
|
654
671
|
expect(document.body.querySelectorAll(".hk-tpl-row")).toHaveLength(5);
|
|
655
672
|
});
|
|
656
673
|
});
|
|
674
|
+
|
|
675
|
+
describe("HkTemplateField vocabulary semantics guards", () => {
|
|
676
|
+
it("the rows containers carry no listbox role (menu/dialog context)", async () => {
|
|
677
|
+
const el = mountWithTokens([
|
|
678
|
+
{ name: "id", group: "identity" },
|
|
679
|
+
{ name: "md5_email", group: "email" },
|
|
680
|
+
]);
|
|
681
|
+
await nextTick();
|
|
682
|
+
await openTrigger(el, "{{ ");
|
|
683
|
+
const panelRows = document.body.querySelector<HTMLElement>(".hk-tpl-rows")!;
|
|
684
|
+
expect(panelRows.getAttribute("role")).toBeNull();
|
|
685
|
+
|
|
686
|
+
const el2 = mount({ modelValue: "a {{ username }} b" });
|
|
687
|
+
await nextTick();
|
|
688
|
+
editable(el2).querySelector<HTMLElement>(".hk-tpl-chip")!.click();
|
|
689
|
+
await nextTick();
|
|
690
|
+
const editorRows = document.body.querySelector<HTMLElement>(".hk-tpl-editor-rows")!;
|
|
691
|
+
expect(editorRows.getAttribute("role")).toBeNull();
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it("the panel's active highlight follows the filtered index inside wrappers", async () => {
|
|
695
|
+
const el = mountWithTokens([
|
|
696
|
+
{ name: "id", group: "identity" },
|
|
697
|
+
{ name: "username", group: "identity" },
|
|
698
|
+
{ name: "md5_email", group: "email" },
|
|
699
|
+
{ name: "sha256_email", group: "email" },
|
|
700
|
+
]);
|
|
701
|
+
await nextTick();
|
|
702
|
+
await openTrigger(el, "{{ ");
|
|
703
|
+
editable(el).dispatchEvent(
|
|
704
|
+
new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }),
|
|
705
|
+
);
|
|
706
|
+
await nextTick();
|
|
707
|
+
const active = Array.from(document.body.querySelectorAll<HTMLElement>(".hk-tpl-row[data-active]"));
|
|
708
|
+
expect(active).toHaveLength(1);
|
|
709
|
+
// Index 1 across the WHOLE filtered list (username) — the highlight
|
|
710
|
+
// must not restart inside each group wrapper.
|
|
711
|
+
expect(active[0]!.textContent).toContain("username");
|
|
712
|
+
});
|
|
713
|
+
});
|
|
@@ -28,6 +28,12 @@ import {
|
|
|
28
28
|
} from "./templateGrammar";
|
|
29
29
|
import "./HkTemplateField.scss";
|
|
30
30
|
|
|
31
|
+
/** What renderGroupedRows hands its row callback. */
|
|
32
|
+
interface HkTemplateFieldRow {
|
|
33
|
+
def: HkTemplateTokenDef;
|
|
34
|
+
index: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
/** Matches an UNFINISHED `{{ query` the caret is typing inside — the
|
|
32
38
|
* autocomplete trigger. Identifiers only; anything else (a filter pipe,
|
|
33
39
|
* a space) drops out of trigger state and closes the panel. */
|
|
@@ -875,34 +881,48 @@ export const HkTemplateField = defineComponent({
|
|
|
875
881
|
};
|
|
876
882
|
}
|
|
877
883
|
|
|
878
|
-
/**
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
884
|
+
/** Grouped rows as labeled WRAPPERS, not sibling headings: a
|
|
885
|
+
* `role="group"` box carrying the group's name in `aria-label` is
|
|
886
|
+
* the pattern screen readers announce on entry (a bare
|
|
887
|
+
* role="presentation" heading band was invisible to them — the
|
|
888
|
+
* R3 P3 finding), and it is the sanctioned way to segment choices
|
|
889
|
+
* inside HkMenu's role="menu" surface. The visible heading text
|
|
890
|
+
* lives in the inner .hk-tpl-group-label band. */
|
|
883
891
|
function renderGroupedRows(
|
|
884
892
|
defs: readonly HkTemplateTokenDef[],
|
|
885
|
-
renderRow: (
|
|
893
|
+
renderRow: (row: HkTemplateFieldRow) => unknown,
|
|
886
894
|
): unknown[] {
|
|
887
895
|
const out: unknown[] = [];
|
|
888
|
-
let
|
|
896
|
+
let run: { label: string; rows: unknown[] } | null = null;
|
|
897
|
+
const flush = () => {
|
|
898
|
+
if (!run) return;
|
|
899
|
+
out.push(
|
|
900
|
+
<div class="hk-tpl-group" role="group" aria-label={run.label}>
|
|
901
|
+
<div class="hk-tpl-group-label" aria-hidden="true">{run.label}</div>
|
|
902
|
+
{run.rows}
|
|
903
|
+
</div>,
|
|
904
|
+
);
|
|
905
|
+
run = null;
|
|
906
|
+
};
|
|
889
907
|
for (let i = 0; i < defs.length; i++) {
|
|
890
908
|
const def = defs[i]!;
|
|
891
909
|
if (def.group !== undefined) {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
</div>,
|
|
897
|
-
);
|
|
910
|
+
const label = props.groupLabel ? props.groupLabel(def.group) : def.group;
|
|
911
|
+
if (!run || run.label !== label) {
|
|
912
|
+
flush();
|
|
913
|
+
run = { label, rows: [] };
|
|
898
914
|
}
|
|
899
|
-
lastGroup = def.group;
|
|
900
915
|
}
|
|
901
916
|
// Tokens without a group simply continue under the previous
|
|
902
|
-
//
|
|
903
|
-
// partially-grouped vocabularies (see the prop doc).
|
|
904
|
-
|
|
917
|
+
// group's visual section — the documented contract for
|
|
918
|
+
// partially-grouped vocabularies (see the prop doc). When the
|
|
919
|
+
// run is open they ride inside it; a leading ungrouped run
|
|
920
|
+
// renders bare, exactly as an ungrouped vocabulary does.
|
|
921
|
+
const row = renderRow({ def, index: i });
|
|
922
|
+
if (run) run.rows.push(row);
|
|
923
|
+
else out.push(row);
|
|
905
924
|
}
|
|
925
|
+
flush();
|
|
906
926
|
return out;
|
|
907
927
|
}
|
|
908
928
|
|
|
@@ -981,13 +1001,13 @@ export const HkTemplateField = defineComponent({
|
|
|
981
1001
|
title={t("hikari::templateField.suggestTitle", "Placeholders")}
|
|
982
1002
|
>
|
|
983
1003
|
{filteredTokens.value.length > 0 ? (
|
|
984
|
-
<div class="hk-tpl-rows"
|
|
985
|
-
{renderGroupedRows(filteredTokens.value, (def,
|
|
1004
|
+
<div class="hk-tpl-rows">
|
|
1005
|
+
{renderGroupedRows(filteredTokens.value, ({ def, index }) =>
|
|
986
1006
|
renderTokenRow(
|
|
987
1007
|
def,
|
|
988
|
-
|
|
1008
|
+
index === Math.min(suggestActive.value, filteredTokens.value.length - 1),
|
|
989
1009
|
() => pickSuggestion(def),
|
|
990
|
-
|
|
1010
|
+
index,
|
|
991
1011
|
),
|
|
992
1012
|
)}
|
|
993
1013
|
</div>
|
|
@@ -1054,8 +1074,8 @@ export const HkTemplateField = defineComponent({
|
|
|
1054
1074
|
/>
|
|
1055
1075
|
</div>
|
|
1056
1076
|
<div ref={editorRowsHostRef} class="hk-tpl-editor-scroll">
|
|
1057
|
-
<div ref={editorRowsRef} class="hk-tpl-editor-rows"
|
|
1058
|
-
{renderGroupedRows(editorRows.value, (def) =>
|
|
1077
|
+
<div ref={editorRowsRef} class="hk-tpl-editor-rows">
|
|
1078
|
+
{renderGroupedRows(editorRows.value, ({ def }) =>
|
|
1059
1079
|
renderTokenRow(def, def.name === editorToken.value, () => applyChipToken(def.name)),
|
|
1060
1080
|
)}
|
|
1061
1081
|
{editorRows.value.length === 0 && (
|