@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
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
import { Component, inject, signal } from '@angular/core';
|
|
2
|
+
import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
|
|
3
|
+
import {
|
|
4
|
+
fireEvent,
|
|
5
|
+
render,
|
|
6
|
+
waitForUpdate,
|
|
7
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
|
|
10
|
+
import { provideIconTheme } from '../icon/icon-theme';
|
|
11
|
+
import {
|
|
12
|
+
type SegmentedHost,
|
|
13
|
+
segmentPlace,
|
|
14
|
+
UI_SEGMENTED_HOST,
|
|
15
|
+
UiSegmentedControl,
|
|
16
|
+
UiSegmentedIndicator,
|
|
17
|
+
} from './segmented-control';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The control, as its segmented control reads it. Named, as a provider's
|
|
21
|
+
* factory in a component must be.
|
|
22
|
+
*/
|
|
23
|
+
const testHost = (): SegmentedHost => inject(SegmentedControlHost);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A segmented control of an app's own, built on UiSegmentedControl the way
|
|
27
|
+
* ui-tabs-list and ui-toggle-group are: a track with the indicator first,
|
|
28
|
+
* and segments that report their width and their touches.
|
|
29
|
+
*/
|
|
30
|
+
@Component({
|
|
31
|
+
selector: 'test-segmented-control',
|
|
32
|
+
imports: [LYNX_ELEMENTS, UiSegmentedIndicator],
|
|
33
|
+
providers: [
|
|
34
|
+
UiSegmentedControl,
|
|
35
|
+
{ provide: UI_SEGMENTED_HOST, useFactory: testHost },
|
|
36
|
+
],
|
|
37
|
+
template: `
|
|
38
|
+
<view [class]="control.trackClass()" (bindmouseleave)="control.cancel()">
|
|
39
|
+
@if (control.underlay()) {
|
|
40
|
+
<ui-segmented-indicator />
|
|
41
|
+
}
|
|
42
|
+
@for (value of values(); track value) {
|
|
43
|
+
<view
|
|
44
|
+
[class]="control.segmentHostClass"
|
|
45
|
+
(bindlayoutchange)="control.measure(value, $event)"
|
|
46
|
+
>
|
|
47
|
+
<view
|
|
48
|
+
[class]="
|
|
49
|
+
control.segmentClass(value, { first: $first, last: $last })
|
|
50
|
+
"
|
|
51
|
+
[style]="control.segmentStyle(value)"
|
|
52
|
+
(bindtouchstart)="control.press(value, $event)"
|
|
53
|
+
(bindtouchmove)="control.move($event)"
|
|
54
|
+
(bindtouchend)="control.release()"
|
|
55
|
+
(bindtouchcancel)="control.cancel()"
|
|
56
|
+
(bindmousedown)="control.press(value, $event)"
|
|
57
|
+
(bindmousemove)="control.move($event)"
|
|
58
|
+
(bindmouseup)="control.release()"
|
|
59
|
+
(bindmouseleave)="control.leave(value)"
|
|
60
|
+
(bindtap)="tap(value)"
|
|
61
|
+
>
|
|
62
|
+
@if (control.stateLayerClass(value); as stateLayer) {
|
|
63
|
+
<view [class]="stateLayer"></view>
|
|
64
|
+
}
|
|
65
|
+
<text [class]="control.labelClass(value)">{{ value }}</text>
|
|
66
|
+
</view>
|
|
67
|
+
</view>
|
|
68
|
+
}
|
|
69
|
+
</view>
|
|
70
|
+
`,
|
|
71
|
+
})
|
|
72
|
+
class SegmentedControlHost implements SegmentedHost {
|
|
73
|
+
readonly control = inject(UiSegmentedControl);
|
|
74
|
+
|
|
75
|
+
readonly values = signal(['Day', 'Week', 'Month']);
|
|
76
|
+
readonly selected = signal<string[]>(['Week']);
|
|
77
|
+
readonly isSegmented = signal(true);
|
|
78
|
+
readonly isMultiple = signal(false);
|
|
79
|
+
readonly isDisabled = signal(false);
|
|
80
|
+
/** What the control asked to select, in order. */
|
|
81
|
+
readonly selections: string[] = [];
|
|
82
|
+
|
|
83
|
+
readonly segmented = this.isSegmented.asReadonly();
|
|
84
|
+
readonly segmentValues = this.values.asReadonly();
|
|
85
|
+
readonly selectedValues = this.selected.asReadonly();
|
|
86
|
+
readonly multiple = this.isMultiple.asReadonly();
|
|
87
|
+
readonly disabled = this.isDisabled.asReadonly();
|
|
88
|
+
|
|
89
|
+
select(value: string): void {
|
|
90
|
+
this.selections.push(value);
|
|
91
|
+
this.selected.set([value]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
tap(value: string): void {
|
|
95
|
+
if (!this.control.tap(value)) return;
|
|
96
|
+
this.selected.update((selected) =>
|
|
97
|
+
this.isMultiple()
|
|
98
|
+
? selected.includes(value)
|
|
99
|
+
? selected.filter((other) => other !== value)
|
|
100
|
+
: [...selected, value]
|
|
101
|
+
: [value],
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Accessibility settings a test can change, as the host app would. */
|
|
107
|
+
const settings = {
|
|
108
|
+
reduceMotion: signal(false),
|
|
109
|
+
reduceTransparency: signal(false),
|
|
110
|
+
increaseContrast: signal(false),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const IOS_26 = { platform: 'iOS', osVersion: '26.0' };
|
|
114
|
+
const IOS_18 = { platform: 'iOS', osVersion: '18.6' };
|
|
115
|
+
const ANDROID = { platform: 'Android', osVersion: '15' };
|
|
116
|
+
|
|
117
|
+
/** Each segment's width, in px, as Lynx lays the 300px track out. */
|
|
118
|
+
const WIDTHS: Readonly<Record<string, number>> = {
|
|
119
|
+
Day: 100,
|
|
120
|
+
Week: 100,
|
|
121
|
+
Month: 100,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Waits `ms` of real time, then lets the update it led to apply.
|
|
126
|
+
*/
|
|
127
|
+
const after = async (ms: number) => {
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
129
|
+
await waitForUpdate();
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Long enough for the lens to settle after a tap (the slide, 350ms, then
|
|
134
|
+
* its shrink, 150ms). Chained timers can each fire a clock tick late on
|
|
135
|
+
* Windows, so it leaves room.
|
|
136
|
+
*/
|
|
137
|
+
const SETTLED = 700;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Renders the control on a platform (none gives Material), in a light or a
|
|
141
|
+
* dark theme.
|
|
142
|
+
*/
|
|
143
|
+
const renderControl = async (
|
|
144
|
+
platform?: { platform: string; osVersion: string },
|
|
145
|
+
scheme: 'light' | 'dark' = 'light',
|
|
146
|
+
) => {
|
|
147
|
+
vi.stubGlobal('SystemInfo', platform);
|
|
148
|
+
const result = await render(SegmentedControlHost, {
|
|
149
|
+
providers: [
|
|
150
|
+
{ provide: LynxAccessibilitySettings, useValue: settings },
|
|
151
|
+
provideIconTheme({ colorScheme: scheme }),
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
const host = result.componentRef.instance as SegmentedControlHost;
|
|
155
|
+
/**
|
|
156
|
+
* A segment's own view, which takes its touches.
|
|
157
|
+
*/
|
|
158
|
+
const segment = (label: string) => result.getByText(label).parentElement!;
|
|
159
|
+
/**
|
|
160
|
+
* A segment's slot in the track, which reports its width.
|
|
161
|
+
*/
|
|
162
|
+
const slot = (label: string) => segment(label).parentElement!;
|
|
163
|
+
const track = () => slot('Day').parentElement!;
|
|
164
|
+
/**
|
|
165
|
+
* What the track draws under the segments, if anything.
|
|
166
|
+
*/
|
|
167
|
+
const underlay = () => {
|
|
168
|
+
const first = track().firstElementChild!;
|
|
169
|
+
return first.classList.contains('h-full') ? first : null;
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* The indicator's frame, which moves between the segments.
|
|
173
|
+
*/
|
|
174
|
+
const frame = () =>
|
|
175
|
+
underlay()!.querySelector<HTMLElement>(
|
|
176
|
+
'.bottom-0.top-0:not(.w-\\[1px\\])',
|
|
177
|
+
)!;
|
|
178
|
+
/**
|
|
179
|
+
* The opaque thumb in the frame, beside the lens. (Angular's `@if` leaves
|
|
180
|
+
* a hidden anchor view there too.)
|
|
181
|
+
*/
|
|
182
|
+
const thumb = () =>
|
|
183
|
+
Array.from(frame().children).find(
|
|
184
|
+
(child) =>
|
|
185
|
+
child.classList.contains('absolute') && child.localName !== 'blur-view',
|
|
186
|
+
) as HTMLElement;
|
|
187
|
+
/**
|
|
188
|
+
* The glass lens, while it's drawn.
|
|
189
|
+
*/
|
|
190
|
+
const lens = () => frame().querySelector<HTMLElement>('blur-view');
|
|
191
|
+
const separators = () =>
|
|
192
|
+
Array.from(underlay()!.querySelectorAll<HTMLElement>('.w-\\[1px\\]'));
|
|
193
|
+
/**
|
|
194
|
+
* Reports every segment's width, as Lynx does after laying them out.
|
|
195
|
+
*/
|
|
196
|
+
const measure = async (widths: Record<string, number> = WIDTHS) => {
|
|
197
|
+
for (const [label, width] of Object.entries(widths)) {
|
|
198
|
+
fireEvent.layoutchange(slot(label), { detail: { width, height: 32 } });
|
|
199
|
+
}
|
|
200
|
+
await waitForUpdate();
|
|
201
|
+
// The indicator is placed at once, and slides from the next change on.
|
|
202
|
+
await after(0);
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* A touch event's fields, with the finger at `pageX` if given.
|
|
206
|
+
*/
|
|
207
|
+
const touch = (pageX?: number) =>
|
|
208
|
+
pageX === undefined ? {} : { touches: [{ pageX }] };
|
|
209
|
+
return {
|
|
210
|
+
...result,
|
|
211
|
+
host,
|
|
212
|
+
segment,
|
|
213
|
+
slot,
|
|
214
|
+
track,
|
|
215
|
+
underlay,
|
|
216
|
+
frame,
|
|
217
|
+
thumb,
|
|
218
|
+
lens,
|
|
219
|
+
separators,
|
|
220
|
+
measure,
|
|
221
|
+
press: async (label: string, pageX?: number) => {
|
|
222
|
+
fireEvent.touchstart(segment(label), touch(pageX));
|
|
223
|
+
await waitForUpdate();
|
|
224
|
+
},
|
|
225
|
+
move: async (label: string, pageX?: number) => {
|
|
226
|
+
fireEvent.touchmove(segment(label), touch(pageX));
|
|
227
|
+
await waitForUpdate();
|
|
228
|
+
},
|
|
229
|
+
release: async (label: string) => {
|
|
230
|
+
fireEvent.touchend(segment(label));
|
|
231
|
+
await waitForUpdate();
|
|
232
|
+
},
|
|
233
|
+
tap: async (label: string) => {
|
|
234
|
+
fireEvent.tap(segment(label));
|
|
235
|
+
await waitForUpdate();
|
|
236
|
+
},
|
|
237
|
+
update: async (change: () => void) => {
|
|
238
|
+
change();
|
|
239
|
+
await waitForUpdate();
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
beforeEach(() => {
|
|
245
|
+
// The indicator's selector isn't a Lynx element, so the renderer falls
|
|
246
|
+
// back to a <view> for it and warns.
|
|
247
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
afterEach(() => {
|
|
251
|
+
vi.restoreAllMocks();
|
|
252
|
+
vi.unstubAllGlobals();
|
|
253
|
+
settings.reduceMotion.set(false);
|
|
254
|
+
settings.reduceTransparency.set(false);
|
|
255
|
+
settings.increaseContrast.set(false);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('UiSegmentedControl', () => {
|
|
259
|
+
describe('the track', () => {
|
|
260
|
+
it("is iOS 26's capsule, iOS 13's rounded track before, and Material's group on Android", async () => {
|
|
261
|
+
const glass = await renderControl(IOS_26);
|
|
262
|
+
expect(glass.track().classList).toContain('rounded-full');
|
|
263
|
+
expect(glass.track().classList).toContain('bg-muted');
|
|
264
|
+
expect(glass.track().classList).toContain('h-[32px]');
|
|
265
|
+
glass.unmount();
|
|
266
|
+
|
|
267
|
+
const frosted = await renderControl(IOS_18);
|
|
268
|
+
expect(frosted.track().classList).toContain('rounded-[9px]');
|
|
269
|
+
expect(frosted.track().classList).toContain('bg-muted');
|
|
270
|
+
frosted.unmount();
|
|
271
|
+
|
|
272
|
+
const material = await renderControl(ANDROID);
|
|
273
|
+
expect(material.track().classList).toContain('gap-[2px]');
|
|
274
|
+
expect(material.track().classList).toContain('h-[40px]');
|
|
275
|
+
expect(material.track().classList).not.toContain('bg-muted');
|
|
276
|
+
// Material's segments fill themselves: nothing under them.
|
|
277
|
+
expect(material.underlay()).toBeNull();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('dims a disabled control, which ignores touches', async () => {
|
|
281
|
+
const { host, track, press, tap, update, measure } =
|
|
282
|
+
await renderControl(IOS_26);
|
|
283
|
+
await measure();
|
|
284
|
+
await update(() => host.isDisabled.set(true));
|
|
285
|
+
expect(track().classList).toContain('opacity-50');
|
|
286
|
+
|
|
287
|
+
await press('Week', 150);
|
|
288
|
+
await tap('Month');
|
|
289
|
+
expect(host.selected()).toEqual(['Week']);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('the indicator', () => {
|
|
294
|
+
it('takes an equal share of the track until the segments are measured', async () => {
|
|
295
|
+
const { frame } = await renderControl(IOS_26);
|
|
296
|
+
expect(frame().style.left).toBe(`${100 / 3}%`);
|
|
297
|
+
expect(frame().style.width).toBe(`${100 / 3}%`);
|
|
298
|
+
expect(frame().style.opacity).toBe('1');
|
|
299
|
+
expect(frame().style.transition).toBe('none');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('sits on the measured selected segment, placed without sliding', async () => {
|
|
303
|
+
const { frame, measure } = await renderControl(IOS_26);
|
|
304
|
+
await measure({ Day: 80, Week: 120, Month: 100 });
|
|
305
|
+
expect(frame().style.left).toBe('80px');
|
|
306
|
+
expect(frame().style.width).toBe('120px');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('slides to a new selection once placed, unless Reduce Motion is on', async () => {
|
|
310
|
+
const { host, frame, measure, update } = await renderControl(IOS_26);
|
|
311
|
+
await measure();
|
|
312
|
+
|
|
313
|
+
await update(() => host.selected.set(['Month']));
|
|
314
|
+
expect(frame().style.left).toBe('200px');
|
|
315
|
+
expect(frame().style.transition).toContain('left 350ms');
|
|
316
|
+
|
|
317
|
+
await update(() => settings.reduceMotion.set(true));
|
|
318
|
+
await update(() => host.selected.set(['Day']));
|
|
319
|
+
expect(frame().style.left).toBe('0px');
|
|
320
|
+
expect(frame().style.transition).toBe('none');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('slides on the gentler curve of the design before iOS 26', async () => {
|
|
324
|
+
const { host, frame, measure, update } = await renderControl(IOS_18);
|
|
325
|
+
await measure();
|
|
326
|
+
await update(() => host.selected.set(['Month']));
|
|
327
|
+
expect(frame().style.transition).toContain('left 250ms');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('fades out while nothing is selected', async () => {
|
|
331
|
+
const { host, frame, measure, update } = await renderControl(IOS_26);
|
|
332
|
+
await measure();
|
|
333
|
+
await update(() => host.selected.set([]));
|
|
334
|
+
expect(frame().style.opacity).toBe('0');
|
|
335
|
+
// It stays where an equal share would put the first segment.
|
|
336
|
+
expect(frame().style.left).toBe('0%');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('takes a width only from a layoutchange that reports one, and only when it changed', async () => {
|
|
340
|
+
const { frame, slot, measure } = await renderControl(IOS_26);
|
|
341
|
+
// Older Android puts the size in `params`.
|
|
342
|
+
fireLynxEvent(slot('Day'), 'layoutchange', { params: { width: 90 } });
|
|
343
|
+
fireLynxEvent(slot('Week'), 'layoutchange', { detail: {} });
|
|
344
|
+
await waitForUpdate();
|
|
345
|
+
expect(frame().style.width).toBe(`${100 / 3}%`);
|
|
346
|
+
|
|
347
|
+
await measure({ Day: 90, Week: 110, Month: 100 });
|
|
348
|
+
expect(frame().style.left).toBe('90px');
|
|
349
|
+
// The same width again changes nothing.
|
|
350
|
+
fireEvent.layoutchange(slot('Week'), { detail: { width: 110 } });
|
|
351
|
+
await waitForUpdate();
|
|
352
|
+
expect(frame().style.width).toBe('110px');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('follows a segment that grows, as after a rotation', async () => {
|
|
356
|
+
const { frame, measure } = await renderControl(IOS_26);
|
|
357
|
+
await measure();
|
|
358
|
+
await measure({ Day: 150, Week: 150, Month: 150 });
|
|
359
|
+
expect(frame().style.left).toBe('150px');
|
|
360
|
+
expect(frame().style.width).toBe('150px');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("draws iOS 26's thumb as a white capsule, and iOS 13's with round corners and a drop shadow", async () => {
|
|
364
|
+
const glass = await renderControl(IOS_26);
|
|
365
|
+
expect(glass.thumb().classList).toContain('rounded-full');
|
|
366
|
+
expect(glass.thumb().classList).toContain('bg-background');
|
|
367
|
+
expect(glass.thumb().style.left).toBe('2px');
|
|
368
|
+
expect(glass.thumb().style.getPropertyValue('box-shadow')).toBe(
|
|
369
|
+
'0 1px 2px rgba(0, 0, 0, 0.1)',
|
|
370
|
+
);
|
|
371
|
+
glass.unmount();
|
|
372
|
+
|
|
373
|
+
const frosted = await renderControl(IOS_18);
|
|
374
|
+
expect(frosted.thumb().classList).toContain('rounded-[7px]');
|
|
375
|
+
expect(frosted.thumb().style.getPropertyValue('box-shadow')).toContain(
|
|
376
|
+
'0 3px 8px',
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("draws a dark theme's thumb in UIKit's gray", async () => {
|
|
381
|
+
const { thumb } = await renderControl(IOS_26, 'dark');
|
|
382
|
+
expect(thumb().classList).not.toContain('bg-background');
|
|
383
|
+
expect(thumb().style.getPropertyValue('background-color')).toBe(
|
|
384
|
+
'rgb(90, 90, 95)',
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('gives the thumb an edge under Increase Contrast', async () => {
|
|
389
|
+
const { thumb, update } = await renderControl(IOS_26);
|
|
390
|
+
await update(() => settings.increaseContrast.set(true));
|
|
391
|
+
expect(thumb().classList).toContain('border-foreground');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('is hidden from screen readers, which read the segments', async () => {
|
|
395
|
+
const { underlay } = await renderControl(IOS_26);
|
|
396
|
+
expect(underlay()!.getAttribute('accessibility-elements-hidden')).toBe(
|
|
397
|
+
'true',
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe('the glass lens', () => {
|
|
403
|
+
it('lifts the thumb into glass as a tap moves it, and settles back', async () => {
|
|
404
|
+
const { frame, thumb, lens, measure, segment, host, componentRef } =
|
|
405
|
+
await renderControl(IOS_26);
|
|
406
|
+
await measure();
|
|
407
|
+
expect(lens()).toBeNull();
|
|
408
|
+
|
|
409
|
+
fireEvent.tap(segment('Month'));
|
|
410
|
+
// Checked before the next tick, when the lens opens.
|
|
411
|
+
componentRef.changeDetectorRef.detectChanges();
|
|
412
|
+
expect(host.selected()).toEqual(['Month']);
|
|
413
|
+
// Drawn at the thumb's size under the thumb first, so it grows from
|
|
414
|
+
// there.
|
|
415
|
+
expect(lens()!.style.left).toBe('2px');
|
|
416
|
+
expect(thumb().style.opacity).toBe('1');
|
|
417
|
+
|
|
418
|
+
await after(0);
|
|
419
|
+
expect(lens()!.style.left).toBe('-10px');
|
|
420
|
+
expect(lens()!.style.top).toBe('-3px');
|
|
421
|
+
expect(thumb().style.opacity).toBe('0');
|
|
422
|
+
expect(frame().style.left).toBe('200px');
|
|
423
|
+
|
|
424
|
+
await after(SETTLED);
|
|
425
|
+
expect(lens()).toBeNull();
|
|
426
|
+
expect(thumb().style.opacity).toBe('1');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("doesn't lift for a tap on the selected segment", async () => {
|
|
430
|
+
const { lens, measure, tap } = await renderControl(IOS_26);
|
|
431
|
+
await measure();
|
|
432
|
+
await tap('Week');
|
|
433
|
+
expect(lens()).toBeNull();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("doesn't lift under Reduce Motion or Reduce Transparency, or before iOS 26", async () => {
|
|
437
|
+
const glass = await renderControl(IOS_26);
|
|
438
|
+
await glass.measure();
|
|
439
|
+
await glass.update(() => settings.reduceMotion.set(true));
|
|
440
|
+
await glass.tap('Month');
|
|
441
|
+
expect(glass.lens()).toBeNull();
|
|
442
|
+
// The thumb fades and shrinks at once, too.
|
|
443
|
+
expect(glass.thumb().style.transition).toBe('none');
|
|
444
|
+
|
|
445
|
+
await glass.update(() => {
|
|
446
|
+
settings.reduceMotion.set(false);
|
|
447
|
+
settings.reduceTransparency.set(true);
|
|
448
|
+
});
|
|
449
|
+
await glass.tap('Day');
|
|
450
|
+
expect(glass.lens()).toBeNull();
|
|
451
|
+
glass.unmount();
|
|
452
|
+
|
|
453
|
+
const frosted = await renderControl(IOS_18);
|
|
454
|
+
await frosted.measure();
|
|
455
|
+
await frosted.tap('Month');
|
|
456
|
+
expect(frosted.lens()).toBeNull();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('grows the lens again if it moves again before settling', async () => {
|
|
460
|
+
const { lens, measure, tap } = await renderControl(IOS_26);
|
|
461
|
+
await measure();
|
|
462
|
+
await tap('Month');
|
|
463
|
+
await after(0);
|
|
464
|
+
// The slide is over and the lens is shrinking back under the thumb.
|
|
465
|
+
await after(400);
|
|
466
|
+
expect(lens()!.style.left).toBe('2px');
|
|
467
|
+
|
|
468
|
+
await tap('Day');
|
|
469
|
+
expect(lens()!.style.left).toBe('-10px');
|
|
470
|
+
await after(SETTLED);
|
|
471
|
+
expect(lens()).toBeNull();
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
describe('dragging', () => {
|
|
476
|
+
it('lifts the selected thumb under the finger, follows it, and selects where it lets go', async () => {
|
|
477
|
+
const {
|
|
478
|
+
host,
|
|
479
|
+
frame,
|
|
480
|
+
lens,
|
|
481
|
+
segment,
|
|
482
|
+
measure,
|
|
483
|
+
press,
|
|
484
|
+
move,
|
|
485
|
+
release,
|
|
486
|
+
tap,
|
|
487
|
+
getByText,
|
|
488
|
+
} = await renderControl(IOS_26);
|
|
489
|
+
await measure();
|
|
490
|
+
|
|
491
|
+
await press('Week', 150);
|
|
492
|
+
await after(0);
|
|
493
|
+
expect(lens()!.style.left).toBe('-10px');
|
|
494
|
+
|
|
495
|
+
await move('Week', 230);
|
|
496
|
+
// It follows the finger at once, kept its width.
|
|
497
|
+
expect(frame().style.left).toBe('180px');
|
|
498
|
+
expect(frame().style.width).toBe('100px');
|
|
499
|
+
expect(frame().style.transition).toBe('none');
|
|
500
|
+
// The label under it reads as selected as it passes.
|
|
501
|
+
expect(getByText('Month').classList).toContain('font-medium');
|
|
502
|
+
expect(getByText('Week').classList).not.toContain('font-medium');
|
|
503
|
+
// The segment it left isn't dimmed as pressed.
|
|
504
|
+
expect(segment('Week').classList).not.toContain('opacity-60');
|
|
505
|
+
|
|
506
|
+
await release('Week');
|
|
507
|
+
expect(host.selections).toEqual(['Month']);
|
|
508
|
+
expect(frame().style.left).toBe('200px');
|
|
509
|
+
expect(frame().style.transition).toContain('left 350ms');
|
|
510
|
+
|
|
511
|
+
// The tap Lynx may send as the finger lifts doesn't reselect Week.
|
|
512
|
+
await tap('Week');
|
|
513
|
+
expect(host.selected()).toEqual(['Month']);
|
|
514
|
+
|
|
515
|
+
await after(SETTLED);
|
|
516
|
+
expect(lens()).toBeNull();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('stays within the track', async () => {
|
|
520
|
+
const { frame, measure, press, move } = await renderControl(IOS_26);
|
|
521
|
+
await measure();
|
|
522
|
+
await press('Week', 150);
|
|
523
|
+
await move('Week', -500);
|
|
524
|
+
expect(frame().style.left).toBe('0px');
|
|
525
|
+
await move('Week', 900);
|
|
526
|
+
expect(frame().style.left).toBe('200px');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('leaves a press that barely moved to the tap, and the drag back where it started selects nothing', async () => {
|
|
530
|
+
const { host, frame, measure, press, move, release, tap } =
|
|
531
|
+
await renderControl(IOS_26);
|
|
532
|
+
await measure();
|
|
533
|
+
|
|
534
|
+
await press('Week', 150);
|
|
535
|
+
await move('Week', 153);
|
|
536
|
+
await release('Week');
|
|
537
|
+
expect(host.selections).toEqual([]);
|
|
538
|
+
expect(frame().style.left).toBe('100px');
|
|
539
|
+
// The tap then goes through, on the segment that's selected already.
|
|
540
|
+
await tap('Week');
|
|
541
|
+
expect(host.selected()).toEqual(['Week']);
|
|
542
|
+
|
|
543
|
+
await press('Week', 150);
|
|
544
|
+
await move('Week', 180);
|
|
545
|
+
await release('Week');
|
|
546
|
+
expect(host.selections).toEqual([]);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('goes back to the selected segment when the touch is taken away, or the mouse leaves', async () => {
|
|
550
|
+
const { host, frame, track, segment, measure, press, move } =
|
|
551
|
+
await renderControl(IOS_26);
|
|
552
|
+
await measure();
|
|
553
|
+
|
|
554
|
+
await press('Week', 150);
|
|
555
|
+
await move('Week', 260);
|
|
556
|
+
fireEvent.touchcancel(segment('Week'));
|
|
557
|
+
await waitForUpdate();
|
|
558
|
+
expect(frame().style.left).toBe('100px');
|
|
559
|
+
expect(host.selections).toEqual([]);
|
|
560
|
+
|
|
561
|
+
// In the web preview, with the mouse.
|
|
562
|
+
fireEvent.mousedown(segment('Week'), { pageX: 150 });
|
|
563
|
+
fireEvent.mousemove(segment('Month'), { pageX: 260 });
|
|
564
|
+
await waitForUpdate();
|
|
565
|
+
expect(frame().style.left).toBe('200px');
|
|
566
|
+
// Leaving one segment for another doesn't end the drag.
|
|
567
|
+
fireLynxEvent(segment('Week'), 'mouseleave');
|
|
568
|
+
await waitForUpdate();
|
|
569
|
+
expect(frame().style.left).toBe('200px');
|
|
570
|
+
fireLynxEvent(track(), 'mouseleave');
|
|
571
|
+
await waitForUpdate();
|
|
572
|
+
expect(frame().style.left).toBe('100px');
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('lets go of a pressed segment that the pointer leaves, or whose touch is taken away', async () => {
|
|
576
|
+
const { segment, measure, press } = await renderControl(IOS_26);
|
|
577
|
+
await measure();
|
|
578
|
+
await press('Day', 50);
|
|
579
|
+
// Leaving another segment leaves Day pressed.
|
|
580
|
+
fireLynxEvent(segment('Month'), 'mouseleave');
|
|
581
|
+
await waitForUpdate();
|
|
582
|
+
expect(segment('Day').classList).toContain('opacity-60');
|
|
583
|
+
fireLynxEvent(segment('Day'), 'mouseleave');
|
|
584
|
+
await waitForUpdate();
|
|
585
|
+
expect(segment('Day').classList).not.toContain('opacity-60');
|
|
586
|
+
|
|
587
|
+
await press('Day', 50);
|
|
588
|
+
fireEvent.touchcancel(segment('Day'));
|
|
589
|
+
await waitForUpdate();
|
|
590
|
+
expect(segment('Day').classList).not.toContain('opacity-60');
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('selects with the mouse in the web preview', async () => {
|
|
594
|
+
const { host, segment, measure } = await renderControl(IOS_26);
|
|
595
|
+
await measure();
|
|
596
|
+
fireEvent.mousedown(segment('Week'), { pageX: 150 });
|
|
597
|
+
fireEvent.mousemove(segment('Day'), { pageX: 40 });
|
|
598
|
+
fireEvent.mouseup(segment('Day'));
|
|
599
|
+
await waitForUpdate();
|
|
600
|
+
expect(host.selections).toEqual(['Day']);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it("doesn't take hold of an unselected segment, or one that can't be measured or placed", async () => {
|
|
604
|
+
const { host, frame, segment, measure, press, move, release, update } =
|
|
605
|
+
await renderControl(IOS_26);
|
|
606
|
+
|
|
607
|
+
// Not measured yet.
|
|
608
|
+
await press('Week', 150);
|
|
609
|
+
await move('Week', 260);
|
|
610
|
+
await release('Week');
|
|
611
|
+
expect(host.selections).toEqual([]);
|
|
612
|
+
|
|
613
|
+
await measure();
|
|
614
|
+
// A press with no position, as a synthetic event might be.
|
|
615
|
+
await press('Week');
|
|
616
|
+
await move('Week', 260);
|
|
617
|
+
expect(frame().style.left).toBe('100px');
|
|
618
|
+
await release('Week');
|
|
619
|
+
|
|
620
|
+
// An unselected segment dims while pressed instead.
|
|
621
|
+
await press('Day', 50);
|
|
622
|
+
expect(segment('Day').classList).toContain('opacity-60');
|
|
623
|
+
await move('Day', 260);
|
|
624
|
+
expect(frame().style.left).toBe('100px');
|
|
625
|
+
await release('Day');
|
|
626
|
+
expect(segment('Day').classList).not.toContain('opacity-60');
|
|
627
|
+
|
|
628
|
+
// A move with no position is ignored.
|
|
629
|
+
await press('Week', 150);
|
|
630
|
+
await move('Week');
|
|
631
|
+
expect(frame().style.left).toBe('100px');
|
|
632
|
+
await release('Week');
|
|
633
|
+
|
|
634
|
+
await update(() => host.isMultiple.set(true));
|
|
635
|
+
await press('Week', 150);
|
|
636
|
+
await move('Week', 260);
|
|
637
|
+
await release('Week');
|
|
638
|
+
expect(host.selections).toEqual([]);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('lets go of a drag whose segments change under it', async () => {
|
|
642
|
+
const { host, frame, measure, press, move, release, update } =
|
|
643
|
+
await renderControl(IOS_26);
|
|
644
|
+
await measure();
|
|
645
|
+
|
|
646
|
+
// A segment that hasn't been measured joins: there's no layout.
|
|
647
|
+
await press('Week', 150);
|
|
648
|
+
await update(() => host.values.set(['Day', 'Week', 'Month', 'Year']));
|
|
649
|
+
await move('Week', 260);
|
|
650
|
+
await release('Week');
|
|
651
|
+
expect(host.selections).toEqual([]);
|
|
652
|
+
|
|
653
|
+
// The dragged segment's place is gone.
|
|
654
|
+
await update(() => host.values.set(['Day', 'Week', 'Month']));
|
|
655
|
+
await update(() => host.selected.set(['Month']));
|
|
656
|
+
await press('Month', 250);
|
|
657
|
+
await update(() => host.values.set(['Day']));
|
|
658
|
+
await move('Day', 50);
|
|
659
|
+
await release('Day');
|
|
660
|
+
expect(host.selections).toEqual([]);
|
|
661
|
+
expect(frame()).not.toBeNull();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it("shrinks iOS 13's thumb while it's held", async () => {
|
|
665
|
+
const { thumb, measure, press, release } = await renderControl(IOS_18);
|
|
666
|
+
await measure();
|
|
667
|
+
await press('Week', 150);
|
|
668
|
+
expect(thumb().style.transform).toBe('scale(0.95)');
|
|
669
|
+
await release('Week');
|
|
670
|
+
expect(thumb().style.transform).toBe('scale(1)');
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
describe('segments', () => {
|
|
675
|
+
it('share the track, positioned so the web paints them over the indicator', async () => {
|
|
676
|
+
const { slot } = await renderControl(IOS_26);
|
|
677
|
+
expect(slot('Day').classList).toContain('flex-1');
|
|
678
|
+
expect(slot('Day').classList).toContain('relative');
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('label as UIKit does: medium when selected on iOS 26, semibold before', async () => {
|
|
682
|
+
const glass = await renderControl(IOS_26);
|
|
683
|
+
const label = (label: string) => glass.getByText(label);
|
|
684
|
+
expect(label('Week').classList).toContain('font-medium');
|
|
685
|
+
expect(label('Week').classList).toContain('text-[13px]');
|
|
686
|
+
expect(label('Day').classList).not.toContain('font-medium');
|
|
687
|
+
glass.unmount();
|
|
688
|
+
|
|
689
|
+
const frosted = await renderControl(IOS_18);
|
|
690
|
+
expect(frosted.getByText('Week').classList).toContain('font-semibold');
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it('separate unselected neighbors with a hairline before iOS 26, measured first', async () => {
|
|
694
|
+
const { host, separators, measure, update } = await renderControl(IOS_18);
|
|
695
|
+
expect(separators()).toEqual([]);
|
|
696
|
+
|
|
697
|
+
await measure();
|
|
698
|
+
// Between Day and Week, and between Week and Month: both beside Week.
|
|
699
|
+
expect(separators().map((line) => line.style.left)).toEqual([
|
|
700
|
+
'99.5px',
|
|
701
|
+
'199.5px',
|
|
702
|
+
]);
|
|
703
|
+
expect(separators().map((line) => line.style.opacity)).toEqual([
|
|
704
|
+
'0',
|
|
705
|
+
'0',
|
|
706
|
+
]);
|
|
707
|
+
|
|
708
|
+
await update(() => host.selected.set(['Day']));
|
|
709
|
+
expect(separators().map((line) => line.style.opacity)).toEqual([
|
|
710
|
+
'0',
|
|
711
|
+
'0.3',
|
|
712
|
+
]);
|
|
713
|
+
expect(separators()[1]!.style.transition).toContain('opacity');
|
|
714
|
+
|
|
715
|
+
await update(() => settings.reduceMotion.set(true));
|
|
716
|
+
expect(separators()[1]!.style.transition).toBe('none');
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('draw no separators on iOS 26', async () => {
|
|
720
|
+
const { separators, measure } = await renderControl(IOS_26);
|
|
721
|
+
await measure();
|
|
722
|
+
expect(separators()).toEqual([]);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("are Material's connected buttons: the selected one filled and round", async () => {
|
|
726
|
+
const { host, segment, update, getByText } = await renderControl(ANDROID);
|
|
727
|
+
expect(segment('Week').classList).toContain('bg-primary');
|
|
728
|
+
expect(segment('Week').classList).toContain('rounded-full');
|
|
729
|
+
// Text written `text-foreground` takes the fill's content color.
|
|
730
|
+
expect(segment('Week').classList).toContain(
|
|
731
|
+
'[--foreground:var(--primary-foreground)]',
|
|
732
|
+
);
|
|
733
|
+
expect(getByText('Week').classList).toContain('text-primary-foreground');
|
|
734
|
+
expect(segment('Day').classList).toContain('bg-secondary');
|
|
735
|
+
expect(getByText('Day').classList).toContain('text-secondary-foreground');
|
|
736
|
+
expect(segment('Day').style.transition).toContain('background-color');
|
|
737
|
+
|
|
738
|
+
await update(() => settings.reduceMotion.set(true));
|
|
739
|
+
expect(segment('Day').style.transition).toBe('none');
|
|
740
|
+
|
|
741
|
+
await update(() => settings.increaseContrast.set(true));
|
|
742
|
+
expect(segment('Day').classList).toContain('border-secondary-foreground');
|
|
743
|
+
expect(segment('Week').classList).toContain('border-primary');
|
|
744
|
+
|
|
745
|
+
// They don't dim while pressed: a state layer shows instead.
|
|
746
|
+
fireEvent.touchstart(segment('Day'), { touches: [{ pageX: 50 }] });
|
|
747
|
+
await waitForUpdate();
|
|
748
|
+
expect(segment('Day').classList).not.toContain('opacity-60');
|
|
749
|
+
expect(segment('Day').querySelector('.opacity-10')).not.toBeNull();
|
|
750
|
+
fireEvent.touchend(segment('Day'));
|
|
751
|
+
await waitForUpdate();
|
|
752
|
+
expect(segment('Day').querySelector('.opacity-10')).toBeNull();
|
|
753
|
+
|
|
754
|
+
// Nothing slides on Material, so a tap just selects.
|
|
755
|
+
await update(() => host.selected.set(['Month']));
|
|
756
|
+
expect(segment('Month').classList).toContain('bg-primary');
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it("round the group's ends on Material's end segments, so Increase Contrast's outline follows them", async () => {
|
|
760
|
+
const { host, segment, track, update } = await renderControl(ANDROID);
|
|
761
|
+
await update(() => settings.increaseContrast.set(true));
|
|
762
|
+
expect(segment('Day').classList).toContain('rounded-l-[20px]');
|
|
763
|
+
expect(segment('Day').classList).toContain('rounded-r-[8px]');
|
|
764
|
+
expect(segment('Day').classList).toContain('border-secondary-foreground');
|
|
765
|
+
expect(segment('Month').classList).toContain('rounded-l-[8px]');
|
|
766
|
+
expect(segment('Month').classList).toContain('rounded-r-[20px]');
|
|
767
|
+
// The track doesn't clip the ends, which would cut their outlines.
|
|
768
|
+
expect(track().classList).not.toContain('rounded-full');
|
|
769
|
+
expect(track().classList).not.toContain('overflow-hidden');
|
|
770
|
+
|
|
771
|
+
// An unselected segment between them has small corners all round, and
|
|
772
|
+
// the selected one is round wherever it is.
|
|
773
|
+
await update(() => host.selected.set(['Day']));
|
|
774
|
+
expect(segment('Week').classList).toContain('rounded-[8px]');
|
|
775
|
+
expect(segment('Day').classList).toContain('rounded-full');
|
|
776
|
+
expect(segment('Day').classList).not.toContain('rounded-l-[20px]');
|
|
777
|
+
|
|
778
|
+
// A lone segment is round at both ends.
|
|
779
|
+
await update(() => host.values.set(['Week']));
|
|
780
|
+
expect(segment('Week').classList).toContain('rounded-full');
|
|
781
|
+
expect(segment('Week').classList).toContain('bg-secondary');
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it("give icons Material's content color, and leave them alone elsewhere", async () => {
|
|
785
|
+
const material = await renderControl(ANDROID);
|
|
786
|
+
const control = material.host.control;
|
|
787
|
+
expect(control.iconColor('Week')).toBe('var(--primary-foreground)');
|
|
788
|
+
expect(control.iconColor('Day')).toBe('var(--secondary-foreground)');
|
|
789
|
+
material.unmount();
|
|
790
|
+
|
|
791
|
+
const glass = await renderControl(IOS_26);
|
|
792
|
+
expect(glass.host.control.iconColor('Week')).toBeUndefined();
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it('in a multiple selection, each draw their own thumb, and nothing slides', async () => {
|
|
796
|
+
const { host, segment, underlay, update, measure, separators, tap } =
|
|
797
|
+
await renderControl(IOS_18);
|
|
798
|
+
await update(() => {
|
|
799
|
+
host.isMultiple.set(true);
|
|
800
|
+
host.selected.set(['Day', 'Month']);
|
|
801
|
+
});
|
|
802
|
+
await measure();
|
|
803
|
+
expect(host.control.slides()).toBe(false);
|
|
804
|
+
// Separators, but no indicator.
|
|
805
|
+
expect(
|
|
806
|
+
underlay()!.querySelector('.bottom-0.top-0:not(.w-\\[1px\\])'),
|
|
807
|
+
).toBeNull();
|
|
808
|
+
expect(separators().map((line) => line.style.opacity)).toEqual([
|
|
809
|
+
'0',
|
|
810
|
+
'0',
|
|
811
|
+
]);
|
|
812
|
+
|
|
813
|
+
expect(segment('Day').classList).toContain('m-[2px]');
|
|
814
|
+
expect(segment('Day').classList).toContain('bg-background');
|
|
815
|
+
expect(segment('Day').classList).toContain('rounded-[7px]');
|
|
816
|
+
expect(segment('Day').style.getPropertyValue('box-shadow')).toContain(
|
|
817
|
+
'0 3px 8px',
|
|
818
|
+
);
|
|
819
|
+
expect(segment('Week').classList).toContain('m-[2px]');
|
|
820
|
+
expect(segment('Week').classList).not.toContain('bg-background');
|
|
821
|
+
expect(segment('Week').style.getPropertyValue('box-shadow')).toBe('');
|
|
822
|
+
|
|
823
|
+
await tap('Week');
|
|
824
|
+
expect(host.selected()).toEqual(['Day', 'Month', 'Week']);
|
|
825
|
+
expect(segment('Week').classList).toContain('bg-background');
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('in a dark multiple selection, take the dark thumb color', async () => {
|
|
829
|
+
const { host, segment, update } = await renderControl(IOS_26, 'dark');
|
|
830
|
+
await update(() => host.isMultiple.set(true));
|
|
831
|
+
expect(segment('Week').style.getPropertyValue('background-color')).toBe(
|
|
832
|
+
'rgb(90, 90, 95)',
|
|
833
|
+
);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
it("draw nothing of a segmented control's while the host isn't one", async () => {
|
|
837
|
+
const { host, underlay, update } = await renderControl(IOS_26);
|
|
838
|
+
await update(() => host.isSegmented.set(false));
|
|
839
|
+
expect(underlay()).toBeNull();
|
|
840
|
+
expect(host.control.enabled()).toBe(false);
|
|
841
|
+
expect(host.control.slides()).toBe(false);
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
describe('segmentPlace', () => {
|
|
847
|
+
it("finds a segment's place among its host's segments, and no end for one that isn't there", () => {
|
|
848
|
+
const segments = ['Day', 'Week', 'Month'];
|
|
849
|
+
expect(segmentPlace(segments, 'Day')).toEqual({ first: true, last: false });
|
|
850
|
+
expect(segmentPlace(segments, 'Week')).toEqual({
|
|
851
|
+
first: false,
|
|
852
|
+
last: false,
|
|
853
|
+
});
|
|
854
|
+
expect(segmentPlace(segments, 'Month')).toEqual({
|
|
855
|
+
first: false,
|
|
856
|
+
last: true,
|
|
857
|
+
});
|
|
858
|
+
expect(segmentPlace(['Day'], 'Day')).toEqual({ first: true, last: true });
|
|
859
|
+
expect(segmentPlace([], 'Day')).toEqual({ first: false, last: false });
|
|
860
|
+
});
|
|
861
|
+
});
|