@blotch/dolan 22.2.3 → 22.2.5
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/add.js +9 -1
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +38 -3
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/doctor.js +258 -21
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +425 -11
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +38 -18
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +105 -2
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/theme.js +1 -1
- package/dist/commands/theme.js.map +1 -1
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.js +194 -50
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +570 -3
- package/dist/commands/update.test.js.map +1 -1
- package/dist/lockfile.d.ts +17 -0
- package/dist/lockfile.js +56 -1
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.d.ts +1 -0
- package/dist/lockfile.test.js +65 -0
- package/dist/lockfile.test.js.map +1 -0
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +119 -11
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +61 -6
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +27 -0
- package/dist/test-utils.js +45 -1
- package/dist/test-utils.js.map +1 -1
- package/dist/testing/fire-lynx-event.d.ts +14 -0
- package/dist/testing/fire-lynx-event.js +19 -0
- package/dist/testing/fire-lynx-event.js.map +1 -0
- package/dist/testing/fire-lynx-event.test.d.ts +1 -0
- package/dist/testing/fire-lynx-event.test.js +44 -0
- package/dist/testing/fire-lynx-event.test.js.map +1 -0
- package/dist/testing/lynx-main-thread.d.ts +22 -0
- package/dist/testing/lynx-main-thread.js +11 -0
- package/dist/testing/lynx-main-thread.js.map +1 -0
- package/dist/testing/lynx-main-thread.test.d.ts +1 -0
- package/dist/testing/lynx-main-thread.test.js +24 -0
- package/dist/testing/lynx-main-thread.test.js.map +1 -0
- package/dist/testing/set-input-signal.d.ts +5 -4
- package/dist/testing/set-input-signal.js +5 -4
- package/dist/testing/set-input-signal.js.map +1 -1
- package/dist/ui/boolean-inputs.test.ts +76 -0
- package/dist/ui/components/accordion/accordion.test.ts +158 -144
- package/dist/ui/components/accordion/accordion.ts +19 -14
- package/dist/ui/components/action-sheet/action-sheet.test.ts +637 -183
- package/dist/ui/components/action-sheet/action-sheet.ts +339 -55
- package/dist/ui/components/action-sheet/index.ts +1 -0
- package/dist/ui/components/alert/alert.test.ts +67 -19
- package/dist/ui/components/alert/alert.ts +14 -16
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +740 -129
- package/dist/ui/components/alert-dialog/alert-dialog.ts +672 -58
- package/dist/ui/components/alert-dialog/index.ts +5 -0
- package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
- package/dist/ui/components/area-chart/area-chart.ts +3 -2
- package/dist/ui/components/avatar/avatar.test.ts +89 -45
- package/dist/ui/components/badge/badge.test.ts +57 -23
- package/dist/ui/components/badge/badge.ts +14 -15
- package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +1314 -273
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1046 -119
- package/dist/ui/components/bottom-sheet/index.ts +7 -1
- package/dist/ui/components/button/button.test.ts +646 -2
- package/dist/ui/components/button/button.ts +443 -64
- package/dist/ui/components/button/index.ts +2 -0
- package/dist/ui/components/button-group/button-group.test.ts +528 -2
- package/dist/ui/components/button-group/button-group.ts +428 -70
- package/dist/ui/components/button-group/index.ts +3 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +82 -1
- package/dist/ui/components/card/card.test.ts +97 -61
- package/dist/ui/components/card/card.ts +2 -1
- package/dist/ui/components/chart/chart-presets.test.ts +197 -1
- package/dist/ui/components/chart/chart-presets.ts +14 -7
- package/dist/ui/components/chart/chart-theme.ts +12 -0
- package/dist/ui/components/chart/chart.test.ts +1031 -0
- package/dist/ui/components/chart/chart.ts +66 -46
- package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
- package/dist/ui/components/checkbox/checkbox.ts +25 -24
- package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
- package/dist/ui/components/collapsible/collapsible.ts +21 -15
- package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
- package/dist/ui/components/context-menu/context-menu.ts +110 -0
- package/dist/ui/components/context-menu/index.ts +1 -0
- package/dist/ui/components/dialog/dialog.test.ts +210 -152
- package/dist/ui/components/dialog/dialog.ts +35 -29
- package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
- package/dist/ui/components/dock/dock-scroll.ts +142 -0
- package/dist/ui/components/dock/dock-search.test.ts +594 -0
- package/dist/ui/components/dock/dock.test.ts +752 -0
- package/dist/ui/components/dock/dock.ts +1438 -0
- package/dist/ui/components/dock/index.ts +2 -0
- package/dist/ui/components/dynamic-color/dynamic-color.test.ts +144 -0
- package/dist/ui/components/dynamic-color/dynamic-color.ts +146 -0
- package/dist/ui/components/dynamic-color/index.ts +5 -0
- package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
- package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
- package/dist/ui/components/glass/concentric-radius.ts +17 -0
- package/dist/ui/components/glass/glass-design.test.ts +78 -0
- package/dist/ui/components/glass/glass-design.ts +119 -0
- package/dist/ui/components/glass/glass.test.ts +376 -0
- package/dist/ui/components/glass/glass.ts +430 -0
- package/dist/ui/components/glass/index.ts +19 -0
- package/dist/ui/components/icon/icon-theme.test.ts +64 -0
- package/dist/ui/components/icon/icon-theme.ts +70 -16
- package/dist/ui/components/icon/icon.test.ts +52 -41
- package/dist/ui/components/input/input.test.ts +178 -101
- package/dist/ui/components/input/input.ts +17 -13
- package/dist/ui/components/label/label.test.ts +11 -24
- package/dist/ui/components/label/label.ts +8 -2
- package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
- package/dist/ui/components/line-chart/line-chart.ts +3 -2
- package/dist/ui/components/list/list.test.ts +80 -49
- package/dist/ui/components/list/list.ts +5 -4
- package/dist/ui/components/menu/index.ts +25 -0
- package/dist/ui/components/menu/menu-placement.test.ts +153 -0
- package/dist/ui/components/menu/menu-placement.ts +159 -0
- package/dist/ui/components/menu/menu-popup.test.ts +636 -0
- package/dist/ui/components/menu/menu-popup.ts +565 -0
- package/dist/ui/components/menu/menu.test.ts +523 -0
- package/dist/ui/components/menu/menu.ts +455 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
- package/dist/ui/components/nav-drawer/nav-drawer.ts +75 -58
- package/dist/ui/components/navigation-bar/index.ts +1 -0
- package/dist/ui/components/navigation-bar/navigation-bar.test.ts +425 -0
- package/dist/ui/components/navigation-bar/navigation-bar.ts +411 -0
- package/dist/ui/components/pagination/pagination.test.ts +136 -86
- package/dist/ui/components/popover/index.ts +9 -0
- package/dist/ui/components/popover/popover-panel.test.ts +735 -0
- package/dist/ui/components/popover/popover-panel.ts +603 -0
- package/dist/ui/components/popover/popover.test.ts +286 -0
- package/dist/ui/components/popover/popover.ts +207 -0
- package/dist/ui/components/progress/progress.test.ts +102 -46
- package/dist/ui/components/progress/progress.ts +54 -55
- package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
- package/dist/ui/components/radio-group/radio-group.ts +26 -25
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +74 -1
- package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
- package/dist/ui/components/scroll-area/scroll-area.ts +4 -3
- package/dist/ui/components/search-field/index.ts +15 -0
- package/dist/ui/components/search-field/keyboard.test.ts +147 -0
- package/dist/ui/components/search-field/keyboard.ts +123 -0
- package/dist/ui/components/search-field/search-field.test.ts +549 -0
- package/dist/ui/components/search-field/search-field.ts +628 -0
- package/dist/ui/components/segmented-control/index.ts +8 -0
- package/dist/ui/components/segmented-control/segmented-control.test.ts +861 -0
- package/dist/ui/components/segmented-control/segmented-control.ts +884 -0
- package/dist/ui/components/select/select.test.ts +138 -78
- package/dist/ui/components/select/select.ts +19 -12
- package/dist/ui/components/separator/separator.test.ts +18 -17
- package/dist/ui/components/sheet/sheet.test.ts +151 -11
- package/dist/ui/components/sheet/sheet.ts +68 -5
- package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
- package/dist/ui/components/skeleton/skeleton.ts +10 -16
- package/dist/ui/components/slider/index.ts +1 -0
- package/dist/ui/components/slider/slider.test.ts +695 -0
- package/dist/ui/components/slider/slider.ts +901 -0
- package/dist/ui/components/spinner/spinner.test.ts +64 -46
- package/dist/ui/components/spinner/spinner.ts +16 -19
- package/dist/ui/components/switch/index.ts +1 -1
- package/dist/ui/components/switch/switch.test.ts +534 -48
- package/dist/ui/components/switch/switch.ts +613 -48
- package/dist/ui/components/tab-bar/index.ts +1 -0
- package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
- package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
- package/dist/ui/components/tabs/index.ts +7 -1
- package/dist/ui/components/tabs/tabs.test.ts +484 -150
- package/dist/ui/components/tabs/tabs.ts +344 -149
- package/dist/ui/components/textarea/textarea.test.ts +206 -100
- package/dist/ui/components/textarea/textarea.ts +16 -13
- package/dist/ui/components/toast/toast.test.ts +434 -350
- package/dist/ui/components/toast/toast.ts +42 -51
- package/dist/ui/components/toggle/index.ts +1 -0
- package/dist/ui/components/toggle/toggle.test.ts +559 -146
- package/dist/ui/components/toggle/toggle.ts +241 -26
- package/dist/ui/components/toolbar/bar-scroll.test.ts +310 -0
- package/dist/ui/components/toolbar/bar-scroll.ts +372 -0
- package/dist/ui/components/toolbar/index.ts +12 -0
- package/dist/ui/components/toolbar/toolbar.test.ts +438 -0
- package/dist/ui/components/toolbar/toolbar.ts +535 -0
- package/dist/ui/theme/chart-colors.test.ts +8 -3
- package/dist/ui/theme/dark.css +38 -2
- package/dist/ui/theme/default.css +55 -7
- package/dist/ui/theme/icon-colors.test.ts +8 -3
- package/dist/ui/theme/rgb-channels.test.ts +84 -0
- package/dist/ui/theme/tailwind-build.test.ts +114 -0
- package/dist/ui/theme/tailwind-css.test.ts +83 -0
- package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
- package/dist/ui/theme/tailwind-plugin.ts +93 -29
- package/dist/ui/theme/tailwind.css +113 -0
- package/dist/ui/utils/animate.test.ts +13 -1
- package/dist/utils/analyze.d.ts +19 -0
- package/dist/utils/analyze.js +16 -0
- package/dist/utils/analyze.js.map +1 -1
- package/dist/utils/analyze.test.js +14 -1
- package/dist/utils/analyze.test.js.map +1 -1
- package/dist/utils/installed.d.ts +25 -1
- package/dist/utils/installed.js +33 -0
- package/dist/utils/installed.js.map +1 -1
- package/dist/utils/installed.test.js +55 -1
- package/dist/utils/installed.test.js.map +1 -1
- package/dist/utils/resolve-paths.d.ts +4 -4
- package/dist/utils/resolve-paths.js +5 -7
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +9 -5
- package/dist/utils/resolve-paths.test.js.map +1 -1
- package/dist/utils/tailwind.d.ts +45 -0
- package/dist/utils/tailwind.js +90 -0
- package/dist/utils/tailwind.js.map +1 -0
- package/dist/utils/tailwind.test.d.ts +1 -0
- package/dist/utils/tailwind.test.js +134 -0
- package/dist/utils/tailwind.test.js.map +1 -0
- package/package.json +9 -1
- package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -63
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -58
- package/dist/ui/components/button/button-render.test.ts +0 -132
- package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -59
- package/dist/ui/components/chart/chart-render.test.ts +0 -730
- package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -83
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -64
|
@@ -1,115 +1,236 @@
|
|
|
1
|
-
import { signal } from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
waitForUpdate,
|
|
6
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
7
|
+
import {
|
|
8
|
+
type MockInstance,
|
|
9
|
+
afterEach,
|
|
10
|
+
beforeEach,
|
|
11
|
+
describe,
|
|
12
|
+
expect,
|
|
13
|
+
it,
|
|
14
|
+
vi,
|
|
15
|
+
} from 'vitest';
|
|
16
|
+
import {
|
|
17
|
+
type LynxMainThread,
|
|
18
|
+
lynxMainThread,
|
|
19
|
+
} from '../../../test-utils/lynx-main-thread';
|
|
5
20
|
import { UiTextarea } from './textarea';
|
|
6
21
|
|
|
22
|
+
@Component({
|
|
23
|
+
imports: [UiTextarea],
|
|
24
|
+
template: `
|
|
25
|
+
<ui-textarea
|
|
26
|
+
[(value)]="notes"
|
|
27
|
+
[label]="label()"
|
|
28
|
+
placeholder="Add a note"
|
|
29
|
+
[helperText]="helperText()"
|
|
30
|
+
[error]="error()"
|
|
31
|
+
[disabled]="disabled()"
|
|
32
|
+
[minLines]="minLines()"
|
|
33
|
+
[maxLines]="maxLines()"
|
|
34
|
+
class="w-full"
|
|
35
|
+
(changed)="changes.push($event)"
|
|
36
|
+
(focused)="events.push('focused')"
|
|
37
|
+
(blurred)="events.push('blurred')"
|
|
38
|
+
/>
|
|
39
|
+
`,
|
|
40
|
+
})
|
|
41
|
+
class TextareaHost {
|
|
42
|
+
readonly notes = signal('');
|
|
43
|
+
readonly label = signal('');
|
|
44
|
+
readonly helperText = signal('');
|
|
45
|
+
readonly error = signal('');
|
|
46
|
+
readonly disabled = signal(false);
|
|
47
|
+
readonly minLines = signal(3);
|
|
48
|
+
readonly maxLines = signal<number | undefined>(undefined);
|
|
49
|
+
readonly changes: string[] = [];
|
|
50
|
+
readonly events: string[] = [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const renderTextarea = async () => {
|
|
54
|
+
const result = await render(TextareaHost);
|
|
55
|
+
return {
|
|
56
|
+
...result,
|
|
57
|
+
host: result.componentRef.instance as TextareaHost,
|
|
58
|
+
field: () => result.container.querySelector('textarea')!,
|
|
59
|
+
// The field's own column; the host element renders as a view too.
|
|
60
|
+
column: () => result.container.querySelector('.flex-col')!,
|
|
61
|
+
wrapper: () => result.container.querySelector('.bg-muted') as HTMLElement,
|
|
62
|
+
// The layer that draws the focus ring around the field.
|
|
63
|
+
ring: () => result.container.querySelector('.absolute') as HTMLElement,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** `__ElementAnimate`'s first argument for starting an animation. */
|
|
68
|
+
const START = 0;
|
|
69
|
+
/** `__ElementAnimate`'s first argument for canceling an animation. */
|
|
70
|
+
const CANCEL = 3;
|
|
71
|
+
|
|
7
72
|
describe('UiTextarea', () => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
73
|
+
// Watched per test: the testing library resets the main thread after each.
|
|
74
|
+
let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
11
77
|
});
|
|
78
|
+
afterEach(() => animate.mockRestore());
|
|
12
79
|
|
|
13
|
-
it('
|
|
14
|
-
const {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const emitted: string[] = [];
|
|
19
|
-
inst.changed.subscribe((v) => emitted.push(v));
|
|
20
|
-
inst.onInput({ detail: { value: 'hello' } });
|
|
21
|
-
expect(inst.value()).toBe('hello');
|
|
22
|
-
expect(emitted).toEqual(['hello']);
|
|
80
|
+
it('renders a native textarea with its placeholder, and no label', async () => {
|
|
81
|
+
const { field, column, queryByText } = await renderTextarea();
|
|
82
|
+
expect(field().getAttribute('placeholder')).toBe('Add a note');
|
|
83
|
+
expect(column().classList).toContain('w-full');
|
|
84
|
+
expect(queryByText('Add a note')).toBeNull();
|
|
23
85
|
});
|
|
24
86
|
|
|
25
|
-
it('
|
|
26
|
-
const {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
setInputSignal(inst.disabled, true);
|
|
31
|
-
const emitted = vi.fn();
|
|
32
|
-
inst.changed.subscribe(emitted);
|
|
33
|
-
inst.onInput({ detail: { value: 'blocked' } });
|
|
34
|
-
expect(inst.value()).toBe('');
|
|
35
|
-
expect(emitted).not.toHaveBeenCalled();
|
|
87
|
+
it('shows its label', async () => {
|
|
88
|
+
const { host, getByText } = await renderTextarea();
|
|
89
|
+
host.label.set('Notes');
|
|
90
|
+
await waitForUpdate();
|
|
91
|
+
expect(getByText('Notes').classList).toContain('font-medium');
|
|
36
92
|
});
|
|
37
93
|
|
|
38
|
-
it('
|
|
39
|
-
const {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const blurred = vi.fn();
|
|
46
|
-
inst.focused.subscribe(focused);
|
|
47
|
-
inst.blurred.subscribe(blurred);
|
|
48
|
-
inst.onFocus();
|
|
49
|
-
inst.onBlur();
|
|
50
|
-
expect(focused).toHaveBeenCalledTimes(1);
|
|
51
|
-
expect(blurred).toHaveBeenCalledTimes(1);
|
|
94
|
+
it('updates `value` and emits `changed` as the user types', async () => {
|
|
95
|
+
const { host, field } = await renderTextarea();
|
|
96
|
+
// Lynx puts the typed text in the event's detail, not on its target.
|
|
97
|
+
fireEvent.input(field(), { detail: { value: 'Call' } });
|
|
98
|
+
fireEvent.input(field(), { detail: { value: 'Call Sam' } });
|
|
99
|
+
expect(host.notes()).toBe('Call Sam');
|
|
100
|
+
expect(host.changes).toEqual(['Call', 'Call Sam']);
|
|
52
101
|
});
|
|
53
102
|
|
|
54
|
-
it('
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const inst = componentRef.instance as UiTextarea;
|
|
60
|
-
const view = container.querySelector('view') as Element;
|
|
61
|
-
(inst as unknown as { containerRef: unknown }).containerRef = signal({
|
|
62
|
-
nativeElement: view,
|
|
63
|
-
});
|
|
64
|
-
setInputSignal(inst.error, 'Too short');
|
|
103
|
+
it('hides its placeholder while it has a value', async () => {
|
|
104
|
+
// Lynx's iOS textarea would otherwise draw the placeholder over a value
|
|
105
|
+
// the app sets.
|
|
106
|
+
const { host, field } = await renderTextarea();
|
|
107
|
+
host.notes.set('Call Sam');
|
|
65
108
|
await waitForUpdate();
|
|
109
|
+
expect(field().hasAttribute('placeholder')).toBe(false);
|
|
110
|
+
|
|
111
|
+
host.notes.set('');
|
|
66
112
|
await waitForUpdate();
|
|
67
|
-
expect(
|
|
68
|
-
|
|
69
|
-
|
|
113
|
+
expect(field().getAttribute('placeholder')).toBe('Add a note');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("dims and ignores typing while it's disabled", async () => {
|
|
117
|
+
const { host, field, wrapper } = await renderTextarea();
|
|
118
|
+
host.disabled.set(true);
|
|
70
119
|
await waitForUpdate();
|
|
120
|
+
expect(wrapper().classList).toContain('opacity-50');
|
|
121
|
+
|
|
122
|
+
fireEvent.input(field(), { detail: { value: 'blocked' } });
|
|
123
|
+
expect(host.notes()).toBe('');
|
|
124
|
+
expect(host.changes).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('disables the native textarea only while disabled', async () => {
|
|
128
|
+
// Lynx treats any `disabled` value, even false, as disabled, so the
|
|
129
|
+
// attribute has to go rather than be set to false.
|
|
130
|
+
const setAttribute = vi.spyOn(lynxMainThread(), '__SetAttribute');
|
|
131
|
+
const { host, field } = await renderTextarea();
|
|
132
|
+
expect(setAttribute).not.toHaveBeenCalledWith(
|
|
133
|
+
field(),
|
|
134
|
+
'disabled',
|
|
135
|
+
expect.anything(),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
host.disabled.set(true);
|
|
139
|
+
await waitForUpdate();
|
|
140
|
+
expect(setAttribute).toHaveBeenLastCalledWith(field(), 'disabled', 'true');
|
|
141
|
+
setAttribute.mockRestore();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('shows a focus ring while focused, and emits `focused` and `blurred`', async () => {
|
|
145
|
+
const { host, field, ring } = await renderTextarea();
|
|
146
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('0');
|
|
147
|
+
|
|
148
|
+
fireEvent.focus(field());
|
|
71
149
|
await waitForUpdate();
|
|
72
|
-
|
|
150
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('1');
|
|
151
|
+
expect(ring().style.getPropertyValue('box-shadow')).toContain(
|
|
152
|
+
'var(--ring)',
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
fireEvent.blur(field());
|
|
73
156
|
await waitForUpdate();
|
|
157
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('0');
|
|
158
|
+
expect(host.events).toEqual(['focused', 'blurred']);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('shows its helper text', async () => {
|
|
162
|
+
const { host, getByText } = await renderTextarea();
|
|
163
|
+
host.helperText.set('Only you can see this.');
|
|
74
164
|
await waitForUpdate();
|
|
75
|
-
expect(
|
|
165
|
+
expect(getByText('Only you can see this.').classList).toContain(
|
|
166
|
+
'text-muted-foreground',
|
|
167
|
+
);
|
|
76
168
|
});
|
|
77
169
|
|
|
78
|
-
it('
|
|
79
|
-
const {
|
|
80
|
-
|
|
81
|
-
|
|
170
|
+
it('shows an error in place of the helper text, with a destructive ring', async () => {
|
|
171
|
+
const { host, ring, getByText, queryByText } = await renderTextarea();
|
|
172
|
+
host.helperText.set('Only you can see this.');
|
|
173
|
+
host.error.set('Too short');
|
|
82
174
|
await waitForUpdate();
|
|
83
|
-
expect(
|
|
175
|
+
expect(getByText('Too short').classList).toContain('text-destructive');
|
|
176
|
+
expect(queryByText('Only you can see this.')).toBeNull();
|
|
177
|
+
// An invalid field keeps its ring even when it isn't focused.
|
|
178
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('1');
|
|
179
|
+
expect(ring().style.getPropertyValue('box-shadow')).toContain(
|
|
180
|
+
'var(--destructive)',
|
|
181
|
+
);
|
|
84
182
|
});
|
|
85
183
|
|
|
86
|
-
it('
|
|
87
|
-
const {
|
|
88
|
-
const
|
|
89
|
-
|
|
184
|
+
it('shakes when an error appears, but not when it only changes', async () => {
|
|
185
|
+
const { host, column } = await renderTextarea();
|
|
186
|
+
const shakes = () =>
|
|
187
|
+
animate.mock.calls.filter(([, [action]]) => action === START);
|
|
188
|
+
expect(shakes()).toEqual([]);
|
|
189
|
+
|
|
190
|
+
host.error.set('Required');
|
|
191
|
+
await waitForUpdate();
|
|
192
|
+
expect(animate).toHaveBeenCalledWith(column(), [
|
|
193
|
+
START,
|
|
194
|
+
expect.any(String),
|
|
195
|
+
expect.arrayContaining([{ transform: 'translateX(-4px)' }]),
|
|
196
|
+
expect.anything(),
|
|
197
|
+
]);
|
|
198
|
+
expect(shakes()).toHaveLength(1);
|
|
199
|
+
|
|
200
|
+
host.error.set('Too short');
|
|
90
201
|
await waitForUpdate();
|
|
91
|
-
expect(
|
|
202
|
+
expect(shakes()).toHaveLength(1);
|
|
203
|
+
|
|
204
|
+
// Once fixed, the next error shakes again, replacing the last shake.
|
|
205
|
+
host.error.set('');
|
|
206
|
+
await waitForUpdate();
|
|
207
|
+
host.error.set('Required');
|
|
208
|
+
await waitForUpdate();
|
|
209
|
+
expect(shakes()).toHaveLength(2);
|
|
210
|
+
expect(animate).toHaveBeenCalledWith(column(), [
|
|
211
|
+
CANCEL,
|
|
212
|
+
expect.any(String),
|
|
213
|
+
]);
|
|
92
214
|
});
|
|
93
215
|
|
|
94
|
-
it('
|
|
95
|
-
//
|
|
96
|
-
const {
|
|
97
|
-
|
|
98
|
-
|
|
216
|
+
it('opens at minLines tall and grows without limit by default', async () => {
|
|
217
|
+
// Each line is 20px, plus 20px of vertical padding.
|
|
218
|
+
const { host, field, wrapper } = await renderTextarea();
|
|
219
|
+
expect(wrapper().style.getPropertyValue('min-height')).toBe('80px');
|
|
220
|
+
expect(wrapper().style.getPropertyValue('max-height')).toBe('');
|
|
221
|
+
expect(field().hasAttribute('enable-scroll-bar')).toBe(false);
|
|
222
|
+
|
|
223
|
+
host.minLines.set(5);
|
|
99
224
|
await waitForUpdate();
|
|
100
|
-
|
|
101
|
-
const wrapper = container.querySelector('[style*="min-height"]');
|
|
102
|
-
expect(wrapper?.getAttribute('style')).toContain('min-height: 120px');
|
|
103
|
-
expect(wrapper?.getAttribute('style')).not.toContain('max-height');
|
|
225
|
+
expect(wrapper().style.getPropertyValue('min-height')).toBe('120px');
|
|
104
226
|
});
|
|
105
227
|
|
|
106
|
-
it('
|
|
107
|
-
const {
|
|
108
|
-
|
|
109
|
-
setInputSignal(inst.maxLines, 4);
|
|
228
|
+
it('stops growing at maxLines and shows a scroll bar', async () => {
|
|
229
|
+
const { host, field, wrapper } = await renderTextarea();
|
|
230
|
+
host.maxLines.set(4);
|
|
110
231
|
await waitForUpdate();
|
|
111
|
-
|
|
112
|
-
expect(
|
|
232
|
+
expect(wrapper().style.getPropertyValue('max-height')).toBe('100px');
|
|
233
|
+
expect(field().getAttribute('enable-scroll-bar')).toBe('true');
|
|
113
234
|
});
|
|
114
235
|
|
|
115
236
|
it('makes the wrapper a stacking context so the field scrolls with it', async () => {
|
|
@@ -117,22 +238,7 @@ describe('UiTextarea', () => {
|
|
|
117
238
|
// when an ancestor inside the scroll content is a stacking context. Without
|
|
118
239
|
// z-0 on the wrapper, the native field (and its placeholder) stays fixed on
|
|
119
240
|
// screen while the page scrolls.
|
|
120
|
-
const {
|
|
121
|
-
|
|
122
|
-
'[class*="rounded-xl"][class*="bg-muted"]',
|
|
123
|
-
);
|
|
124
|
-
expect(wrapper?.getAttribute('class')?.split(/\s+/)).toContain('z-0');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('applies the disabled dim class when disabled', async () => {
|
|
128
|
-
// Covers the true branch of `disabled() && 'opacity-50'` in textareaWrapperClass.
|
|
129
|
-
const { componentRef, container } = await render(UiTextarea);
|
|
130
|
-
const inst = componentRef.instance as UiTextarea;
|
|
131
|
-
setInputSignal(inst.disabled, true);
|
|
132
|
-
await waitForUpdate();
|
|
133
|
-
const wrapper = container.querySelector(
|
|
134
|
-
'[class*="rounded-xl"][class*="bg-muted"]',
|
|
135
|
-
);
|
|
136
|
-
expect(wrapper?.getAttribute('class')).toContain('opacity-50');
|
|
241
|
+
const { wrapper } = await renderTextarea();
|
|
242
|
+
expect(wrapper().classList).toContain('z-0');
|
|
137
243
|
});
|
|
138
244
|
});
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
type ElementRef,
|
|
3
3
|
Component,
|
|
4
4
|
ViewEncapsulation,
|
|
5
|
+
booleanAttribute,
|
|
5
6
|
computed,
|
|
6
7
|
effect,
|
|
7
8
|
input,
|
|
@@ -95,7 +96,7 @@ export class UiTextarea implements FormValueControl<string> {
|
|
|
95
96
|
readonly placeholder = input<string>('');
|
|
96
97
|
readonly helperText = input<string>('');
|
|
97
98
|
readonly error = input<string>('');
|
|
98
|
-
readonly disabled = input(false);
|
|
99
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
99
100
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
100
101
|
|
|
101
102
|
// SwiftUI-style lineLimit(min...max). The field opens at minLines, grows with
|
|
@@ -113,18 +114,20 @@ export class UiTextarea implements FormValueControl<string> {
|
|
|
113
114
|
#shakeAnim?: AnimationHandle;
|
|
114
115
|
#previousError = '';
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
this.#
|
|
126
|
-
|
|
127
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Shakes the field when an error first appears. A field rather than a
|
|
119
|
+
* constructor: Angular's test build keeps a function for a constructor that
|
|
120
|
+
* nothing calls, which an app's coverage report would show as uncovered.
|
|
121
|
+
*/
|
|
122
|
+
protected readonly shakeOnError = effect(() => {
|
|
123
|
+
const err = this.error();
|
|
124
|
+
const el = this.containerRef()?.nativeElement;
|
|
125
|
+
if (err && !this.#previousError && el) {
|
|
126
|
+
this.#shakeAnim?.cancel();
|
|
127
|
+
this.#shakeAnim = shake(el);
|
|
128
|
+
}
|
|
129
|
+
this.#previousError = err;
|
|
130
|
+
});
|
|
128
131
|
|
|
129
132
|
protected readonly containerClass = computed(() =>
|
|
130
133
|
cn('flex-col gap-1.5 flex', this.userClass()),
|