@blotch/dolan 22.2.4 → 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.test.js +2 -0
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/doctor.js +78 -21
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +167 -26
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/init.test.js +4 -7
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/update.d.ts +3 -1
- package/dist/commands/update.js +78 -15
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +257 -2
- package/dist/commands/update.test.js.map +1 -1
- package/dist/lockfile.d.ts +10 -0
- package/dist/lockfile.js +20 -1
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.js +43 -3
- package/dist/lockfile.test.js.map +1 -1
- package/dist/registry.js +73 -11
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +16 -3
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +24 -2
- package/dist/test-utils.js +41 -2
- package/dist/test-utils.js.map +1 -1
- package/dist/ui/boolean-inputs.test.ts +76 -0
- package/dist/ui/components/action-sheet/action-sheet.test.ts +370 -1
- package/dist/ui/components/action-sheet/action-sheet.ts +306 -28
- package/dist/ui/components/action-sheet/index.ts +1 -0
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +552 -8
- package/dist/ui/components/alert-dialog/alert-dialog.ts +644 -36
- package/dist/ui/components/alert-dialog/index.ts +5 -0
- package/dist/ui/components/area-chart/area-chart.ts +3 -2
- package/dist/ui/components/badge/badge.ts +2 -1
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +968 -3
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1021 -95
- package/dist/ui/components/bottom-sheet/index.ts +7 -1
- package/dist/ui/components/button/button.test.ts +36 -1
- package/dist/ui/components/button/button.ts +17 -7
- package/dist/ui/components/button-group/button-group.test.ts +53 -9
- package/dist/ui/components/button-group/button-group.ts +67 -21
- package/dist/ui/components/card/card.test.ts +16 -0
- package/dist/ui/components/card/card.ts +2 -1
- package/dist/ui/components/chart/chart-presets.ts +6 -6
- package/dist/ui/components/chart/chart.test.ts +14 -0
- package/dist/ui/components/chart/chart.ts +10 -3
- package/dist/ui/components/checkbox/checkbox.ts +2 -1
- package/dist/ui/components/collapsible/collapsible.ts +2 -1
- package/dist/ui/components/dialog/dialog.ts +6 -6
- package/dist/ui/components/dock/dock-scroll.ts +9 -0
- package/dist/ui/components/dock/dock-search.test.ts +594 -0
- package/dist/ui/components/dock/dock.test.ts +4 -1
- package/dist/ui/components/dock/dock.ts +529 -51
- 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/glass/glass.test.ts +39 -1
- package/dist/ui/components/glass/glass.ts +87 -17
- package/dist/ui/components/glass/index.ts +1 -0
- package/dist/ui/components/icon/icon-theme.test.ts +28 -0
- package/dist/ui/components/icon/icon-theme.ts +38 -7
- package/dist/ui/components/input/input.ts +2 -1
- package/dist/ui/components/label/label.ts +8 -2
- package/dist/ui/components/line-chart/line-chart.ts +3 -2
- package/dist/ui/components/list/list.ts +5 -4
- package/dist/ui/components/menu/menu-popup.test.ts +18 -14
- package/dist/ui/components/menu/menu-popup.ts +24 -3
- package/dist/ui/components/menu/menu.test.ts +99 -5
- package/dist/ui/components/menu/menu.ts +74 -34
- package/dist/ui/components/nav-drawer/nav-drawer.ts +3 -2
- 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/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.ts +2 -1
- package/dist/ui/components/radio-group/radio-group.ts +2 -1
- 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.ts +2 -1
- package/dist/ui/components/sheet/sheet.test.ts +90 -3
- package/dist/ui/components/sheet/sheet.ts +59 -4
- 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/switch/index.ts +1 -1
- package/dist/ui/components/switch/switch.test.ts +464 -48
- package/dist/ui/components/switch/switch.ts +613 -43
- package/dist/ui/components/tabs/index.ts +7 -1
- package/dist/ui/components/tabs/tabs.test.ts +278 -2
- package/dist/ui/components/tabs/tabs.ts +233 -30
- package/dist/ui/components/textarea/textarea.ts +2 -1
- package/dist/ui/components/toggle/index.ts +1 -0
- package/dist/ui/components/toggle/toggle.test.ts +310 -4
- 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/utils/analyze.d.ts +10 -5
- package/dist/utils/analyze.js +3 -2
- package/dist/utils/analyze.js.map +1 -1
- package/dist/utils/installed.d.ts +13 -1
- package/dist/utils/installed.js +17 -0
- package/dist/utils/installed.js.map +1 -1
- package/dist/utils/installed.test.js +39 -1
- package/dist/utils/installed.test.js.map +1 -1
- package/dist/utils/tailwind.d.ts +25 -5
- package/dist/utils/tailwind.js +53 -14
- package/dist/utils/tailwind.js.map +1 -1
- package/dist/utils/tailwind.test.js +85 -22
- package/dist/utils/tailwind.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
waitForUpdate,
|
|
6
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
7
|
+
import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
|
|
10
|
+
import { UiGlass } from '../glass/glass';
|
|
11
|
+
import { UiPopover, UiPopoverContent, UiPopoverTrigger } from './popover';
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
imports: [
|
|
15
|
+
LYNX_ELEMENTS,
|
|
16
|
+
UiGlass,
|
|
17
|
+
UiPopover,
|
|
18
|
+
UiPopoverContent,
|
|
19
|
+
UiPopoverTrigger,
|
|
20
|
+
],
|
|
21
|
+
template: `
|
|
22
|
+
<ui-popover
|
|
23
|
+
[open]="open()"
|
|
24
|
+
(openChange)="open.set($event)"
|
|
25
|
+
(closed)="closed()"
|
|
26
|
+
>
|
|
27
|
+
@if (glassTrigger()) {
|
|
28
|
+
<blur-view
|
|
29
|
+
ui-glass
|
|
30
|
+
uiPopoverTrigger
|
|
31
|
+
class="trigger"
|
|
32
|
+
[accessibility-element]="true"
|
|
33
|
+
accessibility-traits="button"
|
|
34
|
+
accessibility-label="Tip"
|
|
35
|
+
>
|
|
36
|
+
<text>Tip</text>
|
|
37
|
+
</blur-view>
|
|
38
|
+
} @else if (withTrigger()) {
|
|
39
|
+
<view
|
|
40
|
+
uiPopoverTrigger
|
|
41
|
+
class="trigger"
|
|
42
|
+
[accessibility-element]="true"
|
|
43
|
+
accessibility-traits="button"
|
|
44
|
+
accessibility-label="Tip"
|
|
45
|
+
>
|
|
46
|
+
<text>Tip</text>
|
|
47
|
+
</view>
|
|
48
|
+
}
|
|
49
|
+
<ui-popover-content class="w-60 p-3">
|
|
50
|
+
<text>Liked trips show up first.</text>
|
|
51
|
+
</ui-popover-content>
|
|
52
|
+
</ui-popover>
|
|
53
|
+
<view class="like" (bindtap)="liked()"><text>Like</text></view>
|
|
54
|
+
`,
|
|
55
|
+
})
|
|
56
|
+
class PopoverHost {
|
|
57
|
+
readonly open = signal(false);
|
|
58
|
+
readonly withTrigger = signal(true);
|
|
59
|
+
readonly glassTrigger = signal(false);
|
|
60
|
+
readonly closed = vi.fn();
|
|
61
|
+
readonly liked = vi.fn();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The Lynx function that runs an element's UI methods, such as measuring it
|
|
66
|
+
* (`boundingClientRect`). The testing library defines it on the global
|
|
67
|
+
* object.
|
|
68
|
+
*/
|
|
69
|
+
type InvokeUIMethod = {
|
|
70
|
+
__InvokeUIMethod: (
|
|
71
|
+
element: Element,
|
|
72
|
+
method: string,
|
|
73
|
+
params: Record<string, unknown>,
|
|
74
|
+
callback: (result: { code: number; data?: unknown }) => void,
|
|
75
|
+
) => void;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Accessibility settings a test can change, as the host app would. */
|
|
79
|
+
const settings = {
|
|
80
|
+
reduceMotion: signal(false),
|
|
81
|
+
reduceTransparency: signal(false),
|
|
82
|
+
increaseContrast: signal(false),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Renders the popover on a platform (none for Material), measuring elements
|
|
87
|
+
* as `boundingClientRect` would on a phone: the safe area's markers a
|
|
88
|
+
* phone's screen, the content 240 by 80, and anything else, the trigger, a
|
|
89
|
+
* small box near the top.
|
|
90
|
+
*/
|
|
91
|
+
const renderPopover = async (
|
|
92
|
+
platform: { platform: string; osVersion: string } | null = {
|
|
93
|
+
platform: 'iOS',
|
|
94
|
+
osVersion: '26.0',
|
|
95
|
+
},
|
|
96
|
+
) => {
|
|
97
|
+
vi.stubGlobal('SystemInfo', platform ?? undefined);
|
|
98
|
+
const measured: Element[] = [];
|
|
99
|
+
vi.spyOn(
|
|
100
|
+
globalThis as unknown as InvokeUIMethod,
|
|
101
|
+
'__InvokeUIMethod',
|
|
102
|
+
).mockImplementation((element, method, _params, callback) => {
|
|
103
|
+
if (method !== 'boundingClientRect') {
|
|
104
|
+
callback({ code: 0 });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
measured.push(element);
|
|
108
|
+
const style = element.getAttribute('style') ?? '';
|
|
109
|
+
const data = style.includes('safe-area-inset')
|
|
110
|
+
? { left: 0, top: 0, width: 402, height: 840 }
|
|
111
|
+
: element.classList.contains('shrink-0')
|
|
112
|
+
? { left: 0, top: 0, width: 240, height: 80 }
|
|
113
|
+
: { left: 16, top: 80, width: 80, height: 44 };
|
|
114
|
+
callback({ code: 0, data });
|
|
115
|
+
});
|
|
116
|
+
const result = await render(PopoverHost, {
|
|
117
|
+
providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
|
|
118
|
+
});
|
|
119
|
+
const host = result.componentRef.instance as PopoverHost;
|
|
120
|
+
return {
|
|
121
|
+
...result,
|
|
122
|
+
host,
|
|
123
|
+
measured,
|
|
124
|
+
trigger: () => result.container.querySelector('.trigger') as HTMLElement,
|
|
125
|
+
panel: () =>
|
|
126
|
+
result.container.querySelector(
|
|
127
|
+
'blur-view[ui-glass]:not(.trigger)',
|
|
128
|
+
) as HTMLElement | null,
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
type Rendered = Awaited<ReturnType<typeof renderPopover>>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Waits until the popover has grown to its frame, its content shown.
|
|
136
|
+
*/
|
|
137
|
+
const waitForOpen = async ({ container }: Rendered) => {
|
|
138
|
+
await vi.waitFor(() =>
|
|
139
|
+
expect(
|
|
140
|
+
(container.querySelector('.shrink-0') as HTMLElement | null)?.style
|
|
141
|
+
.opacity,
|
|
142
|
+
).toBe('1'),
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Lets Lynx's timers run: the touch check waits for the next one.
|
|
148
|
+
*/
|
|
149
|
+
const nextTimer = () => new Promise((resolve) => setTimeout(resolve, 5));
|
|
150
|
+
|
|
151
|
+
// The renderer warns that <ui-popover> and its parts aren't Lynx elements.
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
afterEach(() => {
|
|
157
|
+
vi.restoreAllMocks();
|
|
158
|
+
vi.unstubAllGlobals();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('UiPopover', () => {
|
|
162
|
+
it('opens and closes when its trigger is tapped', async () => {
|
|
163
|
+
const rendered = await renderPopover();
|
|
164
|
+
const { host, trigger, getByText } = rendered;
|
|
165
|
+
|
|
166
|
+
fireEvent.tap(trigger());
|
|
167
|
+
expect(host.open()).toBe(true);
|
|
168
|
+
await waitForOpen(rendered);
|
|
169
|
+
expect(getByText('Liked trips show up first.')).toBeTruthy();
|
|
170
|
+
// It opened from the trigger.
|
|
171
|
+
expect(rendered.measured[0]).toBe(trigger());
|
|
172
|
+
|
|
173
|
+
fireEvent.tap(trigger());
|
|
174
|
+
expect(host.open()).toBe(false);
|
|
175
|
+
await vi.waitFor(() => expect(host.closed).toHaveBeenCalledTimes(1));
|
|
176
|
+
await waitForUpdate();
|
|
177
|
+
expect(rendered.panel()).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('closes on a touch outside it, which still reaches what it touched', async () => {
|
|
181
|
+
const rendered = await renderPopover();
|
|
182
|
+
rendered.host.open.set(true);
|
|
183
|
+
await waitForOpen(rendered);
|
|
184
|
+
|
|
185
|
+
// A tap on the like button: Lynx tells the popover through its global
|
|
186
|
+
// listener, and the button gets its tap as ever.
|
|
187
|
+
fireLynxEvent(rendered.panel()!, 'touchstart', {}, 'global-bindEvent');
|
|
188
|
+
fireEvent.tap(rendered.container.querySelector('.like')!);
|
|
189
|
+
await nextTimer();
|
|
190
|
+
expect(rendered.host.open()).toBe(false);
|
|
191
|
+
expect(rendered.host.liked).toHaveBeenCalledTimes(1);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("doesn't take a touch on its trigger for one outside it", async () => {
|
|
195
|
+
const rendered = await renderPopover();
|
|
196
|
+
rendered.host.open.set(true);
|
|
197
|
+
await waitForOpen(rendered);
|
|
198
|
+
|
|
199
|
+
fireLynxEvent(rendered.trigger(), 'touchstart', {}, 'bindEvent');
|
|
200
|
+
fireLynxEvent(rendered.panel()!, 'touchstart', {}, 'global-bindEvent');
|
|
201
|
+
await nextTimer();
|
|
202
|
+
fireLynxEvent(rendered.trigger(), 'mousedown', {}, 'bindEvent');
|
|
203
|
+
fireLynxEvent(rendered.panel()!, 'mousedown', {}, 'global-bindEvent');
|
|
204
|
+
await nextTimer();
|
|
205
|
+
expect(rendered.host.open()).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('hides its trigger while Liquid Glass grows out of it', async () => {
|
|
209
|
+
const rendered = await renderPopover();
|
|
210
|
+
expect(rendered.trigger().style.visibility).toBe('');
|
|
211
|
+
|
|
212
|
+
rendered.host.open.set(true);
|
|
213
|
+
await waitForOpen(rendered);
|
|
214
|
+
expect(rendered.trigger().style.visibility).toBe('hidden');
|
|
215
|
+
|
|
216
|
+
rendered.host.open.set(false);
|
|
217
|
+
await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
|
|
218
|
+
await waitForUpdate();
|
|
219
|
+
expect(rendered.trigger().style.visibility).toBe('');
|
|
220
|
+
// A trigger that isn't glass has no glass to hand over.
|
|
221
|
+
expect(rendered.trigger().hasAttribute('glass-effect-id')).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("takes a glass trigger's glass over, shown as the trigger hides", async () => {
|
|
225
|
+
const rendered = await renderPopover();
|
|
226
|
+
rendered.host.glassTrigger.set(true);
|
|
227
|
+
await waitForUpdate();
|
|
228
|
+
const id = rendered.trigger().getAttribute('glass-effect-id');
|
|
229
|
+
expect(id).toMatch(/^ui-popover-\d+$/);
|
|
230
|
+
|
|
231
|
+
rendered.host.open.set(true);
|
|
232
|
+
// Shown at the trigger's place in the same update that hides the
|
|
233
|
+
// trigger, under the same id.
|
|
234
|
+
await waitForOpen(rendered);
|
|
235
|
+
expect(rendered.panel()!.style.visibility).toBe('');
|
|
236
|
+
expect(rendered.panel()!.getAttribute('glass-effect-id')).toBe(id);
|
|
237
|
+
expect(rendered.trigger().style.visibility).toBe('hidden');
|
|
238
|
+
|
|
239
|
+
rendered.host.open.set(false);
|
|
240
|
+
await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
|
|
241
|
+
await waitForUpdate();
|
|
242
|
+
expect(rendered.panel()).toBeNull();
|
|
243
|
+
expect(rendered.trigger().style.visibility).toBe('');
|
|
244
|
+
|
|
245
|
+
// A trigger that goes takes its glass with it.
|
|
246
|
+
rendered.host.glassTrigger.set(false);
|
|
247
|
+
await waitForUpdate();
|
|
248
|
+
expect(rendered.trigger().tagName.toLowerCase()).toBe('view');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('leaves its trigger showing beside a Material surface', async () => {
|
|
252
|
+
const rendered = await renderPopover(null);
|
|
253
|
+
rendered.host.open.set(true);
|
|
254
|
+
await waitForOpen(rendered);
|
|
255
|
+
|
|
256
|
+
expect(rendered.trigger().style.visibility).toBe('');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('opens from itself when nothing is its trigger', async () => {
|
|
260
|
+
const rendered = await renderPopover();
|
|
261
|
+
rendered.host.withTrigger.set(false);
|
|
262
|
+
await waitForUpdate();
|
|
263
|
+
|
|
264
|
+
rendered.host.open.set(true);
|
|
265
|
+
await waitForOpen(rendered);
|
|
266
|
+
// The popover's own element, which holds the content's slot.
|
|
267
|
+
expect(rendered.measured[0]).not.toBe(rendered.trigger());
|
|
268
|
+
expect(rendered.measured[0]?.classList.contains('like')).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('UiPopoverContent', () => {
|
|
273
|
+
it('is a padded column, with its class on its own element', async () => {
|
|
274
|
+
const rendered = await renderPopover();
|
|
275
|
+
rendered.host.open.set(true);
|
|
276
|
+
await waitForOpen(rendered);
|
|
277
|
+
|
|
278
|
+
const content = rendered.getByText(
|
|
279
|
+
'Liked trips show up first.',
|
|
280
|
+
).parentElement!;
|
|
281
|
+
expect(content.classList).toContain('flex-col');
|
|
282
|
+
expect(content.classList).toContain('w-60');
|
|
283
|
+
expect(content.classList).toContain('p-3');
|
|
284
|
+
expect(content.classList).not.toContain('p-4');
|
|
285
|
+
});
|
|
286
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
DestroyRef,
|
|
4
|
+
Directive,
|
|
5
|
+
ElementRef,
|
|
6
|
+
ViewEncapsulation,
|
|
7
|
+
computed,
|
|
8
|
+
inject,
|
|
9
|
+
input,
|
|
10
|
+
model,
|
|
11
|
+
output,
|
|
12
|
+
signal,
|
|
13
|
+
viewChild,
|
|
14
|
+
} from '@angular/core';
|
|
15
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
16
|
+
|
|
17
|
+
import { cn } from '../../utils/cn';
|
|
18
|
+
import { UiGlass } from '../glass/glass';
|
|
19
|
+
import { type PopoverAnchor, UiPopoverPanel } from './popover-panel';
|
|
20
|
+
|
|
21
|
+
/** How many popovers there have been, for their glass ids. */
|
|
22
|
+
let popoverCount = 0;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A popover's content: a column, padded, that a `ui-popover` shows when it
|
|
26
|
+
* opens.
|
|
27
|
+
*
|
|
28
|
+
* ```html
|
|
29
|
+
* <ui-popover-content class="w-64 gap-1">
|
|
30
|
+
* <text class="text-base font-semibold text-glass-foreground">Kyoto</text>
|
|
31
|
+
* <text class="text-sm text-glass-muted-foreground">Day 3 of 5</text>
|
|
32
|
+
* </ui-popover-content>
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* Its classes go on its own element, so `class` sets its width, padding and
|
|
36
|
+
* spacing. It's as wide as its content by default, and no wider than the
|
|
37
|
+
* screen. Color text in it with `text-glass-foreground` and
|
|
38
|
+
* `text-glass-muted-foreground`, which suit the glass and Material's solid
|
|
39
|
+
* surface alike.
|
|
40
|
+
*/
|
|
41
|
+
@Component({
|
|
42
|
+
selector: 'ui-popover-content',
|
|
43
|
+
encapsulation: ViewEncapsulation.None,
|
|
44
|
+
// The classes on the host itself: it's the column, with no inner view in
|
|
45
|
+
// a host of no size of its own between it and the panel.
|
|
46
|
+
host: { '[class]': 'contentClass()' },
|
|
47
|
+
template: `<ng-content />`,
|
|
48
|
+
})
|
|
49
|
+
export class UiPopoverContent {
|
|
50
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
51
|
+
|
|
52
|
+
protected readonly contentClass = computed(() =>
|
|
53
|
+
cn('flex-col gap-1 p-4 flex', this.userClass()),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A popover: content that opens from the control that presents it, and
|
|
59
|
+
* leaves the rest of the screen as it was.
|
|
60
|
+
*
|
|
61
|
+
* ```html
|
|
62
|
+
* <ui-popover>
|
|
63
|
+
* <ui-button uiPopoverTrigger variant="outline">Details</ui-button>
|
|
64
|
+
* <ui-popover-content class="w-64">
|
|
65
|
+
* <text class="text-sm text-glass-foreground">Check-in from 3pm.</text>
|
|
66
|
+
* </ui-popover-content>
|
|
67
|
+
* </ui-popover>
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* Mark the element that opens it with `uiPopoverTrigger`; what's in the
|
|
71
|
+
* `<ui-popover-content>` is the popover. A tap on the trigger opens and
|
|
72
|
+
* closes it, and a touch anywhere else closes it without being swallowed:
|
|
73
|
+
* nothing dims, and the button under the finger still gets the tap.
|
|
74
|
+
* `[(open)]` opens and closes it from code.
|
|
75
|
+
*
|
|
76
|
+
* On iOS 26 and later it's Liquid Glass that grows out of the trigger, over
|
|
77
|
+
* it, as iOS 26 grows a popover out of its source item. Before iOS 26 it's
|
|
78
|
+
* the frosted material beside the trigger, with an arrow pointing at it, as
|
|
79
|
+
* UIKit drew a popover on iPhone. On Android it's a Material 3 menu surface
|
|
80
|
+
* that unfolds below the trigger, or Liquid Glass with
|
|
81
|
+
* `provideGlass({ android: 'glass' })`. It stays inside the safe area, and
|
|
82
|
+
* opens upward near the bottom of the screen.
|
|
83
|
+
*/
|
|
84
|
+
@Component({
|
|
85
|
+
selector: 'ui-popover',
|
|
86
|
+
imports: [LYNX_ELEMENTS, UiPopoverPanel],
|
|
87
|
+
encapsulation: ViewEncapsulation.None,
|
|
88
|
+
template: `
|
|
89
|
+
<ng-content />
|
|
90
|
+
<ui-popover-panel
|
|
91
|
+
#panel
|
|
92
|
+
[open]="open()"
|
|
93
|
+
[anchor]="anchor()"
|
|
94
|
+
[glassId]="glassId"
|
|
95
|
+
(dismissed)="close()"
|
|
96
|
+
(closed)="closed.emit()"
|
|
97
|
+
>
|
|
98
|
+
<ng-content select="ui-popover-content" />
|
|
99
|
+
</ui-popover-panel>
|
|
100
|
+
`,
|
|
101
|
+
})
|
|
102
|
+
export class UiPopover {
|
|
103
|
+
/** Whether the popover is open. */
|
|
104
|
+
readonly open = model(false);
|
|
105
|
+
/** The popover has closed and its close animation has finished. */
|
|
106
|
+
readonly closed = output<void>();
|
|
107
|
+
|
|
108
|
+
readonly #host = inject<ElementRef<PopoverAnchor>>(ElementRef);
|
|
109
|
+
readonly #trigger = signal<PopoverAnchor | null>(null);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The id the popover's glass and a glass trigger share (see UiGlass's
|
|
113
|
+
* `glassId`), unique to the popover, so it takes over the trigger's glass
|
|
114
|
+
* as it grows out of it.
|
|
115
|
+
*/
|
|
116
|
+
readonly glassId = `ui-popover-${++popoverCount}`;
|
|
117
|
+
|
|
118
|
+
// Required: always in the template.
|
|
119
|
+
readonly panel = viewChild.required<UiPopoverPanel>('panel');
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* What the popover opens from: its trigger, or the popover's own element
|
|
123
|
+
* when nothing is marked `uiPopoverTrigger`.
|
|
124
|
+
*/
|
|
125
|
+
protected readonly anchor = computed(
|
|
126
|
+
() => this.#trigger() ?? this.#host.nativeElement,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
/** Whether the popover covers its trigger now; see coversAnchor. */
|
|
130
|
+
readonly coversTrigger = computed(() => this.panel().coversAnchor());
|
|
131
|
+
|
|
132
|
+
toggle(): void {
|
|
133
|
+
this.open.update((open) => !open);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
close(): void {
|
|
137
|
+
this.open.set(false);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @internal Called by the `uiPopoverTrigger` directive.
|
|
142
|
+
*/
|
|
143
|
+
setTrigger(trigger: PopoverAnchor | null): void {
|
|
144
|
+
this.#trigger.set(trigger);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Marks the element that opens a `ui-popover`: a tap on it opens or closes
|
|
150
|
+
* the popover, which opens from it.
|
|
151
|
+
*
|
|
152
|
+
* ```html
|
|
153
|
+
* <ui-popover>
|
|
154
|
+
* <ui-button uiPopoverTrigger>Details</ui-button>
|
|
155
|
+
* …
|
|
156
|
+
* </ui-popover>
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* A touch on the trigger doesn't count as one outside the popover, so a tap
|
|
160
|
+
* on it closes an open popover rather than closing and reopening it.
|
|
161
|
+
*/
|
|
162
|
+
@Directive({
|
|
163
|
+
selector: '[uiPopoverTrigger]',
|
|
164
|
+
host: {
|
|
165
|
+
'(click)': 'popover.toggle()',
|
|
166
|
+
// Bubbling, from what the touch landed on in the trigger: Lynx 4.1 ran
|
|
167
|
+
// no capture listener on iOS (see UiPopoverPanel).
|
|
168
|
+
'(bindtouchstart)': 'popover.panel().touchedAnchor()',
|
|
169
|
+
'(bindmousedown)': 'popover.panel().touchedAnchor()',
|
|
170
|
+
'[style.visibility]': 'popover.coversTrigger() ? "hidden" : null',
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
export class UiPopoverTrigger {
|
|
174
|
+
protected readonly popover = inject(UiPopover);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Points the popover at this element until the directive is destroyed. A
|
|
178
|
+
* trigger that's glass itself (`<blur-view ui-glass uiPopoverTrigger>`)
|
|
179
|
+
* shares the popover's glass id meanwhile, so the popover takes its glass
|
|
180
|
+
* over (see UiPopoverPanel's coversAnchor).
|
|
181
|
+
*/
|
|
182
|
+
protected readonly unregister = registerTrigger(
|
|
183
|
+
this.popover,
|
|
184
|
+
inject<ElementRef<PopoverAnchor>>(ElementRef).nativeElement,
|
|
185
|
+
inject(UiGlass, { self: true, optional: true }),
|
|
186
|
+
inject(DestroyRef),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Makes `trigger` the element `popover` opens from, and pairs its `glass`,
|
|
192
|
+
* if it's glass, with the popover's, until `destroyRef` is destroyed.
|
|
193
|
+
* Returns what takes the registration back early.
|
|
194
|
+
*/
|
|
195
|
+
const registerTrigger = (
|
|
196
|
+
popover: UiPopover,
|
|
197
|
+
trigger: PopoverAnchor,
|
|
198
|
+
glass: UiGlass | null,
|
|
199
|
+
destroyRef: DestroyRef,
|
|
200
|
+
): (() => void) => {
|
|
201
|
+
popover.setTrigger(trigger);
|
|
202
|
+
glass?.pair(popover.glassId);
|
|
203
|
+
return destroyRef.onDestroy(() => {
|
|
204
|
+
popover.setTrigger(null);
|
|
205
|
+
glass?.pair(null);
|
|
206
|
+
});
|
|
207
|
+
};
|
|
@@ -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,
|
|
@@ -29,7 +30,7 @@ export class UiProgress {
|
|
|
29
30
|
/**
|
|
30
31
|
* When true, shows an indeterminate (looping) animation
|
|
31
32
|
*/
|
|
32
|
-
readonly indeterminate = input(false);
|
|
33
|
+
readonly indeterminate = input(false, { transform: booleanAttribute });
|
|
33
34
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
34
35
|
|
|
35
36
|
readonly fillRef = viewChild.required<ElementRef>('fill');
|
|
@@ -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
|
inject,
|
|
@@ -28,7 +29,7 @@ import { cn } from '../../utils/cn';
|
|
|
28
29
|
})
|
|
29
30
|
export class UiRadioGroup {
|
|
30
31
|
readonly value = model<string>('');
|
|
31
|
-
readonly disabled = input(false);
|
|
32
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
32
33
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
33
34
|
|
|
34
35
|
readonly changed = output<string>();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Component,
|
|
3
3
|
ViewEncapsulation,
|
|
4
|
+
booleanAttribute,
|
|
4
5
|
computed,
|
|
5
6
|
input,
|
|
6
7
|
output,
|
|
@@ -40,9 +41,9 @@ import { cn } from '../../utils/cn';
|
|
|
40
41
|
export class UiScrollArea {
|
|
41
42
|
readonly orientation = input<'vertical' | 'horizontal'>('vertical');
|
|
42
43
|
readonly height = input<string>('');
|
|
43
|
-
readonly bounces = input(true);
|
|
44
|
-
readonly scrollBar = input(true);
|
|
45
|
-
readonly nestedScroll = input(false);
|
|
44
|
+
readonly bounces = input(true, { transform: booleanAttribute });
|
|
45
|
+
readonly scrollBar = input(true, { transform: booleanAttribute });
|
|
46
|
+
readonly nestedScroll = input(false, { transform: booleanAttribute });
|
|
46
47
|
readonly upperThreshold = input(0);
|
|
47
48
|
readonly lowerThreshold = input(0);
|
|
48
49
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
UI_SEARCH_FIELD_CONTAINER,
|
|
3
|
+
UiSearchField,
|
|
4
|
+
type SearchFieldContainer,
|
|
5
|
+
} from './search-field';
|
|
6
|
+
export {
|
|
7
|
+
KEYBOARD_GAP,
|
|
8
|
+
keyboardHeightOf,
|
|
9
|
+
keyboardLift,
|
|
10
|
+
keyboardScreen,
|
|
11
|
+
measureForKeyboard,
|
|
12
|
+
type KeyboardHeightEvent,
|
|
13
|
+
type KeyboardRect,
|
|
14
|
+
type KeyboardScreen,
|
|
15
|
+
} from './keyboard';
|