@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
package/scss/_view_switcher.scss
CHANGED
|
@@ -47,6 +47,15 @@ adw-view-switcher {
|
|
|
47
47
|
user-select: none;
|
|
48
48
|
-webkit-user-select: none;
|
|
49
49
|
|
|
50
|
+
// A page with neither title nor icon has NO button at all
|
|
51
|
+
// (adw-view-switcher.c:178), spelled here as the `hidden` attribute.
|
|
52
|
+
// Re-assert it as an AUTHOR rule: the UA `[hidden] { display: none }`
|
|
53
|
+
// loses to this block's own author `display: inline-flex` (cascade ORIGIN
|
|
54
|
+
// beats specificity), so without this a hidden button stays on screen.
|
|
55
|
+
&[hidden] {
|
|
56
|
+
display: none;
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
.adw-icon {
|
|
51
60
|
width: 16px;
|
|
52
61
|
height: 16px;
|
|
@@ -59,6 +68,37 @@ adw-view-switcher {
|
|
|
59
68
|
text-overflow: ellipsis;
|
|
60
69
|
}
|
|
61
70
|
|
|
71
|
+
// AdwIndicatorBin's badge — the unread count, or a bare dot when only
|
|
72
|
+
// `needs-attention` is set.
|
|
73
|
+
// Reference: refs/libadwaita/src/adw-indicator-bin.c
|
|
74
|
+
.adw-view-switcher-indicator {
|
|
75
|
+
display: inline-flex;
|
|
76
|
+
box-sizing: border-box;
|
|
77
|
+
align-items: center;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
min-width: 16px;
|
|
80
|
+
height: 16px;
|
|
81
|
+
padding: 0 4px;
|
|
82
|
+
border-radius: 8px;
|
|
83
|
+
background-color: var(--accent-bg-color);
|
|
84
|
+
color: var(--accent-fg-color);
|
|
85
|
+
font-size: 0.625rem;
|
|
86
|
+
font-weight: 700;
|
|
87
|
+
line-height: 1;
|
|
88
|
+
|
|
89
|
+
&[hidden] {
|
|
90
|
+
display: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
&.needs-attention {
|
|
94
|
+
min-width: 6px;
|
|
95
|
+
width: 6px;
|
|
96
|
+
height: 6px;
|
|
97
|
+
padding: 0;
|
|
98
|
+
border-radius: 3px;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
62
102
|
&:hover {
|
|
63
103
|
background-color: var(--button-hover-color);
|
|
64
104
|
}
|
|
@@ -64,6 +64,14 @@ adw-view-switcher-bar {
|
|
|
64
64
|
user-select: none;
|
|
65
65
|
-webkit-user-select: none;
|
|
66
66
|
|
|
67
|
+
// Same cascade trap as the bar itself: a page with neither title nor
|
|
68
|
+
// icon has no button (adw-view-switcher.c:178), and the UA
|
|
69
|
+
// `[hidden] { display: none }` loses to this block's author
|
|
70
|
+
// `display: inline-flex` unless it is re-asserted here.
|
|
71
|
+
&[hidden] {
|
|
72
|
+
display: none;
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
.adw-view-switcher-bar-icon {
|
|
68
76
|
width: 16px;
|
|
69
77
|
height: 16px;
|
|
@@ -78,6 +86,35 @@ adw-view-switcher-bar {
|
|
|
78
86
|
text-overflow: ellipsis;
|
|
79
87
|
}
|
|
80
88
|
|
|
89
|
+
// AdwIndicatorBin's badge — see refs/libadwaita/src/adw-indicator-bin.c.
|
|
90
|
+
.adw-view-switcher-bar-indicator {
|
|
91
|
+
display: inline-flex;
|
|
92
|
+
box-sizing: border-box;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
min-width: 16px;
|
|
96
|
+
height: 16px;
|
|
97
|
+
padding: 0 4px;
|
|
98
|
+
border-radius: 8px;
|
|
99
|
+
background-color: var(--accent-bg-color);
|
|
100
|
+
color: var(--accent-fg-color);
|
|
101
|
+
font-size: 0.625rem;
|
|
102
|
+
font-weight: 700;
|
|
103
|
+
line-height: 1;
|
|
104
|
+
|
|
105
|
+
&[hidden] {
|
|
106
|
+
display: none;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
&.needs-attention {
|
|
110
|
+
min-width: 6px;
|
|
111
|
+
width: 6px;
|
|
112
|
+
height: 6px;
|
|
113
|
+
padding: 0;
|
|
114
|
+
border-radius: 3px;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
81
118
|
&:hover {
|
|
82
119
|
background-color: var(--button-hover-color);
|
|
83
120
|
}
|
package/scss/_wrap_box.scss
CHANGED
|
@@ -5,16 +5,66 @@
|
|
|
5
5
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
6
6
|
// Modifications: Adapted for @gjsify/adwaita-web Web Components.
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
8
|
+
// Direction, gaps, `justify-content` and `align-content` are driven imperatively
|
|
9
|
+
// by the component from its attributes — these are the libadwaita defaults
|
|
10
|
+
// (horizontal, packed to the start of each line, lines wrapping downwards from
|
|
11
|
+
// the top). A wrap box is a pure layout container, so it carries no background
|
|
12
|
+
// of its own; children handle their own theming.
|
|
13
13
|
adw-wrap-box {
|
|
14
14
|
display: flex;
|
|
15
15
|
flex-direction: row;
|
|
16
16
|
flex-wrap: wrap;
|
|
17
17
|
justify-content: flex-start;
|
|
18
|
-
|
|
18
|
+
// Each child is allocated the FULL cross extent of its line — `h = line_size`
|
|
19
|
+
// in adw-wrap-layout.c:746-751 — which is `stretch`, the flexbox default. The
|
|
20
|
+
// port used to write `flex-start` here and drive it from `align`, which is a
|
|
21
|
+
// MAIN-axis property in libadwaita and belongs on `justify-content`.
|
|
22
|
+
align-items: stretch;
|
|
19
23
|
align-content: flex-start;
|
|
20
24
|
}
|
|
25
|
+
|
|
26
|
+
// FILL grows the children and keeps the spacing constant
|
|
27
|
+
// (adw-wrap-layout.c:317-319, :326-341); SPREAD deliberately does NOT, so the
|
|
28
|
+
// gaps take the leftover instead. `data-line-justify` is the mode the component
|
|
29
|
+
// resolved for a COMPLETE line.
|
|
30
|
+
//
|
|
31
|
+
// A lone child is excluded because it is not on a complete line: a box with one
|
|
32
|
+
// element child has exactly one line, and that line is the final one — see
|
|
33
|
+
// below.
|
|
34
|
+
adw-wrap-box[data-line-justify='fill'] > *:not(:only-child) {
|
|
35
|
+
flex-grow: 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// `:only-child` is the one line-membership fact CSS knows without a layout pass,
|
|
39
|
+
// and libadwaita treats that line as the LAST one. Both justified modes stretch
|
|
40
|
+
// it: FILL by definition, SPREAD because its "keep the children at their minimum
|
|
41
|
+
// size" branch is guarded by `n_children > 1` (adw-wrap-layout.c:338), so a lone
|
|
42
|
+
// child is stretched regardless (adw-wrap-box.c:349-352).
|
|
43
|
+
adw-wrap-box[data-last-line-justify='fill'] > :only-child,
|
|
44
|
+
adw-wrap-box[data-last-line-justify='spread'] > :only-child {
|
|
45
|
+
flex-grow: 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The final line, when it is NOT justified (`justify-last-line` unset — the C
|
|
49
|
+
// default, adw-wrap-box.c:379-381): a generated flex item absorbs that line's
|
|
50
|
+
// leftover space, so it packs at the start of the main axis the way an
|
|
51
|
+
// un-justified line does, while `justify-content: space-between` keeps spreading
|
|
52
|
+
// the complete lines above it. `::after` is the only per-line hook flexbox
|
|
53
|
+
// offers, and it lands on the final line by construction.
|
|
54
|
+
//
|
|
55
|
+
// The negative main-axis margin cancels exactly the one gap the filler would
|
|
56
|
+
// otherwise add, so an exactly-full line can never be pushed into a new one.
|
|
57
|
+
adw-wrap-box[data-line-justify='spread'][data-last-line-justify='none']::after {
|
|
58
|
+
content: '';
|
|
59
|
+
flex: 1 0 0;
|
|
60
|
+
min-width: 0;
|
|
61
|
+
min-height: 0;
|
|
62
|
+
margin-left: calc(-1 * var(--adw-wrap-box-child-spacing, 0px));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A vertical box packs along the block axis, so the gap to cancel is the one
|
|
66
|
+
// before the filler in that direction.
|
|
67
|
+
adw-wrap-box[orientation='vertical'][data-line-justify='spread'][data-last-line-justify='none']::after {
|
|
68
|
+
margin-left: 0;
|
|
69
|
+
margin-top: calc(-1 * var(--adw-wrap-box-child-spacing, 0px));
|
|
70
|
+
}
|
package/scss/adwaita-skin.scss
CHANGED
|
@@ -13,16 +13,39 @@
|
|
|
13
13
|
// Style-isolation boundary — emitted before the components so their own rules
|
|
14
14
|
// override this typographic/box-model floor (see _reset.scss).
|
|
15
15
|
@use 'reset';
|
|
16
|
+
// The ONE symbolic-icon box, behind <adw-icon>. Emitted before every widget that
|
|
17
|
+
// draws one, so a context that sizes its own icons (the about dialog's 96px, the
|
|
18
|
+
// split button's 14px arrows, the view switcher's 16px) still wins on ordinary
|
|
19
|
+
// specificity.
|
|
20
|
+
@use 'icon';
|
|
21
|
+
// libadwaita's typography + semantic-colour UTILITY CLASSES (`.dimmed`,
|
|
22
|
+
// `.numeric`, `.monospace`, `.title-1`…). Classes only — there is deliberately
|
|
23
|
+
// no <adw-label>, see _labels.scss. Emitted BEFORE every widget so a widget's
|
|
24
|
+
// own rule wins on ordinary specificity, and because two partials @extend from
|
|
25
|
+
// it (`_progress_bar.scss`'s text node, `_data_grid.scss`'s cells) exactly as
|
|
26
|
+
// libadwaita's own `_progress-bar.scss` does.
|
|
27
|
+
@use 'labels';
|
|
16
28
|
@use 'window';
|
|
17
29
|
@use 'headerbar';
|
|
18
30
|
@use 'window_title';
|
|
19
31
|
@use 'button';
|
|
20
32
|
@use 'button_content';
|
|
33
|
+
// The ONE popover surface. Emitted BEFORE the widgets that host one, so a
|
|
34
|
+
// consumer's (or a widget's) own higher-specificity rule still wins over it.
|
|
35
|
+
@use 'popover';
|
|
21
36
|
@use 'split_button';
|
|
22
37
|
@use 'menu_button';
|
|
23
38
|
@use 'drop_down';
|
|
24
39
|
@use 'toggle_group';
|
|
40
|
+
// The ONE toggle, behind <adw-switch>. Emitted before `switch_row` and
|
|
41
|
+
// `expander_row`, the two widgets that host one — and which each carried their
|
|
42
|
+
// own copy of it until it was lifted out of `adw-switch-row {}`.
|
|
43
|
+
@use 'switch';
|
|
25
44
|
@use 'entry';
|
|
45
|
+
// The two form-control primitives, in ONE partial because upstream merges them
|
|
46
|
+
// (there is no `_radio.scss` in libadwaita) — see _checks.scss.
|
|
47
|
+
@use 'checks';
|
|
48
|
+
@use 'progress_bar';
|
|
26
49
|
@use 'preferences';
|
|
27
50
|
@use 'card';
|
|
28
51
|
@use 'data_grid';
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// DOM-level conformance tests for <adw-about-dialog>, driven by the vectors in
|
|
2
|
+
// `@gjsify/adwaita-core/conformance` — the same table the core suite asserts.
|
|
3
|
+
//
|
|
4
|
+
// This element carried its own copy of `parse_person`, its own page-visibility
|
|
5
|
+
// rules and its own set of invented fallbacks, and diverged from libadwaita on
|
|
6
|
+
// thirteen counts. Two of them were not subtle: Support Questions and Report an
|
|
7
|
+
// Issue were rendered on the DETAILS page, one navigation step deeper than GTK
|
|
8
|
+
// puts them (adw-about-dialog.ui:146-186), and `website` was treated as Details
|
|
9
|
+
// content, which is exactly the one input libadwaita leaves OUT of that
|
|
10
|
+
// predicate (:1108). Nothing compared the two, so nothing failed.
|
|
11
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
12
|
+
|
|
13
|
+
import { ADW_ABOUT_DIALOG_LABELS, stripMnemonic } from '@gjsify/adwaita-core';
|
|
14
|
+
import {
|
|
15
|
+
ABOUT_DIALOG_CREDITS_LEGAL_VECTORS,
|
|
16
|
+
ABOUT_DIALOG_DETAILS_VECTORS,
|
|
17
|
+
ABOUT_DIALOG_HEADER_VECTORS,
|
|
18
|
+
ABOUT_DIALOG_SUPPORT_VECTORS,
|
|
19
|
+
CREDITS_SECTIONS_VECTORS,
|
|
20
|
+
CREDIT_PERSON_VECTORS,
|
|
21
|
+
} from '@gjsify/adwaita-core/conformance';
|
|
22
|
+
|
|
23
|
+
/** The label a renderer without an accelerator layer paints for a row. */
|
|
24
|
+
const label = (key: keyof typeof ADW_ABOUT_DIALOG_LABELS) => stripMnemonic(ADW_ABOUT_DIALOG_LABELS[key]);
|
|
25
|
+
|
|
26
|
+
interface Mounted {
|
|
27
|
+
dialog: HTMLElement & { developers: string[]; designers: string[]; artists: string[]; documenters: string[] };
|
|
28
|
+
host: HTMLElement;
|
|
29
|
+
/** Every `window.open` the dialog performed, most recent last. */
|
|
30
|
+
opened: string[];
|
|
31
|
+
/** Restore the stubbed `window.open`. */
|
|
32
|
+
dispose: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Mount a dialog and set every attribute imperatively — several vectors hinge on
|
|
37
|
+
* exact whitespace, which a value parsed out of markup is not a reliable
|
|
38
|
+
* carrier for. `window.open` is stubbed because the link rows really do call it.
|
|
39
|
+
*/
|
|
40
|
+
function mount(attributes: Record<string, string> = {}): Mounted {
|
|
41
|
+
const host = document.createElement('div');
|
|
42
|
+
document.body.appendChild(host);
|
|
43
|
+
const dialog = document.createElement('adw-about-dialog') as Mounted['dialog'];
|
|
44
|
+
host.appendChild(dialog);
|
|
45
|
+
for (const [name, value] of Object.entries(attributes)) dialog.setAttribute(name, value);
|
|
46
|
+
|
|
47
|
+
const opened: string[] = [];
|
|
48
|
+
const original = window.open;
|
|
49
|
+
(window as unknown as { open: unknown }).open = (url?: string | URL) => {
|
|
50
|
+
opened.push(String(url ?? ''));
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
dialog,
|
|
56
|
+
host,
|
|
57
|
+
opened,
|
|
58
|
+
dispose: () => {
|
|
59
|
+
(window as unknown as { open: unknown }).open = original;
|
|
60
|
+
host.remove();
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The navigation page with `tag`, or `null` when the dialog did not build it. */
|
|
66
|
+
function page(dialog: HTMLElement, tag: string): HTMLElement | null {
|
|
67
|
+
return dialog.querySelector(`adw-navigation-page[tag="${tag}"]`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The preferences group the template calls `id`, on whichever page it lives. */
|
|
71
|
+
function group(dialog: HTMLElement, id: string): HTMLElement | null {
|
|
72
|
+
return dialog.querySelector(`adw-preferences-group[data-group="${id}"]`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Whether a row with `title` exists inside `scope`. */
|
|
76
|
+
function hasRow(scope: Element | null, title: string): boolean {
|
|
77
|
+
if (!scope) return false;
|
|
78
|
+
return scope.querySelector(`adw-action-row[title="${title}"]`) !== null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The row titles inside `scope`, in document order. */
|
|
82
|
+
function rowTitles(scope: Element | null): string[] {
|
|
83
|
+
if (!scope) return [];
|
|
84
|
+
return [...scope.querySelectorAll('adw-action-row')].map((row) => row.getAttribute('title') ?? '');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Activate a row and report the URI it asked for.
|
|
89
|
+
*
|
|
90
|
+
* The URI never reaches the DOM — it is captured in the row's activation
|
|
91
|
+
* handler — so the only way to observe it is to drive the row and listen.
|
|
92
|
+
*/
|
|
93
|
+
function activateRow(dialog: HTMLElement, row: Element): string | null {
|
|
94
|
+
let uri: string | null = null;
|
|
95
|
+
const listener = (event: Event) => {
|
|
96
|
+
uri = (event as CustomEvent<{ uri: string }>).detail.uri;
|
|
97
|
+
};
|
|
98
|
+
dialog.addEventListener('activate-link', listener);
|
|
99
|
+
row.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
|
|
100
|
+
dialog.removeEventListener('activate-link', listener);
|
|
101
|
+
return uri;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const AdwAboutDialogTest = async () => {
|
|
105
|
+
await describe('adw-about-dialog credit parsing (parse_person vectors)', async () => {
|
|
106
|
+
for (const { input, name, uri, rule } of CREDIT_PERSON_VECTORS) {
|
|
107
|
+
await it(`${JSON.stringify(input)} — ${rule}`, () => {
|
|
108
|
+
const mounted = mount({ 'application-name': 'Builder' });
|
|
109
|
+
mounted.dialog.developers = [input];
|
|
110
|
+
|
|
111
|
+
const credits = page(mounted.dialog, 'credits');
|
|
112
|
+
const rows = credits ? [...credits.querySelectorAll('adw-action-row')] : [];
|
|
113
|
+
expect(rows).toHaveLength(1);
|
|
114
|
+
|
|
115
|
+
const row = rows[0]!;
|
|
116
|
+
expect(row.getAttribute('title')).toBe(name);
|
|
117
|
+
expect(activateRow(mounted.dialog, row)).toBe(uri);
|
|
118
|
+
|
|
119
|
+
mounted.dispose();
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await it('makes a row with an empty link a LINK row, not a plain one', () => {
|
|
124
|
+
// `if (link)` is a pointer test in the C, so `"Ada <>"` still links,
|
|
125
|
+
// to the URI `mailto:` — collapsing "" to null loses the row type.
|
|
126
|
+
const mounted = mount();
|
|
127
|
+
mounted.dialog.developers = ['Ada <>'];
|
|
128
|
+
const row = page(mounted.dialog, 'credits')!.querySelector('adw-action-row')!;
|
|
129
|
+
expect(row.hasAttribute('activatable')).toBe(true);
|
|
130
|
+
expect(activateRow(mounted.dialog, row)).toBe('mailto:');
|
|
131
|
+
mounted.dispose();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await describe('adw-about-dialog credits page (update_credits vectors)', async () => {
|
|
136
|
+
for (const { input, sections, rule } of CREDITS_SECTIONS_VECTORS) {
|
|
137
|
+
// `add_credit_section` has no counterpart on this element yet, so the
|
|
138
|
+
// rows exercising it are asserted by the core suite alone.
|
|
139
|
+
if (input.creditSections) continue;
|
|
140
|
+
// Same for `translator-credits`: no property, so no "Translated by".
|
|
141
|
+
if (input.translatorCredits !== undefined) continue;
|
|
142
|
+
|
|
143
|
+
await it(rule, () => {
|
|
144
|
+
const mounted = mount();
|
|
145
|
+
mounted.dialog.developers = [...(input.developers ?? [])];
|
|
146
|
+
mounted.dialog.designers = [...(input.designers ?? [])];
|
|
147
|
+
mounted.dialog.artists = [...(input.artists ?? [])];
|
|
148
|
+
mounted.dialog.documenters = [...(input.documenters ?? [])];
|
|
149
|
+
|
|
150
|
+
const credits = page(mounted.dialog, 'credits');
|
|
151
|
+
const groups = credits ? [...credits.querySelectorAll('adw-preferences-group')] : [];
|
|
152
|
+
expect(groups.map((node) => node.getAttribute('title'))).toStrictEqual(
|
|
153
|
+
sections.map((section) => section.title),
|
|
154
|
+
);
|
|
155
|
+
for (const [index, section] of sections.entries()) {
|
|
156
|
+
expect(rowTitles(groups[index]!)).toStrictEqual(section.people.map((person) => person.name));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
mounted.dispose();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await describe('adw-about-dialog main page (update_details vectors)', async () => {
|
|
165
|
+
// `add_link` has no counterpart on this element, so `has_custom_links`
|
|
166
|
+
// is always FALSE here; those rows are asserted by the core suite.
|
|
167
|
+
for (const vector of ABOUT_DIALOG_DETAILS_VECTORS.filter((row) => !row.customLinks && !row.releaseNotes)) {
|
|
168
|
+
const { website, comments, visible, rule } = vector;
|
|
169
|
+
await it(`website=${website} comments=${comments} — ${rule}`, () => {
|
|
170
|
+
const mounted = mount({
|
|
171
|
+
'application-name': 'Builder',
|
|
172
|
+
...(website ? { website: 'https://example.org' } : {}),
|
|
173
|
+
...(comments ? { comments: 'A description.' } : {}),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const main = page(mounted.dialog, 'main');
|
|
177
|
+
const details = page(mounted.dialog, 'details');
|
|
178
|
+
|
|
179
|
+
expect({
|
|
180
|
+
detailsGroup: group(mounted.dialog, 'details') !== null,
|
|
181
|
+
detailsRow: hasRow(main, label('detailsRow')),
|
|
182
|
+
websiteRow: hasRow(main, label('websiteRow')),
|
|
183
|
+
commentsLabel: details?.querySelector('.adw-about-dialog-comments') != null,
|
|
184
|
+
linksGroup: group(mounted.dialog, 'links') !== null,
|
|
185
|
+
detailsWebsiteRow: hasRow(details, label('websiteRow')),
|
|
186
|
+
}).toStrictEqual({
|
|
187
|
+
detailsGroup: visible.detailsGroup,
|
|
188
|
+
detailsRow: visible.detailsRow,
|
|
189
|
+
websiteRow: visible.websiteRow,
|
|
190
|
+
commentsLabel: visible.commentsLabel,
|
|
191
|
+
linksGroup: visible.linksGroup,
|
|
192
|
+
detailsWebsiteRow: visible.detailsWebsiteRow,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
mounted.dispose();
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await it('keeps a website-only dialog on the main page with no Details row', () => {
|
|
200
|
+
// `show_details` is `has_comments || has_custom_links` (:1108) —
|
|
201
|
+
// website is deliberately not in it, so a dialog whose only extra is
|
|
202
|
+
// a website links straight from the main page.
|
|
203
|
+
const mounted = mount({ 'application-name': 'Builder', website: 'https://example.org' });
|
|
204
|
+
expect(page(mounted.dialog, 'details')).toBe(null);
|
|
205
|
+
expect(hasRow(page(mounted.dialog, 'main'), label('websiteRow'))).toBe(true);
|
|
206
|
+
mounted.dispose();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await describe('adw-about-dialog main page (update_support vectors)', async () => {
|
|
211
|
+
// debug-info has no counterpart on this element (there is no
|
|
212
|
+
// Troubleshooting page), so those rows are asserted by the core suite.
|
|
213
|
+
for (const vector of ABOUT_DIALOG_SUPPORT_VECTORS.filter((row) => !row.debugInfo)) {
|
|
214
|
+
const { supportUrl, issueUrl, visible, rule } = vector;
|
|
215
|
+
await it(`support=${supportUrl} issue=${issueUrl} — ${rule}`, () => {
|
|
216
|
+
const mounted = mount({
|
|
217
|
+
'application-name': 'Builder',
|
|
218
|
+
...(supportUrl ? { 'support-url': 'https://example.org/help' } : {}),
|
|
219
|
+
...(issueUrl ? { 'issue-url': 'https://example.org/issues' } : {}),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const main = page(mounted.dialog, 'main');
|
|
223
|
+
expect({
|
|
224
|
+
supportGroup: group(mounted.dialog, 'support') !== null,
|
|
225
|
+
supportRow: hasRow(main, label('supportRow')),
|
|
226
|
+
issueRow: hasRow(main, label('issueRow')),
|
|
227
|
+
}).toStrictEqual({
|
|
228
|
+
supportGroup: visible.supportGroup,
|
|
229
|
+
supportRow: visible.supportRow,
|
|
230
|
+
issueRow: visible.issueRow,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
mounted.dispose();
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await it('puts the support and issue rows on the MAIN page, not on Details', () => {
|
|
238
|
+
// adw-about-dialog.ui:146-186 — `support_group` is a child of the
|
|
239
|
+
// main page, and the class docs say so outright (:87-90).
|
|
240
|
+
const mounted = mount({
|
|
241
|
+
'application-name': 'Builder',
|
|
242
|
+
'support-url': 'https://example.org/help',
|
|
243
|
+
'issue-url': 'https://example.org/issues',
|
|
244
|
+
});
|
|
245
|
+
expect(rowTitles(page(mounted.dialog, 'main'))).toContain(label('supportRow'));
|
|
246
|
+
expect(rowTitles(page(mounted.dialog, 'main'))).toContain(label('issueRow'));
|
|
247
|
+
// With neither comments nor custom links there is no Details page.
|
|
248
|
+
expect(page(mounted.dialog, 'details')).toBe(null);
|
|
249
|
+
mounted.dispose();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await describe('adw-about-dialog main page (credits/legal vectors)', async () => {
|
|
254
|
+
// No acknowledgements API on this element; the core suite covers it.
|
|
255
|
+
for (const vector of ABOUT_DIALOG_CREDITS_LEGAL_VECTORS.filter((row) => !row.hasAcknowledgements)) {
|
|
256
|
+
const { hasCredits, hasLegal, visible, rule } = vector;
|
|
257
|
+
await it(`credits=${hasCredits} legal=${hasLegal} — ${rule}`, () => {
|
|
258
|
+
const mounted = mount({
|
|
259
|
+
'application-name': 'Builder',
|
|
260
|
+
...(hasLegal ? { copyright: '© 2026 Ada' } : {}),
|
|
261
|
+
});
|
|
262
|
+
if (hasCredits) mounted.dialog.developers = ['Ada'];
|
|
263
|
+
|
|
264
|
+
const main = page(mounted.dialog, 'main');
|
|
265
|
+
expect({
|
|
266
|
+
creditsLegalGroup: group(mounted.dialog, 'credits-legal') !== null,
|
|
267
|
+
creditsRow: hasRow(main, label('creditsRow')),
|
|
268
|
+
legalRow: hasRow(main, label('legalRow')),
|
|
269
|
+
}).toStrictEqual({
|
|
270
|
+
creditsLegalGroup: visible.creditsLegalGroup,
|
|
271
|
+
creditsRow: visible.creditsRow,
|
|
272
|
+
legalRow: visible.legalRow,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
mounted.dispose();
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await describe('adw-about-dialog header fields (no invented fallbacks)', async () => {
|
|
281
|
+
for (const { property, value, visible, rule } of ABOUT_DIALOG_HEADER_VECTORS) {
|
|
282
|
+
if (property === 'application-icon' || property === 'application-name') {
|
|
283
|
+
await it(`${property}=${JSON.stringify(value)} — ${rule}`, () => {
|
|
284
|
+
const mounted = mount(value.length > 0 ? { [property]: value } : {});
|
|
285
|
+
const main = page(mounted.dialog, 'main')!;
|
|
286
|
+
const node =
|
|
287
|
+
property === 'application-icon'
|
|
288
|
+
? main.querySelector('.adw-about-dialog-icon')
|
|
289
|
+
: main.querySelector('.adw-about-dialog-name');
|
|
290
|
+
expect(node !== null).toBe(visible);
|
|
291
|
+
mounted.dispose();
|
|
292
|
+
});
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await it(`${property}=${JSON.stringify(value)} — ${rule}`, () => {
|
|
297
|
+
const mounted = mount(value.length > 0 ? { [property]: value } : {});
|
|
298
|
+
const main = page(mounted.dialog, 'main')!;
|
|
299
|
+
const selector =
|
|
300
|
+
property === 'developer-name' ? '.adw-about-dialog-developer' : '.adw-about-dialog-version';
|
|
301
|
+
expect(main.querySelector(selector) !== null).toBe(visible);
|
|
302
|
+
mounted.dispose();
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await it('titles the dialog "About", not "About <app name>"', () => {
|
|
307
|
+
// The main page's title is bound to AdwDialog:title, whose template
|
|
308
|
+
// default is the bare word (.ui:6, :19); the application name lives
|
|
309
|
+
// in the header revealer instead (.ui:29-31).
|
|
310
|
+
const mounted = mount({ 'application-name': 'Builder' });
|
|
311
|
+
expect(page(mounted.dialog, 'main')!.getAttribute('title')).toBe('About');
|
|
312
|
+
mounted.dispose();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await describe('adw-about-dialog activate-link is cancellable', async () => {
|
|
317
|
+
await it('opens the URI when nothing handled the signal', () => {
|
|
318
|
+
const mounted = mount({ 'application-name': 'Builder', website: 'https://example.org' });
|
|
319
|
+
const row = page(mounted.dialog, 'main')!.querySelector(`adw-action-row[title="${label('websiteRow')}"]`)!;
|
|
320
|
+
row.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
|
|
321
|
+
expect(mounted.opened).toStrictEqual(['https://example.org']);
|
|
322
|
+
mounted.dispose();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await it('does NOT open when a handler cancels it', () => {
|
|
326
|
+
// `activate_link_cb` returns the accumulated handler result
|
|
327
|
+
// (:429-437, accumulator `g_signal_accumulator_true_handled`
|
|
328
|
+
// :2098): a handler saying "handled" suppresses the default
|
|
329
|
+
// navigation. `preventDefault()` is the web spelling of that, and it
|
|
330
|
+
// was unavailable — the event was not cancelable and `window.open`
|
|
331
|
+
// ran regardless.
|
|
332
|
+
const mounted = mount({ 'application-name': 'Builder', website: 'https://example.org' });
|
|
333
|
+
mounted.dialog.addEventListener('activate-link', (event) => event.preventDefault());
|
|
334
|
+
const row = page(mounted.dialog, 'main')!.querySelector(`adw-action-row[title="${label('websiteRow')}"]`)!;
|
|
335
|
+
row.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
|
|
336
|
+
expect(mounted.opened).toStrictEqual([]);
|
|
337
|
+
mounted.dispose();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await it('is cancelable on a credit row too', () => {
|
|
341
|
+
const mounted = mount();
|
|
342
|
+
mounted.dialog.developers = ['Ada <ada@lovelace.org>'];
|
|
343
|
+
mounted.dialog.addEventListener('activate-link', (event) => event.preventDefault());
|
|
344
|
+
const row = page(mounted.dialog, 'credits')!.querySelector('adw-action-row')!;
|
|
345
|
+
row.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
|
|
346
|
+
expect(mounted.opened).toStrictEqual([]);
|
|
347
|
+
mounted.dispose();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
};
|