@gjsify/adwaita-web 0.30.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,136 +1,480 @@
|
|
|
1
|
-
// <adw-carousel> — a
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// AdwCarousel properties. Navigation: drag/scroll-snap (touch + trackpad),
|
|
6
|
-
// optional scroll-wheel paging, and `scroll-to(index)`.
|
|
1
|
+
// <adw-carousel> — a swipeable pager (the web counterpart of Adw.Carousel).
|
|
2
|
+
// Pages are declared as direct children; the carousel lays them out in a
|
|
3
|
+
// scroll-snap strip, tracks the fractional scroll position, and exposes the
|
|
4
|
+
// AdwCarousel properties and signals.
|
|
7
5
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
6
|
+
// The BEHAVIOUR is headless and lives in `@gjsify/adwaita-core` (ADR 0004) as
|
|
7
|
+
// {@link CarouselState}, shared with the NativeScript twin and pinned by the
|
|
8
|
+
// conformance vectors in `@gjsify/adwaita-core/conformance`: snap points, the
|
|
9
|
+
// range and its clamp, which page a position settles on, the wheel axis rules
|
|
10
|
+
// and their 150 ms lockout, the keynav step, and the insert/reorder/remove
|
|
11
|
+
// ordering with its position compensation. This element is the DOM half only —
|
|
12
|
+
// it turns children into a scroll-snap strip, converts scroll offsets to and
|
|
13
|
+
// from positions, and re-emits changes as DOM events.
|
|
14
|
+
//
|
|
15
|
+
// What the lift fixed here, all of it C-derived:
|
|
16
|
+
// - `allow-scroll-wheel` defaults to TRUE (adw-carousel.c:1103-1106, :1201).
|
|
17
|
+
// It was read as a bare attribute presence, so a plain <adw-carousel>
|
|
18
|
+
// ignored the wheel entirely while the header comment claimed the opposite,
|
|
19
|
+
// and every doc example had to opt in.
|
|
20
|
+
// - the wheel now follows `scroll_cb` (:537-574): a touchpad propagates, a
|
|
21
|
+
// mouse wheel pages a horizontal carousel, a horizontal delta is the
|
|
22
|
+
// fallback the old `|dy| <= |dx|` early-return dropped, exactly one page per
|
|
23
|
+
// notch behind a 150 ms lockout instead of a non-decaying accumulator (which
|
|
24
|
+
// cancelled +30 then −30 out), and the event is consumed ONLY when it paged.
|
|
25
|
+
// - `page-changed` fires on every settle, including back onto the same page,
|
|
26
|
+
// and can report -1 (:363-376, :1150-1151); it used to be gated on an index
|
|
27
|
+
// change, which made -1 unreachable.
|
|
28
|
+
// - a fractional position resolves to a page with C's half-DOWN tie-break
|
|
29
|
+
// (:198-201), where `Math.round` picked the later page at every .5.
|
|
30
|
+
// - `allow-long-swipes` was dead code (`step * 1` vs `step`). It now maps onto
|
|
31
|
+
// `scroll-snap-stop`, which is the same rule in CSS: one flick, one page.
|
|
32
|
+
// - `<adw-carousel position="2">` used to be dropped, because attribute
|
|
33
|
+
// changes are delivered before `connectedCallback`; and the attribute path
|
|
34
|
+
// parsed with `parseInt` while the property path rounded. Both now parse a
|
|
35
|
+
// float and resolve it through the same page lookup.
|
|
36
|
+
// - the page list is no longer frozen at `connectedCallback`: `insertPage` /
|
|
37
|
+
// `removePage` / `reorderPage` mirror the C methods (:1370-1532).
|
|
14
38
|
//
|
|
15
39
|
// adw-carousel
|
|
16
|
-
// Attributes: allow-scroll-wheel (
|
|
17
|
-
// (
|
|
18
|
-
//
|
|
40
|
+
// Attributes: allow-scroll-wheel (default TRUE, set "false" to disable),
|
|
41
|
+
// allow-long-swipes (default false), interactive (default TRUE),
|
|
42
|
+
// spacing (px between pages, default 0), position (INITIAL/declarative page
|
|
43
|
+
// to show — write-only, the live position is the `position` property and the
|
|
44
|
+
// `notify::position` event).
|
|
19
45
|
// Events: `notify::position` (fractional scroll position changed),
|
|
20
|
-
// `notify::n-pages` (page count changed), `page-changed`
|
|
21
|
-
//
|
|
22
|
-
// story uses.
|
|
46
|
+
// `notify::n-pages` (page count changed), `page-changed`
|
|
47
|
+
// (`detail = { index }`, -1 when empty).
|
|
23
48
|
// adw-carousel-indicator-dots / adw-carousel-indicator-lines
|
|
24
49
|
// Properties: carousel (the bound AdwCarousel element), or a `for` attribute
|
|
25
50
|
// with the carousel element's id.
|
|
26
51
|
//
|
|
27
52
|
// Reference: refs/libadwaita/src/adw-carousel.c (AdwCarousel behaviour)
|
|
28
53
|
// Reference: refs/libadwaita/src/adw-carousel-indicator-dots.c
|
|
29
|
-
// (DOTS_RADIUS 3, DOTS_RADIUS_SELECTED 4, DOTS_OPACITY 0.3,
|
|
30
|
-
// DOTS_OPACITY_SELECTED 0.9, DOTS_SPACING 7)
|
|
31
54
|
// Reference: refs/libadwaita/src/adw-carousel-indicator-lines.c
|
|
32
|
-
// (LINE_WIDTH 3, LINE_LENGTH 35, LINE_SPACING 5,
|
|
33
|
-
// LINE_OPACITY 0.3, LINE_OPACITY_ACTIVE 0.9)
|
|
34
55
|
// Reference: refs/adwaita-web/adwaita-web/scss/_carousel_indicators.scss
|
|
35
56
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
36
57
|
// Modifications: Implemented as Web Components for @gjsify/adwaita-web.
|
|
37
58
|
|
|
59
|
+
import { CarouselState } from '@gjsify/adwaita-core';
|
|
60
|
+
import type { CarouselScrollRequest, CarouselScrollSource, CarouselStateChange } from '@gjsify/adwaita-core';
|
|
61
|
+
|
|
38
62
|
/** Common surface an indicator binds to. The concrete element is AdwCarousel. */
|
|
39
63
|
interface CarouselHost extends HTMLElement {
|
|
40
64
|
readonly nPages: number;
|
|
41
65
|
readonly position: number;
|
|
66
|
+
/** The page the current position settles on, `-1` when empty. */
|
|
67
|
+
readonly currentPage: number;
|
|
42
68
|
scrollToPage(index: number): void;
|
|
43
69
|
addEventListener(type: string, listener: EventListenerOrEventListenerObject): void;
|
|
44
70
|
removeEventListener(type: string, listener: EventListenerOrEventListenerObject): void;
|
|
45
71
|
}
|
|
46
72
|
|
|
73
|
+
/** A mouse wheel notch is at least this many CSS pixels — below it, assume a touchpad. */
|
|
74
|
+
const WHEEL_NOTCH_PIXELS = 40;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Which `GdkInputSource` a `WheelEvent` most likely came from.
|
|
78
|
+
*
|
|
79
|
+
* The web platform exposes no device class, so this is a HEURISTIC where GDK has
|
|
80
|
+
* a fact (`gdk_device_get_source`, adw-carousel.c:535-536) — but the distinction
|
|
81
|
+
* is load-bearing: libadwaita ignores touchpad scrolling because the touchpad's
|
|
82
|
+
* own kinetic scroll already drives the swipe, which here is the track's native
|
|
83
|
+
* scroll-snap. Line- and page-mode deltas only ever come from a wheel; a
|
|
84
|
+
* touchpad reports small or fractional pixel deltas, and is the only thing that
|
|
85
|
+
* produces a horizontal component alongside a vertical one.
|
|
86
|
+
*/
|
|
87
|
+
function wheelSource(event: WheelEvent): CarouselScrollSource {
|
|
88
|
+
if (event.deltaMode !== 0) return 'mouse';
|
|
89
|
+
const dy = Math.abs(event.deltaY);
|
|
90
|
+
const dx = Math.abs(event.deltaX);
|
|
91
|
+
if (dx > 0 && dy > 0) return 'touchpad';
|
|
92
|
+
const dominant = Math.max(dx, dy);
|
|
93
|
+
if (!Number.isInteger(dominant) || dominant < WHEEL_NOTCH_PIXELS) return 'touchpad';
|
|
94
|
+
return 'mouse';
|
|
95
|
+
}
|
|
96
|
+
|
|
47
97
|
export class AdwCarousel extends HTMLElement implements CarouselHost {
|
|
98
|
+
private readonly _state = new CarouselState({
|
|
99
|
+
// The DOM owns the scroll animation: `scrollTo` only asks, and the
|
|
100
|
+
// scroll handler feeds the ramp back through `setPosition`.
|
|
101
|
+
animateScroll: true,
|
|
102
|
+
onScrollTo: (request) => this._performScroll(request),
|
|
103
|
+
});
|
|
48
104
|
private _trackEl!: HTMLDivElement;
|
|
49
|
-
|
|
50
|
-
private
|
|
105
|
+
/** Page id → its `.adw-carousel-page` slot. Ids are internal; the DOM node is the public handle. */
|
|
106
|
+
private readonly _slots = new Map<string, HTMLDivElement>();
|
|
107
|
+
private _nextPageId = 0;
|
|
51
108
|
private _scrollRaf = 0;
|
|
52
|
-
private _wheelAccum = 0;
|
|
53
109
|
private _initialized = false;
|
|
110
|
+
/** A declared `position` waiting for the track to have a width. */
|
|
111
|
+
private _pendingPosition: number | null = null;
|
|
112
|
+
private _pendingLayout: ResizeObserver | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* `interactive` for the scroll now in flight: the flag of the request that
|
|
115
|
+
* started it, or `null` for a scroll we did not start — which is a user drag,
|
|
116
|
+
* and therefore interactive.
|
|
117
|
+
*/
|
|
118
|
+
private _pendingInteractive: boolean | null = null;
|
|
54
119
|
|
|
55
120
|
static get observedAttributes() {
|
|
56
|
-
return ['allow-scroll-wheel', 'allow-long-swipes', 'position'];
|
|
121
|
+
return ['allow-scroll-wheel', 'allow-long-swipes', 'interactive', 'spacing', 'position'];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
constructor() {
|
|
125
|
+
super();
|
|
126
|
+
// Subscribing touches no attributes and no children, so it is legal in a
|
|
127
|
+
// custom element constructor — unlike anything DOM-shaped.
|
|
128
|
+
this._state.subscribe((change) => this._onStateChange(change));
|
|
129
|
+
this._state.onPageChanged((index) => {
|
|
130
|
+
this.dispatchEvent(new CustomEvent('page-changed', { bubbles: true, detail: { index } }));
|
|
131
|
+
});
|
|
57
132
|
}
|
|
58
133
|
|
|
59
134
|
/** Number of pages in the carousel. */
|
|
60
135
|
get nPages(): number {
|
|
61
|
-
return this.
|
|
136
|
+
return this._state.nPages;
|
|
62
137
|
}
|
|
63
138
|
|
|
64
139
|
/** Fractional scroll position (0 = first page, 1 = second, …). */
|
|
65
140
|
get position(): number {
|
|
66
|
-
return this.
|
|
141
|
+
return this._state.position;
|
|
67
142
|
}
|
|
68
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Scroll to the page this position settles on.
|
|
146
|
+
*
|
|
147
|
+
* `AdwCarousel:position` is READ-ONLY in C (adw-carousel.c:1036-1041) — the
|
|
148
|
+
* setter is a port convenience, and it resolves the value with the same
|
|
149
|
+
* `get_page_at_position` rule the carousel uses everywhere else, so setting
|
|
150
|
+
* 0.5 lands on page 0 exactly as scrolling there would.
|
|
151
|
+
*/
|
|
69
152
|
set position(value: number) {
|
|
70
|
-
|
|
71
|
-
this.scrollToPage(Math.round(clamped));
|
|
153
|
+
this._scrollToPosition(value);
|
|
72
154
|
}
|
|
73
155
|
|
|
74
|
-
/**
|
|
156
|
+
/** The page the current position settles on, `-1` when the carousel is empty. */
|
|
157
|
+
get currentPage(): number {
|
|
158
|
+
return this._state.pageAt(this._state.position);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Whether the scroll wheel navigates between pages. Defaults to TRUE, as in libadwaita. */
|
|
75
162
|
get allowScrollWheel(): boolean {
|
|
76
|
-
return this.
|
|
163
|
+
return this._state.allowScrollWheel;
|
|
77
164
|
}
|
|
78
165
|
|
|
79
166
|
set allowScrollWheel(value: boolean) {
|
|
80
|
-
this.
|
|
167
|
+
this.setAttribute('allow-scroll-wheel', value ? 'true' : 'false');
|
|
81
168
|
}
|
|
82
169
|
|
|
83
|
-
/** Whether a single flick
|
|
170
|
+
/** Whether a single flick may cross more than one page. */
|
|
84
171
|
get allowLongSwipes(): boolean {
|
|
85
|
-
return this.
|
|
172
|
+
return this._state.allowLongSwipes;
|
|
86
173
|
}
|
|
87
174
|
|
|
88
175
|
set allowLongSwipes(value: boolean) {
|
|
89
176
|
this.toggleAttribute('allow-long-swipes', value);
|
|
90
177
|
}
|
|
91
178
|
|
|
179
|
+
/** Whether the carousel can be navigated at all. Defaults to TRUE. */
|
|
180
|
+
get interactive(): boolean {
|
|
181
|
+
return this._state.interactive;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
set interactive(value: boolean) {
|
|
185
|
+
this.setAttribute('interactive', value ? 'true' : 'false');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Spacing between pages in px (`AdwCarousel:spacing`). */
|
|
189
|
+
get spacing(): number {
|
|
190
|
+
return this._state.spacing;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
set spacing(value: number) {
|
|
194
|
+
this.setAttribute('spacing', String(value));
|
|
195
|
+
}
|
|
196
|
+
|
|
92
197
|
connectedCallback() {
|
|
93
198
|
if (this._initialized) return;
|
|
94
199
|
this._initialized = true;
|
|
95
200
|
|
|
96
|
-
// The declared children become the carousel's pages. Snapshot them
|
|
97
|
-
// before we move them into the scroll track.
|
|
98
|
-
this._pages = Array.from(this.children).filter((c): c is HTMLElement => c instanceof HTMLElement);
|
|
99
|
-
|
|
100
201
|
this._trackEl = document.createElement('div');
|
|
101
202
|
this._trackEl.className = 'adw-carousel-track';
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
203
|
+
// The scroll animation is CSS, not a hard-coded `behavior: 'smooth'`, so
|
|
204
|
+
// it stays overridable: a consumer honouring prefers-reduced-motion, or a
|
|
205
|
+
// test that needs the scroll to land now, sets `scroll-behavior: auto` on
|
|
206
|
+
// the track and every scroll becomes instant.
|
|
207
|
+
this._trackEl.style.setProperty('scroll-behavior', 'smooth');
|
|
208
|
+
|
|
209
|
+
// Read the declared properties BEFORE adopting children: `spacing` is
|
|
210
|
+
// part of the page pitch, so a page added first would be measured
|
|
211
|
+
// against the wrong distance.
|
|
212
|
+
this._applyAttribute('allow-scroll-wheel', this.getAttribute('allow-scroll-wheel'));
|
|
213
|
+
this._applyAttribute('allow-long-swipes', this.getAttribute('allow-long-swipes'));
|
|
214
|
+
this._applyAttribute('interactive', this.getAttribute('interactive'));
|
|
215
|
+
this._applyAttribute('spacing', this.getAttribute('spacing'));
|
|
216
|
+
|
|
217
|
+
const declared = Array.from(this.children).filter(
|
|
218
|
+
(child): child is HTMLElement => child instanceof HTMLElement,
|
|
219
|
+
);
|
|
108
220
|
this.replaceChildren(this._trackEl);
|
|
221
|
+
for (const page of declared) this._insert(page, -1);
|
|
109
222
|
|
|
110
223
|
this._trackEl.addEventListener('scroll', this._onScroll, { passive: true });
|
|
111
224
|
this._trackEl.addEventListener('wheel', this._onWheel, { passive: false });
|
|
112
225
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
226
|
+
// A declared `position` is applied last, once the pages it names exist.
|
|
227
|
+
// Custom-element upgrade delivers attributes BEFORE connectedCallback, so
|
|
228
|
+
// reading it here is the only way `<adw-carousel position="2">` works at
|
|
229
|
+
// all — it used to be dropped on the floor.
|
|
230
|
+
this._applyAttribute('position', this.getAttribute('position'));
|
|
116
231
|
}
|
|
117
232
|
|
|
118
233
|
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
|
|
119
234
|
if (!this._initialized) return;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
235
|
+
this._applyAttribute(name, newValue);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
disconnectedCallback() {
|
|
239
|
+
// A pending declared position watches the track for its first layout;
|
|
240
|
+
// a carousel removed before that happens must not keep an observer alive.
|
|
241
|
+
this._cancelPendingPosition();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** One attribute → one state property. Shared by the initial read and later changes. */
|
|
245
|
+
private _applyAttribute(name: string, value: string | null): void {
|
|
246
|
+
switch (name) {
|
|
247
|
+
case 'allow-scroll-wheel':
|
|
248
|
+
// Absent means TRUE here: the property defaults TRUE in C, so the
|
|
249
|
+
// usual "presence = on" spelling would invert it.
|
|
250
|
+
this._state.setAllowScrollWheel(value !== 'false');
|
|
251
|
+
return;
|
|
252
|
+
case 'allow-long-swipes':
|
|
253
|
+
this._state.setAllowLongSwipes(value !== null && value !== 'false');
|
|
254
|
+
this._applySnapStop();
|
|
255
|
+
return;
|
|
256
|
+
case 'interactive':
|
|
257
|
+
this._state.setInteractive(value !== 'false');
|
|
258
|
+
return;
|
|
259
|
+
case 'spacing': {
|
|
260
|
+
const spacing = value === null ? 0 : Number.parseFloat(value);
|
|
261
|
+
if (Number.isFinite(spacing)) this._state.setSpacing(Math.max(0, spacing));
|
|
262
|
+
this._trackEl.style.columnGap = `${this._state.spacing}px`;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
case 'position':
|
|
266
|
+
// One parse for both paths. The attribute used to go through
|
|
267
|
+
// parseInt while the property rounded, so "1.7" meant page 1
|
|
268
|
+
// declaratively and page 2 imperatively.
|
|
269
|
+
if (value !== null) this._positionWhenMeasurable(Number.parseFloat(value));
|
|
123
270
|
}
|
|
124
|
-
// allow-scroll-wheel / allow-long-swipes are read live in the handlers.
|
|
125
271
|
}
|
|
126
272
|
|
|
127
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* Scroll to `value`, waiting for the first layout when the track has no
|
|
275
|
+
* width yet.
|
|
276
|
+
*
|
|
277
|
+
* `connectedCallback` runs DURING insertion, so the track is still 0px wide
|
|
278
|
+
* and a scroll offset has nowhere to land — a declared
|
|
279
|
+
* `<adw-carousel position="2">` silently stayed on page 0. Deferring to the
|
|
280
|
+
* first non-zero measurement is the only point at which the offset means
|
|
281
|
+
* anything; a later assignment simply replaces the pending one.
|
|
282
|
+
*/
|
|
283
|
+
private _positionWhenMeasurable(value: number): void {
|
|
284
|
+
if (!Number.isFinite(value)) return;
|
|
285
|
+
if (this._trackEl.clientWidth > 0) {
|
|
286
|
+
this._cancelPendingPosition();
|
|
287
|
+
this._scrollInstantly(value);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
this._pendingPosition = value;
|
|
291
|
+
if (this._pendingLayout) return;
|
|
292
|
+
this._pendingLayout = new ResizeObserver(() => {
|
|
293
|
+
if (this._trackEl.clientWidth <= 0) return;
|
|
294
|
+
const pending = this._pendingPosition;
|
|
295
|
+
this._cancelPendingPosition();
|
|
296
|
+
if (pending !== null) this._scrollInstantly(pending);
|
|
297
|
+
});
|
|
298
|
+
this._pendingLayout.observe(this._trackEl);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Jump to `value` without the reveal animation.
|
|
303
|
+
*
|
|
304
|
+
* A DECLARED position is where the carousel STARTS, not somewhere it travels
|
|
305
|
+
* to — `<adw-carousel position="2">` should render already showing page 2.
|
|
306
|
+
* Left smooth, the track is still at offset 0 while the animation runs, and
|
|
307
|
+
* the `scroll` event that fires from there recomputes the position back to
|
|
308
|
+
* page 0. So the one assignment is made instant and the caller's
|
|
309
|
+
* `scroll-behavior` is put back.
|
|
310
|
+
*/
|
|
311
|
+
private _scrollInstantly(value: number): void {
|
|
312
|
+
const previous = this._trackEl.style.getPropertyValue('scroll-behavior');
|
|
313
|
+
this._trackEl.style.setProperty('scroll-behavior', 'auto');
|
|
314
|
+
try {
|
|
315
|
+
this._scrollToPosition(value);
|
|
316
|
+
} finally {
|
|
317
|
+
if (previous) this._trackEl.style.setProperty('scroll-behavior', previous);
|
|
318
|
+
else this._trackEl.style.removeProperty('scroll-behavior');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Drop a pending declared position and stop watching for a layout. */
|
|
323
|
+
private _cancelPendingPosition(): void {
|
|
324
|
+
this._pendingLayout?.disconnect();
|
|
325
|
+
this._pendingLayout = undefined;
|
|
326
|
+
this._pendingPosition = null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Resolve a fractional position to a page and scroll there; a no-op when there is none. */
|
|
330
|
+
private _scrollToPosition(value: number): void {
|
|
331
|
+
if (!Number.isFinite(value)) return;
|
|
332
|
+
const page = this._state.pageAt(value);
|
|
333
|
+
if (page >= 0) this.scrollToPage(page);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Insert `page` at `position` — `adw_carousel_insert` (adw-carousel.c:1370-1407).
|
|
338
|
+
* `-1` or a position past the end appends. Returns whether it was added.
|
|
339
|
+
*/
|
|
340
|
+
insertPage(page: HTMLElement, position = -1): boolean {
|
|
341
|
+
if (!this._initialized) {
|
|
342
|
+
this.appendChild(page);
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
return this._insert(page, position);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** `adw_carousel_append` (adw-carousel.c:1348-1357). */
|
|
349
|
+
appendPage(page: HTMLElement): boolean {
|
|
350
|
+
return this.insertPage(page, -1);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** `adw_carousel_prepend` (adw-carousel.c:1330-1339). */
|
|
354
|
+
prependPage(page: HTMLElement): boolean {
|
|
355
|
+
return this.insertPage(page, 0);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Remove `page` — `adw_carousel_remove` (adw-carousel.c:1508-1532). */
|
|
359
|
+
removePage(page: HTMLElement): boolean {
|
|
360
|
+
const id = this._idOf(page);
|
|
361
|
+
if (id === null) return false;
|
|
362
|
+
if (!this._state.removePage(id)) return false;
|
|
363
|
+
// reveal-duration is 0 by default and this port has no reveal ramp, so
|
|
364
|
+
// the shrink completes at once — `adw_animation_skip` (:331).
|
|
365
|
+
this._state.skipReveal(id);
|
|
366
|
+
this._syncTrack();
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Move `page` — `adw_carousel_reorder` (adw-carousel.c:1419-1499). */
|
|
371
|
+
reorderPage(page: HTMLElement, position: number): boolean {
|
|
372
|
+
const id = this._idOf(page);
|
|
373
|
+
if (id === null) return false;
|
|
374
|
+
if (!this._state.reorderPage(id, position)) return false;
|
|
375
|
+
this._syncTrack();
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Scroll to the page at `index`. Out-of-range, fractional and NaN indices are
|
|
381
|
+
* refused rather than clamped, matching `adw_carousel_get_nth_page`'s
|
|
382
|
+
* precondition (adw-carousel.c:1616).
|
|
383
|
+
*/
|
|
128
384
|
scrollToPage(index: number): void {
|
|
385
|
+
this._state.scrollTo(index);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** One keynav step — `navigate_to_direction` (adw-carousel.c:475-508). */
|
|
389
|
+
navigate(direction: 'back' | 'forward'): boolean {
|
|
390
|
+
return this._state.navigate(direction);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private _insert(page: HTMLElement, position: number): boolean {
|
|
394
|
+
const id = `page-${this._nextPageId++}`;
|
|
395
|
+
if (!this._state.insertPage(id, position)) return false;
|
|
396
|
+
|
|
397
|
+
const slot = document.createElement('div');
|
|
398
|
+
slot.className = 'adw-carousel-page';
|
|
399
|
+
slot.appendChild(page);
|
|
400
|
+
this._slots.set(id, slot);
|
|
401
|
+
|
|
402
|
+
// No reveal ramp in this port: with reveal-duration 0 the page is at full
|
|
403
|
+
// size immediately, which is what a zero-duration AdwTimedAnimation does.
|
|
404
|
+
this._state.skipReveal(id);
|
|
405
|
+
this._syncTrack();
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** The state's id for a page element, or `null` when it is not one of ours. */
|
|
410
|
+
private _idOf(page: HTMLElement): string | null {
|
|
411
|
+
for (const [id, slot] of this._slots) if (slot.contains(page)) return id;
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Make the DOM order match the model order, and drop slots whose page is gone. */
|
|
416
|
+
private _syncTrack(): void {
|
|
417
|
+
const ids = this._state.ids;
|
|
418
|
+
const live = new Set(ids);
|
|
419
|
+
for (const [id, slot] of this._slots) {
|
|
420
|
+
if (live.has(id)) continue;
|
|
421
|
+
slot.remove();
|
|
422
|
+
this._slots.delete(id);
|
|
423
|
+
}
|
|
424
|
+
ids.forEach((id, index) => {
|
|
425
|
+
const slot = this._slots.get(id);
|
|
426
|
+
if (!slot) return;
|
|
427
|
+
if (this._trackEl.children[index] !== slot) {
|
|
428
|
+
this._trackEl.insertBefore(slot, this._trackEl.children[index] ?? null);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
this._applySnapStop();
|
|
432
|
+
this._applyScrollFromModel();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Put the scroll offset back where the model says it is, instantly.
|
|
437
|
+
*
|
|
438
|
+
* A page revealed at or before the current one moves the position with it
|
|
439
|
+
* (`shift_position`, adw-carousel.c:241-258) precisely so the page on screen
|
|
440
|
+
* does not slide sideways — but a scroll container knows nothing about that
|
|
441
|
+
* and would keep its old offset, showing the newly-inserted page instead. GTK
|
|
442
|
+
* gets this for free because the same allocation that adds the child also
|
|
443
|
+
* lays it out; here it takes one write.
|
|
444
|
+
*/
|
|
445
|
+
private _applyScrollFromModel(): void {
|
|
446
|
+
const distance = this._distance();
|
|
447
|
+
if (!(distance > 0)) return;
|
|
448
|
+
this._trackEl.scrollTo({ left: this._state.position * distance, behavior: 'instant' });
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* `allow-long-swipes` in CSS: `scroll-snap-stop: always` forces the scroll to
|
|
453
|
+
* stop at every page, which is exactly what the property being FALSE means —
|
|
454
|
+
* "each swipe can only move to the adjacent pages" (adw-carousel.c:1111-1113).
|
|
455
|
+
*/
|
|
456
|
+
private _applySnapStop(): void {
|
|
457
|
+
const stop = this._state.allowLongSwipes ? 'normal' : 'always';
|
|
458
|
+
for (const slot of this._slots.values()) slot.style.setProperty('scroll-snap-stop', stop);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** The px pitch between two page origins — `self->distance` (adw-carousel.c:767). */
|
|
462
|
+
private _distance(): number {
|
|
463
|
+
const first = this._trackEl?.firstElementChild as HTMLElement | null;
|
|
464
|
+
if (!first) return 0;
|
|
465
|
+
return this._state.pageDistance(first.getBoundingClientRect().width);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Run the scroll the state asked for, in the track's own scroll coordinates. */
|
|
469
|
+
private _performScroll(request: CarouselScrollRequest): void {
|
|
129
470
|
if (!this._trackEl) return;
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
471
|
+
const distance = this._distance();
|
|
472
|
+
if (!(distance > 0)) return;
|
|
473
|
+
this._pendingInteractive = request.interactive;
|
|
474
|
+
// `offset = distance * position` (adw-carousel.c:797-801). The old code
|
|
475
|
+
// used `slot.offsetLeft`, which is measured from the nearest POSITIONED
|
|
476
|
+
// ancestor and only coincides with the scroll offset by accident.
|
|
477
|
+
this._trackEl.scrollTo({ left: request.index * distance });
|
|
134
478
|
}
|
|
135
479
|
|
|
136
480
|
private _onScroll = (): void => {
|
|
@@ -142,47 +486,49 @@ export class AdwCarousel extends HTMLElement implements CarouselHost {
|
|
|
142
486
|
};
|
|
143
487
|
|
|
144
488
|
private _onWheel = (event: WheelEvent): void => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
const step = this._wheelAccum > 0 ? 1 : -1;
|
|
155
|
-
this._wheelAccum = 0;
|
|
156
|
-
const current = Math.round(this._position);
|
|
157
|
-
const target = this.allowLongSwipes ? current + step * 1 : current + step;
|
|
158
|
-
this.scrollToPage(target);
|
|
489
|
+
const step = this._state.handleWheel({
|
|
490
|
+
deltaX: event.deltaX,
|
|
491
|
+
deltaY: event.deltaY,
|
|
492
|
+
source: wheelSource(event),
|
|
493
|
+
});
|
|
494
|
+
// GDK_EVENT_STOP only when it actually paged (adw-carousel.c:561-562 vs
|
|
495
|
+
// :576). The old code preventDefault()ed before its threshold test, so it
|
|
496
|
+
// swallowed sub-threshold scrolls without doing anything with them.
|
|
497
|
+
if (step !== 0) event.preventDefault();
|
|
159
498
|
};
|
|
160
499
|
|
|
161
500
|
private _updatePositionFromScroll(): void {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const next = this._trackEl.scrollLeft /
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
private _slotWidth(): number {
|
|
178
|
-
const first = this._trackEl?.children[0] as HTMLElement | undefined;
|
|
179
|
-
return first ? first.offsetWidth : 0;
|
|
501
|
+
const distance = this._distance();
|
|
502
|
+
if (!(distance > 0)) return;
|
|
503
|
+
|
|
504
|
+
const next = this._trackEl.scrollLeft / distance;
|
|
505
|
+
// A scroll event that does not MOVE the position carries no news: it is
|
|
506
|
+
// the echo of the offset `_applyScrollFromModel` just wrote, and reading
|
|
507
|
+
// it back as an arrival would announce a `page-changed` for an insert
|
|
508
|
+
// that deliberately kept the same page on screen. The notification still
|
|
509
|
+
// goes out — C's notify at :281 is unconditional.
|
|
510
|
+
if (!this._state.setPosition(next, this._pendingInteractive ?? true)) return;
|
|
511
|
+
// `scroll_animation_done_cb` has an animation to tell it the scroll
|
|
512
|
+
// finished; scroll-snap has only the offset, so the core stands ARRIVAL
|
|
513
|
+
// at a snap point in for it (adw-carousel.c:363-376).
|
|
514
|
+
if (this._state.settleIfArrived()) this._pendingInteractive = null;
|
|
180
515
|
}
|
|
181
516
|
|
|
182
|
-
private
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
517
|
+
private _onStateChange(change: CarouselStateChange): void {
|
|
518
|
+
if (change.reason === 'n-pages') {
|
|
519
|
+
this.dispatchEvent(
|
|
520
|
+
new CustomEvent('notify::n-pages', { bubbles: true, detail: { nPages: change.nPages } }),
|
|
521
|
+
);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (change.reason === 'position') {
|
|
525
|
+
this.dispatchEvent(
|
|
526
|
+
new CustomEvent('notify::position', {
|
|
527
|
+
bubbles: true,
|
|
528
|
+
detail: { position: change.position, page: change.page, interactive: change.interactive },
|
|
529
|
+
}),
|
|
530
|
+
);
|
|
531
|
+
}
|
|
186
532
|
}
|
|
187
533
|
}
|
|
188
534
|
|
|
@@ -261,6 +607,10 @@ abstract class AdwCarouselIndicator extends HTMLElement {
|
|
|
261
607
|
return this._carousel?.position ?? 0;
|
|
262
608
|
}
|
|
263
609
|
|
|
610
|
+
protected currentPage(): number {
|
|
611
|
+
return this._carousel?.currentPage ?? -1;
|
|
612
|
+
}
|
|
613
|
+
|
|
264
614
|
protected goTo(index: number): void {
|
|
265
615
|
this._carousel?.scrollToPage(index);
|
|
266
616
|
}
|
|
@@ -270,29 +620,43 @@ abstract class AdwCarouselIndicator extends HTMLElement {
|
|
|
270
620
|
|
|
271
621
|
export class AdwCarouselIndicatorDots extends AdwCarouselIndicator {
|
|
272
622
|
protected _render(): void {
|
|
273
|
-
renderIndicator(this, 'adw-carousel-dot', this.nPages(), this.position(), (
|
|
623
|
+
renderIndicator(this, 'adw-carousel-dot', this.nPages(), this.position(), this.currentPage(), (i) =>
|
|
624
|
+
this.goTo(i),
|
|
625
|
+
);
|
|
274
626
|
}
|
|
275
627
|
}
|
|
276
628
|
|
|
277
629
|
export class AdwCarouselIndicatorLines extends AdwCarouselIndicator {
|
|
278
630
|
protected _render(): void {
|
|
279
|
-
renderIndicator(this, 'adw-carousel-line', this.nPages(), this.position(), (
|
|
631
|
+
renderIndicator(this, 'adw-carousel-line', this.nPages(), this.position(), this.currentPage(), (i) =>
|
|
632
|
+
this.goTo(i),
|
|
633
|
+
);
|
|
280
634
|
}
|
|
281
635
|
}
|
|
282
636
|
|
|
283
637
|
/**
|
|
284
|
-
* Shared rebuild for both indicator kinds: one child per page, the
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* `--adw-carousel-progress
|
|
288
|
-
*
|
|
289
|
-
*
|
|
638
|
+
* Shared rebuild for both indicator kinds: one child per page, the CURRENT page
|
|
639
|
+
* carries `.active`, and clicking scrolls the carousel. The dot/line look (size,
|
|
640
|
+
* growth, opacity ramp) is in SCSS, driven by a per-marker
|
|
641
|
+
* `--adw-carousel-progress`.
|
|
642
|
+
*
|
|
643
|
+
* `current` comes from the carousel's own `get_page_at_position` rather than
|
|
644
|
+
* from the progress ramp: the old `progress > 0.5` test left NO dot marked at an
|
|
645
|
+
* exact half-way position — with two pages at position 0.5 both dots scored
|
|
646
|
+
* exactly 0.5 and every one of them reported `aria-current="false"`.
|
|
647
|
+
*
|
|
648
|
+
* The ramp itself is still the `1 − |position − i|` approximation, which is
|
|
649
|
+
* exact only while every page is fully revealed. libadwaita's real per-dot
|
|
650
|
+
* progress (`snapshot_dots`, adw-carousel-indicator-dots.c:114-134) also scales
|
|
651
|
+
* radius and opacity by that page's size, which needs a reveal ramp this port
|
|
652
|
+
* does not have.
|
|
290
653
|
*/
|
|
291
654
|
function renderIndicator(
|
|
292
655
|
host: HTMLElement,
|
|
293
656
|
markerClass: string,
|
|
294
657
|
nPages: number,
|
|
295
658
|
position: number,
|
|
659
|
+
current: number,
|
|
296
660
|
onClick: (index: number) => void,
|
|
297
661
|
): void {
|
|
298
662
|
if (host.childElementCount !== nPages) {
|
|
@@ -308,13 +672,11 @@ function renderIndicator(
|
|
|
308
672
|
host.replaceChildren(...markers);
|
|
309
673
|
}
|
|
310
674
|
Array.from(host.children).forEach((child, i) => {
|
|
311
|
-
// Proximity in [0,1]: 1 on the focused page, ramping to 0 one page away
|
|
312
|
-
// (matches AdwCarousel's per-dot lerp progress = 1 - |position - i|).
|
|
313
675
|
const progress = Math.max(0, 1 - Math.abs(position - i));
|
|
314
676
|
const marker = child as HTMLElement;
|
|
315
677
|
marker.style.setProperty('--adw-carousel-progress', progress.toFixed(3));
|
|
316
|
-
marker.classList.toggle('active',
|
|
317
|
-
marker.setAttribute('aria-current',
|
|
678
|
+
marker.classList.toggle('active', i === current);
|
|
679
|
+
marker.setAttribute('aria-current', i === current ? 'true' : 'false');
|
|
318
680
|
});
|
|
319
681
|
}
|
|
320
682
|
|