@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
|
@@ -1,68 +1,436 @@
|
|
|
1
1
|
// <adw-entry-row> — A boxed-list row that is itself a text entry: the title
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// doubles as placeholder text and the editable value sits inline on the row.
|
|
3
|
+
//
|
|
4
|
+
// The DERIVATION behind all of that — `update_empty`'s five-output truth table,
|
|
5
|
+
// the `text_changed` apply latch with both of its reset paths, character-counted
|
|
6
|
+
// max-length truncation and the two-way <kbd>Enter</kbd> dispatch — is HEADLESS
|
|
7
|
+
// and lives in `@gjsify/adwaita-core` (ADR 0004) as {@link EntryRowState}. This
|
|
8
|
+
// element composes it and keeps only the DOM half; `@gjsify/adwaita-nativescript`
|
|
9
|
+
// composes the same state machine, so both ports share one behaviour and are
|
|
10
|
+
// held to one table (`@gjsify/adwaita-core/conformance`).
|
|
11
|
+
//
|
|
12
|
+
// Attributes: title, text, editable, max-length, show-apply-button,
|
|
13
|
+
// activates-default. `text-length` is REFLECTED (read-only, mirrors
|
|
14
|
+
// `Adw.EntryRow:text-length`).
|
|
15
|
+
// Properties: text, editable, maxLength, textLength, showApplyButton,
|
|
16
|
+
// activatesDefault, editing.
|
|
17
|
+
// Events:
|
|
18
|
+
// `changed` + `notify::text` — every buffer change. libadwaita implements
|
|
19
|
+
// GtkEditable through a delegate (adw-entry-row.c:87-93, :821-834) so it
|
|
20
|
+
// emits BOTH spellings; the two ports had each picked one and drifted, so
|
|
21
|
+
// this emits both and code written against either runs against the other.
|
|
22
|
+
// `notify::text-length` — on_length_changed (C:268-273).
|
|
23
|
+
// `apply` — the apply button, or Enter while it shows (C:240).
|
|
24
|
+
// `entry-activated` — Enter otherwise (C:256), AFTER the default was activated.
|
|
25
|
+
//
|
|
26
|
+
// Reference: refs/libadwaita/src/adw-entry-row.c, adw-entry-row.ui
|
|
7
27
|
// Reference: refs/adwaita-web/adwaita-web/docs/widgets/entryrow.md
|
|
8
28
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_entries.scss
|
|
9
29
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
10
|
-
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web
|
|
30
|
+
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
31
|
+
// edit icon node is <adw-icon>.
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
ENTRY_ROW_APPLY_ICON_NAME,
|
|
35
|
+
ENTRY_ROW_APPLY_TOOLTIP,
|
|
36
|
+
ENTRY_ROW_EDIT_ICON_NAME,
|
|
37
|
+
EntryRowState,
|
|
38
|
+
type EntryRowRenderState,
|
|
39
|
+
} from '@gjsify/adwaita-core';
|
|
40
|
+
|
|
41
|
+
import { type AdwIcon, createAdwIcon } from './adw-icon.js';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Show/hide a rendered part.
|
|
45
|
+
*
|
|
46
|
+
* `hidden` carries the semantics and is what the conformance suite reads, but
|
|
47
|
+
* author CSS beats the UA `[hidden]` rule: `.adw-icon` already ships its own
|
|
48
|
+
* `&[hidden] { display: none }` override in `_icon.scss` and `.adw-button`
|
|
49
|
+
* does not, so a hidden apply button would still paint. The inline `display`
|
|
50
|
+
* rides along until the entry-row SCSS pass lands.
|
|
51
|
+
*/
|
|
52
|
+
function setPartVisible(part: HTMLElement, visible: boolean): void {
|
|
53
|
+
part.hidden = !visible;
|
|
54
|
+
part.style.display = visible ? '' : 'none';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Read a boolean attribute the HTML way, with an explicit `="false"` opt-out. */
|
|
58
|
+
function boolAttribute(value: string | null): boolean {
|
|
59
|
+
return value !== null && value !== 'false';
|
|
60
|
+
}
|
|
11
61
|
|
|
12
62
|
export class AdwEntryRow extends HTMLElement {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
63
|
+
/** The headless `update_empty` derivation + apply latch (ADR 0004). */
|
|
64
|
+
protected readonly _state = new EntryRowState();
|
|
65
|
+
protected _input!: HTMLInputElement;
|
|
66
|
+
/** The large placeholder title (`empty_title` in adw-entry-row.ui). */
|
|
67
|
+
protected _emptyTitle!: HTMLSpanElement;
|
|
68
|
+
/** The small label above the value (`title` in adw-entry-row.ui). */
|
|
69
|
+
protected _title!: HTMLSpanElement;
|
|
70
|
+
/** The `editable-area` gizmo: both titles plus the input. */
|
|
71
|
+
protected _area!: HTMLDivElement;
|
|
72
|
+
protected _editIcon!: AdwIcon;
|
|
73
|
+
protected _indicator!: HTMLSpanElement;
|
|
74
|
+
protected _applyButton!: HTMLButtonElement;
|
|
75
|
+
protected _prefixes!: HTMLDivElement;
|
|
76
|
+
protected _suffixes!: HTMLDivElement;
|
|
77
|
+
protected _initialized = false;
|
|
78
|
+
/** Light-DOM children captured at build time, adopted after `_onConnected`. */
|
|
79
|
+
private _authored: Element[] = [];
|
|
80
|
+
private _lastText = '';
|
|
81
|
+
private _lastLength = 0;
|
|
82
|
+
|
|
83
|
+
static get observedAttributes(): string[] {
|
|
84
|
+
return ['title', 'text', 'editable', 'max-length', 'show-apply-button', 'activates-default'];
|
|
85
|
+
}
|
|
16
86
|
|
|
17
|
-
|
|
18
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Class-name prefix for this row's parts. `<adw-password-entry-row>`
|
|
89
|
+
* overrides it so `_password_entry_row.scss` keeps matching its own parts —
|
|
90
|
+
* the two SCSS partials are near-identical but each scopes to its own tag.
|
|
91
|
+
*/
|
|
92
|
+
protected get _classPrefix(): string {
|
|
93
|
+
return 'adw-entry-row';
|
|
19
94
|
}
|
|
20
95
|
|
|
96
|
+
/** The editable text. */
|
|
21
97
|
get text(): string {
|
|
22
|
-
return this.
|
|
98
|
+
return this._state.text;
|
|
23
99
|
}
|
|
24
100
|
|
|
25
101
|
set text(value: string) {
|
|
26
|
-
|
|
27
|
-
|
|
102
|
+
this._state.setText(value ?? '');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** `Adw.EntryRow:text-length` — the number of CHARACTERS, not UTF-16 units. */
|
|
106
|
+
get textLength(): number {
|
|
107
|
+
return this._state.textLength;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** `Adw.EntryRow:max-length` — maximum number of characters, `0` = unlimited. */
|
|
111
|
+
get maxLength(): number {
|
|
112
|
+
return this._state.maxLength;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
set maxLength(value: number) {
|
|
116
|
+
this._state.setMaxLength(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Whether the entry accepts edits (`GtkEditable:editable`). */
|
|
120
|
+
get editable(): boolean {
|
|
121
|
+
return this._state.editable;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
set editable(value: boolean) {
|
|
125
|
+
this._state.setEditable(value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** `Adw.EntryRow:show-apply-button` — typing reveals an apply button. */
|
|
129
|
+
get showApplyButton(): boolean {
|
|
130
|
+
return this._state.showApplyButton;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
set showApplyButton(value: boolean) {
|
|
134
|
+
this._state.setShowApplyButton(value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** `Adw.EntryRow:activates-default` — Enter may submit the surrounding form. */
|
|
138
|
+
get activatesDefault(): boolean {
|
|
139
|
+
return this._state.activatesDefault;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
set activatesDefault(value: boolean) {
|
|
143
|
+
this._state.setActivatesDefault(value);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Whether the embedded entry currently has focus. */
|
|
147
|
+
get editing(): boolean {
|
|
148
|
+
return this._state.editing;
|
|
28
149
|
}
|
|
29
150
|
|
|
30
151
|
connectedCallback() {
|
|
31
152
|
if (this._initialized) return;
|
|
32
153
|
this._initialized = true;
|
|
33
154
|
|
|
34
|
-
this.
|
|
35
|
-
|
|
36
|
-
|
|
155
|
+
this._build();
|
|
156
|
+
|
|
157
|
+
// Seed from the attributes BEFORE subscribing, so building the initial
|
|
158
|
+
// DOM is not driven by change notifications (the adw-spin-row pattern).
|
|
159
|
+
// `text` is only taken when the attribute is present, so a property set
|
|
160
|
+
// before the element was connected survives.
|
|
161
|
+
this._state.setMaxLength(Number.parseInt(this.getAttribute('max-length') ?? '', 10));
|
|
162
|
+
this._state.setEditable(this.getAttribute('editable') !== 'false');
|
|
163
|
+
this._state.setShowApplyButton(boolAttribute(this.getAttribute('show-apply-button')));
|
|
164
|
+
this._state.setActivatesDefault(boolAttribute(this.getAttribute('activates-default')));
|
|
165
|
+
if (this.hasAttribute('text')) this._state.setText(this.getAttribute('text') ?? '');
|
|
166
|
+
this._renderTitle();
|
|
167
|
+
|
|
168
|
+
// Subclass parts first, THEN the author's children: `AdwPasswordEntryRow`
|
|
169
|
+
// installs its peek toggle from its own `init` (C:152), so a consumer
|
|
170
|
+
// suffix lands after it and both remain.
|
|
171
|
+
this._onConnected();
|
|
172
|
+
this._adoptAuthored();
|
|
173
|
+
|
|
174
|
+
this._lastText = this._state.text;
|
|
175
|
+
this._lastLength = this._state.textLength;
|
|
176
|
+
this._render(this._state.state);
|
|
177
|
+
this._state.subscribe((state) => this._onStateChanged(state));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null) {
|
|
181
|
+
if (!this._initialized) return;
|
|
182
|
+
switch (name) {
|
|
183
|
+
case 'title':
|
|
184
|
+
this._renderTitle();
|
|
185
|
+
break;
|
|
186
|
+
case 'text':
|
|
187
|
+
this._state.setText(value ?? '');
|
|
188
|
+
break;
|
|
189
|
+
case 'editable':
|
|
190
|
+
this._state.setEditable(value !== 'false');
|
|
191
|
+
break;
|
|
192
|
+
case 'max-length':
|
|
193
|
+
this._state.setMaxLength(Number.parseInt(value ?? '', 10));
|
|
194
|
+
break;
|
|
195
|
+
case 'show-apply-button':
|
|
196
|
+
this._state.setShowApplyButton(boolAttribute(value));
|
|
197
|
+
break;
|
|
198
|
+
case 'activates-default':
|
|
199
|
+
this._state.setActivatesDefault(boolAttribute(value));
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Add a widget after the editable area — `adw_entry_row_add_suffix`
|
|
206
|
+
* (adw-entry-row.c:885-899), which APPENDS into a box, so several suffixes
|
|
207
|
+
* coexist and the password row's own peek toggle survives a consumer's.
|
|
208
|
+
*/
|
|
209
|
+
addSuffix(node: Node): void {
|
|
210
|
+
this._suffixes.append(node);
|
|
211
|
+
setPartVisible(this._suffixes, true);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Add a widget before the editable area — `adw_entry_row_add_prefix`
|
|
216
|
+
* (C:864-880). PREPENDS, like the `gtk_box_prepend` it mirrors, so several
|
|
217
|
+
* prefixes stack in reverse call order exactly as they do in GTK.
|
|
218
|
+
*/
|
|
219
|
+
addPrefix(node: Node): void {
|
|
220
|
+
this._prefixes.prepend(node);
|
|
221
|
+
setPartVisible(this._prefixes, true);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Drive the trailing indicator — `adw_entry_row_set_show_indicator`
|
|
226
|
+
* (adw-entry-row-private.h, C:1281-1296). Private-to-the-library upstream,
|
|
227
|
+
* public here because `<adw-password-entry-row>` is a separate custom element
|
|
228
|
+
* rather than a C subclass with access to the private header.
|
|
229
|
+
*/
|
|
230
|
+
setShowIndicator(show: boolean): void {
|
|
231
|
+
this._state.setShowIndicator(show);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** `adw_entry_row_set_indicator_icon_name` (C:1263-1273). */
|
|
235
|
+
setIndicatorIconName(iconName: string): void {
|
|
236
|
+
this._indicator.dataset.iconName = iconName;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** `adw_entry_row_set_indicator_tooltip` (C:1275-1279). */
|
|
240
|
+
setIndicatorTooltip(tooltip: string): void {
|
|
241
|
+
this._indicator.title = tooltip;
|
|
242
|
+
this._indicator.setAttribute('aria-label', tooltip);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Apply the pending edit — the apply button click (C:229-241). */
|
|
246
|
+
apply(): void {
|
|
247
|
+
this._state.apply();
|
|
248
|
+
this.dispatchEvent(new CustomEvent('apply', { bubbles: true }));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Focus the entry without selecting — `adw_entry_row_grab_focus_without_selecting` (C:1379-1389). */
|
|
252
|
+
grabFocusWithoutSelecting(): boolean {
|
|
253
|
+
this._input.focus();
|
|
254
|
+
return this._state.editing;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Subclass hook: runs after the DOM exists and the state is seeded, before the first render. */
|
|
258
|
+
protected _onConnected(): void {}
|
|
259
|
+
|
|
260
|
+
private _build(): void {
|
|
261
|
+
const prefix = this._classPrefix;
|
|
262
|
+
|
|
263
|
+
// Author-supplied light-DOM children are KEPT: `replaceChildren` used to
|
|
264
|
+
// discard them silently, so `<adw-entry-row><adw-button>Go</adw-button>`
|
|
265
|
+
// rendered without its button. Elements only — GtkBuildable also only
|
|
266
|
+
// ever adopts widgets, and keeping stray whitespace would make an empty
|
|
267
|
+
// affordance box measure as non-empty.
|
|
268
|
+
this._authored = [...this.children];
|
|
269
|
+
this._prefixes = document.createElement('div');
|
|
270
|
+
this._prefixes.className = `${prefix}-prefixes`;
|
|
271
|
+
this._suffixes = document.createElement('div');
|
|
272
|
+
this._suffixes.className = `${prefix}-suffixes`;
|
|
273
|
+
|
|
274
|
+
// TWO title labels, exactly like adw-entry-row.ui: `empty_title` is the
|
|
275
|
+
// large placeholder shown while the row is empty, `title` the small label
|
|
276
|
+
// above the value. GTK cross-fades them over EMPTY_ANIMATION_DURATION_MS
|
|
277
|
+
// (C:433-439); until the SCSS pass lands this is a hard swap at the two
|
|
278
|
+
// animation endpoints, which is why the render reads `emptyTarget`.
|
|
279
|
+
this._emptyTitle = document.createElement('span');
|
|
280
|
+
this._emptyTitle.className = `adw-row-title ${prefix}-empty-title`;
|
|
281
|
+
this._title = document.createElement('span');
|
|
282
|
+
// `adw-row-title` is what the SCSS styles; the prefixed class is how the
|
|
283
|
+
// conformance suite tells the two titles apart.
|
|
284
|
+
this._title.className = `adw-row-title ${prefix}-title`;
|
|
37
285
|
|
|
38
286
|
this._input = document.createElement('input');
|
|
39
|
-
this._input.className =
|
|
287
|
+
this._input.className = `${prefix}-input`;
|
|
40
288
|
this._input.type = 'text';
|
|
41
|
-
this._input.value = this.getAttribute('text') ?? '';
|
|
42
289
|
this._input.addEventListener('input', () => {
|
|
43
|
-
|
|
290
|
+
// Route through the core so truncation counts CHARACTERS —
|
|
291
|
+
// `input.maxLength` counts UTF-16 units and is deliberately unset.
|
|
292
|
+
this._state.setText(this._input.value);
|
|
293
|
+
if (this._input.value !== this._state.text) this._input.value = this._state.text;
|
|
44
294
|
});
|
|
295
|
+
this._input.addEventListener('focus', () => this._state.setEditing(true));
|
|
296
|
+
this._input.addEventListener('blur', () => this._state.setEditing(false));
|
|
297
|
+
this._input.addEventListener('keydown', (event) => {
|
|
298
|
+
if (event.key !== 'Enter') return;
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
this._activate();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
this._area = document.createElement('div');
|
|
304
|
+
this._area.className = `${prefix}-text`;
|
|
305
|
+
this._area.append(this._emptyTitle, this._title, this._input);
|
|
306
|
+
|
|
307
|
+
// The caps-lock warning slot. `caps-lock-symbolic` is not in the curated
|
|
308
|
+
// @gjsify/adwaita-icons set, so the glyph stands in and the canonical
|
|
309
|
+
// name travels in `data-icon-name` — the same fallback the dialog close
|
|
310
|
+
// buttons use.
|
|
311
|
+
this._indicator = document.createElement('span');
|
|
312
|
+
this._indicator.className = `${prefix}-indicator`;
|
|
313
|
+
this._indicator.setAttribute('role', 'img');
|
|
314
|
+
this._indicator.textContent = '⇪';
|
|
45
315
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
316
|
+
this._applyButton = document.createElement('button');
|
|
317
|
+
this._applyButton.type = 'button';
|
|
318
|
+
this._applyButton.className = `adw-button suggested-action circular icon-only ${prefix}-apply`;
|
|
319
|
+
this._applyButton.dataset.iconName = ENTRY_ROW_APPLY_ICON_NAME;
|
|
320
|
+
this._applyButton.title = ENTRY_ROW_APPLY_TOOLTIP;
|
|
321
|
+
this._applyButton.setAttribute('aria-label', ENTRY_ROW_APPLY_TOOLTIP);
|
|
322
|
+
this._applyButton.textContent = '✓';
|
|
323
|
+
this._applyButton.addEventListener('click', () => this.apply());
|
|
51
324
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
edit.addEventListener('click', () => this._input.focus());
|
|
325
|
+
// The libadwaita name travels in `data-icon-name`; the drawn glyph is
|
|
326
|
+
// the curated @gjsify/adwaita-icons spelling of the same symbolic, which
|
|
327
|
+
// is a different string (`adw-entry-edit-symbolic` vs `document-edit`).
|
|
328
|
+
this._editIcon = createAdwIcon('document-edit', 'adw-row-edit', `${prefix}-edit`);
|
|
329
|
+
this._editIcon.dataset.iconName = ENTRY_ROW_EDIT_ICON_NAME;
|
|
58
330
|
|
|
59
|
-
|
|
331
|
+
// Snapshot order: indicator → apply → edit icon inside the editable area
|
|
332
|
+
// (C:429-431), with the prefixes/suffixes boxes outside it.
|
|
333
|
+
this.replaceChildren(
|
|
334
|
+
this._prefixes,
|
|
335
|
+
this._area,
|
|
336
|
+
this._indicator,
|
|
337
|
+
this._applyButton,
|
|
338
|
+
this._editIcon,
|
|
339
|
+
this._suffixes,
|
|
340
|
+
);
|
|
341
|
+
setPartVisible(this._prefixes, false);
|
|
342
|
+
setPartVisible(this._suffixes, false);
|
|
343
|
+
|
|
344
|
+
this.addEventListener('click', (event) => this._maybeFocus(event));
|
|
60
345
|
}
|
|
61
346
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Re-home the children the author wrote. libadwaita's buildable maps an
|
|
349
|
+
* untyped `<child>` to a suffix (C:796-812); `slot="prefix"` is the web
|
|
350
|
+
* spelling of its `type="prefix"`.
|
|
351
|
+
*/
|
|
352
|
+
private _adoptAuthored(): void {
|
|
353
|
+
for (const node of this._authored) {
|
|
354
|
+
if (node.getAttribute('slot') === 'prefix') this.addPrefix(node);
|
|
355
|
+
else this.addSuffix(node);
|
|
356
|
+
}
|
|
357
|
+
this._authored = [];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* `pressed_cb` (C:201-227): a click landing on the row, the editable area,
|
|
362
|
+
* the indicator or either affordance box grabs focus; anything else (a suffix
|
|
363
|
+
* control, the apply button) is left alone. The pencil is `can-target=False`
|
|
364
|
+
* in the .ui, so a click on it picks the editable area — hence it focuses too.
|
|
365
|
+
*/
|
|
366
|
+
private _maybeFocus(event: MouseEvent): void {
|
|
367
|
+
const target = event.target;
|
|
368
|
+
if (
|
|
369
|
+
target === this ||
|
|
370
|
+
target === this._area ||
|
|
371
|
+
target === this._emptyTitle ||
|
|
372
|
+
target === this._title ||
|
|
373
|
+
target === this._editIcon ||
|
|
374
|
+
target === this._indicator ||
|
|
375
|
+
target === this._prefixes ||
|
|
376
|
+
target === this._suffixes
|
|
377
|
+
) {
|
|
378
|
+
this._input.focus();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** `text_activated_cb` (C:243-266) — exactly one of the two signals. */
|
|
383
|
+
private _activate(): void {
|
|
384
|
+
const activation = this._state.activate();
|
|
385
|
+
if (activation.signal === 'apply') {
|
|
386
|
+
this.dispatchEvent(new CustomEvent('apply', { bubbles: true }));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// C:253-256 — the default widget is activated BEFORE `entry-activated`.
|
|
390
|
+
// Implicit form submission is the web's default-widget activation.
|
|
391
|
+
if (activation.activateDefault) this.closest('form')?.requestSubmit();
|
|
392
|
+
this.dispatchEvent(new CustomEvent('entry-activated', { bubbles: true }));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private _renderTitle(): void {
|
|
396
|
+
const title = this.getAttribute('title') ?? '';
|
|
397
|
+
this._emptyTitle.textContent = title;
|
|
398
|
+
this._title.textContent = title;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Paint one render snapshot. Overridden (via `super`) by the password row. */
|
|
402
|
+
protected _render(state: EntryRowRenderState): void {
|
|
403
|
+
if (this._input.value !== state.text) this._input.value = state.text;
|
|
404
|
+
this._input.readOnly = !state.editable;
|
|
405
|
+
this.classList.toggle('focused', state.editing);
|
|
406
|
+
this.classList.toggle('empty', state.empty);
|
|
407
|
+
// The two titles are the animation's endpoints; a renderer with a real
|
|
408
|
+
// cross-fade would lerp between them over EMPTY_ANIMATION_DURATION_MS.
|
|
409
|
+
setPartVisible(this._emptyTitle, state.emptyTarget === 0);
|
|
410
|
+
setPartVisible(this._title, state.emptyTarget === 1);
|
|
411
|
+
setPartVisible(this._editIcon, state.editIconVisible);
|
|
412
|
+
this._editIcon.classList.toggle('disabled', !state.editIconSensitive);
|
|
413
|
+
this._editIcon.setAttribute('aria-disabled', String(!state.editIconSensitive));
|
|
414
|
+
setPartVisible(this._indicator, state.indicatorVisible);
|
|
415
|
+
setPartVisible(this._applyButton, state.applyButtonVisible);
|
|
416
|
+
this.setAttribute('text-length', String(state.textLength));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private _onStateChanged(state: EntryRowRenderState): void {
|
|
420
|
+
this._render(state);
|
|
421
|
+
|
|
422
|
+
if (state.text !== this._lastText) {
|
|
423
|
+
this._lastText = state.text;
|
|
424
|
+
const detail = { text: state.text };
|
|
425
|
+
this.dispatchEvent(new CustomEvent('changed', { bubbles: true, detail }));
|
|
426
|
+
this.dispatchEvent(new CustomEvent('notify::text', { bubbles: true, detail }));
|
|
427
|
+
}
|
|
428
|
+
if (state.textLength !== this._lastLength) {
|
|
429
|
+
this._lastLength = state.textLength;
|
|
430
|
+
this.dispatchEvent(
|
|
431
|
+
new CustomEvent('notify::text-length', { bubbles: true, detail: { textLength: state.textLength } }),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
66
434
|
}
|
|
67
435
|
}
|
|
68
436
|
|
|
@@ -18,27 +18,50 @@
|
|
|
18
18
|
// `notify::expanded` event. `@gjsify/adwaita-nativescript` composes the same
|
|
19
19
|
// state machine, so both ports share one behaviour.
|
|
20
20
|
//
|
|
21
|
+
// The optional enable switch is `<adw-switch>`. Both the markup and the
|
|
22
|
+
// stylesheet block behind it used to be hand-built copies of
|
|
23
|
+
// `<adw-switch-row>`'s — `_expander_row.scss:73-121` was character-for-character
|
|
24
|
+
// `_switch_row.scss:15-63` — because `.adw-switch` was nested inside
|
|
25
|
+
// `adw-switch-row {` and unusable from here.
|
|
26
|
+
//
|
|
21
27
|
// Reference: refs/adwaita-web/adwaita-web/scss/_expander_row.scss
|
|
22
28
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_expander-row.scss
|
|
23
29
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
24
30
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
25
|
-
// disclosure state machine composed from @gjsify/adwaita-core
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
// disclosure state machine composed from @gjsify/adwaita-core, the enable
|
|
32
|
+
// switch from <adw-switch> and the chevron from <adw-icon>.
|
|
33
|
+
|
|
34
|
+
import { ExpanderState, deriveRowLabels } from '@gjsify/adwaita-core';
|
|
35
|
+
|
|
36
|
+
// SIDE-EFFECT imports, deliberately separate from the type import below: they
|
|
37
|
+
// are what guarantee `adw-switch` / `adw-icon` are defined before this module's
|
|
38
|
+
// own `customElements.define` can upgrade a server-rendered `<adw-expander-row>`
|
|
39
|
+
// and build them. A combined `import { AdwSwitch }` would NOT do it — the
|
|
40
|
+
// binding is only ever used in type position here, and this package compiles
|
|
41
|
+
// without `verbatimModuleSyntax`, so TypeScript would elide the whole statement
|
|
42
|
+
// and take the registration with it.
|
|
43
|
+
import './adw-switch.js';
|
|
44
|
+
import type { AdwSwitch } from './adw-switch.js';
|
|
45
|
+
import { type AdwIcon, createAdwIcon } from './adw-icon.js';
|
|
28
46
|
|
|
29
47
|
export class AdwExpanderRow extends HTMLElement {
|
|
30
48
|
private _headerEl!: HTMLDivElement;
|
|
31
49
|
private _titleEl!: HTMLSpanElement;
|
|
32
50
|
private _subtitleEl!: HTMLSpanElement;
|
|
33
|
-
private _enableSwitch!:
|
|
34
|
-
private
|
|
35
|
-
private _chevronEl!: HTMLSpanElement;
|
|
51
|
+
private _enableSwitch!: AdwSwitch;
|
|
52
|
+
private _chevronEl!: AdwIcon;
|
|
36
53
|
private _contentEl!: HTMLDivElement;
|
|
37
54
|
private _prefixEl!: HTMLDivElement;
|
|
38
55
|
private _suffixEl!: HTMLDivElement;
|
|
39
56
|
/** The headless expanded/collapsed disclosure state machine (ADR 0004). */
|
|
40
57
|
private readonly _state = new ExpanderState();
|
|
41
58
|
private _initialized = false;
|
|
59
|
+
/**
|
|
60
|
+
* Guards the enable switch's `notify::active` while WE are the ones writing
|
|
61
|
+
* it. Only a user toggle publishes `notify::enable-expansion`; a programmatic
|
|
62
|
+
* `enableExpansion = …` re-syncs the control silently, as it always has.
|
|
63
|
+
*/
|
|
64
|
+
private _reflectingEnable = false;
|
|
42
65
|
|
|
43
66
|
constructor() {
|
|
44
67
|
super();
|
|
@@ -114,17 +137,11 @@ export class AdwExpanderRow extends HTMLElement {
|
|
|
114
137
|
|
|
115
138
|
// Optional enable switch (the same toggle Adw.ExpanderRow shows when
|
|
116
139
|
// show-enable-switch is set). Clicking it does NOT toggle the disclosure.
|
|
117
|
-
this.
|
|
118
|
-
this.
|
|
119
|
-
this._enableSwitch =
|
|
120
|
-
|
|
121
|
-
this.
|
|
122
|
-
const slider = document.createElement('span');
|
|
123
|
-
slider.className = 'adw-switch-slider';
|
|
124
|
-
this._enableLabel.append(this._enableSwitch, slider);
|
|
125
|
-
|
|
126
|
-
this._chevronEl = document.createElement('span');
|
|
127
|
-
this._chevronEl.className = 'adw-icon adw-icon--go-down adw-expander-row-chevron';
|
|
140
|
+
this._enableSwitch = document.createElement('adw-switch') as AdwSwitch;
|
|
141
|
+
this._enableSwitch.classList.add('adw-expander-row-enable-switch');
|
|
142
|
+
this._enableSwitch.active = this.enableExpansion;
|
|
143
|
+
|
|
144
|
+
this._chevronEl = createAdwIcon('go-down', 'adw-expander-row-chevron');
|
|
128
145
|
|
|
129
146
|
// Header prefix / suffix widget slots (Adw.ExpanderRow add_prefix/add_suffix).
|
|
130
147
|
this._prefixEl = document.createElement('div');
|
|
@@ -135,7 +152,7 @@ export class AdwExpanderRow extends HTMLElement {
|
|
|
135
152
|
this._suffixEl.className = 'adw-expander-row-suffix';
|
|
136
153
|
for (const child of suffixChildren) this._suffixEl.appendChild(child);
|
|
137
154
|
|
|
138
|
-
this._headerEl.append(this._prefixEl, textEl, this._suffixEl, this.
|
|
155
|
+
this._headerEl.append(this._prefixEl, textEl, this._suffixEl, this._enableSwitch, this._chevronEl);
|
|
139
156
|
|
|
140
157
|
// Disclosed content — a nested listbox holding the rows.
|
|
141
158
|
this._contentEl = document.createElement('div');
|
|
@@ -146,7 +163,7 @@ export class AdwExpanderRow extends HTMLElement {
|
|
|
146
163
|
|
|
147
164
|
this._headerEl.addEventListener('click', (event) => {
|
|
148
165
|
// Clicks on the enable switch toggle expansion-enable, not disclosure.
|
|
149
|
-
if (this.
|
|
166
|
+
if (this._enableSwitch.contains(event.target as Node)) return;
|
|
150
167
|
// The core state machine flips + reflects + re-renders; the event is
|
|
151
168
|
// emitted here so only a USER disclosure notifies (a programmatic
|
|
152
169
|
// `expanded = …` stays silent, as it always has on this port).
|
|
@@ -157,12 +174,19 @@ export class AdwExpanderRow extends HTMLElement {
|
|
|
157
174
|
}
|
|
158
175
|
});
|
|
159
176
|
|
|
160
|
-
this._enableSwitch.addEventListener('
|
|
161
|
-
|
|
177
|
+
this._enableSwitch.addEventListener('notify::active', (event) => {
|
|
178
|
+
// The switch's own notify is stopped at the row: the row publishes
|
|
179
|
+
// `notify::enable-expansion`, and letting an identically-shaped
|
|
180
|
+
// `notify::active` past would put a second, differently-named event
|
|
181
|
+
// for the same user action on every ancestor listener.
|
|
182
|
+
event.stopPropagation();
|
|
183
|
+
if (this._reflectingEnable) return;
|
|
184
|
+
const enabled = this._enableSwitch.active;
|
|
185
|
+
this.setAttribute('enable-expansion', enabled ? 'true' : 'false');
|
|
162
186
|
this.dispatchEvent(
|
|
163
187
|
new CustomEvent('notify::enable-expansion', {
|
|
164
188
|
bubbles: true,
|
|
165
|
-
detail: { enableExpansion:
|
|
189
|
+
detail: { enableExpansion: enabled },
|
|
166
190
|
}),
|
|
167
191
|
);
|
|
168
192
|
});
|
|
@@ -182,15 +206,23 @@ export class AdwExpanderRow extends HTMLElement {
|
|
|
182
206
|
}
|
|
183
207
|
|
|
184
208
|
private _render() {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
209
|
+
// The `string_is_not_empty` label rule, from core — this block was one of
|
|
210
|
+
// six hand-rolled copies, all of which omitted the TITLE half.
|
|
211
|
+
const labels = deriveRowLabels({
|
|
212
|
+
title: this.getAttribute('title'),
|
|
213
|
+
subtitle: this.getAttribute('subtitle'),
|
|
214
|
+
});
|
|
215
|
+
this._titleEl.textContent = labels.title;
|
|
216
|
+
this._titleEl.hidden = !labels.titleVisible;
|
|
217
|
+
this._subtitleEl.textContent = labels.subtitle;
|
|
218
|
+
this._subtitleEl.hidden = !labels.subtitleVisible;
|
|
190
219
|
|
|
191
220
|
const showSwitch = this.hasAttribute('show-enable-switch');
|
|
192
|
-
this.
|
|
193
|
-
|
|
221
|
+
this._enableSwitch.hidden = !showSwitch;
|
|
222
|
+
// OUR write, not the user's — see `_reflectingEnable`.
|
|
223
|
+
this._reflectingEnable = true;
|
|
224
|
+
this._enableSwitch.active = this.enableExpansion;
|
|
225
|
+
this._reflectingEnable = false;
|
|
194
226
|
|
|
195
227
|
this._prefixEl.hidden = this._prefixEl.childElementCount === 0;
|
|
196
228
|
this._suffixEl.hidden = this._suffixEl.childElementCount === 0;
|