@blotch/dolan 22.2.6 → 22.2.8
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 +59 -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 +134 -2
- package/dist/ui/components/icon/icon.ts +57 -18
- 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
|
@@ -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');
|
|
@@ -16,16 +16,18 @@ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
|
16
16
|
import { type AnimationHandle, popIn, popOut } from '../../utils/animate';
|
|
17
17
|
import { cn } from '../../utils/cn';
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The group and each item are their host elements, styled through `host`.
|
|
21
|
+
* Angular gives a component's host every static attribute the app writes on
|
|
22
|
+
* it, so a class on <ui-radio-group> or <ui-radio-group-item> lands on the
|
|
23
|
+
* host whatever the component does: with a styled view inside as well, a gap
|
|
24
|
+
* or padding applied twice.
|
|
25
|
+
*/
|
|
19
26
|
@Component({
|
|
20
27
|
selector: 'ui-radio-group',
|
|
21
|
-
standalone: true,
|
|
22
|
-
imports: [LYNX_ELEMENTS],
|
|
23
28
|
encapsulation: ViewEncapsulation.None,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<ng-content />
|
|
27
|
-
</view>
|
|
28
|
-
`,
|
|
29
|
+
host: { '[class]': 'containerClass()' },
|
|
30
|
+
template: '<ng-content />',
|
|
29
31
|
})
|
|
30
32
|
export class UiRadioGroup {
|
|
31
33
|
readonly value = model<string>('');
|
|
@@ -47,22 +49,23 @@ export class UiRadioGroup {
|
|
|
47
49
|
|
|
48
50
|
@Component({
|
|
49
51
|
selector: 'ui-radio-group-item',
|
|
50
|
-
standalone: true,
|
|
51
52
|
imports: [LYNX_ELEMENTS],
|
|
52
53
|
encapsulation: ViewEncapsulation.None,
|
|
54
|
+
host: {
|
|
55
|
+
'[class]': 'itemClass()',
|
|
56
|
+
'(bindtap)': 'select()',
|
|
57
|
+
},
|
|
53
58
|
template: `
|
|
54
|
-
<view [class]="
|
|
55
|
-
<view
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
/>
|
|
63
|
-
</view>
|
|
64
|
-
<text [class]="labelClass()"><ng-content /></text>
|
|
59
|
+
<view [class]="circleClass()">
|
|
60
|
+
<view
|
|
61
|
+
#dot
|
|
62
|
+
[class]="dotClass()"
|
|
63
|
+
[style]="
|
|
64
|
+
isSelected() ? 'opacity: 1;' : 'opacity: 0; transform: scale(0.6);'
|
|
65
|
+
"
|
|
66
|
+
/>
|
|
65
67
|
</view>
|
|
68
|
+
<text [class]="labelClass()"><ng-content /></text>
|
|
66
69
|
`,
|
|
67
70
|
})
|
|
68
71
|
export class UiRadioGroupItem {
|
|
@@ -42,6 +42,8 @@ const renderScrollArea = async () => {
|
|
|
42
42
|
...result,
|
|
43
43
|
host: result.componentRef.instance as ScrollAreaHost,
|
|
44
44
|
scrollView: () => result.container.querySelector('scroll-view')!,
|
|
45
|
+
/** The scroll area's box, its host, which the scroll view fills. */
|
|
46
|
+
box: () => result.container.querySelector('scroll-view')!.parentElement!,
|
|
45
47
|
/** The view inside the scroll view that lays out the content. */
|
|
46
48
|
content: () => result.getByText('Body').parentElement!,
|
|
47
49
|
};
|
|
@@ -61,13 +63,23 @@ const watchAttributes = () => {
|
|
|
61
63
|
|
|
62
64
|
describe('UiScrollArea', () => {
|
|
63
65
|
it('renders projected content in a vertical scroll view', async () => {
|
|
64
|
-
const { scrollView, content } = await renderScrollArea();
|
|
66
|
+
const { scrollView, box, content } = await renderScrollArea();
|
|
65
67
|
expect(scrollView().getAttribute('scroll-orientation')).toBe('vertical');
|
|
66
|
-
expect(scrollView().classList).toContain('
|
|
68
|
+
expect(scrollView().classList).toContain('grow');
|
|
69
|
+
expect(box().classList).toContain('w-full');
|
|
70
|
+
expect(box().classList).toContain('bg-card');
|
|
67
71
|
expect(content().classList).toContain('flex-col');
|
|
68
72
|
expect(content().classList).toContain('gap-2');
|
|
69
73
|
});
|
|
70
74
|
|
|
75
|
+
// Angular gives a component's host every static attribute written on it,
|
|
76
|
+
// so the scroll view taking the class as well applied it twice (issue
|
|
77
|
+
// #181).
|
|
78
|
+
it('puts its class on one element, its box', async () => {
|
|
79
|
+
const { container, box } = await renderScrollArea();
|
|
80
|
+
expect([...container.querySelectorAll('.bg-card')]).toEqual([box()]);
|
|
81
|
+
});
|
|
82
|
+
|
|
71
83
|
it('gives the scroll view an explicit height, which it needs to scroll', async () => {
|
|
72
84
|
const { host, scrollView } = await renderScrollArea();
|
|
73
85
|
expect(scrollView().getAttribute('style') ?? '').not.toContain('height');
|
|
@@ -19,6 +19,18 @@ import { cn } from '../../utils/cn';
|
|
|
19
19
|
// removed the attribute when false, and a missing attribute means Lynx's
|
|
20
20
|
// default: on, for `bounces` and `scroll-bar-enable`. So neither could be
|
|
21
21
|
// turned off. (`[attr.x]` would send the string "false" instead.)
|
|
22
|
+
//
|
|
23
|
+
// The host is the scroll area's box, and the <scroll-view> fills it.
|
|
24
|
+
// Angular gives a component's host every static attribute the app writes
|
|
25
|
+
// on it, so a class on <ui-scroll-area> lands on the host whatever the
|
|
26
|
+
// component does with its `class` input: on the <scroll-view> as well, a
|
|
27
|
+
// margin or padding applied twice. Padding the box insets the scroll view;
|
|
28
|
+
// `contentClass` pads what scrolls. `grow` fills a box with a height of its
|
|
29
|
+
// own (`h-64`, `flex-1`), and a box without one takes the scroll view's,
|
|
30
|
+
// which `height` sets: the scroll view keeps its own height as its flex
|
|
31
|
+
// basis. Not `h-full`: a percentage height doesn't reliably resolve on a
|
|
32
|
+
// flex item in Lynx.
|
|
33
|
+
host: { '[class]': 'hostClass()' },
|
|
22
34
|
template: `
|
|
23
35
|
<scroll-view
|
|
24
36
|
[attr.scroll-orientation]="orientation()"
|
|
@@ -27,7 +39,7 @@ import { cn } from '../../utils/cn';
|
|
|
27
39
|
[enable-nested-scroll]="nestedScroll()"
|
|
28
40
|
[attr.upper-threshold]="upperThreshold()"
|
|
29
41
|
[attr.lower-threshold]="lowerThreshold()"
|
|
30
|
-
|
|
42
|
+
class="grow"
|
|
31
43
|
[style]="scrollStyle()"
|
|
32
44
|
(bindscrolltoupper)="scrolledToStart.emit()"
|
|
33
45
|
(bindscrolltolower)="scrolledToEnd.emit()"
|
|
@@ -55,8 +67,8 @@ export class UiScrollArea {
|
|
|
55
67
|
readonly scrolledToStart = output<void>();
|
|
56
68
|
readonly scrolledToEnd = output<void>();
|
|
57
69
|
|
|
58
|
-
protected readonly
|
|
59
|
-
cn('w-full', this.userClass()),
|
|
70
|
+
protected readonly hostClass = computed(() =>
|
|
71
|
+
cn('w-full flex-col flex', this.userClass()),
|
|
60
72
|
);
|
|
61
73
|
|
|
62
74
|
// Explicit height is required for scroll-view to scroll in Lynx
|
|
@@ -41,6 +41,12 @@ class FieldHost {
|
|
|
41
41
|
readonly field = viewChild.required(UiSearchField);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
@Component({
|
|
45
|
+
imports: [UiSearchField],
|
|
46
|
+
template: `<ui-search-field placeholder="Songs and artists" class="mx-4" />`,
|
|
47
|
+
})
|
|
48
|
+
class StaticAttributesHost {}
|
|
49
|
+
|
|
44
50
|
/** A bar that holds the field in a shape of its own, as the dock does. */
|
|
45
51
|
const container = {
|
|
46
52
|
searching: signal(false),
|
|
@@ -203,6 +209,25 @@ afterEach(() => {
|
|
|
203
209
|
Object.assign(screen, { fieldBottom: 820, pageBottom: 874, fails: false });
|
|
204
210
|
});
|
|
205
211
|
|
|
212
|
+
describe('UiSearchField’s static attributes', () => {
|
|
213
|
+
// Angular gives a component's host every static attribute written on it
|
|
214
|
+
// (issue #181): the placeholder made a second match for
|
|
215
|
+
// getByPlaceholderText.
|
|
216
|
+
it('puts its class on one element, and its placeholder only on its input', async () => {
|
|
217
|
+
vi.stubGlobal('SystemInfo', IOS_26);
|
|
218
|
+
const { container, getAllByPlaceholderText } = await render(
|
|
219
|
+
StaticAttributesHost,
|
|
220
|
+
{
|
|
221
|
+
providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
expect(container.querySelectorAll('.mx-4')).toHaveLength(1);
|
|
225
|
+
expect(getAllByPlaceholderText('Songs and artists')).toEqual([
|
|
226
|
+
container.querySelector('input'),
|
|
227
|
+
]);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
206
231
|
describe('UiSearchField on iOS 26 and later', () => {
|
|
207
232
|
it('is a glass capsule with a magnifier and a search box that searches on return', async () => {
|
|
208
233
|
const { getByRole, field, fieldHost, glass, icons } =
|