@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,15 +1,15 @@
|
|
|
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
10
|
model,
|
|
10
11
|
output,
|
|
11
12
|
signal,
|
|
12
|
-
viewChild,
|
|
13
13
|
} from '@angular/core';
|
|
14
14
|
import { type FormValueControl } from '@angular/forms/signals';
|
|
15
15
|
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
@@ -19,69 +19,88 @@ import { cn } from '../../utils/cn';
|
|
|
19
19
|
|
|
20
20
|
@Component({
|
|
21
21
|
selector: 'ui-input',
|
|
22
|
-
standalone: true,
|
|
23
22
|
imports: [LYNX_ELEMENTS],
|
|
24
23
|
encapsulation: ViewEncapsulation.None,
|
|
24
|
+
// The host is the field's column. Angular gives a component's host every
|
|
25
|
+
// static attribute the app writes on it, so a class on <ui-input> lands
|
|
26
|
+
// there whatever the component does: a column inside the host applied it
|
|
27
|
+
// twice. A placeholder lands there too, and goes: it belongs to the
|
|
28
|
+
// <input>, and a second element with it is a second match for
|
|
29
|
+
// getByPlaceholderText.
|
|
30
|
+
host: {
|
|
31
|
+
'[class]': 'containerClass()',
|
|
32
|
+
'[attr.placeholder]': 'null',
|
|
33
|
+
},
|
|
25
34
|
template: `
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
35
|
+
<!-- The <input> below carries the label as its accessibility-label, so
|
|
36
|
+
screen readers read it with the field. Hidden here, or VoiceOver and
|
|
37
|
+
TalkBack would stop on it a second time. -->
|
|
38
|
+
@if (label()) {
|
|
39
|
+
<text [class]="labelClass()" accessibility-elements-hidden="true">{{
|
|
40
|
+
label()
|
|
41
|
+
}}</text>
|
|
42
|
+
}
|
|
43
|
+
<!-- Focus ring is an inline box-shadow on a dedicated overlay layer, not a
|
|
44
|
+
Tailwind ring-* class (the Lynx preset doesn't wire up --tw-ring-*, so
|
|
45
|
+
ring-* renders nothing on device). It rides its OWN layer so it can
|
|
46
|
+
fade: box-shadow is animatable:no on Lynx (the core rejects a
|
|
47
|
+
transition: box-shadow), but opacity is animatable, so we transition
|
|
48
|
+
the layer's opacity instead. The overlay is the FIRST child so Lynx,
|
|
49
|
+
which paints in strict source order, keeps the <input> on top and
|
|
50
|
+
tappable. Source order alone is NOT enough on web: CSS paints a
|
|
51
|
+
positioned (absolute) sibling above a static one regardless of source
|
|
52
|
+
order, so this transparent overlay would swallow every tap — no focus,
|
|
53
|
+
no typing. The <input> below therefore carries position:relative +
|
|
54
|
+
z-index:1 so it sits above the overlay on BOTH platforms (required on
|
|
55
|
+
web). On Lynx, though, a z-indexed element is stacked against its
|
|
56
|
+
nearest stacking context, and a scroll-view isn't one unless it sets
|
|
57
|
+
z-index itself — so the native input would stay fixed on screen while
|
|
58
|
+
the page scrolled (only its placeholder visibly floats). The wrapper's
|
|
59
|
+
z-0 makes it that stacking context, keeping the input's z-index local
|
|
60
|
+
so it scrolls with the field. pointer-events is avoided — it errors
|
|
61
|
+
the Lynx build. The shadow is OUTSET only: Lynx doesn't render inset box-shadows,
|
|
62
|
+
so the crisp 1px "border" is an outset ring hugging the edge, not a
|
|
63
|
+
real inset border. -->
|
|
64
|
+
<view [class]="inputWrapperClass()">
|
|
65
|
+
<view
|
|
66
|
+
class="rounded-xl opacity-0 absolute bottom-0 left-0 right-0 top-0"
|
|
67
|
+
[style.transition]="'opacity 150ms ease'"
|
|
68
|
+
[style.box-shadow]="ringShadow()"
|
|
69
|
+
[style.opacity]="ringVisible() ? 1 : 0"
|
|
70
|
+
></view>
|
|
71
|
+
<!-- [attr.disabled]="disabled() || undefined": passing 'undefined'
|
|
72
|
+
removes the attribute entirely; passing 'false' would set
|
|
73
|
+
disabled="false" which Lynx still treats as disabled. -->
|
|
74
|
+
<!-- [value] not [attr.value]: [attr.value] calls renderer.setAttribute()
|
|
75
|
+
which routes to __SetAttribute — a no-op for live text on Lynx native
|
|
76
|
+
inputs. [value] is an Angular input binding that triggers
|
|
77
|
+
LynxInput.ngOnChanges, where the setValue UIMethod override lives.
|
|
78
|
+
Without this, form model resets would update the signal but the
|
|
79
|
+
native input would keep showing the user's old typed text. -->
|
|
80
|
+
<!-- accessibility-element: on iOS, Lynx makes only <text> and <image>
|
|
81
|
+
accessibility elements by default, so the UITextField under an
|
|
82
|
+
<input> isn't one. VoiceOver then read the placeholder inside it,
|
|
83
|
+
or nothing once it held text. As one, it reads its
|
|
84
|
+
accessibility-label, the label, then its text. -->
|
|
85
|
+
<input
|
|
86
|
+
accessibility-element="true"
|
|
87
|
+
[attr.accessibility-label]="label() || null"
|
|
88
|
+
[attr.placeholder]="placeholder()"
|
|
89
|
+
[attr.type]="type()"
|
|
90
|
+
[value]="value()"
|
|
91
|
+
[attr.disabled]="disabled() || undefined"
|
|
92
|
+
class="text-sm text-foreground"
|
|
93
|
+
style="border: none; background: transparent; height: 100%; width: 100%; position: relative; z-index: 1;"
|
|
94
|
+
(bindinput)="onInput($any($event))"
|
|
95
|
+
(bindfocus)="onFocus()"
|
|
96
|
+
(bindblur)="onBlur()"
|
|
97
|
+
/>
|
|
84
98
|
</view>
|
|
99
|
+
@if (error()) {
|
|
100
|
+
<text [class]="errorClass()">{{ error() }}</text>
|
|
101
|
+
} @else if (helperText()) {
|
|
102
|
+
<text [class]="helperClass()">{{ helperText() }}</text>
|
|
103
|
+
}
|
|
85
104
|
`,
|
|
86
105
|
})
|
|
87
106
|
export class UiInput implements FormValueControl<string> {
|
|
@@ -97,8 +116,15 @@ export class UiInput implements FormValueControl<string> {
|
|
|
97
116
|
readonly changed = output<string>();
|
|
98
117
|
readonly focused = output<void>();
|
|
99
118
|
readonly blurred = output<void>();
|
|
119
|
+
/**
|
|
120
|
+
* The field lost focus, for forms, which mark it as touched as they do a
|
|
121
|
+
* native input (FormUiControl's `touch`). Signal Forms, reactive forms and
|
|
122
|
+
* template-driven forms all listen for it; a `touched` model's change
|
|
123
|
+
* isn't read by any of them.
|
|
124
|
+
*/
|
|
125
|
+
readonly touch = output<void>();
|
|
100
126
|
|
|
101
|
-
readonly
|
|
127
|
+
readonly #host = inject(ElementRef).nativeElement;
|
|
102
128
|
readonly #isFocused = signal(false);
|
|
103
129
|
#shakeAnim?: AnimationHandle;
|
|
104
130
|
#previousError = '';
|
|
@@ -111,10 +137,9 @@ export class UiInput implements FormValueControl<string> {
|
|
|
111
137
|
*/
|
|
112
138
|
protected readonly shakeOnError = effect(() => {
|
|
113
139
|
const err = this.error();
|
|
114
|
-
|
|
115
|
-
if (err && !this.#previousError && el) {
|
|
140
|
+
if (err && !this.#previousError) {
|
|
116
141
|
this.#shakeAnim?.cancel();
|
|
117
|
-
this.#shakeAnim = shake(
|
|
142
|
+
this.#shakeAnim = shake(this.#host);
|
|
118
143
|
}
|
|
119
144
|
this.#previousError = err;
|
|
120
145
|
});
|
|
@@ -179,6 +204,7 @@ export class UiInput implements FormValueControl<string> {
|
|
|
179
204
|
protected onBlur(): void {
|
|
180
205
|
this.#isFocused.set(false);
|
|
181
206
|
this.blurred.emit();
|
|
207
|
+
this.touch.emit();
|
|
182
208
|
}
|
|
183
209
|
|
|
184
210
|
/**
|
|
@@ -11,6 +11,12 @@ class LabelHost {
|
|
|
11
11
|
readonly disabled = signal(false);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
@Component({
|
|
15
|
+
imports: [UiLabel],
|
|
16
|
+
template: `<ui-label class="mb-2">Email</ui-label>`,
|
|
17
|
+
})
|
|
18
|
+
class StaticClassHost {}
|
|
19
|
+
|
|
14
20
|
describe('UiLabel', () => {
|
|
15
21
|
it('renders its text, dimmed while disabled', async () => {
|
|
16
22
|
const { componentRef, getByText } = await render(LabelHost);
|
|
@@ -22,4 +28,13 @@ describe('UiLabel', () => {
|
|
|
22
28
|
await waitForUpdate();
|
|
23
29
|
expect(label.classList).toContain('opacity-50');
|
|
24
30
|
});
|
|
31
|
+
|
|
32
|
+
// A class on a component lands on its host too (issue #181): the label is
|
|
33
|
+
// one <text>, so the class styles one element.
|
|
34
|
+
it('is one text, with the class written on it', async () => {
|
|
35
|
+
const { container, getByText } = await render(StaticClassHost);
|
|
36
|
+
const label = getByText('Email');
|
|
37
|
+
expect(label.tagName.toLowerCase()).toBe('text');
|
|
38
|
+
expect([...container.querySelectorAll('.mb-2')]).toEqual([label]);
|
|
39
|
+
});
|
|
25
40
|
});
|
|
@@ -5,16 +5,22 @@ import {
|
|
|
5
5
|
computed,
|
|
6
6
|
input,
|
|
7
7
|
} from '@angular/core';
|
|
8
|
-
import {
|
|
8
|
+
import { registerHostElement } from '@blotch/angular-lynx';
|
|
9
9
|
|
|
10
10
|
import { cn } from '../../utils/cn';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The label is its host, a `<text>` (registerHostElement, below), styled
|
|
14
|
+
* through `host`. Angular gives a component's host every static attribute the
|
|
15
|
+
* app writes on it, so `<ui-label class="mb-2">` puts `mb-2` on the host
|
|
16
|
+
* whatever the component does: with a styled `<text>` inside a view host as
|
|
17
|
+
* well, the margin applied twice.
|
|
18
|
+
*/
|
|
12
19
|
@Component({
|
|
13
20
|
selector: 'ui-label',
|
|
14
|
-
standalone: true,
|
|
15
|
-
imports: [LYNX_ELEMENTS],
|
|
16
21
|
encapsulation: ViewEncapsulation.None,
|
|
17
|
-
|
|
22
|
+
host: { '[class]': 'labelClass()' },
|
|
23
|
+
template: '<ng-content />',
|
|
18
24
|
})
|
|
19
25
|
export class UiLabel {
|
|
20
26
|
readonly disabled = input(false, { transform: booleanAttribute });
|
|
@@ -28,3 +34,5 @@ export class UiLabel {
|
|
|
28
34
|
),
|
|
29
35
|
);
|
|
30
36
|
}
|
|
37
|
+
|
|
38
|
+
registerHostElement(UiLabel, 'text');
|
|
@@ -45,6 +45,8 @@ const renderList = async () => {
|
|
|
45
45
|
...result,
|
|
46
46
|
host: result.componentRef.instance as ListHost,
|
|
47
47
|
list: () => result.container.querySelector('list')!,
|
|
48
|
+
/** The list's box, its host, which the <list> fills. */
|
|
49
|
+
box: () => result.container.querySelector('list')!.parentElement!,
|
|
48
50
|
};
|
|
49
51
|
};
|
|
50
52
|
|
|
@@ -65,12 +67,20 @@ describe('UiList', () => {
|
|
|
65
67
|
// Lynx `<list>` is virtualized: items join the native tree only through
|
|
66
68
|
// its componentAtIndex callback, which a unit test doesn't drive. So this
|
|
67
69
|
// checks the list itself rather than the projected item.
|
|
68
|
-
const { list } = await renderList();
|
|
70
|
+
const { list, box } = await renderList();
|
|
69
71
|
expect(list().getAttribute('list-type')).toBe('single');
|
|
70
72
|
expect(list().getAttribute('span-count')).toBe('1');
|
|
71
73
|
expect(list().getAttribute('scroll-orientation')).toBe('vertical');
|
|
72
|
-
expect(list().classList).toContain('
|
|
73
|
-
expect(
|
|
74
|
+
expect(list().classList).toContain('grow');
|
|
75
|
+
expect(box().classList).toContain('w-full');
|
|
76
|
+
expect(box().classList).toContain('bg-card');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Angular gives a component's host every static attribute written on it,
|
|
80
|
+
// so the <list> taking the class as well applied it twice (issue #181).
|
|
81
|
+
it('puts its class on one element, its box', async () => {
|
|
82
|
+
const { container, box } = await renderList();
|
|
83
|
+
expect([...container.querySelectorAll('.bg-card')]).toEqual([box()]);
|
|
74
84
|
});
|
|
75
85
|
|
|
76
86
|
it('gives the list an explicit height, which it needs to scroll', async () => {
|
|
@@ -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();
|