@blotch/dolan 22.2.7 → 22.2.9
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/ui/components/button/button.test.ts +30 -0
- package/dist/ui/components/dock/dock.ts +4 -5
- package/dist/ui/components/icon/icon.test.ts +117 -2
- package/dist/ui/components/icon/icon.ts +39 -2
- package/dist/ui/components/input/input.ts +1 -1
- package/dist/ui/components/navigation-bar/navigation-bar.ts +10 -11
- package/dist/ui/components/search-field/search-field.ts +3 -3
- package/dist/ui/components/textarea/textarea.ts +1 -1
- package/dist/ui/components/toolbar/bar-scroll.ts +14 -16
- package/package.json +1 -1
|
@@ -102,6 +102,23 @@ const renderButton = async (
|
|
|
102
102
|
})
|
|
103
103
|
class StaticAttributesHost {}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* A sign-in button with the provider's mark, an app's own SVG.
|
|
107
|
+
*/
|
|
108
|
+
@Component({
|
|
109
|
+
imports: [UiButton, UiIcon],
|
|
110
|
+
template: `
|
|
111
|
+
<ui-button variant="outline">
|
|
112
|
+
<ui-icon [svg]="mark" />
|
|
113
|
+
Continue with GitHub
|
|
114
|
+
</ui-button>
|
|
115
|
+
`,
|
|
116
|
+
})
|
|
117
|
+
class OwnSvgHost {
|
|
118
|
+
readonly mark =
|
|
119
|
+
'<svg viewBox="0 0 16 16"><path fill="currentColor" d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0Z"/></svg>';
|
|
120
|
+
}
|
|
121
|
+
|
|
105
122
|
const IOS_26 = { platform: 'iOS', osVersion: '26.0' };
|
|
106
123
|
const IOS_18 = { platform: 'iOS', osVersion: '18.5' };
|
|
107
124
|
const ANDROID = { platform: 'Android', osVersion: '15' };
|
|
@@ -314,6 +331,19 @@ describe('UiButton', () => {
|
|
|
314
331
|
);
|
|
315
332
|
});
|
|
316
333
|
|
|
334
|
+
it("puts an icon of the app's own SVG before the label, in the label color", async () => {
|
|
335
|
+
const { container, getByText } = await render(OwnSvgHost);
|
|
336
|
+
const icon = container.querySelector('svg')!;
|
|
337
|
+
|
|
338
|
+
expect(container.firstElementChild!.classList).toContain('gap-2');
|
|
339
|
+
expect(
|
|
340
|
+
icon.compareDocumentPosition(getByText('Continue with GitHub')),
|
|
341
|
+
).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
|
342
|
+
expect(icon.getAttribute('content')).toContain(
|
|
343
|
+
`fill="${ICON_LIGHT_COLORS['foreground']}"`,
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
|
|
317
347
|
it('puts one icon on each side of the label', async () => {
|
|
318
348
|
const { host, getByText, icons, update } = await renderButton();
|
|
319
349
|
await update(() => {
|
|
@@ -20,8 +20,10 @@ import {
|
|
|
20
20
|
import { toSignal } from '@angular/core/rxjs-interop';
|
|
21
21
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
22
22
|
import {
|
|
23
|
+
type LayoutChangeDetail,
|
|
23
24
|
LYNX_ELEMENTS,
|
|
24
25
|
LynxAccessibilitySettings,
|
|
26
|
+
type LynxEvent,
|
|
25
27
|
LynxRouterLinkActive,
|
|
26
28
|
} from '@blotch/angular-lynx';
|
|
27
29
|
|
|
@@ -205,9 +207,6 @@ const CAPSULE_OPACITY = 0.1;
|
|
|
205
207
|
*/
|
|
206
208
|
const ACCESSORY_ICON_COLOR = signal('var(--glass-foreground)').asReadonly();
|
|
207
209
|
|
|
208
|
-
/** What Lynx's `layoutchange` event carries, as far as the dock reads it. */
|
|
209
|
-
type LayoutChangeEvent = Event & { detail: { width: number } };
|
|
210
|
-
|
|
211
210
|
/**
|
|
212
211
|
* A CSS length in px.
|
|
213
212
|
*/
|
|
@@ -1014,8 +1013,8 @@ export class UiDock implements SearchFieldContainer {
|
|
|
1014
1013
|
);
|
|
1015
1014
|
});
|
|
1016
1015
|
|
|
1017
|
-
protected measure(
|
|
1018
|
-
this.width.set(
|
|
1016
|
+
protected measure({ detail }: LynxEvent<LayoutChangeDetail>): void {
|
|
1017
|
+
this.width.set(detail.width);
|
|
1019
1018
|
}
|
|
1020
1019
|
|
|
1021
1020
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Component, signal } from '@angular/core';
|
|
1
|
+
import { Component, ErrorHandler, signal } from '@angular/core';
|
|
2
2
|
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { UiIcon } from './icon';
|
|
5
5
|
import {
|
|
6
6
|
ICON_DARK_COLORS,
|
|
@@ -122,6 +122,121 @@ describe('UiIcon on its own', () => {
|
|
|
122
122
|
});
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
+
/** A one-color mark, written as an app would: no namespace. */
|
|
126
|
+
const ONE_COLOR_MARK =
|
|
127
|
+
'<svg viewBox="0 0 16 16"><path fill="currentColor" d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0Z"/></svg>';
|
|
128
|
+
|
|
129
|
+
/** A multi-color mark, which already has the namespace. */
|
|
130
|
+
const TWO_COLOR_MARK =
|
|
131
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 1"><path fill="#4285f4" d="M0 0h1v1H0z"/><path fill="#34a853" d="M1 0h1v1H1z"/></svg>';
|
|
132
|
+
|
|
133
|
+
@Component({
|
|
134
|
+
imports: [UiIcon],
|
|
135
|
+
template: `<ui-icon [svg]="svg()" [color]="color()" size="lg" />`,
|
|
136
|
+
})
|
|
137
|
+
class SvgHost {
|
|
138
|
+
readonly svg = signal(ONE_COLOR_MARK);
|
|
139
|
+
readonly color = signal<string | undefined>(undefined);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const renderSvg = async (providers: Parameters<typeof render>[1] = {}) => {
|
|
143
|
+
const result = await render(SvgHost, providers);
|
|
144
|
+
return {
|
|
145
|
+
...result,
|
|
146
|
+
host: result.componentRef.instance as SvgHost,
|
|
147
|
+
content: () =>
|
|
148
|
+
result.container.querySelector('svg')!.getAttribute('content')!,
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
describe("UiIcon with an app's own svg", () => {
|
|
153
|
+
it('draws the markup at the size given, with currentColor in the theme foreground', async () => {
|
|
154
|
+
const { container, content } = await renderSvg();
|
|
155
|
+
expect(container.querySelector('svg')!.getAttribute('style')).toContain(
|
|
156
|
+
'32px',
|
|
157
|
+
);
|
|
158
|
+
expect(content()).toContain('d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0Z"');
|
|
159
|
+
expect(content()).toContain(`fill="${ICON_LIGHT_COLORS['foreground']}"`);
|
|
160
|
+
expect(content()).not.toContain('currentColor');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('follows the color scheme and the color input, as the set does', async () => {
|
|
164
|
+
const { host, content } = await renderSvg({
|
|
165
|
+
providers: [provideIconTheme({ colorScheme: 'dark' })],
|
|
166
|
+
});
|
|
167
|
+
expect(content()).toContain(ICON_DARK_COLORS['foreground']);
|
|
168
|
+
|
|
169
|
+
host.color.set('var(--primary)');
|
|
170
|
+
await waitForUpdate();
|
|
171
|
+
expect(content()).toContain(ICON_DARK_COLORS['primary']);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('keeps the colors a multi-color mark writes out', async () => {
|
|
175
|
+
const { host, content } = await renderSvg();
|
|
176
|
+
host.svg.set(TWO_COLOR_MARK);
|
|
177
|
+
host.color.set('rgb(255, 0, 0)');
|
|
178
|
+
await waitForUpdate();
|
|
179
|
+
expect(content()).toBe(TWO_COLOR_MARK);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// The web preview draws the markup as an image, which a browser leaves
|
|
183
|
+
// blank without the namespace.
|
|
184
|
+
it('adds the SVG namespace when the markup has none, and only then', async () => {
|
|
185
|
+
const { host, content } = await renderSvg();
|
|
186
|
+
expect(content()).toMatch(
|
|
187
|
+
/^<svg xmlns="http:\/\/www\.w3\.org\/2000\/svg" viewBox="0 0 16 16">/,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
host.svg.set(TWO_COLOR_MARK);
|
|
191
|
+
await waitForUpdate();
|
|
192
|
+
expect(content().match(/xmlns=/g)).toHaveLength(1);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
@Component({
|
|
197
|
+
imports: [UiIcon],
|
|
198
|
+
template: `<ui-icon [name]="name()" [svg]="svg()" />`,
|
|
199
|
+
})
|
|
200
|
+
class NameAndSvgHost {
|
|
201
|
+
readonly name = signal<IconName | undefined>(undefined);
|
|
202
|
+
readonly svg = signal<string | undefined>(undefined);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Renders the host, keeping the errors Angular reports: an error in change
|
|
207
|
+
* detection goes to the app's ErrorHandler rather than out of render().
|
|
208
|
+
*/
|
|
209
|
+
const renderNameAndSvg = async () => {
|
|
210
|
+
const handleError = vi.fn();
|
|
211
|
+
const result = await render(NameAndSvgHost, {
|
|
212
|
+
providers: [{ provide: ErrorHandler, useValue: { handleError } }],
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
host: result.componentRef.instance as NameAndSvgHost,
|
|
216
|
+
handleError,
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const NAME_OR_SVG = new Error('<ui-icon> needs a name or an svg, not both.');
|
|
221
|
+
|
|
222
|
+
describe('UiIcon without exactly one of name and svg', () => {
|
|
223
|
+
it('reports an error with neither', async () => {
|
|
224
|
+
const { handleError } = await renderNameAndSvg();
|
|
225
|
+
expect(handleError).toHaveBeenCalledWith(NAME_OR_SVG);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('reports an error with both', async () => {
|
|
229
|
+
const { host, handleError } = await renderNameAndSvg();
|
|
230
|
+
host.name.set('check');
|
|
231
|
+
await waitForUpdate();
|
|
232
|
+
handleError.mockClear();
|
|
233
|
+
|
|
234
|
+
host.svg.set(ONE_COLOR_MARK);
|
|
235
|
+
await waitForUpdate();
|
|
236
|
+
expect(handleError).toHaveBeenCalledWith(NAME_OR_SVG);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
125
240
|
describe('ICONS registry', () => {
|
|
126
241
|
it('exports at least one icon', () => {
|
|
127
242
|
expect(Object.keys(ICONS).length).toBeGreaterThan(0);
|
|
@@ -7,6 +7,19 @@ import { type IconName, ICONS } from './icons';
|
|
|
7
7
|
|
|
8
8
|
const SIZE_MAP = { xs: 16, sm: 20, md: 24, lg: 32 } as const;
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Gives the markup's root `<svg>` the SVG namespace when it has none. The web
|
|
12
|
+
* preview draws `content` as an image of the markup, and a browser draws
|
|
13
|
+
* nothing for an SVG image without the namespace. An app's own SVG often
|
|
14
|
+
* leaves it out, since an `<svg>` written in an HTML page doesn't need it.
|
|
15
|
+
*/
|
|
16
|
+
const withSvgNamespace = (markup: string): string =>
|
|
17
|
+
markup.replace(/<svg\b([^>]*)>/, (tag, attributes: string) =>
|
|
18
|
+
/\sxmlns\s*=/.test(attributes)
|
|
19
|
+
? tag
|
|
20
|
+
: `<svg xmlns="http://www.w3.org/2000/svg"${attributes}>`,
|
|
21
|
+
);
|
|
22
|
+
|
|
10
23
|
/**
|
|
11
24
|
* The icon is its host, an `<svg>` (registerHostElement, below). Angular gives
|
|
12
25
|
* a component's host every static attribute the app writes on it, so
|
|
@@ -18,6 +31,9 @@ const SIZE_MAP = { xs: 16, sm: 20, md: 24, lg: 32 } as const;
|
|
|
18
31
|
* Lynx's native <svg> element can't contain child nodes in the flat element
|
|
19
32
|
* model. Passing the full SVG string as the `content` attribute is how Lynx
|
|
20
33
|
* renders vector graphics without a DOM tree.
|
|
34
|
+
*
|
|
35
|
+
* It draws an icon from the set (`name`) or the app's own markup (`svg`),
|
|
36
|
+
* such as a sign-in provider's mark, which the set can't hold.
|
|
21
37
|
*/
|
|
22
38
|
@Component({
|
|
23
39
|
selector: 'ui-icon',
|
|
@@ -31,7 +47,15 @@ const SIZE_MAP = { xs: 16, sm: 20, md: 24, lg: 32 } as const;
|
|
|
31
47
|
template: '',
|
|
32
48
|
})
|
|
33
49
|
export class UiIcon {
|
|
34
|
-
|
|
50
|
+
/** An icon from the set (see `ICONS`). Give this or `svg`. */
|
|
51
|
+
readonly name = input<IconName | undefined>(undefined);
|
|
52
|
+
/**
|
|
53
|
+
* SVG markup to draw instead of an icon from the set. `currentColor` in it
|
|
54
|
+
* takes the icon's color, as in the set's icons, so a one-color mark
|
|
55
|
+
* follows the theme; colors written out stay, so a multi-color one keeps
|
|
56
|
+
* them. Give this or `name`.
|
|
57
|
+
*/
|
|
58
|
+
readonly svg = input<string | undefined>(undefined);
|
|
35
59
|
readonly size = input<'xs' | 'sm' | 'md' | 'lg'>('md');
|
|
36
60
|
/**
|
|
37
61
|
* A literal color or a theme token (`var(--primary)`). Without one, the icon
|
|
@@ -60,11 +84,24 @@ export class UiIcon {
|
|
|
60
84
|
// climbs on every render/interaction as each tick re-runs and re-throws.
|
|
61
85
|
// split/join is ES2019-safe and is preferred over `replace(/.../g)` here
|
|
62
86
|
// because the search term is a plain literal and needs no regex escaping.
|
|
63
|
-
return
|
|
87
|
+
return this.#markup()
|
|
64
88
|
.split('currentColor')
|
|
65
89
|
.join(this.#resolveColor(this.color()));
|
|
66
90
|
}
|
|
67
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The markup to draw. Neither input, or both, is a mistake in the template
|
|
94
|
+
* that the compiler can't catch, since neither input can be required, so
|
|
95
|
+
* it fails loudly rather than drawing an empty box or picking one.
|
|
96
|
+
*/
|
|
97
|
+
#markup(): string {
|
|
98
|
+
const name = this.name();
|
|
99
|
+
const svg = this.svg();
|
|
100
|
+
if (svg !== undefined && name === undefined) return withSvgNamespace(svg);
|
|
101
|
+
if (name !== undefined && svg === undefined) return ICONS[name];
|
|
102
|
+
throw new Error('<ui-icon> needs a name or an svg, not both.');
|
|
103
|
+
}
|
|
104
|
+
|
|
68
105
|
protected readonly sizePx = computed(() => SIZE_MAP[this.size()]);
|
|
69
106
|
|
|
70
107
|
protected readonly svgClass = computed(() => cn(this.userClass()));
|
|
@@ -91,7 +91,7 @@ import { cn } from '../../utils/cn';
|
|
|
91
91
|
[attr.disabled]="disabled() || undefined"
|
|
92
92
|
class="text-sm text-foreground"
|
|
93
93
|
style="border: none; background: transparent; height: 100%; width: 100%; position: relative; z-index: 1;"
|
|
94
|
-
(bindinput)="onInput($
|
|
94
|
+
(bindinput)="onInput($event)"
|
|
95
95
|
(bindfocus)="onFocus()"
|
|
96
96
|
(bindblur)="onBlur()"
|
|
97
97
|
/>
|
|
@@ -12,8 +12,10 @@ import {
|
|
|
12
12
|
} from '@angular/core';
|
|
13
13
|
import { ROUTES, Router } from '@angular/router';
|
|
14
14
|
import {
|
|
15
|
+
type LayoutChangeDetail,
|
|
15
16
|
LYNX_ELEMENTS,
|
|
16
17
|
LynxAccessibilitySettings,
|
|
18
|
+
type LynxEvent,
|
|
17
19
|
LynxLocationStrategy,
|
|
18
20
|
} from '@blotch/angular-lynx';
|
|
19
21
|
|
|
@@ -48,9 +50,6 @@ const ROW_PADDING: Readonly<Record<GlassDesign, number>> = {
|
|
|
48
50
|
/** The space between a centered title and the controls beside it, in px. */
|
|
49
51
|
const TITLE_GAP = 8;
|
|
50
52
|
|
|
51
|
-
/** What Lynx's `layoutchange` event carries, as far as the bar reads it. */
|
|
52
|
-
type LayoutChangeEvent = Event & { detail: { width: number } };
|
|
53
|
-
|
|
54
53
|
/**
|
|
55
54
|
* How the inline title comes in as the large title scrolls under the bar,
|
|
56
55
|
* and how Material's bar changes color as content scrolls under it.
|
|
@@ -379,20 +378,20 @@ export class UiNavigationBar {
|
|
|
379
378
|
),
|
|
380
379
|
);
|
|
381
380
|
|
|
382
|
-
protected measureRow(
|
|
383
|
-
this.rowWidth.set(
|
|
381
|
+
protected measureRow({ detail }: LynxEvent<LayoutChangeDetail>): void {
|
|
382
|
+
this.rowWidth.set(detail.width);
|
|
384
383
|
}
|
|
385
384
|
|
|
386
|
-
protected measureTitle(
|
|
387
|
-
this.titleWidth.set(
|
|
385
|
+
protected measureTitle({ detail }: LynxEvent<LayoutChangeDetail>): void {
|
|
386
|
+
this.titleWidth.set(detail.width);
|
|
388
387
|
}
|
|
389
388
|
|
|
390
|
-
protected measureLeading(
|
|
391
|
-
this.leadingWidth.set(
|
|
389
|
+
protected measureLeading({ detail }: LynxEvent<LayoutChangeDetail>): void {
|
|
390
|
+
this.leadingWidth.set(detail.width);
|
|
392
391
|
}
|
|
393
392
|
|
|
394
|
-
protected measureTrailing(
|
|
395
|
-
this.trailingWidth.set(
|
|
393
|
+
protected measureTrailing({ detail }: LynxEvent<LayoutChangeDetail>): void {
|
|
394
|
+
this.trailingWidth.set(detail.width);
|
|
396
395
|
}
|
|
397
396
|
|
|
398
397
|
/**
|
|
@@ -184,11 +184,11 @@ const iconSvg = (icon: IconName, color: string): string =>
|
|
|
184
184
|
[value]="value()"
|
|
185
185
|
[class]="inputClass()"
|
|
186
186
|
[style.font-size.px]="look().fontSize"
|
|
187
|
-
(bindinput)="onInput($
|
|
188
|
-
(bindconfirm)="onConfirm($
|
|
187
|
+
(bindinput)="onInput($event)"
|
|
188
|
+
(bindconfirm)="onConfirm($event)"
|
|
189
189
|
(bindfocus)="onFocus()"
|
|
190
190
|
(bindblur)="onBlur()"
|
|
191
|
-
(bindkeyboardheightchange)="onKeyboard($
|
|
191
|
+
(bindkeyboardheightchange)="onKeyboard($event)"
|
|
192
192
|
/>
|
|
193
193
|
@if (value() && !disabled()) {
|
|
194
194
|
<view
|
|
@@ -85,7 +85,7 @@ import { cn } from '../../utils/cn';
|
|
|
85
85
|
[attr.enable-scroll-bar]="maxLines() != null ? 'true' : null"
|
|
86
86
|
class="text-sm text-foreground"
|
|
87
87
|
style="border: none; background: transparent; height: 100%; width: 100%; line-height: 20px; position: relative; z-index: 1;"
|
|
88
|
-
(bindinput)="onInput($
|
|
88
|
+
(bindinput)="onInput($event)"
|
|
89
89
|
(bindfocus)="onFocus()"
|
|
90
90
|
(bindblur)="onBlur()"
|
|
91
91
|
></textarea>
|
|
@@ -12,7 +12,13 @@ import {
|
|
|
12
12
|
type Signal,
|
|
13
13
|
type WritableSignal,
|
|
14
14
|
} from '@angular/core';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
type LayoutChangeDetail,
|
|
17
|
+
LYNX_ELEMENTS,
|
|
18
|
+
type LynxEvent,
|
|
19
|
+
LynxSystemInfo,
|
|
20
|
+
type ScrollDetail,
|
|
21
|
+
} from '@blotch/angular-lynx';
|
|
16
22
|
|
|
17
23
|
import { cn } from '../../utils/cn';
|
|
18
24
|
import { type GlassDesign, injectGlassDesign } from '../glass/glass-design';
|
|
@@ -131,10 +137,6 @@ export const edgeFadeOverlay = (toward: 'bottom' | 'top'): string => {
|
|
|
131
137
|
return `linear-gradient(to ${toward}, ${stops.join(', ')})`;
|
|
132
138
|
};
|
|
133
139
|
|
|
134
|
-
/** What Lynx sends with `scroll` and `layoutchange`, as far as this reads. */
|
|
135
|
-
type ScrollDetail = { scrollTop: number; scrollHeight: number };
|
|
136
|
-
type LayoutDetail = { height: number };
|
|
137
|
-
|
|
138
140
|
/**
|
|
139
141
|
* The platform, as SystemInfo names it, or undefined where there's no
|
|
140
142
|
* SystemInfo to read, such as in unit tests, where LynxSystemInfo throws.
|
|
@@ -234,19 +236,19 @@ export class UiBarScroll {
|
|
|
234
236
|
this.#renderer.listen(
|
|
235
237
|
this.#element,
|
|
236
238
|
'bindscroll',
|
|
237
|
-
(event:
|
|
239
|
+
(event: LynxEvent<ScrollDetail>) => this.#scrolled(event.detail),
|
|
238
240
|
),
|
|
239
241
|
// iOS and Android report the content's height when it changes, which a
|
|
240
242
|
// frosted toolbar needs before anything scrolls.
|
|
241
243
|
this.#renderer.listen(
|
|
242
244
|
this.#element,
|
|
243
245
|
'bindcontentsizechanged',
|
|
244
|
-
(event:
|
|
246
|
+
(event: LynxEvent<ScrollDetail>) => this.#scrolled(event.detail),
|
|
245
247
|
),
|
|
246
248
|
this.#renderer.listen(
|
|
247
249
|
this.#element,
|
|
248
250
|
'bindlayoutchange',
|
|
249
|
-
(event:
|
|
251
|
+
(event: LynxEvent<LayoutChangeDetail>) => {
|
|
250
252
|
this.viewport.set(event.detail.height);
|
|
251
253
|
this.#update();
|
|
252
254
|
},
|
|
@@ -334,16 +336,12 @@ export class UiBarScroll {
|
|
|
334
336
|
return `linear-gradient(to bottom, ${stops.join(', ')})`;
|
|
335
337
|
});
|
|
336
338
|
|
|
337
|
-
protected measureTop(
|
|
338
|
-
this.topSpace.set(
|
|
339
|
-
(event as Event & { detail: LayoutDetail }).detail.height,
|
|
340
|
-
);
|
|
339
|
+
protected measureTop({ detail }: LynxEvent<LayoutChangeDetail>): void {
|
|
340
|
+
this.topSpace.set(detail.height);
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
protected measureBottom(
|
|
344
|
-
this.bottomSpace.set(
|
|
345
|
-
(event as Event & { detail: LayoutDetail }).detail.height,
|
|
346
|
-
);
|
|
343
|
+
protected measureBottom({ detail }: LynxEvent<LayoutChangeDetail>): void {
|
|
344
|
+
this.bottomSpace.set(detail.height);
|
|
347
345
|
}
|
|
348
346
|
|
|
349
347
|
#scrolled({ scrollTop, scrollHeight }: ScrollDetail): void {
|