@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,8 +1,6 @@
|
|
|
1
1
|
import { Component, signal } from '@angular/core';
|
|
2
2
|
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
|
-
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
3
|
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
4
|
import { UiIcon } from './icon';
|
|
7
5
|
import {
|
|
8
6
|
ICON_DARK_COLORS,
|
|
@@ -13,84 +11,97 @@ import {
|
|
|
13
11
|
import { ICONS, type IconName } from './icons';
|
|
14
12
|
|
|
15
13
|
@Component({
|
|
16
|
-
imports: [UiIcon
|
|
17
|
-
template:
|
|
14
|
+
imports: [UiIcon],
|
|
15
|
+
template: `
|
|
16
|
+
<ui-icon
|
|
17
|
+
[name]="name()"
|
|
18
|
+
[size]="size()"
|
|
19
|
+
[color]="color()"
|
|
20
|
+
[class]="className()"
|
|
21
|
+
/>
|
|
22
|
+
`,
|
|
18
23
|
})
|
|
19
24
|
class IconHost {
|
|
20
|
-
name
|
|
21
|
-
size
|
|
22
|
-
color
|
|
25
|
+
readonly name = signal<IconName>('check');
|
|
26
|
+
readonly size = signal<'xs' | 'sm' | 'md' | 'lg'>('md');
|
|
27
|
+
readonly color = signal<string | undefined>(undefined);
|
|
28
|
+
readonly className = signal('');
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
const renderIcon = async (providers: Parameters<typeof render>[1] = {}) => {
|
|
32
|
+
const result = await render(IconHost, providers);
|
|
33
|
+
return {
|
|
34
|
+
...result,
|
|
35
|
+
host: result.componentRef.instance as IconHost,
|
|
36
|
+
svg: () => result.container.querySelector('svg')!,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
25
40
|
describe('UiIcon', () => {
|
|
26
|
-
it('
|
|
27
|
-
const {
|
|
28
|
-
expect(
|
|
41
|
+
it('draws the named icon at the default size', async () => {
|
|
42
|
+
const { svg } = await renderIcon();
|
|
43
|
+
expect(svg().getAttribute('style')).toContain('24px');
|
|
44
|
+
expect(svg().getAttribute('content')).toContain('<svg');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('draws another icon when the name changes', async () => {
|
|
48
|
+
const { host, svg } = await renderIcon();
|
|
49
|
+
const check = svg().getAttribute('content');
|
|
50
|
+
host.name.set('x');
|
|
51
|
+
await waitForUpdate();
|
|
52
|
+
expect(svg().getAttribute('content')).not.toBe(check);
|
|
29
53
|
});
|
|
30
54
|
|
|
31
55
|
it('applies the size input to the width/height style', async () => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const { componentRef, container } = await render(UiIcon);
|
|
35
|
-
setInputSignal((componentRef.instance as UiIcon).name, 'check' as IconName);
|
|
36
|
-
setInputSignal((componentRef.instance as UiIcon).size, 'xs');
|
|
56
|
+
const { host, svg } = await renderIcon();
|
|
57
|
+
host.size.set('xs');
|
|
37
58
|
await waitForUpdate();
|
|
38
|
-
|
|
39
|
-
expect(svg?.getAttribute('style')).toContain('16px');
|
|
59
|
+
expect(svg().getAttribute('style')).toContain('16px');
|
|
40
60
|
});
|
|
41
61
|
|
|
42
62
|
it('substitutes currentColor with the color input value', async () => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
// Lynx's SVG parser reads neither `currentColor` nor CSS variables.
|
|
64
|
+
const { host, svg } = await renderIcon();
|
|
65
|
+
host.color.set('rgb(255, 0, 0)');
|
|
46
66
|
await waitForUpdate();
|
|
47
|
-
|
|
48
|
-
expect(svg
|
|
49
|
-
expect(svg?.getAttribute('content')).not.toContain('currentColor');
|
|
67
|
+
expect(svg().getAttribute('content')).toContain('rgb(255, 0, 0)');
|
|
68
|
+
expect(svg().getAttribute('content')).not.toContain('currentColor');
|
|
50
69
|
});
|
|
51
70
|
|
|
52
71
|
it("draws in the light theme's foreground when no color is set", async () => {
|
|
53
|
-
const {
|
|
54
|
-
|
|
55
|
-
await waitForUpdate();
|
|
56
|
-
const content = container.querySelector('svg')?.getAttribute('content');
|
|
72
|
+
const { svg } = await renderIcon();
|
|
73
|
+
const content = svg().getAttribute('content');
|
|
57
74
|
expect(content).toContain(ICON_LIGHT_COLORS['foreground']);
|
|
58
75
|
expect(content).not.toContain('currentColor');
|
|
59
76
|
});
|
|
60
77
|
|
|
61
78
|
it('resolves a theme token against the active color scheme', async () => {
|
|
62
|
-
const {
|
|
79
|
+
const { host, svg } = await renderIcon({
|
|
63
80
|
providers: [provideIconTheme({ colorScheme: 'dark' })],
|
|
64
81
|
});
|
|
65
|
-
|
|
66
|
-
setInputSignal(icon.name, 'check' as IconName);
|
|
67
|
-
setInputSignal(icon.color, 'var(--primary)');
|
|
82
|
+
host.color.set('var(--primary)');
|
|
68
83
|
await waitForUpdate();
|
|
69
|
-
expect(
|
|
84
|
+
expect(svg().getAttribute('content')).toContain(
|
|
70
85
|
ICON_DARK_COLORS['primary'],
|
|
71
86
|
);
|
|
72
87
|
});
|
|
73
88
|
|
|
74
89
|
it("takes its container's color when it sets none", async () => {
|
|
75
|
-
const {
|
|
90
|
+
const { svg } = await renderIcon({
|
|
76
91
|
providers: [
|
|
77
92
|
{ provide: UI_ICON_COLOR, useValue: signal('var(--destructive)') },
|
|
78
93
|
],
|
|
79
94
|
});
|
|
80
|
-
|
|
81
|
-
await waitForUpdate();
|
|
82
|
-
expect(container.querySelector('svg')?.getAttribute('content')).toContain(
|
|
95
|
+
expect(svg().getAttribute('content')).toContain(
|
|
83
96
|
ICON_LIGHT_COLORS['destructive'],
|
|
84
97
|
);
|
|
85
98
|
});
|
|
86
99
|
|
|
87
100
|
it('applies the class input to the svg', async () => {
|
|
88
|
-
const {
|
|
89
|
-
|
|
90
|
-
setInputSignal(icon.name, 'check' as IconName);
|
|
91
|
-
setInputSignal(icon.userClass, 'mr-2');
|
|
101
|
+
const { host, svg } = await renderIcon();
|
|
102
|
+
host.className.set('mr-2');
|
|
92
103
|
await waitForUpdate();
|
|
93
|
-
expect(
|
|
104
|
+
expect(svg().getAttribute('class')).toBe('mr-2');
|
|
94
105
|
});
|
|
95
106
|
});
|
|
96
107
|
|
|
@@ -1,111 +1,203 @@
|
|
|
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 { UiInput } from './input';
|
|
6
21
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
22
|
+
@Component({
|
|
23
|
+
imports: [UiInput],
|
|
24
|
+
template: `
|
|
25
|
+
<ui-input
|
|
26
|
+
[(value)]="email"
|
|
27
|
+
[label]="label()"
|
|
28
|
+
placeholder="you@example.com"
|
|
29
|
+
type="email"
|
|
30
|
+
[helperText]="helperText()"
|
|
31
|
+
[error]="error()"
|
|
32
|
+
[disabled]="disabled()"
|
|
33
|
+
class="w-full"
|
|
34
|
+
(changed)="changes.push($event)"
|
|
35
|
+
(focused)="events.push('focused')"
|
|
36
|
+
(blurred)="events.push('blurred')"
|
|
37
|
+
/>
|
|
38
|
+
`,
|
|
39
|
+
})
|
|
40
|
+
class InputHost {
|
|
41
|
+
readonly email = signal('');
|
|
42
|
+
readonly label = signal('');
|
|
43
|
+
readonly helperText = signal('');
|
|
44
|
+
readonly error = signal('');
|
|
45
|
+
readonly disabled = signal(false);
|
|
46
|
+
readonly changes: string[] = [];
|
|
47
|
+
readonly events: string[] = [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const renderInput = async () => {
|
|
51
|
+
const result = await render(InputHost);
|
|
52
|
+
return {
|
|
53
|
+
...result,
|
|
54
|
+
host: result.componentRef.instance as InputHost,
|
|
55
|
+
field: () => result.container.querySelector('input')!,
|
|
56
|
+
// The field's own column; the host element renders as a view too.
|
|
57
|
+
column: () => result.container.querySelector('.flex-col')!,
|
|
58
|
+
wrapper: () => result.container.querySelector('.bg-muted')!,
|
|
59
|
+
// The layer that draws the focus ring around the field.
|
|
60
|
+
ring: () => result.container.querySelector('.absolute') as HTMLElement,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** `__ElementAnimate`'s first argument for starting an animation. */
|
|
65
|
+
const START = 0;
|
|
66
|
+
/** `__ElementAnimate`'s first argument for canceling an animation. */
|
|
67
|
+
const CANCEL = 3;
|
|
12
68
|
|
|
13
|
-
|
|
14
|
-
|
|
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']);
|
|
69
|
+
describe('UiInput', () => {
|
|
70
|
+
// Watched per test: the testing library resets the main thread after each.
|
|
71
|
+
let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
23
74
|
});
|
|
75
|
+
afterEach(() => animate.mockRestore());
|
|
24
76
|
|
|
25
|
-
it('
|
|
26
|
-
const {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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();
|
|
77
|
+
it('renders a native input with its placeholder and type, and no label', async () => {
|
|
78
|
+
const { field, column, queryByText } = await renderInput();
|
|
79
|
+
expect(field().getAttribute('placeholder')).toBe('you@example.com');
|
|
80
|
+
expect(field().getAttribute('type')).toBe('email');
|
|
81
|
+
expect(column().classList).toContain('w-full');
|
|
82
|
+
expect(queryByText('you@example.com')).toBeNull();
|
|
36
83
|
});
|
|
37
84
|
|
|
38
|
-
it('
|
|
39
|
-
const {
|
|
40
|
-
|
|
41
|
-
onFocus: () => void;
|
|
42
|
-
};
|
|
43
|
-
const emitted = vi.fn();
|
|
44
|
-
inst.focused.subscribe(emitted);
|
|
45
|
-
inst.onFocus();
|
|
85
|
+
it('shows its label', async () => {
|
|
86
|
+
const { host, getByText } = await renderInput();
|
|
87
|
+
host.label.set('Email');
|
|
46
88
|
await waitForUpdate();
|
|
47
|
-
expect(
|
|
48
|
-
// Focus turns the overlay opacity to 1 via ringVisible().
|
|
49
|
-
const overlay = container.querySelector('[style*="box-shadow"]');
|
|
50
|
-
expect(overlay?.getAttribute('style')).toContain('opacity');
|
|
89
|
+
expect(getByText('Email').classList).toContain('font-medium');
|
|
51
90
|
});
|
|
52
91
|
|
|
53
|
-
it('
|
|
54
|
-
const {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
inst.blurred.subscribe(emitted);
|
|
61
|
-
inst.onFocus();
|
|
62
|
-
inst.onBlur();
|
|
63
|
-
expect(emitted).toHaveBeenCalledTimes(1);
|
|
92
|
+
it('updates `value` and emits `changed` as the user types', async () => {
|
|
93
|
+
const { host, field } = await renderInput();
|
|
94
|
+
// Lynx puts the typed text in the event's detail, not on its target.
|
|
95
|
+
fireEvent.input(field(), { detail: { value: 'jane@' } });
|
|
96
|
+
fireEvent.input(field(), { detail: { value: 'jane@example.com' } });
|
|
97
|
+
expect(host.email()).toBe('jane@example.com');
|
|
98
|
+
expect(host.changes).toEqual(['jane@', 'jane@example.com']);
|
|
64
99
|
});
|
|
65
100
|
|
|
66
|
-
it(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// JIT + jsdom doesn't populate template-ref viewChild queries, so we
|
|
70
|
-
// monkey-patch containerRef with a signal returning a fake ElementRef so the
|
|
71
|
-
// effect's `el` check passes and the shake path runs.
|
|
72
|
-
const { componentRef, container } = await render(UiInput);
|
|
73
|
-
const inst = componentRef.instance as UiInput;
|
|
74
|
-
const view = container.querySelector('view') as Element;
|
|
75
|
-
(inst as unknown as { containerRef: unknown }).containerRef = signal({
|
|
76
|
-
nativeElement: view,
|
|
77
|
-
});
|
|
78
|
-
setInputSignal(inst.error, 'Required');
|
|
79
|
-
await waitForUpdate();
|
|
101
|
+
it("dims and ignores typing while it's disabled", async () => {
|
|
102
|
+
const { host, field, wrapper } = await renderInput();
|
|
103
|
+
host.disabled.set(true);
|
|
80
104
|
await waitForUpdate();
|
|
81
|
-
|
|
82
|
-
|
|
105
|
+
expect(wrapper().classList).toContain('opacity-50');
|
|
106
|
+
|
|
107
|
+
fireEvent.input(field(), { detail: { value: 'blocked' } });
|
|
108
|
+
expect(host.email()).toBe('');
|
|
109
|
+
expect(host.changes).toEqual([]);
|
|
110
|
+
});
|
|
83
111
|
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
|
|
112
|
+
it('disables the native input only while disabled', async () => {
|
|
113
|
+
// Lynx treats any `disabled` value, even false, as disabled, so the
|
|
114
|
+
// attribute has to go rather than be set to false.
|
|
115
|
+
const setAttribute = vi.spyOn(lynxMainThread(), '__SetAttribute');
|
|
116
|
+
const { host, field } = await renderInput();
|
|
117
|
+
expect(setAttribute).not.toHaveBeenCalledWith(
|
|
118
|
+
field(),
|
|
119
|
+
'disabled',
|
|
120
|
+
expect.anything(),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
host.disabled.set(true);
|
|
87
124
|
await waitForUpdate();
|
|
125
|
+
expect(setAttribute).toHaveBeenLastCalledWith(field(), 'disabled', 'true');
|
|
126
|
+
setAttribute.mockRestore();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('shows a focus ring while focused, and emits `focused` and `blurred`', async () => {
|
|
130
|
+
const { host, field, ring } = await renderInput();
|
|
131
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('0');
|
|
132
|
+
|
|
133
|
+
fireEvent.focus(field());
|
|
88
134
|
await waitForUpdate();
|
|
89
|
-
|
|
135
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('1');
|
|
136
|
+
expect(ring().style.getPropertyValue('box-shadow')).toContain(
|
|
137
|
+
'var(--ring)',
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
fireEvent.blur(field());
|
|
90
141
|
await waitForUpdate();
|
|
142
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('0');
|
|
143
|
+
expect(host.events).toEqual(['focused', 'blurred']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('shows its helper text', async () => {
|
|
147
|
+
const { host, getByText } = await renderInput();
|
|
148
|
+
host.helperText.set("We'll never share it.");
|
|
91
149
|
await waitForUpdate();
|
|
92
|
-
expect(
|
|
150
|
+
expect(getByText("We'll never share it.").classList).toContain(
|
|
151
|
+
'text-muted-foreground',
|
|
152
|
+
);
|
|
93
153
|
});
|
|
94
154
|
|
|
95
|
-
it('
|
|
96
|
-
const {
|
|
97
|
-
|
|
98
|
-
|
|
155
|
+
it('shows an error in place of the helper text, with a destructive ring', async () => {
|
|
156
|
+
const { host, ring, getByText, queryByText } = await renderInput();
|
|
157
|
+
host.helperText.set("We'll never share it.");
|
|
158
|
+
host.error.set('Enter a valid email');
|
|
99
159
|
await waitForUpdate();
|
|
100
|
-
expect(
|
|
160
|
+
expect(getByText('Enter a valid email').classList).toContain(
|
|
161
|
+
'text-destructive',
|
|
162
|
+
);
|
|
163
|
+
expect(queryByText("We'll never share it.")).toBeNull();
|
|
164
|
+
// An invalid field keeps its ring even when it isn't focused.
|
|
165
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('1');
|
|
166
|
+
expect(ring().style.getPropertyValue('box-shadow')).toContain(
|
|
167
|
+
'var(--destructive)',
|
|
168
|
+
);
|
|
101
169
|
});
|
|
102
170
|
|
|
103
|
-
it('
|
|
104
|
-
const {
|
|
105
|
-
const
|
|
106
|
-
|
|
171
|
+
it('shakes when an error appears, but not when it only changes', async () => {
|
|
172
|
+
const { host, column } = await renderInput();
|
|
173
|
+
const shakes = () =>
|
|
174
|
+
animate.mock.calls.filter(([, [action]]) => action === START);
|
|
175
|
+
expect(shakes()).toEqual([]);
|
|
176
|
+
|
|
177
|
+
host.error.set('Required');
|
|
178
|
+
await waitForUpdate();
|
|
179
|
+
expect(animate).toHaveBeenCalledWith(column(), [
|
|
180
|
+
START,
|
|
181
|
+
expect.any(String),
|
|
182
|
+
expect.arrayContaining([{ transform: 'translateX(-4px)' }]),
|
|
183
|
+
expect.anything(),
|
|
184
|
+
]);
|
|
185
|
+
expect(shakes()).toHaveLength(1);
|
|
186
|
+
|
|
187
|
+
host.error.set('Enter a valid email');
|
|
107
188
|
await waitForUpdate();
|
|
108
|
-
expect(
|
|
189
|
+
expect(shakes()).toHaveLength(1);
|
|
190
|
+
|
|
191
|
+
// Once fixed, the next error shakes again, replacing the last shake.
|
|
192
|
+
host.error.set('');
|
|
193
|
+
await waitForUpdate();
|
|
194
|
+
host.error.set('Required');
|
|
195
|
+
await waitForUpdate();
|
|
196
|
+
expect(shakes()).toHaveLength(2);
|
|
197
|
+
expect(animate).toHaveBeenCalledWith(column(), [
|
|
198
|
+
CANCEL,
|
|
199
|
+
expect.any(String),
|
|
200
|
+
]);
|
|
109
201
|
});
|
|
110
202
|
|
|
111
203
|
it('makes the wrapper a stacking context so the field scrolls with it', async () => {
|
|
@@ -113,22 +205,7 @@ describe('UiInput', () => {
|
|
|
113
205
|
// when an ancestor inside the scroll content is a stacking context. Without
|
|
114
206
|
// z-0 on the wrapper, the native field (and its placeholder) stays fixed on
|
|
115
207
|
// screen while the page scrolls.
|
|
116
|
-
const {
|
|
117
|
-
|
|
118
|
-
'[class*="rounded-xl"][class*="bg-muted"]',
|
|
119
|
-
);
|
|
120
|
-
expect(wrapper?.getAttribute('class')?.split(/\s+/)).toContain('z-0');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('applies the disabled dim class when disabled', async () => {
|
|
124
|
-
// Covers the true branch of `disabled() && 'opacity-50'` in inputWrapperClass.
|
|
125
|
-
const { componentRef, container } = await render(UiInput);
|
|
126
|
-
const inst = componentRef.instance as UiInput;
|
|
127
|
-
setInputSignal(inst.disabled, true);
|
|
128
|
-
await waitForUpdate();
|
|
129
|
-
const wrapper = container.querySelector(
|
|
130
|
-
'[class*="rounded-xl"][class*="bg-muted"]',
|
|
131
|
-
);
|
|
132
|
-
expect(wrapper?.getAttribute('class')).toContain('opacity-50');
|
|
208
|
+
const { wrapper } = await renderInput();
|
|
209
|
+
expect(wrapper().classList).toContain('z-0');
|
|
133
210
|
});
|
|
134
211
|
});
|
|
@@ -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,
|
|
@@ -90,7 +91,7 @@ export class UiInput implements FormValueControl<string> {
|
|
|
90
91
|
readonly type = input<string>('text');
|
|
91
92
|
readonly helperText = input<string>('');
|
|
92
93
|
readonly error = input<string>('');
|
|
93
|
-
readonly disabled = input(false);
|
|
94
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
94
95
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
95
96
|
|
|
96
97
|
readonly changed = output<string>();
|
|
@@ -102,18 +103,21 @@ export class UiInput implements FormValueControl<string> {
|
|
|
102
103
|
#shakeAnim?: AnimationHandle;
|
|
103
104
|
#previousError = '';
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Shakes the field when an error appears (a change from no error to one). A
|
|
108
|
+
* field rather than a constructor: Angular's test build keeps a function for
|
|
109
|
+
* a constructor that nothing calls, which an app's coverage report would show
|
|
110
|
+
* as uncovered.
|
|
111
|
+
*/
|
|
112
|
+
protected readonly shakeOnError = effect(() => {
|
|
113
|
+
const err = this.error();
|
|
114
|
+
const el = this.containerRef()?.nativeElement;
|
|
115
|
+
if (err && !this.#previousError && el) {
|
|
116
|
+
this.#shakeAnim?.cancel();
|
|
117
|
+
this.#shakeAnim = shake(el);
|
|
118
|
+
}
|
|
119
|
+
this.#previousError = err;
|
|
120
|
+
});
|
|
117
121
|
|
|
118
122
|
protected readonly containerClass = computed(() =>
|
|
119
123
|
cn('flex-col gap-1.5 flex', this.userClass()),
|
|
@@ -1,38 +1,25 @@
|
|
|
1
|
-
import { Component } from '@angular/core';
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
2
|
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
|
-
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
3
|
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
4
|
import { UiLabel } from './label';
|
|
7
5
|
|
|
8
6
|
@Component({
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
template: `<ui-label>{{ label }}</ui-label>`,
|
|
7
|
+
imports: [UiLabel],
|
|
8
|
+
template: `<ui-label [disabled]="disabled()">Name</ui-label>`,
|
|
12
9
|
})
|
|
13
10
|
class LabelHost {
|
|
14
|
-
|
|
11
|
+
readonly disabled = signal(false);
|
|
15
12
|
}
|
|
16
13
|
|
|
17
14
|
describe('UiLabel', () => {
|
|
18
|
-
it('renders
|
|
19
|
-
const { getByText } = await render(LabelHost);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
it('renders standalone with no host wrapper (default disabled=false)', async () => {
|
|
24
|
-
const { container } = await render(UiLabel);
|
|
25
|
-
const textEl = container.querySelector('text');
|
|
26
|
-
expect(textEl?.getAttribute('class')).toContain('font-medium');
|
|
27
|
-
});
|
|
15
|
+
it('renders its text, dimmed while disabled', async () => {
|
|
16
|
+
const { componentRef, getByText } = await render(LabelHost);
|
|
17
|
+
const label = getByText('Name');
|
|
18
|
+
expect(label.classList).toContain('font-medium');
|
|
19
|
+
expect(label.classList).not.toContain('opacity-50');
|
|
28
20
|
|
|
29
|
-
|
|
30
|
-
// JIT template bindings don't wire signal inputs — drive the input
|
|
31
|
-
// directly to exercise the `disabled() && 'opacity-50'` truthy branch.
|
|
32
|
-
const { componentRef, container } = await render(UiLabel);
|
|
33
|
-
setInputSignal((componentRef.instance as UiLabel).disabled, true);
|
|
21
|
+
(componentRef.instance as LabelHost).disabled.set(true);
|
|
34
22
|
await waitForUpdate();
|
|
35
|
-
|
|
36
|
-
expect(textEl?.getAttribute('class')).toContain('opacity-50');
|
|
23
|
+
expect(label.classList).toContain('opacity-50');
|
|
37
24
|
});
|
|
38
25
|
});
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ViewEncapsulation,
|
|
4
|
+
booleanAttribute,
|
|
5
|
+
computed,
|
|
6
|
+
input,
|
|
7
|
+
} from '@angular/core';
|
|
2
8
|
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
3
9
|
|
|
4
10
|
import { cn } from '../../utils/cn';
|
|
@@ -11,7 +17,7 @@ import { cn } from '../../utils/cn';
|
|
|
11
17
|
template: `<text [class]="labelClass()"><ng-content /></text>`,
|
|
12
18
|
})
|
|
13
19
|
export class UiLabel {
|
|
14
|
-
readonly disabled = input(false);
|
|
20
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
15
21
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
16
22
|
|
|
17
23
|
protected readonly labelClass = computed(() =>
|