@gjsify/adwaita-web 0.31.0 → 0.33.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 +835 -299
- 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-combo-row.d.ts +10 -0
- 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 +73 -11
- package/lib/types/elements/adw-menu-button.d.ts +6 -6
- package/lib/types/elements/adw-navigation-split-view.d.ts +48 -0
- package/lib/types/elements/adw-navigation-view.d.ts +63 -20
- package/lib/types/elements/adw-overlay-split-view.d.ts +94 -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-spinner.d.ts +23 -0
- 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 +39 -19
- package/lib/types/elements/adw-view-switcher-bar.d.ts +25 -7
- package/lib/types/elements/adw-view-switcher.d.ts +76 -13
- package/lib/types/elements/adw-window-title.d.ts +3 -1
- package/lib/types/elements/adw-wrap-box.d.ts +58 -3
- 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/_combo_row.scss +19 -0
- 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/_navigation_split_view.scss +47 -8
- package/scss/_overlay_split_view.scss +117 -32
- 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 +37 -19
- 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 +63 -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 +65 -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 +176 -0
- package/src/adw-banner.spec.ts +152 -0
- package/src/adw-button-content.spec.ts +207 -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-row-state.spec.ts +49 -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 +341 -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 +400 -0
- package/src/elements/adw-about-dialog.ts +223 -163
- package/src/elements/adw-action-row.ts +131 -11
- 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 +32 -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 +325 -131
- package/src/elements/adw-menu-button.ts +93 -70
- package/src/elements/adw-navigation-split-view.ts +220 -14
- package/src/elements/adw-navigation-view.ts +286 -148
- package/src/elements/adw-overlay-split-view.ts +536 -28
- 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 +168 -9
- 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 +700 -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 +183 -85
- package/src/elements/adw-view-switcher-bar.ts +171 -81
- package/src/elements/adw-view-switcher.ts +328 -117
- package/src/elements/adw-window-title.ts +44 -11
- package/src/elements/adw-wrap-box.ts +329 -78
- 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 +893 -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 +665 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// <adw-checkbox> and <adw-radio> — the two form-control primitives, in ONE
|
|
2
|
+
// module, for the same reason `scss/_checks.scss` is one partial: upstream
|
|
3
|
+
// merges them (`refs/libadwaita/src/stylesheet/widgets/_checks.scss` has no
|
|
4
|
+
// `_radio.scss` beside it), and everything but the corner radius, the glyph and
|
|
5
|
+
// the group is shared. `refs/adwaita-web` split them into two files and the two
|
|
6
|
+
// promptly drifted apart.
|
|
7
|
+
//
|
|
8
|
+
// WHAT LIVES IN `@gjsify/adwaita-core`, AND WHAT DOES NOT
|
|
9
|
+
//
|
|
10
|
+
// The checkbox has no core class: it is one boolean plus a third state, which
|
|
11
|
+
// ADR 0004 calls out as too trivial to model. What IS core is the pair of
|
|
12
|
+
// pieces a second renderer would otherwise re-invent:
|
|
13
|
+
//
|
|
14
|
+
// - {@link RadioGroupState} — group exclusivity. The browser gives the inner
|
|
15
|
+
// `<input type="radio" name="…">` exclusivity for free, and STOPS THERE: it
|
|
16
|
+
// unchecks the sibling INPUT and leaves the sibling `<adw-radio>` host's
|
|
17
|
+
// `checked` attribute — the published state, and the selector
|
|
18
|
+
// `_checks.scss` paints from — stale, so a group left to the browser draws
|
|
19
|
+
// two selected radios. The shared state is what repaints the loser.
|
|
20
|
+
// - {@link resolveCheckState} — indeterminate outranks checked, which is read
|
|
21
|
+
// off the cascade in `_checks.scss:79-81` and is what `aria-checked` needs.
|
|
22
|
+
//
|
|
23
|
+
// WHAT IS NOT VERIFIABLE IN THIS TREE. `GtkCheckButton` is a GTK widget:
|
|
24
|
+
// `refs/gtk` is EMPTY here and libadwaita vendors no `adw-checkbox.c`, so its
|
|
25
|
+
// group semantics (`gtk_check_button_set_group`, whether a fresh group starts
|
|
26
|
+
// with a member active) and the exact meaning of `GtkCheckButton:inconsistent`
|
|
27
|
+
// are NOT verifiable and are not guessed at. Where a rule was needed the HTML
|
|
28
|
+
// one is used and cited as such — clearing `indeterminate` on activation is the
|
|
29
|
+
// HTML pre-click activation step, not a reading of GTK.
|
|
30
|
+
//
|
|
31
|
+
// NAMING. The state attribute is `checked`, not GtkCheckButton's `active`:
|
|
32
|
+
// these elements wrap a real `<input>`, `checked` is the spelling every author
|
|
33
|
+
// already writes, and it is what the stylesheet selects on. The published event
|
|
34
|
+
// follows the attribute (`notify::checked`), not the GObject property.
|
|
35
|
+
//
|
|
36
|
+
// Attributes (both):
|
|
37
|
+
// checked — boolean; reflects (and drives) the checked state.
|
|
38
|
+
// indeterminate — boolean; the third state, drawn as a dash. Outranks checked.
|
|
39
|
+
// disabled — boolean; the control does not respond and is dimmed.
|
|
40
|
+
// label — the text beside the indicator. When absent, the element's
|
|
41
|
+
// existing child nodes become the label (so a server-rendered
|
|
42
|
+
// `<adw-checkbox>Enable</adw-checkbox>` upgrades intact).
|
|
43
|
+
// Attributes (<adw-radio> only):
|
|
44
|
+
// name — the group. Exclusivity is scoped to it, exactly as `name`
|
|
45
|
+
// scopes `<input type="radio">` — including the empty case:
|
|
46
|
+
// HTML forms a radio button group only from a NON-EMPTY name,
|
|
47
|
+
// so a nameless radio checks itself and unchecks nothing.
|
|
48
|
+
// value — this member's value. Defaults to the label text, so a group
|
|
49
|
+
// written without values still has distinguishable members.
|
|
50
|
+
// Properties: checked / indeterminate / disabled / label (+ name / value).
|
|
51
|
+
// Events:
|
|
52
|
+
// `notify::checked` (CustomEvent, bubbles, detail = { checked }) — fires on
|
|
53
|
+
// every change, programmatic included, on EVERY member a radio pick moves.
|
|
54
|
+
//
|
|
55
|
+
// A11Y: the inner input carries the role (native `checkbox` / `radio`) and an
|
|
56
|
+
// explicit `aria-checked` derived from {@link resolveCheckState}, so the
|
|
57
|
+
// indeterminate state announces as `mixed` for BOTH — HTML has a native mixed
|
|
58
|
+
// checkbox (`input.indeterminate`) but no mixed radio.
|
|
59
|
+
//
|
|
60
|
+
// Reference: refs/libadwaita/src/stylesheet/widgets/_checks.scss
|
|
61
|
+
// Reference: refs/adwaita-web/adwaita-web/scss/{_checkbox,_radio}.scss
|
|
62
|
+
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
63
|
+
// Copyright (c) 2025 csm (adwaita-web). MIT License.
|
|
64
|
+
// Modifications: Implemented as Web Components for @gjsify/adwaita-web.
|
|
65
|
+
|
|
66
|
+
import { RadioGroupState, resolveCheckState } from '@gjsify/adwaita-core';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The document's radio groups. ONE registry, as global as a bare
|
|
70
|
+
* `<input type="radio" name="x">` outside a form is — two independent groups
|
|
71
|
+
* therefore need two names.
|
|
72
|
+
*/
|
|
73
|
+
const radioGroups = new RadioGroupState();
|
|
74
|
+
|
|
75
|
+
/** The markup + state shared by `<adw-checkbox>` and `<adw-radio>`. */
|
|
76
|
+
abstract class AdwCheckBase extends HTMLElement {
|
|
77
|
+
protected _input!: HTMLInputElement;
|
|
78
|
+
protected _indicator!: HTMLSpanElement;
|
|
79
|
+
protected _label!: HTMLSpanElement;
|
|
80
|
+
private _initialized = false;
|
|
81
|
+
|
|
82
|
+
/** `checkbox` or `radio` — the native control this element is built around. */
|
|
83
|
+
protected abstract get inputType(): 'checkbox' | 'radio';
|
|
84
|
+
|
|
85
|
+
/** What a click / Space / arrow key on the control means for this kind. */
|
|
86
|
+
protected abstract activate(): void;
|
|
87
|
+
|
|
88
|
+
/** Whether the control is checked. */
|
|
89
|
+
get checked(): boolean {
|
|
90
|
+
return this.hasAttribute('checked');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
set checked(value: boolean) {
|
|
94
|
+
// `toggleAttribute` with a force flag is a no-op when the attribute
|
|
95
|
+
// already holds that state, so re-setting the current value mutates
|
|
96
|
+
// nothing and the notify never runs.
|
|
97
|
+
this.toggleAttribute('checked', !!value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Whether the control is in the third, inconsistent state. */
|
|
101
|
+
get indeterminate(): boolean {
|
|
102
|
+
return this.hasAttribute('indeterminate');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
set indeterminate(value: boolean) {
|
|
106
|
+
this.toggleAttribute('indeterminate', !!value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Whether the control is inert. */
|
|
110
|
+
get disabled(): boolean {
|
|
111
|
+
return this.hasAttribute('disabled');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
set disabled(value: boolean) {
|
|
115
|
+
this.toggleAttribute('disabled', !!value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The label text beside the indicator. */
|
|
119
|
+
get label(): string {
|
|
120
|
+
return this.getAttribute('label') ?? this._label?.textContent ?? '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
set label(value: string) {
|
|
124
|
+
this.setAttribute('label', value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
connectedCallback() {
|
|
128
|
+
if (this._initialized) {
|
|
129
|
+
this.onConnected();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this._initialized = true;
|
|
133
|
+
|
|
134
|
+
// Captured BEFORE the element takes over its own children: a
|
|
135
|
+
// server-rendered `<adw-checkbox>Enable</adw-checkbox>` keeps its text.
|
|
136
|
+
const authored = [...this.childNodes];
|
|
137
|
+
|
|
138
|
+
this._input = document.createElement('input');
|
|
139
|
+
this._input.type = this.inputType;
|
|
140
|
+
this._input.className = 'adw-check-input';
|
|
141
|
+
|
|
142
|
+
this._indicator = document.createElement('span');
|
|
143
|
+
this._indicator.className = 'adw-check-indicator';
|
|
144
|
+
|
|
145
|
+
this._label = document.createElement('span');
|
|
146
|
+
this._label.className = 'adw-check-label';
|
|
147
|
+
const authoredLabel = this.getAttribute('label');
|
|
148
|
+
if (authoredLabel === null) this._label.append(...authored);
|
|
149
|
+
else this._label.textContent = authoredLabel;
|
|
150
|
+
|
|
151
|
+
this.replaceChildren(this._input, this._indicator, this._label);
|
|
152
|
+
|
|
153
|
+
// Keyboard (Space, and the arrow keys that move within a radio group)
|
|
154
|
+
// and a direct programmatic `input.click()` both surface as `change`.
|
|
155
|
+
this._input.addEventListener('change', () => this.activate());
|
|
156
|
+
|
|
157
|
+
// The indicator and the label cover the control; the input is 0x0, so
|
|
158
|
+
// every pointer click lands on one of them. A click that DID reach the
|
|
159
|
+
// input is already handled above — activating again would undo it.
|
|
160
|
+
this.addEventListener('click', (event) => {
|
|
161
|
+
if (this.disabled) return;
|
|
162
|
+
if (event.target === this._input) return;
|
|
163
|
+
this.activate();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
this.onConnected();
|
|
167
|
+
this.render();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
disconnectedCallback() {
|
|
171
|
+
this.onDisconnected();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
attributeChangedCallback(name: string) {
|
|
175
|
+
if (!this._initialized) return;
|
|
176
|
+
if (name === 'label') {
|
|
177
|
+
this._label.textContent = this.getAttribute('label') ?? '';
|
|
178
|
+
}
|
|
179
|
+
this.render();
|
|
180
|
+
if (name === 'checked') {
|
|
181
|
+
this.dispatchEvent(
|
|
182
|
+
new CustomEvent('notify::checked', { bubbles: true, detail: { checked: this.checked } }),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Extra wiring for a subclass on connect. */
|
|
188
|
+
protected onConnected(): void {}
|
|
189
|
+
|
|
190
|
+
/** Extra teardown for a subclass on disconnect. */
|
|
191
|
+
protected onDisconnected(): void {}
|
|
192
|
+
|
|
193
|
+
protected render(): void {
|
|
194
|
+
const state = resolveCheckState(this.checked, this.indeterminate);
|
|
195
|
+
this._input.checked = state === 'checked';
|
|
196
|
+
this._input.indeterminate = state === 'indeterminate';
|
|
197
|
+
// Explicit, because HTML has a native mixed CHECKBOX (the property
|
|
198
|
+
// above) but no mixed radio — this is the one spelling that announces
|
|
199
|
+
// the third state for both.
|
|
200
|
+
this._input.setAttribute('aria-checked', state === 'indeterminate' ? 'mixed' : String(state === 'checked'));
|
|
201
|
+
this._input.disabled = this.disabled;
|
|
202
|
+
this._label.hidden = this._label.childNodes.length === 0;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The Adwaita checkbox — `checkbutton > check` as a custom element. */
|
|
207
|
+
export class AdwCheckbox extends AdwCheckBase {
|
|
208
|
+
static get observedAttributes() {
|
|
209
|
+
return ['checked', 'indeterminate', 'disabled', 'label'];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
protected get inputType(): 'checkbox' {
|
|
213
|
+
return 'checkbox';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
protected activate(): void {
|
|
217
|
+
// HTML's pre-click activation steps clear `indeterminate` before the
|
|
218
|
+
// toggle, so a user can always reach a definite state. That rule is the
|
|
219
|
+
// HTML spec's, not a reading of GtkCheckButton:inconsistent, whose
|
|
220
|
+
// behaviour is not verifiable in this tree.
|
|
221
|
+
this.indeterminate = false;
|
|
222
|
+
this.checked = !this.checked;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The Adwaita radio — `checkbutton > radio`, exclusive within its `name`. */
|
|
227
|
+
export class AdwRadio extends AdwCheckBase {
|
|
228
|
+
private _unsubscribe: (() => void) | null = null;
|
|
229
|
+
|
|
230
|
+
static get observedAttributes() {
|
|
231
|
+
return ['checked', 'indeterminate', 'disabled', 'label', 'name', 'value'];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
protected get inputType(): 'radio' {
|
|
235
|
+
return 'radio';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The group this member belongs to. */
|
|
239
|
+
get name(): string {
|
|
240
|
+
return this.getAttribute('name') ?? '';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
set name(value: string) {
|
|
244
|
+
this.setAttribute('name', value);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* This member's value. Falls back to the label text so a group authored
|
|
249
|
+
* without values still has members the group state can tell apart.
|
|
250
|
+
*/
|
|
251
|
+
get value(): string {
|
|
252
|
+
return this.getAttribute('value') ?? this.label.trim();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
set value(value: string) {
|
|
256
|
+
this.setAttribute('value', value);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
protected activate(): void {
|
|
260
|
+
this.indeterminate = false;
|
|
261
|
+
// An UNNAMED radio is not in a group — HTML forms a radio button group
|
|
262
|
+
// only from a non-empty `name`, so a nameless one checks itself and
|
|
263
|
+
// never unchecks anything. Without this guard every nameless radio in
|
|
264
|
+
// the document would join one `''` group and fight over it.
|
|
265
|
+
if (this.name === '') {
|
|
266
|
+
this.checked = true;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// The pick goes through the shared state, never straight onto this
|
|
270
|
+
// element: the member that LOSES the selection is a different element
|
|
271
|
+
// and only the state knows which one it is.
|
|
272
|
+
radioGroups.select(this.name, this.value);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
protected onConnected(): void {
|
|
276
|
+
this._input.name = this.name;
|
|
277
|
+
if (this.name === '') return; // ungrouped — nothing to subscribe to
|
|
278
|
+
this._unsubscribe ??= radioGroups.subscribe((change) => {
|
|
279
|
+
if (change.name !== this.name) return;
|
|
280
|
+
this.checked = change.selected === this.value;
|
|
281
|
+
});
|
|
282
|
+
// A member declared checked seeds the group, so the first user pick
|
|
283
|
+
// knows what it is deselecting.
|
|
284
|
+
if (this.checked) radioGroups.select(this.name, this.value);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
protected onDisconnected(): void {
|
|
288
|
+
this._unsubscribe?.();
|
|
289
|
+
this._unsubscribe = null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
protected render(): void {
|
|
293
|
+
super.render();
|
|
294
|
+
this._input.name = this.name;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
customElements.define('adw-checkbox', AdwCheckbox);
|
|
299
|
+
customElements.define('adw-radio', AdwRadio);
|
|
@@ -1,27 +1,97 @@
|
|
|
1
|
-
// <adw-clamp> — Constrains its
|
|
2
|
-
// Attributes: maximum-size (px, default 600), tightening-threshold (
|
|
3
|
-
//
|
|
1
|
+
// <adw-clamp> — Constrains its children to a maximum width and centers them.
|
|
2
|
+
// Attributes: maximum-size (px, default 600), tightening-threshold (px, default 400).
|
|
3
|
+
//
|
|
4
|
+
// The clamp is NOT a `max-width`, which is what this element used to be. Adwaita
|
|
5
|
+
// eases the child from "all the available width" into the cap across a tightening
|
|
6
|
+
// region, and stamps a `small`/`medium`/`large` class on it — the arithmetic lives
|
|
7
|
+
// in `@gjsify/adwaita-core` (`clampAllocate`) so this element and the NativeScript
|
|
8
|
+
// widget produce the same width for the same viewport.
|
|
9
|
+
//
|
|
10
|
+
// The cap now sits on the CHILDREN, not on this element. The element has to keep
|
|
11
|
+
// reporting the AVAILABLE width — it is what the curve is evaluated against — and
|
|
12
|
+
// an element that caps itself reports the capped width back to its own
|
|
13
|
+
// ResizeObserver, which is a feedback loop, not a measurement.
|
|
14
|
+
//
|
|
15
|
+
// `childMin` is passed as 0 rather than measured. libadwaita's
|
|
16
|
+
// `lower = MAX (…, child_min)` says a child is never squeezed below its own
|
|
17
|
+
// minimum, and the browser already applies that rule at the other end: `min-width`
|
|
18
|
+
// wins over `max-width` (CSS 2.1 §10.4), so a child that declares a minimum keeps
|
|
19
|
+
// it whatever cap this element writes. A child that declares none has a minimum of
|
|
20
|
+
// 0, which is the value passed here — measuring `min-content` per child on every
|
|
21
|
+
// resize would cost a synchronous reflow to arrive at the same layout.
|
|
22
|
+
//
|
|
23
|
+
// Reference: refs/libadwaita/src/adw-clamp-layout.c (child_size_from_clamp, allocate)
|
|
4
24
|
// Reference: refs/adwaita-web/adwaita-web/docs/widgets/clamp.md
|
|
5
|
-
// Reference: refs/libadwaita (Adw.Clamp)
|
|
6
25
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
7
26
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
8
27
|
|
|
28
|
+
import { ADW_CLAMP_DEFAULTS, ADW_CLAMP_SIZE_CLASSES, clampAllocate, normalizeClampSize } from '@gjsify/adwaita-core';
|
|
29
|
+
|
|
9
30
|
export class AdwClamp extends HTMLElement {
|
|
31
|
+
private _resize: ResizeObserver | null = null;
|
|
32
|
+
private _mutations: MutationObserver | null = null;
|
|
33
|
+
|
|
10
34
|
static get observedAttributes() {
|
|
11
|
-
return ['maximum-size'];
|
|
35
|
+
return ['maximum-size', 'tightening-threshold'];
|
|
12
36
|
}
|
|
13
37
|
|
|
14
38
|
connectedCallback() {
|
|
15
|
-
|
|
39
|
+
// ResizeObserver delivers an initial observation on observe(), so the
|
|
40
|
+
// first allocation lands before paint without a separate seeding pass.
|
|
41
|
+
this._resize = new ResizeObserver(() => this._allocate());
|
|
42
|
+
this._resize.observe(this);
|
|
43
|
+
// Children appended AFTER connect are clamped too. Without this they
|
|
44
|
+
// would keep whatever width they came with — the same "evaluated once at
|
|
45
|
+
// connect time" bug the toolbar view's empty-bar handling still has.
|
|
46
|
+
this._mutations = new MutationObserver(() => this._allocate());
|
|
47
|
+
this._mutations.observe(this, { childList: true });
|
|
48
|
+
this._allocate();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
disconnectedCallback() {
|
|
52
|
+
this._resize?.disconnect();
|
|
53
|
+
this._resize = null;
|
|
54
|
+
this._mutations?.disconnect();
|
|
55
|
+
this._mutations = null;
|
|
16
56
|
}
|
|
17
57
|
|
|
18
58
|
attributeChangedCallback() {
|
|
19
|
-
this.
|
|
59
|
+
this._allocate();
|
|
20
60
|
}
|
|
21
61
|
|
|
22
|
-
private
|
|
23
|
-
const
|
|
24
|
-
|
|
62
|
+
private _allocate(): void {
|
|
63
|
+
const params = {
|
|
64
|
+
maximumSize: normalizeClampSize(this.getAttribute('maximum-size'), ADW_CLAMP_DEFAULTS.maximumSize),
|
|
65
|
+
tighteningThreshold: normalizeClampSize(
|
|
66
|
+
this.getAttribute('tightening-threshold'),
|
|
67
|
+
ADW_CLAMP_DEFAULTS.tighteningThreshold,
|
|
68
|
+
),
|
|
69
|
+
childMin: 0,
|
|
70
|
+
childNat: 0,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const available = this.clientWidth;
|
|
74
|
+
const children = Array.from(this.children) as HTMLElement[];
|
|
75
|
+
|
|
76
|
+
// No box yet (disconnected, or measured before the first layout). GTK's
|
|
77
|
+
// equivalent is `for_size = -1`, where the child reports its natural size
|
|
78
|
+
// rather than being squeezed to nothing — so leave the children alone
|
|
79
|
+
// instead of capping them at 0 and collapsing the page.
|
|
80
|
+
if (available <= 0) {
|
|
81
|
+
for (const child of children) {
|
|
82
|
+
child.style.maxWidth = '';
|
|
83
|
+
child.style.marginInline = '';
|
|
84
|
+
for (const cls of ADW_CLAMP_SIZE_CLASSES) child.classList.remove(cls);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { childSize, sizeClass } = clampAllocate(available, params);
|
|
90
|
+
for (const child of children) {
|
|
91
|
+
child.style.maxWidth = `${childSize}px`;
|
|
92
|
+
child.style.marginInline = 'auto';
|
|
93
|
+
for (const cls of ADW_CLAMP_SIZE_CLASSES) child.classList.toggle(cls, cls === sizeClass);
|
|
94
|
+
}
|
|
25
95
|
}
|
|
26
96
|
}
|
|
27
97
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// title/subtitle text column added to match Adw.ComboRow; the selection state
|
|
18
18
|
// machine composed from @gjsify/adwaita-core.
|
|
19
19
|
|
|
20
|
-
import { ComboState } from '@gjsify/adwaita-core';
|
|
20
|
+
import { ComboState, deriveRowLabels, normalizeComboOptions } from '@gjsify/adwaita-core';
|
|
21
21
|
|
|
22
22
|
export class AdwComboRow extends HTMLElement {
|
|
23
23
|
private _select!: HTMLSelectElement;
|
|
@@ -46,8 +46,10 @@ export class AdwComboRow extends HTMLElement {
|
|
|
46
46
|
|
|
47
47
|
const items: string[] = JSON.parse(this.getAttribute('items') || '[]');
|
|
48
48
|
// Seed the headless state BEFORE subscribing, so building the initial DOM
|
|
49
|
-
// below is not driven by a change notification.
|
|
50
|
-
|
|
49
|
+
// below is not driven by a change notification. The string→descriptor
|
|
50
|
+
// mapping is core's: `<adw-drop-down>` had its own, richer copy, so the
|
|
51
|
+
// two selectors accepted different option vocabularies.
|
|
52
|
+
this._state.setOptions(normalizeComboOptions(items));
|
|
51
53
|
this._state.setSelectedIndex(parseInt(this.getAttribute('selected') || '0', 10));
|
|
52
54
|
const selectedIdx = this._state.selectedIndex;
|
|
53
55
|
|
|
@@ -83,6 +85,7 @@ export class AdwComboRow extends HTMLElement {
|
|
|
83
85
|
this._state.subscribe((change) => {
|
|
84
86
|
this._valueEl.textContent = change.label;
|
|
85
87
|
if (this._select.selectedIndex !== change.selected) this._select.selectedIndex = change.selected;
|
|
88
|
+
this._syncChooser();
|
|
86
89
|
if (!change.interactive) return;
|
|
87
90
|
this.setAttribute('selected', String(change.selected));
|
|
88
91
|
this.dispatchEvent(
|
|
@@ -99,6 +102,7 @@ export class AdwComboRow extends HTMLElement {
|
|
|
99
102
|
this._state.select(this._select.selectedIndex);
|
|
100
103
|
});
|
|
101
104
|
|
|
105
|
+
this._syncChooser();
|
|
102
106
|
this._renderText();
|
|
103
107
|
}
|
|
104
108
|
|
|
@@ -113,11 +117,32 @@ export class AdwComboRow extends HTMLElement {
|
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
/**
|
|
121
|
+
* `model_changed` (adw-combo-row.c:187-195) — one option or none is not a
|
|
122
|
+
* choice, so the arrow goes and the row stops being activatable.
|
|
123
|
+
*
|
|
124
|
+
* Both halves have to be expressed: `data-presents-chooser` gates the
|
|
125
|
+
* `.adw-row-value::after` mask in the stylesheet, and DISABLING the overlaid
|
|
126
|
+
* `<select>` is what makes the row inert — hiding the arrow alone would leave
|
|
127
|
+
* a row that still opens a one-entry popup on click.
|
|
128
|
+
*/
|
|
129
|
+
private _syncChooser() {
|
|
130
|
+
const presents = this._state.presentsChooser;
|
|
131
|
+
this.dataset.presentsChooser = presents ? 'true' : 'false';
|
|
132
|
+
this._select.disabled = !presents;
|
|
133
|
+
}
|
|
134
|
+
|
|
116
135
|
private _renderText() {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
136
|
+
// The `string_is_not_empty` label rule, from core — this block was one of
|
|
137
|
+
// six hand-rolled copies, all of which omitted the TITLE half.
|
|
138
|
+
const labels = deriveRowLabels({
|
|
139
|
+
title: this.getAttribute('title'),
|
|
140
|
+
subtitle: this.getAttribute('subtitle'),
|
|
141
|
+
});
|
|
142
|
+
this._titleEl.textContent = labels.title;
|
|
143
|
+
this._titleEl.hidden = !labels.titleVisible;
|
|
144
|
+
this._subtitleEl.textContent = labels.subtitle;
|
|
145
|
+
this._subtitleEl.hidden = !labels.subtitleVisible;
|
|
121
146
|
}
|
|
122
147
|
}
|
|
123
148
|
|