@celestia-island/hikari 0.32.0 → 0.32.1
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
|
@@ -35,10 +35,12 @@
|
|
|
35
35
|
centered hk-btn-block. HkButton has a fragment root, so inbound classes
|
|
36
36
|
don't fall through — scope everything off the `.s-auth-methods`
|
|
37
37
|
container (descendant match: the component wrapper is display:contents).
|
|
38
|
-
The label column width is MEASURED by the component (widest label text
|
|
39
|
-
published as `--auth-methods-label-width` on its
|
|
40
|
-
the rows); the
|
|
41
|
-
|
|
38
|
+
The label column width is MEASURED by the component (widest label text
|
|
39
|
+
+ 1px headroom, published as `--auth-methods-label-width` on its
|
|
40
|
+
wrapper, inherited into the rows); the `max-content` fallback only
|
|
41
|
+
covers the pre-measure / un-measurable frame, and apps may still
|
|
42
|
+
override the var explicitly. Labels are centered within the shared
|
|
43
|
+
column so every row's text sits on the same vertical center line. */
|
|
42
44
|
.s-auth-methods .hk-btn .s-auth-methods-icon {
|
|
43
45
|
width: 20px;
|
|
44
46
|
flex: none;
|
|
@@ -48,9 +50,15 @@
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
.s-auth-methods .hk-btn .s-auth-methods-label {
|
|
51
|
-
width: var(--auth-methods-label-width,
|
|
53
|
+
width: var(--auth-methods-label-width, max-content);
|
|
54
|
+
/* Hardened floor: even if the published column were ever stale or too
|
|
55
|
+
narrow (engine DPR / zoom mismatch), the label can never be clipped —
|
|
56
|
+
the row just widens to its own text. When the measurement is correct
|
|
57
|
+
the var already exceeds every label's natural width, so the floor is
|
|
58
|
+
inert and alignment is preserved. */
|
|
59
|
+
min-width: max-content;
|
|
52
60
|
flex: none;
|
|
53
|
-
text-align:
|
|
61
|
+
text-align: center;
|
|
54
62
|
white-space: nowrap;
|
|
55
63
|
overflow: hidden;
|
|
56
64
|
text-overflow: ellipsis;
|
|
@@ -64,35 +64,36 @@ describe("HkAuthMethodList", () => {
|
|
|
64
64
|
expect(picked).toBe("linuxdo");
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
it("publishes the widest label text as the label-column custom property", async () => {
|
|
67
|
+
it("publishes the widest label text plus a 1px headroom as the label-column custom property", async () => {
|
|
68
68
|
const c = mount(h("div", { class: "s-auth-methods" }, h(HkAuthMethodList, { methods })));
|
|
69
69
|
await nextTick();
|
|
70
70
|
const wrapper = c.querySelector<HTMLElement>(".s-auth-methods-list")!;
|
|
71
71
|
expect(wrapper).toBeTruthy();
|
|
72
|
-
// Layout engines in test DOM report zero text metrics; stub
|
|
73
|
-
// on the label spans
|
|
72
|
+
// Layout engines in test DOM report zero text metrics; stub a fractional
|
|
73
|
+
// getBoundingClientRect width on the label spans so the widest-wins pair
|
|
74
|
+
// (LinuxDo wider) exercises the ceil+1 headroom instead of the integer
|
|
75
|
+
// scrollWidth that would previously round a fraction and clip the widest.
|
|
74
76
|
const spans = [...wrapper.querySelectorAll<HTMLElement>(".s-auth-methods-label")];
|
|
75
77
|
expect(spans.length).toBe(2);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
// 61.4 ceil→62 (+1→63) and a shorter 58.0: the published column must
|
|
79
|
+
// exceed BOTH so the widest label can never sit on the clip boundary.
|
|
80
|
+
Object.defineProperty(spans[0]!, "getBoundingClientRect", { configurable: true, value: () => ({ width: 58 }) });
|
|
81
|
+
Object.defineProperty(spans[1]!, "getBoundingClientRect", { configurable: true, value: () => ({ width: 61.4 }) });
|
|
79
82
|
await nextTick();
|
|
80
|
-
const { app } = mounts[mounts.length - 1]!;
|
|
81
83
|
// Re-run measurement via a re-mount-less route: the component exposes
|
|
82
84
|
// measure() — reach it through the rendered component instance.
|
|
83
85
|
const instance = (wrapper as unknown as { __vueParentComponent?: { exposed?: Record<string, unknown> } })
|
|
84
86
|
.__vueParentComponent?.exposed;
|
|
85
87
|
expect(typeof instance?.measure).toBe("function");
|
|
86
88
|
(instance!.measure as () => void)();
|
|
87
|
-
expect(wrapper.style.getPropertyValue("--auth-methods-label-width")).toBe("
|
|
88
|
-
void app;
|
|
89
|
+
expect(wrapper.style.getPropertyValue("--auth-methods-label-width")).toBe("63px");
|
|
89
90
|
});
|
|
90
91
|
|
|
91
92
|
it("leaves the column fallback intact when no text metrics are available", async () => {
|
|
92
93
|
const c = mount(h("div", { class: "s-auth-methods" }, h(HkAuthMethodList, { methods })));
|
|
93
94
|
await nextTick();
|
|
94
95
|
const wrapper = c.querySelector<HTMLElement>(".s-auth-methods-list")!;
|
|
95
|
-
// Zero
|
|
96
|
+
// Zero text metrics (no layout) must not publish a 0px or a 1px column.
|
|
96
97
|
expect(wrapper.style.getPropertyValue("--auth-methods-label-width")).toBe("");
|
|
97
98
|
});
|
|
98
99
|
});
|
|
@@ -12,11 +12,16 @@ import "./HkAuthMethodList.scss";
|
|
|
12
12
|
* then the same width, the row can stay centered like any HkButton
|
|
13
13
|
* without the columns drifting.
|
|
14
14
|
*
|
|
15
|
-
* The label column is auto-sized: after mount (and whenever the label set
|
|
16
|
-
*
|
|
17
|
-
* text and publishes it as
|
|
18
|
-
* so the aligned text region
|
|
19
|
-
*
|
|
15
|
+
* The label column is auto-sized: after mount (and whenever the label set,
|
|
16
|
+
* the document fonts, the browser zoom, or the container size change) the
|
|
17
|
+
* component measures the widest label text and publishes it as
|
|
18
|
+
* `--auth-methods-label-width` on its wrapper, so the aligned text region
|
|
19
|
+
* hugs the longest label instead of a blind hardcoded width. The measured
|
|
20
|
+
* fraction is rounded UP with a 1px margin — the column is always strictly
|
|
21
|
+
* wider than the longest label, so the text can never overflow into an
|
|
22
|
+
* ellipsis. Labels are centered within that shared column, keeping every
|
|
23
|
+
* row's text on the same vertical center line. Consumers can still
|
|
24
|
+
* override the var explicitly.
|
|
20
25
|
*
|
|
21
26
|
* The wrapper is `display: contents`: the component keeps a DOM node for
|
|
22
27
|
* measurement and CSS-var inheritance while the buttons remain direct
|
|
@@ -53,12 +58,25 @@ export default defineComponent({
|
|
|
53
58
|
setup(props, { emit, expose }) {
|
|
54
59
|
const listRef = ref<HTMLDivElement | null>(null);
|
|
55
60
|
|
|
61
|
+
let resizeObserver: ResizeObserver | undefined;
|
|
62
|
+
const onResize = () => void nextTick(measure);
|
|
63
|
+
const onFontsReady = () => measure();
|
|
64
|
+
|
|
56
65
|
/** Measure the widest label text and publish it as the label-column
|
|
57
66
|
* width custom property. Each span is momentarily set to
|
|
58
67
|
* `max-content` while reading: a plain `scrollWidth` on the fixed
|
|
59
68
|
* column would report `max(textWidth, columnWidth)` and the column
|
|
60
69
|
* could never shrink below its fallback. All reads happen inside one
|
|
61
|
-
* synchronous task, so no intermediate paint is observable.
|
|
70
|
+
* synchronous task, so no intermediate paint is observable.
|
|
71
|
+
*
|
|
72
|
+
* Precision: the widest width is read as a fractional
|
|
73
|
+
* `getBoundingClientRect()` (not the integer `scrollWidth`) and
|
|
74
|
+
* rounded UP with a 1px margin before publishing. An integer read can
|
|
75
|
+
* round the longest label DOWN (some engines floor it), pinning its
|
|
76
|
+
* column exactly on the glyph boundary — then any sub-pixel
|
|
77
|
+
* font-metric difference (zoom, DPR, engine) makes that one label
|
|
78
|
+
* overflow and render an ellipsis while the shorter rows stay fine.
|
|
79
|
+
* The headroom keeps the longest label strictly inside its column. */
|
|
62
80
|
function measure() {
|
|
63
81
|
const root = listRef.value;
|
|
64
82
|
if (!root) return;
|
|
@@ -66,22 +84,55 @@ export default defineComponent({
|
|
|
66
84
|
for (const label of root.querySelectorAll<HTMLElement>(".s-auth-methods-label")) {
|
|
67
85
|
const previous = label.style.width;
|
|
68
86
|
label.style.width = "max-content";
|
|
69
|
-
widest = Math.max(widest, label.
|
|
87
|
+
widest = Math.max(widest, label.getBoundingClientRect().width);
|
|
70
88
|
label.style.width = previous;
|
|
71
89
|
}
|
|
72
|
-
|
|
90
|
+
// Guard: only publish when real text metrics were observed, so a
|
|
91
|
+
// hidden / zero-size frame keeps the CSS `max-content` fallback
|
|
92
|
+
// instead of collapsing the column to a clipping 1px.
|
|
93
|
+
if (widest > 0) {
|
|
94
|
+
root.style.setProperty("--auth-methods-label-width", `${Math.ceil(widest) + 1}px`);
|
|
95
|
+
}
|
|
96
|
+
// Keep observing every rendered row so a layout change (the panel
|
|
97
|
+
// becoming visible, a container width change) re-measures. Rows are
|
|
98
|
+
// full-width `.hk-btn-block`, so publishing the column width never
|
|
99
|
+
// resizes them (no feedback loop into the observer).
|
|
100
|
+
if (resizeObserver) {
|
|
101
|
+
for (const button of root.querySelectorAll<HTMLElement>(".hk-btn")) {
|
|
102
|
+
resizeObserver.observe(button);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
73
105
|
}
|
|
74
106
|
|
|
75
107
|
onMounted(() => {
|
|
76
108
|
void nextTick(measure);
|
|
77
109
|
// Webfonts change text metrics after first paint; re-measure when
|
|
78
110
|
// they finish loading (no-op in environments without document.fonts).
|
|
79
|
-
if (typeof document !== "undefined" && document.fonts
|
|
111
|
+
if (typeof document !== "undefined" && document.fonts) {
|
|
80
112
|
void document.fonts.ready.then(measure).catch(() => {});
|
|
113
|
+
document.fonts.addEventListener?.("loadingdone", onFontsReady);
|
|
114
|
+
}
|
|
115
|
+
// Browser zoom and container-width changes reflow the text; without a
|
|
116
|
+
// re-measure the published column goes stale and the longest label
|
|
117
|
+
// overflows into the ellipsis. `resize` covers page zoom; the
|
|
118
|
+
// ResizeObserver covers the panel appearing / the container resizing.
|
|
119
|
+
if (typeof window !== "undefined") {
|
|
120
|
+
window.addEventListener("resize", onResize);
|
|
121
|
+
if (!resizeObserver && typeof ResizeObserver !== "undefined") {
|
|
122
|
+
resizeObserver = new ResizeObserver(onResize);
|
|
123
|
+
}
|
|
81
124
|
}
|
|
82
125
|
});
|
|
83
126
|
onBeforeUnmount(() => {
|
|
84
127
|
listRef.value?.style.removeProperty("--auth-methods-label-width");
|
|
128
|
+
resizeObserver?.disconnect();
|
|
129
|
+
resizeObserver = undefined;
|
|
130
|
+
if (typeof window !== "undefined") {
|
|
131
|
+
window.removeEventListener("resize", onResize);
|
|
132
|
+
}
|
|
133
|
+
if (typeof document !== "undefined" && document.fonts) {
|
|
134
|
+
document.fonts.removeEventListener?.("loadingdone", onFontsReady);
|
|
135
|
+
}
|
|
85
136
|
});
|
|
86
137
|
watch(
|
|
87
138
|
() => props.methods.map((method) => method.label).join("\u0000"),
|