@celestia-island/hikari 0.55.126 → 0.55.128
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
|
@@ -93,6 +93,87 @@
|
|
|
93
93
|
color: #fff;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// ── Actions on a tinted surface ─────────────────────────────────────
|
|
97
|
+
// The card's surface IS the variant color, so the action row takes its
|
|
98
|
+
// ink from that surface instead of leaking the global accent: a plain
|
|
99
|
+
// primary button reads as a foreign brand on the tinted card (a pink
|
|
100
|
+
// confirm on the teal info card under a pink-themed consumer), and the
|
|
101
|
+
// danger confirm's own red dissolves into its red card. The confirm
|
|
102
|
+
// inverts to a filled white control labeled in a deepened shade of the
|
|
103
|
+
// variant color — the strongest move on every variant surface — and the
|
|
104
|
+
// cancel quiets to a translucent outline in the card's own ink (the
|
|
105
|
+
// house grammar for quiet controls on tinted toast surfaces).
|
|
106
|
+
.hk-blocking-toast-info {
|
|
107
|
+
--hk-blocking-toast-ink: var(--hi-color-info, #6abed6);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.hk-blocking-toast-warning {
|
|
111
|
+
--hk-blocking-toast-ink: var(--hi-color-warning, #eed677);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.hk-blocking-toast-danger {
|
|
115
|
+
--hk-blocking-toast-ink: var(--hi-color-danger, #f85149);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* The confirm arrives as .hk-btn-primary (info/warning cards) or
|
|
119
|
+
.hk-btn-danger (danger card) — both recolor to the same inverted
|
|
120
|
+
control. Labels darken the variant ink (55% toward black) so every
|
|
121
|
+
variant stays readable on the white fill; svg rides the label color
|
|
122
|
+
so a themed glyph ink can't wash out against it. */
|
|
123
|
+
.hk-blocking-toast-actions .hk-btn-primary,
|
|
124
|
+
.hk-blocking-toast-actions .hk-btn-danger {
|
|
125
|
+
background: #fff;
|
|
126
|
+
border-color: transparent;
|
|
127
|
+
color: color-mix(in srgb, var(--hk-blocking-toast-ink, #6abed6) 55%, #000);
|
|
128
|
+
|
|
129
|
+
svg {
|
|
130
|
+
color: inherit;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.hk-blocking-toast-actions .hk-btn-primary:hover:not(:disabled):not(:focus-visible),
|
|
135
|
+
.hk-blocking-toast-actions .hk-btn-danger:hover:not(:disabled):not(:focus-visible) {
|
|
136
|
+
filter: none;
|
|
137
|
+
box-shadow: none;
|
|
138
|
+
background: rgb(255 255 255 / 88%);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.hk-blocking-toast-actions .hk-btn-primary:active:not(:disabled),
|
|
142
|
+
.hk-blocking-toast-actions .hk-btn-danger:active:not(:disabled) {
|
|
143
|
+
filter: none;
|
|
144
|
+
background: rgb(255 255 255 / 78%);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* The secondary cancel stops being a heavy surface block (it rendered
|
|
148
|
+
as a white pill competing with the confirm on the tinted card) and
|
|
149
|
+
picks up the quiet outline in the card's currentColor — white on
|
|
150
|
+
info/danger, near-black on warning — so it reads on every variant
|
|
151
|
+
without its own per-variant table. */
|
|
152
|
+
.hk-blocking-toast-actions .hk-btn-secondary {
|
|
153
|
+
background: transparent;
|
|
154
|
+
border-color: color-mix(in srgb, currentColor 45%, transparent);
|
|
155
|
+
backdrop-filter: none;
|
|
156
|
+
color: inherit;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.hk-blocking-toast-actions .hk-btn-secondary:hover:not(:disabled) {
|
|
160
|
+
border-color: color-mix(in srgb, currentColor 70%, transparent);
|
|
161
|
+
background: color-mix(in srgb, currentColor 18%, transparent);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.hk-blocking-toast-actions .hk-btn-secondary:active:not(:disabled) {
|
|
165
|
+
filter: none;
|
|
166
|
+
background: color-mix(in srgb, currentColor 28%, transparent);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* The keyboard ring follows the card ink too — the global primary ring
|
|
170
|
+
would leak the same foreign accent the fill just removed. */
|
|
171
|
+
.hk-blocking-toast-actions .hk-btn:focus-visible {
|
|
172
|
+
box-shadow:
|
|
173
|
+
0 0 0 2px rgb(255 255 255 / 60%),
|
|
174
|
+
0 0 0 4px currentColor;
|
|
175
|
+
}
|
|
176
|
+
|
|
96
177
|
// Enter/leave mirrors the toast slide (settle, no bounce); leaving
|
|
97
178
|
// cards go absolute so the remaining ones glide up (hk-toast pattern).
|
|
98
179
|
.hk-blocking-toast-enter-active {
|
|
@@ -673,22 +673,85 @@ describe("HkTemplateField partially-grouped vocabulary", () => {
|
|
|
673
673
|
});
|
|
674
674
|
|
|
675
675
|
describe("HkTemplateField vocabulary semantics guards", () => {
|
|
676
|
-
it("
|
|
676
|
+
it("container roles split by context: panel bare in the menu, editor a labeled listbox", async () => {
|
|
677
677
|
const el = mountWithTokens([
|
|
678
678
|
{ name: "id", group: "identity" },
|
|
679
679
|
{ name: "md5_email", group: "email" },
|
|
680
680
|
]);
|
|
681
681
|
await nextTick();
|
|
682
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).
|
|
683
685
|
const panelRows = document.body.querySelector<HTMLElement>(".hk-tpl-rows")!;
|
|
684
686
|
expect(panelRows.getAttribute("role")).toBeNull();
|
|
685
687
|
|
|
688
|
+
// The editor surface is a DIALOG — a listbox is the correct list
|
|
689
|
+
// semantics there (restored deliberately, with an accessible name).
|
|
686
690
|
const el2 = mount({ modelValue: "a {{ username }} b" });
|
|
687
691
|
await nextTick();
|
|
688
692
|
editable(el2).querySelector<HTMLElement>(".hk-tpl-chip")!.click();
|
|
689
693
|
await nextTick();
|
|
690
694
|
const editorRows = document.body.querySelector<HTMLElement>(".hk-tpl-editor-rows")!;
|
|
691
|
-
expect(editorRows.getAttribute("role")).
|
|
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");
|
|
692
755
|
});
|
|
693
756
|
|
|
694
757
|
it("the panel's active highlight follows the filtered index inside wrappers", async () => {
|
|
@@ -114,6 +114,9 @@ export const HkTemplateField = defineComponent({
|
|
|
114
114
|
// association HkInput gives a bare `label` prop — label clicks
|
|
115
115
|
// focus, screen readers announce the field by name).
|
|
116
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}`;
|
|
117
120
|
|
|
118
121
|
const editRef = ref<HTMLElement | null>(null);
|
|
119
122
|
/** The value the DOM currently represents. Authoritative while the
|
|
@@ -926,18 +929,31 @@ export const HkTemplateField = defineComponent({
|
|
|
926
929
|
return out;
|
|
927
930
|
}
|
|
928
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). */
|
|
929
937
|
const renderTokenRow = (
|
|
930
938
|
def: HkTemplateTokenDef,
|
|
931
939
|
active: boolean,
|
|
932
940
|
onPick: () => void,
|
|
933
|
-
|
|
941
|
+
opts: {
|
|
942
|
+
i?: number;
|
|
943
|
+
role: "menuitem" | "option";
|
|
944
|
+
selected?: boolean;
|
|
945
|
+
id?: string;
|
|
946
|
+
},
|
|
934
947
|
) => (
|
|
935
948
|
<button
|
|
936
949
|
key={def.name}
|
|
937
950
|
type="button"
|
|
938
951
|
class="hk-tpl-row"
|
|
952
|
+
role={opts.role}
|
|
953
|
+
id={opts.id}
|
|
954
|
+
aria-selected={opts.role === "option" ? (opts.selected || false) : undefined}
|
|
939
955
|
data-active={active || undefined}
|
|
940
|
-
ref={i !== undefined ? setRowRef(i) : undefined}
|
|
956
|
+
ref={opts.i !== undefined ? setRowRef(opts.i) : undefined}
|
|
941
957
|
onMousedown={(e: MouseEvent) => e.preventDefault()}
|
|
942
958
|
onClick={(e: MouseEvent) => {
|
|
943
959
|
e.stopPropagation();
|
|
@@ -966,6 +982,21 @@ export const HkTemplateField = defineComponent({
|
|
|
966
982
|
contenteditable={!props.disabled}
|
|
967
983
|
role="textbox"
|
|
968
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
|
+
}
|
|
969
1000
|
id={fieldId}
|
|
970
1001
|
spellcheck={false}
|
|
971
1002
|
data-ph={props.placeholder}
|
|
@@ -1001,13 +1032,17 @@ export const HkTemplateField = defineComponent({
|
|
|
1001
1032
|
title={t("hikari::templateField.suggestTitle", "Placeholders")}
|
|
1002
1033
|
>
|
|
1003
1034
|
{filteredTokens.value.length > 0 ? (
|
|
1004
|
-
<div class="hk-tpl-rows">
|
|
1035
|
+
<div class="hk-tpl-rows" id={panelRowsId}>
|
|
1005
1036
|
{renderGroupedRows(filteredTokens.value, ({ def, index }) =>
|
|
1006
1037
|
renderTokenRow(
|
|
1007
1038
|
def,
|
|
1008
1039
|
index === Math.min(suggestActive.value, filteredTokens.value.length - 1),
|
|
1009
1040
|
() => pickSuggestion(def),
|
|
1010
|
-
|
|
1041
|
+
{
|
|
1042
|
+
i: index,
|
|
1043
|
+
role: "menuitem",
|
|
1044
|
+
id: `${panelRowsId}-row-${index}`,
|
|
1045
|
+
},
|
|
1011
1046
|
),
|
|
1012
1047
|
)}
|
|
1013
1048
|
</div>
|
|
@@ -1074,9 +1109,17 @@ export const HkTemplateField = defineComponent({
|
|
|
1074
1109
|
/>
|
|
1075
1110
|
</div>
|
|
1076
1111
|
<div ref={editorRowsHostRef} class="hk-tpl-editor-scroll">
|
|
1077
|
-
<div
|
|
1112
|
+
<div
|
|
1113
|
+
ref={editorRowsRef}
|
|
1114
|
+
class="hk-tpl-editor-rows"
|
|
1115
|
+
role="listbox"
|
|
1116
|
+
aria-label={t("hikari::templateField.chipEditorTitle", "Edit placeholder")}
|
|
1117
|
+
>
|
|
1078
1118
|
{renderGroupedRows(editorRows.value, ({ def }) =>
|
|
1079
|
-
renderTokenRow(def, def.name === editorToken.value, () => applyChipToken(def.name)
|
|
1119
|
+
renderTokenRow(def, def.name === editorToken.value, () => applyChipToken(def.name), {
|
|
1120
|
+
role: "option",
|
|
1121
|
+
selected: def.name === editorToken.value,
|
|
1122
|
+
}),
|
|
1080
1123
|
)}
|
|
1081
1124
|
{editorRows.value.length === 0 && (
|
|
1082
1125
|
<p class="hk-tpl-rows-empty">
|