@blotch/dolan 22.2.6 → 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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type ElementRef,
|
|
3
2
|
Component,
|
|
3
|
+
ElementRef,
|
|
4
4
|
ViewEncapsulation,
|
|
5
5
|
computed,
|
|
6
6
|
effect,
|
|
7
|
+
inject,
|
|
7
8
|
input,
|
|
8
|
-
viewChild,
|
|
9
9
|
} from '@angular/core';
|
|
10
10
|
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
11
11
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
@@ -53,21 +53,30 @@ export type AlertVariant = NonNullable<
|
|
|
53
53
|
VariantProps<typeof alertVariants>['variant']
|
|
54
54
|
>;
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The alert is its host, styled through `host`. Angular gives a component's
|
|
58
|
+
* host every static attribute the app writes on it, so a class on <ui-alert>
|
|
59
|
+
* lands on the host whatever the component does: with a styled view inside as
|
|
60
|
+
* well, the padding and border applied twice. Its title lands there too, and
|
|
61
|
+
* goes: it's shown in a <text>, and on the web preview a `title` attribute is
|
|
62
|
+
* a tooltip (and a second match for getByTitle).
|
|
63
|
+
*/
|
|
56
64
|
@Component({
|
|
57
65
|
selector: 'ui-alert',
|
|
58
|
-
standalone: true,
|
|
59
66
|
imports: [LYNX_ELEMENTS],
|
|
60
67
|
encapsulation: ViewEncapsulation.None,
|
|
68
|
+
host: {
|
|
69
|
+
'[class]': 'containerClass()',
|
|
70
|
+
'[attr.title]': 'null',
|
|
71
|
+
},
|
|
61
72
|
template: `
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<ng-content />
|
|
70
|
-
</view>
|
|
73
|
+
@if (title()) {
|
|
74
|
+
<text [class]="titleClass()">{{ title() }}</text>
|
|
75
|
+
}
|
|
76
|
+
@if (description()) {
|
|
77
|
+
<text [class]="descriptionClass()">{{ description() }}</text>
|
|
78
|
+
}
|
|
79
|
+
<ng-content />
|
|
71
80
|
`,
|
|
72
81
|
})
|
|
73
82
|
export class UiAlert {
|
|
@@ -76,7 +85,7 @@ export class UiAlert {
|
|
|
76
85
|
readonly description = input<string>('');
|
|
77
86
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
78
87
|
|
|
79
|
-
readonly
|
|
88
|
+
readonly #host = inject(ElementRef).nativeElement;
|
|
80
89
|
|
|
81
90
|
/**
|
|
82
91
|
* Reveals the alert with a subtle entrance on first render. A field rather
|
|
@@ -85,7 +94,7 @@ export class UiAlert {
|
|
|
85
94
|
* as uncovered.
|
|
86
95
|
*/
|
|
87
96
|
protected readonly entrance = effect(() => {
|
|
88
|
-
revealIn(this
|
|
97
|
+
revealIn(this.#host, { fromY: 4 });
|
|
89
98
|
});
|
|
90
99
|
|
|
91
100
|
protected readonly containerClass = computed(() =>
|
|
@@ -139,7 +139,6 @@ describe('UiAlertDialog', () => {
|
|
|
139
139
|
expect(getByText('Sure?').classList).toContain('text-xl');
|
|
140
140
|
expect(getByText('Cannot undo').classList).toContain('text-sm');
|
|
141
141
|
expect(getByText('Cannot undo').classList).toContain('italic');
|
|
142
|
-
// Each part's own view, not its host element, which keeps the bare class.
|
|
143
142
|
const header = container.querySelector('.flex-col.gap-2');
|
|
144
143
|
expect(header?.classList).not.toContain('gap-1.5');
|
|
145
144
|
const footer = container.querySelector('.pt-4');
|
|
@@ -147,6 +146,23 @@ describe('UiAlertDialog', () => {
|
|
|
147
146
|
expect(footer?.classList).not.toContain('justify-end');
|
|
148
147
|
});
|
|
149
148
|
|
|
149
|
+
// Angular gives a component's host every static attribute written on it
|
|
150
|
+
// (issue #181), so each class also styled a host: the dialog's in the page,
|
|
151
|
+
// where it is written, and each part's around its own view or text.
|
|
152
|
+
it('puts each class on one element: the panel, or the part', async () => {
|
|
153
|
+
const { container, getByText, panel } = await renderAlertDialog();
|
|
154
|
+
expect([...container.querySelectorAll('.max-w-sm')]).toEqual([panel()]);
|
|
155
|
+
// The header's: the footer has a gap-2 of its own.
|
|
156
|
+
expect(container.querySelectorAll('.flex-col.gap-2')).toHaveLength(1);
|
|
157
|
+
expect(container.querySelectorAll('.justify-start')).toHaveLength(1);
|
|
158
|
+
expect([...container.querySelectorAll('.text-xl')]).toEqual([
|
|
159
|
+
getByText('Sure?'),
|
|
160
|
+
]);
|
|
161
|
+
expect([...container.querySelectorAll('.italic')]).toEqual([
|
|
162
|
+
getByText('Cannot undo'),
|
|
163
|
+
]);
|
|
164
|
+
});
|
|
165
|
+
|
|
150
166
|
it('stays hidden until it is opened', async () => {
|
|
151
167
|
const { host, overlay, panel } = await renderAlertDialog();
|
|
152
168
|
await after(CLOSED);
|
|
@@ -793,7 +809,6 @@ it("draws an alert dialog's parts in the default look outside one", async () =>
|
|
|
793
809
|
expect(getByText('Alone').classList).toContain('text-lg');
|
|
794
810
|
expect(getByText('Outside a dialog').classList).toContain('text-sm');
|
|
795
811
|
expect(getByText('Footer').parentElement!.classList).toContain('pt-4');
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
);
|
|
812
|
+
// The title is the header's own child: the header is its host.
|
|
813
|
+
expect(getByText('Alone').parentElement!.classList).toContain('gap-1.5');
|
|
799
814
|
});
|
|
@@ -15,7 +15,11 @@ import {
|
|
|
15
15
|
signal,
|
|
16
16
|
viewChild,
|
|
17
17
|
} from '@angular/core';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
LYNX_ELEMENTS,
|
|
20
|
+
LynxAccessibilitySettings,
|
|
21
|
+
registerHostElement,
|
|
22
|
+
} from '@blotch/angular-lynx';
|
|
19
23
|
|
|
20
24
|
import {
|
|
21
25
|
type AnimationHandle,
|
|
@@ -173,6 +177,14 @@ const METRICS: Readonly<Record<GlassDesign, AlertMetrics>> = {
|
|
|
173
177
|
selector: 'ui-alert-dialog',
|
|
174
178
|
imports: [LYNX_ELEMENTS, NgTemplateOutlet, UiGlass],
|
|
175
179
|
encapsulation: ViewEncapsulation.None,
|
|
180
|
+
// The dialog's class is its panel's, inside the overlay. Angular also gives
|
|
181
|
+
// the host every static attribute the app writes on <ui-alert-dialog>, and
|
|
182
|
+
// the host sits in the page's layout where the dialog is written, so a
|
|
183
|
+
// `class="p-8"` padded the page there too. The host drops it. The parts
|
|
184
|
+
// below are their host elements for the same reason: a class on a part
|
|
185
|
+
// lands on its host anyway, so a view or text inside it styled with the
|
|
186
|
+
// same class applied it twice.
|
|
187
|
+
host: { '[attr.class]': 'null' },
|
|
176
188
|
template: `
|
|
177
189
|
<overlay [attr.visible]="overlayVisible()" [style]="overlayStyle()">
|
|
178
190
|
<!-- The overlay sizes only its first child, to the screen. With the
|
|
@@ -473,13 +485,9 @@ const injectAlertLook = () => {
|
|
|
473
485
|
|
|
474
486
|
@Component({
|
|
475
487
|
selector: 'ui-alert-dialog-header',
|
|
476
|
-
imports: [LYNX_ELEMENTS],
|
|
477
488
|
encapsulation: ViewEncapsulation.None,
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
<ng-content />
|
|
481
|
-
</view>
|
|
482
|
-
`,
|
|
489
|
+
host: { '[class]': 'containerClass()' },
|
|
490
|
+
template: '<ng-content />',
|
|
483
491
|
})
|
|
484
492
|
export class UiAlertDialogHeader {
|
|
485
493
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
@@ -507,9 +515,9 @@ export class UiAlertDialogHeader {
|
|
|
507
515
|
|
|
508
516
|
@Component({
|
|
509
517
|
selector: 'ui-alert-dialog-title',
|
|
510
|
-
imports: [LYNX_ELEMENTS],
|
|
511
518
|
encapsulation: ViewEncapsulation.None,
|
|
512
|
-
|
|
519
|
+
host: { '[class]': 'textClass()' },
|
|
520
|
+
template: '<ng-content />',
|
|
513
521
|
})
|
|
514
522
|
export class UiAlertDialogTitle {
|
|
515
523
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
@@ -534,11 +542,13 @@ export class UiAlertDialogTitle {
|
|
|
534
542
|
);
|
|
535
543
|
}
|
|
536
544
|
|
|
545
|
+
registerHostElement(UiAlertDialogTitle, 'text');
|
|
546
|
+
|
|
537
547
|
@Component({
|
|
538
548
|
selector: 'ui-alert-dialog-description',
|
|
539
|
-
imports: [LYNX_ELEMENTS],
|
|
540
549
|
encapsulation: ViewEncapsulation.None,
|
|
541
|
-
|
|
550
|
+
host: { '[class]': 'textClass()' },
|
|
551
|
+
template: '<ng-content />',
|
|
542
552
|
})
|
|
543
553
|
export class UiAlertDialogDescription {
|
|
544
554
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
@@ -562,6 +572,8 @@ export class UiAlertDialogDescription {
|
|
|
562
572
|
);
|
|
563
573
|
}
|
|
564
574
|
|
|
575
|
+
registerHostElement(UiAlertDialogDescription, 'text');
|
|
576
|
+
|
|
565
577
|
/**
|
|
566
578
|
* `UiAlertDialogAction`, for `contentChildren()`. A call rather than the
|
|
567
579
|
* class itself: the query's metadata keeps an arrow function of its own for
|
|
@@ -574,16 +586,15 @@ const actionType = () => UiAlertDialogAction;
|
|
|
574
586
|
selector: 'ui-alert-dialog-footer',
|
|
575
587
|
imports: [LYNX_ELEMENTS],
|
|
576
588
|
encapsulation: ViewEncapsulation.None,
|
|
589
|
+
host: { '[class]': 'containerClass()' },
|
|
577
590
|
template: `
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
<ng-content />
|
|
586
|
-
</view>
|
|
591
|
+
@if (separated()) {
|
|
592
|
+
<!-- The hairline over the buttons of Apple's alerts before iOS 26. -->
|
|
593
|
+
<view
|
|
594
|
+
class="h-[0.5px] w-full bg-glass-foreground opacity-20 absolute left-0 top-0"
|
|
595
|
+
></view>
|
|
596
|
+
}
|
|
597
|
+
<ng-content />
|
|
587
598
|
`,
|
|
588
599
|
})
|
|
589
600
|
export class UiAlertDialogFooter {
|
|
@@ -1,10 +1,46 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
1
2
|
import { render } from '@blotch/angular-lynx-testing-library';
|
|
3
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
2
4
|
import { describe, expect, it } from 'vitest';
|
|
3
5
|
import { UiAspectRatio } from './aspect-ratio';
|
|
4
6
|
|
|
7
|
+
@Component({
|
|
8
|
+
imports: [UiAspectRatio, LYNX_ELEMENTS],
|
|
9
|
+
template: `
|
|
10
|
+
<ui-aspect-ratio [ratio]="16 / 9" class="rounded-md">
|
|
11
|
+
<text>Media</text>
|
|
12
|
+
</ui-aspect-ratio>
|
|
13
|
+
`,
|
|
14
|
+
})
|
|
15
|
+
class AspectRatioHost {}
|
|
16
|
+
|
|
17
|
+
@Component({
|
|
18
|
+
imports: [UiAspectRatio],
|
|
19
|
+
template: `<ui-aspect-ratio />`,
|
|
20
|
+
})
|
|
21
|
+
class DefaultHost {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The ratio the box's inline style holds. jsdom writes `aspect-ratio: 1` back
|
|
25
|
+
* as `1` or, in newer versions (an app's), as `1 / 1`.
|
|
26
|
+
*/
|
|
27
|
+
const ratioOf = (box: HTMLElement) =>
|
|
28
|
+
parseFloat(box.style.getPropertyValue('aspect-ratio'));
|
|
29
|
+
|
|
5
30
|
describe('UiAspectRatio', () => {
|
|
6
31
|
it('renders with the default 1:1 ratio', async () => {
|
|
7
|
-
const { container } = await render(
|
|
8
|
-
|
|
32
|
+
const { container } = await render(DefaultHost);
|
|
33
|
+
const box = container.querySelector('.overflow-hidden') as HTMLElement;
|
|
34
|
+
expect(ratioOf(box)).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// A class on a component lands on its host too (issue #181): the host is
|
|
38
|
+
// the box, so the class styles one element, the one with the ratio.
|
|
39
|
+
it('is one box, with the ratio and the class written on it', async () => {
|
|
40
|
+
const { container, getByText } = await render(AspectRatioHost);
|
|
41
|
+
const box = getByText('Media').parentElement as HTMLElement;
|
|
42
|
+
expect([...container.querySelectorAll('.rounded-md')]).toEqual([box]);
|
|
43
|
+
expect(box.classList).toContain('overflow-hidden');
|
|
44
|
+
expect(ratioOf(box)).toBe(16 / 9);
|
|
9
45
|
});
|
|
10
46
|
});
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { Component, ViewEncapsulation, computed, input } from '@angular/core';
|
|
2
|
-
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
3
2
|
|
|
4
3
|
import { cn } from '../../utils/cn';
|
|
5
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The box is the host, styled through `host`. Angular gives a component's
|
|
7
|
+
* host every static attribute the app writes on it, so a class on
|
|
8
|
+
* <ui-aspect-ratio> lands on the host whatever the component does: with a
|
|
9
|
+
* styled view inside as well, a margin or padding applied twice.
|
|
10
|
+
*/
|
|
6
11
|
@Component({
|
|
7
12
|
selector: 'ui-aspect-ratio',
|
|
8
|
-
standalone: true,
|
|
9
|
-
imports: [LYNX_ELEMENTS],
|
|
10
13
|
encapsulation: ViewEncapsulation.None,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
host: {
|
|
15
|
+
'[class]': 'containerClass()',
|
|
16
|
+
'[style]': 'containerStyle()',
|
|
17
|
+
},
|
|
18
|
+
template: '<ng-content />',
|
|
16
19
|
})
|
|
17
20
|
export class UiAspectRatio {
|
|
18
21
|
readonly ratio = input(1);
|
|
@@ -41,6 +41,13 @@ describe('UiAvatar', () => {
|
|
|
41
41
|
expect(avatar().classList).toContain('border');
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
// A class on a component lands on its host too (issue #181): the host is
|
|
45
|
+
// the circle, so the border is drawn once.
|
|
46
|
+
it('puts its class on one element, the circle', async () => {
|
|
47
|
+
const { container, avatar } = await renderAvatar();
|
|
48
|
+
expect([...container.querySelectorAll('.border')]).toEqual([avatar()]);
|
|
49
|
+
});
|
|
50
|
+
|
|
44
51
|
it('covers the image with a placeholder while it loads', async () => {
|
|
45
52
|
const { image, skeleton, initials } = await renderAvatar(
|
|
46
53
|
'https://example.com/avatar.png',
|
|
@@ -10,42 +10,47 @@ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
|
10
10
|
import { UiSkeleton } from '../skeleton/skeleton';
|
|
11
11
|
import { cn } from '../../utils/cn';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The avatar's circle is its host, styled through `host`, with the image or
|
|
15
|
+
* initials inside. Angular gives a component's host every static attribute the
|
|
16
|
+
* app writes on it, so a class on <ui-avatar> lands on the host whatever the
|
|
17
|
+
* component does: with a styled circle inside as well, a margin or border
|
|
18
|
+
* applied twice.
|
|
19
|
+
*/
|
|
13
20
|
@Component({
|
|
14
21
|
selector: 'ui-avatar',
|
|
15
|
-
standalone: true,
|
|
16
22
|
imports: [LYNX_ELEMENTS, UiSkeleton],
|
|
17
23
|
encapsulation: ViewEncapsulation.None,
|
|
24
|
+
host: { '[class]': 'containerClass()' },
|
|
18
25
|
template: `
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<ui-skeleton [class]="skeletonClass()" />
|
|
40
|
-
}
|
|
41
|
-
} @else {
|
|
42
|
-
<!-- Shared by the no-src case and the load-error case: initials if a
|
|
43
|
-
fallback was provided, otherwise a plain muted circle. -->
|
|
44
|
-
<view [class]="fallbackClass()">
|
|
45
|
-
<text [class]="initialsClass()">{{ fallback() }}</text>
|
|
46
|
-
</view>
|
|
26
|
+
@if (src() && !errored()) {
|
|
27
|
+
<!-- The <image> stays mounted whenever src is set so its bindload /
|
|
28
|
+
binderror events can fire — an @if-swapped-out image would never
|
|
29
|
+
start loading, so the load event that reveals it would never come.
|
|
30
|
+
Native Lynx <image> renders nothing until loaded (there is no
|
|
31
|
+
broken-image glyph like the web), so while loading the box is
|
|
32
|
+
simply empty; the skeleton below sits on top of it as a placeholder.
|
|
33
|
+
onImageLoad() flips the loaded state, removing the skeleton to
|
|
34
|
+
reveal the image underneath. -->
|
|
35
|
+
<image
|
|
36
|
+
[attr.src]="src()"
|
|
37
|
+
[class]="imageClass()"
|
|
38
|
+
(bindload)="onImageLoad()"
|
|
39
|
+
(binderror)="onImageError()"
|
|
40
|
+
/>
|
|
41
|
+
<!-- Overlay placeholder shown only while the image is loading. It paints
|
|
42
|
+
above the <image> because it comes later in source order (Lynx paint
|
|
43
|
+
order follows the DOM, so no z-index is needed). -->
|
|
44
|
+
@if (!loaded()) {
|
|
45
|
+
<ui-skeleton [class]="skeletonClass()" />
|
|
47
46
|
}
|
|
48
|
-
|
|
47
|
+
} @else {
|
|
48
|
+
<!-- Shared by the no-src case and the load-error case: initials if a
|
|
49
|
+
fallback was provided, otherwise a plain muted circle. -->
|
|
50
|
+
<view [class]="fallbackClass()">
|
|
51
|
+
<text [class]="initialsClass()">{{ fallback() }}</text>
|
|
52
|
+
</view>
|
|
53
|
+
}
|
|
49
54
|
`,
|
|
50
55
|
})
|
|
51
56
|
export class UiAvatar {
|
|
@@ -53,6 +53,15 @@ describe('UiBadge', () => {
|
|
|
53
53
|
expect(label.parentElement?.classList).toContain('border-border');
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
// A class on a component lands on its host too (issue #181): the host is
|
|
57
|
+
// the pill, so the margin applies once.
|
|
58
|
+
it('puts its class on one element, the pill', async () => {
|
|
59
|
+
const { container, getByText } = await render(BadgeHost);
|
|
60
|
+
expect([...container.querySelectorAll('.ml-1')]).toEqual([
|
|
61
|
+
getByText('Status').parentElement,
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
56
65
|
it('pops in on first render', async () => {
|
|
57
66
|
const animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
58
67
|
const { getByText } = await render(BadgeHost);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type ElementRef,
|
|
3
2
|
Component,
|
|
3
|
+
ElementRef,
|
|
4
4
|
ViewEncapsulation,
|
|
5
5
|
booleanAttribute,
|
|
6
6
|
computed,
|
|
7
7
|
effect,
|
|
8
|
+
inject,
|
|
8
9
|
input,
|
|
9
|
-
viewChild,
|
|
10
10
|
} from '@angular/core';
|
|
11
11
|
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
12
12
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
@@ -53,16 +53,18 @@ export type BadgeVariant = NonNullable<
|
|
|
53
53
|
VariantProps<typeof badgeVariants>['variant']
|
|
54
54
|
>;
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The badge's pill is its host, styled through `host`, with its label inside.
|
|
58
|
+
* Angular gives a component's host every static attribute the app writes on
|
|
59
|
+
* it, so a class on <ui-badge> lands on the host whatever the component does:
|
|
60
|
+
* with a styled pill inside as well, a margin applied twice.
|
|
61
|
+
*/
|
|
56
62
|
@Component({
|
|
57
63
|
selector: 'ui-badge',
|
|
58
|
-
standalone: true,
|
|
59
64
|
imports: [LYNX_ELEMENTS],
|
|
60
65
|
encapsulation: ViewEncapsulation.None,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<text [class]="labelClass()"><ng-content /></text>
|
|
64
|
-
</view>
|
|
65
|
-
`,
|
|
66
|
+
host: { '[class]': 'containerClass()' },
|
|
67
|
+
template: `<text [class]="labelClass()"><ng-content /></text>`,
|
|
66
68
|
})
|
|
67
69
|
export class UiBadge {
|
|
68
70
|
readonly variant = input<BadgeVariant>('default');
|
|
@@ -72,7 +74,7 @@ export class UiBadge {
|
|
|
72
74
|
readonly animated = input(true, { transform: booleanAttribute });
|
|
73
75
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
74
76
|
|
|
75
|
-
readonly
|
|
77
|
+
readonly #host = inject(ElementRef).nativeElement;
|
|
76
78
|
|
|
77
79
|
/**
|
|
78
80
|
* Pops the badge in on first render, unless `animated` is off. A field
|
|
@@ -82,7 +84,7 @@ export class UiBadge {
|
|
|
82
84
|
*/
|
|
83
85
|
protected readonly entrance = effect(() => {
|
|
84
86
|
if (this.animated()) {
|
|
85
|
-
popIn(this
|
|
87
|
+
popIn(this.#host, { duration: 200 });
|
|
86
88
|
}
|
|
87
89
|
});
|
|
88
90
|
|
|
@@ -132,6 +132,13 @@ describe('UiBottomSheet', () => {
|
|
|
132
132
|
expect(panel.classList).toContain('bg-background');
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
+
// Angular gives a component's host every static attribute written on it
|
|
136
|
+
// (issue #181): the class also styled the sheet's host, in the page.
|
|
137
|
+
it('puts its class on the panel alone', async () => {
|
|
138
|
+
const { container, panel } = await renderSheet();
|
|
139
|
+
expect([...container.querySelectorAll('.max-h-96')]).toEqual([panel]);
|
|
140
|
+
});
|
|
141
|
+
|
|
135
142
|
it('fades the backdrop in and slides the panel up when opened', async () => {
|
|
136
143
|
const { host, backdrop, panel } = await renderSheet();
|
|
137
144
|
await openSheet(host);
|
|
@@ -263,6 +263,11 @@ const layoutWidth = (event: unknown): number =>
|
|
|
263
263
|
selector: 'ui-bottom-sheet',
|
|
264
264
|
imports: [LYNX_ELEMENTS, UiGlass],
|
|
265
265
|
encapsulation: ViewEncapsulation.None,
|
|
266
|
+
// The sheet's class is its panel's, inside the overlay. Angular also gives
|
|
267
|
+
// the host every static attribute the app writes on <ui-bottom-sheet>, and
|
|
268
|
+
// the host sits in the page's layout where the sheet is written, so a
|
|
269
|
+
// `class="px-8"` padded the page there too. The host drops it.
|
|
270
|
+
host: { '[attr.class]': 'null' },
|
|
266
271
|
template: `
|
|
267
272
|
<!-- Android's back button closes the sheet: while an overlay shows, it
|
|
268
273
|
takes the press and fires requestclose. -->
|
|
@@ -77,10 +77,10 @@ const renderButton = async (
|
|
|
77
77
|
...result,
|
|
78
78
|
host,
|
|
79
79
|
/**
|
|
80
|
-
* The button's
|
|
81
|
-
*
|
|
80
|
+
* The button's box, its host: it takes the presses, and holds the glass
|
|
81
|
+
* on a glass variant.
|
|
82
82
|
*/
|
|
83
|
-
button: () => result.container.firstElementChild
|
|
83
|
+
button: () => result.container.firstElementChild!,
|
|
84
84
|
glass: () => result.container.querySelector('blur-view')!,
|
|
85
85
|
spinner: () => result.container.querySelector('svg'),
|
|
86
86
|
icon: () => result.container.querySelector('svg')!,
|
|
@@ -92,6 +92,16 @@ const renderButton = async (
|
|
|
92
92
|
};
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
+
@Component({
|
|
96
|
+
imports: [UiButton, UiIcon],
|
|
97
|
+
template: `
|
|
98
|
+
<ui-button size="icon" accessibility-label="Search" class="mt-2">
|
|
99
|
+
<ui-icon name="search" />
|
|
100
|
+
</ui-button>
|
|
101
|
+
`,
|
|
102
|
+
})
|
|
103
|
+
class StaticAttributesHost {}
|
|
104
|
+
|
|
95
105
|
const IOS_26 = { platform: 'iOS', osVersion: '26.0' };
|
|
96
106
|
const IOS_18 = { platform: 'iOS', osVersion: '18.5' };
|
|
97
107
|
const ANDROID = { platform: 'Android', osVersion: '15' };
|
|
@@ -118,6 +128,22 @@ afterEach(() => {
|
|
|
118
128
|
});
|
|
119
129
|
|
|
120
130
|
describe('UiButton', () => {
|
|
131
|
+
// Angular gives a component's host every static attribute written on it
|
|
132
|
+
// (issue #181): the class styled a box inside the host as well, and the
|
|
133
|
+
// label named two elements.
|
|
134
|
+
it('puts its class and accessibility label on one element, the button', async () => {
|
|
135
|
+
vi.stubGlobal('SystemInfo', undefined);
|
|
136
|
+
const { container, getAllByAccessibilityLabel } = await render(
|
|
137
|
+
StaticAttributesHost,
|
|
138
|
+
{
|
|
139
|
+
providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
const button = container.firstElementChild!;
|
|
143
|
+
expect([...container.querySelectorAll('.mt-2')]).toEqual([button]);
|
|
144
|
+
expect(getAllByAccessibilityLabel('Search')).toEqual([button]);
|
|
145
|
+
});
|
|
146
|
+
|
|
121
147
|
it('renders its label with the classes for its variant and size', async () => {
|
|
122
148
|
const { button, getByText } = await renderButton();
|
|
123
149
|
expect(button().classList).toContain('bg-primary');
|