@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
|
@@ -1,120 +1,142 @@
|
|
|
1
|
-
// <adw-password-entry-row> —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
1
|
+
// <adw-password-entry-row> — An entry row tailored for secrets: the value is
|
|
2
|
+
// masked, a trailing peek button reveals it, and a warning shows while Caps Lock
|
|
3
|
+
// is engaged.
|
|
4
|
+
//
|
|
5
|
+
// It now EXTENDS `<adw-entry-row>`, as `AdwPasswordEntryRow` extends
|
|
6
|
+
// `AdwEntryRow` in the C (adw-password-entry-row.c:50). This file used to be a
|
|
7
|
+
// near copy-paste of `adw-entry-row.ts` — a normalized diff showed the
|
|
8
|
+
// connectedCallback and attributeChangedCallback bodies were the same code — so
|
|
9
|
+
// every entry-row fix had to be made twice and one of the two always drifted.
|
|
10
|
+
// What is left here is only what the C subclass adds: the masked input, the peek
|
|
11
|
+
// toggle, and the caps-lock source.
|
|
12
|
+
//
|
|
13
|
+
// The reveal/caps-lock DERIVATION is HEADLESS and lives in
|
|
14
|
+
// `@gjsify/adwaita-core` (ADR 0004) as {@link PasswordEntryRowState}, which
|
|
15
|
+
// composes the parent's `EntryRowState` exactly like the C drives its
|
|
16
|
+
// parent through `adw_entry_row_set_show_indicator`. Two rules fall out of it
|
|
17
|
+
// that neither port had: peeking suppresses the caps-lock warning, and so does
|
|
18
|
+
// losing focus.
|
|
19
|
+
//
|
|
20
|
+
// Attributes: everything `<adw-entry-row>` observes, plus `revealed`.
|
|
21
|
+
// Properties: `revealed`, `capsLockOn` (+ everything inherited).
|
|
22
|
+
// Events: `notify::revealed` — only on a REAL change. The old implementation
|
|
23
|
+
// fired it on every `setAttribute`, including a redundant one, because
|
|
24
|
+
// attributeChangedCallback runs for a same-value write; the core's setter
|
|
25
|
+
// guard (the house rule at adw-entry-row.c:984/:1198/:1247) is what fixes it.
|
|
26
|
+
//
|
|
27
|
+
// Reference: refs/libadwaita/src/adw-password-entry-row.c
|
|
10
28
|
// Reference: refs/adwaita-web/adwaita-web/docs/widgets/passwordentryrow.md
|
|
11
|
-
// Reference: refs/libadwaita/src/adw-password-entry-row.c (peek toggle: the
|
|
12
|
-
// view-reveal / view-conceal symbolic swap; CSS node `row` carries `.entry`
|
|
13
|
-
// and `.password` style classes).
|
|
14
29
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
15
30
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
16
31
|
|
|
17
|
-
|
|
18
|
-
private _input!: HTMLInputElement;
|
|
19
|
-
private _titleEl!: HTMLSpanElement;
|
|
20
|
-
private _toggle!: HTMLButtonElement;
|
|
21
|
-
private _toggleIcon!: HTMLSpanElement;
|
|
22
|
-
private _initialized = false;
|
|
32
|
+
import { PasswordEntryRowState, type PasswordEntryRowRenderState } from '@gjsify/adwaita-core';
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
import { type AdwIcon, createAdwIcon } from './adw-icon.js';
|
|
35
|
+
|
|
36
|
+
import { AdwEntryRow } from './adw-entry-row.js';
|
|
37
|
+
|
|
38
|
+
export class AdwPasswordEntryRow extends AdwEntryRow {
|
|
39
|
+
/** The headless peek + caps-lock derivation, composing the parent row's state. */
|
|
40
|
+
private readonly _password = new PasswordEntryRowState(this._state);
|
|
41
|
+
private _toggle!: HTMLButtonElement;
|
|
42
|
+
private _toggleIcon!: AdwIcon;
|
|
27
43
|
|
|
28
|
-
get
|
|
29
|
-
return
|
|
44
|
+
static get observedAttributes(): string[] {
|
|
45
|
+
return [...AdwEntryRow.observedAttributes, 'revealed'];
|
|
30
46
|
}
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
// `_password_entry_row.scss` scopes its rules to `.adw-password-entry-row-*`,
|
|
49
|
+
// so the inherited DOM builder must name the parts that way.
|
|
50
|
+
protected override get _classPrefix(): string {
|
|
51
|
+
return 'adw-password-entry-row';
|
|
35
52
|
}
|
|
36
53
|
|
|
37
|
-
/** Whether the contents are shown in clear text (
|
|
54
|
+
/** Whether the contents are shown in clear text (`GtkText:visibility`). */
|
|
38
55
|
get revealed(): boolean {
|
|
39
|
-
return this.
|
|
56
|
+
return this._password.revealed;
|
|
40
57
|
}
|
|
41
58
|
|
|
42
59
|
set revealed(value: boolean) {
|
|
43
|
-
|
|
44
|
-
else this.removeAttribute('revealed');
|
|
60
|
+
this._password.setRevealed(value);
|
|
45
61
|
}
|
|
46
62
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.
|
|
63
|
+
/** Whether Caps Lock is engaged, as last observed from a keyboard event. */
|
|
64
|
+
get capsLockOn(): boolean {
|
|
65
|
+
return this._password.capsLockOn;
|
|
66
|
+
}
|
|
50
67
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Feed the platform's Caps Lock state in — GDK gets it from the keyboard
|
|
70
|
+
* device (adw-password-entry-row.c:111-115). Public because a host that
|
|
71
|
+
* knows better than `KeyboardEvent.getModifierState` (a virtual keyboard, a
|
|
72
|
+
* test) can drive it directly.
|
|
73
|
+
*/
|
|
74
|
+
setCapsLockOn(on: boolean): void {
|
|
75
|
+
this._password.setCapsLockOn(on);
|
|
76
|
+
}
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// and starts with visibility off.
|
|
59
|
-
this._input.type = this.hasAttribute('revealed') ? 'text' : 'password';
|
|
60
|
-
this._input.value = this.getAttribute('text') ?? '';
|
|
78
|
+
protected override _onConnected(): void {
|
|
79
|
+
// `gtk_text_set_visibility (…, FALSE)` + GTK_INPUT_PURPOSE_PASSWORD (C:158-160).
|
|
80
|
+
this._input.type = 'password';
|
|
61
81
|
this._input.autocomplete = 'off';
|
|
62
|
-
this._input.addEventListener('input', () => {
|
|
63
|
-
this.dispatchEvent(new CustomEvent('changed', { bubbles: true, detail: { text: this._input.value } }));
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Title floats as a small label above the masked value, both left-aligned
|
|
67
|
-
// in a column (the filled Adw.EntryRow look).
|
|
68
|
-
const text = document.createElement('div');
|
|
69
|
-
text.className = 'adw-password-entry-row-text';
|
|
70
|
-
text.append(this._titleEl, this._input);
|
|
71
82
|
|
|
72
|
-
// Trailing peek toggle — a flat icon button, vertically centred. Swaps
|
|
73
|
-
// between the reveal / conceal symbolics like the native widget.
|
|
74
83
|
this._toggle = document.createElement('button');
|
|
75
|
-
this._toggle.className = 'adw-button flat circular icon-only adw-password-entry-row-toggle';
|
|
76
84
|
this._toggle.type = 'button';
|
|
77
|
-
this.
|
|
78
|
-
this._toggleIcon
|
|
79
|
-
this._toggle.
|
|
80
|
-
this._toggle.addEventListener('click', () =>
|
|
81
|
-
|
|
85
|
+
this._toggle.className = `adw-button flat circular icon-only ${this._classPrefix}-toggle`;
|
|
86
|
+
this._toggleIcon = createAdwIcon(null);
|
|
87
|
+
this._toggle.append(this._toggleIcon);
|
|
88
|
+
this._toggle.addEventListener('click', () => this._password.togglePeek());
|
|
89
|
+
// C:152 — installed through add_suffix, so it is the FIRST suffix and a
|
|
90
|
+
// consumer's own suffix lands after it instead of replacing it.
|
|
91
|
+
this.addSuffix(this._toggle);
|
|
92
|
+
|
|
93
|
+
// GDK reads caps lock off the keyboard device; the browser exposes it
|
|
94
|
+
// only on keyboard events, so those are the source here.
|
|
95
|
+
const readCapsLock = (event: KeyboardEvent) => this._password.setCapsLockOn(event.getModifierState('CapsLock'));
|
|
96
|
+
this._input.addEventListener('keydown', readCapsLock);
|
|
97
|
+
this._input.addEventListener('keyup', readCapsLock);
|
|
98
|
+
|
|
99
|
+
// The indicator icon + tooltip are set once at init (C:169-171); their
|
|
100
|
+
// canonical names come from the core so both ports spell them alike.
|
|
101
|
+
const initial = this._password.state;
|
|
102
|
+
this.setIndicatorIconName(initial.indicatorIconName);
|
|
103
|
+
this.setIndicatorTooltip(initial.indicatorTooltip);
|
|
104
|
+
|
|
105
|
+
// Seed from the attribute BEFORE subscribing, so the first paint emits no
|
|
106
|
+
// `notify::revealed`.
|
|
107
|
+
if (this.hasAttribute('revealed')) this._password.setRevealed(this.getAttribute('revealed') !== 'false');
|
|
108
|
+
this._renderPassword(this._password.state);
|
|
109
|
+
this._password.subscribe((state) => {
|
|
110
|
+
this._renderPassword(state);
|
|
111
|
+
this.dispatchEvent(
|
|
112
|
+
new CustomEvent('notify::revealed', { bubbles: true, detail: { revealed: state.revealed } }),
|
|
113
|
+
);
|
|
82
114
|
});
|
|
83
|
-
|
|
84
|
-
// Trailing edit pencil (before the peek toggle), mirroring native.
|
|
85
|
-
const edit = document.createElement('span');
|
|
86
|
-
edit.className = 'adw-row-edit adw-icon adw-icon--document-edit';
|
|
87
|
-
edit.setAttribute('aria-hidden', 'true');
|
|
88
|
-
edit.addEventListener('click', () => this._input.focus());
|
|
89
|
-
|
|
90
|
-
this.replaceChildren(text, edit, this._toggle);
|
|
91
|
-
this._renderRevealed();
|
|
92
115
|
}
|
|
93
116
|
|
|
94
|
-
attributeChangedCallback(name: string,
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
} else if (name === 'text') {
|
|
99
|
-
if (this._input.value !== (value ?? '')) this._input.value = value ?? '';
|
|
100
|
-
} else if (name === 'revealed') {
|
|
101
|
-
this._renderRevealed();
|
|
102
|
-
this.dispatchEvent(
|
|
103
|
-
new CustomEvent('notify::revealed', { bubbles: true, detail: { revealed: this.revealed } }),
|
|
104
|
-
);
|
|
117
|
+
override attributeChangedCallback(name: string, oldValue: string | null, value: string | null) {
|
|
118
|
+
if (name !== 'revealed') {
|
|
119
|
+
super.attributeChangedCallback(name, oldValue, value);
|
|
120
|
+
return;
|
|
105
121
|
}
|
|
122
|
+
if (!this._initialized) return;
|
|
123
|
+
// The core guard makes this idempotent, which is what keeps the reflection
|
|
124
|
+
// in `_renderPassword` from bouncing back as a second notification.
|
|
125
|
+
this._password.setRevealed(value !== null && value !== 'false');
|
|
106
126
|
}
|
|
107
127
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
this._input.type = revealed ? 'text' : 'password';
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
this._toggleIcon.
|
|
114
|
-
|
|
115
|
-
this._toggle.title =
|
|
116
|
-
this._toggle.setAttribute('aria-label',
|
|
117
|
-
this._toggle.setAttribute('aria-pressed', String(revealed));
|
|
128
|
+
/** `notify_visibility_cb` (C:62-81), applied to the DOM. */
|
|
129
|
+
private _renderPassword(state: PasswordEntryRowRenderState): void {
|
|
130
|
+
this._input.type = state.revealed ? 'text' : 'password';
|
|
131
|
+
// The libadwaita name travels in `data-icon-name`; the mask class is the
|
|
132
|
+
// curated @gjsify/adwaita-icons spelling of the same symbolic.
|
|
133
|
+
this._toggleIcon.dataset.iconName = state.peekIconName;
|
|
134
|
+
this._toggleIcon.iconName = state.peekIconName;
|
|
135
|
+
this._toggle.title = state.peekLabel;
|
|
136
|
+
this._toggle.setAttribute('aria-label', state.peekLabel);
|
|
137
|
+
this._toggle.setAttribute('aria-pressed', String(state.revealed));
|
|
138
|
+
if (state.revealed) this.setAttribute('revealed', '');
|
|
139
|
+
else this.removeAttribute('revealed');
|
|
118
140
|
}
|
|
119
141
|
}
|
|
120
142
|
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// <adw-popover> — the elevated surface that pops up next to an anchor, the web
|
|
2
|
+
// counterpart of GtkPopover as libadwaita styles it. It is the ONE popover in
|
|
3
|
+
// this package: `<adw-menu-button>`, `<adw-drop-down>` and `<adw-split-button>`
|
|
4
|
+
// each used to build their own, and no two agreed. The surface disagreed (12px /
|
|
5
|
+
// 12px / 9px radius against libadwaita's 15px; two correct three-layer shadows
|
|
6
|
+
// and one invented two-layer one) and so did the behaviour — the split button
|
|
7
|
+
// had neither Escape dismissal nor arrow-key navigation, while the other two
|
|
8
|
+
// spelled the same wrap arithmetic out twice.
|
|
9
|
+
//
|
|
10
|
+
// The dismissal + keyboard state machine lives in `@gjsify/adwaita-core`
|
|
11
|
+
// ({@link PopoverState}, {@link resolvePopoverKey}, ADR 0004); this element owns
|
|
12
|
+
// the DOM: the surface node, the document-level listeners, focus movement, and
|
|
13
|
+
// the `open` attribute reflection.
|
|
14
|
+
//
|
|
15
|
+
// PLACEMENT IS CSS, NOT SCRIPT. `_popover.scss` positions the surface off the
|
|
16
|
+
// `position` / `align` attributes; nothing here measures a rect or flips on
|
|
17
|
+
// overflow, exactly as the three copies never did. Core carries no positioner
|
|
18
|
+
// for the same reason — see the module header of `adwaita-core/src/popover.ts`.
|
|
19
|
+
//
|
|
20
|
+
// Attributes:
|
|
21
|
+
// open — boolean; reflects (and drives) the visible state.
|
|
22
|
+
// position — bottom | top | start | end (default bottom) — which side of the
|
|
23
|
+
// anchor the surface sits on. `GtkPopover:position`'s own
|
|
24
|
+
// autohide/has-arrow semantics are NOT verifiable in this tree
|
|
25
|
+
// (refs/gtk is empty, libadwaita vendors no adw-popover.c), so only
|
|
26
|
+
// the placement axis is modelled.
|
|
27
|
+
// align — start | end (default start) — which edge it lines up with.
|
|
28
|
+
// role — menu | listbox (default menu) — the ARIA role of the surface.
|
|
29
|
+
// An a11y fact ONLY; it does not pick the surface variant, see
|
|
30
|
+
// `menu` below.
|
|
31
|
+
// menu — boolean; libadwaita's `.menu` STYLE CLASS on the popover node
|
|
32
|
+
// (0 padding on the contents, `$menu_margin` on the item box —
|
|
33
|
+
// _menus.scss:58-66), opt-in exactly as it is in GTK.
|
|
34
|
+
// DO NOT infer it from `role`: `GtkDropDown`'s popover is a
|
|
35
|
+
// `popover.menu` too (`dropdown { popover.menu { … } }`,
|
|
36
|
+
// _dropdowns.scss:22) while its rows are `option`s, not
|
|
37
|
+
// `menuitem`s. Tying the surface to the role gets that backwards
|
|
38
|
+
// and pads the drop-down like a bare content popover.
|
|
39
|
+
// Properties:
|
|
40
|
+
// open — whether the popover is showing (get/set).
|
|
41
|
+
// anchor — the element the surface is positioned against and returns focus
|
|
42
|
+
// to. Defaults to `parentElement` (get/set).
|
|
43
|
+
// items — the navigable rows, in DOM order (get). Used for keyboard moves.
|
|
44
|
+
// Methods (GtkPopover's own vocabulary):
|
|
45
|
+
// popup() — show it. popdown() — hide it.
|
|
46
|
+
// Events:
|
|
47
|
+
// `notify::open` (CustomEvent, bubbles, detail = { open }) — mirrors the
|
|
48
|
+
// GObject property-notify; fires on every change, programmatic included.
|
|
49
|
+
// `popover-item-activated` (CustomEvent, bubbles, detail = { index }) — a row
|
|
50
|
+
// was chosen by keyboard. Click activation stays the row's own listener.
|
|
51
|
+
//
|
|
52
|
+
// Reference: refs/libadwaita/src/stylesheet/widgets/_popovers.scss:7-28 (popover > contents)
|
|
53
|
+
// Reference: refs/libadwaita/src/stylesheet/widgets/_menus.scss:58-66, :134-156 (popover.menu, modelbutton)
|
|
54
|
+
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
55
|
+
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
56
|
+
|
|
57
|
+
import { PopoverState, resolvePopoverKey } from '@gjsify/adwaita-core';
|
|
58
|
+
|
|
59
|
+
/** Which side of the anchor the surface sits on. */
|
|
60
|
+
export type AdwPopoverPosition = 'bottom' | 'top' | 'start' | 'end';
|
|
61
|
+
|
|
62
|
+
/** Which edge of the anchor the surface lines up with. */
|
|
63
|
+
export type AdwPopoverAlign = 'start' | 'end';
|
|
64
|
+
|
|
65
|
+
/** The ARIA role of the surface — it decides the row role consumers give their items. */
|
|
66
|
+
export type AdwPopoverRole = 'menu' | 'listbox';
|
|
67
|
+
|
|
68
|
+
const POSITIONS: readonly AdwPopoverPosition[] = ['bottom', 'top', 'start', 'end'];
|
|
69
|
+
const ALIGNS: readonly AdwPopoverAlign[] = ['start', 'end'];
|
|
70
|
+
const ROLES: readonly AdwPopoverRole[] = ['menu', 'listbox'];
|
|
71
|
+
|
|
72
|
+
/** The row selector keyboard navigation walks. */
|
|
73
|
+
const ITEM_SELECTOR = '.adw-popover-item';
|
|
74
|
+
|
|
75
|
+
export class AdwPopover extends HTMLElement {
|
|
76
|
+
private readonly _state = new PopoverState();
|
|
77
|
+
private _anchor: HTMLElement | null = null;
|
|
78
|
+
private _initialized = false;
|
|
79
|
+
/** Guards the `open` attribute↔property loop, which would otherwise recurse. */
|
|
80
|
+
private _reflecting = false;
|
|
81
|
+
|
|
82
|
+
private _onDocumentPointerDown = (event: Event): void => {
|
|
83
|
+
const target = event.target as Node;
|
|
84
|
+
if (this.contains(target)) return;
|
|
85
|
+
// A click on the anchor is the anchor's own toggle — closing here too
|
|
86
|
+
// would close and immediately reopen (or reopen and immediately close).
|
|
87
|
+
if (this.anchor?.contains(target) === true) return;
|
|
88
|
+
this._state.dismiss();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
private _onDocumentKeyDown = (event: KeyboardEvent): void => {
|
|
92
|
+
if (event.key !== 'Escape') return;
|
|
93
|
+
// Captured at the document, so a nested popover's Escape does not also
|
|
94
|
+
// close its parent.
|
|
95
|
+
event.stopPropagation();
|
|
96
|
+
this._state.dismiss();
|
|
97
|
+
this.anchor?.focus();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
static get observedAttributes() {
|
|
101
|
+
return ['open', 'position', 'align', 'role', 'menu'];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Whether the popover is showing. */
|
|
105
|
+
get open(): boolean {
|
|
106
|
+
return this._state.open;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
set open(value: boolean) {
|
|
110
|
+
if (value) this._state.popup();
|
|
111
|
+
else this._state.popdown();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The element the surface is positioned against and hands focus back to.
|
|
116
|
+
* Defaults to `parentElement`, which is the common case: the popover is the
|
|
117
|
+
* last child of the widget that owns it.
|
|
118
|
+
*/
|
|
119
|
+
get anchor(): HTMLElement | null {
|
|
120
|
+
return this._anchor ?? this.parentElement;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
set anchor(value: HTMLElement | null) {
|
|
124
|
+
this._anchor = value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Which side of the anchor the surface sits on. */
|
|
128
|
+
get position(): AdwPopoverPosition {
|
|
129
|
+
const value = this.getAttribute('position');
|
|
130
|
+
return POSITIONS.includes(value as AdwPopoverPosition) ? (value as AdwPopoverPosition) : 'bottom';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
set position(value: AdwPopoverPosition) {
|
|
134
|
+
this.setAttribute('position', value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Which edge of the anchor the surface lines up with. */
|
|
138
|
+
get align(): AdwPopoverAlign {
|
|
139
|
+
const value = this.getAttribute('align');
|
|
140
|
+
return ALIGNS.includes(value as AdwPopoverAlign) ? (value as AdwPopoverAlign) : 'start';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
set align(value: AdwPopoverAlign) {
|
|
144
|
+
this.setAttribute('align', value);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The surface's ARIA role. */
|
|
148
|
+
get popoverRole(): AdwPopoverRole {
|
|
149
|
+
const value = this.getAttribute('role');
|
|
150
|
+
return ROLES.includes(value as AdwPopoverRole) ? (value as AdwPopoverRole) : 'menu';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The navigable rows, in DOM order. Hidden rows are skipped — a filtered list navigates what it shows. */
|
|
154
|
+
get items(): HTMLElement[] {
|
|
155
|
+
return [...this.querySelectorAll<HTMLElement>(ITEM_SELECTOR)].filter((item) => !item.hidden);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Show the popover — `gtk_popover_popup()`. */
|
|
159
|
+
popup(): void {
|
|
160
|
+
this._state.popup();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Hide the popover — `gtk_popover_popdown()`. */
|
|
164
|
+
popdown(): void {
|
|
165
|
+
this._state.popdown();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Subscribe to open-state changes. Returns an unsubscribe function. */
|
|
169
|
+
subscribe(listener: (open: boolean) => void): () => void {
|
|
170
|
+
return this._state.subscribe((change) => listener(change.open));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
connectedCallback() {
|
|
174
|
+
if (this._initialized) return;
|
|
175
|
+
this._initialized = true;
|
|
176
|
+
|
|
177
|
+
this.classList.add('adw-popover');
|
|
178
|
+
if (!this.hasAttribute('role')) this.setAttribute('role', 'menu');
|
|
179
|
+
// The `menu` variant is NOT defaulted or inferred here — it is GTK's
|
|
180
|
+
// `.menu` style class, and the widget that owns the popover is what adds
|
|
181
|
+
// it, as in the C. See the attribute docs above for why role is the
|
|
182
|
+
// wrong thing to key it off.
|
|
183
|
+
|
|
184
|
+
this.addEventListener('keydown', (event) => this._onKeyDown(event));
|
|
185
|
+
this._state.subscribe((change) => this._onStateChange(change.open));
|
|
186
|
+
|
|
187
|
+
// The `open` attribute may already be present in server-rendered markup.
|
|
188
|
+
if (this.hasAttribute('open')) this._state.popup();
|
|
189
|
+
this._render();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
disconnectedCallback() {
|
|
193
|
+
this._unbindDocument();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
attributeChangedCallback(name: string, _previous: string | null, value: string | null) {
|
|
197
|
+
if (!this._initialized) return;
|
|
198
|
+
if (name === 'open') {
|
|
199
|
+
if (this._reflecting) return;
|
|
200
|
+
if (value !== null) this._state.popup();
|
|
201
|
+
else this._state.popdown();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (name === 'role' && value === null) {
|
|
205
|
+
// A removed role falls back to the default rather than leaving the
|
|
206
|
+
// surface unlabelled to assistive tech.
|
|
207
|
+
this.setAttribute('role', 'menu');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this._render();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── internals ──────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
private _onStateChange(open: boolean): void {
|
|
216
|
+
if (open) this._bindDocument();
|
|
217
|
+
else this._unbindDocument();
|
|
218
|
+
|
|
219
|
+
this._reflecting = true;
|
|
220
|
+
if (open) this.setAttribute('open', '');
|
|
221
|
+
else this.removeAttribute('open');
|
|
222
|
+
this._reflecting = false;
|
|
223
|
+
|
|
224
|
+
this._render();
|
|
225
|
+
this.dispatchEvent(new CustomEvent('notify::open', { bubbles: true, detail: { open } }));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private _bindDocument(): void {
|
|
229
|
+
document.addEventListener('pointerdown', this._onDocumentPointerDown);
|
|
230
|
+
// Capture, so Escape reaches us before a focused child swallows it.
|
|
231
|
+
document.addEventListener('keydown', this._onDocumentKeyDown, true);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private _unbindDocument(): void {
|
|
235
|
+
document.removeEventListener('pointerdown', this._onDocumentPointerDown);
|
|
236
|
+
document.removeEventListener('keydown', this._onDocumentKeyDown, true);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private _render(): void {
|
|
240
|
+
this.hidden = !this._state.open;
|
|
241
|
+
this.dataset.position = this.position;
|
|
242
|
+
this.dataset.align = this.align;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Arrow/Home/End/Enter/Space inside the surface. The arithmetic is core's
|
|
247
|
+
* ({@link resolvePopoverKey}); this only maps its answer onto the DOM.
|
|
248
|
+
*
|
|
249
|
+
* Escape is NOT handled here — it is bound at the document in capture phase
|
|
250
|
+
* so it works no matter where focus sits, including a search entry inside
|
|
251
|
+
* the popover.
|
|
252
|
+
*/
|
|
253
|
+
private _onKeyDown(event: KeyboardEvent): void {
|
|
254
|
+
if (!this._state.open) return;
|
|
255
|
+
const items = this.items;
|
|
256
|
+
const active = document.activeElement as HTMLElement | null;
|
|
257
|
+
const currentIndex = active === null ? -1 : items.indexOf(active);
|
|
258
|
+
const hasSearch = this.querySelector('input, textarea') !== null;
|
|
259
|
+
|
|
260
|
+
const { action, index } = resolvePopoverKey(event.key, {
|
|
261
|
+
itemCount: items.length,
|
|
262
|
+
currentIndex,
|
|
263
|
+
hasSearch,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
if (action === 'focus') {
|
|
267
|
+
event.preventDefault();
|
|
268
|
+
items[index]?.focus();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (action === 'activate') {
|
|
272
|
+
// preventDefault BEFORE the synthetic click: a focused <button>
|
|
273
|
+
// activates natively on Enter (keydown) and Space (keyup), so
|
|
274
|
+
// without it the row would fire twice.
|
|
275
|
+
event.preventDefault();
|
|
276
|
+
items[index]?.click();
|
|
277
|
+
this.dispatchEvent(new CustomEvent('popover-item-activated', { bubbles: true, detail: { index } }));
|
|
278
|
+
}
|
|
279
|
+
// 'close' arrives through the document capture handler; 'none' is not ours.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
customElements.define('adw-popover', AdwPopover);
|