@blotch/dolan 22.2.4 → 22.2.6
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
|
@@ -23,7 +23,9 @@ import { LynxTheme } from '@blotch/angular-lynx';
|
|
|
23
23
|
*
|
|
24
24
|
* The tables mirror `default.css` and `dark.css` (a test in the theme folder
|
|
25
25
|
* keeps them in step). If you change a color there, change it here too, or pass
|
|
26
|
-
* your own with {@link provideIconTheme}.
|
|
26
|
+
* your own with {@link provideIconTheme}. Colors set at runtime, such as
|
|
27
|
+
* Android's wallpaper colors, come from an element above instead (see
|
|
28
|
+
* {@link UI_THEME_COLORS}).
|
|
27
29
|
*/
|
|
28
30
|
|
|
29
31
|
/** Theme token name (without `--`) to a complete CSS color. */
|
|
@@ -207,6 +209,40 @@ export const injectColorScheme = (): Signal<IconColorScheme> => {
|
|
|
207
209
|
);
|
|
208
210
|
};
|
|
209
211
|
|
|
212
|
+
/** Theme colors for each scheme, each a complete CSS color, as in the tables. */
|
|
213
|
+
export type ThemeColors = {
|
|
214
|
+
readonly light?: IconColorTokens;
|
|
215
|
+
readonly dark?: IconColorTokens;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Theme colors that replace the tables' for everything inside an element,
|
|
220
|
+
* as a signal: an element that sets the theme's CSS variables at runtime
|
|
221
|
+
* provides the values it sets, since SVGs and native attributes can't read
|
|
222
|
+
* them. `UiDynamicColor` provides Android's wallpaper colors this way. Icons,
|
|
223
|
+
* glass tints and charts inside it resolve their tokens against them.
|
|
224
|
+
*/
|
|
225
|
+
export const UI_THEME_COLORS = new InjectionToken<Signal<ThemeColors>>(
|
|
226
|
+
'UI_THEME_COLORS',
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The color table for the app's color scheme, for a component's constructor:
|
|
231
|
+
* the theme's copy, with what an element above sets in its place (see
|
|
232
|
+
* {@link UI_THEME_COLORS}). A signal, so it follows scheme and theme changes.
|
|
233
|
+
*/
|
|
234
|
+
export const injectThemeColors = (): Signal<IconColorTokens> => {
|
|
235
|
+
const theme = inject(UI_ICON_THEME);
|
|
236
|
+
const scheme = injectColorScheme();
|
|
237
|
+
const replaced = inject(UI_THEME_COLORS, { optional: true });
|
|
238
|
+
|
|
239
|
+
return computed(() => {
|
|
240
|
+
const active = scheme();
|
|
241
|
+
const colors = replaced?.()[active];
|
|
242
|
+
return colors ? { ...theme[active], ...colors } : theme[active];
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
|
|
210
246
|
/**
|
|
211
247
|
* For an SVG component's constructor: a function that resolves its `color`
|
|
212
248
|
* input (or, without one, the surrounding {@link UI_ICON_COLOR}, then the
|
|
@@ -214,14 +250,9 @@ export const injectColorScheme = (): Signal<IconColorScheme> => {
|
|
|
214
250
|
* `computed()` calling it follows scheme and theme changes.
|
|
215
251
|
*/
|
|
216
252
|
export const injectIconColor = (): ((color: string | undefined) => string) => {
|
|
217
|
-
const
|
|
218
|
-
const scheme = injectColorScheme();
|
|
253
|
+
const tokens = injectThemeColors();
|
|
219
254
|
const inherited = inject(UI_ICON_COLOR, { optional: true });
|
|
220
255
|
|
|
221
|
-
const tokens = computed(() =>
|
|
222
|
-
scheme() === 'dark' ? theme.dark : theme.light,
|
|
223
|
-
);
|
|
224
|
-
|
|
225
256
|
return (color) =>
|
|
226
257
|
resolveIconColor(color ?? inherited?.() ?? DEFAULT_ICON_COLOR, tokens());
|
|
227
258
|
};
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
type ElementRef,
|
|
3
3
|
Component,
|
|
4
4
|
ViewEncapsulation,
|
|
5
|
+
booleanAttribute,
|
|
5
6
|
computed,
|
|
6
7
|
effect,
|
|
7
8
|
input,
|
|
@@ -90,7 +91,7 @@ export class UiInput implements FormValueControl<string> {
|
|
|
90
91
|
readonly type = input<string>('text');
|
|
91
92
|
readonly helperText = input<string>('');
|
|
92
93
|
readonly error = input<string>('');
|
|
93
|
-
readonly disabled = input(false);
|
|
94
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
94
95
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
95
96
|
|
|
96
97
|
readonly changed = output<string>();
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ViewEncapsulation,
|
|
4
|
+
booleanAttribute,
|
|
5
|
+
computed,
|
|
6
|
+
input,
|
|
7
|
+
} from '@angular/core';
|
|
2
8
|
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
3
9
|
|
|
4
10
|
import { cn } from '../../utils/cn';
|
|
@@ -11,7 +17,7 @@ import { cn } from '../../utils/cn';
|
|
|
11
17
|
template: `<text [class]="labelClass()"><ng-content /></text>`,
|
|
12
18
|
})
|
|
13
19
|
export class UiLabel {
|
|
14
|
-
readonly disabled = input(false);
|
|
20
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
15
21
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
16
22
|
|
|
17
23
|
protected readonly labelClass = computed(() =>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Component,
|
|
3
3
|
ViewEncapsulation,
|
|
4
|
+
booleanAttribute,
|
|
4
5
|
computed,
|
|
5
6
|
input,
|
|
6
7
|
output,
|
|
@@ -90,8 +91,8 @@ export class UiLineChart extends UiCartesianPreset<XYDatum> {
|
|
|
90
91
|
readonly color = input('var(--primary)');
|
|
91
92
|
readonly strokeWidth = input(2);
|
|
92
93
|
readonly dotRadius = input(3);
|
|
93
|
-
readonly showDots = input(true);
|
|
94
|
-
readonly smooth = input(false);
|
|
94
|
+
readonly showDots = input(true, { transform: booleanAttribute });
|
|
95
|
+
readonly smooth = input(false, { transform: booleanAttribute });
|
|
95
96
|
|
|
96
97
|
/** The tapped point's datum. */
|
|
97
98
|
readonly pointTap = output<XYDatum>();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Component,
|
|
3
3
|
ViewEncapsulation,
|
|
4
|
+
booleanAttribute,
|
|
4
5
|
computed,
|
|
5
6
|
input,
|
|
6
7
|
output,
|
|
@@ -45,10 +46,10 @@ export class UiList {
|
|
|
45
46
|
readonly spanCount = input(1);
|
|
46
47
|
readonly orientation = input<'vertical' | 'horizontal'>('vertical');
|
|
47
48
|
readonly height = input<string>('');
|
|
48
|
-
readonly bounces = input(true);
|
|
49
|
-
readonly scrollBar = input(true);
|
|
50
|
-
readonly nestedScroll = input(false);
|
|
51
|
-
readonly sticky = input(false);
|
|
49
|
+
readonly bounces = input(true, { transform: booleanAttribute });
|
|
50
|
+
readonly scrollBar = input(true, { transform: booleanAttribute });
|
|
51
|
+
readonly nestedScroll = input(false, { transform: booleanAttribute });
|
|
52
|
+
readonly sticky = input(false, { transform: booleanAttribute });
|
|
52
53
|
readonly stickyOffset = input(0);
|
|
53
54
|
readonly preloadBufferCount = input(0);
|
|
54
55
|
readonly mainAxisGap = input('');
|
|
@@ -138,6 +138,8 @@ const renderPopup = async ({
|
|
|
138
138
|
host,
|
|
139
139
|
invoke,
|
|
140
140
|
held,
|
|
141
|
+
/** How many measurements `held` is keeping back. */
|
|
142
|
+
pendingCount: () => pending.length,
|
|
141
143
|
/** Answers the measurements `held` kept back. */
|
|
142
144
|
release: () => {
|
|
143
145
|
for (const answer of pending.splice(0)) answer();
|
|
@@ -199,6 +201,16 @@ const settle = async () => {
|
|
|
199
201
|
for (let i = 0; i < 10; i++) await Promise.resolve();
|
|
200
202
|
};
|
|
201
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Waits until the menu has asked for the measurements `held` keeps back, so
|
|
206
|
+
* that `release()` has them to answer. Waiting for the overlay isn't enough:
|
|
207
|
+
* it shows first, and the menu measures itself only after LAYOUT_DELAY, a
|
|
208
|
+
* real timer, which a busy CI runner can fire well after that. Released
|
|
209
|
+
* before then, nothing is answered and the menu waits for good.
|
|
210
|
+
*/
|
|
211
|
+
const waitForHeld = (rendered: Rendered) =>
|
|
212
|
+
vi.waitFor(() => expect(rendered.pendingCount()).toBeGreaterThan(0));
|
|
213
|
+
|
|
202
214
|
// The renderer warns that <ui-menu-popup> isn't a Lynx element.
|
|
203
215
|
beforeEach(() => {
|
|
204
216
|
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
@@ -226,9 +238,8 @@ describe('UiMenuPopup', () => {
|
|
|
226
238
|
rendered.held.menu = true;
|
|
227
239
|
host.open.set(true);
|
|
228
240
|
// Measured off screen at its natural size first.
|
|
229
|
-
await
|
|
230
|
-
|
|
231
|
-
);
|
|
241
|
+
await waitForHeld(rendered);
|
|
242
|
+
expect(overlay().getAttribute('visible')).toBe('true');
|
|
232
243
|
expect(style(panel(), 'left')).toBe('-10000px');
|
|
233
244
|
expect(style(panel(), 'width')).toBe('');
|
|
234
245
|
expect(style(panel(), 'transition')).toBe('none');
|
|
@@ -393,9 +404,8 @@ describe('UiMenuPopup', () => {
|
|
|
393
404
|
|
|
394
405
|
rendered.held.menu = true;
|
|
395
406
|
host.open.set(true);
|
|
396
|
-
await
|
|
397
|
-
|
|
398
|
-
);
|
|
407
|
+
await waitForHeld(rendered);
|
|
408
|
+
expect(overlay().getAttribute('visible')).toBe('true');
|
|
399
409
|
expect(slot().children).toHaveLength(0);
|
|
400
410
|
rendered.release();
|
|
401
411
|
await settle();
|
|
@@ -568,10 +578,7 @@ describe('UiMenuPopup', () => {
|
|
|
568
578
|
rendered.held.menu = true;
|
|
569
579
|
|
|
570
580
|
rendered.host.open.set(true);
|
|
571
|
-
await
|
|
572
|
-
expect(rendered.overlay().getAttribute('visible')).toBe('true'),
|
|
573
|
-
);
|
|
574
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
581
|
+
await waitForHeld(rendered);
|
|
575
582
|
rendered.host.open.set(false);
|
|
576
583
|
rendered.tick();
|
|
577
584
|
rendered.release();
|
|
@@ -587,10 +594,7 @@ describe('UiMenuPopup', () => {
|
|
|
587
594
|
rendered.held.menu = true;
|
|
588
595
|
|
|
589
596
|
rendered.host.open.set(true);
|
|
590
|
-
await
|
|
591
|
-
expect(rendered.overlay().getAttribute('visible')).toBe('true'),
|
|
592
|
-
);
|
|
593
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
597
|
+
await waitForHeld(rendered);
|
|
594
598
|
rendered.release();
|
|
595
599
|
await settle();
|
|
596
600
|
// At its start frame now; closing doesn't let it finish growing.
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
ElementRef,
|
|
5
5
|
Renderer2,
|
|
6
6
|
ViewEncapsulation,
|
|
7
|
+
booleanAttribute,
|
|
7
8
|
computed,
|
|
8
9
|
effect,
|
|
9
10
|
inject,
|
|
@@ -133,8 +134,10 @@ export const measureMenuElement = async (
|
|
|
133
134
|
* placeMenu), and then grows it from its start frame (see menuStartFrame)
|
|
134
135
|
* with CSS transitions, which Lynx plays on iOS, Android and the web alike.
|
|
135
136
|
* The menu is one glass shape whose frame grows: iOS's Liquid Glass follows
|
|
136
|
-
* a moving or resizing shape frame by frame
|
|
137
|
-
*
|
|
137
|
+
* a moving or resizing shape frame by frame. It shows at the trigger's place
|
|
138
|
+
* in the same update that hides the trigger, and hides in the one that shows
|
|
139
|
+
* the trigger again, so the shell can pair a glass trigger with it (see
|
|
140
|
+
* glassId).
|
|
138
141
|
*
|
|
139
142
|
* A context menu's element is lifted by moving the element itself into the
|
|
140
143
|
* overlay, above the dimming layer, at the same place on screen, and back
|
|
@@ -198,7 +201,9 @@ export const measureMenuElement = async (
|
|
|
198
201
|
[style.top.px]="frame()?.top ?? 0"
|
|
199
202
|
[style.width.px]="frame()?.width"
|
|
200
203
|
[style.height.px]="frame()?.height"
|
|
204
|
+
[glassId]="glassId()"
|
|
201
205
|
[style.opacity]="panelOpacity()"
|
|
206
|
+
[style.visibility]="panelVisibility()"
|
|
202
207
|
[style.transition]="panelTransition()"
|
|
203
208
|
[style.align-items]="
|
|
204
209
|
placement()?.horizontal === 'right' ? 'flex-end' : 'flex-start'
|
|
@@ -233,7 +238,7 @@ export const measureMenuElement = async (
|
|
|
233
238
|
})
|
|
234
239
|
export class UiMenuPopup implements OnDestroy {
|
|
235
240
|
/** Whether the menu is open. */
|
|
236
|
-
readonly open = input(false);
|
|
241
|
+
readonly open = input(false, { transform: booleanAttribute });
|
|
237
242
|
/** The element the menu opens from: a button, or the pressed element. */
|
|
238
243
|
readonly anchor = input.required<MenuElement>();
|
|
239
244
|
/** A `'pull-down'` or a `'context'` menu; see MenuKind. */
|
|
@@ -243,6 +248,11 @@ export class UiMenuPopup implements OnDestroy {
|
|
|
243
248
|
* in. Without one, the element isn't lifted.
|
|
244
249
|
*/
|
|
245
250
|
readonly anchorHome = input<MenuElement | null>(null);
|
|
251
|
+
/**
|
|
252
|
+
* The id the menu's glass shares with its trigger's (see UiGlass's
|
|
253
|
+
* `glassId`), so that on iOS the menu takes over a glass trigger's glass.
|
|
254
|
+
*/
|
|
255
|
+
readonly glassId = input<string | undefined>(undefined);
|
|
246
256
|
|
|
247
257
|
/** The user tapped outside the menu, or pressed Android's back button. */
|
|
248
258
|
readonly dismissed = output<void>();
|
|
@@ -349,6 +359,17 @@ export class UiMenuPopup implements OnDestroy {
|
|
|
349
359
|
protected readonly panelTransition = computed(() =>
|
|
350
360
|
this.#transition(['left', 'top', 'width', 'height', 'opacity']),
|
|
351
361
|
);
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* The menu's glass is hidden until it's at its start frame, so it shows in
|
|
365
|
+
* the same update that hides the trigger it grows out of, and hides in the
|
|
366
|
+
* one that shows the trigger again: on iOS the shell pairs the two across
|
|
367
|
+
* those changes (see glassId).
|
|
368
|
+
*/
|
|
369
|
+
protected readonly panelVisibility = computed(() => {
|
|
370
|
+
const phase = this.phase();
|
|
371
|
+
return phase === 'closed' || phase === 'measuring' ? 'hidden' : null;
|
|
372
|
+
});
|
|
352
373
|
protected readonly contentTransition = computed(() =>
|
|
353
374
|
this.#transition(['opacity']),
|
|
354
375
|
);
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
|
|
9
9
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
10
|
import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
|
|
11
|
+
import { UiGlass } from '../glass/glass';
|
|
11
12
|
import {
|
|
12
13
|
UiMenu,
|
|
13
14
|
UiMenuItem,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
@Component({
|
|
20
21
|
imports: [
|
|
21
22
|
LYNX_ELEMENTS,
|
|
23
|
+
UiGlass,
|
|
22
24
|
UiMenu,
|
|
23
25
|
UiMenuTrigger,
|
|
24
26
|
UiMenuItem,
|
|
@@ -31,7 +33,18 @@ import {
|
|
|
31
33
|
(openChange)="open.set($event)"
|
|
32
34
|
(closed)="closed()"
|
|
33
35
|
>
|
|
34
|
-
@if (
|
|
36
|
+
@if (glassTrigger()) {
|
|
37
|
+
<blur-view
|
|
38
|
+
ui-glass
|
|
39
|
+
uiMenuTrigger
|
|
40
|
+
class="trigger"
|
|
41
|
+
[accessibility-element]="true"
|
|
42
|
+
accessibility-traits="button"
|
|
43
|
+
accessibility-label="Sort"
|
|
44
|
+
>
|
|
45
|
+
<text>Sort</text>
|
|
46
|
+
</blur-view>
|
|
47
|
+
} @else if (withTrigger()) {
|
|
35
48
|
<view
|
|
36
49
|
uiMenuTrigger
|
|
37
50
|
class="trigger"
|
|
@@ -69,6 +82,8 @@ import {
|
|
|
69
82
|
disabled
|
|
70
83
|
(pressed)="archived()"
|
|
71
84
|
/>
|
|
85
|
+
<ui-menu-item label="Bold" checked />
|
|
86
|
+
<ui-menu-item label="Pin" [checked]="pinned()" />
|
|
72
87
|
</ui-menu-section>
|
|
73
88
|
</ui-menu>
|
|
74
89
|
`,
|
|
@@ -76,7 +91,9 @@ import {
|
|
|
76
91
|
class MenuHost {
|
|
77
92
|
readonly open = signal(false);
|
|
78
93
|
readonly withTrigger = signal(true);
|
|
94
|
+
readonly glassTrigger = signal(false);
|
|
79
95
|
readonly sort = signal<'name' | 'date'>('date');
|
|
96
|
+
readonly pinned = signal<boolean | undefined>(undefined);
|
|
80
97
|
readonly closed = vi.fn();
|
|
81
98
|
readonly shared = vi.fn();
|
|
82
99
|
readonly deleted = vi.fn();
|
|
@@ -223,13 +240,44 @@ describe('UiMenu', () => {
|
|
|
223
240
|
{ relativeTo: 'screen' },
|
|
224
241
|
expect.any(Function),
|
|
225
242
|
);
|
|
226
|
-
expect(style(trigger(), '
|
|
243
|
+
expect(style(trigger(), 'visibility')).toBe('hidden');
|
|
227
244
|
|
|
228
245
|
// A tap on the trigger again closes it, and the trigger comes back.
|
|
229
246
|
fireEvent.tap(trigger());
|
|
230
247
|
await waitForClose(rendered);
|
|
231
248
|
expect(host.open()).toBe(false);
|
|
232
|
-
expect(style(trigger(), '
|
|
249
|
+
expect(style(trigger(), 'visibility')).toBe('');
|
|
250
|
+
// A trigger that isn't glass has no glass to hand over.
|
|
251
|
+
expect(trigger().hasAttribute('glass-effect-id')).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("takes a glass trigger's glass over, shown as the trigger hides", async () => {
|
|
255
|
+
const rendered = await renderMenu();
|
|
256
|
+
rendered.host.glassTrigger.set(true);
|
|
257
|
+
await waitForUpdate();
|
|
258
|
+
const { trigger, panel } = rendered;
|
|
259
|
+
|
|
260
|
+
// The trigger and the menu share an id of the menu's own, for the shell
|
|
261
|
+
// to pair the two across the change.
|
|
262
|
+
const id = trigger().getAttribute('glass-effect-id');
|
|
263
|
+
expect(id).toMatch(/^ui-menu-\d+$/);
|
|
264
|
+
expect(panel().getAttribute('glass-effect-id')).toBe(id);
|
|
265
|
+
expect(style(panel(), 'visibility')).toBe('hidden');
|
|
266
|
+
|
|
267
|
+
fireEvent.tap(trigger());
|
|
268
|
+
await waitForOpen(rendered);
|
|
269
|
+
expect(style(panel(), 'visibility')).toBe('');
|
|
270
|
+
expect(style(trigger(), 'visibility')).toBe('hidden');
|
|
271
|
+
|
|
272
|
+
fireEvent.tap(trigger());
|
|
273
|
+
await waitForClose(rendered);
|
|
274
|
+
expect(style(panel(), 'visibility')).toBe('hidden');
|
|
275
|
+
expect(style(trigger(), 'visibility')).toBe('');
|
|
276
|
+
|
|
277
|
+
// A trigger that goes takes its glass with it.
|
|
278
|
+
rendered.host.glassTrigger.set(false);
|
|
279
|
+
await waitForUpdate();
|
|
280
|
+
expect(trigger().tagName.toLowerCase()).toBe('view');
|
|
233
281
|
});
|
|
234
282
|
|
|
235
283
|
it('closes when an item is chosen, after telling it', async () => {
|
|
@@ -279,7 +327,7 @@ describe('UiMenu', () => {
|
|
|
279
327
|
fireEvent.tap(rendered.trigger());
|
|
280
328
|
await waitForOpen(rendered);
|
|
281
329
|
|
|
282
|
-
expect(style(rendered.trigger(), '
|
|
330
|
+
expect(style(rendered.trigger(), 'visibility')).toBe('');
|
|
283
331
|
// Below the trigger, a gap away.
|
|
284
332
|
expect(style(rendered.panel(), 'top')).toBe(`${80 + 44 + 8}px`);
|
|
285
333
|
});
|
|
@@ -321,6 +369,22 @@ describe('UiMenuItem', () => {
|
|
|
321
369
|
await waitForClose(rendered);
|
|
322
370
|
});
|
|
323
371
|
|
|
372
|
+
it('is checked by a bare checked attribute, and unset when bound to undefined', async () => {
|
|
373
|
+
const rendered = await renderOpenMenu();
|
|
374
|
+
const bold = row(rendered, 'Bold');
|
|
375
|
+
const pin = row(rendered, 'Pin');
|
|
376
|
+
|
|
377
|
+
expect(bold.getAttribute('accessibility-traits')).toBe('button,selected');
|
|
378
|
+
expect(bold.querySelector('.h-5.w-5 svg')).not.toBeNull();
|
|
379
|
+
// Unset, it's an item that's never checked: no checkmark column.
|
|
380
|
+
expect(pin.querySelector('.h-5.w-5')).toBeNull();
|
|
381
|
+
|
|
382
|
+
rendered.host.pinned.set(false);
|
|
383
|
+
await waitForUpdate();
|
|
384
|
+
expect(pin.querySelector('.h-5.w-5')).not.toBeNull();
|
|
385
|
+
expect(pin.querySelector('.h-5.w-5 svg')).toBeNull();
|
|
386
|
+
});
|
|
387
|
+
|
|
324
388
|
it('highlights while pressed, by touch or mouse', async () => {
|
|
325
389
|
const rendered = await renderOpenMenu();
|
|
326
390
|
const share = row(rendered, 'Share');
|
|
@@ -335,13 +399,43 @@ describe('UiMenuItem', () => {
|
|
|
335
399
|
]) {
|
|
336
400
|
fireLynxEvent(share, down!);
|
|
337
401
|
await waitForUpdate();
|
|
338
|
-
expect(style(highlight, 'opacity')).toBe('0.
|
|
402
|
+
expect(style(highlight, 'opacity')).toBe('0.08');
|
|
339
403
|
fireLynxEvent(share, up!);
|
|
340
404
|
await waitForUpdate();
|
|
341
405
|
expect(style(highlight, 'opacity')).toBe('0');
|
|
342
406
|
}
|
|
343
407
|
});
|
|
344
408
|
|
|
409
|
+
it('highlights a pressed row as its platform does', async () => {
|
|
410
|
+
const pressed = async (
|
|
411
|
+
platform: { platform: string; osVersion: string } | null,
|
|
412
|
+
) => {
|
|
413
|
+
const rendered = await renderOpenMenu(platform);
|
|
414
|
+
const share = row(rendered, 'Share');
|
|
415
|
+
fireLynxEvent(share, 'touchstart');
|
|
416
|
+
await waitForUpdate();
|
|
417
|
+
const highlight = share.firstElementChild!;
|
|
418
|
+
return {
|
|
419
|
+
classes: highlight.className.split(' '),
|
|
420
|
+
opacity: style(highlight, 'opacity'),
|
|
421
|
+
};
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// iOS 26's capsule, inset from the menu's sides.
|
|
425
|
+
const glass = await pressed(IOS_26);
|
|
426
|
+
expect(glass.classes).toEqual(
|
|
427
|
+
expect.arrayContaining(['left-[10px]', 'right-[10px]', 'rounded-full']),
|
|
428
|
+
);
|
|
429
|
+
expect(glass.opacity).toBe('0.08');
|
|
430
|
+
// Edge to edge before iOS 26, and as Material's state layer.
|
|
431
|
+
for (const platform of [IOS_18, null]) {
|
|
432
|
+
const { classes, opacity } = await pressed(platform);
|
|
433
|
+
expect(classes).toEqual(expect.arrayContaining(['left-0', 'right-0']));
|
|
434
|
+
expect(classes).not.toContain('rounded-full');
|
|
435
|
+
expect(opacity).toBe('0.1');
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
345
439
|
it("can't be pressed or chosen when disabled", async () => {
|
|
346
440
|
const rendered = await renderOpenMenu();
|
|
347
441
|
const archive = row(rendered, 'Archive, unavailable offline');
|