@gjsify/adwaita-web 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adwaita-web.css +786 -272
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-about-dialog.spec.d.ts +1 -0
- package/lib/types/adw-action-rows.spec.d.ts +1 -0
- package/lib/types/adw-avatar.spec.d.ts +1 -0
- package/lib/types/adw-banner.spec.d.ts +1 -0
- package/lib/types/adw-button-content.spec.d.ts +1 -0
- package/lib/types/adw-carousel.spec.d.ts +1 -0
- package/lib/types/adw-checks.spec.d.ts +1 -0
- package/lib/types/adw-header-bar.spec.d.ts +1 -0
- package/lib/types/adw-icon.spec.d.ts +1 -0
- package/lib/types/adw-progress-bar.spec.d.ts +1 -0
- package/lib/types/adw-sidebar.spec.d.ts +1 -0
- package/lib/types/adw-switch.spec.d.ts +1 -0
- package/lib/types/adw-wrap-box.spec.d.ts +1 -0
- package/lib/types/bottom-sheet.spec.d.ts +1 -0
- package/lib/types/breakpoints.d.ts +22 -0
- package/lib/types/breakpoints.spec.d.ts +1 -0
- package/lib/types/chrome.spec.d.ts +1 -0
- package/lib/types/elements/adw-about-dialog.d.ts +47 -8
- package/lib/types/elements/adw-action-row.d.ts +35 -0
- package/lib/types/elements/adw-avatar.d.ts +11 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
- package/lib/types/elements/adw-button-content.d.ts +12 -0
- package/lib/types/elements/adw-button-row.d.ts +10 -1
- package/lib/types/elements/adw-carousel.d.ts +111 -8
- package/lib/types/elements/adw-checks.d.ts +57 -0
- package/lib/types/elements/adw-clamp.d.ts +4 -1
- package/lib/types/elements/adw-data-grid.d.ts +11 -31
- package/lib/types/elements/adw-drop-down.d.ts +17 -20
- package/lib/types/elements/adw-entry-row.d.ts +93 -3
- package/lib/types/elements/adw-expander-row.d.ts +7 -1
- package/lib/types/elements/adw-header-bar.d.ts +15 -0
- package/lib/types/elements/adw-icon.d.ts +25 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +37 -11
- package/lib/types/elements/adw-menu-button.d.ts +6 -6
- package/lib/types/elements/adw-navigation-split-view.d.ts +6 -0
- package/lib/types/elements/adw-navigation-view.d.ts +63 -20
- package/lib/types/elements/adw-overlay-split-view.d.ts +38 -0
- package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
- package/lib/types/elements/adw-popover.d.ts +58 -0
- package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
- package/lib/types/elements/adw-preferences-group.d.ts +31 -0
- package/lib/types/elements/adw-progress-bar.d.ts +38 -0
- package/lib/types/elements/adw-sidebar.d.ts +37 -5
- package/lib/types/elements/adw-split-button.d.ts +53 -7
- package/lib/types/elements/adw-switch-row.d.ts +11 -2
- package/lib/types/elements/adw-switch.d.ts +15 -0
- package/lib/types/elements/adw-tab-view.d.ts +129 -12
- package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
- package/lib/types/elements/adw-view-stack.d.ts +27 -19
- package/lib/types/elements/adw-view-switcher-bar.d.ts +23 -7
- package/lib/types/elements/adw-view-switcher.d.ts +22 -11
- package/lib/types/elements/adw-window-title.d.ts +3 -1
- package/lib/types/elements/view-switcher-dom.d.ts +44 -0
- package/lib/types/entry-rows.spec.d.ts +1 -0
- package/lib/types/index.d.ts +7 -0
- package/lib/types/navigation-view.spec.d.ts +1 -0
- package/lib/types/preferences-search.d.ts +20 -0
- package/lib/types/preferences.spec.d.ts +1 -0
- package/lib/types/split-button.spec.d.ts +1 -0
- package/lib/types/split-views.spec.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/tab-view.spec.d.ts +1 -0
- package/lib/types/view-stack.spec.d.ts +1 -0
- package/lib/types/view-switcher.spec.d.ts +1 -0
- package/package.json +6 -6
- package/scss/_banner.scss +13 -0
- package/scss/_bottom_sheet.scss +5 -6
- package/scss/_button.scss +3 -26
- package/scss/_button_content.scss +28 -0
- package/scss/_checks.scss +228 -0
- package/scss/_clamp.scss +9 -3
- package/scss/_data_grid.scss +15 -7
- package/scss/_drop_down.scss +12 -55
- package/scss/_expander_row.scss +5 -50
- package/scss/_icon.scss +51 -0
- package/scss/_icons.generated.scss +6 -0
- package/scss/_inline_view_switcher.scss +34 -0
- package/scss/_labels.scss +135 -0
- package/scss/_menu_button.scss +10 -70
- package/scss/_popover.scss +186 -0
- package/scss/_preferences.scss +29 -0
- package/scss/_preferences_dialog.scss +17 -0
- package/scss/_progress_bar.scss +161 -0
- package/scss/_reset.scss +18 -7
- package/scss/_spinner.scss +7 -3
- package/scss/_split_button.scss +6 -50
- package/scss/_switch.scss +90 -0
- package/scss/_switch_row.scss +6 -52
- package/scss/_tab_view.scss +64 -0
- package/scss/_theme.scss +10 -0
- package/scss/_toolbar_view.scss +74 -0
- package/scss/_variables.scss +34 -0
- package/scss/_view_switcher.scss +40 -0
- package/scss/_view_switcher_bar.scss +37 -0
- package/scss/_wrap_box.scss +56 -6
- package/scss/adwaita-skin.scss +23 -0
- package/src/adw-about-dialog.spec.ts +350 -0
- package/src/adw-action-rows.spec.ts +342 -0
- package/src/adw-avatar.spec.ts +139 -0
- package/src/adw-banner.spec.ts +152 -0
- package/src/adw-button-content.spec.ts +183 -0
- package/src/adw-button-row.spec.ts +12 -6
- package/src/adw-carousel.spec.ts +389 -0
- package/src/adw-checks.spec.ts +368 -0
- package/src/adw-drop-down.spec.ts +123 -0
- package/src/adw-header-bar.spec.ts +83 -0
- package/src/adw-icon.spec.ts +167 -0
- package/src/adw-progress-bar.spec.ts +173 -0
- package/src/adw-sidebar.spec.ts +332 -0
- package/src/adw-switch.spec.ts +242 -0
- package/src/adw-tab-view.spec.ts +12 -3
- package/src/adw-view-switcher-bar.spec.ts +61 -13
- package/src/adw-wrap-box.spec.ts +194 -0
- package/src/bottom-sheet.spec.ts +325 -0
- package/src/breakpoints.spec.ts +147 -0
- package/src/breakpoints.ts +67 -0
- package/src/chrome.spec.ts +301 -0
- package/src/elements/adw-about-dialog.ts +223 -163
- package/src/elements/adw-action-row.ts +120 -10
- package/src/elements/adw-avatar.ts +48 -54
- package/src/elements/adw-banner.ts +46 -10
- package/src/elements/adw-bottom-sheet.ts +145 -60
- package/src/elements/adw-button-content.ts +107 -33
- package/src/elements/adw-button-row.ts +61 -37
- package/src/elements/adw-button.ts +5 -7
- package/src/elements/adw-carousel.ts +467 -105
- package/src/elements/adw-checks.ts +299 -0
- package/src/elements/adw-clamp.ts +80 -10
- package/src/elements/adw-combo-row.ts +15 -7
- package/src/elements/adw-data-grid.ts +43 -91
- package/src/elements/adw-drop-down.ts +117 -128
- package/src/elements/adw-entry-row.ts +404 -36
- package/src/elements/adw-expander-row.ts +61 -29
- package/src/elements/adw-header-bar.ts +31 -6
- package/src/elements/adw-icon.ts +148 -0
- package/src/elements/adw-inline-view-switcher.ts +240 -129
- package/src/elements/adw-menu-button.ts +93 -70
- package/src/elements/adw-navigation-split-view.ts +48 -7
- package/src/elements/adw-navigation-view.ts +286 -148
- package/src/elements/adw-overlay-split-view.ts +164 -14
- package/src/elements/adw-password-entry-row.ts +111 -89
- package/src/elements/adw-popover.ts +283 -0
- package/src/elements/adw-preferences-dialog.ts +120 -21
- package/src/elements/adw-preferences-group.ts +128 -22
- package/src/elements/adw-progress-bar.ts +189 -0
- package/src/elements/adw-sidebar.ts +301 -142
- package/src/elements/adw-spin-row.ts +11 -5
- package/src/elements/adw-spinner.ts +23 -8
- package/src/elements/adw-split-button.ts +371 -92
- package/src/elements/adw-status-page.ts +9 -9
- package/src/elements/adw-switch-row.ts +100 -30
- package/src/elements/adw-switch.ts +129 -0
- package/src/elements/adw-tab-view.ts +682 -129
- package/src/elements/adw-toggle-group.ts +5 -7
- package/src/elements/adw-toolbar-view.ts +84 -21
- package/src/elements/adw-view-stack.ts +162 -85
- package/src/elements/adw-view-switcher-bar.ts +163 -81
- package/src/elements/adw-view-switcher.ts +231 -112
- package/src/elements/adw-window-title.ts +44 -11
- package/src/elements/adw-wrap-box.ts +131 -35
- package/src/elements/view-switcher-dom.ts +108 -0
- package/src/entry-rows.spec.ts +391 -0
- package/src/index.ts +23 -0
- package/src/navigation-view.spec.ts +435 -0
- package/src/preferences-search.ts +135 -0
- package/src/preferences.spec.ts +250 -0
- package/src/source-view/adw-source-view.ts +2 -5
- package/src/split-button.spec.ts +470 -0
- package/src/split-views.spec.ts +309 -0
- package/src/styles.generated.ts +1 -1
- package/src/tab-view.spec.ts +451 -0
- package/src/test.browser.mts +49 -0
- package/src/view-stack.spec.ts +272 -0
- package/src/view-switcher.spec.ts +446 -0
|
@@ -0,0 +1,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);
|
|
@@ -5,11 +5,21 @@
|
|
|
5
5
|
// shows the dialog title and a trailing close button.
|
|
6
6
|
//
|
|
7
7
|
// Pages are declared as <adw-preferences-page> children (each with optional
|
|
8
|
-
// `title` / `icon-name` attributes + arbitrary group
|
|
9
|
-
//
|
|
8
|
+
// `title` / `icon-name` / `name` / `use-underline` attributes + arbitrary group
|
|
9
|
+
// content) and are moved — as ELEMENTS, keeping their identity — into the
|
|
10
|
+
// scrolled body. `add(page)` accepts one at any later time, mirroring
|
|
11
|
+
// adw_preferences_dialog_add.
|
|
10
12
|
// (Adw shows a view-switcher when there is more than one page and the window is
|
|
11
|
-
// wide; the web port renders the pages
|
|
12
|
-
// case the storybook exercises.
|
|
13
|
+
// wide, and shows exactly one page at a time; the web port renders the pages
|
|
14
|
+
// stacked, matching the single-page common case the storybook exercises. The
|
|
15
|
+
// page COLLECTION model — visible-page, visible-page-name, the auto-pick —
|
|
16
|
+
// belongs to Adw.ViewStack and is available as `ViewStackState` in
|
|
17
|
+
// @gjsify/adwaita-core when this port grows a switcher.)
|
|
18
|
+
//
|
|
19
|
+
// `search(query)` implements the dialog's search over those pages: the corpus,
|
|
20
|
+
// the filter and the `page → group` result subtitles all come from
|
|
21
|
+
// @gjsify/adwaita-core, so the browser and NativeScript dialogs return the same
|
|
22
|
+
// results for the same tree.
|
|
13
23
|
//
|
|
14
24
|
// Like Adw.Dialog the dialog starts hidden; `present()` / setting the `open`
|
|
15
25
|
// attribute reveals it. It is dismissed by Escape, by clicking the scrim, or by
|
|
@@ -42,16 +52,57 @@
|
|
|
42
52
|
// Copyright (c) 2023-2024 GNOME Foundation Inc. (libadwaita). LGPLv2.1+.
|
|
43
53
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
44
54
|
|
|
55
|
+
import type { PreferencesSearchResult, SearchPreferencesOptions } from '@gjsify/adwaita-core';
|
|
56
|
+
import { searchPreferencesDom } from '../preferences-search.js';
|
|
57
|
+
|
|
45
58
|
const DEFAULT_CONTENT_WIDTH = 640;
|
|
46
59
|
|
|
47
60
|
/**
|
|
48
|
-
* A single preferences page. Child of <adw-preferences-dialog
|
|
49
|
-
*
|
|
61
|
+
* A single preferences page. Child of <adw-preferences-dialog>. Carries optional
|
|
62
|
+
* `title` / `icon-name` / `name` / `use-underline` attributes and any
|
|
50
63
|
* <adw-preferences-group> content — mirrors Adw.PreferencesPage.
|
|
64
|
+
*
|
|
65
|
+
* The dialog keeps the page ELEMENT rather than hoisting its children out,
|
|
66
|
+
* because the page's identity is load-bearing: `adw_preferences_dialog_add`
|
|
67
|
+
* binds title / icon-name / name / use-underline / visible onto the view-stack
|
|
68
|
+
* page (adw-preferences-dialog.c:707-711), and `create_search_row_subtitle`
|
|
69
|
+
* needs the title back the moment a second page is visible.
|
|
51
70
|
*/
|
|
52
71
|
export class AdwPreferencesPage extends HTMLElement {
|
|
53
72
|
static get observedAttributes() {
|
|
54
|
-
return ['title', 'icon-name', 'open'];
|
|
73
|
+
return ['title', 'icon-name', 'name', 'use-underline', 'open'];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `adw_preferences_page_get_name` — the view-stack child name. */
|
|
77
|
+
get name(): string {
|
|
78
|
+
return this.getAttribute('name') ?? '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
set name(value: string) {
|
|
82
|
+
this.setAttribute('name', value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** `AdwPreferencesPage:icon-name`, shown by a view switcher. */
|
|
86
|
+
get iconName(): string {
|
|
87
|
+
return this.getAttribute('icon-name') ?? '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
set iconName(value: string) {
|
|
91
|
+
this.setAttribute('icon-name', value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `AdwPreferencesPage:use-underline` — the title carries a mnemonic. */
|
|
95
|
+
get useUnderline(): boolean {
|
|
96
|
+
return this.hasAttribute('use-underline');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
set useUnderline(value: boolean) {
|
|
100
|
+
this.toggleAttribute('use-underline', value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The page's groups, in document order. */
|
|
104
|
+
get groups(): HTMLElement[] {
|
|
105
|
+
return Array.from(this.querySelectorAll('adw-preferences-group'));
|
|
55
106
|
}
|
|
56
107
|
}
|
|
57
108
|
|
|
@@ -61,6 +112,7 @@ export class AdwPreferencesDialog extends HTMLElement {
|
|
|
61
112
|
private _dialogEl!: HTMLDivElement;
|
|
62
113
|
private _titleEl!: HTMLSpanElement;
|
|
63
114
|
private _bodyEl!: HTMLDivElement;
|
|
115
|
+
private _pagesEl!: HTMLDivElement;
|
|
64
116
|
private _closeBtn!: HTMLButtonElement;
|
|
65
117
|
|
|
66
118
|
static get observedAttributes() {
|
|
@@ -116,18 +168,66 @@ export class AdwPreferencesDialog extends HTMLElement {
|
|
|
116
168
|
this.open = false;
|
|
117
169
|
}
|
|
118
170
|
|
|
171
|
+
/**
|
|
172
|
+
* `adw_preferences_dialog_add` (adw-preferences-dialog.c:693-716) — add a
|
|
173
|
+
* page at ANY time.
|
|
174
|
+
*
|
|
175
|
+
* Before this the pages were snapshotted once, in `connectedCallback`; a
|
|
176
|
+
* page appended later stayed a sibling of the scrim and rendered OUTSIDE
|
|
177
|
+
* the dialog card. GTK and the NativeScript port both accept a page
|
|
178
|
+
* whenever it arrives.
|
|
179
|
+
*/
|
|
180
|
+
add(page: AdwPreferencesPage): void {
|
|
181
|
+
this._pagesEl.appendChild(page);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* `adw_preferences_dialog_remove` (:723-745). C logs
|
|
186
|
+
* `ADW_CRITICAL_CANNOT_REMOVE_CHILD` and no-ops for a page it does not own;
|
|
187
|
+
* returning `false` is the same contract without a console write.
|
|
188
|
+
*
|
|
189
|
+
* NOT named `remove`: `ChildNode.remove()` already exists on every element
|
|
190
|
+
* and takes no arguments, so overriding it with a different signature makes
|
|
191
|
+
* the class un-assignable to `HTMLElement` — which `customElements.define`
|
|
192
|
+
* requires.
|
|
193
|
+
*/
|
|
194
|
+
removePage(page: AdwPreferencesPage): boolean {
|
|
195
|
+
if (page.parentNode !== this._pagesEl) return false;
|
|
196
|
+
this._pagesEl.removeChild(page);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The dialog's pages, in add order. */
|
|
201
|
+
get pages(): AdwPreferencesPage[] {
|
|
202
|
+
return Array.from(this._pagesEl.querySelectorAll('adw-preferences-page')) as AdwPreferencesPage[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The preferences search (`filter_search_results` +
|
|
207
|
+
* `create_search_row_subtitle`), over this dialog's live page subtree.
|
|
208
|
+
*
|
|
209
|
+
* The whole pipeline is `@gjsify/adwaita-core`'s, so this dialog and the
|
|
210
|
+
* NativeScript one answer identically for the same tree — including the
|
|
211
|
+
* three corpus filters (visible page → visible group → titled visible row)
|
|
212
|
+
* and the case fold that makes `strasse` find `Straße`.
|
|
213
|
+
*
|
|
214
|
+
* An empty query returns the WHOLE corpus, which is what C does and what
|
|
215
|
+
* makes the results list non-empty before the user types.
|
|
216
|
+
*/
|
|
217
|
+
search(query: string, options?: SearchPreferencesOptions): PreferencesSearchResult<Element>[] {
|
|
218
|
+
return searchPreferencesDom(this._pagesEl, query, options);
|
|
219
|
+
}
|
|
220
|
+
|
|
119
221
|
connectedCallback() {
|
|
120
222
|
if (this._initialized) return;
|
|
121
223
|
this._initialized = true;
|
|
122
224
|
|
|
123
|
-
// Snapshot the declared
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
const claimed = new Set<Node>([...pages, ...pageContent]);
|
|
130
|
-
const unwrapped = Array.from(this.childNodes).filter((node) => !claimed.has(node));
|
|
225
|
+
// Snapshot the declared children before taking over the subtree. The
|
|
226
|
+
// page ELEMENTS move into the body intact — their title / name /
|
|
227
|
+
// icon-name are what a search result subtitle and a view switcher read
|
|
228
|
+
// — and any unwrapped children follow them (Adw.PreferencesDialog's
|
|
229
|
+
// buildable default adds a child as a page).
|
|
230
|
+
const declared = Array.from(this.childNodes);
|
|
131
231
|
|
|
132
232
|
// Full-cover scrim — clicking it dismisses the dialog (or raises
|
|
133
233
|
// close-attempt when locked).
|
|
@@ -169,15 +269,14 @@ export class AdwPreferencesDialog extends HTMLElement {
|
|
|
169
269
|
|
|
170
270
|
header.append(leading, this._titleEl, trailing);
|
|
171
271
|
|
|
172
|
-
// Scrolled body — a clamped column of the declared pages
|
|
272
|
+
// Scrolled body — a clamped column of the declared pages.
|
|
173
273
|
this._bodyEl = document.createElement('div');
|
|
174
274
|
this._bodyEl.className = 'adw-preferences-dialog-body';
|
|
175
275
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
for (const child of
|
|
179
|
-
|
|
180
|
-
this._bodyEl.appendChild(clamp);
|
|
276
|
+
this._pagesEl = document.createElement('div');
|
|
277
|
+
this._pagesEl.className = 'adw-preferences-dialog-pages';
|
|
278
|
+
for (const child of declared) this._pagesEl.appendChild(child);
|
|
279
|
+
this._bodyEl.appendChild(this._pagesEl);
|
|
181
280
|
|
|
182
281
|
this._dialogEl.append(header, this._bodyEl);
|
|
183
282
|
|