@blotch/dolan 22.2.5 → 22.2.7
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/commands/install-packages.test.js +7 -2
- package/dist/commands/install-packages.test.js.map +1 -1
- package/dist/registry.js +8 -2
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +1 -0
- package/dist/registry.test.js.map +1 -1
- package/dist/ui/components/accordion/accordion.test.ts +24 -7
- package/dist/ui/components/accordion/accordion.ts +34 -44
- package/dist/ui/components/action-sheet/action-sheet.test.ts +18 -1
- package/dist/ui/components/action-sheet/action-sheet.ts +55 -60
- package/dist/ui/components/alert/alert.test.ts +17 -0
- package/dist/ui/components/alert/alert.ts +23 -14
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +19 -4
- package/dist/ui/components/alert-dialog/alert-dialog.ts +31 -20
- package/dist/ui/components/aspect-ratio/aspect-ratio.test.ts +38 -2
- package/dist/ui/components/aspect-ratio/aspect-ratio.ts +11 -8
- package/dist/ui/components/avatar/avatar.test.ts +7 -0
- package/dist/ui/components/avatar/avatar.ts +35 -30
- package/dist/ui/components/badge/badge.test.ts +9 -0
- package/dist/ui/components/badge/badge.ts +12 -10
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +7 -0
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +5 -0
- package/dist/ui/components/button/button.test.ts +29 -3
- package/dist/ui/components/button/button.ts +67 -62
- package/dist/ui/components/button-group/button-group.test.ts +37 -4
- package/dist/ui/components/button-group/button-group.ts +60 -51
- package/dist/ui/components/card/card.test.ts +35 -2
- package/dist/ui/components/card/card.ts +41 -53
- package/dist/ui/components/chart/chart-presets.test.ts +16 -2
- package/dist/ui/components/chart/chart-presets.ts +22 -3
- package/dist/ui/components/chart/chart.test.ts +35 -6
- package/dist/ui/components/chart/chart.ts +14 -3
- package/dist/ui/components/checkbox/checkbox.test.ts +11 -4
- package/dist/ui/components/checkbox/checkbox.ts +21 -14
- package/dist/ui/components/collapsible/collapsible.test.ts +20 -10
- package/dist/ui/components/collapsible/collapsible.ts +29 -40
- package/dist/ui/components/dialog/dialog.test.ts +17 -1
- package/dist/ui/components/dialog/dialog.ts +24 -17
- package/dist/ui/components/dock/dock.test.ts +30 -5
- package/dist/ui/components/dock/dock.ts +29 -27
- package/dist/ui/components/empty-state/empty-state.test.ts +9 -0
- package/dist/ui/components/empty-state/empty-state.ts +20 -11
- package/dist/ui/components/icon/icon.test.ts +17 -0
- package/dist/ui/components/icon/icon.ts +19 -17
- package/dist/ui/components/input/input.test.ts +55 -3
- package/dist/ui/components/input/input.ts +91 -65
- package/dist/ui/components/label/label.test.ts +15 -0
- package/dist/ui/components/label/label.ts +12 -4
- package/dist/ui/components/list/list.test.ts +13 -3
- package/dist/ui/components/list/list.ts +14 -2
- package/dist/ui/components/menu/menu.test.ts +8 -0
- package/dist/ui/components/menu/menu.ts +4 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +34 -21
- package/dist/ui/components/nav-drawer/nav-drawer.ts +60 -68
- package/dist/ui/components/navigation-bar/navigation-bar.test.ts +13 -0
- package/dist/ui/components/navigation-bar/navigation-bar.ts +5 -0
- package/dist/ui/components/pagination/pagination.test.ts +9 -0
- package/dist/ui/components/pagination/pagination.ts +52 -49
- package/dist/ui/components/popover/popover.test.ts +3 -0
- package/dist/ui/components/progress/progress.test.ts +7 -0
- package/dist/ui/components/progress/progress.ts +8 -6
- package/dist/ui/components/radio-group/radio-group.test.ts +8 -0
- package/dist/ui/components/radio-group/radio-group.ts +22 -19
- package/dist/ui/components/scroll-area/scroll-area.test.ts +14 -2
- package/dist/ui/components/scroll-area/scroll-area.ts +15 -3
- package/dist/ui/components/search-field/search-field.test.ts +25 -0
- package/dist/ui/components/search-field/search-field.ts +7 -1
- package/dist/ui/components/select/select.test.ts +32 -5
- package/dist/ui/components/select/select.ts +32 -11
- package/dist/ui/components/separator/separator.test.ts +3 -0
- package/dist/ui/components/sheet/sheet.test.ts +18 -0
- package/dist/ui/components/sheet/sheet.ts +24 -24
- package/dist/ui/components/skeleton/skeleton.test.ts +8 -0
- package/dist/ui/components/skeleton/skeleton.ts +13 -8
- package/dist/ui/components/slider/slider.test.ts +13 -1
- package/dist/ui/components/slider/slider.ts +16 -3
- package/dist/ui/components/spinner/spinner.test.ts +16 -1
- package/dist/ui/components/spinner/spinner.ts +15 -7
- package/dist/ui/components/switch/switch.test.ts +13 -2
- package/dist/ui/components/switch/switch.ts +15 -1
- package/dist/ui/components/tab-bar/tab-bar.test.ts +32 -6
- package/dist/ui/components/tab-bar/tab-bar.ts +28 -24
- package/dist/ui/components/tabs/tabs.test.ts +34 -32
- package/dist/ui/components/tabs/tabs.ts +56 -73
- package/dist/ui/components/textarea/textarea.test.ts +11 -1
- package/dist/ui/components/textarea/textarea.ts +77 -71
- package/dist/ui/components/toggle/toggle.test.ts +47 -16
- package/dist/ui/components/toggle/toggle.ts +78 -80
- package/dist/ui/components/typography/typography.test.ts +16 -0
- package/dist/ui/components/typography/typography.ts +12 -4
- package/package.json +1 -1
|
@@ -19,6 +19,16 @@ import { cn } from '../../utils/cn';
|
|
|
19
19
|
// the attribute when false, and a missing attribute means Lynx's default: on,
|
|
20
20
|
// for `bounces` and `scroll-bar-enable`. So neither could be turned off.
|
|
21
21
|
// (`[attr.x]` would send the string "false" instead.)
|
|
22
|
+
//
|
|
23
|
+
// The host is the list's box, and the <list> fills it. Angular gives a
|
|
24
|
+
// component's host every static attribute the app writes on it, so a class
|
|
25
|
+
// on <ui-list> lands on the host whatever the component does with its
|
|
26
|
+
// `class` input: on the <list> as well, a margin or padding applied twice.
|
|
27
|
+
// `grow` fills a box with a height of its own (`h-64`, `flex-1`), and a
|
|
28
|
+
// box without one takes the list's, which `height` sets: the list keeps
|
|
29
|
+
// its own height as its flex basis. Not `h-full`: a percentage height
|
|
30
|
+
// doesn't reliably resolve on a flex item in Lynx.
|
|
31
|
+
host: { '[class]': 'hostClass()' },
|
|
22
32
|
template: `
|
|
23
33
|
<list
|
|
24
34
|
[attr.list-type]="listType()"
|
|
@@ -32,7 +42,7 @@ import { cn } from '../../utils/cn';
|
|
|
32
42
|
[attr.preload-buffer-count]="preloadBufferCount()"
|
|
33
43
|
[attr.lower-threshold-item-count]="lowerThresholdItemCount()"
|
|
34
44
|
[attr.upper-threshold-item-count]="upperThresholdItemCount()"
|
|
35
|
-
|
|
45
|
+
class="grow"
|
|
36
46
|
[style]="listStyle()"
|
|
37
47
|
(bindscrolltoupper)="scrolledToStart.emit()"
|
|
38
48
|
(bindscrolltolower)="scrolledToEnd.emit()"
|
|
@@ -61,7 +71,9 @@ export class UiList {
|
|
|
61
71
|
readonly scrolledToStart = output<void>();
|
|
62
72
|
readonly scrolledToEnd = output<void>();
|
|
63
73
|
|
|
64
|
-
protected readonly
|
|
74
|
+
protected readonly hostClass = computed(() =>
|
|
75
|
+
cn('w-full flex-col flex', this.userClass()),
|
|
76
|
+
);
|
|
65
77
|
|
|
66
78
|
// Explicit height is required for list to scroll in Lynx. The gaps go here
|
|
67
79
|
// too: they're CSS properties that Lynx's list reads from its style, and as
|
|
@@ -490,6 +490,14 @@ describe('UiMenuSection and UiMenuSeparator', () => {
|
|
|
490
490
|
expect(rendered.menu.getAllByRole('heading')).toHaveLength(1);
|
|
491
491
|
});
|
|
492
492
|
|
|
493
|
+
// Angular gives a component's host every static attribute written on it
|
|
494
|
+
// (issue #181): the title was also the host's tooltip in the web preview.
|
|
495
|
+
it("doesn't give its title to its host as well", async () => {
|
|
496
|
+
const rendered = await renderOpenMenu();
|
|
497
|
+
|
|
498
|
+
expect(rendered.container.querySelectorAll('[title]')).toHaveLength(0);
|
|
499
|
+
});
|
|
500
|
+
|
|
493
501
|
it('indents a Material section title as Material does', async () => {
|
|
494
502
|
const rendered = await renderOpenMenu(null);
|
|
495
503
|
|
|
@@ -231,6 +231,10 @@ export class UiMenuItem {
|
|
|
231
231
|
selector: 'ui-menu-section',
|
|
232
232
|
imports: [LYNX_ELEMENTS],
|
|
233
233
|
encapsulation: ViewEncapsulation.None,
|
|
234
|
+
// The title is the section's text. Angular also gives the host every static
|
|
235
|
+
// attribute the app writes on it, and a `title` on the host is a tooltip in
|
|
236
|
+
// the web preview and a second match for getByTitle, so the host drops it.
|
|
237
|
+
host: { '[attr.title]': 'null' },
|
|
234
238
|
template: `
|
|
235
239
|
<view class="flex-col flex">
|
|
236
240
|
@if (title()) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Component, signal, viewChild } from '@angular/core';
|
|
1
|
+
import { Component, ElementRef, signal, viewChild } from '@angular/core';
|
|
2
2
|
import {
|
|
3
3
|
fireEvent,
|
|
4
4
|
render,
|
|
@@ -100,7 +100,8 @@ class NavDrawerHost {
|
|
|
100
100
|
readonly closes = signal(0);
|
|
101
101
|
readonly presses = signal(0);
|
|
102
102
|
readonly drawer = viewChild.required(UiNavDrawer);
|
|
103
|
-
|
|
103
|
+
// The settings item's element: its host.
|
|
104
|
+
readonly settings = viewChild.required('settings', { read: ElementRef });
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
const renderDrawer = async () => {
|
|
@@ -116,7 +117,7 @@ const renderDrawer = async () => {
|
|
|
116
117
|
backdrop: () => panel().parentElement!,
|
|
117
118
|
scrollView: () => container.querySelector('scroll-view')!,
|
|
118
119
|
trigger: () => container.querySelector('[accessibility-element]')!,
|
|
119
|
-
// An item
|
|
120
|
+
// An item, its host: the view that holds its projected label.
|
|
120
121
|
item: (label: string) => getByText(label).parentElement!,
|
|
121
122
|
svg: (selector: string) => container.querySelector(`svg${selector}`)!,
|
|
122
123
|
};
|
|
@@ -174,11 +175,12 @@ describe('UiNavDrawer', () => {
|
|
|
174
175
|
expect(panel().classList).toContain('bg-background');
|
|
175
176
|
expect(panel().classList).toContain('shadow-lg');
|
|
176
177
|
expect(scrollView().classList).toContain('h-full');
|
|
177
|
-
|
|
178
|
+
// The content's class is on its host, around the scroll view.
|
|
179
|
+
expect(scrollView().parentElement!.classList).toContain('flex-1');
|
|
180
|
+
expect(scrollView().parentElement!.classList).toContain('pt-2');
|
|
178
181
|
expect(item('Delete account').classList).toContain('mt-4');
|
|
179
182
|
expect(trigger().classList).toContain('h-10');
|
|
180
183
|
expect(trigger().classList).toContain('ml-2');
|
|
181
|
-
// Each part's own view, not its host element, which keeps the bare class.
|
|
182
184
|
const header = container.querySelector('.border-b');
|
|
183
185
|
expect(header?.classList).toContain('gap-3');
|
|
184
186
|
expect(header?.classList).not.toContain('gap-1.5');
|
|
@@ -187,6 +189,29 @@ describe('UiNavDrawer', () => {
|
|
|
187
189
|
expect(footer?.classList).not.toContain('gap-2');
|
|
188
190
|
});
|
|
189
191
|
|
|
192
|
+
// Angular gives a component's host every static attribute written on it
|
|
193
|
+
// (issue #181), so each class also styled a host: the drawer's in the
|
|
194
|
+
// page, where it is written, and each part's around its own view.
|
|
195
|
+
it('puts each class on one element: the panel, or the part', async () => {
|
|
196
|
+
const { container, getByText, panel, scrollView, item, trigger } =
|
|
197
|
+
await renderDrawer();
|
|
198
|
+
expect([...container.querySelectorAll('.shadow-lg')]).toEqual([panel()]);
|
|
199
|
+
expect([...container.querySelectorAll('.pt-2')]).toEqual([
|
|
200
|
+
scrollView().parentElement,
|
|
201
|
+
]);
|
|
202
|
+
expect([...container.querySelectorAll('.mt-4')]).toEqual([
|
|
203
|
+
item('Delete account'),
|
|
204
|
+
]);
|
|
205
|
+
expect([...container.querySelectorAll('.ml-2')]).toEqual([trigger()]);
|
|
206
|
+
// The header's label sits right in it: no view between them. (Items
|
|
207
|
+
// have a gap-3 of their own.)
|
|
208
|
+
expect(getByText('Menu').parentElement!.classList).toContain('gap-3');
|
|
209
|
+
expect(getByText('Menu').parentElement!.classList).toContain('border-b');
|
|
210
|
+
expect([...container.querySelectorAll('.gap-4')]).toEqual([
|
|
211
|
+
getByText('Version 1').parentElement,
|
|
212
|
+
]);
|
|
213
|
+
});
|
|
214
|
+
|
|
190
215
|
it('stays hidden until it is opened', async () => {
|
|
191
216
|
const { host, overlay, panel } = await renderDrawer();
|
|
192
217
|
await after(CLOSED);
|
|
@@ -354,10 +379,7 @@ describe('UiNavDrawer', () => {
|
|
|
354
379
|
describe('UiNavDrawerContent', () => {
|
|
355
380
|
it('scrolls the active item into view when the drawer opens', async () => {
|
|
356
381
|
const { host } = await renderDrawer();
|
|
357
|
-
const scroll = vi.spyOn(
|
|
358
|
-
host.settings().containerRef()!.nativeElement,
|
|
359
|
-
'invoke',
|
|
360
|
-
);
|
|
382
|
+
const scroll = vi.spyOn(host.settings().nativeElement, 'invoke');
|
|
361
383
|
host.open.set(true);
|
|
362
384
|
await after(OPENED);
|
|
363
385
|
// Instantly and centered, so the drawer slides in already scrolled.
|
|
@@ -371,20 +393,14 @@ describe('UiNavDrawerContent', () => {
|
|
|
371
393
|
const { host, scrollView } = await renderDrawer();
|
|
372
394
|
host.open.set(true);
|
|
373
395
|
await after(OPENED);
|
|
374
|
-
const scroll = vi.spyOn(
|
|
375
|
-
host.settings().containerRef()!.nativeElement,
|
|
376
|
-
'invoke',
|
|
377
|
-
);
|
|
396
|
+
const scroll = vi.spyOn(host.settings().nativeElement, 'invoke');
|
|
378
397
|
fireEvent.contentsizechanged(scrollView());
|
|
379
398
|
expect(scroll).toHaveBeenCalledWith('scrollIntoView', expect.any(Object));
|
|
380
399
|
});
|
|
381
400
|
|
|
382
401
|
it("doesn't scroll while the drawer is closed", async () => {
|
|
383
402
|
const { host, scrollView } = await renderDrawer();
|
|
384
|
-
const scroll = vi.spyOn(
|
|
385
|
-
host.settings().containerRef()!.nativeElement,
|
|
386
|
-
'invoke',
|
|
387
|
-
);
|
|
403
|
+
const scroll = vi.spyOn(host.settings().nativeElement, 'invoke');
|
|
388
404
|
// Content collapsing as the overlay hides relays out too.
|
|
389
405
|
fireEvent.contentsizechanged(scrollView());
|
|
390
406
|
await after(OPENED);
|
|
@@ -394,10 +410,7 @@ describe('UiNavDrawerContent', () => {
|
|
|
394
410
|
it("doesn't scroll when no item is active", async () => {
|
|
395
411
|
const { host } = await renderDrawer();
|
|
396
412
|
host.active.set('');
|
|
397
|
-
const scroll = vi.spyOn(
|
|
398
|
-
host.settings().containerRef()!.nativeElement,
|
|
399
|
-
'invoke',
|
|
400
|
-
);
|
|
413
|
+
const scroll = vi.spyOn(host.settings().nativeElement, 'invoke');
|
|
401
414
|
host.open.set(true);
|
|
402
415
|
await after(OPENED);
|
|
403
416
|
expect(scroll).not.toHaveBeenCalled();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type ElementRef,
|
|
3
2
|
type OnDestroy,
|
|
4
3
|
Component,
|
|
4
|
+
ElementRef,
|
|
5
5
|
ViewEncapsulation,
|
|
6
6
|
booleanAttribute,
|
|
7
7
|
computed,
|
|
@@ -36,6 +36,14 @@ import { UI_ICON_COLOR } from '../icon/icon-theme';
|
|
|
36
36
|
selector: 'ui-nav-drawer',
|
|
37
37
|
imports: [LYNX_ELEMENTS],
|
|
38
38
|
encapsulation: ViewEncapsulation.None,
|
|
39
|
+
// The drawer's class is its panel's, inside the overlay. Angular also gives
|
|
40
|
+
// the host every static attribute the app writes on <ui-nav-drawer>, and
|
|
41
|
+
// the host sits in the page's layout where the drawer is written, so a
|
|
42
|
+
// `class="p-8"` padded the page there too. The host drops it. The parts
|
|
43
|
+
// below are their host elements for the same reason: a class on a part
|
|
44
|
+
// lands on its host anyway, so a view inside it styled with the same class
|
|
45
|
+
// applied it twice.
|
|
46
|
+
host: { '[attr.class]': 'null' },
|
|
39
47
|
template: `
|
|
40
48
|
<overlay [attr.visible]="overlayVisible()" [style]="overlayStyle()">
|
|
41
49
|
<!-- bg-black/50 dims the content behind the drawer. The fadeIn/fadeOut in
|
|
@@ -238,13 +246,9 @@ export class UiNavDrawer implements OnDestroy {
|
|
|
238
246
|
|
|
239
247
|
@Component({
|
|
240
248
|
selector: 'ui-nav-drawer-header',
|
|
241
|
-
imports: [LYNX_ELEMENTS],
|
|
242
249
|
encapsulation: ViewEncapsulation.None,
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
<ng-content />
|
|
246
|
-
</view>
|
|
247
|
-
`,
|
|
250
|
+
host: { '[class]': 'containerClass()' },
|
|
251
|
+
template: '<ng-content />',
|
|
248
252
|
})
|
|
249
253
|
export class UiNavDrawerHeader {
|
|
250
254
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
@@ -299,9 +303,11 @@ const navDrawerItem = () => UiNavDrawerItem;
|
|
|
299
303
|
// makes the host lay the scroll-view out vertically to fill that height. The
|
|
300
304
|
// panel's flex-col stretches the host to full width by default (Lynx
|
|
301
305
|
// align-items: stretch), so no width class is needed on the host.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
306
|
+
//
|
|
307
|
+
// The app's class goes on the host too, rather than on the scroll-view:
|
|
308
|
+
// Angular gives the host every static attribute written on it anyway, so
|
|
309
|
+
// with the scroll-view styled as well, a padding applied twice.
|
|
310
|
+
host: { '[class]': 'hostClass()' },
|
|
305
311
|
// With the host now a definite height, the scroll-view fills it with `h-full`
|
|
306
312
|
// (the explicit height Lynx requires to actually scroll) and `w-full` to span
|
|
307
313
|
// the host width. This is the bound the scroll-view was missing above.
|
|
@@ -313,7 +319,7 @@ const navDrawerItem = () => UiNavDrawerItem;
|
|
|
313
319
|
template: `
|
|
314
320
|
<scroll-view
|
|
315
321
|
scroll-orientation="vertical"
|
|
316
|
-
|
|
322
|
+
class="h-full w-full"
|
|
317
323
|
(bindcontentsizechanged)="onContentLayout()"
|
|
318
324
|
>
|
|
319
325
|
<ng-content />
|
|
@@ -330,8 +336,8 @@ export class UiNavDrawerContent {
|
|
|
330
336
|
// nested inside the @for group wrappers, not just the direct content children.
|
|
331
337
|
readonly items = contentChildren(navDrawerItem(), { descendants: true });
|
|
332
338
|
|
|
333
|
-
protected readonly
|
|
334
|
-
cn('
|
|
339
|
+
protected readonly hostClass = computed(() =>
|
|
340
|
+
cn('flex-1 flex-col flex', this.userClass()),
|
|
335
341
|
);
|
|
336
342
|
|
|
337
343
|
/**
|
|
@@ -383,26 +389,21 @@ const itemIconColor = () => {
|
|
|
383
389
|
|
|
384
390
|
@Component({
|
|
385
391
|
selector: 'ui-nav-drawer-item',
|
|
386
|
-
imports: [LYNX_ELEMENTS],
|
|
387
392
|
encapsulation: ViewEncapsulation.None,
|
|
388
393
|
// Icons projected into the item (see the variant below) inject this through
|
|
389
394
|
// the item's element injector, which content projection keeps them under.
|
|
390
395
|
providers: [{ provide: UI_ICON_COLOR, useFactory: itemIconColor }],
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
>
|
|
403
|
-
<ng-content />
|
|
404
|
-
</view>
|
|
405
|
-
`,
|
|
396
|
+
host: {
|
|
397
|
+
'[class]': 'containerClass()',
|
|
398
|
+
'(bindtouchstart)': 'onPressStart()',
|
|
399
|
+
'(bindtouchend)': 'onPressEnd()',
|
|
400
|
+
'(bindtouchcancel)': 'onPressCancel()',
|
|
401
|
+
'(bindmousedown)': 'onPressStart()',
|
|
402
|
+
'(bindmouseup)': 'onPressEnd()',
|
|
403
|
+
'(bindmouseleave)': 'onPressCancel()',
|
|
404
|
+
'(bindtap)': 'onTap()',
|
|
405
|
+
},
|
|
406
|
+
template: '<ng-content />',
|
|
406
407
|
})
|
|
407
408
|
export class UiNavDrawerItem {
|
|
408
409
|
readonly #drawer = inject(UiNavDrawer);
|
|
@@ -414,7 +415,7 @@ export class UiNavDrawerItem {
|
|
|
414
415
|
|
|
415
416
|
readonly pressed = output<void>();
|
|
416
417
|
|
|
417
|
-
readonly
|
|
418
|
+
readonly #host = inject(ElementRef).nativeElement;
|
|
418
419
|
#pressAnim?: AnimationHandle;
|
|
419
420
|
|
|
420
421
|
// `destructive` recolors the item's content, which the item doesn't render
|
|
@@ -435,20 +436,17 @@ export class UiNavDrawerItem {
|
|
|
435
436
|
|
|
436
437
|
protected onPressStart(): void {
|
|
437
438
|
this.#pressAnim?.cancel();
|
|
438
|
-
this.#pressAnim = pressDown(
|
|
439
|
-
this.containerRef()?.nativeElement,
|
|
440
|
-
SCALE.pressDownLight,
|
|
441
|
-
);
|
|
439
|
+
this.#pressAnim = pressDown(this.#host, SCALE.pressDownLight);
|
|
442
440
|
}
|
|
443
441
|
|
|
444
442
|
protected onPressEnd(): void {
|
|
445
443
|
this.#pressAnim?.cancel();
|
|
446
|
-
this.#pressAnim = pressRelease(this
|
|
444
|
+
this.#pressAnim = pressRelease(this.#host);
|
|
447
445
|
}
|
|
448
446
|
|
|
449
447
|
protected onPressCancel(): void {
|
|
450
448
|
this.#pressAnim?.cancel();
|
|
451
|
-
this.#pressAnim = pressRelease(this
|
|
449
|
+
this.#pressAnim = pressRelease(this.#host);
|
|
452
450
|
}
|
|
453
451
|
|
|
454
452
|
protected onTap(): void {
|
|
@@ -478,7 +476,7 @@ export class UiNavDrawerItem {
|
|
|
478
476
|
// - Native Lynx (iOS/Android) reads params.scrollIntoViewOptions.
|
|
479
477
|
// - Web maps invoke() onto the DOM element.scrollIntoView(params), which
|
|
480
478
|
// reads top-level ScrollIntoViewOptions (block/behavior).
|
|
481
|
-
this.
|
|
479
|
+
this.#host.invoke('scrollIntoView', {
|
|
482
480
|
scrollIntoViewOptions: { block: 'center', behavior: 'auto' },
|
|
483
481
|
block: 'center',
|
|
484
482
|
behavior: 'auto',
|
|
@@ -488,13 +486,9 @@ export class UiNavDrawerItem {
|
|
|
488
486
|
|
|
489
487
|
@Component({
|
|
490
488
|
selector: 'ui-nav-drawer-footer',
|
|
491
|
-
imports: [LYNX_ELEMENTS],
|
|
492
489
|
encapsulation: ViewEncapsulation.None,
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
<ng-content />
|
|
496
|
-
</view>
|
|
497
|
-
`,
|
|
490
|
+
host: { '[class]': 'containerClass()' },
|
|
491
|
+
template: '<ng-content />',
|
|
498
492
|
})
|
|
499
493
|
export class UiNavDrawerFooter {
|
|
500
494
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
@@ -515,28 +509,26 @@ export class UiNavDrawerFooter {
|
|
|
515
509
|
|
|
516
510
|
@Component({
|
|
517
511
|
selector: 'ui-nav-drawer-trigger',
|
|
518
|
-
imports: [
|
|
512
|
+
imports: [UiIcon],
|
|
519
513
|
encapsulation: ViewEncapsulation.None,
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
</view>
|
|
539
|
-
`,
|
|
514
|
+
// The trigger is only an icon, so it carries the words a screen reader
|
|
515
|
+
// says for it. The label is an attribute binding: a property binding on a
|
|
516
|
+
// component's host is checked against the DOM schema, which has no
|
|
517
|
+
// `accessibility-label`.
|
|
518
|
+
host: {
|
|
519
|
+
'[class]': 'containerClass()',
|
|
520
|
+
'accessibility-element': 'true',
|
|
521
|
+
'accessibility-traits': 'button',
|
|
522
|
+
'[attr.accessibility-label]': 'label()',
|
|
523
|
+
'(bindtouchstart)': 'onPressStart()',
|
|
524
|
+
'(bindtouchend)': 'onPressEnd()',
|
|
525
|
+
'(bindtouchcancel)': 'onPressCancel()',
|
|
526
|
+
'(bindmousedown)': 'onPressStart()',
|
|
527
|
+
'(bindmouseup)': 'onPressEnd()',
|
|
528
|
+
'(bindmouseleave)': 'onPressCancel()',
|
|
529
|
+
'(bindtap)': 'onTap()',
|
|
530
|
+
},
|
|
531
|
+
template: '<ui-icon name="menu" size="md" />',
|
|
540
532
|
})
|
|
541
533
|
export class UiNavDrawerTrigger {
|
|
542
534
|
/** What a screen reader says for the trigger. */
|
|
@@ -545,7 +537,7 @@ export class UiNavDrawerTrigger {
|
|
|
545
537
|
|
|
546
538
|
readonly pressed = output<void>();
|
|
547
539
|
|
|
548
|
-
readonly
|
|
540
|
+
readonly #host = inject(ElementRef).nativeElement;
|
|
549
541
|
#pressAnim?: AnimationHandle;
|
|
550
542
|
|
|
551
543
|
protected readonly containerClass = computed(() =>
|
|
@@ -557,17 +549,17 @@ export class UiNavDrawerTrigger {
|
|
|
557
549
|
|
|
558
550
|
protected onPressStart(): void {
|
|
559
551
|
this.#pressAnim?.cancel();
|
|
560
|
-
this.#pressAnim = pressDown(this
|
|
552
|
+
this.#pressAnim = pressDown(this.#host);
|
|
561
553
|
}
|
|
562
554
|
|
|
563
555
|
protected onPressEnd(): void {
|
|
564
556
|
this.#pressAnim?.cancel();
|
|
565
|
-
this.#pressAnim = pressRelease(this
|
|
557
|
+
this.#pressAnim = pressRelease(this.#host);
|
|
566
558
|
}
|
|
567
559
|
|
|
568
560
|
protected onPressCancel(): void {
|
|
569
561
|
this.#pressAnim?.cancel();
|
|
570
|
-
this.#pressAnim = pressRelease(this
|
|
562
|
+
this.#pressAnim = pressRelease(this.#host);
|
|
571
563
|
}
|
|
572
564
|
|
|
573
565
|
protected onTap(): void {
|
|
@@ -415,6 +415,19 @@ describe('UiNavigationBar without the Lynx router', () => {
|
|
|
415
415
|
expect(result.getByRole('button', { name: 'Back' })).toBeTruthy();
|
|
416
416
|
});
|
|
417
417
|
|
|
418
|
+
// Angular gives a component's host every static attribute written on it:
|
|
419
|
+
// the title was a tooltip over the bar in the web preview (issue #181).
|
|
420
|
+
it('shows a title written as an attribute only in the bar', async () => {
|
|
421
|
+
const result = await render(PlainBarHost, {
|
|
422
|
+
providers: [
|
|
423
|
+
{ provide: LynxSystemInfo, useValue: IOS_26 },
|
|
424
|
+
{ provide: LynxAccessibilitySettings, useValue: settings },
|
|
425
|
+
],
|
|
426
|
+
});
|
|
427
|
+
expect(result.container.querySelectorAll('[title]')).toHaveLength(0);
|
|
428
|
+
expect(result.getByRole('heading', { name: 'Settings' })).toBeTruthy();
|
|
429
|
+
});
|
|
430
|
+
|
|
418
431
|
it('has no Back button with a LocationStrategy of the app’s own', async () => {
|
|
419
432
|
const { back, host } = await renderBar(IOS_26, [
|
|
420
433
|
{ provide: LocationStrategy, useClass: MockLocationStrategy },
|
|
@@ -118,8 +118,13 @@ const historyOf = (injector: Injector): LynxLocationStrategy | null => {
|
|
|
118
118
|
selector: 'ui-navigation-bar',
|
|
119
119
|
imports: [LYNX_ELEMENTS, UiGlass, UiToolbarItem],
|
|
120
120
|
encapsulation: ViewEncapsulation.None,
|
|
121
|
+
// Angular gives a component's host every static attribute the app writes
|
|
122
|
+
// on it, so a `title="Inbox"` lands on the host as well as in the title's
|
|
123
|
+
// text, where a browser shows it as a tooltip over the bar in the web
|
|
124
|
+
// preview, and getByTitle finds it. The host drops it.
|
|
121
125
|
host: {
|
|
122
126
|
'[class]': 'hostClass()',
|
|
127
|
+
'[attr.title]': 'null',
|
|
123
128
|
'[style.transition]': 'containerTransition()',
|
|
124
129
|
'[style.background-image]': 'edgeFade()',
|
|
125
130
|
},
|
|
@@ -91,6 +91,15 @@ describe('UiPagination', () => {
|
|
|
91
91
|
expect(container.querySelector('.gap-1')?.classList).toContain('mt-4');
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
// Angular gives a component's host every static attribute written on it,
|
|
95
|
+
// so a row inside the host as well applied the class twice (issue #181).
|
|
96
|
+
it('puts its class on one element, the row', async () => {
|
|
97
|
+
const { container } = await renderPagination(7, 3);
|
|
98
|
+
expect([...container.querySelectorAll('.mt-4')]).toEqual([
|
|
99
|
+
container.querySelector('.gap-1'),
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
94
103
|
it('leaves a gap before the last page near the start', async () => {
|
|
95
104
|
const { container } = await renderPagination(10, 2);
|
|
96
105
|
expect(shown(container)).toEqual(['1', '2', '3', '...', '10']);
|
|
@@ -31,58 +31,61 @@ const defaultPageLabel = (page: number, current: boolean): string =>
|
|
|
31
31
|
// The chevrons are drawn in the theme's foreground, like the page numbers
|
|
32
32
|
// next to them. An SVG can't read the text color classes, so the color is
|
|
33
33
|
// resolved and written into the markup (see icon-theme.ts).
|
|
34
|
+
//
|
|
35
|
+
// The host is the row, styled through `host` rather than as a view inside
|
|
36
|
+
// it: Angular gives a component's host every static attribute the app
|
|
37
|
+
// writes on it, so a class on <ui-pagination> lands on the host whatever
|
|
38
|
+
// the component does with its `class` input, and a row inside the host
|
|
39
|
+
// applied it twice.
|
|
40
|
+
host: { '[class]': 'containerClass()' },
|
|
34
41
|
template: `
|
|
35
|
-
<view
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
>
|
|
69
|
-
<text class="text-xs text-muted-foreground">...</text>
|
|
70
|
-
</view>
|
|
71
|
-
}
|
|
42
|
+
<view
|
|
43
|
+
[class]="navButtonClass(page() <= 1)"
|
|
44
|
+
[accessibility-element]="true"
|
|
45
|
+
accessibility-traits="button"
|
|
46
|
+
[accessibility-label]="previousLabel()"
|
|
47
|
+
(bindtap)="prev()"
|
|
48
|
+
>
|
|
49
|
+
<svg
|
|
50
|
+
[attr.content]="chevronLeftSvg()"
|
|
51
|
+
style="width: 16px; height: 16px;"
|
|
52
|
+
/>
|
|
53
|
+
</view>
|
|
54
|
+
|
|
55
|
+
@for (item of visiblePages(); track item.key) {
|
|
56
|
+
@if (item.type === 'page') {
|
|
57
|
+
<view
|
|
58
|
+
[class]="pageButtonClass(item.value === page())"
|
|
59
|
+
[accessibility-element]="true"
|
|
60
|
+
accessibility-traits="button"
|
|
61
|
+
[accessibility-label]="pageLabel()(item.value, item.value === page())"
|
|
62
|
+
(bindtap)="goTo(item.value)"
|
|
63
|
+
>
|
|
64
|
+
<text [class]="pageTextClass(item.value === page())">{{
|
|
65
|
+
item.value
|
|
66
|
+
}}</text>
|
|
67
|
+
</view>
|
|
68
|
+
} @else {
|
|
69
|
+
<view
|
|
70
|
+
class="h-8 w-8 items-center flex justify-center"
|
|
71
|
+
[accessibility-elements-hidden]="true"
|
|
72
|
+
>
|
|
73
|
+
<text class="text-xs text-muted-foreground">...</text>
|
|
74
|
+
</view>
|
|
72
75
|
}
|
|
76
|
+
}
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
</view>
|
|
78
|
+
<view
|
|
79
|
+
[class]="navButtonClass(page() >= totalPages())"
|
|
80
|
+
[accessibility-element]="true"
|
|
81
|
+
accessibility-traits="button"
|
|
82
|
+
[accessibility-label]="nextLabel()"
|
|
83
|
+
(bindtap)="next()"
|
|
84
|
+
>
|
|
85
|
+
<svg
|
|
86
|
+
[attr.content]="chevronRightSvg()"
|
|
87
|
+
style="width: 16px; height: 16px;"
|
|
88
|
+
/>
|
|
86
89
|
</view>
|
|
87
90
|
`,
|
|
88
91
|
})
|
|
@@ -282,5 +282,8 @@ describe('UiPopoverContent', () => {
|
|
|
282
282
|
expect(content.classList).toContain('w-60');
|
|
283
283
|
expect(content.classList).toContain('p-3');
|
|
284
284
|
expect(content.classList).not.toContain('p-4');
|
|
285
|
+
// Angular gives a component's host every static attribute written on it
|
|
286
|
+
// (issue #181): the content is its host, so the class is on one element.
|
|
287
|
+
expect([...rendered.container.querySelectorAll('.p-3')]).toEqual([content]);
|
|
285
288
|
});
|
|
286
289
|
});
|
|
@@ -53,6 +53,13 @@ describe('UiProgress', () => {
|
|
|
53
53
|
});
|
|
54
54
|
afterEach(() => animate.mockRestore());
|
|
55
55
|
|
|
56
|
+
// A class on a component lands on its host too (issue #181): the host is
|
|
57
|
+
// the track, so its height is set once.
|
|
58
|
+
it('puts its class on one element, the track', async () => {
|
|
59
|
+
const { container, track } = await renderProgress();
|
|
60
|
+
expect([...container.querySelectorAll('.h-3')]).toEqual([track()]);
|
|
61
|
+
});
|
|
62
|
+
|
|
56
63
|
it('fills the track by the value out of max', async () => {
|
|
57
64
|
const { fill, track } = await renderProgress();
|
|
58
65
|
expect(fill().getAttribute('style')).toContain('width: 40%');
|
|
@@ -13,16 +13,18 @@ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
|
13
13
|
import { type AnimationHandle, DURATION, EASING } from '../../utils/animate';
|
|
14
14
|
import { cn } from '../../utils/cn';
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* The track is the host, styled through `host`, with the fill inside.
|
|
18
|
+
* Angular gives a component's host every static attribute the app writes on
|
|
19
|
+
* it, so a class on <ui-progress> lands on the host whatever the component
|
|
20
|
+
* does: with a styled track inside as well, a margin applied twice.
|
|
21
|
+
*/
|
|
16
22
|
@Component({
|
|
17
23
|
selector: 'ui-progress',
|
|
18
|
-
standalone: true,
|
|
19
24
|
imports: [LYNX_ELEMENTS],
|
|
20
25
|
encapsulation: ViewEncapsulation.None,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<view #fill [class]="fillClass()" [style]="fillStyle()" />
|
|
24
|
-
</view>
|
|
25
|
-
`,
|
|
26
|
+
host: { '[class]': 'trackClass()' },
|
|
27
|
+
template: `<view #fill [class]="fillClass()" [style]="fillStyle()" />`,
|
|
26
28
|
})
|
|
27
29
|
export class UiProgress {
|
|
28
30
|
readonly value = input(0);
|
|
@@ -78,6 +78,14 @@ describe('UiRadioGroup', () => {
|
|
|
78
78
|
expect(item('Banana').classList).toContain('h-11');
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
// A class on a component lands on its host too (issue #181): the group and
|
|
82
|
+
// each item are their hosts, so each class applies once.
|
|
83
|
+
it('puts each class on one element', async () => {
|
|
84
|
+
const { container, item } = await renderGroup();
|
|
85
|
+
expect(container.querySelectorAll('.mt-2')).toHaveLength(1);
|
|
86
|
+
expect([...container.querySelectorAll('.pl-1')]).toEqual([item('Apple')]);
|
|
87
|
+
});
|
|
88
|
+
|
|
81
89
|
it('shows the dot of the selected item only', async () => {
|
|
82
90
|
const { dot } = await renderGroup();
|
|
83
91
|
expect(dot('Apple').getAttribute('style')).toContain('opacity: 1');
|