@celestia-island/hikari 0.55.115 → 0.55.117

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.115",
3
+ "version": "0.55.117",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari Vue 3 component library — production-grade UI components based on shittim-chest design system",
@@ -50,8 +50,14 @@ export const HkAdminTablePage = defineComponent({
50
50
  rowKey: { type: String, default: "id" },
51
51
  emptyTitle: { type: String, default: "" },
52
52
  emptyDescription: { type: String as PropType<string | undefined>, default: undefined },
53
+ /** Left grip column with drag-to-reorder (forwarded to HTable; the
54
+ * `reorder` event is forwarded verbatim — indices into `rows`). */
55
+ draggable: { type: Boolean, default: false },
53
56
  },
54
- setup(props, { slots }) {
57
+ emits: {
58
+ reorder: (_fromIndex: number, _toIndex: number) => true,
59
+ },
60
+ setup(props, { emit, slots }) {
55
61
 
56
62
  return () => {
57
63
  const { t } = useI18n();
@@ -96,7 +102,13 @@ export const HkAdminTablePage = defineComponent({
96
102
  <div class="hk-admin-table-page-body">{slots.body()}</div>
97
103
  ) : (
98
104
  <HCard padded={false}>
99
- <HTable columns={props.columns} rows={props.rows} rowKey={props.rowKey}>
105
+ <HTable
106
+ columns={props.columns}
107
+ rows={props.rows}
108
+ rowKey={props.rowKey}
109
+ draggable={props.draggable}
110
+ onReorder={(from: number, to: number) => emit("reorder", from, to)}
111
+ >
100
112
  {slots}
101
113
  </HTable>
102
114
  </HCard>
@@ -60,13 +60,18 @@ describe("HkAuthMethodList", () => {
60
60
  expect(c.querySelector(".s-auth-methods [role='radiogroup']")).toBeNull();
61
61
  });
62
62
 
63
- it("wraps every tile in a tooltip carrying the provider label", () => {
63
+ it("wraps every tile in a tooltip carrying the provider label", async () => {
64
64
  const c = mount(
65
65
  h("div", { class: "s-auth-methods" }, h(HkAuthMethodList, { methods })),
66
66
  );
67
67
  const wrappers = c.querySelectorAll(".hk-tooltip-wrapper");
68
68
  expect(wrappers.length).toBe(2);
69
- // The teleported popups exist with the label text (hidden until hover).
69
+ // Popups mount lazily on first engage — a touch tap mounts each one
70
+ // immediately — and then carry the label text.
71
+ for (const w of wrappers) {
72
+ w.dispatchEvent(new PointerEvent("pointerdown", { pointerType: "touch", bubbles: true }));
73
+ }
74
+ await Promise.resolve();
70
75
  const popups = [...document.querySelectorAll(".hk-tooltip-popup .hk-tooltip-content")]
71
76
  .map((el) => el.textContent);
72
77
  expect(popups).toContain("GitHub");
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { createApp, h, defineComponent } from "vue";
2
+ import { createApp, h, defineComponent, nextTick } from "vue";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -8,6 +8,7 @@ import HkCopyBadge from "./HkCopyBadge";
8
8
  import { useToast } from "../runtime/useToast";
9
9
 
10
10
  const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
11
+ const appUnmounts: Array<{ app: ReturnType<typeof createApp> }> = [];
11
12
 
12
13
  function mount(renderNode: () => ReturnType<typeof h>) {
13
14
  const container = document.createElement("div");
@@ -15,6 +16,7 @@ function mount(renderNode: () => ReturnType<typeof h>) {
15
16
  const app = createApp({ render: renderNode });
16
17
  app.mount(container);
17
18
  mounts.push({ app, container });
19
+ appUnmounts.push({ app });
18
20
  return container;
19
21
  }
20
22
 
@@ -126,18 +128,64 @@ describe("HkCopyBadge", () => {
126
128
  expect(writeText).not.toHaveBeenCalled();
127
129
  });
128
130
 
129
- it("renders the custom tooltip text", () => {
131
+ /** Engage the lazy tooltip without hovering: a touch tap mounts the
132
+ * popup immediately (showNow), no fake timers needed. */
133
+ function engageTooltip(c: HTMLElement) {
134
+ c.querySelector(".hk-tooltip-wrapper")!.dispatchEvent(
135
+ new PointerEvent("pointerdown", { pointerType: "touch", bubbles: true }),
136
+ );
137
+ }
138
+
139
+ it("renders the custom tooltip text", async () => {
130
140
  const c = mount(() => h(HkCopyBadge, { tooltip: "Open details" }, () => "#d.132"));
131
- const content = c.parentElement!.querySelector(".hk-tooltip-popup .hk-tooltip-content");
141
+ // The popup mounts lazily on first engage (dense-list contract).
142
+ expect(document.querySelector(".hk-tooltip-popup")).toBeNull();
143
+ engageTooltip(c);
144
+ await nextTick();
145
+ const content = document.body.querySelector(".hk-tooltip-popup .hk-tooltip-content");
132
146
  expect(content?.textContent).toBe("Open details");
133
147
  });
134
148
 
135
- it("shows the localized click-to-copy tooltip by default", () => {
149
+ it("shows the localized click-to-copy tooltip by default", async () => {
136
150
  const c = mount(() => h(HkCopyBadge, null, () => "#d.132"));
137
- const content = c.parentElement!.querySelector(".hk-tooltip-popup .hk-tooltip-content");
151
+ engageTooltip(c);
152
+ await nextTick();
153
+ const content = document.body.querySelector(".hk-tooltip-popup .hk-tooltip-content");
138
154
  expect(content?.textContent).toBe("Click to copy");
139
155
  });
140
156
 
157
+ it("links the badge to the popup via aria-describedby across the whole lifecycle", async () => {
158
+ const c = mount(() => h(HkCopyBadge, { tooltip: "Open details" }, () => "#d.132"));
159
+ const badge = c.querySelector(".hk-badge")!;
160
+
161
+ // Dormant: no popup, no describedby.
162
+ expect(badge.getAttribute("aria-describedby")).toBeNull();
163
+ expect(document.body.querySelector(".hk-tooltip-popup")).toBeNull();
164
+
165
+ // Engage: describedby == the popup id.
166
+ engageTooltip(c);
167
+ await nextTick();
168
+ const popup = document.body.querySelector(".hk-tooltip-popup")!;
169
+ expect(badge.getAttribute("aria-describedby")).toBe(popup.getAttribute("id"));
170
+ expect(popup.querySelector(".hk-tooltip-content")!.textContent).toBe("Open details");
171
+
172
+ // Leave: the attribute flips BACK (slot re-invocation must not stick).
173
+ c.querySelector(".hk-tooltip-wrapper")!.dispatchEvent(new MouseEvent("mouseleave"));
174
+ await nextTick();
175
+ expect(badge.getAttribute("aria-describedby")).toBeNull();
176
+
177
+ // Re-engage: returns, reusing the same popup element.
178
+ engageTooltip(c);
179
+ await nextTick();
180
+ expect(badge.getAttribute("aria-describedby")).toBe(
181
+ document.body.querySelector(".hk-tooltip-popup")!.getAttribute("id"),
182
+ );
183
+
184
+ // Unmount while visible: the teleported popup goes away.
185
+ appUnmounts.splice(0).forEach(({ app }) => app.unmount());
186
+ expect(document.body.querySelector(".hk-tooltip-popup")).toBeNull();
187
+ });
188
+
141
189
  it("renders the inert badge face when disabled", () => {
142
190
  const c = mount(() => h(HkCopyBadge, { disabled: true }, () => "#d.132"));
143
191
  const badge = c.querySelector(".hk-badge")!;
@@ -27,9 +27,12 @@ import "./HkCopyBadge.scss";
27
27
  *
28
28
  * A11y: the tag is a real button (role, tab order, Enter/Space), and the
29
29
  * click is stopped from reaching clickable ancestors — a tag inside a
30
- * selectable row copies without selecting the row. Consumer attrs
31
- * (class/style/data-*) land on the badge element itself, so a call site
32
- * skinning the pill keeps working; the tooltip wrapper stays clean.
30
+ * selectable row copies without selecting the row. The tooltip popup is
31
+ * linked to the badge via aria-describedby (HkTooltip hands the popup id
32
+ * down as a slot prop, so the describedby lands on the actual focus
33
+ * target). Consumer attrs (class/style/data-*) land on the badge element
34
+ * itself, so a call site skinning the pill keeps working; the tooltip
35
+ * wrapper stays clean.
33
36
  */
34
37
  export default defineComponent({
35
38
  name: "HkCopyBadge",
@@ -95,7 +98,7 @@ export default defineComponent({
95
98
  // itself; only the interactive contract below is ours to add.
96
99
  const { class: attrClass, style: attrStyle, ...restAttrs } = attrs;
97
100
 
98
- const badge = (
101
+ const buildBadge = (describedBy?: string) => (
99
102
  <HBadge
100
103
  ref={badgeRef}
101
104
  {...restAttrs}
@@ -110,6 +113,7 @@ export default defineComponent({
110
113
  color={props.color}
111
114
  bgColor={props.bgColor}
112
115
  borderColor={props.borderColor}
116
+ aria-describedby={describedBy}
113
117
  {...(props.disabled
114
118
  ? {}
115
119
  : {
@@ -133,8 +137,13 @@ export default defineComponent({
133
137
  </HBadge>
134
138
  );
135
139
 
136
- if (props.disabled) return badge;
137
- return <HTooltip text={tooltipText.value}>{badge}</HTooltip>;
140
+ if (props.disabled) return buildBadge();
141
+ return (
142
+ <HTooltip text={tooltipText.value}>
143
+ {(tip: { popupId: string; visible: boolean }) =>
144
+ buildBadge(tip?.visible ? tip.popupId : undefined)}
145
+ </HTooltip>
146
+ );
138
147
  };
139
148
  },
140
149
  });