@gjsify/adwaita-web 0.31.0 → 0.32.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 +786 -272
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-about-dialog.spec.d.ts +1 -0
- package/lib/types/adw-action-rows.spec.d.ts +1 -0
- package/lib/types/adw-avatar.spec.d.ts +1 -0
- package/lib/types/adw-banner.spec.d.ts +1 -0
- package/lib/types/adw-button-content.spec.d.ts +1 -0
- package/lib/types/adw-carousel.spec.d.ts +1 -0
- package/lib/types/adw-checks.spec.d.ts +1 -0
- package/lib/types/adw-header-bar.spec.d.ts +1 -0
- package/lib/types/adw-icon.spec.d.ts +1 -0
- package/lib/types/adw-progress-bar.spec.d.ts +1 -0
- package/lib/types/adw-sidebar.spec.d.ts +1 -0
- package/lib/types/adw-switch.spec.d.ts +1 -0
- package/lib/types/adw-wrap-box.spec.d.ts +1 -0
- package/lib/types/bottom-sheet.spec.d.ts +1 -0
- package/lib/types/breakpoints.d.ts +22 -0
- package/lib/types/breakpoints.spec.d.ts +1 -0
- package/lib/types/chrome.spec.d.ts +1 -0
- package/lib/types/elements/adw-about-dialog.d.ts +47 -8
- package/lib/types/elements/adw-action-row.d.ts +35 -0
- package/lib/types/elements/adw-avatar.d.ts +11 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
- package/lib/types/elements/adw-button-content.d.ts +12 -0
- package/lib/types/elements/adw-button-row.d.ts +10 -1
- package/lib/types/elements/adw-carousel.d.ts +111 -8
- package/lib/types/elements/adw-checks.d.ts +57 -0
- package/lib/types/elements/adw-clamp.d.ts +4 -1
- package/lib/types/elements/adw-data-grid.d.ts +11 -31
- package/lib/types/elements/adw-drop-down.d.ts +17 -20
- package/lib/types/elements/adw-entry-row.d.ts +93 -3
- package/lib/types/elements/adw-expander-row.d.ts +7 -1
- package/lib/types/elements/adw-header-bar.d.ts +15 -0
- package/lib/types/elements/adw-icon.d.ts +25 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +37 -11
- package/lib/types/elements/adw-menu-button.d.ts +6 -6
- package/lib/types/elements/adw-navigation-split-view.d.ts +6 -0
- package/lib/types/elements/adw-navigation-view.d.ts +63 -20
- package/lib/types/elements/adw-overlay-split-view.d.ts +38 -0
- package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
- package/lib/types/elements/adw-popover.d.ts +58 -0
- package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
- package/lib/types/elements/adw-preferences-group.d.ts +31 -0
- package/lib/types/elements/adw-progress-bar.d.ts +38 -0
- package/lib/types/elements/adw-sidebar.d.ts +37 -5
- package/lib/types/elements/adw-split-button.d.ts +53 -7
- package/lib/types/elements/adw-switch-row.d.ts +11 -2
- package/lib/types/elements/adw-switch.d.ts +15 -0
- package/lib/types/elements/adw-tab-view.d.ts +129 -12
- package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
- package/lib/types/elements/adw-view-stack.d.ts +27 -19
- package/lib/types/elements/adw-view-switcher-bar.d.ts +23 -7
- package/lib/types/elements/adw-view-switcher.d.ts +22 -11
- package/lib/types/elements/adw-window-title.d.ts +3 -1
- package/lib/types/elements/view-switcher-dom.d.ts +44 -0
- package/lib/types/entry-rows.spec.d.ts +1 -0
- package/lib/types/index.d.ts +7 -0
- package/lib/types/navigation-view.spec.d.ts +1 -0
- package/lib/types/preferences-search.d.ts +20 -0
- package/lib/types/preferences.spec.d.ts +1 -0
- package/lib/types/split-button.spec.d.ts +1 -0
- package/lib/types/split-views.spec.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/tab-view.spec.d.ts +1 -0
- package/lib/types/view-stack.spec.d.ts +1 -0
- package/lib/types/view-switcher.spec.d.ts +1 -0
- package/package.json +6 -6
- package/scss/_banner.scss +13 -0
- package/scss/_bottom_sheet.scss +5 -6
- package/scss/_button.scss +3 -26
- package/scss/_button_content.scss +28 -0
- package/scss/_checks.scss +228 -0
- package/scss/_clamp.scss +9 -3
- package/scss/_data_grid.scss +15 -7
- package/scss/_drop_down.scss +12 -55
- package/scss/_expander_row.scss +5 -50
- package/scss/_icon.scss +51 -0
- package/scss/_icons.generated.scss +6 -0
- package/scss/_inline_view_switcher.scss +34 -0
- package/scss/_labels.scss +135 -0
- package/scss/_menu_button.scss +10 -70
- package/scss/_popover.scss +186 -0
- package/scss/_preferences.scss +29 -0
- package/scss/_preferences_dialog.scss +17 -0
- package/scss/_progress_bar.scss +161 -0
- package/scss/_reset.scss +18 -7
- package/scss/_spinner.scss +7 -3
- package/scss/_split_button.scss +6 -50
- package/scss/_switch.scss +90 -0
- package/scss/_switch_row.scss +6 -52
- package/scss/_tab_view.scss +64 -0
- package/scss/_theme.scss +10 -0
- package/scss/_toolbar_view.scss +74 -0
- package/scss/_variables.scss +34 -0
- package/scss/_view_switcher.scss +40 -0
- package/scss/_view_switcher_bar.scss +37 -0
- package/scss/_wrap_box.scss +56 -6
- package/scss/adwaita-skin.scss +23 -0
- package/src/adw-about-dialog.spec.ts +350 -0
- package/src/adw-action-rows.spec.ts +342 -0
- package/src/adw-avatar.spec.ts +139 -0
- package/src/adw-banner.spec.ts +152 -0
- package/src/adw-button-content.spec.ts +183 -0
- package/src/adw-button-row.spec.ts +12 -6
- package/src/adw-carousel.spec.ts +389 -0
- package/src/adw-checks.spec.ts +368 -0
- package/src/adw-drop-down.spec.ts +123 -0
- package/src/adw-header-bar.spec.ts +83 -0
- package/src/adw-icon.spec.ts +167 -0
- package/src/adw-progress-bar.spec.ts +173 -0
- package/src/adw-sidebar.spec.ts +332 -0
- package/src/adw-switch.spec.ts +242 -0
- package/src/adw-tab-view.spec.ts +12 -3
- package/src/adw-view-switcher-bar.spec.ts +61 -13
- package/src/adw-wrap-box.spec.ts +194 -0
- package/src/bottom-sheet.spec.ts +325 -0
- package/src/breakpoints.spec.ts +147 -0
- package/src/breakpoints.ts +67 -0
- package/src/chrome.spec.ts +301 -0
- package/src/elements/adw-about-dialog.ts +223 -163
- package/src/elements/adw-action-row.ts +120 -10
- package/src/elements/adw-avatar.ts +48 -54
- package/src/elements/adw-banner.ts +46 -10
- package/src/elements/adw-bottom-sheet.ts +145 -60
- package/src/elements/adw-button-content.ts +107 -33
- package/src/elements/adw-button-row.ts +61 -37
- package/src/elements/adw-button.ts +5 -7
- package/src/elements/adw-carousel.ts +467 -105
- package/src/elements/adw-checks.ts +299 -0
- package/src/elements/adw-clamp.ts +80 -10
- package/src/elements/adw-combo-row.ts +15 -7
- package/src/elements/adw-data-grid.ts +43 -91
- package/src/elements/adw-drop-down.ts +117 -128
- package/src/elements/adw-entry-row.ts +404 -36
- package/src/elements/adw-expander-row.ts +61 -29
- package/src/elements/adw-header-bar.ts +31 -6
- package/src/elements/adw-icon.ts +148 -0
- package/src/elements/adw-inline-view-switcher.ts +240 -129
- package/src/elements/adw-menu-button.ts +93 -70
- package/src/elements/adw-navigation-split-view.ts +48 -7
- package/src/elements/adw-navigation-view.ts +286 -148
- package/src/elements/adw-overlay-split-view.ts +164 -14
- package/src/elements/adw-password-entry-row.ts +111 -89
- package/src/elements/adw-popover.ts +283 -0
- package/src/elements/adw-preferences-dialog.ts +120 -21
- package/src/elements/adw-preferences-group.ts +128 -22
- package/src/elements/adw-progress-bar.ts +189 -0
- package/src/elements/adw-sidebar.ts +301 -142
- package/src/elements/adw-spin-row.ts +11 -5
- package/src/elements/adw-spinner.ts +23 -8
- package/src/elements/adw-split-button.ts +371 -92
- package/src/elements/adw-status-page.ts +9 -9
- package/src/elements/adw-switch-row.ts +100 -30
- package/src/elements/adw-switch.ts +129 -0
- package/src/elements/adw-tab-view.ts +682 -129
- package/src/elements/adw-toggle-group.ts +5 -7
- package/src/elements/adw-toolbar-view.ts +84 -21
- package/src/elements/adw-view-stack.ts +162 -85
- package/src/elements/adw-view-switcher-bar.ts +163 -81
- package/src/elements/adw-view-switcher.ts +231 -112
- package/src/elements/adw-window-title.ts +44 -11
- package/src/elements/adw-wrap-box.ts +131 -35
- package/src/elements/view-switcher-dom.ts +108 -0
- package/src/entry-rows.spec.ts +391 -0
- package/src/index.ts +23 -0
- package/src/navigation-view.spec.ts +435 -0
- package/src/preferences-search.ts +135 -0
- package/src/preferences.spec.ts +250 -0
- package/src/source-view/adw-source-view.ts +2 -5
- package/src/split-button.spec.ts +470 -0
- package/src/split-views.spec.ts +309 -0
- package/src/styles.generated.ts +1 -1
- package/src/tab-view.spec.ts +451 -0
- package/src/test.browser.mts +49 -0
- package/src/view-stack.spec.ts +272 -0
- package/src/view-switcher.spec.ts +446 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// DOM-level conformance tests for <adw-entry-row> and <adw-password-entry-row>,
|
|
2
|
+
// driven by the SAME vectors the NativeScript renderer asserts against
|
|
3
|
+
// (`@gjsify/adwaita-core/conformance`).
|
|
4
|
+
//
|
|
5
|
+
// Everything these vectors describe used to be MISSING here, not merely
|
|
6
|
+
// different: `update_empty`'s truth table, the apply button (declared `true` in
|
|
7
|
+
// the shared story metadata all along), the Enter key, max-length, the caps-lock
|
|
8
|
+
// warning and its two suppression rules. Nothing failed, because nothing
|
|
9
|
+
// compared this renderer to the C. This suite is that comparison — it drives the
|
|
10
|
+
// real elements through each scenario and reads back what a user would see.
|
|
11
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
ENTRY_MAX_LENGTH_VECTORS,
|
|
15
|
+
ENTRY_ROW_ACTIVATION_VECTORS,
|
|
16
|
+
ENTRY_ROW_STATE_VECTORS,
|
|
17
|
+
ENTRY_TEXT_LENGTH_VECTORS,
|
|
18
|
+
PASSWORD_ENTRY_ROW_VECTORS,
|
|
19
|
+
PASSWORD_REVEAL_GUARD_VECTORS,
|
|
20
|
+
type EntryRowStep,
|
|
21
|
+
type PasswordEntryRowStep,
|
|
22
|
+
} from '@gjsify/adwaita-core/conformance';
|
|
23
|
+
|
|
24
|
+
import type { AdwEntryRow } from './elements/adw-entry-row.js';
|
|
25
|
+
import type { AdwPasswordEntryRow } from './elements/adw-password-entry-row.js';
|
|
26
|
+
|
|
27
|
+
/** The rendered parts one render snapshot drives. */
|
|
28
|
+
interface RowParts {
|
|
29
|
+
row: AdwEntryRow;
|
|
30
|
+
host: HTMLElement;
|
|
31
|
+
input: HTMLInputElement;
|
|
32
|
+
emptyTitle: HTMLElement;
|
|
33
|
+
title: HTMLElement;
|
|
34
|
+
editIcon: HTMLElement;
|
|
35
|
+
indicator: HTMLElement;
|
|
36
|
+
applyButton: HTMLButtonElement;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Mount a row of `tag` and collect its parts. `prefix` is the element's part-class prefix. */
|
|
40
|
+
function mount(tag: string, prefix: string, markup = ''): RowParts {
|
|
41
|
+
const host = document.createElement('div');
|
|
42
|
+
document.body.appendChild(host);
|
|
43
|
+
host.innerHTML = `<${tag} title="Display name">${markup}</${tag}>`;
|
|
44
|
+
const row = host.querySelector(tag) as AdwEntryRow;
|
|
45
|
+
return {
|
|
46
|
+
row,
|
|
47
|
+
host,
|
|
48
|
+
input: row.querySelector(`.${prefix}-input`) as HTMLInputElement,
|
|
49
|
+
emptyTitle: row.querySelector(`.${prefix}-empty-title`) as HTMLElement,
|
|
50
|
+
title: row.querySelector(`.${prefix}-title`) as HTMLElement,
|
|
51
|
+
editIcon: row.querySelector(`.${prefix}-edit`) as HTMLElement,
|
|
52
|
+
indicator: row.querySelector(`.${prefix}-indicator`) as HTMLElement,
|
|
53
|
+
applyButton: row.querySelector(`.${prefix}-apply`) as HTMLButtonElement,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Replay one vector step against the real element, through the API a consumer
|
|
59
|
+
* would use — focus/blur for `editing`, the button for `apply`.
|
|
60
|
+
*/
|
|
61
|
+
function applyStep(parts: RowParts, step: EntryRowStep): void {
|
|
62
|
+
switch (step.op) {
|
|
63
|
+
case 'setText':
|
|
64
|
+
parts.row.text = step.value;
|
|
65
|
+
return;
|
|
66
|
+
case 'setEditing':
|
|
67
|
+
if (step.value) parts.input.focus();
|
|
68
|
+
else parts.input.blur();
|
|
69
|
+
return;
|
|
70
|
+
case 'setEditable':
|
|
71
|
+
parts.row.editable = step.value;
|
|
72
|
+
return;
|
|
73
|
+
case 'setMaxLength':
|
|
74
|
+
parts.row.maxLength = step.value;
|
|
75
|
+
return;
|
|
76
|
+
case 'setShowApplyButton':
|
|
77
|
+
parts.row.showApplyButton = step.value;
|
|
78
|
+
return;
|
|
79
|
+
case 'setShowIndicator':
|
|
80
|
+
parts.row.setShowIndicator(step.value);
|
|
81
|
+
return;
|
|
82
|
+
case 'setActivatesDefault':
|
|
83
|
+
parts.row.activatesDefault = step.value;
|
|
84
|
+
return;
|
|
85
|
+
case 'apply':
|
|
86
|
+
parts.applyButton.click();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Assert the whole render snapshot against the DOM. */
|
|
92
|
+
function expectSnapshot(parts: RowParts, expected: (typeof ENTRY_ROW_STATE_VECTORS)[number]['expected']): void {
|
|
93
|
+
expect(parts.input.value).toBe(expected.text);
|
|
94
|
+
expect(parts.row.getAttribute('text-length')).toBe(String(expected.textLength));
|
|
95
|
+
expect(parts.row.classList.contains('empty')).toBe(expected.empty);
|
|
96
|
+
expect(parts.row.classList.contains('focused')).toBe(expected.editing);
|
|
97
|
+
expect(parts.input.readOnly).toBe(!expected.editable);
|
|
98
|
+
expect(parts.emptyTitle.hidden).toBe(expected.emptyTarget !== 0);
|
|
99
|
+
expect(parts.title.hidden).toBe(expected.emptyTarget !== 1);
|
|
100
|
+
expect(parts.editIcon.hidden).toBe(!expected.editIconVisible);
|
|
101
|
+
expect(parts.editIcon.getAttribute('aria-disabled')).toBe(String(!expected.editIconSensitive));
|
|
102
|
+
expect(parts.indicator.hidden).toBe(!expected.indicatorVisible);
|
|
103
|
+
expect(parts.applyButton.hidden).toBe(!expected.applyButtonVisible);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Collect the names of `events` dispatched on `el`, in order. */
|
|
107
|
+
function recordEvents(el: HTMLElement, events: string[]): { names: string[] } {
|
|
108
|
+
const names: string[] = [];
|
|
109
|
+
for (const name of events) el.addEventListener(name, () => names.push(name));
|
|
110
|
+
return { names };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Press Enter in the field the way a user would. */
|
|
114
|
+
function pressEnter(parts: RowParts): void {
|
|
115
|
+
parts.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const AdwEntryRowsTest = async () => {
|
|
119
|
+
await describe('adw-entry-row update_empty (libadwaita conformance vectors)', async () => {
|
|
120
|
+
for (const { name, steps, expected, rule } of ENTRY_ROW_STATE_VECTORS) {
|
|
121
|
+
await it(`${name} — ${rule}`, () => {
|
|
122
|
+
const parts = mount('adw-entry-row', 'adw-entry-row');
|
|
123
|
+
for (const step of steps) applyStep(parts, step);
|
|
124
|
+
expectSnapshot(parts, expected);
|
|
125
|
+
parts.host.remove();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await it('reflects the CHARACTER count, not the UTF-16 unit count', () => {
|
|
130
|
+
const parts = mount('adw-entry-row', 'adw-entry-row');
|
|
131
|
+
for (const { text, length } of ENTRY_TEXT_LENGTH_VECTORS) {
|
|
132
|
+
parts.row.text = text;
|
|
133
|
+
expect(parts.row.getAttribute('text-length')).toBe(String(length));
|
|
134
|
+
}
|
|
135
|
+
parts.host.remove();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await it('truncates typed text by characters', () => {
|
|
139
|
+
const parts = mount('adw-entry-row', 'adw-entry-row');
|
|
140
|
+
for (const { text, maxLength, clamped } of ENTRY_MAX_LENGTH_VECTORS) {
|
|
141
|
+
parts.row.maxLength = 0;
|
|
142
|
+
parts.row.text = '';
|
|
143
|
+
parts.row.maxLength = maxLength;
|
|
144
|
+
// Type it: the `input` event path must clamp exactly like the
|
|
145
|
+
// property path, which is why `input.maxLength` (UTF-16 units)
|
|
146
|
+
// is deliberately left unset.
|
|
147
|
+
parts.input.value = text;
|
|
148
|
+
parts.input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
149
|
+
expect(parts.input.value).toBe(clamped);
|
|
150
|
+
expect(parts.row.text).toBe(clamped);
|
|
151
|
+
}
|
|
152
|
+
parts.host.remove();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await describe('adw-entry-row Enter dispatch (text_activated_cb)', async () => {
|
|
157
|
+
for (const { name, steps, activation, rule } of ENTRY_ROW_ACTIVATION_VECTORS) {
|
|
158
|
+
await it(`${name} — ${rule}`, () => {
|
|
159
|
+
const parts = mount('adw-entry-row', 'adw-entry-row');
|
|
160
|
+
for (const step of steps) applyStep(parts, step);
|
|
161
|
+
const recorded = recordEvents(parts.row, ['apply', 'entry-activated']);
|
|
162
|
+
pressEnter(parts);
|
|
163
|
+
expect(recorded.names).toStrictEqual([activation.signal]);
|
|
164
|
+
parts.host.remove();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await it('activates the default BEFORE emitting entry-activated', () => {
|
|
169
|
+
// C:253-256 — gtk_widget_activate_default is line 254, the signal is
|
|
170
|
+
// line 256. Implicit form submission is the web's default activation.
|
|
171
|
+
const host = document.createElement('div');
|
|
172
|
+
document.body.appendChild(host);
|
|
173
|
+
host.innerHTML = '<form><adw-entry-row title="T" activates-default></adw-entry-row></form>';
|
|
174
|
+
const form = host.querySelector('form') as HTMLFormElement;
|
|
175
|
+
const row = host.querySelector('adw-entry-row') as AdwEntryRow;
|
|
176
|
+
const input = row.querySelector('.adw-entry-row-input') as HTMLInputElement;
|
|
177
|
+
|
|
178
|
+
const order: string[] = [];
|
|
179
|
+
form.addEventListener('submit', (event) => {
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
order.push('submit');
|
|
182
|
+
});
|
|
183
|
+
row.addEventListener('entry-activated', () => order.push('entry-activated'));
|
|
184
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }));
|
|
185
|
+
|
|
186
|
+
expect(order).toStrictEqual(['submit', 'entry-activated']);
|
|
187
|
+
host.remove();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await it('leaves the default alone when activates-default is unset', () => {
|
|
191
|
+
const host = document.createElement('div');
|
|
192
|
+
document.body.appendChild(host);
|
|
193
|
+
host.innerHTML = '<form><adw-entry-row title="T"></adw-entry-row></form>';
|
|
194
|
+
const form = host.querySelector('form') as HTMLFormElement;
|
|
195
|
+
const row = host.querySelector('adw-entry-row') as AdwEntryRow;
|
|
196
|
+
const input = row.querySelector('.adw-entry-row-input') as HTMLInputElement;
|
|
197
|
+
|
|
198
|
+
const order: string[] = [];
|
|
199
|
+
form.addEventListener('submit', (event) => {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
order.push('submit');
|
|
202
|
+
});
|
|
203
|
+
row.addEventListener('entry-activated', () => order.push('entry-activated'));
|
|
204
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }));
|
|
205
|
+
|
|
206
|
+
expect(order).toStrictEqual(['entry-activated']);
|
|
207
|
+
host.remove();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await describe('adw-entry-row change signals', async () => {
|
|
212
|
+
await it('emits both GtkEditable spellings on every buffer change', () => {
|
|
213
|
+
// libadwaita implements GtkEditable through a delegate (C:87-93,
|
|
214
|
+
// :821-834), so an AdwEntryRow emits `changed` AND `notify::text`.
|
|
215
|
+
// The two ports had each picked one and drifted apart.
|
|
216
|
+
const parts = mount('adw-entry-row', 'adw-entry-row');
|
|
217
|
+
const recorded = recordEvents(parts.row, ['changed', 'notify::text', 'notify::text-length']);
|
|
218
|
+
parts.row.text = 'Ada';
|
|
219
|
+
expect(recorded.names).toStrictEqual(['changed', 'notify::text', 'notify::text-length']);
|
|
220
|
+
parts.host.remove();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await it('says nothing when the text does not actually change', () => {
|
|
224
|
+
const parts = mount('adw-entry-row', 'adw-entry-row');
|
|
225
|
+
parts.row.text = 'Ada';
|
|
226
|
+
const recorded = recordEvents(parts.row, ['changed', 'notify::text', 'notify::text-length']);
|
|
227
|
+
parts.row.text = 'Ada';
|
|
228
|
+
expect(recorded.names).toStrictEqual([]);
|
|
229
|
+
parts.host.remove();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await it('emits notify::text-length only when the COUNT moves', () => {
|
|
233
|
+
const parts = mount('adw-entry-row', 'adw-entry-row');
|
|
234
|
+
parts.row.text = 'Ada';
|
|
235
|
+
const recorded = recordEvents(parts.row, ['notify::text-length']);
|
|
236
|
+
parts.row.text = 'Bob';
|
|
237
|
+
expect(recorded.names).toStrictEqual([]);
|
|
238
|
+
parts.row.text = 'Bobs';
|
|
239
|
+
expect(recorded.names).toStrictEqual(['notify::text-length']);
|
|
240
|
+
parts.host.remove();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await it('emits apply exactly once from the button', () => {
|
|
244
|
+
const parts = mount('adw-entry-row', 'adw-entry-row');
|
|
245
|
+
parts.row.showApplyButton = true;
|
|
246
|
+
parts.input.focus();
|
|
247
|
+
parts.row.text = 'A';
|
|
248
|
+
const recorded = recordEvents(parts.row, ['apply', 'entry-activated']);
|
|
249
|
+
parts.applyButton.click();
|
|
250
|
+
expect(recorded.names).toStrictEqual(['apply']);
|
|
251
|
+
expect(parts.applyButton.hidden).toBe(true);
|
|
252
|
+
parts.host.remove();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await describe('adw-entry-row light-DOM children (GtkBuildable)', async () => {
|
|
257
|
+
await it('keeps author children instead of discarding them', () => {
|
|
258
|
+
// `replaceChildren(text, edit)` used to throw every authored child
|
|
259
|
+
// away silently. add_suffix (C:885-899) appends into a BOX.
|
|
260
|
+
const parts = mount('adw-entry-row', 'adw-entry-row', '<button id="go">Go</button>');
|
|
261
|
+
const go = parts.row.querySelector('#go') as HTMLElement;
|
|
262
|
+
expect(go).not.toBe(null);
|
|
263
|
+
expect(go.closest('.adw-entry-row-suffixes')).not.toBe(null);
|
|
264
|
+
parts.host.remove();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await it('routes slot="prefix" children to the prefix box', () => {
|
|
268
|
+
const parts = mount('adw-entry-row', 'adw-entry-row', '<span id="p" slot="prefix">P</span>');
|
|
269
|
+
const p = parts.row.querySelector('#p') as HTMLElement;
|
|
270
|
+
expect(p.closest('.adw-entry-row-prefixes')).not.toBe(null);
|
|
271
|
+
parts.host.remove();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await it('hides an empty affordance box', () => {
|
|
275
|
+
const parts = mount('adw-entry-row', 'adw-entry-row');
|
|
276
|
+
expect((parts.row.querySelector('.adw-entry-row-suffixes') as HTMLElement).hidden).toBe(true);
|
|
277
|
+
expect((parts.row.querySelector('.adw-entry-row-prefixes') as HTMLElement).hidden).toBe(true);
|
|
278
|
+
parts.host.remove();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await describe('adw-entry-row click-to-focus (pressed_cb)', async () => {
|
|
283
|
+
await it('focuses the entry when the row body is clicked', () => {
|
|
284
|
+
// C:201-227 — the click lands on the row, the header, the editable
|
|
285
|
+
// area, the indicator or an affordance box, and grabs focus. The port
|
|
286
|
+
// had this inverted: only the pencil focused, the body was inert.
|
|
287
|
+
const parts = mount('adw-entry-row', 'adw-entry-row');
|
|
288
|
+
parts.row.click();
|
|
289
|
+
expect(document.activeElement).toBe(parts.input);
|
|
290
|
+
parts.host.remove();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await it('focuses when the pencil is clicked — it is can-target=False upstream', () => {
|
|
294
|
+
const parts = mount('adw-entry-row', 'adw-entry-row');
|
|
295
|
+
parts.editIcon.click();
|
|
296
|
+
expect(document.activeElement).toBe(parts.input);
|
|
297
|
+
parts.host.remove();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await it('leaves a suffix control alone', () => {
|
|
301
|
+
const parts = mount('adw-entry-row', 'adw-entry-row', '<button id="go">Go</button>');
|
|
302
|
+
(parts.row.querySelector('#go') as HTMLElement).click();
|
|
303
|
+
expect(document.activeElement).not.toBe(parts.input);
|
|
304
|
+
parts.host.remove();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await describe('adw-password-entry-row (libadwaita conformance vectors)', async () => {
|
|
309
|
+
/** Replay one password vector step against the real element. */
|
|
310
|
+
function applyPasswordStep(parts: RowParts, step: PasswordEntryRowStep): void {
|
|
311
|
+
const row = parts.row as AdwPasswordEntryRow;
|
|
312
|
+
switch (step.op) {
|
|
313
|
+
case 'setRevealed':
|
|
314
|
+
row.revealed = step.value;
|
|
315
|
+
return;
|
|
316
|
+
case 'togglePeek':
|
|
317
|
+
(parts.row.querySelector('.adw-password-entry-row-toggle') as HTMLButtonElement).click();
|
|
318
|
+
return;
|
|
319
|
+
case 'setCapsLockOn':
|
|
320
|
+
row.setCapsLockOn(step.value);
|
|
321
|
+
return;
|
|
322
|
+
case 'entry':
|
|
323
|
+
applyStep(parts, step.step);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
for (const { name, steps, expected, entryIndicatorVisible, rule } of PASSWORD_ENTRY_ROW_VECTORS) {
|
|
329
|
+
await it(`${name} — ${rule}`, () => {
|
|
330
|
+
const parts = mount('adw-password-entry-row', 'adw-password-entry-row');
|
|
331
|
+
const toggle = parts.row.querySelector('.adw-password-entry-row-toggle') as HTMLButtonElement;
|
|
332
|
+
const toggleIcon = toggle.firstElementChild as HTMLElement;
|
|
333
|
+
for (const step of steps) applyPasswordStep(parts, step);
|
|
334
|
+
|
|
335
|
+
expect(parts.input.type).toBe(expected.revealed ? 'text' : 'password');
|
|
336
|
+
expect(toggleIcon.dataset.iconName).toBe(expected.peekIconName);
|
|
337
|
+
expect(toggle.getAttribute('aria-label')).toBe(expected.peekLabel);
|
|
338
|
+
expect(toggle.getAttribute('aria-pressed')).toBe(String(expected.revealed));
|
|
339
|
+
expect(parts.indicator.dataset.iconName).toBe(expected.indicatorIconName);
|
|
340
|
+
expect(parts.indicator.getAttribute('aria-label')).toBe(expected.indicatorTooltip);
|
|
341
|
+
expect(parts.indicator.hidden).toBe(!entryIndicatorVisible);
|
|
342
|
+
parts.host.remove();
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for (const { name, steps, notifications, rule } of PASSWORD_REVEAL_GUARD_VECTORS) {
|
|
347
|
+
await it(`${name} — ${rule}`, () => {
|
|
348
|
+
const parts = mount('adw-password-entry-row', 'adw-password-entry-row');
|
|
349
|
+
const recorded = recordEvents(parts.row, ['notify::revealed']);
|
|
350
|
+
for (const step of steps) applyPasswordStep(parts, step);
|
|
351
|
+
expect(recorded.names).toHaveLength(notifications);
|
|
352
|
+
parts.host.remove();
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
await it('reads Caps Lock off keyboard events', () => {
|
|
357
|
+
const parts = mount('adw-password-entry-row', 'adw-password-entry-row');
|
|
358
|
+
parts.input.focus();
|
|
359
|
+
parts.input.dispatchEvent(
|
|
360
|
+
new KeyboardEvent('keydown', { key: 'a', bubbles: true, modifierCapsLock: true } as KeyboardEventInit),
|
|
361
|
+
);
|
|
362
|
+
expect((parts.row as AdwPasswordEntryRow).capsLockOn).toBe(true);
|
|
363
|
+
expect(parts.indicator.hidden).toBe(false);
|
|
364
|
+
parts.host.remove();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await it('keeps the peek toggle when a consumer adds a suffix', () => {
|
|
368
|
+
// add_suffix appends into a box (C:885-899) and the toggle is
|
|
369
|
+
// installed through it (adw-password-entry-row.c:152), so both stay.
|
|
370
|
+
const parts = mount('adw-password-entry-row', 'adw-password-entry-row', '<button id="go">Go</button>');
|
|
371
|
+
const suffixes = parts.row.querySelector('.adw-password-entry-row-suffixes') as HTMLElement;
|
|
372
|
+
expect(suffixes.querySelector('.adw-password-entry-row-toggle')).not.toBe(null);
|
|
373
|
+
expect(suffixes.querySelector('#go')).not.toBe(null);
|
|
374
|
+
// C:152 — the row's own toggle is the FIRST suffix.
|
|
375
|
+
expect(suffixes.children[0]?.classList.contains('adw-password-entry-row-toggle')).toBe(true);
|
|
376
|
+
parts.host.remove();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
await it('inherits the entry row rather than copying it', () => {
|
|
380
|
+
// The two element files used to be a normalized-diff-identical
|
|
381
|
+
// copy-paste; the password row now IS an entry row.
|
|
382
|
+
const parts = mount('adw-password-entry-row', 'adw-password-entry-row');
|
|
383
|
+
parts.row.showApplyButton = true;
|
|
384
|
+
parts.input.focus();
|
|
385
|
+
parts.row.text = 'secret';
|
|
386
|
+
expect(parts.applyButton.hidden).toBe(false);
|
|
387
|
+
expect(parts.row.getAttribute('text-length')).toBe('6');
|
|
388
|
+
parts.host.remove();
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,10 @@ if (typeof document !== 'undefined' && !document.getElementById('adwaita-web-sty
|
|
|
20
20
|
document.head.appendChild(style);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// Responsive breakpoints: the browser size source for `@gjsify/adwaita-core`'s
|
|
24
|
+
// `AdwBreakpoint`, and the `breakpoint="…"` attribute wiring the split views use.
|
|
25
|
+
export { addBreakpoints, bindBreakpointSetter } from './breakpoints.js';
|
|
26
|
+
|
|
23
27
|
// Register custom elements (side-effect imports)
|
|
24
28
|
export { AdwAvatar } from './elements/adw-avatar.js';
|
|
25
29
|
export { AdwBanner } from './elements/adw-banner.js';
|
|
@@ -39,6 +43,25 @@ export { AdwHeaderBar } from './elements/adw-header-bar.js';
|
|
|
39
43
|
export { AdwWindowTitle } from './elements/adw-window-title.js';
|
|
40
44
|
export { AdwButton } from './elements/adw-button.js';
|
|
41
45
|
export { AdwButtonContent } from './elements/adw-button-content.js';
|
|
46
|
+
// The ONE symbolic-icon node and the ONE toggle. Exported before the widgets
|
|
47
|
+
// that build them so the barrel's definition order matches theirs; each host
|
|
48
|
+
// ALSO imports them directly, which is what actually guarantees the tags are
|
|
49
|
+
// defined before a server-rendered host upgrades.
|
|
50
|
+
export { AdwIcon, createAdwIcon } from './elements/adw-icon.js';
|
|
51
|
+
export { AdwSwitch } from './elements/adw-switch.js';
|
|
52
|
+
// The two form-control primitives. ONE module, because upstream merges their
|
|
53
|
+
// stylesheet (`_checks.scss`; there is no `_radio.scss` in libadwaita) and
|
|
54
|
+
// everything but the corner radius, the glyph and the group is shared.
|
|
55
|
+
export { AdwCheckbox, AdwRadio } from './elements/adw-checks.js';
|
|
56
|
+
// The determinate progress indicator — <adw-spinner> covers only the
|
|
57
|
+
// indeterminate case.
|
|
58
|
+
export { AdwProgressBar } from './elements/adw-progress-bar.js';
|
|
59
|
+
// The ONE popover surface. Exported before its three hosts so the barrel's own
|
|
60
|
+
// definition order matches theirs; each host ALSO imports it directly, which is
|
|
61
|
+
// what actually guarantees `adw-popover` is defined before a server-rendered
|
|
62
|
+
// host upgrades and builds one.
|
|
63
|
+
export { AdwPopover } from './elements/adw-popover.js';
|
|
64
|
+
export type { AdwPopoverAlign, AdwPopoverPosition, AdwPopoverRole } from './elements/adw-popover.js';
|
|
42
65
|
export { AdwSplitButton } from './elements/adw-split-button.js';
|
|
43
66
|
export { AdwToggle, AdwToggleGroup } from './elements/adw-toggle-group.js';
|
|
44
67
|
export { AdwEntry } from './elements/adw-entry.js';
|