@gjsify/adwaita-web 0.51.0 → 0.52.0
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/dist/adwaita-web.css +255 -27
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/blueprint-layout.spec.d.ts +1 -0
- package/lib/types/blueprint-markup.spec.d.ts +4 -0
- package/lib/types/blueprint-tree.spec.d.ts +1 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +31 -13
- package/lib/types/elements/adw-carousel.d.ts +5 -0
- package/lib/types/elements/adw-header-bar.d.ts +15 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +1 -0
- package/lib/types/elements/adw-navigation-view.d.ts +1 -0
- package/lib/types/elements/adw-sidebar.d.ts +10 -0
- package/lib/types/elements/adw-tab-view.d.ts +1 -0
- package/lib/types/elements/adw-toggle-group.d.ts +11 -0
- package/lib/types/elements/gtk-action-bar.d.ts +19 -0
- package/lib/types/elements/gtk-box.d.ts +17 -0
- package/lib/types/elements/gtk-button.d.ts +2 -0
- package/lib/types/elements/gtk-label.d.ts +122 -0
- package/lib/types/gio-menu.spec.d.ts +1 -0
- package/lib/types/gtk-box.spec.d.ts +1 -0
- package/lib/types/gtk-label.spec.d.ts +1 -0
- package/lib/types/gtk-value-doors.spec.d.ts +1 -0
- package/lib/types/index.d.ts +4 -1
- package/lib/types/namespace/gio.d.ts +1 -0
- package/lib/types/namespace/gtk.d.ts +5 -0
- package/lib/types/shared-tree-builder.d.ts +64 -0
- package/lib/types/source-view/asm6502.d.ts +1 -20
- package/lib/types/source-view/index.d.ts +3 -2
- package/lib/types/source-view/theme.d.ts +1 -1
- package/lib/types/string-list-slot.d.ts +4 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/tags.spec.d.ts +1 -0
- package/lib/types/value-lists.spec.d.ts +1 -0
- package/package.json +8 -6
- package/scss/_action_bar.scss +51 -0
- package/scss/_banner.scss +8 -1
- package/scss/_bottom_sheet.scss +57 -0
- package/scss/_box.scss +23 -0
- package/scss/_button.scss +29 -7
- package/scss/_button_content.scss +6 -1
- package/scss/_button_row.scss +5 -2
- package/scss/_carousel.scss +6 -0
- package/scss/_labels.scss +126 -12
- package/scss/_navigation_split_view.scss +18 -0
- package/scss/_overlay_split_view.scss +5 -0
- package/scss/_reset.scss +29 -4
- package/scss/_split_button.scss +5 -2
- package/scss/_status_page.scss +31 -17
- package/scss/_theme.scss +11 -3
- package/scss/_toolbar_view.scss +10 -0
- package/scss/_variables.scss +1 -0
- package/scss/_widget.scss +55 -0
- package/scss/_window_title.scss +9 -0
- package/scss/adwaita-skin.scss +8 -2
- package/src/adw-banner.spec.ts +16 -0
- package/src/adw-button-row.spec.ts +8 -0
- package/src/adw-header-bar.spec.ts +17 -0
- package/src/blueprint-layout.spec.ts +223 -0
- package/src/blueprint-markup.spec.ts +155 -0
- package/src/blueprint-tree.spec.ts +174 -0
- package/src/bottom-sheet.spec.ts +166 -1
- package/src/chrome.spec.ts +5 -2
- package/src/elements/adw-bottom-sheet.ts +166 -59
- package/src/elements/adw-carousel.ts +10 -5
- package/src/elements/adw-clamp.ts +12 -0
- package/src/elements/adw-combo-row.ts +13 -4
- package/src/elements/adw-header-bar.ts +46 -3
- package/src/elements/adw-inline-view-switcher.ts +11 -0
- package/src/elements/adw-navigation-view.ts +10 -0
- package/src/elements/adw-sidebar.ts +22 -0
- package/src/elements/adw-spin-row.ts +44 -5
- package/src/elements/adw-status-page.ts +12 -6
- package/src/elements/adw-tab-view.ts +20 -3
- package/src/elements/adw-toggle-group.ts +45 -4
- package/src/elements/adw-toolbar-view.ts +9 -0
- package/src/elements/gtk-action-bar.ts +90 -0
- package/src/elements/gtk-box.ts +87 -0
- package/src/elements/gtk-button.ts +60 -13
- package/src/elements/gtk-drop-down.ts +10 -1
- package/src/elements/gtk-label.ts +415 -0
- package/src/gio-menu.spec.ts +125 -0
- package/src/globals.d.ts +7 -0
- package/src/gtk-box.spec.ts +168 -0
- package/src/gtk-button.spec.ts +123 -1
- package/src/gtk-label.spec.ts +595 -0
- package/src/gtk-value-doors.spec.ts +104 -0
- package/src/index.ts +14 -1
- package/src/keyboard-operable.spec.ts +2 -1
- package/src/namespace/gio.ts +19 -0
- package/src/namespace/gtk.ts +14 -0
- package/src/shared-tree-builder.ts +268 -0
- package/src/shared-trees.spec.ts +296 -32
- package/src/source-view/adw-source-view.ts +3 -3
- package/src/source-view/asm6502.ts +11 -191
- package/src/source-view/index.ts +17 -11
- package/src/source-view/theme.ts +16 -16
- package/src/split-button.spec.ts +7 -0
- package/src/string-list-slot.ts +21 -0
- package/src/styles.generated.ts +1 -1
- package/src/tags.spec.ts +51 -0
- package/src/test.browser.mts +18 -0
- package/src/value-lists.spec.ts +93 -0
- package/lib/types/source-view/hex.d.ts +0 -21
- package/src/source-view/hex.ts +0 -37
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
// DOM-level tests for <gtk-label>.
|
|
2
|
+
//
|
|
3
|
+
// Three things matter more than the attribute round-trips. The text reaches the DOM as ONE
|
|
4
|
+
// TEXT NODE whatever it holds — `use-markup` included, which is where an `innerHTML` port
|
|
5
|
+
// would execute a string. `xalign` is read off where the text actually SITS in the label's
|
|
6
|
+
// box, not off a style the element wrote. And the typography classes from `_labels.scss`
|
|
7
|
+
// apply to the element, including inside another widget whose isolation floor would
|
|
8
|
+
// otherwise repaint it (`_reset.scss`).
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
LABEL_CHAR_COUNT_VECTORS,
|
|
14
|
+
LABEL_DISPLAY_TEXT_VECTORS,
|
|
15
|
+
LABEL_EFFECTIVE_LINES_VECTORS,
|
|
16
|
+
LABEL_ELLIPSIZE_OVERFLOW_VECTORS,
|
|
17
|
+
LABEL_ELLIPSIZE_VECTORS,
|
|
18
|
+
LABEL_JUSTIFY_VECTORS,
|
|
19
|
+
LABEL_WIDTH_CHARS_EXTENT_VECTORS,
|
|
20
|
+
LABEL_WRAP_MODE_VECTORS,
|
|
21
|
+
LABEL_XALIGN_VECTORS,
|
|
22
|
+
LABEL_YALIGN_ALIGN_ITEMS_VECTORS,
|
|
23
|
+
LABEL_YALIGN_VECTORS,
|
|
24
|
+
} from '@gjsify/adwaita-core/conformance';
|
|
25
|
+
|
|
26
|
+
import type { GtkLabel } from './elements/gtk-label.js';
|
|
27
|
+
|
|
28
|
+
function mount(label: string, parent?: HTMLElement): { el: GtkLabel; host: HTMLElement } {
|
|
29
|
+
const host = document.createElement('div');
|
|
30
|
+
host.style.width = '400px';
|
|
31
|
+
document.body.appendChild(host);
|
|
32
|
+
const el = document.createElement('gtk-label') as GtkLabel;
|
|
33
|
+
el.label = label;
|
|
34
|
+
(parent ?? host).appendChild(el);
|
|
35
|
+
if (parent) host.appendChild(parent);
|
|
36
|
+
return { el, host };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Where the rendered TEXT sits, measured on the text node rather than the element. */
|
|
40
|
+
function textRect(el: HTMLElement): DOMRect {
|
|
41
|
+
const range = document.createRange();
|
|
42
|
+
range.selectNodeContents(el);
|
|
43
|
+
return range.getBoundingClientRect();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const GtkLabelTest = async () => {
|
|
47
|
+
await describe('<gtk-label> against LABEL_DISPLAY_TEXT_VECTORS', async () => {
|
|
48
|
+
for (const vector of LABEL_DISPLAY_TEXT_VECTORS) {
|
|
49
|
+
await it(vector.rule, () => {
|
|
50
|
+
const { el, host } = mount(vector.label);
|
|
51
|
+
el.useMarkup = vector.useMarkup;
|
|
52
|
+
el.useUnderline = vector.useUnderline;
|
|
53
|
+
// What reached the SCREEN, and that it is one text node: no row may build
|
|
54
|
+
// an element, whatever its markup says.
|
|
55
|
+
expect(el.textContent).toBe(vector.text);
|
|
56
|
+
expect(el.getText()).toBe(vector.text);
|
|
57
|
+
expect(el.childElementCount).toBe(0);
|
|
58
|
+
host.remove();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await describe('<gtk-label> against LABEL_XALIGN_VECTORS and LABEL_JUSTIFY_VECTORS', async () => {
|
|
64
|
+
for (const vector of LABEL_XALIGN_VECTORS) {
|
|
65
|
+
await it(`xalign: ${vector.rule}`, () => {
|
|
66
|
+
const { el, host } = mount('Hi');
|
|
67
|
+
if (vector.value === null) el.removeAttribute('xalign');
|
|
68
|
+
else el.setAttribute('xalign', String(vector.value));
|
|
69
|
+
expect(el.xalign).toBe(vector.xalign);
|
|
70
|
+
host.remove();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
for (const vector of LABEL_JUSTIFY_VECTORS) {
|
|
74
|
+
await it(`justify: ${vector.rule}`, () => {
|
|
75
|
+
const { el, host } = mount('Hi');
|
|
76
|
+
if (vector.value === null) el.removeAttribute('justify');
|
|
77
|
+
else el.setAttribute('justify', vector.value);
|
|
78
|
+
expect(el.justify).toBe(vector.justify);
|
|
79
|
+
host.remove();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
for (const vector of LABEL_YALIGN_VECTORS) {
|
|
83
|
+
await it(`yalign: ${vector.rule}`, () => {
|
|
84
|
+
const { el, host } = mount('Hi');
|
|
85
|
+
if (vector.value === null) el.removeAttribute('yalign');
|
|
86
|
+
else el.setAttribute('yalign', String(vector.value));
|
|
87
|
+
expect(el.yalign).toBe(vector.yalign);
|
|
88
|
+
host.remove();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await describe('<gtk-label> against LABEL_ELLIPSIZE_VECTORS and LABEL_WRAP_MODE_VECTORS', async () => {
|
|
94
|
+
for (const vector of LABEL_ELLIPSIZE_VECTORS) {
|
|
95
|
+
await it(`ellipsize: ${vector.rule}`, () => {
|
|
96
|
+
const { el, host } = mount('Hi');
|
|
97
|
+
if (vector.value === null) el.removeAttribute('ellipsize');
|
|
98
|
+
else el.setAttribute('ellipsize', vector.value);
|
|
99
|
+
expect(el.ellipsize).toBe(vector.ellipsize);
|
|
100
|
+
host.remove();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
for (const vector of LABEL_WRAP_MODE_VECTORS) {
|
|
104
|
+
await it(`wrap-mode: ${vector.rule}`, () => {
|
|
105
|
+
const { el, host } = mount('Hi');
|
|
106
|
+
if (vector.value === null) el.removeAttribute('wrap-mode');
|
|
107
|
+
else el.setAttribute('wrap-mode', vector.value);
|
|
108
|
+
expect(el.wrapMode).toBe(vector.wrapMode);
|
|
109
|
+
host.remove();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await describe('<gtk-label> against LABEL_CHAR_COUNT_VECTORS — lines / width-chars / max-width-chars', async () => {
|
|
115
|
+
for (const vector of LABEL_CHAR_COUNT_VECTORS) {
|
|
116
|
+
await it(`lines: ${vector.rule}`, () => {
|
|
117
|
+
const { el, host } = mount('Hi');
|
|
118
|
+
if (vector.value === null) el.removeAttribute('lines');
|
|
119
|
+
else el.setAttribute('lines', String(vector.value));
|
|
120
|
+
expect(el.lines).toBe(vector.count);
|
|
121
|
+
host.remove();
|
|
122
|
+
});
|
|
123
|
+
await it(`width-chars: ${vector.rule}`, () => {
|
|
124
|
+
const { el, host } = mount('Hi');
|
|
125
|
+
if (vector.value === null) el.removeAttribute('width-chars');
|
|
126
|
+
else el.setAttribute('width-chars', String(vector.value));
|
|
127
|
+
expect(el.widthChars).toBe(vector.count);
|
|
128
|
+
host.remove();
|
|
129
|
+
});
|
|
130
|
+
await it(`max-width-chars: ${vector.rule}`, () => {
|
|
131
|
+
const { el, host } = mount('Hi');
|
|
132
|
+
if (vector.value === null) el.removeAttribute('max-width-chars');
|
|
133
|
+
else el.setAttribute('max-width-chars', String(vector.value));
|
|
134
|
+
expect(el.maxWidthChars).toBe(vector.count);
|
|
135
|
+
host.remove();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await describe('<gtk-label> against LABEL_ELLIPSIZE_OVERFLOW_VECTORS', async () => {
|
|
141
|
+
for (const vector of LABEL_ELLIPSIZE_OVERFLOW_VECTORS) {
|
|
142
|
+
await it(vector.rule, () => {
|
|
143
|
+
const { el, host } = mount('word '.repeat(60).trim());
|
|
144
|
+
el.style.width = '200px';
|
|
145
|
+
el.ellipsize = vector.ellipsize;
|
|
146
|
+
const cs = getComputedStyle(el.querySelector('.adw-label-text') ?? el);
|
|
147
|
+
expect(cs.textOverflow).toBe(vector.overflow);
|
|
148
|
+
host.remove();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await describe('<gtk-label> against LABEL_EFFECTIVE_LINES_VECTORS', async () => {
|
|
154
|
+
for (const vector of LABEL_EFFECTIVE_LINES_VECTORS) {
|
|
155
|
+
await it(vector.rule, () => {
|
|
156
|
+
const { el, host } = mount('word '.repeat(60).trim());
|
|
157
|
+
el.ellipsize = vector.ellipsize;
|
|
158
|
+
el.lines = vector.lines;
|
|
159
|
+
const held = el.style.getPropertyValue('--gtk-label-lines');
|
|
160
|
+
expect(held).toBe(vector.effective === null ? '' : String(vector.effective));
|
|
161
|
+
host.remove();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// The regression this table exists to pin: `wrap` must not change the answer,
|
|
166
|
+
// in EITHER direction — measured against a real Gtk.Label, `label.ts`'s header.
|
|
167
|
+
await it('wrap changes nothing while ellipsize is active', () => {
|
|
168
|
+
const { el, host } = mount('word '.repeat(60).trim());
|
|
169
|
+
el.ellipsize = 'end';
|
|
170
|
+
el.lines = 2;
|
|
171
|
+
const withoutWrap = el.style.getPropertyValue('--gtk-label-lines');
|
|
172
|
+
el.wrap = true;
|
|
173
|
+
expect(el.style.getPropertyValue('--gtk-label-lines')).toBe(withoutWrap);
|
|
174
|
+
host.remove();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await it('wrap changes nothing while ellipsize is none — lines never applies', () => {
|
|
178
|
+
const { el, host } = mount('word '.repeat(60).trim());
|
|
179
|
+
el.lines = 2;
|
|
180
|
+
const withoutWrap = el.style.getPropertyValue('--gtk-label-lines');
|
|
181
|
+
el.wrap = true;
|
|
182
|
+
expect(el.style.getPropertyValue('--gtk-label-lines')).toBe(withoutWrap);
|
|
183
|
+
expect(withoutWrap).toBe('');
|
|
184
|
+
host.remove();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await describe('<gtk-label> against LABEL_WIDTH_CHARS_EXTENT_VECTORS', async () => {
|
|
189
|
+
for (const vector of LABEL_WIDTH_CHARS_EXTENT_VECTORS) {
|
|
190
|
+
await it(vector.rule, () => {
|
|
191
|
+
const { el, host } = mount('Hi');
|
|
192
|
+
el.widthChars = vector.widthChars;
|
|
193
|
+
el.maxWidthChars = vector.maxWidthChars;
|
|
194
|
+
expect(el.style.minWidth).toBe(vector.minCh === null ? '' : `${vector.minCh}ch`);
|
|
195
|
+
expect(el.style.maxWidth).toBe(vector.maxCh === null ? '' : `${vector.maxCh}ch`);
|
|
196
|
+
host.remove();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await describe('<gtk-label> text', async () => {
|
|
202
|
+
await it('shows the label as text', () => {
|
|
203
|
+
const { el, host } = mount('Hello');
|
|
204
|
+
expect(el.textContent).toBe('Hello');
|
|
205
|
+
expect(textRect(el).width).toBeGreaterThan(0);
|
|
206
|
+
host.remove();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await describe('<gtk-label> xalign and justify', async () => {
|
|
211
|
+
await it('centres the text in its box by default (xalign 0.5)', () => {
|
|
212
|
+
const { el, host } = mount('Hi');
|
|
213
|
+
el.style.width = '400px';
|
|
214
|
+
const box = el.getBoundingClientRect();
|
|
215
|
+
const text = textRect(el);
|
|
216
|
+
expect(Math.abs(text.left - box.left - (box.right - text.right)) <= 1).toBe(true);
|
|
217
|
+
host.remove();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await it('puts the text at the start for 0 and the end for 1', () => {
|
|
221
|
+
const { el, host } = mount('Hi');
|
|
222
|
+
el.style.width = '400px';
|
|
223
|
+
el.xalign = 0;
|
|
224
|
+
expect(Math.round(textRect(el).left)).toBe(Math.round(el.getBoundingClientRect().left));
|
|
225
|
+
el.xalign = 1;
|
|
226
|
+
expect(Math.round(textRect(el).right)).toBe(Math.round(el.getBoundingClientRect().right));
|
|
227
|
+
host.remove();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await it('splits the free space in the xalign ratio, as the C does', () => {
|
|
231
|
+
const { el, host } = mount('Hi');
|
|
232
|
+
el.style.width = '400px';
|
|
233
|
+
el.xalign = 0.25;
|
|
234
|
+
const box = el.getBoundingClientRect();
|
|
235
|
+
const text = textRect(el);
|
|
236
|
+
const free = box.width - text.width;
|
|
237
|
+
expect(Math.abs(text.left - box.left - free * 0.25) <= 1).toBe(true);
|
|
238
|
+
host.remove();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await it("aligns the lines through Pango's switch: left/right are start/end, fill justifies", () => {
|
|
242
|
+
const { el, host } = mount('Hi');
|
|
243
|
+
expect(el.justify).toBe('left');
|
|
244
|
+
expect(getComputedStyle(el).textAlign).toBe('start');
|
|
245
|
+
el.justify = 'center';
|
|
246
|
+
expect(getComputedStyle(el).textAlign).toBe('center');
|
|
247
|
+
el.justify = 'right';
|
|
248
|
+
expect(getComputedStyle(el).textAlign).toBe('end');
|
|
249
|
+
el.justify = 'fill';
|
|
250
|
+
expect(getComputedStyle(el).textAlign).toBe('justify');
|
|
251
|
+
host.remove();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await describe('<gtk-label> wrap and selectable', async () => {
|
|
256
|
+
await it('stays on one line unless wrap is set', () => {
|
|
257
|
+
const long = 'word '.repeat(60).trim();
|
|
258
|
+
const { el, host } = mount(long);
|
|
259
|
+
const oneLine = textRect(el).height;
|
|
260
|
+
el.wrap = true;
|
|
261
|
+
expect(textRect(el).height).toBeGreaterThan(oneLine * 2);
|
|
262
|
+
expect(el.getBoundingClientRect().width <= 400).toBe(true);
|
|
263
|
+
host.remove();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await it('centres a WRAPPED label by xalign too, not just a one-line one', () => {
|
|
267
|
+
// GTK centres the wrapped BLOCK (gtk_label_get_layout_location), not each line
|
|
268
|
+
// independently — the defect this vector was written for: the block used to
|
|
269
|
+
// fill the full width with no inset at all once it needed more than one line.
|
|
270
|
+
//
|
|
271
|
+
// Measured on the wrap SPAN's own box, not `textRect()`'s `Range` (every other
|
|
272
|
+
// vector in this file uses it): `white-space: pre-wrap` lets a wrapped line's
|
|
273
|
+
// trailing separator space HANG past its line box (CSS Text 3 § white-space-
|
|
274
|
+
// phase-2, a real browser behaviour and not a bug here), which widens a
|
|
275
|
+
// `Range`'s bounding rect on the side a line wrapped without moving anything a
|
|
276
|
+
// viewer can see — the span's own layout box is what is actually centred.
|
|
277
|
+
const long = 'word '.repeat(60).trim();
|
|
278
|
+
const { el, host } = mount(long);
|
|
279
|
+
el.wrap = true;
|
|
280
|
+
const box = el.getBoundingClientRect();
|
|
281
|
+
const span = el.querySelector('.adw-label-text')!.getBoundingClientRect();
|
|
282
|
+
expect(span.width < box.width).toBe(true);
|
|
283
|
+
expect(Math.abs(span.left - box.left - (box.right - span.right)) <= 1).toBe(true);
|
|
284
|
+
host.remove();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await it('keeps a wrapped block flush with the edge at xalign 0, no inset', () => {
|
|
288
|
+
const long = 'word '.repeat(60).trim();
|
|
289
|
+
const { el, host } = mount(long);
|
|
290
|
+
el.wrap = true;
|
|
291
|
+
el.xalign = 0;
|
|
292
|
+
const span = el.querySelector('.adw-label-text')!.getBoundingClientRect();
|
|
293
|
+
expect(Math.round(span.left)).toBe(Math.round(el.getBoundingClientRect().left));
|
|
294
|
+
host.remove();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// The block's width is measured, so it has to be measured again when the space
|
|
298
|
+
// changes. Widening is the direction that proves it: narrowing is also caught by
|
|
299
|
+
// the span's `max-width: 100%`, but a block pinned at 200px stays 200px wide in a
|
|
300
|
+
// 400px container until something measures it again.
|
|
301
|
+
await it('re-measures a wrapped block when its container widens', async () => {
|
|
302
|
+
const long = 'word '.repeat(60).trim();
|
|
303
|
+
const { el, host } = mount(long);
|
|
304
|
+
host.style.width = '200px';
|
|
305
|
+
el.wrap = true;
|
|
306
|
+
// Settle the one-off font re-measure first, or it would widen the block on its own.
|
|
307
|
+
await document.fonts.ready;
|
|
308
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
309
|
+
host.style.width = '400px';
|
|
310
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
311
|
+
const span = el.querySelector('.adw-label-text')!.getBoundingClientRect();
|
|
312
|
+
expect(span.width > 300).toBe(true);
|
|
313
|
+
host.remove();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await it('is not selectable unless asked, as GtkLabel', () => {
|
|
317
|
+
const { el, host } = mount('Hi');
|
|
318
|
+
expect(getComputedStyle(el).userSelect).toBe('none');
|
|
319
|
+
el.selectable = true;
|
|
320
|
+
expect(getComputedStyle(el).userSelect).toBe('text');
|
|
321
|
+
host.remove();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await describe('<gtk-label> ellipsize', async () => {
|
|
326
|
+
const long = 'word '.repeat(60).trim();
|
|
327
|
+
|
|
328
|
+
await it('stays as wide as its text when ellipsize is none, the default', () => {
|
|
329
|
+
// `host`, not `el`, is constrained: `gtk-label` is a BLOCK-level flex
|
|
330
|
+
// container and fills its own containing block by default (same as a plain
|
|
331
|
+
// `<div>`), so pinning `el.style.width` would force the box regardless of
|
|
332
|
+
// ellipsize — the text overflowing that box, measured on the text itself, is
|
|
333
|
+
// what "not ellipsized" actually means.
|
|
334
|
+
const { el, host } = mount(long);
|
|
335
|
+
host.style.width = '200px';
|
|
336
|
+
expect(textRect(el).width).toBeGreaterThan(200);
|
|
337
|
+
expect(el.childElementCount).toBe(0);
|
|
338
|
+
host.remove();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
await it('trims to the box and keeps the full text in the property, for end', () => {
|
|
342
|
+
const { el, host } = mount(long);
|
|
343
|
+
el.style.width = '200px';
|
|
344
|
+
el.ellipsize = 'end';
|
|
345
|
+
const box = el.getBoundingClientRect();
|
|
346
|
+
expect(box.width).toBeLessThanOrEqual(201);
|
|
347
|
+
// Held faithfully: the PROPERTY still carries the whole string, as GTK's
|
|
348
|
+
// `label` does — only what the box can show is trimmed.
|
|
349
|
+
expect(el.label).toBe(long);
|
|
350
|
+
expect(el.getText()).toBe(long);
|
|
351
|
+
host.remove();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
await it('draws the SAME end-ellipsis for start and middle — the declared divergence', () => {
|
|
355
|
+
const endEl = mount(long);
|
|
356
|
+
endEl.el.style.width = '200px';
|
|
357
|
+
endEl.el.ellipsize = 'end';
|
|
358
|
+
const startEl = mount(long);
|
|
359
|
+
startEl.el.style.width = '200px';
|
|
360
|
+
startEl.el.ellipsize = 'start';
|
|
361
|
+
const middleEl = mount(long);
|
|
362
|
+
middleEl.el.style.width = '200px';
|
|
363
|
+
middleEl.el.ellipsize = 'middle';
|
|
364
|
+
// `ellipsize` is held exactly as authored…
|
|
365
|
+
expect(startEl.el.ellipsize).toBe('start');
|
|
366
|
+
expect(middleEl.el.ellipsize).toBe('middle');
|
|
367
|
+
// …while every non-`none` mode renders identically to `end`, the one
|
|
368
|
+
// truncating value CSS actually has (`labelEllipsizeOverflowValue`).
|
|
369
|
+
expect(Math.round(startEl.el.getBoundingClientRect().width)).toBe(
|
|
370
|
+
Math.round(endEl.el.getBoundingClientRect().width),
|
|
371
|
+
);
|
|
372
|
+
expect(Math.round(middleEl.el.getBoundingClientRect().width)).toBe(
|
|
373
|
+
Math.round(endEl.el.getBoundingClientRect().width),
|
|
374
|
+
);
|
|
375
|
+
endEl.host.remove();
|
|
376
|
+
startEl.host.remove();
|
|
377
|
+
middleEl.host.remove();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
await it('centres a SHORT ellipsized label by xalign, exactly as the plain text node does', () => {
|
|
381
|
+
// A label that FITS has free space regardless of `ellipsize` — the box only
|
|
382
|
+
// shrinks to the container once the text no longer fits one line.
|
|
383
|
+
const { el, host } = mount('Hi');
|
|
384
|
+
el.style.width = '400px';
|
|
385
|
+
el.ellipsize = 'end';
|
|
386
|
+
const box = el.getBoundingClientRect();
|
|
387
|
+
const span = el.querySelector('.adw-label-text')!.getBoundingClientRect();
|
|
388
|
+
expect(Math.abs(span.left - box.left - (box.right - span.right)) <= 1).toBe(true);
|
|
389
|
+
host.remove();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
await describe('<gtk-label> wrap-mode', async () => {
|
|
394
|
+
// ONE unbreakable "word", no space for the default WORD mode to break at.
|
|
395
|
+
const unbreakable = 'x'.repeat(80);
|
|
396
|
+
|
|
397
|
+
await it('overflows its container under the default, word, with nothing to break at', () => {
|
|
398
|
+
// `.adw-label-text { max-width: 100% }` caps the SPAN's own BOX at the
|
|
399
|
+
// container, wrap-mode or not — the difference is whether the TEXT inside it
|
|
400
|
+
// overflows that box, which is what `textRect` (on the span, not the host)
|
|
401
|
+
// measures.
|
|
402
|
+
const { el, host } = mount(unbreakable);
|
|
403
|
+
host.style.width = '100px';
|
|
404
|
+
el.wrap = true;
|
|
405
|
+
const span = el.querySelector('.adw-label-text') as HTMLElement;
|
|
406
|
+
expect(textRect(span).width).toBeGreaterThan(100);
|
|
407
|
+
host.remove();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await it('breaks mid-word and fits its container under char', () => {
|
|
411
|
+
const { el, host } = mount(unbreakable);
|
|
412
|
+
host.style.width = '100px';
|
|
413
|
+
el.wrap = true;
|
|
414
|
+
el.wrapMode = 'char';
|
|
415
|
+
const span = el.querySelector('.adw-label-text') as HTMLElement;
|
|
416
|
+
expect(textRect(span).width).toBeLessThanOrEqual(100);
|
|
417
|
+
host.remove();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
await it("has no effect while wrap is off, as the pspec says ('only … if line wrapping is on')", () => {
|
|
421
|
+
const { el, host } = mount(unbreakable);
|
|
422
|
+
const withoutMode = textRect(el).width;
|
|
423
|
+
el.wrapMode = 'char';
|
|
424
|
+
expect(textRect(el).width).toBe(withoutMode);
|
|
425
|
+
host.remove();
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
await describe('<gtk-label> lines — gated by ellipsize alone, wrap plays no part', async () => {
|
|
430
|
+
const paragraph = 'word '.repeat(60).trim();
|
|
431
|
+
|
|
432
|
+
// THE FINDING THIS BLOCK EXISTS TO PIN: `wrap=false` does NOT keep an ellipsized,
|
|
433
|
+
// `lines`-capped label to one line — a real `Gtk.Label` allocated with
|
|
434
|
+
// `wrap=FALSE, ellipsize=END, lines=2` lays out exactly two lines, ellipsized
|
|
435
|
+
// (`label.ts`'s header, `@gjsify/adwaita-core`). No `wrap` is set anywhere below.
|
|
436
|
+
await it('wraps onto multiple lines from ellipsize + lines ALONE, wrap never set', () => {
|
|
437
|
+
const { el, host } = mount(paragraph);
|
|
438
|
+
host.style.width = '150px';
|
|
439
|
+
el.ellipsize = 'end';
|
|
440
|
+
el.lines = 2;
|
|
441
|
+
const span = el.querySelector('.adw-label-text')!;
|
|
442
|
+
// Two lines of wrapped text, not one: the span's rendered text spans more than
|
|
443
|
+
// a single line's height.
|
|
444
|
+
const oneLine = mount('word').el;
|
|
445
|
+
oneLine.ellipsize = 'end';
|
|
446
|
+
const oneLineHeight = oneLine.querySelector('.adw-label-text')!.getBoundingClientRect().height;
|
|
447
|
+
expect(span.getBoundingClientRect().height).toBeGreaterThan(oneLineHeight * 1.5);
|
|
448
|
+
oneLine.parentElement?.remove();
|
|
449
|
+
host.remove();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await it('caps at 2 lines rather than the 1-line default a real allocation gives', () => {
|
|
453
|
+
const { el: capped, host: cappedHost } = mount(paragraph);
|
|
454
|
+
cappedHost.style.width = '150px';
|
|
455
|
+
capped.ellipsize = 'end';
|
|
456
|
+
capped.lines = 2;
|
|
457
|
+
|
|
458
|
+
const { el: oneLine, host: oneLineHost } = mount(paragraph);
|
|
459
|
+
oneLineHost.style.width = '150px';
|
|
460
|
+
oneLine.ellipsize = 'end';
|
|
461
|
+
// `lines` left unset: Pango's own default is ONE line, not unlimited.
|
|
462
|
+
|
|
463
|
+
const cappedHeight = capped.querySelector('.adw-label-text')!.getBoundingClientRect().height;
|
|
464
|
+
const oneLineHeight = oneLine.querySelector('.adw-label-text')!.getBoundingClientRect().height;
|
|
465
|
+
expect(cappedHeight).toBeGreaterThan(oneLineHeight * 1.5);
|
|
466
|
+
expect(cappedHeight).toBeLessThan(oneLineHeight * 2.5);
|
|
467
|
+
cappedHost.remove();
|
|
468
|
+
oneLineHost.remove();
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
await it('still caps below the TRUE unclamped height once ellipsize is none', () => {
|
|
472
|
+
const { el: capped, host: cappedHost } = mount(paragraph);
|
|
473
|
+
cappedHost.style.width = '150px';
|
|
474
|
+
capped.ellipsize = 'end';
|
|
475
|
+
capped.lines = 2;
|
|
476
|
+
|
|
477
|
+
const { el: unclamped, host: unclampedHost } = mount(paragraph);
|
|
478
|
+
unclampedHost.style.width = '150px';
|
|
479
|
+
unclamped.wrap = true;
|
|
480
|
+
// `ellipsize` stays `none`: wraps freely onto however many lines the text needs.
|
|
481
|
+
|
|
482
|
+
const cappedHeight = capped.querySelector('.adw-label-text')!.getBoundingClientRect().height;
|
|
483
|
+
const unclampedHeight = unclamped.querySelector('.adw-label-text')!.getBoundingClientRect().height;
|
|
484
|
+
expect(cappedHeight).toBeLessThan(unclampedHeight);
|
|
485
|
+
cappedHost.remove();
|
|
486
|
+
unclampedHost.remove();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
await it('has no effect at all while ellipsize is none, however lines is set', () => {
|
|
490
|
+
const { el: capped, host: cappedHost } = mount(paragraph);
|
|
491
|
+
capped.lines = 1;
|
|
492
|
+
const { el: free, host: freeHost } = mount(paragraph);
|
|
493
|
+
expect(Math.round(capped.getBoundingClientRect().width)).toBe(
|
|
494
|
+
Math.round(free.getBoundingClientRect().width),
|
|
495
|
+
);
|
|
496
|
+
cappedHost.remove();
|
|
497
|
+
freeHost.remove();
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
await describe('<gtk-label> yalign', async () => {
|
|
502
|
+
await it('sits the text at the top for 0 and the bottom for 1, in a taller box', () => {
|
|
503
|
+
const { el, host } = mount('Hi');
|
|
504
|
+
el.style.height = '100px';
|
|
505
|
+
el.yalign = 0;
|
|
506
|
+
const top = textRect(el);
|
|
507
|
+
const box = el.getBoundingClientRect();
|
|
508
|
+
expect(Math.round(top.top)).toBe(Math.round(box.top));
|
|
509
|
+
el.yalign = 1;
|
|
510
|
+
const bottom = textRect(el);
|
|
511
|
+
expect(Math.round(bottom.bottom)).toBe(Math.round(box.bottom));
|
|
512
|
+
host.remove();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
await it('centres by default, 0.5, the same box the two edges measured against', () => {
|
|
516
|
+
const { el, host } = mount('Hi');
|
|
517
|
+
el.style.height = '100px';
|
|
518
|
+
const box = el.getBoundingClientRect();
|
|
519
|
+
const text = textRect(el);
|
|
520
|
+
const topGap = text.top - box.top;
|
|
521
|
+
const bottomGap = box.bottom - text.bottom;
|
|
522
|
+
expect(Math.abs(topGap - bottomGap) <= 1).toBe(true);
|
|
523
|
+
host.remove();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
for (const vector of LABEL_YALIGN_ALIGN_ITEMS_VECTORS) {
|
|
527
|
+
await it(`align-items: ${vector.rule}`, () => {
|
|
528
|
+
const { el, host } = mount('Hi');
|
|
529
|
+
el.yalign = vector.yalign;
|
|
530
|
+
expect(getComputedStyle(el).alignItems).toBe(vector.alignItems);
|
|
531
|
+
host.remove();
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
await it('sits an intermediate yalign (0.3) at the SAME top gap as 0.5 — true nearest, not a half-split', () => {
|
|
536
|
+
// The exact regression a strict `< 0.5`/`> 0.5` split gets wrong: 0.3 is
|
|
537
|
+
// NEARER 0.5 than 0, so it must render identically to 0.5, not to 0.
|
|
538
|
+
const { el, host } = mount('Hi');
|
|
539
|
+
el.style.height = '100px';
|
|
540
|
+
el.yalign = 0.5;
|
|
541
|
+
const centredTop = textRect(el).top;
|
|
542
|
+
el.yalign = 0.3;
|
|
543
|
+
expect(Math.round(textRect(el).top)).toBe(Math.round(centredTop));
|
|
544
|
+
host.remove();
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
await describe('<gtk-label> width-chars and max-width-chars', async () => {
|
|
549
|
+
await it('widens a short label to at least width-chars characters', () => {
|
|
550
|
+
// `gtk-label` is a BLOCK-level flex container and fills the 400px `host` by
|
|
551
|
+
// default (same as a plain `<div>` would) — `min-width` has nothing to widen
|
|
552
|
+
// against there, so both sides shrink-to-fit with `display: inline-flex`
|
|
553
|
+
// first, a test-only override that does not touch the component's own CSS.
|
|
554
|
+
const { el: plain, host: plainHost } = mount('Hi');
|
|
555
|
+
plain.style.display = 'inline-flex';
|
|
556
|
+
const { el: widened, host: widenedHost } = mount('Hi');
|
|
557
|
+
widened.style.display = 'inline-flex';
|
|
558
|
+
widened.widthChars = 20;
|
|
559
|
+
expect(widened.getBoundingClientRect().width).toBeGreaterThan(plain.getBoundingClientRect().width);
|
|
560
|
+
plainHost.remove();
|
|
561
|
+
widenedHost.remove();
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
await describe('<gtk-label> and the typography classes', async () => {
|
|
566
|
+
await it('takes .title-1 and .heading from _labels.scss', () => {
|
|
567
|
+
const { el, host } = mount('Title');
|
|
568
|
+
el.classList.add('title-1');
|
|
569
|
+
expect(getComputedStyle(el).fontWeight).toBe('800');
|
|
570
|
+
el.classList.replace('title-1', 'heading');
|
|
571
|
+
expect(getComputedStyle(el).fontWeight).toBe('700');
|
|
572
|
+
host.remove();
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
await it('dims under .dimmed', () => {
|
|
576
|
+
// `.dim-label` is deliberately NOT here: `style-classes.spec.ts` ledgers it as
|
|
577
|
+
// deprecated in favour of this one.
|
|
578
|
+
const { el, host } = mount('Quiet');
|
|
579
|
+
expect(getComputedStyle(el).opacity).toBe('1');
|
|
580
|
+
el.classList.add('dimmed');
|
|
581
|
+
expect(getComputedStyle(el).opacity === '1').toBe(false);
|
|
582
|
+
host.remove();
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
await it('inherits colour and size inside another widget instead of the isolation floor', () => {
|
|
586
|
+
const clamp = document.createElement('adw-clamp');
|
|
587
|
+
clamp.style.color = 'rgb(255, 0, 0)';
|
|
588
|
+
clamp.style.fontSize = '30px';
|
|
589
|
+
const { el, host } = mount('Inside', clamp);
|
|
590
|
+
expect(getComputedStyle(el).color).toBe('rgb(255, 0, 0)');
|
|
591
|
+
expect(getComputedStyle(el).fontSize).toBe('30px');
|
|
592
|
+
host.remove();
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
};
|