@celestia-island/hikari 0.55.125 → 0.55.127
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,116 @@ 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("container roles split by context: panel bare in the menu, editor a labeled listbox", 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
|
+
// The panel surface is HkMenu's role="menu" — the rows container
|
|
684
|
+
// must not re-role it (listbox-in-menu was the illegal nesting).
|
|
685
|
+
const panelRows = document.body.querySelector<HTMLElement>(".hk-tpl-rows")!;
|
|
686
|
+
expect(panelRows.getAttribute("role")).toBeNull();
|
|
687
|
+
|
|
688
|
+
// The editor surface is a DIALOG — a listbox is the correct list
|
|
689
|
+
// semantics there (restored deliberately, with an accessible name).
|
|
690
|
+
const el2 = mount({ modelValue: "a {{ username }} b" });
|
|
691
|
+
await nextTick();
|
|
692
|
+
editable(el2).querySelector<HTMLElement>(".hk-tpl-chip")!.click();
|
|
693
|
+
await nextTick();
|
|
694
|
+
const editorRows = document.body.querySelector<HTMLElement>(".hk-tpl-editor-rows")!;
|
|
695
|
+
expect(editorRows.getAttribute("role")).toBe("listbox");
|
|
696
|
+
expect(editorRows.getAttribute("aria-label")).toBeTruthy();
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it("panel rows are menuitems and the editable announces the active one", async () => {
|
|
700
|
+
const el = mountWithTokens([
|
|
701
|
+
{ name: "id", group: "identity" },
|
|
702
|
+
{ name: "username", group: "identity" },
|
|
703
|
+
]);
|
|
704
|
+
await nextTick();
|
|
705
|
+
const edit = editable(el);
|
|
706
|
+
await openTrigger(el, "{{ ");
|
|
707
|
+
// Combobox wiring on the editable: expanded + owns the rows.
|
|
708
|
+
expect(edit.getAttribute("aria-haspopup")).toBe("menu");
|
|
709
|
+
expect(edit.getAttribute("aria-expanded")).toBe("true");
|
|
710
|
+
const rowsId = edit.getAttribute("aria-owns")!;
|
|
711
|
+
expect(rowsId).toBeTruthy();
|
|
712
|
+
expect(document.getElementById(rowsId)?.classList.contains("hk-tpl-rows")).toBe(true);
|
|
713
|
+
// Rows are menuitems with stable ids.
|
|
714
|
+
const rows = Array.from(document.body.querySelectorAll<HTMLElement>(".hk-tpl-rows .hk-tpl-row"));
|
|
715
|
+
expect(rows.every((r) => r.getAttribute("role") === "menuitem")).toBe(true);
|
|
716
|
+
expect(rows[0]!.id).toBe(`${rowsId}-row-0`);
|
|
717
|
+
// The activedescendant starts at the first row and follows ArrowDown.
|
|
718
|
+
expect(edit.getAttribute("aria-activedescendant")).toBe(`${rowsId}-row-0`);
|
|
719
|
+
edit.dispatchEvent(
|
|
720
|
+
new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }),
|
|
721
|
+
);
|
|
722
|
+
await nextTick();
|
|
723
|
+
expect(edit.getAttribute("aria-activedescendant")).toBe(`${rowsId}-row-1`);
|
|
724
|
+
|
|
725
|
+
// Zero matches while the panel is open: the rows container does
|
|
726
|
+
// not render, so aria-owns must not dangle (R1 finding).
|
|
727
|
+
await openTrigger(el, "{{ idzzz");
|
|
728
|
+
expect(edit.getAttribute("aria-owns")).toBeNull();
|
|
729
|
+
expect(edit.getAttribute("aria-activedescendant")).toBeNull();
|
|
730
|
+
expect(edit.getAttribute("aria-expanded")).toBe("true");
|
|
731
|
+
|
|
732
|
+
// Collapsed state: no expansion attributes dangle.
|
|
733
|
+
edit.dispatchEvent(
|
|
734
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }),
|
|
735
|
+
);
|
|
736
|
+
await nextTick();
|
|
737
|
+
expect(edit.getAttribute("aria-expanded")).toBeNull();
|
|
738
|
+
expect(edit.getAttribute("aria-owns")).toBeNull();
|
|
739
|
+
expect(edit.getAttribute("aria-activedescendant")).toBeNull();
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it("editor rows are options with the current token selected", async () => {
|
|
743
|
+
const el = mount({ modelValue: "a {{ username }} b" });
|
|
744
|
+
await nextTick();
|
|
745
|
+
editable(el).querySelector<HTMLElement>(".hk-tpl-chip")!.click();
|
|
746
|
+
await nextTick();
|
|
747
|
+
const options = Array.from(
|
|
748
|
+
document.body.querySelectorAll<HTMLElement>(".hk-tpl-editor-rows .hk-tpl-row"),
|
|
749
|
+
);
|
|
750
|
+
expect(options.length).toBeGreaterThan(1);
|
|
751
|
+
expect(options.every((o) => o.getAttribute("role") === "option")).toBe(true);
|
|
752
|
+
const selected = options.filter((o) => o.getAttribute("aria-selected") === "true");
|
|
753
|
+
expect(selected).toHaveLength(1);
|
|
754
|
+
expect(selected[0]!.textContent).toContain("username");
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it("the panel's active highlight follows the filtered index inside wrappers", async () => {
|
|
758
|
+
const el = mountWithTokens([
|
|
759
|
+
{ name: "id", group: "identity" },
|
|
760
|
+
{ name: "username", group: "identity" },
|
|
761
|
+
{ name: "md5_email", group: "email" },
|
|
762
|
+
{ name: "sha256_email", group: "email" },
|
|
763
|
+
]);
|
|
764
|
+
await nextTick();
|
|
765
|
+
await openTrigger(el, "{{ ");
|
|
766
|
+
editable(el).dispatchEvent(
|
|
767
|
+
new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }),
|
|
768
|
+
);
|
|
769
|
+
await nextTick();
|
|
770
|
+
const active = Array.from(document.body.querySelectorAll<HTMLElement>(".hk-tpl-row[data-active]"));
|
|
771
|
+
expect(active).toHaveLength(1);
|
|
772
|
+
// Index 1 across the WHOLE filtered list (username) — the highlight
|
|
773
|
+
// must not restart inside each group wrapper.
|
|
774
|
+
expect(active[0]!.textContent).toContain("username");
|
|
775
|
+
});
|
|
776
|
+
});
|
|
@@ -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. */
|
|
@@ -108,6 +114,9 @@ export const HkTemplateField = defineComponent({
|
|
|
108
114
|
// association HkInput gives a bare `label` prop — label clicks
|
|
109
115
|
// focus, screen readers announce the field by name).
|
|
110
116
|
const fieldId = useId();
|
|
117
|
+
/** Stable id for the panel rows container + its row ids (combobox
|
|
118
|
+
* aria-owns/activedescendant targets). */
|
|
119
|
+
const panelRowsId = `hk-tpl-panel-${fieldId}`;
|
|
111
120
|
|
|
112
121
|
const editRef = ref<HTMLElement | null>(null);
|
|
113
122
|
/** The value the DOM currently represents. Authoritative while the
|
|
@@ -875,49 +884,76 @@ export const HkTemplateField = defineComponent({
|
|
|
875
884
|
};
|
|
876
885
|
}
|
|
877
886
|
|
|
878
|
-
/**
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
887
|
+
/** Grouped rows as labeled WRAPPERS, not sibling headings: a
|
|
888
|
+
* `role="group"` box carrying the group's name in `aria-label` is
|
|
889
|
+
* the pattern screen readers announce on entry (a bare
|
|
890
|
+
* role="presentation" heading band was invisible to them — the
|
|
891
|
+
* R3 P3 finding), and it is the sanctioned way to segment choices
|
|
892
|
+
* inside HkMenu's role="menu" surface. The visible heading text
|
|
893
|
+
* lives in the inner .hk-tpl-group-label band. */
|
|
883
894
|
function renderGroupedRows(
|
|
884
895
|
defs: readonly HkTemplateTokenDef[],
|
|
885
|
-
renderRow: (
|
|
896
|
+
renderRow: (row: HkTemplateFieldRow) => unknown,
|
|
886
897
|
): unknown[] {
|
|
887
898
|
const out: unknown[] = [];
|
|
888
|
-
let
|
|
899
|
+
let run: { label: string; rows: unknown[] } | null = null;
|
|
900
|
+
const flush = () => {
|
|
901
|
+
if (!run) return;
|
|
902
|
+
out.push(
|
|
903
|
+
<div class="hk-tpl-group" role="group" aria-label={run.label}>
|
|
904
|
+
<div class="hk-tpl-group-label" aria-hidden="true">{run.label}</div>
|
|
905
|
+
{run.rows}
|
|
906
|
+
</div>,
|
|
907
|
+
);
|
|
908
|
+
run = null;
|
|
909
|
+
};
|
|
889
910
|
for (let i = 0; i < defs.length; i++) {
|
|
890
911
|
const def = defs[i]!;
|
|
891
912
|
if (def.group !== undefined) {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
</div>,
|
|
897
|
-
);
|
|
913
|
+
const label = props.groupLabel ? props.groupLabel(def.group) : def.group;
|
|
914
|
+
if (!run || run.label !== label) {
|
|
915
|
+
flush();
|
|
916
|
+
run = { label, rows: [] };
|
|
898
917
|
}
|
|
899
|
-
lastGroup = def.group;
|
|
900
918
|
}
|
|
901
919
|
// Tokens without a group simply continue under the previous
|
|
902
|
-
//
|
|
903
|
-
// partially-grouped vocabularies (see the prop doc).
|
|
904
|
-
|
|
920
|
+
// group's visual section — the documented contract for
|
|
921
|
+
// partially-grouped vocabularies (see the prop doc). When the
|
|
922
|
+
// run is open they ride inside it; a leading ungrouped run
|
|
923
|
+
// renders bare, exactly as an ungrouped vocabulary does.
|
|
924
|
+
const row = renderRow({ def, index: i });
|
|
925
|
+
if (run) run.rows.push(row);
|
|
926
|
+
else out.push(row);
|
|
905
927
|
}
|
|
928
|
+
flush();
|
|
906
929
|
return out;
|
|
907
930
|
}
|
|
908
931
|
|
|
932
|
+
/** Row chrome shared by both surfaces. The ROLE rides the surface:
|
|
933
|
+
* the panel lives inside HkMenu's role="menu" (menuitem — the
|
|
934
|
+
* HkMenuActionItem family pattern), the editor lives in a dialog
|
|
935
|
+
* and is a proper listbox (option + aria-selected on the token
|
|
936
|
+
* being edited). */
|
|
909
937
|
const renderTokenRow = (
|
|
910
938
|
def: HkTemplateTokenDef,
|
|
911
939
|
active: boolean,
|
|
912
940
|
onPick: () => void,
|
|
913
|
-
|
|
941
|
+
opts: {
|
|
942
|
+
i?: number;
|
|
943
|
+
role: "menuitem" | "option";
|
|
944
|
+
selected?: boolean;
|
|
945
|
+
id?: string;
|
|
946
|
+
},
|
|
914
947
|
) => (
|
|
915
948
|
<button
|
|
916
949
|
key={def.name}
|
|
917
950
|
type="button"
|
|
918
951
|
class="hk-tpl-row"
|
|
952
|
+
role={opts.role}
|
|
953
|
+
id={opts.id}
|
|
954
|
+
aria-selected={opts.role === "option" ? (opts.selected || false) : undefined}
|
|
919
955
|
data-active={active || undefined}
|
|
920
|
-
ref={i !== undefined ? setRowRef(i) : undefined}
|
|
956
|
+
ref={opts.i !== undefined ? setRowRef(opts.i) : undefined}
|
|
921
957
|
onMousedown={(e: MouseEvent) => e.preventDefault()}
|
|
922
958
|
onClick={(e: MouseEvent) => {
|
|
923
959
|
e.stopPropagation();
|
|
@@ -946,6 +982,21 @@ export const HkTemplateField = defineComponent({
|
|
|
946
982
|
contenteditable={!props.disabled}
|
|
947
983
|
role="textbox"
|
|
948
984
|
aria-multiline="false"
|
|
985
|
+
// Combobox announcement wiring: the editable OWNS the popup
|
|
986
|
+
// (aria-owns makes the teleported rows virtual descendants)
|
|
987
|
+
// and points aria-activedescendant at the highlighted row,
|
|
988
|
+
// so arrow-key navigation is announced without moving DOM
|
|
989
|
+
// focus out of the field.
|
|
990
|
+
aria-haspopup="menu"
|
|
991
|
+
aria-expanded={suggestOpen.value || undefined}
|
|
992
|
+
aria-owns={
|
|
993
|
+
suggestOpen.value && filteredTokens.value.length > 0 ? panelRowsId : undefined
|
|
994
|
+
}
|
|
995
|
+
aria-activedescendant={
|
|
996
|
+
suggestOpen.value && filteredTokens.value.length > 0
|
|
997
|
+
? `${panelRowsId}-row-${Math.min(suggestActive.value, filteredTokens.value.length - 1)}`
|
|
998
|
+
: undefined
|
|
999
|
+
}
|
|
949
1000
|
id={fieldId}
|
|
950
1001
|
spellcheck={false}
|
|
951
1002
|
data-ph={props.placeholder}
|
|
@@ -981,13 +1032,17 @@ export const HkTemplateField = defineComponent({
|
|
|
981
1032
|
title={t("hikari::templateField.suggestTitle", "Placeholders")}
|
|
982
1033
|
>
|
|
983
1034
|
{filteredTokens.value.length > 0 ? (
|
|
984
|
-
<div class="hk-tpl-rows"
|
|
985
|
-
{renderGroupedRows(filteredTokens.value, (def,
|
|
1035
|
+
<div class="hk-tpl-rows" id={panelRowsId}>
|
|
1036
|
+
{renderGroupedRows(filteredTokens.value, ({ def, index }) =>
|
|
986
1037
|
renderTokenRow(
|
|
987
1038
|
def,
|
|
988
|
-
|
|
1039
|
+
index === Math.min(suggestActive.value, filteredTokens.value.length - 1),
|
|
989
1040
|
() => pickSuggestion(def),
|
|
990
|
-
|
|
1041
|
+
{
|
|
1042
|
+
i: index,
|
|
1043
|
+
role: "menuitem",
|
|
1044
|
+
id: `${panelRowsId}-row-${index}`,
|
|
1045
|
+
},
|
|
991
1046
|
),
|
|
992
1047
|
)}
|
|
993
1048
|
</div>
|
|
@@ -1054,9 +1109,17 @@ export const HkTemplateField = defineComponent({
|
|
|
1054
1109
|
/>
|
|
1055
1110
|
</div>
|
|
1056
1111
|
<div ref={editorRowsHostRef} class="hk-tpl-editor-scroll">
|
|
1057
|
-
<div
|
|
1058
|
-
{
|
|
1059
|
-
|
|
1112
|
+
<div
|
|
1113
|
+
ref={editorRowsRef}
|
|
1114
|
+
class="hk-tpl-editor-rows"
|
|
1115
|
+
role="listbox"
|
|
1116
|
+
aria-label={t("hikari::templateField.chipEditorTitle", "Edit placeholder")}
|
|
1117
|
+
>
|
|
1118
|
+
{renderGroupedRows(editorRows.value, ({ def }) =>
|
|
1119
|
+
renderTokenRow(def, def.name === editorToken.value, () => applyChipToken(def.name), {
|
|
1120
|
+
role: "option",
|
|
1121
|
+
selected: def.name === editorToken.value,
|
|
1122
|
+
}),
|
|
1060
1123
|
)}
|
|
1061
1124
|
{editorRows.value.length === 0 && (
|
|
1062
1125
|
<p class="hk-tpl-rows-empty">
|