@gjsify/adwaita-web 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adwaita-web.css +786 -272
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-about-dialog.spec.d.ts +1 -0
- package/lib/types/adw-action-rows.spec.d.ts +1 -0
- package/lib/types/adw-avatar.spec.d.ts +1 -0
- package/lib/types/adw-banner.spec.d.ts +1 -0
- package/lib/types/adw-button-content.spec.d.ts +1 -0
- package/lib/types/adw-carousel.spec.d.ts +1 -0
- package/lib/types/adw-checks.spec.d.ts +1 -0
- package/lib/types/adw-header-bar.spec.d.ts +1 -0
- package/lib/types/adw-icon.spec.d.ts +1 -0
- package/lib/types/adw-progress-bar.spec.d.ts +1 -0
- package/lib/types/adw-sidebar.spec.d.ts +1 -0
- package/lib/types/adw-switch.spec.d.ts +1 -0
- package/lib/types/adw-wrap-box.spec.d.ts +1 -0
- package/lib/types/bottom-sheet.spec.d.ts +1 -0
- package/lib/types/breakpoints.d.ts +22 -0
- package/lib/types/breakpoints.spec.d.ts +1 -0
- package/lib/types/chrome.spec.d.ts +1 -0
- package/lib/types/elements/adw-about-dialog.d.ts +47 -8
- package/lib/types/elements/adw-action-row.d.ts +35 -0
- package/lib/types/elements/adw-avatar.d.ts +11 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
- package/lib/types/elements/adw-button-content.d.ts +12 -0
- package/lib/types/elements/adw-button-row.d.ts +10 -1
- package/lib/types/elements/adw-carousel.d.ts +111 -8
- package/lib/types/elements/adw-checks.d.ts +57 -0
- package/lib/types/elements/adw-clamp.d.ts +4 -1
- package/lib/types/elements/adw-data-grid.d.ts +11 -31
- package/lib/types/elements/adw-drop-down.d.ts +17 -20
- package/lib/types/elements/adw-entry-row.d.ts +93 -3
- package/lib/types/elements/adw-expander-row.d.ts +7 -1
- package/lib/types/elements/adw-header-bar.d.ts +15 -0
- package/lib/types/elements/adw-icon.d.ts +25 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +37 -11
- package/lib/types/elements/adw-menu-button.d.ts +6 -6
- package/lib/types/elements/adw-navigation-split-view.d.ts +6 -0
- package/lib/types/elements/adw-navigation-view.d.ts +63 -20
- package/lib/types/elements/adw-overlay-split-view.d.ts +38 -0
- package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
- package/lib/types/elements/adw-popover.d.ts +58 -0
- package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
- package/lib/types/elements/adw-preferences-group.d.ts +31 -0
- package/lib/types/elements/adw-progress-bar.d.ts +38 -0
- package/lib/types/elements/adw-sidebar.d.ts +37 -5
- package/lib/types/elements/adw-split-button.d.ts +53 -7
- package/lib/types/elements/adw-switch-row.d.ts +11 -2
- package/lib/types/elements/adw-switch.d.ts +15 -0
- package/lib/types/elements/adw-tab-view.d.ts +129 -12
- package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
- package/lib/types/elements/adw-view-stack.d.ts +27 -19
- package/lib/types/elements/adw-view-switcher-bar.d.ts +23 -7
- package/lib/types/elements/adw-view-switcher.d.ts +22 -11
- package/lib/types/elements/adw-window-title.d.ts +3 -1
- package/lib/types/elements/view-switcher-dom.d.ts +44 -0
- package/lib/types/entry-rows.spec.d.ts +1 -0
- package/lib/types/index.d.ts +7 -0
- package/lib/types/navigation-view.spec.d.ts +1 -0
- package/lib/types/preferences-search.d.ts +20 -0
- package/lib/types/preferences.spec.d.ts +1 -0
- package/lib/types/split-button.spec.d.ts +1 -0
- package/lib/types/split-views.spec.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/tab-view.spec.d.ts +1 -0
- package/lib/types/view-stack.spec.d.ts +1 -0
- package/lib/types/view-switcher.spec.d.ts +1 -0
- package/package.json +6 -6
- package/scss/_banner.scss +13 -0
- package/scss/_bottom_sheet.scss +5 -6
- package/scss/_button.scss +3 -26
- package/scss/_button_content.scss +28 -0
- package/scss/_checks.scss +228 -0
- package/scss/_clamp.scss +9 -3
- package/scss/_data_grid.scss +15 -7
- package/scss/_drop_down.scss +12 -55
- package/scss/_expander_row.scss +5 -50
- package/scss/_icon.scss +51 -0
- package/scss/_icons.generated.scss +6 -0
- package/scss/_inline_view_switcher.scss +34 -0
- package/scss/_labels.scss +135 -0
- package/scss/_menu_button.scss +10 -70
- package/scss/_popover.scss +186 -0
- package/scss/_preferences.scss +29 -0
- package/scss/_preferences_dialog.scss +17 -0
- package/scss/_progress_bar.scss +161 -0
- package/scss/_reset.scss +18 -7
- package/scss/_spinner.scss +7 -3
- package/scss/_split_button.scss +6 -50
- package/scss/_switch.scss +90 -0
- package/scss/_switch_row.scss +6 -52
- package/scss/_tab_view.scss +64 -0
- package/scss/_theme.scss +10 -0
- package/scss/_toolbar_view.scss +74 -0
- package/scss/_variables.scss +34 -0
- package/scss/_view_switcher.scss +40 -0
- package/scss/_view_switcher_bar.scss +37 -0
- package/scss/_wrap_box.scss +56 -6
- package/scss/adwaita-skin.scss +23 -0
- package/src/adw-about-dialog.spec.ts +350 -0
- package/src/adw-action-rows.spec.ts +342 -0
- package/src/adw-avatar.spec.ts +139 -0
- package/src/adw-banner.spec.ts +152 -0
- package/src/adw-button-content.spec.ts +183 -0
- package/src/adw-button-row.spec.ts +12 -6
- package/src/adw-carousel.spec.ts +389 -0
- package/src/adw-checks.spec.ts +368 -0
- package/src/adw-drop-down.spec.ts +123 -0
- package/src/adw-header-bar.spec.ts +83 -0
- package/src/adw-icon.spec.ts +167 -0
- package/src/adw-progress-bar.spec.ts +173 -0
- package/src/adw-sidebar.spec.ts +332 -0
- package/src/adw-switch.spec.ts +242 -0
- package/src/adw-tab-view.spec.ts +12 -3
- package/src/adw-view-switcher-bar.spec.ts +61 -13
- package/src/adw-wrap-box.spec.ts +194 -0
- package/src/bottom-sheet.spec.ts +325 -0
- package/src/breakpoints.spec.ts +147 -0
- package/src/breakpoints.ts +67 -0
- package/src/chrome.spec.ts +301 -0
- package/src/elements/adw-about-dialog.ts +223 -163
- package/src/elements/adw-action-row.ts +120 -10
- package/src/elements/adw-avatar.ts +48 -54
- package/src/elements/adw-banner.ts +46 -10
- package/src/elements/adw-bottom-sheet.ts +145 -60
- package/src/elements/adw-button-content.ts +107 -33
- package/src/elements/adw-button-row.ts +61 -37
- package/src/elements/adw-button.ts +5 -7
- package/src/elements/adw-carousel.ts +467 -105
- package/src/elements/adw-checks.ts +299 -0
- package/src/elements/adw-clamp.ts +80 -10
- package/src/elements/adw-combo-row.ts +15 -7
- package/src/elements/adw-data-grid.ts +43 -91
- package/src/elements/adw-drop-down.ts +117 -128
- package/src/elements/adw-entry-row.ts +404 -36
- package/src/elements/adw-expander-row.ts +61 -29
- package/src/elements/adw-header-bar.ts +31 -6
- package/src/elements/adw-icon.ts +148 -0
- package/src/elements/adw-inline-view-switcher.ts +240 -129
- package/src/elements/adw-menu-button.ts +93 -70
- package/src/elements/adw-navigation-split-view.ts +48 -7
- package/src/elements/adw-navigation-view.ts +286 -148
- package/src/elements/adw-overlay-split-view.ts +164 -14
- package/src/elements/adw-password-entry-row.ts +111 -89
- package/src/elements/adw-popover.ts +283 -0
- package/src/elements/adw-preferences-dialog.ts +120 -21
- package/src/elements/adw-preferences-group.ts +128 -22
- package/src/elements/adw-progress-bar.ts +189 -0
- package/src/elements/adw-sidebar.ts +301 -142
- package/src/elements/adw-spin-row.ts +11 -5
- package/src/elements/adw-spinner.ts +23 -8
- package/src/elements/adw-split-button.ts +371 -92
- package/src/elements/adw-status-page.ts +9 -9
- package/src/elements/adw-switch-row.ts +100 -30
- package/src/elements/adw-switch.ts +129 -0
- package/src/elements/adw-tab-view.ts +682 -129
- package/src/elements/adw-toggle-group.ts +5 -7
- package/src/elements/adw-toolbar-view.ts +84 -21
- package/src/elements/adw-view-stack.ts +162 -85
- package/src/elements/adw-view-switcher-bar.ts +163 -81
- package/src/elements/adw-view-switcher.ts +231 -112
- package/src/elements/adw-window-title.ts +44 -11
- package/src/elements/adw-wrap-box.ts +131 -35
- package/src/elements/view-switcher-dom.ts +108 -0
- package/src/entry-rows.spec.ts +391 -0
- package/src/index.ts +23 -0
- package/src/navigation-view.spec.ts +435 -0
- package/src/preferences-search.ts +135 -0
- package/src/preferences.spec.ts +250 -0
- package/src/source-view/adw-source-view.ts +2 -5
- package/src/split-button.spec.ts +470 -0
- package/src/split-views.spec.ts +309 -0
- package/src/styles.generated.ts +1 -1
- package/src/tab-view.spec.ts +451 -0
- package/src/test.browser.mts +49 -0
- package/src/view-stack.spec.ts +272 -0
- package/src/view-switcher.spec.ts +446 -0
|
@@ -1,59 +1,156 @@
|
|
|
1
1
|
// <adw-tab-view> — A dynamic tabbed container with a tab bar header (the web
|
|
2
|
-
// counterpart of Adw.TabView + Adw.TabBar). One page is shown at a time; the
|
|
3
|
-
//
|
|
2
|
+
// counterpart of Adw.TabView + Adw.TabBar). One page is shown at a time; the tab
|
|
3
|
+
// bar renders one chip per page (title + close affordance) over a
|
|
4
4
|
// headerbar-colored strip, with the selected chip raised onto the view
|
|
5
|
-
// background.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// background.
|
|
6
|
+
//
|
|
7
|
+
// The MODEL is HEADLESS and lives in `@gjsify/adwaita-core` (ADR 0004) as
|
|
8
|
+
// {@link TabViewState}, shared with the NativeScript twin and specced by the
|
|
9
|
+
// conformance vectors in `@gjsify/adwaita-core/conformance`. This element is the
|
|
10
|
+
// DOM half only: it turns declared markup into pages, builds and MOVES tab
|
|
11
|
+
// chips, toggles visibility, reflects the selection to an attribute, re-emits
|
|
12
|
+
// changes as DOM events, and maps key chords onto the state machine.
|
|
13
|
+
//
|
|
14
|
+
// What the lift fixed here, all of it C-derived:
|
|
15
|
+
// - `close-page` removed NOTHING, so every close was permanently denied. The
|
|
16
|
+
// default now closes a non-pinned page and denies a pinned one, exactly as
|
|
17
|
+
// `close_page_cb` does (adw-tab-view.c:1986-1993), and the event is
|
|
18
|
+
// CANCELABLE so `preventDefault()` defers the close until
|
|
19
|
+
// `closePageFinish()` — the "save before closing?" seam that did not exist.
|
|
20
|
+
// - `selected="99"` CLAMPED to the last page and `selected="-1"` to the first;
|
|
21
|
+
// libadwaita ignores both (:2145, :4126-4134).
|
|
22
|
+
// - `view.selected = 2` changed the visible page and fired nothing, while a
|
|
23
|
+
// tab click fired; C notifies on every path (:1854).
|
|
24
|
+
// - `<adw-tab-page>` declared `observedAttributes = ['title']` with no
|
|
25
|
+
// `attributeChangedCallback` behind it, so the declaration was dead and a
|
|
26
|
+
// title could never change. Titles, tooltips, icons and the loading state
|
|
27
|
+
// are now live, as `AdwTab` keeps them (adw-tab.c:930-931).
|
|
28
|
+
// - the bar hid on `pages <= 1` with no pinned clause, so a single PINNED tab
|
|
29
|
+
// hid the bar where libadwaita keeps it (adw-tab-bar.c:163).
|
|
30
|
+
// - the close button was always visible; it now follows `tabCloseVisible`
|
|
31
|
+
// (adw-tab.c:124 + the pinned gate at :645-650).
|
|
32
|
+
// - a roving tabindex was installed with NOTHING wired to move it, so every
|
|
33
|
+
// inactive tab was keyboard-unreachable under role=tablist.
|
|
34
|
+
//
|
|
35
|
+
// Pages are declared as <adw-tab-page> children (the element itself becomes the
|
|
36
|
+
// page panel, so its attributes stay live) or added imperatively.
|
|
8
37
|
// Attributes:
|
|
9
|
-
// selected
|
|
10
|
-
// autohide
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// no-close
|
|
15
|
-
//
|
|
38
|
+
// selected — zero-based index of the visible page; out-of-range is IGNORED.
|
|
39
|
+
// autohide — hide the tab bar when it has nothing to show. NB Adw.TabBar
|
|
40
|
+
// defaults this to TRUE (`DEFAULT_TAB_AUTOHIDE`); an HTML boolean attribute
|
|
41
|
+
// cannot, so this stays opt-in — a deliberate, citable inversion.
|
|
42
|
+
// expand-tabs — tabs stretch to fill the bar evenly (Adw.TabBar `expand-tabs`).
|
|
43
|
+
// no-close — render no close affordance; web-specific escape hatch for
|
|
44
|
+
// static tab sets such as documentation command tabs.
|
|
45
|
+
// <adw-tab-page> attributes: page-id, title, tooltip, icon, indicator-icon,
|
|
46
|
+
// loading, needs-attention, pinned.
|
|
16
47
|
// Events:
|
|
17
|
-
// `notify::selected-page` (CustomEvent, bubbles,
|
|
18
|
-
//
|
|
19
|
-
// `close-page` (CustomEvent, bubbles,
|
|
20
|
-
//
|
|
48
|
+
// `notify::selected-page` (CustomEvent, bubbles,
|
|
49
|
+
// detail = { selected, selectedId, previousId, interactive }).
|
|
50
|
+
// `close-page` (CustomEvent, bubbles, CANCELABLE,
|
|
51
|
+
// detail = { index, id }) — one per close ATTEMPT. `preventDefault()` holds
|
|
52
|
+
// the page open until `closePageFinish(id, confirm)`.
|
|
53
|
+
// `page-attached` / `page-detached` / `page-reordered` / `page-pinned` /
|
|
54
|
+
// `page-updated` (CustomEvent, bubbles,
|
|
55
|
+
// detail = { id, position, previousPosition }) — one per page-list change,
|
|
56
|
+
// so a bound tab bar can move ONE chip instead of rebuilding.
|
|
21
57
|
// Reference: refs/libadwaita/src/adw-tab-view.c (AdwTabView behaviour)
|
|
22
58
|
// Reference: refs/libadwaita/src/adw-tab-bar.c (AdwTabBar autohide)
|
|
59
|
+
// Reference: refs/libadwaita/src/adw-tab.c (AdwTab chip)
|
|
23
60
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_tab-view.scss
|
|
24
61
|
// Reference: refs/adwaita-web/adwaita-web/scss/_tabs.scss
|
|
62
|
+
// Reference: packages/nativescript-bridge/adwaita/src/widgets/adw-tab-view.ts (NS twin)
|
|
25
63
|
// Copyright (c) 2020-2022 Purism SPC / GNOME contributors (libadwaita). LGPLv2.1+.
|
|
26
|
-
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web
|
|
64
|
+
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
65
|
+
// tab icon + indicator nodes are <adw-icon>.
|
|
27
66
|
|
|
28
|
-
|
|
67
|
+
import { TabViewState, tabCloseVisible, tabIconState, tabTooltip, tabsRevealed } from '@gjsify/adwaita-core';
|
|
68
|
+
import type { AdwTabPageSpec, AdwTabPageState, TabViewPagesChange, TabViewSelectionChange } from '@gjsify/adwaita-core';
|
|
69
|
+
|
|
70
|
+
import { type AdwIcon, createAdwIcon } from './adw-icon.js';
|
|
71
|
+
|
|
72
|
+
/** A page descriptor as `pages` exposes it. */
|
|
73
|
+
export type AdwTabViewPage = AdwTabPageState<HTMLElement>;
|
|
74
|
+
|
|
75
|
+
/** The page spec the imperative insertion methods take. */
|
|
76
|
+
export type AdwTabViewPageSpec = AdwTabPageSpec<HTMLElement>;
|
|
77
|
+
|
|
78
|
+
/** The live `<adw-tab-page>` properties, each mapped onto a state setter. */
|
|
79
|
+
const PAGE_ATTRIBUTES = ['title', 'tooltip', 'icon', 'indicator-icon', 'loading', 'needs-attention', 'pinned'];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A single page. Declared as a child of <adw-tab-view>; the element itself
|
|
83
|
+
* becomes the page panel, so its attributes keep driving the tab after connect —
|
|
84
|
+
* the old implementation moved the children out and discarded the element, which
|
|
85
|
+
* is why its `observedAttributes` declaration had nothing to act on.
|
|
86
|
+
*/
|
|
29
87
|
export class AdwTabPage extends HTMLElement {
|
|
30
88
|
static get observedAttributes() {
|
|
31
|
-
return
|
|
89
|
+
return PAGE_ATTRIBUTES;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null) {
|
|
93
|
+
// `closest` still finds the view after adoption: the element is moved
|
|
94
|
+
// into the view's page container, not replaced by a wrapper.
|
|
95
|
+
const view = this.closest('adw-tab-view') as AdwTabView | null;
|
|
96
|
+
view?.syncDeclaredPage(this, name, value);
|
|
32
97
|
}
|
|
33
98
|
}
|
|
34
99
|
|
|
35
100
|
export class AdwTabView extends HTMLElement {
|
|
36
|
-
private
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
private
|
|
101
|
+
private readonly _state = new TabViewState<HTMLElement>({
|
|
102
|
+
onClosePage: (page) => this._requestClose(page),
|
|
103
|
+
});
|
|
104
|
+
private readonly _barEl: HTMLDivElement;
|
|
105
|
+
private readonly _tabBoxEl: HTMLDivElement;
|
|
106
|
+
private readonly _pagesEl: HTMLDivElement;
|
|
107
|
+
/** Tab chips keyed by page id — the bar is edited in place, never rebuilt. */
|
|
108
|
+
private readonly _tabs = new Map<string, HTMLButtonElement>();
|
|
109
|
+
/** Page panels keyed by page id; kept so a DETACHED page's node is still reachable. */
|
|
110
|
+
private readonly _panels = new Map<string, HTMLElement>();
|
|
111
|
+
private readonly _hovered = new Set<string>();
|
|
41
112
|
private _initialized = false;
|
|
113
|
+
private _generatedIds = 0;
|
|
114
|
+
/** Guards the `selected` attribute reflection against re-entering the model. */
|
|
115
|
+
private _reflecting = false;
|
|
42
116
|
|
|
43
117
|
static get observedAttributes() {
|
|
44
118
|
return ['selected', 'autohide', 'expand-tabs', 'no-close'];
|
|
45
119
|
}
|
|
46
120
|
|
|
47
|
-
|
|
121
|
+
constructor() {
|
|
122
|
+
super();
|
|
123
|
+
// Detached nodes only: a custom element constructor may not add children.
|
|
124
|
+
// They are attached in connectedCallback, and pages added BEFORE that
|
|
125
|
+
// land in the detached container and come along with it.
|
|
126
|
+
this._barEl = document.createElement('div');
|
|
127
|
+
this._barEl.className = 'adw-tab-bar';
|
|
128
|
+
this._barEl.setAttribute('role', 'tablist');
|
|
129
|
+
this._tabBoxEl = document.createElement('div');
|
|
130
|
+
this._tabBoxEl.className = 'adw-tab-box';
|
|
131
|
+
this._barEl.appendChild(this._tabBoxEl);
|
|
132
|
+
this._pagesEl = document.createElement('div');
|
|
133
|
+
this._pagesEl.className = 'adw-tab-view-pages';
|
|
134
|
+
|
|
135
|
+
this._state.subscribePages((change) => this._onPagesChange(change));
|
|
136
|
+
this._state.subscribe((change) => this._onSelectionChange(change));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- Declarative surface ------------------------------------------------
|
|
140
|
+
|
|
141
|
+
/** Zero-based index of the visible page, `-1` when the view is empty. */
|
|
48
142
|
get selected(): number {
|
|
49
|
-
return this.
|
|
143
|
+
return this._state.selectedIndex;
|
|
50
144
|
}
|
|
51
145
|
|
|
52
146
|
set selected(value: number) {
|
|
53
|
-
this.
|
|
147
|
+
this._state.selectNthPage(value);
|
|
54
148
|
}
|
|
55
149
|
|
|
56
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Whether the tab bar hides itself when it has nothing to show —
|
|
152
|
+
* `tabsRevealed`, which keeps the bar up for a single PINNED tab.
|
|
153
|
+
*/
|
|
57
154
|
get autohide(): boolean {
|
|
58
155
|
return this.hasAttribute('autohide');
|
|
59
156
|
}
|
|
@@ -87,120 +184,576 @@ export class AdwTabView extends HTMLElement {
|
|
|
87
184
|
if (this._initialized) return;
|
|
88
185
|
this._initialized = true;
|
|
89
186
|
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
const
|
|
187
|
+
// Read the declared selection BEFORE adopting anything: the auto-select
|
|
188
|
+
// of the first page reflects its own index into this very attribute.
|
|
189
|
+
const declaredSelection = this.getAttribute('selected');
|
|
93
190
|
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
this._barEl.
|
|
98
|
-
this.
|
|
191
|
+
// Snapshot the declared pages, then take over the subtree. The elements
|
|
192
|
+
// themselves become the panels, so they must survive replaceChildren.
|
|
193
|
+
const declared = Array.from(this.querySelectorAll(':scope > adw-tab-page')) as AdwTabPage[];
|
|
194
|
+
this.replaceChildren(this._barEl, this._pagesEl);
|
|
195
|
+
for (const pageEl of declared) this._adoptDeclaredPage(pageEl);
|
|
99
196
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.
|
|
197
|
+
if (declaredSelection !== null) this._applySelectedAttribute(declaredSelection);
|
|
198
|
+
this._applyBarVisibility();
|
|
199
|
+
this._reflectSelected();
|
|
103
200
|
|
|
104
|
-
this.
|
|
105
|
-
|
|
201
|
+
this.addEventListener('keydown', (event) => this._onKeyDown(event));
|
|
202
|
+
}
|
|
106
203
|
|
|
107
|
-
|
|
108
|
-
this.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
tab.appendChild(label);
|
|
122
|
-
|
|
123
|
-
// Close affordance — a small flat button drawn with a CSS "×" glyph
|
|
124
|
-
// (no symbolic close icon ships in @gjsify/adwaita-icons).
|
|
125
|
-
const close = document.createElement('button');
|
|
126
|
-
close.type = 'button';
|
|
127
|
-
close.className = 'adw-tab-close';
|
|
128
|
-
close.setAttribute('aria-label', 'Close tab');
|
|
129
|
-
close.addEventListener('click', (event) => {
|
|
130
|
-
event.stopPropagation();
|
|
131
|
-
this.dispatchEvent(new CustomEvent('close-page', { bubbles: true, detail: { index } }));
|
|
132
|
-
});
|
|
133
|
-
tab.appendChild(close);
|
|
134
|
-
|
|
135
|
-
tab.addEventListener('click', () => this._selectIndex(index));
|
|
136
|
-
tabBox.appendChild(tab);
|
|
137
|
-
this._tabs.push(tab);
|
|
138
|
-
|
|
139
|
-
// The page content area — the declared element's children are moved
|
|
140
|
-
// into a panel that the view toggles visible.
|
|
141
|
-
const panel = document.createElement('div');
|
|
142
|
-
panel.className = 'adw-tab-page';
|
|
143
|
-
panel.setAttribute('role', 'tabpanel');
|
|
144
|
-
for (const child of Array.from(page.childNodes)) panel.appendChild(child);
|
|
145
|
-
this._pagesEl.appendChild(panel);
|
|
146
|
-
this._pages.push(panel);
|
|
147
|
-
});
|
|
204
|
+
attributeChangedCallback(name: string, _old: string | null, _value: string | null) {
|
|
205
|
+
if (!this._initialized) return;
|
|
206
|
+
if (name === 'selected') {
|
|
207
|
+
// The LIVE attribute, not the value captured when the reaction was
|
|
208
|
+
// queued: reflection writes this attribute, and a custom-element
|
|
209
|
+
// reaction can run after a later write has already superseded it.
|
|
210
|
+
if (this._reflecting) return;
|
|
211
|
+
const current = this.getAttribute('selected');
|
|
212
|
+
if (current !== null) this._applySelectedAttribute(current);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// autohide gates the bar; expand-tabs and no-close are styling only.
|
|
216
|
+
this._applyBarVisibility();
|
|
217
|
+
}
|
|
148
218
|
|
|
149
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Push a live `<adw-tab-page>` attribute change into the model. Called by
|
|
221
|
+
* {@link AdwTabPage.attributeChangedCallback}; not part of the public API.
|
|
222
|
+
*/
|
|
223
|
+
syncDeclaredPage(pageEl: AdwTabPage, name: string, value: string | null): void {
|
|
224
|
+
const id = pageEl.dataset.pageId;
|
|
225
|
+
if (id === undefined) return;
|
|
226
|
+
switch (name) {
|
|
227
|
+
case 'title':
|
|
228
|
+
this._state.setPageTitle(id, value);
|
|
229
|
+
return;
|
|
230
|
+
case 'tooltip':
|
|
231
|
+
this._state.setPageTooltip(id, value);
|
|
232
|
+
return;
|
|
233
|
+
case 'icon':
|
|
234
|
+
this._state.setPageIcon(id, value);
|
|
235
|
+
return;
|
|
236
|
+
case 'indicator-icon':
|
|
237
|
+
this._state.setPageIndicatorIcon(id, value);
|
|
238
|
+
return;
|
|
239
|
+
case 'loading':
|
|
240
|
+
this._state.setPageLoading(id, value !== null);
|
|
241
|
+
return;
|
|
242
|
+
case 'needs-attention':
|
|
243
|
+
this._state.setPageNeedsAttention(id, value !== null);
|
|
244
|
+
return;
|
|
245
|
+
case 'pinned':
|
|
246
|
+
this._state.setPagePinned(id, value !== null);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// --- Model surface (thin delegations to TabViewState) --------------------
|
|
150
252
|
|
|
151
|
-
|
|
152
|
-
|
|
253
|
+
/** All pages in order. */
|
|
254
|
+
get pages(): readonly AdwTabViewPage[] {
|
|
255
|
+
return this._state.pages;
|
|
153
256
|
}
|
|
154
257
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
258
|
+
/** Number of pages. */
|
|
259
|
+
get nPages(): number {
|
|
260
|
+
return this._state.nPages;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Length of the pinned prefix. */
|
|
264
|
+
get nPinnedPages(): number {
|
|
265
|
+
return this._state.nPinnedPages;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Id of the selected page, `null` when the view is empty. */
|
|
269
|
+
get selectedId(): string | null {
|
|
270
|
+
return this._state.selectedId;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Index of the selected page, `-1` when the view is empty. */
|
|
274
|
+
get selectedIndex(): number {
|
|
275
|
+
return this._state.selectedIndex;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The selected page's panel element, or `null`. */
|
|
279
|
+
get selectedContent(): HTMLElement | null {
|
|
280
|
+
return this._state.selectedPage?.content ?? null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
isClosing(id: string): boolean {
|
|
284
|
+
return this._state.isClosing(id);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
setSelectedPage(id: string | null): boolean {
|
|
288
|
+
return this._state.setSelectedPage(id);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
selectNthPage(n: number): boolean {
|
|
292
|
+
return this._state.selectNthPage(n);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
selectPreviousPage(): boolean {
|
|
296
|
+
return this._state.selectPreviousPage();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
selectNextPage(): boolean {
|
|
300
|
+
return this._state.selectNextPage();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
selectFirstPage(): boolean {
|
|
304
|
+
return this._state.selectFirstPage();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
selectLastPage(): boolean {
|
|
308
|
+
return this._state.selectLastPage();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Ctrl+Tab — the next page, wrapping to the first. */
|
|
312
|
+
cycleNextPage(): boolean {
|
|
313
|
+
return this._state.cycleNextPage();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Ctrl+Shift+Tab — the previous page, wrapping to the last. */
|
|
317
|
+
cyclePreviousPage(): boolean {
|
|
318
|
+
return this._state.cyclePreviousPage();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Add a page opened FROM `parentId`, deriving its position (Adw.TabView.add_page). */
|
|
322
|
+
addPage(spec: AdwTabViewPageSpec, parentId: string | null = null): number {
|
|
323
|
+
return this._state.addPage(this._withPanel(spec), parentId);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
insertPage(spec: AdwTabViewPageSpec, position: number): number {
|
|
327
|
+
return this._state.insertPage(this._withPanel(spec), position);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
prependPage(spec: AdwTabViewPageSpec): number {
|
|
331
|
+
return this._state.prependPage(this._withPanel(spec));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
appendPage(spec: AdwTabViewPageSpec): number {
|
|
335
|
+
return this._state.appendPage(this._withPanel(spec));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
insertPinnedPage(spec: AdwTabViewPageSpec, position: number): number {
|
|
339
|
+
return this._state.insertPinnedPage(this._withPanel(spec), position);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
prependPinnedPage(spec: AdwTabViewPageSpec): number {
|
|
343
|
+
return this._state.prependPinnedPage(this._withPanel(spec));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
appendPinnedPage(spec: AdwTabViewPageSpec): number {
|
|
347
|
+
return this._state.appendPinnedPage(this._withPanel(spec));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Pin or unpin a page, re-ordering it in the same step. Returns its new position. */
|
|
351
|
+
setPagePinned(id: string, pinned: boolean): number {
|
|
352
|
+
return this._state.setPagePinned(id, pinned);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Request a close. Dispatches a CANCELABLE `close-page`; calling
|
|
357
|
+
* `preventDefault()` on it holds the page open until {@link closePageFinish}.
|
|
358
|
+
*/
|
|
359
|
+
closePage(id: string): boolean {
|
|
360
|
+
return this._state.closePage(id);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
closePageFinish(id: string, confirm: boolean): boolean {
|
|
364
|
+
return this._state.closePageFinish(id, confirm);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
closeOtherPages(id: string): void {
|
|
368
|
+
this._state.closeOtherPages(id);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
closePagesBefore(id: string): void {
|
|
372
|
+
this._state.closePagesBefore(id);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
closePagesAfter(id: string): void {
|
|
376
|
+
this._state.closePagesAfter(id);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Remove a page unconditionally, running the successor rule first. */
|
|
380
|
+
detachPage(id: string): AdwTabViewPage | null {
|
|
381
|
+
return this._state.detachPage(id);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
reorderPage(id: string, position: number): boolean {
|
|
385
|
+
return this._state.reorderPage(id, position);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
reorderBackward(id: string): boolean {
|
|
389
|
+
return this._state.reorderBackward(id);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
reorderForward(id: string): boolean {
|
|
393
|
+
return this._state.reorderForward(id);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
reorderFirst(id: string): boolean {
|
|
397
|
+
return this._state.reorderFirst(id);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
reorderLast(id: string): boolean {
|
|
401
|
+
return this._state.reorderLast(id);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
setPageTitle(id: string, title: string | null): boolean {
|
|
405
|
+
return this._state.setPageTitle(id, title);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
setPageTooltip(id: string, tooltip: string | null): boolean {
|
|
409
|
+
return this._state.setPageTooltip(id, tooltip);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
setPageIcon(id: string, icon: string | null): boolean {
|
|
413
|
+
return this._state.setPageIcon(id, icon);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
setPageLoading(id: string, loading: boolean): boolean {
|
|
417
|
+
return this._state.setPageLoading(id, loading);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
setPageNeedsAttention(id: string, needsAttention: boolean): boolean {
|
|
421
|
+
return this._state.setPageNeedsAttention(id, needsAttention);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Every precondition the model refused, as C would have warned about it. */
|
|
425
|
+
get diagnostics(): readonly string[] {
|
|
426
|
+
return this._state.diagnostics;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// --- Adoption -----------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
private _adoptDeclaredPage(pageEl: AdwTabPage): void {
|
|
432
|
+
// The declared element IS the panel — attributes stay live on it, which
|
|
433
|
+
// is what makes `<adw-tab-page title>` observable at all.
|
|
434
|
+
const spec: AdwTabViewPageSpec = {
|
|
435
|
+
id: pageEl.getAttribute('page-id') ?? this._nextId(),
|
|
436
|
+
title: pageEl.getAttribute('title'),
|
|
437
|
+
tooltip: pageEl.getAttribute('tooltip'),
|
|
438
|
+
icon: pageEl.getAttribute('icon'),
|
|
439
|
+
indicatorIcon: pageEl.getAttribute('indicator-icon'),
|
|
440
|
+
loading: pageEl.hasAttribute('loading'),
|
|
441
|
+
needsAttention: pageEl.hasAttribute('needs-attention'),
|
|
442
|
+
content: pageEl,
|
|
443
|
+
};
|
|
444
|
+
if (pageEl.hasAttribute('pinned')) this.appendPinnedPage(spec);
|
|
445
|
+
else this.appendPage(spec);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Give a spec its panel, so `append({ id })` with no content is legal. */
|
|
449
|
+
private _withPanel(spec: AdwTabViewPageSpec): AdwTabViewPageSpec {
|
|
450
|
+
const content = spec.content ?? document.createElement('adw-tab-page');
|
|
451
|
+
content.classList.add('adw-tab-page');
|
|
452
|
+
content.setAttribute('role', 'tabpanel');
|
|
453
|
+
content.dataset.pageId = spec.id;
|
|
454
|
+
return { ...spec, content };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private _nextId(): string {
|
|
458
|
+
// Monotonic and never reused: an id is the stand-in for a page POINTER,
|
|
459
|
+
// so recycling one would silently alias a closed tab to a new one.
|
|
460
|
+
this._generatedIds += 1;
|
|
461
|
+
return `tab-${this._generatedIds}`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// --- Rendering ----------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
private _onPagesChange(change: TabViewPagesChange): void {
|
|
467
|
+
switch (change.kind) {
|
|
468
|
+
case 'attached': {
|
|
469
|
+
const page = this._state.getPage(change.id);
|
|
470
|
+
if (page) this._insertTab(page, change.position);
|
|
471
|
+
break;
|
|
162
472
|
}
|
|
163
|
-
|
|
473
|
+
case 'detached': {
|
|
474
|
+
this._tabs.get(change.id)?.remove();
|
|
475
|
+
this._tabs.delete(change.id);
|
|
476
|
+
this._hovered.delete(change.id);
|
|
477
|
+
// The page is already out of the model, so its node has to come
|
|
478
|
+
// from here rather than from `getPage(id)?.content`.
|
|
479
|
+
this._panels.get(change.id)?.remove();
|
|
480
|
+
this._panels.delete(change.id);
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
case 'reordered':
|
|
484
|
+
case 'pinned': {
|
|
485
|
+
this._moveTab(change.id, change.position);
|
|
486
|
+
this._refreshTab(change.id);
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
case 'updated':
|
|
490
|
+
this._refreshTab(change.id);
|
|
491
|
+
break;
|
|
164
492
|
}
|
|
165
|
-
|
|
166
|
-
this.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
493
|
+
this._applyBarVisibility();
|
|
494
|
+
this._applyActiveState();
|
|
495
|
+
this.dispatchEvent(
|
|
496
|
+
new CustomEvent(`page-${change.kind}`, {
|
|
497
|
+
bubbles: true,
|
|
498
|
+
detail: { id: change.id, position: change.position, previousPosition: change.previousPosition },
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private _insertTab(page: AdwTabViewPage, position: number): void {
|
|
504
|
+
const tab = document.createElement('button');
|
|
505
|
+
tab.type = 'button';
|
|
506
|
+
tab.className = 'adw-tab';
|
|
507
|
+
tab.setAttribute('role', 'tab');
|
|
508
|
+
tab.dataset.pageId = page.id;
|
|
509
|
+
|
|
510
|
+
// Decorative mask-image nodes — <adw-icon> carries the convention (and
|
|
511
|
+
// the `-symbolic` strip, which is that convention and not C: the name
|
|
512
|
+
// reaches `GtkImage` untouched there).
|
|
513
|
+
const icon = createAdwIcon(null, 'adw-tab-icon');
|
|
514
|
+
tab.appendChild(icon);
|
|
515
|
+
|
|
516
|
+
const label = document.createElement('span');
|
|
517
|
+
label.className = 'adw-tab-title';
|
|
518
|
+
tab.appendChild(label);
|
|
519
|
+
|
|
520
|
+
tab.appendChild(createAdwIcon(null, 'adw-tab-indicator'));
|
|
521
|
+
|
|
522
|
+
// Close affordance — a small flat button drawn with a CSS "×" glyph (no
|
|
523
|
+
// symbolic close icon ships in @gjsify/adwaita-icons). `can-focus=False`
|
|
524
|
+
// in adw-tab.ui:72, so it stays out of the tab order here too.
|
|
525
|
+
const close = document.createElement('button');
|
|
526
|
+
close.type = 'button';
|
|
527
|
+
close.className = 'adw-tab-close';
|
|
528
|
+
close.tabIndex = -1;
|
|
529
|
+
close.setAttribute('aria-label', 'Close tab');
|
|
530
|
+
close.addEventListener('click', (event) => {
|
|
531
|
+
event.stopPropagation();
|
|
532
|
+
this._state.closePage(page.id);
|
|
533
|
+
});
|
|
534
|
+
tab.appendChild(close);
|
|
535
|
+
|
|
536
|
+
tab.addEventListener('click', () => this._state.setSelectedPage(page.id));
|
|
537
|
+
tab.addEventListener('pointerenter', () => {
|
|
538
|
+
this._hovered.add(page.id);
|
|
539
|
+
this._refreshCloseVisibility(page.id);
|
|
540
|
+
});
|
|
541
|
+
tab.addEventListener('pointerleave', () => {
|
|
542
|
+
this._hovered.delete(page.id);
|
|
543
|
+
this._refreshCloseVisibility(page.id);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
this._tabBoxEl.insertBefore(tab, this._tabBoxEl.children[position] ?? null);
|
|
547
|
+
this._tabs.set(page.id, tab);
|
|
548
|
+
|
|
549
|
+
const panel = page.content;
|
|
550
|
+
if (panel) {
|
|
551
|
+
this._panels.set(page.id, panel);
|
|
552
|
+
this._pagesEl.insertBefore(panel, this._pagesEl.children[position] ?? null);
|
|
553
|
+
}
|
|
554
|
+
this._refreshTab(page.id);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Remove-then-insert, the same shape the model's own reorder has. */
|
|
558
|
+
private _moveTab(id: string, position: number): void {
|
|
559
|
+
const tab = this._tabs.get(id);
|
|
560
|
+
if (tab) {
|
|
561
|
+
tab.remove();
|
|
562
|
+
this._tabBoxEl.insertBefore(tab, this._tabBoxEl.children[position] ?? null);
|
|
563
|
+
}
|
|
564
|
+
const panel = this._panels.get(id);
|
|
565
|
+
if (panel) {
|
|
566
|
+
panel.remove();
|
|
567
|
+
this._pagesEl.insertBefore(panel, this._pagesEl.children[position] ?? null);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private _refreshTab(id: string): void {
|
|
572
|
+
const page = this._state.getPage(id);
|
|
573
|
+
const tab = this._tabs.get(id);
|
|
574
|
+
if (!page || !tab) return;
|
|
575
|
+
|
|
576
|
+
const label = tab.querySelector('.adw-tab-title') as HTMLElement | null;
|
|
577
|
+
if (label) label.textContent = page.title;
|
|
578
|
+
// The tooltip is Pango MARKUP when the page sets one of its own
|
|
579
|
+
// (adw-tab.c:141-146). The DOM `title` attribute is a TEXT sink, so the
|
|
580
|
+
// markup is shown verbatim rather than being pushed through an HTML sink
|
|
581
|
+
// — a renderer that interpreted it would be executing page-supplied markup.
|
|
582
|
+
tab.title = tabTooltip(page);
|
|
583
|
+
tab.classList.toggle('pinned', page.pinned);
|
|
584
|
+
tab.classList.toggle('needs-attention', page.needsAttention);
|
|
585
|
+
tab.classList.toggle('closing', page.closing);
|
|
586
|
+
|
|
587
|
+
const icons = tabIconState(page, this.getAttribute('default-icon'));
|
|
588
|
+
const icon = tab.querySelector('.adw-tab-icon') as AdwIcon | null;
|
|
589
|
+
if (icon) {
|
|
590
|
+
icon.iconName = icons.icon;
|
|
591
|
+
icon.classList.toggle('loading', icons.spinner);
|
|
592
|
+
icon.hidden = !icons.iconVisible;
|
|
593
|
+
}
|
|
594
|
+
const indicator = tab.querySelector('.adw-tab-indicator') as AdwIcon | null;
|
|
595
|
+
if (indicator) {
|
|
596
|
+
indicator.iconName = page.indicatorIcon;
|
|
597
|
+
indicator.hidden = !icons.indicatorVisible;
|
|
598
|
+
}
|
|
599
|
+
this._refreshCloseVisibility(id);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* `tabCloseVisible` (adw-tab.c:124 + the pinned gate at :645-650). `dragging`
|
|
604
|
+
* is constantly false — tab drag-and-drop is compositor work and is not
|
|
605
|
+
* modelled — and `fullyVisible` is measured against the bar's scroll window,
|
|
606
|
+
* which is exactly what the C term means for a horizontally scrolled bar.
|
|
607
|
+
*/
|
|
608
|
+
private _refreshCloseVisibility(id: string): void {
|
|
609
|
+
const page = this._state.getPage(id);
|
|
610
|
+
const tab = this._tabs.get(id);
|
|
611
|
+
if (!page || !tab) return;
|
|
612
|
+
const close = tab.querySelector('.adw-tab-close') as HTMLElement | null;
|
|
613
|
+
if (!close) return;
|
|
614
|
+
|
|
615
|
+
const barLeft = this._barEl.scrollLeft;
|
|
616
|
+
const fullyVisible =
|
|
617
|
+
tab.offsetLeft >= barLeft && tab.offsetLeft + tab.offsetWidth <= barLeft + this._barEl.clientWidth;
|
|
618
|
+
close.hidden = !tabCloseVisible({
|
|
619
|
+
hovering: this._hovered.has(id),
|
|
620
|
+
fullyVisible,
|
|
621
|
+
selected: this._state.selectedId === id,
|
|
622
|
+
dragging: false,
|
|
623
|
+
pinned: page.pinned,
|
|
197
624
|
});
|
|
625
|
+
}
|
|
198
626
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
627
|
+
private _onSelectionChange(change: TabViewSelectionChange): void {
|
|
628
|
+
this._applyActiveState();
|
|
629
|
+
this._reflectSelected();
|
|
630
|
+
this.dispatchEvent(
|
|
631
|
+
new CustomEvent('notify::selected-page', {
|
|
632
|
+
bubbles: true,
|
|
633
|
+
detail: {
|
|
634
|
+
selected: change.selectedIndex,
|
|
635
|
+
selectedId: change.selectedId,
|
|
636
|
+
previousId: change.previousId,
|
|
637
|
+
interactive: change.interactive,
|
|
638
|
+
},
|
|
639
|
+
}),
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private _applyActiveState(): void {
|
|
644
|
+
const selected = this._state.selectedId;
|
|
645
|
+
for (const page of this._state.pages) {
|
|
646
|
+
const isActive = page.id === selected;
|
|
647
|
+
const tab = this._tabs.get(page.id);
|
|
648
|
+
if (tab) {
|
|
649
|
+
tab.classList.toggle('active', isActive);
|
|
650
|
+
tab.setAttribute('aria-selected', String(isActive));
|
|
651
|
+
tab.tabIndex = isActive ? 0 : -1;
|
|
652
|
+
}
|
|
653
|
+
const panel = page.content;
|
|
654
|
+
if (panel) {
|
|
655
|
+
panel.classList.toggle('active-page', isActive);
|
|
656
|
+
panel.hidden = !isActive;
|
|
657
|
+
}
|
|
658
|
+
this._refreshCloseVisibility(page.id);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
private _applyBarVisibility(): void {
|
|
663
|
+
const revealed = tabsRevealed({
|
|
664
|
+
autohide: this.autohide,
|
|
665
|
+
nPages: this._state.nPages,
|
|
666
|
+
nPinnedPages: this._state.nPinnedPages,
|
|
667
|
+
// Tab transfer between views is drag-and-drop, i.e. compositor work,
|
|
668
|
+
// and is not modelled here.
|
|
669
|
+
isTransferringPage: false,
|
|
203
670
|
});
|
|
671
|
+
this._barEl.hidden = !revealed;
|
|
672
|
+
this._barEl.classList.toggle('single-tab', this._state.nPages <= 1);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private _reflectSelected(): void {
|
|
676
|
+
this._reflecting = true;
|
|
677
|
+
const index = this._state.selectedIndex;
|
|
678
|
+
if (index < 0) this.removeAttribute('selected');
|
|
679
|
+
else if (this.getAttribute('selected') !== String(index)) this.setAttribute('selected', String(index));
|
|
680
|
+
this._reflecting = false;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private _applySelectedAttribute(value: string): void {
|
|
684
|
+
const index = Number.parseInt(value, 10);
|
|
685
|
+
// An unparseable value is IGNORED rather than treated as 0: the old
|
|
686
|
+
// `Number.isNaN(raw) ? 0 : clamp(raw)` made `selected="oops"` select the
|
|
687
|
+
// first page, and `selected="99"` the last, where libadwaita refuses both
|
|
688
|
+
// (adw-tab-view.c:2145, :4126-4134).
|
|
689
|
+
if (Number.isNaN(index)) return;
|
|
690
|
+
this._state.selectNthPage(index);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// --- Keyboard -----------------------------------------------------------
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* The `Adw.TabView` shortcut table (`init_shortcuts`, adw-tab-view.c:2192+),
|
|
697
|
+
* plus the ArrowLeft/ArrowRight movement `role=tablist` promises.
|
|
698
|
+
*
|
|
699
|
+
* Ctrl+Tab WRAPS and Ctrl+Page-Up/Down does not — the same `last` flag that
|
|
700
|
+
* separates them in `select_page_cb` (:2017-2041). Alt+0 is page index 9,
|
|
701
|
+
* because pages count from 0 (:2149-2150).
|
|
702
|
+
*/
|
|
703
|
+
private _onKeyDown(event: KeyboardEvent): void {
|
|
704
|
+
const inBar = event.target instanceof Node && this._barEl.contains(event.target);
|
|
705
|
+
let handled = false;
|
|
706
|
+
|
|
707
|
+
if (event.ctrlKey && event.key === 'Tab') {
|
|
708
|
+
handled = event.shiftKey ? this._state.cyclePreviousPage() : this._state.cycleNextPage();
|
|
709
|
+
} else if (event.ctrlKey && event.shiftKey && (event.key === 'PageUp' || event.key === 'PageDown')) {
|
|
710
|
+
const id = this._state.selectedId;
|
|
711
|
+
if (id !== null) {
|
|
712
|
+
handled = event.key === 'PageUp' ? this._state.reorderBackward(id) : this._state.reorderForward(id);
|
|
713
|
+
}
|
|
714
|
+
} else if (event.ctrlKey && event.shiftKey && (event.key === 'Home' || event.key === 'End')) {
|
|
715
|
+
const id = this._state.selectedId;
|
|
716
|
+
if (id !== null) {
|
|
717
|
+
handled = event.key === 'Home' ? this._state.reorderFirst(id) : this._state.reorderLast(id);
|
|
718
|
+
}
|
|
719
|
+
} else if (event.ctrlKey && (event.key === 'PageUp' || event.key === 'PageDown')) {
|
|
720
|
+
handled = event.key === 'PageUp' ? this._state.selectPreviousPage() : this._state.selectNextPage();
|
|
721
|
+
} else if (event.ctrlKey && (event.key === 'Home' || event.key === 'End')) {
|
|
722
|
+
handled = event.key === 'Home' ? this._state.selectFirstPage() : this._state.selectLastPage();
|
|
723
|
+
} else if (event.altKey && /^[0-9]$/.test(event.key)) {
|
|
724
|
+
const digit = Number.parseInt(event.key, 10);
|
|
725
|
+
handled = this._state.selectNthPage(digit === 0 ? 9 : digit - 1);
|
|
726
|
+
} else if (inBar && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {
|
|
727
|
+
handled = event.key === 'ArrowLeft' ? this._state.selectPreviousPage() : this._state.selectNextPage();
|
|
728
|
+
} else if (inBar && (event.key === 'Home' || event.key === 'End')) {
|
|
729
|
+
handled = event.key === 'Home' ? this._state.selectFirstPage() : this._state.selectLastPage();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (!handled) return;
|
|
733
|
+
event.preventDefault();
|
|
734
|
+
// Roving tabindex: the newly-active tab is the only focusable one, so
|
|
735
|
+
// focus has to travel with it or the next keypress goes nowhere.
|
|
736
|
+
if (inBar) {
|
|
737
|
+
const id = this._state.selectedId;
|
|
738
|
+
if (id !== null) this._tabs.get(id)?.focus();
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* The `close-page` seam. A CANCELABLE DOM event is the idiomatic spelling of
|
|
744
|
+
* the C signal's return value: letting it through takes libadwaita's default
|
|
745
|
+
* (`!pinned`, adw-tab-view.c:1990-1991), and `preventDefault()` defers the
|
|
746
|
+
* close until the app calls {@link closePageFinish}.
|
|
747
|
+
*/
|
|
748
|
+
private _requestClose(page: AdwTabViewPage): boolean | 'defer' {
|
|
749
|
+
const proceed = this.dispatchEvent(
|
|
750
|
+
new CustomEvent('close-page', {
|
|
751
|
+
bubbles: true,
|
|
752
|
+
cancelable: true,
|
|
753
|
+
detail: { index: this._state.getPagePosition(page.id), id: page.id },
|
|
754
|
+
}),
|
|
755
|
+
);
|
|
756
|
+
return proceed ? !page.pinned : 'defer';
|
|
204
757
|
}
|
|
205
758
|
}
|
|
206
759
|
|