@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
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
render,
|
|
5
5
|
waitForUpdate,
|
|
6
6
|
} from '@blotch/angular-lynx-testing-library';
|
|
7
|
-
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
7
|
+
import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
|
|
8
8
|
import {
|
|
9
9
|
type MockInstance,
|
|
10
10
|
afterEach,
|
|
@@ -15,12 +15,16 @@ import {
|
|
|
15
15
|
vi,
|
|
16
16
|
} from 'vitest';
|
|
17
17
|
import { DURATION } from '../../utils/animate';
|
|
18
|
+
import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
|
|
18
19
|
import {
|
|
19
20
|
type LynxMainThread,
|
|
20
21
|
lynxMainThread,
|
|
21
22
|
} from '../../../test-utils/lynx-main-thread';
|
|
22
23
|
import {
|
|
24
|
+
type AlertDialogFooterOrientation,
|
|
25
|
+
type AlertDialogVariant,
|
|
23
26
|
UiAlertDialog,
|
|
27
|
+
UiAlertDialogAction,
|
|
24
28
|
UiAlertDialogDescription,
|
|
25
29
|
UiAlertDialogFooter,
|
|
26
30
|
UiAlertDialogHeader,
|
|
@@ -101,14 +105,25 @@ const START = 0;
|
|
|
101
105
|
/** `__ElementAnimate`'s first argument for canceling an animation. */
|
|
102
106
|
const CANCEL = 3;
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
afterEach(() => animate.mockRestore());
|
|
108
|
+
/** Accessibility settings a test can change, as the host app would. */
|
|
109
|
+
const settings = {
|
|
110
|
+
reduceMotion: signal(false),
|
|
111
|
+
reduceTransparency: signal(false),
|
|
112
|
+
increaseContrast: signal(false),
|
|
113
|
+
};
|
|
111
114
|
|
|
115
|
+
// Watched per test: the testing library resets the main thread after each.
|
|
116
|
+
let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
119
|
+
});
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
animate.mockRestore();
|
|
122
|
+
vi.unstubAllGlobals();
|
|
123
|
+
settings.reduceMotion.set(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('UiAlertDialog', () => {
|
|
112
127
|
it('renders each part with its projected content', async () => {
|
|
113
128
|
const { getByText } = await renderAlertDialog();
|
|
114
129
|
expect(getByText('Sure?')).toBeTruthy();
|
|
@@ -232,6 +247,34 @@ describe('UiAlertDialog', () => {
|
|
|
232
247
|
expect(host.open()).toBe(true);
|
|
233
248
|
});
|
|
234
249
|
|
|
250
|
+
it('only fades its panel under Reduce Motion', async () => {
|
|
251
|
+
const result = await render(AlertDialogHost, {
|
|
252
|
+
providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
|
|
253
|
+
});
|
|
254
|
+
const host = result.componentRef.instance as AlertDialogHost;
|
|
255
|
+
const panel = result.container.querySelector(
|
|
256
|
+
'[accessibility-exclusive-focus]',
|
|
257
|
+
)!;
|
|
258
|
+
settings.reduceMotion.set(true);
|
|
259
|
+
host.open.set(true);
|
|
260
|
+
await after(OPENED);
|
|
261
|
+
expect(animate).toHaveBeenCalledWith(panel, [
|
|
262
|
+
START,
|
|
263
|
+
expect.any(String),
|
|
264
|
+
[{ opacity: 0 }, { opacity: 1 }],
|
|
265
|
+
expect.any(Object),
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
host.open.set(false);
|
|
269
|
+
await after(CLOSED);
|
|
270
|
+
expect(animate).toHaveBeenCalledWith(panel, [
|
|
271
|
+
START,
|
|
272
|
+
expect.any(String),
|
|
273
|
+
[{ opacity: 1 }, { opacity: 0 }],
|
|
274
|
+
expect.any(Object),
|
|
275
|
+
]);
|
|
276
|
+
});
|
|
277
|
+
|
|
235
278
|
it('stops its close sequence when it is destroyed while closing', async () => {
|
|
236
279
|
const warn = vi.spyOn(console, 'warn');
|
|
237
280
|
const { host } = await renderAlertDialog();
|
|
@@ -253,3 +296,504 @@ describe('UiAlertDialog', () => {
|
|
|
253
296
|
expect(host.closes()).toBe(0);
|
|
254
297
|
});
|
|
255
298
|
});
|
|
299
|
+
|
|
300
|
+
@Component({
|
|
301
|
+
imports: [
|
|
302
|
+
UiAlertDialog,
|
|
303
|
+
UiAlertDialogHeader,
|
|
304
|
+
UiAlertDialogTitle,
|
|
305
|
+
UiAlertDialogDescription,
|
|
306
|
+
UiAlertDialogFooter,
|
|
307
|
+
UiAlertDialogAction,
|
|
308
|
+
LYNX_ELEMENTS,
|
|
309
|
+
],
|
|
310
|
+
template: `
|
|
311
|
+
<ui-alert-dialog
|
|
312
|
+
[variant]="variant()"
|
|
313
|
+
[(open)]="open"
|
|
314
|
+
class="tall"
|
|
315
|
+
(closed)="closes.set(closes() + 1)"
|
|
316
|
+
>
|
|
317
|
+
<ui-alert-dialog-header>
|
|
318
|
+
<ui-alert-dialog-title>Delete Photo?</ui-alert-dialog-title>
|
|
319
|
+
<ui-alert-dialog-description>
|
|
320
|
+
It's deleted from all your devices.
|
|
321
|
+
</ui-alert-dialog-description>
|
|
322
|
+
</ui-alert-dialog-header>
|
|
323
|
+
<ui-alert-dialog-footer [orientation]="orientation()">
|
|
324
|
+
<ui-alert-dialog-action
|
|
325
|
+
variant="cancel"
|
|
326
|
+
(pressed)="choices.push('cancel')"
|
|
327
|
+
>
|
|
328
|
+
Cancel
|
|
329
|
+
</ui-alert-dialog-action>
|
|
330
|
+
@if (withSave()) {
|
|
331
|
+
<ui-alert-dialog-action
|
|
332
|
+
[preferred]="savePreferred()"
|
|
333
|
+
(pressed)="choices.push('save')"
|
|
334
|
+
>
|
|
335
|
+
Save
|
|
336
|
+
</ui-alert-dialog-action>
|
|
337
|
+
}
|
|
338
|
+
<ui-alert-dialog-action
|
|
339
|
+
variant="destructive"
|
|
340
|
+
class="wide"
|
|
341
|
+
[preferred]="deletePreferred()"
|
|
342
|
+
[accessibilityLabel]="deleteLabel()"
|
|
343
|
+
(pressed)="choices.push('delete')"
|
|
344
|
+
>
|
|
345
|
+
Delete
|
|
346
|
+
</ui-alert-dialog-action>
|
|
347
|
+
</ui-alert-dialog-footer>
|
|
348
|
+
</ui-alert-dialog>
|
|
349
|
+
`,
|
|
350
|
+
})
|
|
351
|
+
class GlassAlertHost {
|
|
352
|
+
readonly variant = signal<AlertDialogVariant>('glass');
|
|
353
|
+
readonly open = signal(false);
|
|
354
|
+
readonly orientation = signal<AlertDialogFooterOrientation>('auto');
|
|
355
|
+
readonly withSave = signal(false);
|
|
356
|
+
readonly savePreferred = signal(false);
|
|
357
|
+
readonly deletePreferred = signal(false);
|
|
358
|
+
readonly deleteLabel = signal<string | undefined>(undefined);
|
|
359
|
+
readonly closes = signal(0);
|
|
360
|
+
readonly choices: string[] = [];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const IOS_26 = { platform: 'iOS', osVersion: '26.0' };
|
|
364
|
+
const IOS_18 = { platform: 'iOS', osVersion: '18.6' };
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Renders the glass alert on a platform, as `SystemInfo` reports it, or with
|
|
368
|
+
* none, which is Material's design.
|
|
369
|
+
*/
|
|
370
|
+
const renderGlassAlert = async (
|
|
371
|
+
platform: { platform: string; osVersion: string } | null = IOS_26,
|
|
372
|
+
) => {
|
|
373
|
+
vi.stubGlobal('SystemInfo', platform ?? undefined);
|
|
374
|
+
const result = await render(GlassAlertHost, {
|
|
375
|
+
providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
|
|
376
|
+
});
|
|
377
|
+
const host = result.componentRef.instance as GlassAlertHost;
|
|
378
|
+
const panel = () =>
|
|
379
|
+
result.container.querySelector('[accessibility-exclusive-focus]')!;
|
|
380
|
+
const shell = () => panel().parentElement!;
|
|
381
|
+
/**
|
|
382
|
+
* An action's own element, from its label.
|
|
383
|
+
*/
|
|
384
|
+
const action = (label: string) => result.getByText(label).parentElement!;
|
|
385
|
+
const update = async (change: () => void) => {
|
|
386
|
+
change();
|
|
387
|
+
await waitForUpdate();
|
|
388
|
+
};
|
|
389
|
+
return {
|
|
390
|
+
...result,
|
|
391
|
+
host,
|
|
392
|
+
panel,
|
|
393
|
+
shell,
|
|
394
|
+
dim: () => shell().previousElementSibling!,
|
|
395
|
+
action,
|
|
396
|
+
footer: () => action('Cancel').parentElement!,
|
|
397
|
+
style: (element: Element) => element.getAttribute('style') ?? '',
|
|
398
|
+
update,
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
describe('UiAlertDialog with variant="glass"', () => {
|
|
403
|
+
describe('on iOS 26 and later', () => {
|
|
404
|
+
it('draws a Liquid Glass panel with concentric corners', async () => {
|
|
405
|
+
const { panel, style } = await renderGlassAlert();
|
|
406
|
+
expect(panel().tagName.toLowerCase()).toBe('blur-view');
|
|
407
|
+
expect(panel().getAttribute('blur-effect')).toBe('glass');
|
|
408
|
+
// 40 = the 24px capsule buttons' radius + their 16px inset.
|
|
409
|
+
expect(style(panel())).toContain('border-radius: 40px');
|
|
410
|
+
expect(panel().classList).toContain('max-w-[320px]');
|
|
411
|
+
expect(panel().classList).toContain('pt-[22px]');
|
|
412
|
+
// The dialog's class still goes on the panel.
|
|
413
|
+
expect(panel().classList).toContain('tall');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('sets the title and message leading, in the glass colors', async () => {
|
|
417
|
+
const { getByText, container } = await renderGlassAlert();
|
|
418
|
+
expect(getByText('Delete Photo?').classList).toContain('text-[17px]');
|
|
419
|
+
expect(getByText('Delete Photo?').classList).toContain(
|
|
420
|
+
'text-glass-foreground',
|
|
421
|
+
);
|
|
422
|
+
expect(getByText('Delete Photo?').classList).not.toContain('text-center');
|
|
423
|
+
expect(
|
|
424
|
+
getByText("It's deleted from all your devices.").classList,
|
|
425
|
+
).toContain('text-glass-muted-foreground');
|
|
426
|
+
expect(container.querySelector('.px-\\[14px\\]')).toBeTruthy();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('puts two actions side by side on capsules, cancel first', async () => {
|
|
430
|
+
const { action, footer, style } = await renderGlassAlert();
|
|
431
|
+
expect(footer().classList).toContain('flex-row');
|
|
432
|
+
expect(action('Cancel').classList).toContain('rounded-full');
|
|
433
|
+
expect(action('Cancel').classList).toContain('flex-1');
|
|
434
|
+
expect(action('Cancel').classList).toContain('h-[48px]');
|
|
435
|
+
expect(style(action('Cancel'))).toContain('order: 0');
|
|
436
|
+
expect(style(action('Delete'))).toContain('order: 1');
|
|
437
|
+
// The capsule's fill: the glass's foreground, faintly.
|
|
438
|
+
const fill = action('Cancel').firstElementChild!;
|
|
439
|
+
expect(fill.classList).toContain('bg-glass-foreground');
|
|
440
|
+
expect(style(fill)).toContain('opacity: 0.12');
|
|
441
|
+
expect(action('Delete').lastElementChild!.classList).toContain(
|
|
442
|
+
'text-destructive',
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('stacks three actions, cancel at the bottom wherever it is written', async () => {
|
|
447
|
+
const { host, action, footer, style, update } = await renderGlassAlert();
|
|
448
|
+
await update(() => host.withSave.set(true));
|
|
449
|
+
expect(footer().classList).toContain('flex-col');
|
|
450
|
+
expect(action('Cancel').classList).not.toContain('flex-1');
|
|
451
|
+
expect(style(action('Save'))).toContain('order: 0');
|
|
452
|
+
expect(style(action('Delete'))).toContain('order: 1');
|
|
453
|
+
expect(style(action('Cancel'))).toContain('order: 2');
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('lays its actions out as the footer says', async () => {
|
|
457
|
+
const { host, footer, update } = await renderGlassAlert();
|
|
458
|
+
await update(() => host.orientation.set('vertical'));
|
|
459
|
+
expect(footer().classList).toContain('flex-col');
|
|
460
|
+
await update(() => {
|
|
461
|
+
host.withSave.set(true);
|
|
462
|
+
host.orientation.set('horizontal');
|
|
463
|
+
});
|
|
464
|
+
expect(footer().classList).toContain('flex-row');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('fills the preferred action with the tint, or the destructive color', async () => {
|
|
468
|
+
const { host, action, update } = await renderGlassAlert();
|
|
469
|
+
await update(() => {
|
|
470
|
+
host.withSave.set(true);
|
|
471
|
+
host.savePreferred.set(true);
|
|
472
|
+
});
|
|
473
|
+
expect(action('Save').classList).toContain('bg-glass-tint');
|
|
474
|
+
expect(action('Save').lastElementChild!.classList).toContain(
|
|
475
|
+
'text-glass-tint-foreground',
|
|
476
|
+
);
|
|
477
|
+
expect(action('Save').lastElementChild!.classList).toContain(
|
|
478
|
+
'font-semibold',
|
|
479
|
+
);
|
|
480
|
+
// Its highlight is the tint's foreground, shown only under the finger.
|
|
481
|
+
expect(action('Save').firstElementChild!.classList).toContain(
|
|
482
|
+
'bg-glass-tint-foreground',
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
await update(() => host.deletePreferred.set(true));
|
|
486
|
+
expect(action('Delete').classList).toContain('bg-destructive');
|
|
487
|
+
expect(action('Delete').lastElementChild!.classList).toContain(
|
|
488
|
+
'text-destructive-foreground',
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('deepens a capsule while it is pressed, by touch or by mouse', async () => {
|
|
493
|
+
const { host, action, style, update } = await renderGlassAlert();
|
|
494
|
+
const fill = () => action('Cancel').firstElementChild!;
|
|
495
|
+
await update(() => fireEvent.touchstart(action('Cancel')));
|
|
496
|
+
expect(style(fill())).toContain('opacity: 0.2');
|
|
497
|
+
await update(() => fireEvent.touchend(action('Cancel')));
|
|
498
|
+
expect(style(fill())).toContain('opacity: 0.12');
|
|
499
|
+
await update(() => fireLynxEvent(action('Cancel'), 'mousedown'));
|
|
500
|
+
expect(style(fill())).toContain('opacity: 0.2');
|
|
501
|
+
await update(() => fireLynxEvent(action('Cancel'), 'mouseleave'));
|
|
502
|
+
expect(style(fill())).toContain('opacity: 0.12');
|
|
503
|
+
await update(() => fireLynxEvent(action('Cancel'), 'mousedown'));
|
|
504
|
+
await update(() => fireLynxEvent(action('Cancel'), 'mouseup'));
|
|
505
|
+
expect(style(fill())).toContain('opacity: 0.12');
|
|
506
|
+
await update(() => fireEvent.touchstart(action('Cancel')));
|
|
507
|
+
await update(() => fireEvent.touchcancel(action('Cancel')));
|
|
508
|
+
expect(style(fill())).toContain('opacity: 0.12');
|
|
509
|
+
|
|
510
|
+
// A preferred capsule lightens under the finger instead.
|
|
511
|
+
await update(() => host.deletePreferred.set(true));
|
|
512
|
+
const highlight = () => action('Delete').firstElementChild!;
|
|
513
|
+
expect(style(highlight())).toContain('opacity: 0');
|
|
514
|
+
await update(() => fireEvent.touchstart(action('Delete')));
|
|
515
|
+
expect(style(highlight())).toContain('opacity: 0.1');
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('scales its glass in from a little larger as it fades, and fades it out', async () => {
|
|
519
|
+
const { host, shell, dim, update } = await renderGlassAlert();
|
|
520
|
+
await update(() => host.open.set(true));
|
|
521
|
+
await after(OPENED);
|
|
522
|
+
expect(animate).toHaveBeenCalledWith(dim(), [
|
|
523
|
+
START,
|
|
524
|
+
expect.any(String),
|
|
525
|
+
[{ opacity: 0 }, { opacity: 1 }],
|
|
526
|
+
expect.any(Object),
|
|
527
|
+
]);
|
|
528
|
+
// The view the glass is in, never the glass: on iOS a blur view loses
|
|
529
|
+
// its material at less than full opacity.
|
|
530
|
+
expect(animate).toHaveBeenCalledWith(shell(), [
|
|
531
|
+
START,
|
|
532
|
+
expect.any(String),
|
|
533
|
+
[
|
|
534
|
+
{ transform: 'scale(1.15) translateY(0px)', opacity: 0 },
|
|
535
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 1 },
|
|
536
|
+
],
|
|
537
|
+
expect.objectContaining({ duration: DURATION.slow }),
|
|
538
|
+
]);
|
|
539
|
+
animate.mockClear();
|
|
540
|
+
|
|
541
|
+
await update(() => host.open.set(false));
|
|
542
|
+
await after(CLOSED);
|
|
543
|
+
expect(animate).toHaveBeenCalledWith(shell(), [
|
|
544
|
+
START,
|
|
545
|
+
expect.any(String),
|
|
546
|
+
[
|
|
547
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 1 },
|
|
548
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 0 },
|
|
549
|
+
],
|
|
550
|
+
expect.any(Object),
|
|
551
|
+
]);
|
|
552
|
+
expect(animate).toHaveBeenCalledWith(dim(), [
|
|
553
|
+
START,
|
|
554
|
+
expect.any(String),
|
|
555
|
+
[{ opacity: 1 }, { opacity: 0 }],
|
|
556
|
+
expect.any(Object),
|
|
557
|
+
]);
|
|
558
|
+
expect(host.closes()).toBe(1);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('only fades under Reduce Motion', async () => {
|
|
562
|
+
const { host, shell, update } = await renderGlassAlert();
|
|
563
|
+
settings.reduceMotion.set(true);
|
|
564
|
+
await update(() => host.open.set(true));
|
|
565
|
+
await after(OPENED);
|
|
566
|
+
expect(animate).toHaveBeenCalledWith(shell(), [
|
|
567
|
+
START,
|
|
568
|
+
expect.any(String),
|
|
569
|
+
[{ opacity: 0 }, { opacity: 1 }],
|
|
570
|
+
expect.any(Object),
|
|
571
|
+
]);
|
|
572
|
+
await update(() => host.open.set(false));
|
|
573
|
+
await after(CLOSED);
|
|
574
|
+
expect(animate).toHaveBeenCalledWith(shell(), [
|
|
575
|
+
START,
|
|
576
|
+
expect.any(String),
|
|
577
|
+
[{ opacity: 1 }, { opacity: 0 }],
|
|
578
|
+
expect.any(Object),
|
|
579
|
+
]);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('stays open when the dimmed screen or the panel is tapped', async () => {
|
|
583
|
+
const { host, dim, panel, update } = await renderGlassAlert();
|
|
584
|
+
await update(() => host.open.set(true));
|
|
585
|
+
await after(OPENED);
|
|
586
|
+
await update(() => fireEvent.tap(dim()));
|
|
587
|
+
await update(() => fireEvent.tap(panel(), { eventType: 'catchEvent' }));
|
|
588
|
+
expect(host.open()).toBe(true);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('dims the screen at 20%, with no blur', async () => {
|
|
592
|
+
const { dim } = await renderGlassAlert();
|
|
593
|
+
expect(dim().classList).toContain('bg-black/20');
|
|
594
|
+
expect(dim().tagName.toLowerCase()).toBe('view');
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
describe('on iOS 15 to 25', () => {
|
|
599
|
+
it("draws that era's frosted alert, 270pt wide with 14pt corners", async () => {
|
|
600
|
+
const { panel, style, getByText } = await renderGlassAlert(IOS_18);
|
|
601
|
+
expect(panel().getAttribute('blur-effect')).toBe('extra-light');
|
|
602
|
+
expect(style(panel())).toContain('border-radius: 14px');
|
|
603
|
+
expect(panel().classList).toContain('w-[270px]');
|
|
604
|
+
expect(getByText('Delete Photo?').classList).toContain('text-center');
|
|
605
|
+
expect(
|
|
606
|
+
getByText("It's deleted from all your devices.").classList,
|
|
607
|
+
).toContain('text-[13px]');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('parts its buttons with hairlines', async () => {
|
|
611
|
+
const { host, action, footer, update } = await renderGlassAlert(IOS_18);
|
|
612
|
+
// The footer's own line over the buttons.
|
|
613
|
+
expect(footer().firstElementChild!.classList).toContain('h-[0.5px]');
|
|
614
|
+
// Beside the second button in a row, but not the first.
|
|
615
|
+
expect(action('Cancel').querySelector('.w-\\[0\\.5px\\]')).toBeNull();
|
|
616
|
+
expect(action('Delete').querySelector('.w-\\[0\\.5px\\]')).toBeTruthy();
|
|
617
|
+
expect(action('Delete').classList).toContain('h-[44px]');
|
|
618
|
+
expect(action('Delete').classList).toContain('flex-1');
|
|
619
|
+
|
|
620
|
+
// Above every button but the first in a stack.
|
|
621
|
+
await update(() => host.withSave.set(true));
|
|
622
|
+
expect(action('Save').querySelector('.h-\\[0\\.5px\\]')).toBeNull();
|
|
623
|
+
expect(action('Delete').querySelector('.h-\\[0\\.5px\\]')).toBeTruthy();
|
|
624
|
+
expect(action('Cancel').querySelector('.h-\\[0\\.5px\\]')).toBeTruthy();
|
|
625
|
+
expect(action('Cancel').classList).not.toContain('flex-1');
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('sets the preferred action in bold, or else the cancel', async () => {
|
|
629
|
+
const { host, getByText, update } = await renderGlassAlert(IOS_18);
|
|
630
|
+
await update(() => host.withSave.set(true));
|
|
631
|
+
expect(getByText('Cancel').classList).toContain('font-semibold');
|
|
632
|
+
expect(getByText('Cancel').classList).toContain('text-glass-tint');
|
|
633
|
+
expect(getByText('Save').classList).not.toContain('font-semibold');
|
|
634
|
+
expect(getByText('Delete').classList).toContain('text-destructive');
|
|
635
|
+
|
|
636
|
+
await update(() => host.savePreferred.set(true));
|
|
637
|
+
expect(getByText('Save').classList).toContain('font-semibold');
|
|
638
|
+
expect(getByText('Cancel').classList).not.toContain('font-semibold');
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('lights a button up while it is pressed', async () => {
|
|
642
|
+
const { action, style, update } = await renderGlassAlert(IOS_18);
|
|
643
|
+
const highlight = () => action('Delete').firstElementChild!;
|
|
644
|
+
expect(style(highlight())).toContain('opacity: 0');
|
|
645
|
+
await update(() => fireEvent.touchstart(action('Delete')));
|
|
646
|
+
expect(style(highlight())).toContain('opacity: 0.1');
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
describe('on Android', () => {
|
|
651
|
+
it("draws Material 3's basic dialog", async () => {
|
|
652
|
+
const { panel, style, getByText, dim, footer } =
|
|
653
|
+
await renderGlassAlert(null);
|
|
654
|
+
expect(style(panel())).toContain('border-radius: 28px');
|
|
655
|
+
expect(panel().classList).toContain('bg-secondary');
|
|
656
|
+
expect(panel().classList).toContain('p-[24px]');
|
|
657
|
+
expect(panel().classList).toContain('min-w-[280px]');
|
|
658
|
+
expect(getByText('Delete Photo?').classList).toContain('text-[24px]');
|
|
659
|
+
expect(
|
|
660
|
+
getByText("It's deleted from all your devices.").classList,
|
|
661
|
+
).toContain('text-[14px]');
|
|
662
|
+
expect(dim().classList).toContain('bg-black/[0.32]');
|
|
663
|
+
expect(footer().classList).toContain('justify-end');
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('keeps its text buttons in a row at the end, cancel first', async () => {
|
|
667
|
+
const { host, action, footer, getByText, style, update } =
|
|
668
|
+
await renderGlassAlert(null);
|
|
669
|
+
await update(() => host.withSave.set(true));
|
|
670
|
+
expect(footer().classList).toContain('flex-row');
|
|
671
|
+
expect(style(action('Cancel'))).toContain('order: 0');
|
|
672
|
+
expect(action('Save').classList).toContain('h-[40px]');
|
|
673
|
+
expect(getByText('Save').classList).toContain('text-primary');
|
|
674
|
+
expect(getByText('Delete').classList).toContain('text-destructive');
|
|
675
|
+
// The state layer takes the label's color.
|
|
676
|
+
expect(action('Save').firstElementChild!.classList).toContain(
|
|
677
|
+
'bg-primary',
|
|
678
|
+
);
|
|
679
|
+
expect(action('Delete').firstElementChild!.classList).toContain(
|
|
680
|
+
'bg-destructive',
|
|
681
|
+
);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('grows in and shrinks back as it fades', async () => {
|
|
685
|
+
const { host, shell, update } = await renderGlassAlert(null);
|
|
686
|
+
await update(() => host.open.set(true));
|
|
687
|
+
await after(OPENED);
|
|
688
|
+
expect(animate).toHaveBeenCalledWith(shell(), [
|
|
689
|
+
START,
|
|
690
|
+
expect.any(String),
|
|
691
|
+
[
|
|
692
|
+
{ transform: 'scale(0.9) translateY(0px)', opacity: 0 },
|
|
693
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 1 },
|
|
694
|
+
],
|
|
695
|
+
expect.any(Object),
|
|
696
|
+
]);
|
|
697
|
+
await update(() => host.open.set(false));
|
|
698
|
+
await after(CLOSED);
|
|
699
|
+
expect(animate).toHaveBeenCalledWith(shell(), [
|
|
700
|
+
START,
|
|
701
|
+
expect.any(String),
|
|
702
|
+
[
|
|
703
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 1 },
|
|
704
|
+
{ transform: 'scale(0.95) translateY(0px)', opacity: 0 },
|
|
705
|
+
],
|
|
706
|
+
expect.any(Object),
|
|
707
|
+
]);
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('emits the chosen action and closes, once', async () => {
|
|
712
|
+
const { host, getByRole, action, update } = await renderGlassAlert();
|
|
713
|
+
await update(() => host.open.set(true));
|
|
714
|
+
await after(OPENED);
|
|
715
|
+
expect(getByRole('button', { name: 'Delete' })).toBeTruthy();
|
|
716
|
+
await update(() => fireEvent.tap(action('Delete')));
|
|
717
|
+
// A second tap while it closes does nothing.
|
|
718
|
+
await update(() => fireEvent.tap(action('Cancel')));
|
|
719
|
+
expect(host.choices).toEqual(['delete']);
|
|
720
|
+
expect(host.open()).toBe(false);
|
|
721
|
+
await after(CLOSED);
|
|
722
|
+
expect(host.closes()).toBe(1);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it('makes an action one element that says its accessibility label', async () => {
|
|
726
|
+
const { host, getByRole, action, update } = await renderGlassAlert();
|
|
727
|
+
// Open: a screen reader reaches nothing in the hidden overlay.
|
|
728
|
+
await update(() => {
|
|
729
|
+
host.open.set(true);
|
|
730
|
+
host.deleteLabel.set('Delete the photo');
|
|
731
|
+
});
|
|
732
|
+
await after(OPENED);
|
|
733
|
+
expect(getByRole('button', { name: 'Delete the photo' })).toBe(
|
|
734
|
+
action('Delete'),
|
|
735
|
+
);
|
|
736
|
+
expect(
|
|
737
|
+
action('Delete').lastElementChild!.getAttribute('accessibility-traits'),
|
|
738
|
+
).toBeNull();
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('draws its own panel again with variant="default", and its actions as the default buttons', async () => {
|
|
742
|
+
const { host, panel, action, getByText, style, update } =
|
|
743
|
+
await renderGlassAlert();
|
|
744
|
+
await update(() => host.variant.set('default'));
|
|
745
|
+
expect(panel().tagName.toLowerCase()).toBe('view');
|
|
746
|
+
expect(panel().classList).toContain('bg-background');
|
|
747
|
+
expect(action('Cancel').classList).toContain('border-border');
|
|
748
|
+
expect(action('Delete').classList).toContain('bg-destructive');
|
|
749
|
+
// The given class wins over the action's own.
|
|
750
|
+
expect(action('Delete').classList).toContain('wide');
|
|
751
|
+
expect(getByText('Cancel').classList).toContain('text-foreground');
|
|
752
|
+
expect(getByText('Delete').classList).toContain(
|
|
753
|
+
'text-destructive-foreground',
|
|
754
|
+
);
|
|
755
|
+
// Written order, no fill or hairline.
|
|
756
|
+
expect(style(action('Cancel'))).not.toContain('order');
|
|
757
|
+
expect(action('Cancel').querySelector('.absolute')).toBeNull();
|
|
758
|
+
|
|
759
|
+
await update(() => host.withSave.set(true));
|
|
760
|
+
expect(action('Save').classList).toContain('bg-primary');
|
|
761
|
+
expect(getByText('Save').classList).toContain('text-primary-foreground');
|
|
762
|
+
|
|
763
|
+
// A pressed button dims.
|
|
764
|
+
await update(() => fireEvent.touchstart(action('Save')));
|
|
765
|
+
expect(style(action('Save'))).toContain('opacity: 0.8');
|
|
766
|
+
await update(() => fireEvent.touchend(action('Save')));
|
|
767
|
+
expect(style(action('Save'))).not.toContain('opacity');
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
@Component({
|
|
772
|
+
imports: [
|
|
773
|
+
UiAlertDialogHeader,
|
|
774
|
+
UiAlertDialogTitle,
|
|
775
|
+
UiAlertDialogDescription,
|
|
776
|
+
UiAlertDialogFooter,
|
|
777
|
+
LYNX_ELEMENTS,
|
|
778
|
+
],
|
|
779
|
+
template: `
|
|
780
|
+
<ui-alert-dialog-header>
|
|
781
|
+
<ui-alert-dialog-title>Alone</ui-alert-dialog-title>
|
|
782
|
+
<ui-alert-dialog-description
|
|
783
|
+
>Outside a dialog</ui-alert-dialog-description
|
|
784
|
+
>
|
|
785
|
+
</ui-alert-dialog-header>
|
|
786
|
+
<ui-alert-dialog-footer><text>Footer</text></ui-alert-dialog-footer>
|
|
787
|
+
`,
|
|
788
|
+
})
|
|
789
|
+
class LoosePartsHost {}
|
|
790
|
+
|
|
791
|
+
it("draws an alert dialog's parts in the default look outside one", async () => {
|
|
792
|
+
const { getByText } = await render(LoosePartsHost);
|
|
793
|
+
expect(getByText('Alone').classList).toContain('text-lg');
|
|
794
|
+
expect(getByText('Outside a dialog').classList).toContain('text-sm');
|
|
795
|
+
expect(getByText('Footer').parentElement!.classList).toContain('pt-4');
|
|
796
|
+
expect(getByText('Alone').parentElement!.parentElement!.classList).toContain(
|
|
797
|
+
'gap-1.5',
|
|
798
|
+
);
|
|
799
|
+
});
|