@blotch/dolan 0.0.0-stage → 22.2.1
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.d.ts +22 -0
- package/dist/commands/add.js +132 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/add.test.d.ts +1 -0
- package/dist/commands/add.test.js +209 -0
- package/dist/commands/add.test.js.map +1 -0
- package/dist/commands/diff.d.ts +13 -0
- package/dist/commands/diff.js +155 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/diff.test.d.ts +1 -0
- package/dist/commands/diff.test.js +327 -0
- package/dist/commands/diff.test.js.map +1 -0
- package/dist/commands/doctor.d.ts +15 -0
- package/dist/commands/doctor.js +248 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/doctor.test.d.ts +1 -0
- package/dist/commands/doctor.test.js +301 -0
- package/dist/commands/doctor.test.js.map +1 -0
- package/dist/commands/eject.d.ts +11 -0
- package/dist/commands/eject.js +55 -0
- package/dist/commands/eject.js.map +1 -0
- package/dist/commands/eject.test.d.ts +1 -0
- package/dist/commands/eject.test.js +151 -0
- package/dist/commands/eject.test.js.map +1 -0
- package/dist/commands/info.d.ts +1 -0
- package/dist/commands/info.js +73 -0
- package/dist/commands/info.js.map +1 -0
- package/dist/commands/info.test.d.ts +1 -0
- package/dist/commands/info.test.js +143 -0
- package/dist/commands/info.test.js.map +1 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +98 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/commands/init.test.js +154 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/commands/list.d.ts +16 -0
- package/dist/commands/list.js +124 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/list.test.d.ts +1 -0
- package/dist/commands/list.test.js +285 -0
- package/dist/commands/list.test.js.map +1 -0
- package/dist/commands/outdated.d.ts +13 -0
- package/dist/commands/outdated.js +116 -0
- package/dist/commands/outdated.js.map +1 -0
- package/dist/commands/outdated.test.d.ts +1 -0
- package/dist/commands/outdated.test.js +208 -0
- package/dist/commands/outdated.test.js.map +1 -0
- package/dist/commands/remove.d.ts +18 -0
- package/dist/commands/remove.js +161 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/remove.test.d.ts +1 -0
- package/dist/commands/remove.test.js +361 -0
- package/dist/commands/remove.test.js.map +1 -0
- package/dist/commands/theme.d.ts +1 -0
- package/dist/commands/theme.js +66 -0
- package/dist/commands/theme.js.map +1 -0
- package/dist/commands/theme.test.d.ts +1 -0
- package/dist/commands/theme.test.js +133 -0
- package/dist/commands/theme.test.js.map +1 -0
- package/dist/commands/update.d.ts +31 -0
- package/dist/commands/update.js +601 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/update.test.d.ts +1 -0
- package/dist/commands/update.test.js +1233 -0
- package/dist/commands/update.test.js.map +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +48 -0
- package/dist/config.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/lockfile.d.ts +21 -0
- package/dist/lockfile.js +39 -0
- package/dist/lockfile.js.map +1 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +85 -0
- package/dist/registry.js.map +1 -0
- package/dist/registry.test.d.ts +1 -0
- package/dist/registry.test.js +95 -0
- package/dist/registry.test.js.map +1 -0
- package/dist/test-utils.d.ts +16 -0
- package/dist/test-utils.js +65 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/ui/components/accordion/accordion.test.ts +194 -0
- package/dist/ui/components/accordion/accordion.ts +173 -0
- package/dist/ui/components/accordion/index.ts +6 -0
- package/dist/ui/components/action-sheet/action-sheet.test.ts +211 -0
- package/dist/ui/components/action-sheet/action-sheet.ts +353 -0
- package/dist/ui/components/action-sheet/index.ts +6 -0
- package/dist/ui/components/alert/alert.test.ts +48 -0
- package/dist/ui/components/alert/alert.ts +104 -0
- package/dist/ui/components/alert/index.ts +1 -0
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +141 -0
- package/dist/ui/components/alert-dialog/alert-dialog.ts +243 -0
- package/dist/ui/components/alert-dialog/index.ts +7 -0
- package/dist/ui/components/area-chart/area-chart-render.test.ts +121 -0
- package/dist/ui/components/area-chart/area-chart.test.ts +158 -0
- package/dist/ui/components/area-chart/area-chart.ts +349 -0
- package/dist/ui/components/area-chart/index.ts +6 -0
- package/dist/ui/components/aspect-ratio/aspect-ratio.test.ts +10 -0
- package/dist/ui/components/aspect-ratio/aspect-ratio.ts +28 -0
- package/dist/ui/components/aspect-ratio/index.ts +1 -0
- package/dist/ui/components/avatar/avatar.test.ts +69 -0
- package/dist/ui/components/avatar/avatar.ts +131 -0
- package/dist/ui/components/avatar/index.ts +1 -0
- package/dist/ui/components/badge/badge.test.ts +49 -0
- package/dist/ui/components/badge/badge.ts +97 -0
- package/dist/ui/components/badge/index.ts +1 -0
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +174 -0
- package/dist/ui/components/bar-chart/bar-chart.test.ts +73 -0
- package/dist/ui/components/bar-chart/bar-chart.ts +327 -0
- package/dist/ui/components/bar-chart/index.ts +6 -0
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +163 -0
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +339 -0
- package/dist/ui/components/bottom-sheet/index.ts +1 -0
- package/dist/ui/components/button/button-render.test.ts +113 -0
- package/dist/ui/components/button/button.test.ts +70 -0
- package/dist/ui/components/button/button.ts +170 -0
- package/dist/ui/components/button/index.ts +6 -0
- package/dist/ui/components/button-group/button-group-render.test.ts +146 -0
- package/dist/ui/components/button-group/button-group.test.ts +54 -0
- package/dist/ui/components/button-group/button-group.ts +209 -0
- package/dist/ui/components/button-group/index.ts +8 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +174 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +96 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.ts +355 -0
- package/dist/ui/components/candlestick-chart/index.ts +7 -0
- package/dist/ui/components/card/card.test.ts +110 -0
- package/dist/ui/components/card/card.ts +162 -0
- package/dist/ui/components/card/index.ts +8 -0
- package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +188 -0
- package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +549 -0
- package/dist/ui/components/cartesian-chart/cartesian-chart.ts +1140 -0
- package/dist/ui/components/cartesian-chart/index.ts +18 -0
- package/dist/ui/components/checkbox/checkbox.test.ts +59 -0
- package/dist/ui/components/checkbox/checkbox.ts +96 -0
- package/dist/ui/components/checkbox/index.ts +1 -0
- package/dist/ui/components/collapsible/collapsible.test.ts +112 -0
- package/dist/ui/components/collapsible/collapsible.ts +122 -0
- package/dist/ui/components/collapsible/index.ts +5 -0
- package/dist/ui/components/dialog/dialog.test.ts +157 -0
- package/dist/ui/components/dialog/dialog.ts +248 -0
- package/dist/ui/components/dialog/index.ts +7 -0
- package/dist/ui/components/empty-state/empty-state.test.ts +60 -0
- package/dist/ui/components/empty-state/empty-state.ts +46 -0
- package/dist/ui/components/empty-state/index.ts +1 -0
- package/dist/ui/components/icon/icon.test.ts +52 -0
- package/dist/ui/components/icon/icon.ts +54 -0
- package/dist/ui/components/icon/icons.ts +569 -0
- package/dist/ui/components/icon/index.ts +2 -0
- package/dist/ui/components/input/index.ts +1 -0
- package/dist/ui/components/input/input.test.ts +134 -0
- package/dist/ui/components/input/input.ts +189 -0
- package/dist/ui/components/label/index.ts +1 -0
- package/dist/ui/components/label/label.test.ts +38 -0
- package/dist/ui/components/label/label.ts +24 -0
- package/dist/ui/components/line-chart/index.ts +6 -0
- package/dist/ui/components/line-chart/line-chart-render.test.ts +121 -0
- package/dist/ui/components/line-chart/line-chart.test.ts +81 -0
- package/dist/ui/components/line-chart/line-chart.ts +273 -0
- package/dist/ui/components/list/index.ts +1 -0
- package/dist/ui/components/list/list.test.ts +35 -0
- package/dist/ui/components/list/list.ts +71 -0
- package/dist/ui/components/nav-drawer/index.ts +8 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +267 -0
- package/dist/ui/components/nav-drawer/nav-drawer.ts +504 -0
- package/dist/ui/components/pagination/index.ts +1 -0
- package/dist/ui/components/pagination/pagination.test.ts +96 -0
- package/dist/ui/components/pagination/pagination.ts +147 -0
- package/dist/ui/components/progress/index.ts +1 -0
- package/dist/ui/components/progress/progress.test.ts +71 -0
- package/dist/ui/components/progress/progress.ts +118 -0
- package/dist/ui/components/radio-group/index.ts +1 -0
- package/dist/ui/components/radio-group/radio-group.test.ts +115 -0
- package/dist/ui/components/radio-group/radio-group.ts +129 -0
- package/dist/ui/components/scatter-chart/index.ts +6 -0
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +157 -0
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +57 -0
- package/dist/ui/components/scatter-chart/scatter-chart.ts +277 -0
- package/dist/ui/components/scroll-area/index.ts +1 -0
- package/dist/ui/components/scroll-area/scroll-area.test.ts +40 -0
- package/dist/ui/components/scroll-area/scroll-area.ts +73 -0
- package/dist/ui/components/select/index.ts +1 -0
- package/dist/ui/components/select/select.test.ts +107 -0
- package/dist/ui/components/select/select.ts +146 -0
- package/dist/ui/components/separator/index.ts +1 -0
- package/dist/ui/components/separator/separator.test.ts +26 -0
- package/dist/ui/components/separator/separator.ts +45 -0
- package/dist/ui/components/sheet/index.ts +7 -0
- package/dist/ui/components/sheet/sheet.test.ts +44 -0
- package/dist/ui/components/sheet/sheet.ts +114 -0
- package/dist/ui/components/skeleton/index.ts +1 -0
- package/dist/ui/components/skeleton/skeleton.test.ts +13 -0
- package/dist/ui/components/skeleton/skeleton.ts +45 -0
- package/dist/ui/components/spinner/index.ts +1 -0
- package/dist/ui/components/spinner/spinner.test.ts +39 -0
- package/dist/ui/components/spinner/spinner.ts +83 -0
- package/dist/ui/components/switch/index.ts +1 -0
- package/dist/ui/components/switch/switch.test.ts +63 -0
- package/dist/ui/components/switch/switch.ts +98 -0
- package/dist/ui/components/tabs/index.ts +1 -0
- package/dist/ui/components/tabs/tabs.test.ts +199 -0
- package/dist/ui/components/tabs/tabs.ts +317 -0
- package/dist/ui/components/textarea/index.ts +1 -0
- package/dist/ui/components/textarea/textarea.test.ts +138 -0
- package/dist/ui/components/textarea/textarea.ts +221 -0
- package/dist/ui/components/toast/index.ts +9 -0
- package/dist/ui/components/toast/toast-state.test.ts +54 -0
- package/dist/ui/components/toast/toast-state.ts +46 -0
- package/dist/ui/components/toast/toast.test.ts +342 -0
- package/dist/ui/components/toast/toast.ts +527 -0
- package/dist/ui/components/toggle/index.ts +7 -0
- package/dist/ui/components/toggle/toggle.test.ts +186 -0
- package/dist/ui/components/toggle/toggle.ts +218 -0
- package/dist/ui/components/typography/index.ts +5 -0
- package/dist/ui/components/typography/typography.test.ts +55 -0
- package/dist/ui/components/typography/typography.ts +44 -0
- package/dist/ui/theme/dark.css +41 -0
- package/dist/ui/theme/default.css +56 -0
- package/dist/ui/theme/tailwind-plugin.test.ts +124 -0
- package/dist/ui/theme/tailwind-plugin.ts +151 -0
- package/dist/ui/utils/animate.test.ts +324 -0
- package/dist/ui/utils/animate.ts +544 -0
- package/dist/ui/utils/cn.test.ts +27 -0
- package/dist/ui/utils/cn.ts +6 -0
- package/dist/utils/analyze.d.ts +32 -0
- package/dist/utils/analyze.js +109 -0
- package/dist/utils/analyze.js.map +1 -0
- package/dist/utils/analyze.test.d.ts +1 -0
- package/dist/utils/analyze.test.js +146 -0
- package/dist/utils/analyze.test.js.map +1 -0
- package/dist/utils/animate.d.ts +210 -0
- package/dist/utils/animate.js +423 -0
- package/dist/utils/animate.js.map +1 -0
- package/dist/utils/animate.test.d.ts +1 -0
- package/dist/utils/animate.test.js +365 -0
- package/dist/utils/animate.test.js.map +1 -0
- package/dist/utils/banner.d.ts +1 -0
- package/dist/utils/banner.js +15 -0
- package/dist/utils/banner.js.map +1 -0
- package/dist/utils/banner.test.d.ts +1 -0
- package/dist/utils/banner.test.js +29 -0
- package/dist/utils/banner.test.js.map +1 -0
- package/dist/utils/cn.d.ts +2 -0
- package/dist/utils/cn.js +6 -0
- package/dist/utils/cn.js.map +1 -0
- package/dist/utils/cn.test.d.ts +1 -0
- package/dist/utils/cn.test.js +21 -0
- package/dist/utils/cn.test.js.map +1 -0
- package/dist/utils/format.d.ts +31 -0
- package/dist/utils/format.js +95 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/format.test.d.ts +1 -0
- package/dist/utils/format.test.js +71 -0
- package/dist/utils/format.test.js.map +1 -0
- package/dist/utils/logger.d.ts +6 -0
- package/dist/utils/logger.js +16 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/logger.test.d.ts +1 -0
- package/dist/utils/logger.test.js +36 -0
- package/dist/utils/logger.test.js.map +1 -0
- package/dist/utils/resolve-paths.d.ts +12 -0
- package/dist/utils/resolve-paths.js +29 -0
- package/dist/utils/resolve-paths.js.map +1 -0
- package/dist/utils/resolve-paths.test.d.ts +1 -0
- package/dist/utils/resolve-paths.test.js +52 -0
- package/dist/utils/resolve-paths.test.js.map +1 -0
- package/package.json +56 -4
- package/README.md +0 -3
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
|
+
import { UiBottomSheet } from './bottom-sheet';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [UiBottomSheet, LYNX_ELEMENTS],
|
|
11
|
+
template: `<ui-bottom-sheet [(open)]="open"
|
|
12
|
+
><text>Panel</text></ui-bottom-sheet
|
|
13
|
+
>`,
|
|
14
|
+
})
|
|
15
|
+
class BottomSheetHost {
|
|
16
|
+
open = true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('UiBottomSheet', () => {
|
|
20
|
+
const patchRefs = (inst: UiBottomSheet, view: Element) => {
|
|
21
|
+
(inst as unknown as { backdropRef: unknown }).backdropRef = signal({
|
|
22
|
+
nativeElement: view,
|
|
23
|
+
});
|
|
24
|
+
(inst as unknown as { panelRef: unknown }).panelRef = signal({
|
|
25
|
+
nativeElement: view,
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
it('renders the panel content when open', async () => {
|
|
30
|
+
const { getByText } = await render(BottomSheetHost);
|
|
31
|
+
expect(getByText('Panel')).toBeTruthy();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('backdrop tap closes the sheet', async () => {
|
|
35
|
+
const { componentRef } = await render(UiBottomSheet);
|
|
36
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
37
|
+
inst.open.set(true);
|
|
38
|
+
await waitForUpdate();
|
|
39
|
+
(inst as unknown as { onBackdropTap: () => void }).onBackdropTap();
|
|
40
|
+
expect(inst.open()).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('panel tap is a no-op', async () => {
|
|
44
|
+
const { componentRef } = await render(UiBottomSheet);
|
|
45
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
46
|
+
expect(() =>
|
|
47
|
+
(inst as unknown as { onPanelTap: () => void }).onPanelTap(),
|
|
48
|
+
).not.toThrow();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('open effect runs the animate-in and animate-out lifecycles', async () => {
|
|
52
|
+
const { componentRef, container } = await render(UiBottomSheet);
|
|
53
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
54
|
+
const view = container.querySelector('view') as Element;
|
|
55
|
+
patchRefs(inst, view);
|
|
56
|
+
|
|
57
|
+
inst.open.set(true);
|
|
58
|
+
await waitForUpdate();
|
|
59
|
+
await waitForUpdate();
|
|
60
|
+
await waitForUpdate();
|
|
61
|
+
|
|
62
|
+
inst.open.set(false);
|
|
63
|
+
await new Promise((r) => setTimeout(r, 320));
|
|
64
|
+
await waitForUpdate();
|
|
65
|
+
expect(inst.open()).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('open effect bails when backdrop/panel refs are undefined', async () => {
|
|
69
|
+
const { componentRef } = await render(UiBottomSheet);
|
|
70
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
71
|
+
inst.open.set(true);
|
|
72
|
+
await waitForUpdate();
|
|
73
|
+
await waitForUpdate();
|
|
74
|
+
inst.open.set(false);
|
|
75
|
+
await waitForUpdate();
|
|
76
|
+
expect(inst.open()).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('drag beyond the dismiss threshold closes the sheet', async () => {
|
|
80
|
+
const { componentRef, container } = await render(UiBottomSheet);
|
|
81
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
82
|
+
const view = container.querySelector('view') as Element;
|
|
83
|
+
patchRefs(inst, view);
|
|
84
|
+
inst.open.set(true);
|
|
85
|
+
await waitForUpdate();
|
|
86
|
+
|
|
87
|
+
const cast = inst as unknown as {
|
|
88
|
+
onHandleTouchStart: (e: { touches: { clientY: number }[] }) => void;
|
|
89
|
+
onHandleTouchMove: (e: { touches: { clientY: number }[] }) => void;
|
|
90
|
+
onHandleTouchEnd: () => void;
|
|
91
|
+
};
|
|
92
|
+
cast.onHandleTouchStart({ touches: [{ clientY: 100 }] });
|
|
93
|
+
cast.onHandleTouchMove({ touches: [{ clientY: 300 }] });
|
|
94
|
+
cast.onHandleTouchEnd();
|
|
95
|
+
await waitForUpdate();
|
|
96
|
+
expect(inst.open()).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('drag below the dismiss threshold snaps back to open', async () => {
|
|
100
|
+
const { componentRef, container } = await render(UiBottomSheet);
|
|
101
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
102
|
+
const view = container.querySelector('view') as Element;
|
|
103
|
+
patchRefs(inst, view);
|
|
104
|
+
inst.open.set(true);
|
|
105
|
+
await waitForUpdate();
|
|
106
|
+
|
|
107
|
+
const cast = inst as unknown as {
|
|
108
|
+
onHandleTouchStart: (e: { touches: { clientY: number }[] }) => void;
|
|
109
|
+
onHandleTouchMove: (e: { touches: { clientY: number }[] }) => void;
|
|
110
|
+
onHandleTouchEnd: () => void;
|
|
111
|
+
};
|
|
112
|
+
cast.onHandleTouchStart({ touches: [{ clientY: 100 }] });
|
|
113
|
+
cast.onHandleTouchMove({ touches: [{ clientY: 130 }] });
|
|
114
|
+
cast.onHandleTouchEnd();
|
|
115
|
+
await waitForUpdate();
|
|
116
|
+
expect(inst.open()).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('touch handlers guard against missing touches and non-active drags', async () => {
|
|
120
|
+
const { componentRef } = await render(UiBottomSheet);
|
|
121
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
122
|
+
const cast = inst as unknown as {
|
|
123
|
+
onHandleTouchStart: (e: { touches?: { clientY: number }[] }) => void;
|
|
124
|
+
onHandleTouchMove: (e: { touches?: { clientY: number }[] }) => void;
|
|
125
|
+
onHandleTouchEnd: () => void;
|
|
126
|
+
};
|
|
127
|
+
// touchmove without prior touchstart is a no-op (guards `#isDragging`).
|
|
128
|
+
cast.onHandleTouchMove({ touches: [{ clientY: 200 }] });
|
|
129
|
+
// touchstart / touchmove without touches entries is a no-op.
|
|
130
|
+
cast.onHandleTouchStart({ touches: [] });
|
|
131
|
+
cast.onHandleTouchStart({ touches: [{ clientY: 100 }] });
|
|
132
|
+
cast.onHandleTouchMove({ touches: [] });
|
|
133
|
+
// touchend without an active drag is also a no-op.
|
|
134
|
+
cast.onHandleTouchEnd();
|
|
135
|
+
cast.onHandleTouchEnd();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('applies a user class on the panel', async () => {
|
|
139
|
+
const { componentRef, container } = await render(UiBottomSheet);
|
|
140
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
141
|
+
setInputSignal(inst.userClass, 'custom-panel');
|
|
142
|
+
await waitForUpdate();
|
|
143
|
+
expect(
|
|
144
|
+
Array.from(container.querySelectorAll('view')).some((el) =>
|
|
145
|
+
el.getAttribute('class')?.includes('custom-panel'),
|
|
146
|
+
),
|
|
147
|
+
).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('overlayStyle switches between visible and hidden variants', async () => {
|
|
151
|
+
const { componentRef, container } = await render(UiBottomSheet);
|
|
152
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
153
|
+
let overlay = container.querySelector('overlay');
|
|
154
|
+
expect(overlay?.getAttribute('style')).toContain('display: none');
|
|
155
|
+
|
|
156
|
+
const view = container.querySelector('view') as Element;
|
|
157
|
+
patchRefs(inst, view);
|
|
158
|
+
inst.open.set(true);
|
|
159
|
+
for (let i = 0; i < 10; i++) await waitForUpdate();
|
|
160
|
+
overlay = container.querySelector('overlay');
|
|
161
|
+
expect(overlay?.getAttribute('style')).not.toContain('display: none');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// cspell:words WAAPI tweens
|
|
2
|
+
import {
|
|
3
|
+
type ElementRef,
|
|
4
|
+
Component,
|
|
5
|
+
ViewEncapsulation,
|
|
6
|
+
computed,
|
|
7
|
+
effect,
|
|
8
|
+
input,
|
|
9
|
+
model,
|
|
10
|
+
output,
|
|
11
|
+
signal,
|
|
12
|
+
viewChild,
|
|
13
|
+
} from '@angular/core';
|
|
14
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
type AnimationHandle,
|
|
18
|
+
DURATION,
|
|
19
|
+
EASING,
|
|
20
|
+
fadeIn,
|
|
21
|
+
fadeOut,
|
|
22
|
+
slideIn,
|
|
23
|
+
slideOut,
|
|
24
|
+
} from '../../utils/animate';
|
|
25
|
+
import { cn } from '../../utils/cn';
|
|
26
|
+
|
|
27
|
+
// Drag-to-dismiss tuning for the handle strip at the top of the sheet.
|
|
28
|
+
//
|
|
29
|
+
// Minimum downward travel (px) before releasing dismisses the sheet instead of
|
|
30
|
+
// snapping it back — small enough to feel responsive, large enough that a stray
|
|
31
|
+
// tap or tiny movement doesn't accidentally close it.
|
|
32
|
+
const DISMISS_THRESHOLD = 100;
|
|
33
|
+
// Off-screen translateY target (px) for the dismiss slide. Deliberately larger
|
|
34
|
+
// than any realistic sheet height so the panel fully clears the viewport. Kept
|
|
35
|
+
// in px — not % — so the exit interpolates continuously from the px-based drag
|
|
36
|
+
// offset; WAAPI won't tween between px and % transforms.
|
|
37
|
+
const DISMISS_TRANSLATE = 600;
|
|
38
|
+
|
|
39
|
+
// Backdrop dim tuning. The scrim sits at BACKDROP_MAX_DIM alpha when the sheet
|
|
40
|
+
// is fully open and lightens toward transparent as the handle is dragged down,
|
|
41
|
+
// so the dimming reads as coupled to how much of the sheet is still on screen.
|
|
42
|
+
const BACKDROP_MAX_DIM = 0.5;
|
|
43
|
+
// Drag distance (px) over which the backdrop fades from full dim to fully clear.
|
|
44
|
+
// Ideally this would be the panel's own height, but measuring it on the drag's
|
|
45
|
+
// background thread isn't cheap (getBoundingClientRect is main-thread/async), so
|
|
46
|
+
// we approximate a full reveal with a fixed travel that stays within the
|
|
47
|
+
// DISMISS_TRANSLATE off-screen distance. The dim reduces linearly across it.
|
|
48
|
+
const BACKDROP_CLEAR_DISTANCE = 500;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Low-level bottom-sheet primitive: a full-screen `<overlay>` with a dimmed
|
|
52
|
+
* backdrop and a panel docked to the bottom that slides up on open, slides
|
|
53
|
+
* down on close, and can be dragged down by its handle to dismiss.
|
|
54
|
+
*
|
|
55
|
+
* It owns none of the content — consumers project whatever they want via
|
|
56
|
+
* `<ng-content>` (a scrollable item list for `select`, arbitrary content for
|
|
57
|
+
* `sheet`). This is the shared engine both those components build on, so the
|
|
58
|
+
* overlay/animation/drag behavior lives in exactly one place.
|
|
59
|
+
*/
|
|
60
|
+
@Component({
|
|
61
|
+
selector: 'ui-bottom-sheet',
|
|
62
|
+
standalone: true,
|
|
63
|
+
imports: [LYNX_ELEMENTS],
|
|
64
|
+
encapsulation: ViewEncapsulation.None,
|
|
65
|
+
template: `
|
|
66
|
+
<overlay [attr.visible]="overlayVisible()" [style]="overlayStyle()">
|
|
67
|
+
<view
|
|
68
|
+
#backdrop
|
|
69
|
+
class="h-full w-full"
|
|
70
|
+
[style]="backdropStyle()"
|
|
71
|
+
(bindtap)="onBackdropTap()"
|
|
72
|
+
>
|
|
73
|
+
<view
|
|
74
|
+
#panel
|
|
75
|
+
[class]="panelClass()"
|
|
76
|
+
[style]="panelPositionStyle()"
|
|
77
|
+
(catchtap)="onPanelTap()"
|
|
78
|
+
>
|
|
79
|
+
<view
|
|
80
|
+
class="items-center pb-3 pt-2 flex justify-center"
|
|
81
|
+
(catchtouchstart)="onHandleTouchStart($event)"
|
|
82
|
+
(catchtouchmove)="onHandleTouchMove($event)"
|
|
83
|
+
(catchtouchend)="onHandleTouchEnd()"
|
|
84
|
+
(catchtouchcancel)="onHandleTouchEnd()"
|
|
85
|
+
>
|
|
86
|
+
<view class="h-1 w-10 rounded-full bg-muted" />
|
|
87
|
+
</view>
|
|
88
|
+
<ng-content />
|
|
89
|
+
</view>
|
|
90
|
+
</view>
|
|
91
|
+
</overlay>
|
|
92
|
+
`,
|
|
93
|
+
})
|
|
94
|
+
export class UiBottomSheet {
|
|
95
|
+
readonly open = model(false);
|
|
96
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
97
|
+
|
|
98
|
+
readonly closed = output<void>();
|
|
99
|
+
|
|
100
|
+
protected readonly overlayVisible = signal(false);
|
|
101
|
+
/**
|
|
102
|
+
* `width`/`height`/`top`/`left` are web-only necessities: on web
|
|
103
|
+
* `<overlay>` has no intrinsic full-screen sizing (unlike native), so
|
|
104
|
+
* without them it collapses to 0x0 and nothing renders. `z-index` keeps it
|
|
105
|
+
* above sibling content that would otherwise occasionally paint on top.
|
|
106
|
+
*/
|
|
107
|
+
protected readonly overlayStyle = computed(() =>
|
|
108
|
+
this.overlayVisible()
|
|
109
|
+
? 'position: fixed; overflow: visible; width: 100vw; height: 100vh; top: 0; left: 0; z-index: 999;'
|
|
110
|
+
: 'position: fixed; overflow: visible; width: 100vw; height: 100vh; top: 0; left: 0; z-index: 999; display: none;',
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
readonly backdropRef = viewChild<ElementRef>('backdrop');
|
|
114
|
+
readonly panelRef = viewChild<ElementRef>('panel');
|
|
115
|
+
#backdropAnim?: AnimationHandle;
|
|
116
|
+
#panelAnim?: AnimationHandle;
|
|
117
|
+
#hasBeenOpen = false;
|
|
118
|
+
|
|
119
|
+
// --- Drag-to-dismiss state ---
|
|
120
|
+
// Live downward drag distance (px), applied to the panel as a translateY via
|
|
121
|
+
// `panelPositionStyle()` so the sheet tracks the finger in real time.
|
|
122
|
+
protected readonly dragOffset = signal(0);
|
|
123
|
+
// Screen Y where the current drag began; the delta from here drives dragOffset.
|
|
124
|
+
#dragStartY = 0;
|
|
125
|
+
// True only between touchstart and touchend on the handle, so unrelated move
|
|
126
|
+
// events can't shift the sheet.
|
|
127
|
+
#isDragging = false;
|
|
128
|
+
|
|
129
|
+
constructor() {
|
|
130
|
+
// `#hasBeenOpen` prevents the close animation from running on the initial
|
|
131
|
+
// effect evaluation when `open` starts as false. Without this guard, the
|
|
132
|
+
// first run would call #doClose() and emit `closed` before the sheet
|
|
133
|
+
// has ever been opened.
|
|
134
|
+
effect(() => {
|
|
135
|
+
const isOpen = this.open();
|
|
136
|
+
if (isOpen) {
|
|
137
|
+
this.#hasBeenOpen = true;
|
|
138
|
+
this.#doOpen();
|
|
139
|
+
} else if (this.#hasBeenOpen) {
|
|
140
|
+
this.#doClose();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
protected readonly panelClass = computed(() =>
|
|
146
|
+
cn(
|
|
147
|
+
'flex-col rounded-t-2xl border-t border-border bg-background flex',
|
|
148
|
+
'w-full',
|
|
149
|
+
// The panel docks at bottom: 0 (see panelPositionStyle), so its lower edge
|
|
150
|
+
// sits behind the home indicator. `pb-safe` pads the bottom by
|
|
151
|
+
// env(safe-area-inset-bottom), lifting the projected content clear of it
|
|
152
|
+
// while the panel background still fills to the screen edge. Degrades to no
|
|
153
|
+
// padding where there's no bottom inset (web/older devices). Mirrors how
|
|
154
|
+
// the toast bakes the safe area into its own docked positioning.
|
|
155
|
+
'pb-safe',
|
|
156
|
+
this.userClass(),
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Docked at the bottom, full width; `translateY(dragOffset)` layers the live
|
|
161
|
+
// drag on top so the sheet follows the finger. The open/close slide
|
|
162
|
+
// animations run via element.animate() and override this transform while they
|
|
163
|
+
// play (see #animateIn/#animateOut), then hand back to it once canceled.
|
|
164
|
+
protected readonly panelPositionStyle = computed(
|
|
165
|
+
() =>
|
|
166
|
+
`position: absolute; bottom: 0; left: 0; right: 0; transform: translateY(${this.dragOffset()}px);`,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Backdrop scrim dimmed in tandem with the sheet's reveal: full dim while
|
|
170
|
+
// docked (dragOffset 0), lightening as the handle is dragged down so the
|
|
171
|
+
// content behind shows through progressively. Driven through the same live
|
|
172
|
+
// `[style]` path as panelPositionStyle — dragOffset updates on the drag's
|
|
173
|
+
// background thread, and inline styles reflect there (unlike element.animate()
|
|
174
|
+
// opacity tweens, which the open/close fade uses on the main thread instead).
|
|
175
|
+
// The alpha lives in the background-color rather than the element's opacity so
|
|
176
|
+
// it composes cleanly with the fadeIn/fadeOut opacity animations without the
|
|
177
|
+
// two mechanisms fighting over the same property.
|
|
178
|
+
protected readonly backdropStyle = computed(() => {
|
|
179
|
+
const revealed = Math.max(
|
|
180
|
+
0,
|
|
181
|
+
1 - this.dragOffset() / BACKDROP_CLEAR_DISTANCE,
|
|
182
|
+
);
|
|
183
|
+
return `background-color: rgba(0, 0, 0, ${BACKDROP_MAX_DIM * revealed});`;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
protected onBackdropTap(): void {
|
|
187
|
+
this.open.set(false);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* No-op tap handler for the panel. `catchtap` (vs `bindtap`) already stops
|
|
192
|
+
* the tap from bubbling to the backdrop — Lynx controls propagation via the
|
|
193
|
+
* event prefix, not at runtime. (The renderer shims `event.stopPropagation()`
|
|
194
|
+
* as a no-op so DOM-style handlers don't crash, but it has no effect here.)
|
|
195
|
+
*/
|
|
196
|
+
protected onPanelTap(): void {}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Begin a drag on the handle strip. The open slideIn holds the panel at
|
|
200
|
+
* translateY(0%) via fill:'forwards', which would override our inline
|
|
201
|
+
* transform — cancel it so `dragOffset` (applied through panelPositionStyle)
|
|
202
|
+
* actually moves the panel from here on.
|
|
203
|
+
*/
|
|
204
|
+
protected onHandleTouchStart(e: {
|
|
205
|
+
touches?: ReadonlyArray<{ clientY: number }>;
|
|
206
|
+
}): void {
|
|
207
|
+
const y = e.touches?.[0]?.clientY;
|
|
208
|
+
if (y == null) return;
|
|
209
|
+
this.#panelAnim?.cancel();
|
|
210
|
+
this.#isDragging = true;
|
|
211
|
+
this.#dragStartY = y;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
protected onHandleTouchMove(e: {
|
|
215
|
+
touches?: ReadonlyArray<{ clientY: number }>;
|
|
216
|
+
}): void {
|
|
217
|
+
if (!this.#isDragging) return;
|
|
218
|
+
const y = e.touches?.[0]?.clientY;
|
|
219
|
+
if (y == null) return;
|
|
220
|
+
// Clamp to >= 0: the sheet is already docked at the bottom, so only a
|
|
221
|
+
// downward (dismiss-direction) drag should move it.
|
|
222
|
+
this.dragOffset.set(Math.max(0, y - this.#dragStartY));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Release the drag. Past the threshold we set `open` to false and let the
|
|
227
|
+
* effect drive teardown (#doClose → drag-aware #animateOut), which keeps the
|
|
228
|
+
* two-way `open` binding in sync and reuses the `closed` emit. Otherwise the
|
|
229
|
+
* sheet springs back to fully open.
|
|
230
|
+
*/
|
|
231
|
+
protected onHandleTouchEnd(): void {
|
|
232
|
+
if (!this.#isDragging) return;
|
|
233
|
+
this.#isDragging = false;
|
|
234
|
+
if (this.dragOffset() > DISMISS_THRESHOLD) {
|
|
235
|
+
this.open.set(false);
|
|
236
|
+
} else {
|
|
237
|
+
this.#snapBack(this.dragOffset());
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Two-phase open: make the overlay visible first so native elements exist
|
|
243
|
+
* in the tree, then animate on the next frame. Without this, animate()
|
|
244
|
+
* targets elements that haven't been flushed to native yet and silently fails.
|
|
245
|
+
*/
|
|
246
|
+
#doOpen(): void {
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
this.overlayVisible.set(true);
|
|
249
|
+
setTimeout(() => this.#animateIn(), 0);
|
|
250
|
+
}, 0);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Reverse: animate out first, then hide the overlay once the animation
|
|
255
|
+
* completes. The +20ms buffer absorbs timer imprecision in the Lynx
|
|
256
|
+
* runtime — hiding the overlay mid-animation causes elements to vanish
|
|
257
|
+
* before the fade finishes.
|
|
258
|
+
*/
|
|
259
|
+
#doClose(): void {
|
|
260
|
+
setTimeout(() => {
|
|
261
|
+
this.#animateOut();
|
|
262
|
+
setTimeout(() => {
|
|
263
|
+
this.overlayVisible.set(false);
|
|
264
|
+
this.closed.emit();
|
|
265
|
+
// Reset for the next open: the sheet returns to its docked position
|
|
266
|
+
// while hidden, so a fresh open animates cleanly from off-screen.
|
|
267
|
+
this.dragOffset.set(0);
|
|
268
|
+
}, DURATION.normal + 20);
|
|
269
|
+
}, 0);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#animateIn(): void {
|
|
273
|
+
const backdrop = this.backdropRef()?.nativeElement;
|
|
274
|
+
const panel = this.panelRef()?.nativeElement;
|
|
275
|
+
if (!backdrop || !panel) return;
|
|
276
|
+
|
|
277
|
+
this.#backdropAnim?.cancel();
|
|
278
|
+
this.#panelAnim?.cancel();
|
|
279
|
+
|
|
280
|
+
this.#backdropAnim = fadeIn(backdrop, { duration: DURATION.normal });
|
|
281
|
+
this.#panelAnim = slideIn(panel, 'up', {
|
|
282
|
+
duration: DURATION.slow,
|
|
283
|
+
easing: EASING.sheet,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#animateOut(): void {
|
|
288
|
+
const backdrop = this.backdropRef()?.nativeElement;
|
|
289
|
+
const panel = this.panelRef()?.nativeElement;
|
|
290
|
+
if (!backdrop || !panel) return;
|
|
291
|
+
|
|
292
|
+
this.#backdropAnim?.cancel();
|
|
293
|
+
this.#panelAnim?.cancel();
|
|
294
|
+
|
|
295
|
+
this.#backdropAnim = fadeOut(backdrop, { duration: DURATION.normal });
|
|
296
|
+
|
|
297
|
+
// If the sheet was dragged, continue from the finger's offset off-screen
|
|
298
|
+
// (px → px) rather than slideOut's 0% → 100%, which would snap it up first.
|
|
299
|
+
const offset = this.dragOffset();
|
|
300
|
+
this.#panelAnim =
|
|
301
|
+
offset > 0
|
|
302
|
+
? panel.animate(
|
|
303
|
+
[
|
|
304
|
+
{ transform: `translateY(${offset}px)`, opacity: 1 },
|
|
305
|
+
{ transform: `translateY(${DISMISS_TRANSLATE}px)`, opacity: 0.8 },
|
|
306
|
+
],
|
|
307
|
+
{
|
|
308
|
+
duration: DURATION.normal,
|
|
309
|
+
easing: EASING.accelerate,
|
|
310
|
+
fill: 'forwards',
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
: slideOut(panel, 'down', {
|
|
314
|
+
duration: DURATION.normal,
|
|
315
|
+
easing: EASING.accelerate,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Release below the dismiss threshold: spring the sheet back to fully open,
|
|
321
|
+
* continuing smoothly from wherever the finger let go (px → 0px).
|
|
322
|
+
*/
|
|
323
|
+
#snapBack(fromOffset: number): void {
|
|
324
|
+
const panel = this.panelRef()?.nativeElement;
|
|
325
|
+
this.#panelAnim?.cancel();
|
|
326
|
+
if (panel) {
|
|
327
|
+
this.#panelAnim = panel.animate(
|
|
328
|
+
[
|
|
329
|
+
{ transform: `translateY(${fromOffset}px)` },
|
|
330
|
+
{ transform: 'translateY(0px)' },
|
|
331
|
+
],
|
|
332
|
+
{ duration: DURATION.fast, easing: EASING.spring, fill: 'forwards' },
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
// Match the inline transform to the animation's resting state so the two
|
|
336
|
+
// agree once fill:'forwards' hands control back to the style binding.
|
|
337
|
+
this.dragOffset.set(0);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiBottomSheet } from './bottom-sheet';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
|
+
import { UiButton } from './button';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [UiButton, LYNX_ELEMENTS],
|
|
11
|
+
template: `<ui-button (pressed)="onPressed()"><text>Go</text></ui-button>`,
|
|
12
|
+
})
|
|
13
|
+
class ButtonHost {
|
|
14
|
+
count = 0;
|
|
15
|
+
onPressed = () => {
|
|
16
|
+
this.count++;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('UiButton (render)', () => {
|
|
21
|
+
it('renders and mounts without throwing', async () => {
|
|
22
|
+
const { container } = await render(ButtonHost);
|
|
23
|
+
expect(container).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('emits `pressed` from onTap() when not disabled/loading', async () => {
|
|
27
|
+
const { componentRef } = await render(UiButton);
|
|
28
|
+
const inst = componentRef.instance as unknown as {
|
|
29
|
+
onTap: () => void;
|
|
30
|
+
pressed: { subscribe: (cb: () => void) => void };
|
|
31
|
+
};
|
|
32
|
+
const emitted = vi.fn();
|
|
33
|
+
inst.pressed.subscribe(emitted);
|
|
34
|
+
inst.onTap();
|
|
35
|
+
expect(emitted).toHaveBeenCalledTimes(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('onTap() is a no-op when disabled', async () => {
|
|
39
|
+
const { componentRef } = await render(UiButton);
|
|
40
|
+
const inst = componentRef.instance as unknown as UiButton & {
|
|
41
|
+
onTap: () => void;
|
|
42
|
+
};
|
|
43
|
+
setInputSignal(inst.disabled, true);
|
|
44
|
+
const emitted = vi.fn();
|
|
45
|
+
inst.pressed.subscribe(emitted);
|
|
46
|
+
inst.onTap();
|
|
47
|
+
expect(emitted).not.toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('onTap() is a no-op when loading', async () => {
|
|
51
|
+
const { componentRef } = await render(UiButton);
|
|
52
|
+
const inst = componentRef.instance as unknown as UiButton & {
|
|
53
|
+
onTap: () => void;
|
|
54
|
+
};
|
|
55
|
+
setInputSignal(inst.loading, true);
|
|
56
|
+
const emitted = vi.fn();
|
|
57
|
+
inst.pressed.subscribe(emitted);
|
|
58
|
+
inst.onTap();
|
|
59
|
+
expect(emitted).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('shows the spinner while loading', async () => {
|
|
63
|
+
const { componentRef, container } = await render(UiButton);
|
|
64
|
+
setInputSignal((componentRef.instance as UiButton).loading, true);
|
|
65
|
+
await waitForUpdate();
|
|
66
|
+
// Spinner renders as a nested view with an svg — searching for that
|
|
67
|
+
// suffices without asserting on the internal Lynx tree.
|
|
68
|
+
expect(container.querySelector('svg')).toBeTruthy();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('applies disabled dim opacity when disabled or loading', async () => {
|
|
72
|
+
const { componentRef, container } = await render(UiButton);
|
|
73
|
+
setInputSignal((componentRef.instance as UiButton).disabled, true);
|
|
74
|
+
await waitForUpdate();
|
|
75
|
+
const outer = container.querySelector('view');
|
|
76
|
+
expect(outer?.getAttribute('class')).toContain('opacity-50');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('picks the right spinner size for each button size', async () => {
|
|
80
|
+
// spinnerSize returns 'xs' for size='sm' and 'sm' otherwise. Cover both.
|
|
81
|
+
const { componentRef } = await render(UiButton);
|
|
82
|
+
const inst = componentRef.instance as unknown as UiButton & {
|
|
83
|
+
spinnerSize: () => 'xs' | 'sm';
|
|
84
|
+
};
|
|
85
|
+
setInputSignal(inst.size, 'sm');
|
|
86
|
+
await waitForUpdate();
|
|
87
|
+
expect(inst.spinnerSize()).toBe('xs');
|
|
88
|
+
|
|
89
|
+
setInputSignal(inst.size, 'default');
|
|
90
|
+
await waitForUpdate();
|
|
91
|
+
expect(inst.spinnerSize()).toBe('sm');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('press handlers run cleanly (guarded by disabled/loading state)', async () => {
|
|
95
|
+
// onPressStart/End are no-ops when the button is disabled/loading; the
|
|
96
|
+
// active-press path relies on the ref being wired up on-device — assert
|
|
97
|
+
// only that the handlers run without throwing here.
|
|
98
|
+
const { componentRef } = await render(UiButton);
|
|
99
|
+
const inst = componentRef.instance as unknown as {
|
|
100
|
+
onPressStart: () => void;
|
|
101
|
+
onPressEnd: () => void;
|
|
102
|
+
onPressCancel: () => void;
|
|
103
|
+
};
|
|
104
|
+
expect(() => inst.onPressStart()).not.toThrow();
|
|
105
|
+
expect(() => inst.onPressEnd()).not.toThrow();
|
|
106
|
+
expect(() => inst.onPressCancel()).not.toThrow();
|
|
107
|
+
|
|
108
|
+
// Disabled path (early return in onPressStart / onPressEnd)
|
|
109
|
+
setInputSignal((componentRef.instance as UiButton).disabled, true);
|
|
110
|
+
expect(() => inst.onPressStart()).not.toThrow();
|
|
111
|
+
expect(() => inst.onPressEnd()).not.toThrow();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('@angular/core', () => ({
|
|
4
|
+
Component: () => () => {},
|
|
5
|
+
ViewEncapsulation: { None: 0 },
|
|
6
|
+
computed: () => () => {},
|
|
7
|
+
input: () => () => {},
|
|
8
|
+
output: () => () => {},
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('@blotch/angular-lynx', () => ({
|
|
12
|
+
LYNX_ELEMENTS: [],
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const { buttonVariants } = await import('./button');
|
|
16
|
+
|
|
17
|
+
describe('buttonVariants', () => {
|
|
18
|
+
it('returns default variant and size classes', () => {
|
|
19
|
+
const result = buttonVariants();
|
|
20
|
+
expect(result).toContain('bg-primary');
|
|
21
|
+
expect(result).toContain('h-10');
|
|
22
|
+
expect(result).toContain('px-4');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns destructive variant classes', () => {
|
|
26
|
+
const result = buttonVariants({ variant: 'destructive' });
|
|
27
|
+
expect(result).toContain('bg-destructive');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns outline variant classes', () => {
|
|
31
|
+
const result = buttonVariants({ variant: 'outline' });
|
|
32
|
+
expect(result).toContain('border');
|
|
33
|
+
expect(result).toContain('bg-background');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns secondary variant classes', () => {
|
|
37
|
+
const result = buttonVariants({ variant: 'secondary' });
|
|
38
|
+
expect(result).toContain('bg-secondary');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns ghost variant classes', () => {
|
|
42
|
+
const result = buttonVariants({ variant: 'ghost' });
|
|
43
|
+
expect(result).toContain('bg-transparent');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns small size classes', () => {
|
|
47
|
+
const result = buttonVariants({ size: 'sm' });
|
|
48
|
+
expect(result).toContain('h-9');
|
|
49
|
+
expect(result).toContain('px-3');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns large size classes', () => {
|
|
53
|
+
const result = buttonVariants({ size: 'lg' });
|
|
54
|
+
expect(result).toContain('h-11');
|
|
55
|
+
expect(result).toContain('px-8');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns icon size classes', () => {
|
|
59
|
+
const result = buttonVariants({ size: 'icon' });
|
|
60
|
+
expect(result).toContain('h-10');
|
|
61
|
+
expect(result).toContain('w-10');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('always includes base classes', () => {
|
|
65
|
+
const result = buttonVariants({ variant: 'ghost', size: 'sm' });
|
|
66
|
+
expect(result).toContain('flex');
|
|
67
|
+
expect(result).toContain('items-center');
|
|
68
|
+
expect(result).toContain('rounded-md');
|
|
69
|
+
});
|
|
70
|
+
});
|