@gjsify/adwaita-web 0.17.0 → 0.19.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 +44 -20
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-tab-view.spec.d.ts +1 -0
- package/lib/types/elements/adw-tab-view.d.ts +6 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +5 -5
- package/scss/_tab_view.scss +62 -29
- package/src/adw-tab-view.spec.ts +108 -0
- package/src/elements/adw-tab-view.ts +25 -1
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/adwaita-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Adwaita/Libadwaita web components for browser targets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"@codemirror/language": "^6.12.4",
|
|
42
42
|
"@codemirror/state": "^6.7.0",
|
|
43
43
|
"@codemirror/view": "^6.43.4",
|
|
44
|
-
"@gjsify/adwaita-fonts": "^0.
|
|
45
|
-
"@gjsify/adwaita-icons": "^0.
|
|
44
|
+
"@gjsify/adwaita-fonts": "^0.19.0",
|
|
45
|
+
"@gjsify/adwaita-icons": "^0.19.0",
|
|
46
46
|
"@lezer/highlight": "^1.2.3"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@gjsify/cli": "^0.
|
|
50
|
-
"@gjsify/unit": "^0.
|
|
49
|
+
"@gjsify/cli": "^0.19.0",
|
|
50
|
+
"@gjsify/unit": "^0.19.0",
|
|
51
51
|
"sass": "^1.101.0",
|
|
52
52
|
"typescript": "^6.0.3"
|
|
53
53
|
},
|
package/scss/_tab_view.scss
CHANGED
|
@@ -17,18 +17,22 @@ adw-tab-view {
|
|
|
17
17
|
color: var(--window-fg-color);
|
|
18
18
|
|
|
19
19
|
// --- Tab bar (the AdwTabBar strip) ---
|
|
20
|
+
// Faithful port of libadwaita's `tabbar` (refs/libadwaita _tab-view.scss +
|
|
21
|
+
// _colors.scss): a headerbar-colored box with an inset bottom shade; tabs
|
|
22
|
+
// FILL their slot as rounded rects and use the currentColor alpha fills —
|
|
23
|
+
// selected 10% (hover 13%, active 19%), plain hover 7%, active 16%.
|
|
20
24
|
.adw-tab-bar {
|
|
21
25
|
display: flex;
|
|
22
|
-
align-items:
|
|
26
|
+
align-items: stretch;
|
|
23
27
|
flex-shrink: 0;
|
|
24
28
|
box-sizing: border-box;
|
|
25
|
-
padding: 0
|
|
29
|
+
padding: 0 1px 1px;
|
|
26
30
|
background-color: var(--headerbar-bg-color);
|
|
27
31
|
color: var(--headerbar-fg-color);
|
|
28
32
|
// libadwaita's inset bottom shade under the bar.
|
|
29
33
|
box-shadow: inset 0 -1px var(--headerbar-shade-color);
|
|
30
34
|
overflow-x: auto;
|
|
31
|
-
scrollbar-width:
|
|
35
|
+
scrollbar-width: none;
|
|
32
36
|
|
|
33
37
|
&[hidden] {
|
|
34
38
|
display: none;
|
|
@@ -38,65 +42,84 @@ adw-tab-view {
|
|
|
38
42
|
.adw-tab-box {
|
|
39
43
|
display: flex;
|
|
40
44
|
align-items: stretch;
|
|
41
|
-
|
|
42
|
-
padding:
|
|
43
|
-
min-height: 34px;
|
|
45
|
+
flex: 1 1 auto;
|
|
46
|
+
padding: 4px 0;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
// --- A single tab
|
|
49
|
+
// --- A single tab ---
|
|
47
50
|
.adw-tab {
|
|
51
|
+
position: relative;
|
|
48
52
|
box-sizing: border-box;
|
|
49
53
|
display: inline-flex;
|
|
50
54
|
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
51
56
|
gap: var(--spacing-xs);
|
|
52
57
|
max-width: 200px;
|
|
53
|
-
min-height:
|
|
58
|
+
min-height: 30px;
|
|
54
59
|
margin: 0;
|
|
55
|
-
padding:
|
|
60
|
+
padding: 0 var(--spacing-m);
|
|
56
61
|
border: none;
|
|
57
62
|
border-radius: var(--button-radius);
|
|
58
63
|
background-color: transparent;
|
|
59
|
-
color:
|
|
64
|
+
color: inherit;
|
|
60
65
|
font-family: var(--font-family);
|
|
61
66
|
font-size: var(--font-size-base);
|
|
62
|
-
font-weight: 700;
|
|
63
67
|
line-height: 1;
|
|
64
68
|
cursor: pointer;
|
|
65
69
|
user-select: none;
|
|
66
70
|
-webkit-user-select: none;
|
|
67
|
-
transition:
|
|
68
|
-
background-color 150ms ease-in-out,
|
|
69
|
-
color 150ms ease-in-out,
|
|
70
|
-
box-shadow 150ms ease-in-out;
|
|
71
|
+
transition: background-color 150ms ease-in-out;
|
|
71
72
|
|
|
72
73
|
&:hover {
|
|
73
|
-
background-color:
|
|
74
|
+
background-color: color-mix(in srgb, currentColor 7%, transparent);
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
&:active {
|
|
77
|
-
background-color:
|
|
78
|
+
background-color: color-mix(in srgb, currentColor 16%, transparent);
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
&:focus-visible {
|
|
81
82
|
outline: 2px solid var(--accent-color);
|
|
82
|
-
outline-offset: -
|
|
83
|
+
outline-offset: -2px;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
// The selected tab —
|
|
86
|
-
//
|
|
86
|
+
// The selected tab — libadwaita's currentColor alpha fill covering the
|
|
87
|
+
// whole tab slot (NOT a raised chip on the view background).
|
|
87
88
|
&.active {
|
|
88
|
-
background-color:
|
|
89
|
-
color: var(--view-fg-color);
|
|
90
|
-
box-shadow:
|
|
91
|
-
0 1px 3px 1px rgba(0, 0, 6, 0.07),
|
|
92
|
-
0 2px 6px 2px rgba(0, 0, 6, 0.03);
|
|
89
|
+
background-color: color-mix(in srgb, currentColor 10%, transparent);
|
|
93
90
|
|
|
94
91
|
&:hover {
|
|
95
|
-
background-color:
|
|
92
|
+
background-color: color-mix(in srgb, currentColor 13%, transparent);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
&:active {
|
|
96
|
+
background-color: color-mix(in srgb, currentColor 19%, transparent);
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
// Thin separators between tabs, hidden next to the selected or hovered tab
|
|
102
|
+
// (libadwaita tabbox > separator + its .hidden state).
|
|
103
|
+
.adw-tab + .adw-tab::before {
|
|
104
|
+
content: '';
|
|
105
|
+
position: absolute;
|
|
106
|
+
left: 0;
|
|
107
|
+
top: 3px;
|
|
108
|
+
bottom: 3px;
|
|
109
|
+
width: 1px;
|
|
110
|
+
background-color: currentColor;
|
|
111
|
+
opacity: 0.2;
|
|
112
|
+
transition: opacity 150ms ease-in-out;
|
|
113
|
+
pointer-events: none;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.adw-tab.active + .adw-tab::before,
|
|
117
|
+
.adw-tab + .adw-tab.active::before,
|
|
118
|
+
.adw-tab:hover + .adw-tab::before,
|
|
119
|
+
.adw-tab + .adw-tab:hover::before {
|
|
120
|
+
opacity: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
100
123
|
.adw-tab-title {
|
|
101
124
|
white-space: nowrap;
|
|
102
125
|
overflow: hidden;
|
|
@@ -154,15 +177,25 @@ adw-tab-view {
|
|
|
154
177
|
}
|
|
155
178
|
}
|
|
156
179
|
|
|
157
|
-
//
|
|
180
|
+
// Adw.TabBar `expand-tabs` — tabs stretch to fill the bar evenly.
|
|
181
|
+
&[expand-tabs] .adw-tab {
|
|
182
|
+
flex: 1 1 0;
|
|
183
|
+
max-width: none;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Web-specific escape hatch: static tab sets (e.g. documentation command
|
|
187
|
+
// tabs) render no close affordance.
|
|
188
|
+
&[no-close] .adw-tab-close {
|
|
189
|
+
display: none;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Single-tab bars draw no tab background (libadwaita keeps a lone tab
|
|
158
193
|
// non-interactive-looking).
|
|
159
194
|
.adw-tab-bar.single-tab .adw-tab {
|
|
160
195
|
&,
|
|
161
196
|
&:hover,
|
|
162
197
|
&:active {
|
|
163
198
|
background: none;
|
|
164
|
-
box-shadow: none;
|
|
165
|
-
color: var(--headerbar-fg-color);
|
|
166
199
|
}
|
|
167
200
|
}
|
|
168
201
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// DOM-level behaviour tests for <adw-tab-view>. Runs in a real browser via the
|
|
2
|
+
// @gjsify/adwaita-web browser test axis (the entry self-applies the compiled
|
|
3
|
+
// stylesheet, so computed-style assertions are valid). Asserts page building,
|
|
4
|
+
// selected↔click sync, notify/close events, and the expand-tabs / no-close
|
|
5
|
+
// attributes.
|
|
6
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
7
|
+
|
|
8
|
+
import type { AdwTabView } from './elements/adw-tab-view.js';
|
|
9
|
+
|
|
10
|
+
function makeTabView(attrs = ''): AdwTabView {
|
|
11
|
+
const host = document.createElement('div');
|
|
12
|
+
host.innerHTML = `<adw-tab-view ${attrs}>
|
|
13
|
+
<adw-tab-page title="One"><p>first</p></adw-tab-page>
|
|
14
|
+
<adw-tab-page title="Two"><p>second</p></adw-tab-page>
|
|
15
|
+
<adw-tab-page title="Three"><p>third</p></adw-tab-page>
|
|
16
|
+
</adw-tab-view>`;
|
|
17
|
+
document.body.appendChild(host);
|
|
18
|
+
return host.querySelector('adw-tab-view') as AdwTabView;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const AdwTabViewTest = async () => {
|
|
22
|
+
await describe('adw-tab-view pages', async () => {
|
|
23
|
+
await it('builds one tab per page and shows the first page', async () => {
|
|
24
|
+
const view = makeTabView();
|
|
25
|
+
expect(view.querySelectorAll('.adw-tab').length).toBe(3);
|
|
26
|
+
expect(view.selected).toBe(0);
|
|
27
|
+
const pages = view.querySelectorAll('.adw-tab-page');
|
|
28
|
+
expect((pages[0] as HTMLElement).hidden).toBe(false);
|
|
29
|
+
expect((pages[1] as HTMLElement).hidden).toBe(true);
|
|
30
|
+
view.parentElement?.remove();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await it('clicking a tab selects its page and notifies', async () => {
|
|
34
|
+
const view = makeTabView();
|
|
35
|
+
let notified = -1;
|
|
36
|
+
view.addEventListener('notify::selected-page', (event) => {
|
|
37
|
+
notified = (event as CustomEvent).detail.selected;
|
|
38
|
+
});
|
|
39
|
+
(view.querySelectorAll('.adw-tab')[2] as HTMLButtonElement).click();
|
|
40
|
+
expect(view.selected).toBe(2);
|
|
41
|
+
expect(notified).toBe(2);
|
|
42
|
+
expect((view.querySelectorAll('.adw-tab-page')[2] as HTMLElement).hidden).toBe(false);
|
|
43
|
+
view.parentElement?.remove();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await it('setting the selected attribute switches pages', async () => {
|
|
47
|
+
const view = makeTabView();
|
|
48
|
+
view.setAttribute('selected', '1');
|
|
49
|
+
expect((view.querySelectorAll('.adw-tab-page')[1] as HTMLElement).hidden).toBe(false);
|
|
50
|
+
expect((view.querySelectorAll('.adw-tab-page')[0] as HTMLElement).hidden).toBe(true);
|
|
51
|
+
view.parentElement?.remove();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await it('emits close-page from the close affordance', async () => {
|
|
55
|
+
const view = makeTabView();
|
|
56
|
+
let closed = -1;
|
|
57
|
+
view.addEventListener('close-page', (event) => {
|
|
58
|
+
closed = (event as CustomEvent).detail.index;
|
|
59
|
+
});
|
|
60
|
+
(view.querySelectorAll('.adw-tab-close')[1] as HTMLButtonElement).click();
|
|
61
|
+
expect(closed).toBe(1);
|
|
62
|
+
// Closing must not also select the tab.
|
|
63
|
+
expect(view.selected).toBe(0);
|
|
64
|
+
view.parentElement?.remove();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await describe('adw-tab-view expand-tabs / no-close', async () => {
|
|
69
|
+
await it('no-close hides every close affordance', async () => {
|
|
70
|
+
const view = makeTabView('no-close');
|
|
71
|
+
expect(view.noClose).toBe(true);
|
|
72
|
+
for (const close of view.querySelectorAll('.adw-tab-close')) {
|
|
73
|
+
expect(getComputedStyle(close).display).toBe('none');
|
|
74
|
+
}
|
|
75
|
+
view.parentElement?.remove();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await it('expand-tabs stretches tabs evenly across the bar', async () => {
|
|
79
|
+
const view = makeTabView('expand-tabs');
|
|
80
|
+
expect(view.expandTabs).toBe(true);
|
|
81
|
+
for (const tab of view.querySelectorAll('.adw-tab')) {
|
|
82
|
+
expect(getComputedStyle(tab).flexGrow).toBe('1');
|
|
83
|
+
}
|
|
84
|
+
view.parentElement?.remove();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await it('expand-tabs draws separators, hidden next to the active tab', async () => {
|
|
88
|
+
const view = makeTabView('expand-tabs');
|
|
89
|
+
const tabs = view.querySelectorAll('.adw-tab');
|
|
90
|
+
// Tab 0 is active: the separator before tab 1 is hidden, the one
|
|
91
|
+
// before tab 2 (between two inactive tabs) is visible.
|
|
92
|
+
expect(getComputedStyle(tabs[1], '::before').opacity).toBe('0');
|
|
93
|
+
expect(getComputedStyle(tabs[2], '::before').opacity).toBe('0.2');
|
|
94
|
+
view.parentElement?.remove();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await it('properties reflect to attributes', async () => {
|
|
98
|
+
const view = makeTabView();
|
|
99
|
+
view.noClose = true;
|
|
100
|
+
view.expandTabs = true;
|
|
101
|
+
expect(view.hasAttribute('no-close')).toBe(true);
|
|
102
|
+
expect(view.hasAttribute('expand-tabs')).toBe(true);
|
|
103
|
+
view.noClose = false;
|
|
104
|
+
expect(view.hasAttribute('no-close')).toBe(false);
|
|
105
|
+
view.parentElement?.remove();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
};
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
// selected (zero-based index of the visible page, default 0)
|
|
10
10
|
// autohide (boolean — hide the tab bar when only one page remains, mirroring
|
|
11
11
|
// the Adw.TabBar `autohide` property)
|
|
12
|
+
// expand-tabs (boolean — tabs stretch to fill the bar evenly, mirroring the
|
|
13
|
+
// Adw.TabBar `expand-tabs` property)
|
|
14
|
+
// no-close (boolean — render no close affordance; web-specific escape hatch
|
|
15
|
+
// for static tab sets such as documentation command tabs)
|
|
12
16
|
// Events:
|
|
13
17
|
// `notify::selected-page` (CustomEvent, bubbles, `detail = { selected }`) when
|
|
14
18
|
// the visible page changes — mirrors the Adw.TabView `selected-page` property.
|
|
@@ -37,7 +41,7 @@ export class AdwTabView extends HTMLElement {
|
|
|
37
41
|
private _initialized = false;
|
|
38
42
|
|
|
39
43
|
static get observedAttributes() {
|
|
40
|
-
return ['selected', 'autohide'];
|
|
44
|
+
return ['selected', 'autohide', 'expand-tabs', 'no-close'];
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
/** Zero-based index of the visible page. */
|
|
@@ -59,6 +63,26 @@ export class AdwTabView extends HTMLElement {
|
|
|
59
63
|
else this.removeAttribute('autohide');
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
/** Whether tabs stretch to fill the bar evenly (Adw.TabBar `expand-tabs`). */
|
|
67
|
+
get expandTabs(): boolean {
|
|
68
|
+
return this.hasAttribute('expand-tabs');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
set expandTabs(value: boolean) {
|
|
72
|
+
if (value) this.setAttribute('expand-tabs', '');
|
|
73
|
+
else this.removeAttribute('expand-tabs');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Whether the close affordance is omitted (static tab sets). */
|
|
77
|
+
get noClose(): boolean {
|
|
78
|
+
return this.hasAttribute('no-close');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
set noClose(value: boolean) {
|
|
82
|
+
if (value) this.setAttribute('no-close', '');
|
|
83
|
+
else this.removeAttribute('no-close');
|
|
84
|
+
}
|
|
85
|
+
|
|
62
86
|
connectedCallback() {
|
|
63
87
|
if (this._initialized) return;
|
|
64
88
|
this._initialized = true;
|