@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
|
@@ -2,11 +2,14 @@ import {
|
|
|
2
2
|
Component,
|
|
3
3
|
Directive,
|
|
4
4
|
ElementRef,
|
|
5
|
+
Injector,
|
|
6
|
+
Renderer2,
|
|
5
7
|
ViewEncapsulation,
|
|
6
8
|
computed,
|
|
7
9
|
contentChild,
|
|
8
10
|
contentChildren,
|
|
9
11
|
effect,
|
|
12
|
+
forwardRef,
|
|
10
13
|
inject,
|
|
11
14
|
input,
|
|
12
15
|
output,
|
|
@@ -35,6 +38,17 @@ import { type GlassShape, UiGlass, UiGlassGroup } from '../glass/glass';
|
|
|
35
38
|
import { type GlassDesign, injectGlassDesign } from '../glass/glass-design';
|
|
36
39
|
import { injectIconColor, UI_ICON_COLOR } from '../icon/icon-theme';
|
|
37
40
|
import { type IconName, ICONS } from '../icon/icons';
|
|
41
|
+
import {
|
|
42
|
+
KEYBOARD_GAP,
|
|
43
|
+
keyboardLift,
|
|
44
|
+
keyboardScreen,
|
|
45
|
+
measureForKeyboard,
|
|
46
|
+
} from '../search-field/keyboard';
|
|
47
|
+
import {
|
|
48
|
+
type SearchFieldContainer,
|
|
49
|
+
UI_SEARCH_FIELD_CONTAINER,
|
|
50
|
+
UiSearchField,
|
|
51
|
+
} from '../search-field/search-field';
|
|
38
52
|
import { UiDockState } from './dock-scroll';
|
|
39
53
|
|
|
40
54
|
/** The dock's sizes and shapes in one design. Sizes are in px. */
|
|
@@ -127,6 +141,15 @@ const MINIMIZED_SIZE = 48;
|
|
|
127
141
|
*/
|
|
128
142
|
const MATERIAL_HIDDEN_OFFSET = 64;
|
|
129
143
|
|
|
144
|
+
/**
|
|
145
|
+
* The search field's heights in the dock, in px: iOS 26's, as tall as the
|
|
146
|
+
* minimized bar beside it; Material 3's search bar; and the bar before
|
|
147
|
+
* iOS 26 that holds UISearchBar's 36pt field, 8pt in from its edges.
|
|
148
|
+
*/
|
|
149
|
+
const GLASS_SEARCH_SIZE = MINIMIZED_SIZE;
|
|
150
|
+
const MATERIAL_SEARCH_HEIGHT = 56;
|
|
151
|
+
const FROSTED_SEARCH_HEIGHT = 52;
|
|
152
|
+
|
|
130
153
|
/**
|
|
131
154
|
* Glass shapes this close merge, in px: just under the gap between them, so
|
|
132
155
|
* the bar and the action stay apart at rest, and reach for each other as the
|
|
@@ -149,6 +172,18 @@ const FRAME_TRANSITION = [
|
|
|
149
172
|
.map((property) => `${property} ${DURATION.slow + 50}ms ${EASING.sheet}`)
|
|
150
173
|
.join(', ');
|
|
151
174
|
|
|
175
|
+
/**
|
|
176
|
+
* How the dock follows the keyboard up and back down. The keyboard's height
|
|
177
|
+
* arrives a little after it starts to move, so it's quicker than the frames.
|
|
178
|
+
*/
|
|
179
|
+
const KEYBOARD_TRANSITION = `padding-bottom ${DURATION.normal}ms ${EASING.sheet}`;
|
|
180
|
+
|
|
181
|
+
/** The action's icon, fading as the action shrinks into the search field. */
|
|
182
|
+
const ICON_OUT_TRANSITION = `opacity ${DURATION.fast}ms ${EASING.accelerate}`;
|
|
183
|
+
|
|
184
|
+
/** The search field's content, fading in as its glass grows. */
|
|
185
|
+
const SEARCH_IN_TRANSITION = `opacity ${DURATION.normal}ms ${EASING.decelerate} ${DURATION.fast}ms`;
|
|
186
|
+
|
|
152
187
|
/** The selection capsule's slide, with a slight overshoot, and its fade. */
|
|
153
188
|
const CAPSULE_TRANSITION = `transform ${DURATION.slow}ms ${EASING.springSubtle}, opacity ${DURATION.normal}ms ${EASING.standard}`;
|
|
154
189
|
|
|
@@ -202,6 +237,11 @@ const iconSvg = (icon: IconName, color: string): string =>
|
|
|
202
237
|
const itemType = () => UiDockItem;
|
|
203
238
|
const actionType = () => UiDockAction;
|
|
204
239
|
const accessoryType = () => UiDockAccessory;
|
|
240
|
+
const searchFieldType = () => UiSearchField;
|
|
241
|
+
/**
|
|
242
|
+
* UiDock, for its own providers, which run before the class is defined.
|
|
243
|
+
*/
|
|
244
|
+
const dockType = () => UiDock;
|
|
205
245
|
|
|
206
246
|
/**
|
|
207
247
|
* Marks the content of the dock's accessory: a bar above the dock for
|
|
@@ -255,19 +295,50 @@ export class UiDockAccessory {}
|
|
|
255
295
|
* group, and the accessory a capsule above. It minimizes as content
|
|
256
296
|
* scrolls down (see UiDockScroll): the bar shrinks to the selected item's
|
|
257
297
|
* icon, and the accessory moves down between it and the action.
|
|
258
|
-
* - `'frosted'` (iOS 15 to 25): the tab bar of that time, edge to edge on
|
|
259
|
-
*
|
|
298
|
+
* - `'frosted'` (iOS 15 to 25): the tab bar of that time, edge to edge on
|
|
299
|
+
* the bars' chrome material, with the action as its last button. It
|
|
300
|
+
* doesn't minimize.
|
|
260
301
|
* - `'material'` (Android): a Material 3 Expressive floating toolbar, with an
|
|
261
302
|
* active indicator behind the selected item's icon, and the action as a
|
|
262
303
|
* floating action button. Minimizing slides the toolbar away and leaves
|
|
263
304
|
* the button.
|
|
305
|
+
*
|
|
306
|
+
* With a `<ui-search-field>` in it as well, the action opens a search field
|
|
307
|
+
* at the bottom of the screen, as iOS 26 searches on iPhone:
|
|
308
|
+
*
|
|
309
|
+
* ```html
|
|
310
|
+
* <ui-dock>
|
|
311
|
+
* <ui-dock-item route="/home" icon="home" label="Home" />
|
|
312
|
+
* <ui-dock-action route="/search" icon="search" label="Search" />
|
|
313
|
+
* <ui-search-field [(value)]="query" (submitted)="search($event)" />
|
|
314
|
+
* </ui-dock>
|
|
315
|
+
* ```
|
|
316
|
+
*
|
|
317
|
+
* With a `route`, it's the search tab: the field is open while the route is
|
|
318
|
+
* active. Without one, pressing the action opens it and focuses it, and
|
|
319
|
+
* UiDockState's `searching` says whether it's open. On glass, the action
|
|
320
|
+
* grows into the field across the bottom while the bar shrinks to a button
|
|
321
|
+
* that goes back to the last item. On Material, the action grows into a
|
|
322
|
+
* search bar and the toolbar slides away; the field's back arrow returns.
|
|
323
|
+
* Before iOS 26, the field opens in a bar above the tab bar. The field rises
|
|
324
|
+
* with the dock above the keyboard.
|
|
264
325
|
*/
|
|
265
326
|
@Component({
|
|
266
327
|
selector: 'ui-dock',
|
|
267
328
|
imports: [LYNX_ELEMENTS, UiGlass, UiGlassGroup],
|
|
268
329
|
encapsulation: ViewEncapsulation.None,
|
|
269
|
-
providers: [
|
|
270
|
-
|
|
330
|
+
providers: [
|
|
331
|
+
{ provide: UI_ICON_COLOR, useValue: ACCESSORY_ICON_COLOR },
|
|
332
|
+
// The search field in the dock draws only its content, in the dock's
|
|
333
|
+
// glass, and tells the dock when it's canceled.
|
|
334
|
+
{ provide: UI_SEARCH_FIELD_CONTAINER, useExisting: forwardRef(dockType) },
|
|
335
|
+
],
|
|
336
|
+
host: {
|
|
337
|
+
'[class]': 'hostClass()',
|
|
338
|
+
// Above the keyboard while the search field has it.
|
|
339
|
+
'[style.padding-bottom]': 'keyboardPadding()',
|
|
340
|
+
'[style.transition]': 'keyboardTransition()',
|
|
341
|
+
},
|
|
271
342
|
// The shapes are direct children of the glass group, so they merge on iOS
|
|
272
343
|
// and the web. Each one keeps its element as the dock minimizes and
|
|
273
344
|
// expands: its frame moves and resizes with a CSS transition, which iOS's
|
|
@@ -279,6 +350,7 @@ export class UiDockAccessory {}
|
|
|
279
350
|
//
|
|
280
351
|
// The dock measures its width with an empty view rather than on the
|
|
281
352
|
// group: Lynx for Web sends no layoutchange for a blur-view.
|
|
353
|
+
|
|
282
354
|
template: `
|
|
283
355
|
<view class="h-0 w-full" (bindlayoutchange)="measure($event)"></view>
|
|
284
356
|
<blur-view
|
|
@@ -291,6 +363,7 @@ export class UiDockAccessory {}
|
|
|
291
363
|
<blur-view
|
|
292
364
|
ui-glass
|
|
293
365
|
interactive
|
|
366
|
+
material="chrome"
|
|
294
367
|
class="flex-row flex absolute overflow-hidden"
|
|
295
368
|
[shape]="look().accessoryShape"
|
|
296
369
|
[radius]="accessoryRadius()"
|
|
@@ -302,6 +375,7 @@ export class UiDockAccessory {}
|
|
|
302
375
|
<blur-view
|
|
303
376
|
ui-glass
|
|
304
377
|
interactive
|
|
378
|
+
material="chrome"
|
|
305
379
|
[class]="barClass()"
|
|
306
380
|
[shape]="look().barShape"
|
|
307
381
|
[radius]="look().radius"
|
|
@@ -316,7 +390,7 @@ export class UiDockAccessory {}
|
|
|
316
390
|
accessibility-traits="tabbar"
|
|
317
391
|
[class]="rowClass()"
|
|
318
392
|
[style]="rowStyle()"
|
|
319
|
-
[attr.accessibility-elements-hidden]="
|
|
393
|
+
[attr.accessibility-elements-hidden]="rowHidden() || null"
|
|
320
394
|
>
|
|
321
395
|
<view
|
|
322
396
|
class="rounded-full bg-glass-foreground absolute bottom-0 left-0 top-0"
|
|
@@ -324,17 +398,18 @@ export class UiDockAccessory {}
|
|
|
324
398
|
></view>
|
|
325
399
|
<ng-content />
|
|
326
400
|
</view>
|
|
327
|
-
@if (
|
|
401
|
+
@if (barCollapsed()) {
|
|
328
402
|
<!-- Minimized, the bar is one button that brings the dock back,
|
|
329
|
-
showing the selected item's icon.
|
|
403
|
+
showing the selected item's icon. While the glass dock
|
|
404
|
+
searches, it's the button that goes back to the last item. -->
|
|
330
405
|
<view
|
|
331
406
|
#restore
|
|
332
407
|
class="items-center flex absolute bottom-0 left-0 right-0 top-0 justify-center"
|
|
333
408
|
accessibility-element="true"
|
|
334
409
|
accessibility-traits="button"
|
|
335
410
|
[attr.accessibility-label]="expandLabel()"
|
|
336
|
-
[attr.accessibility-value]="
|
|
337
|
-
(bindtap)="
|
|
411
|
+
[attr.accessibility-value]="restoreItem()?.label()"
|
|
412
|
+
(bindtap)="onRestore()"
|
|
338
413
|
>
|
|
339
414
|
<svg
|
|
340
415
|
[attr.content]="minimizedIcon()"
|
|
@@ -347,11 +422,13 @@ export class UiDockAccessory {}
|
|
|
347
422
|
<blur-view
|
|
348
423
|
ui-glass
|
|
349
424
|
interactive
|
|
425
|
+
material="chrome"
|
|
350
426
|
[class]="actionClass()"
|
|
351
427
|
[shape]="look().actionShape"
|
|
352
428
|
[radius]="look().radius"
|
|
353
429
|
[tint]="actionTint()"
|
|
354
430
|
[style]="frames().action"
|
|
431
|
+
[attr.accessibility-elements-hidden]="actionHidden() || null"
|
|
355
432
|
>
|
|
356
433
|
@if (design() === 'frosted') {
|
|
357
434
|
<view
|
|
@@ -360,11 +437,56 @@ export class UiDockAccessory {}
|
|
|
360
437
|
}
|
|
361
438
|
<ng-content select="ui-dock-action" />
|
|
362
439
|
</blur-view>
|
|
440
|
+
@if (searchField(); as field) {
|
|
441
|
+
<!-- The search field's own shape, after the action so it draws
|
|
442
|
+
over it: it grows out of the action as the search opens, and
|
|
443
|
+
shrinks back into it as it closes. -->
|
|
444
|
+
<blur-view
|
|
445
|
+
ui-glass
|
|
446
|
+
interactive
|
|
447
|
+
material="chrome"
|
|
448
|
+
class="flex-row flex absolute overflow-hidden"
|
|
449
|
+
[shape]="searchShape()"
|
|
450
|
+
[radius]="0"
|
|
451
|
+
[style]="frames().search"
|
|
452
|
+
>
|
|
453
|
+
@if (design() === 'frosted') {
|
|
454
|
+
<view
|
|
455
|
+
class="h-[0.5px] bg-border absolute left-0 right-0 top-0"
|
|
456
|
+
></view>
|
|
457
|
+
}
|
|
458
|
+
<view
|
|
459
|
+
[class]="searchContentClass()"
|
|
460
|
+
[style]="searchContentStyle()"
|
|
461
|
+
[attr.accessibility-elements-hidden]="!searching() || null"
|
|
462
|
+
>
|
|
463
|
+
<ng-content select="ui-search-field" />
|
|
464
|
+
</view>
|
|
465
|
+
</blur-view>
|
|
466
|
+
@if (design() === 'glass' && field.cancelable()) {
|
|
467
|
+
<!-- iOS 26's cancel beside the field: a glass circle that grows
|
|
468
|
+
out of the field's end while it's in use. -->
|
|
469
|
+
<blur-view
|
|
470
|
+
ui-glass
|
|
471
|
+
interactive
|
|
472
|
+
shape="circle"
|
|
473
|
+
class="items-center flex absolute justify-center overflow-hidden"
|
|
474
|
+
accessibility-element="true"
|
|
475
|
+
accessibility-traits="button"
|
|
476
|
+
[attr.accessibility-label]="field.cancelLabel()"
|
|
477
|
+
[attr.accessibility-elements-hidden]="!cancelShown() || null"
|
|
478
|
+
[style]="frames().cancel"
|
|
479
|
+
(bindtap)="field.cancel()"
|
|
480
|
+
>
|
|
481
|
+
<svg [attr.content]="cancelIcon()" [style]="cancelIconStyle()" />
|
|
482
|
+
</blur-view>
|
|
483
|
+
}
|
|
484
|
+
}
|
|
363
485
|
}
|
|
364
486
|
</blur-view>
|
|
365
487
|
`,
|
|
366
488
|
})
|
|
367
|
-
export class UiDock {
|
|
489
|
+
export class UiDock implements SearchFieldContainer {
|
|
368
490
|
/**
|
|
369
491
|
* What a screen reader says for the minimized dock, a button that brings
|
|
370
492
|
* it back. The selected item's label follows it.
|
|
@@ -375,6 +497,9 @@ export class UiDock {
|
|
|
375
497
|
readonly #state = inject(UiDockState);
|
|
376
498
|
readonly #settings = inject(LynxAccessibilitySettings);
|
|
377
499
|
readonly #resolveColor = injectIconColor();
|
|
500
|
+
readonly #injector = inject(Injector);
|
|
501
|
+
readonly #renderer = inject(Renderer2);
|
|
502
|
+
readonly #host = inject(ElementRef);
|
|
378
503
|
|
|
379
504
|
/** The design on this platform (see GlassDesign), which the items follow. */
|
|
380
505
|
readonly design = injectGlassDesign();
|
|
@@ -385,7 +510,8 @@ export class UiDock {
|
|
|
385
510
|
protected readonly items = contentChildren(itemType());
|
|
386
511
|
protected readonly action = contentChild(actionType());
|
|
387
512
|
protected readonly accessory = contentChild(accessoryType());
|
|
388
|
-
protected readonly
|
|
513
|
+
protected readonly searchField = contentChild(searchFieldType());
|
|
514
|
+
protected readonly restoreButton = viewChild<ElementRef>('restore');
|
|
389
515
|
|
|
390
516
|
/**
|
|
391
517
|
* The dock's width, from Lynx's layout. The glass bar shrinks from it when
|
|
@@ -398,20 +524,93 @@ export class UiDock {
|
|
|
398
524
|
this.items().find((item) => item.active()),
|
|
399
525
|
);
|
|
400
526
|
|
|
527
|
+
/**
|
|
528
|
+
* The item selected last, which stays while the search's route is active
|
|
529
|
+
* and no item is: the glass bar's button goes back to it. Kept by an
|
|
530
|
+
* effect rather than a linkedSignal, which would only see the items
|
|
531
|
+
* selected while something read it.
|
|
532
|
+
*/
|
|
533
|
+
readonly #lastItem = signal<UiDockItem | undefined>(undefined);
|
|
534
|
+
protected readonly rememberItem = effect(() => {
|
|
535
|
+
const item = this.selectedItem();
|
|
536
|
+
if (item) this.#lastItem.set(item);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Whether the search field is open: with a search field and an action in
|
|
541
|
+
* the dock, while the action's route is active, or while UiDockState says
|
|
542
|
+
* so for an action without one.
|
|
543
|
+
*/
|
|
544
|
+
readonly searching = computed(() => {
|
|
545
|
+
const action = this.action();
|
|
546
|
+
return (
|
|
547
|
+
!!this.searchField() &&
|
|
548
|
+
!!action &&
|
|
549
|
+
(action.active() || this.#state.searching())
|
|
550
|
+
);
|
|
551
|
+
});
|
|
552
|
+
|
|
401
553
|
/**
|
|
402
554
|
* Whether the dock is minimized now: as UiDockState says, in the designs
|
|
403
|
-
* that minimize, once the dock knows the width it shrinks from
|
|
555
|
+
* that minimize, once the dock knows the width it shrinks from, and not
|
|
556
|
+
* while it searches.
|
|
404
557
|
*/
|
|
405
558
|
readonly minimized = computed(
|
|
406
559
|
() =>
|
|
407
560
|
this.#state.minimized() &&
|
|
408
561
|
this.design() !== 'frosted' &&
|
|
409
|
-
this.width() > 0
|
|
562
|
+
this.width() > 0 &&
|
|
563
|
+
!this.searching(),
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
/** The glass dock searching: its bar shrinks to a button beside the field. */
|
|
567
|
+
readonly #glassSearch = computed(
|
|
568
|
+
() => this.searching() && this.design() === 'glass' && this.width() > 0,
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
/** Whether the bar is one button: minimized, or beside the glass search. */
|
|
572
|
+
protected readonly barCollapsed = computed(
|
|
573
|
+
() => this.minimized() || this.#glassSearch(),
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Whether the bar's items are out of sight: in a collapsed bar, or in
|
|
578
|
+
* Material's toolbar while it's slid away for the search bar.
|
|
579
|
+
*/
|
|
580
|
+
protected readonly rowHidden = computed(
|
|
581
|
+
() =>
|
|
582
|
+
this.barCollapsed() || (this.searching() && this.design() === 'material'),
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* The glass and Material action is the field's while it searches: it
|
|
587
|
+
* shrinks into the field's end, and its icon fades out on the way.
|
|
588
|
+
*/
|
|
589
|
+
readonly actionHidden = computed(
|
|
590
|
+
() => this.searching() && this.design() !== 'frosted',
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
/** The item the collapsed bar shows, and goes back to while searching. */
|
|
594
|
+
protected readonly restoreItem = computed(() =>
|
|
595
|
+
this.#glassSearch() ? this.#lastItem() : this.selectedItem(),
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
/** Whether the field's Cancel shows, beside it on glass. */
|
|
599
|
+
protected readonly cancelShown = computed(
|
|
600
|
+
() => this.searching() && !!this.searchField()?.cancelShown(),
|
|
410
601
|
);
|
|
411
602
|
|
|
412
603
|
/** No sliding, shrinking or fading under Reduce Motion. */
|
|
413
604
|
readonly #animate = computed(() => !this.#settings.reduceMotion());
|
|
414
605
|
|
|
606
|
+
/**
|
|
607
|
+
* How far up the keyboard reaches into the dock, from its bottom edge, in
|
|
608
|
+
* px: 0 while it's down, or while the search field doesn't have it.
|
|
609
|
+
*/
|
|
610
|
+
readonly #keyboardOverlap = signal(0);
|
|
611
|
+
/** Each keyboard change's measuring, so a late answer can't win. */
|
|
612
|
+
#measuring = 0;
|
|
613
|
+
|
|
415
614
|
protected readonly hostClass = computed(() =>
|
|
416
615
|
cn(
|
|
417
616
|
'flex-col flex absolute bottom-0 left-0 right-0',
|
|
@@ -420,6 +619,24 @@ export class UiDock {
|
|
|
420
619
|
),
|
|
421
620
|
);
|
|
422
621
|
|
|
622
|
+
/**
|
|
623
|
+
* While the search field has the keyboard, the dock floats above it: the
|
|
624
|
+
* glass and Material shapes a little above it, as they float above the home
|
|
625
|
+
* indicator, and the field's bar before iOS 26 on top of it, with the tab
|
|
626
|
+
* bar under the keyboard, as Safari's bar did.
|
|
627
|
+
*/
|
|
628
|
+
protected readonly keyboardPadding = computed(() => {
|
|
629
|
+
const overlap = this.searching() ? this.#keyboardOverlap() : 0;
|
|
630
|
+
if (overlap <= 0) return null;
|
|
631
|
+
return this.design() === 'frosted'
|
|
632
|
+
? `calc(${overlap - this.look().bar}px - env(safe-area-inset-bottom))`
|
|
633
|
+
: px(overlap + KEYBOARD_GAP);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
protected readonly keyboardTransition = computed(() =>
|
|
637
|
+
this.searchField() && this.#animate() ? KEYBOARD_TRANSITION : null,
|
|
638
|
+
);
|
|
639
|
+
|
|
423
640
|
protected readonly accessoryRadius = computed(() =>
|
|
424
641
|
this.design() === 'frosted' ? FROSTED_ACCESSORY_RADIUS : this.look().radius,
|
|
425
642
|
);
|
|
@@ -428,20 +645,44 @@ export class UiDock {
|
|
|
428
645
|
* Each shape's frame in the dock, bottom up, and the dock's height. The
|
|
429
646
|
* shapes are positioned absolutely, so a minimized dock can move each one
|
|
430
647
|
* on its own: the accessory from above the bar into the row beside it.
|
|
648
|
+
*
|
|
649
|
+
* The search field's shape and its Cancel are there only with a search
|
|
650
|
+
* field. Closed, they're points inside the action, which they grow out of
|
|
651
|
+
* as the search opens.
|
|
431
652
|
*/
|
|
432
653
|
protected readonly frames = computed(() => {
|
|
433
654
|
const look = this.look();
|
|
434
655
|
const design = this.design();
|
|
435
656
|
const hasAction = !!this.action();
|
|
436
657
|
const minimized = this.minimized();
|
|
658
|
+
const searching = this.searching();
|
|
659
|
+
const width = this.width();
|
|
437
660
|
const transition = this.#animate() ? FRAME_TRANSITION : 'none';
|
|
438
661
|
const actionSpace = hasAction ? look.action + look.gap : 0;
|
|
662
|
+
/**
|
|
663
|
+
* A point, a shape with no size, `right` and `bottom` px in.
|
|
664
|
+
*/
|
|
665
|
+
const point = (right: number, bottom: number) => ({
|
|
666
|
+
left: px(Math.max(0, width - right)),
|
|
667
|
+
right: px(right),
|
|
668
|
+
bottom: px(bottom),
|
|
669
|
+
height: '0px',
|
|
670
|
+
transition,
|
|
671
|
+
});
|
|
439
672
|
|
|
440
673
|
if (design === 'frosted') {
|
|
441
674
|
const above = look.bar + look.gap;
|
|
675
|
+
const strip = searching ? FROSTED_SEARCH_HEIGHT : 0;
|
|
676
|
+
// Only with a search field, so the dock moves as it did without one.
|
|
677
|
+
const searchTransition = this.searchField() ? transition : undefined;
|
|
442
678
|
return {
|
|
443
679
|
stage: {
|
|
444
|
-
height: abovePx(
|
|
680
|
+
height: abovePx(
|
|
681
|
+
this.accessory()
|
|
682
|
+
? above + strip + look.accessory
|
|
683
|
+
: look.bar + strip,
|
|
684
|
+
),
|
|
685
|
+
transition: searchTransition,
|
|
445
686
|
},
|
|
446
687
|
bar: {
|
|
447
688
|
left: '0px',
|
|
@@ -458,49 +699,107 @@ export class UiDock {
|
|
|
458
699
|
accessory: {
|
|
459
700
|
left: px(FROSTED_ACCESSORY_INSET),
|
|
460
701
|
right: px(FROSTED_ACCESSORY_INSET),
|
|
461
|
-
bottom: abovePx(above),
|
|
702
|
+
bottom: abovePx(above + strip),
|
|
462
703
|
height: px(look.accessory),
|
|
704
|
+
transition: searchTransition,
|
|
705
|
+
},
|
|
706
|
+
// A bar on the tab bar's material, above it.
|
|
707
|
+
search: {
|
|
708
|
+
left: '0px',
|
|
709
|
+
right: '0px',
|
|
710
|
+
bottom: abovePx(look.bar),
|
|
711
|
+
height: px(strip),
|
|
712
|
+
transition,
|
|
463
713
|
},
|
|
714
|
+
cancel: point(0, 0),
|
|
464
715
|
};
|
|
465
716
|
}
|
|
466
717
|
|
|
467
|
-
if (design === 'glass' && minimized) {
|
|
718
|
+
if (design === 'glass' && (minimized || this.#glassSearch())) {
|
|
468
719
|
const size = MINIMIZED_SIZE;
|
|
469
720
|
const besideAction = hasAction ? size + look.gap : 0;
|
|
721
|
+
const cancel = this.cancelShown();
|
|
722
|
+
const cancelSpace = cancel ? GLASS_SEARCH_SIZE + look.gap : 0;
|
|
723
|
+
const accessoryHeight = this.accessory() ? look.gap + look.accessory : 0;
|
|
470
724
|
return {
|
|
471
|
-
stage: {
|
|
472
|
-
|
|
473
|
-
left: '0px',
|
|
474
|
-
right: px(this.width() - size),
|
|
475
|
-
bottom: '0px',
|
|
476
|
-
height: px(size),
|
|
477
|
-
transition,
|
|
478
|
-
},
|
|
479
|
-
action: {
|
|
480
|
-
right: '0px',
|
|
481
|
-
bottom: '0px',
|
|
482
|
-
width: px(size),
|
|
483
|
-
height: px(size),
|
|
725
|
+
stage: {
|
|
726
|
+
height: px(searching ? size + accessoryHeight : size),
|
|
484
727
|
transition,
|
|
485
728
|
},
|
|
486
|
-
|
|
487
|
-
left:
|
|
488
|
-
right: px(
|
|
729
|
+
bar: {
|
|
730
|
+
left: '0px',
|
|
731
|
+
right: px(width - size),
|
|
489
732
|
bottom: '0px',
|
|
490
733
|
height: px(size),
|
|
491
734
|
transition,
|
|
492
735
|
},
|
|
736
|
+
// Searching, the action shrinks into the end of the field, which
|
|
737
|
+
// grows out of it.
|
|
738
|
+
action: searching
|
|
739
|
+
? {
|
|
740
|
+
right: px(cancelSpace + size / 2),
|
|
741
|
+
bottom: px(size / 2),
|
|
742
|
+
width: '0px',
|
|
743
|
+
height: '0px',
|
|
744
|
+
transition,
|
|
745
|
+
}
|
|
746
|
+
: {
|
|
747
|
+
right: '0px',
|
|
748
|
+
bottom: '0px',
|
|
749
|
+
width: px(size),
|
|
750
|
+
height: px(size),
|
|
751
|
+
transition,
|
|
752
|
+
},
|
|
753
|
+
// Minimized, the accessory moves down between the bar and the
|
|
754
|
+
// action; searching, it stays above the field.
|
|
755
|
+
accessory: searching
|
|
756
|
+
? {
|
|
757
|
+
left: '0px',
|
|
758
|
+
right: '0px',
|
|
759
|
+
bottom: px(size + look.gap),
|
|
760
|
+
height: px(look.accessory),
|
|
761
|
+
transition,
|
|
762
|
+
}
|
|
763
|
+
: {
|
|
764
|
+
left: px(size + look.gap),
|
|
765
|
+
right: px(besideAction),
|
|
766
|
+
bottom: '0px',
|
|
767
|
+
height: px(size),
|
|
768
|
+
transition,
|
|
769
|
+
},
|
|
770
|
+
search: searching
|
|
771
|
+
? {
|
|
772
|
+
left: px(size + look.gap),
|
|
773
|
+
right: px(cancelSpace),
|
|
774
|
+
bottom: '0px',
|
|
775
|
+
height: px(GLASS_SEARCH_SIZE),
|
|
776
|
+
transition,
|
|
777
|
+
}
|
|
778
|
+
: point(size / 2, size / 2),
|
|
779
|
+
cancel: cancel
|
|
780
|
+
? {
|
|
781
|
+
left: px(width - GLASS_SEARCH_SIZE),
|
|
782
|
+
right: '0px',
|
|
783
|
+
bottom: '0px',
|
|
784
|
+
height: px(GLASS_SEARCH_SIZE),
|
|
785
|
+
transition,
|
|
786
|
+
}
|
|
787
|
+
: point(searching ? 0 : size / 2, size / 2),
|
|
493
788
|
};
|
|
494
789
|
}
|
|
495
790
|
|
|
496
|
-
// Glass or Material, expanded; or Material minimized, which
|
|
497
|
-
// toolbar and the accessory below the screen
|
|
498
|
-
|
|
791
|
+
// Glass or Material, expanded; or Material minimized or searching, which
|
|
792
|
+
// slides the toolbar and the accessory below the screen. Minimized, it
|
|
793
|
+
// keeps the button; searching, the button grows into the search bar.
|
|
794
|
+
const hidden = design === 'material' && (minimized || searching);
|
|
499
795
|
const slide = (by: number) =>
|
|
500
796
|
design === 'material' ? `translateY(${hidden ? by : 0}px)` : null;
|
|
501
797
|
const withAccessory = this.accessory()
|
|
502
798
|
? look.bar + look.gap + look.accessory
|
|
503
799
|
: look.bar;
|
|
800
|
+
const actionCenter = point(look.action / 2, look.bar / 2);
|
|
801
|
+
// Material's button is shorter than the toolbar, and centered on it.
|
|
802
|
+
const actionBottom = (look.bar - look.action) / 2;
|
|
504
803
|
return {
|
|
505
804
|
stage: { height: px(withAccessory), transition },
|
|
506
805
|
bar: {
|
|
@@ -511,14 +810,22 @@ export class UiDock {
|
|
|
511
810
|
transform: slide(look.bar + MATERIAL_HIDDEN_OFFSET),
|
|
512
811
|
transition,
|
|
513
812
|
},
|
|
514
|
-
action:
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
813
|
+
action:
|
|
814
|
+
design === 'material' && searching
|
|
815
|
+
? {
|
|
816
|
+
right: px(look.action / 2),
|
|
817
|
+
bottom: px(look.bar / 2),
|
|
818
|
+
width: '0px',
|
|
819
|
+
height: '0px',
|
|
820
|
+
transition,
|
|
821
|
+
}
|
|
822
|
+
: {
|
|
823
|
+
right: '0px',
|
|
824
|
+
bottom: px(actionBottom),
|
|
825
|
+
width: px(look.action),
|
|
826
|
+
height: px(look.action),
|
|
827
|
+
transition,
|
|
828
|
+
},
|
|
522
829
|
accessory: {
|
|
523
830
|
left: '0px',
|
|
524
831
|
right: '0px',
|
|
@@ -527,6 +834,17 @@ export class UiDock {
|
|
|
527
834
|
transform: slide(withAccessory + MATERIAL_HIDDEN_OFFSET),
|
|
528
835
|
transition,
|
|
529
836
|
},
|
|
837
|
+
search:
|
|
838
|
+
design === 'material' && searching
|
|
839
|
+
? {
|
|
840
|
+
left: '0px',
|
|
841
|
+
right: '0px',
|
|
842
|
+
bottom: px((look.bar - MATERIAL_SEARCH_HEIGHT) / 2),
|
|
843
|
+
height: px(MATERIAL_SEARCH_HEIGHT),
|
|
844
|
+
transition,
|
|
845
|
+
}
|
|
846
|
+
: actionCenter,
|
|
847
|
+
cancel: actionCenter,
|
|
530
848
|
};
|
|
531
849
|
});
|
|
532
850
|
|
|
@@ -551,6 +869,43 @@ export class UiDock {
|
|
|
551
869
|
this.design() === 'material' ? 'var(--primary)' : undefined,
|
|
552
870
|
);
|
|
553
871
|
|
|
872
|
+
/**
|
|
873
|
+
* A capsule on glass and Material, as iOS 26's field and Material 3's
|
|
874
|
+
* search bar are; before iOS 26, a bar from edge to edge.
|
|
875
|
+
*/
|
|
876
|
+
protected readonly searchShape = computed(() =>
|
|
877
|
+
this.design() === 'frosted' ? 'rounded' : 'capsule',
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
protected readonly searchContentClass = computed(() =>
|
|
881
|
+
cn(
|
|
882
|
+
'flex-1 flex-row items-center flex',
|
|
883
|
+
this.design() === 'frosted' && 'px-[8px]',
|
|
884
|
+
),
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* The field's content fades in once its shape has mostly grown, and out at
|
|
889
|
+
* once as the search closes.
|
|
890
|
+
*/
|
|
891
|
+
protected readonly searchContentStyle = computed(() => {
|
|
892
|
+
const searching = this.searching();
|
|
893
|
+
return {
|
|
894
|
+
opacity: searching ? '1' : '0',
|
|
895
|
+
transition: searching && this.#animate() ? SEARCH_IN_TRANSITION : 'none',
|
|
896
|
+
};
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
protected readonly cancelIcon = computed(() =>
|
|
900
|
+
iconSvg('x', this.#resolveColor('var(--glass-foreground)')),
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
protected readonly cancelIconStyle = computed(() => ({
|
|
904
|
+
width: '20px',
|
|
905
|
+
height: '20px',
|
|
906
|
+
opacity: this.cancelShown() ? '1' : '0',
|
|
907
|
+
}));
|
|
908
|
+
|
|
554
909
|
protected readonly rowClass = computed(() =>
|
|
555
910
|
cn(
|
|
556
911
|
'flex-row flex relative',
|
|
@@ -566,15 +921,15 @@ export class UiDock {
|
|
|
566
921
|
*/
|
|
567
922
|
protected readonly rowStyle = computed(() => {
|
|
568
923
|
const look = this.look();
|
|
569
|
-
const
|
|
924
|
+
const hidden = this.rowHidden();
|
|
570
925
|
const action = this.action() ? look.action + look.gap : 0;
|
|
571
926
|
const floating = this.design() !== 'frosted' && this.width() > 0;
|
|
572
927
|
return {
|
|
573
928
|
margin: px(look.inset),
|
|
574
929
|
width: floating ? px(this.width() - action - 2 * look.inset) : null,
|
|
575
|
-
opacity:
|
|
930
|
+
opacity: hidden ? '0' : '1',
|
|
576
931
|
transition: this.#animate()
|
|
577
|
-
?
|
|
932
|
+
? hidden
|
|
578
933
|
? ROW_OUT_TRANSITION
|
|
579
934
|
: ROW_IN_TRANSITION
|
|
580
935
|
: 'none',
|
|
@@ -605,22 +960,60 @@ export class UiDock {
|
|
|
605
960
|
};
|
|
606
961
|
});
|
|
607
962
|
|
|
608
|
-
/**
|
|
963
|
+
/**
|
|
964
|
+
* The icon of the collapsed bar's item (see restoreItem), or the first
|
|
965
|
+
* item's.
|
|
966
|
+
*/
|
|
609
967
|
protected readonly minimizedIcon = computed(() => {
|
|
610
|
-
const item = this.
|
|
968
|
+
const item = this.restoreItem() ?? this.items()[0];
|
|
611
969
|
return item
|
|
612
970
|
? iconSvg(item.icon(), this.#resolveColor('var(--primary)'))
|
|
613
971
|
: '';
|
|
614
972
|
});
|
|
615
973
|
|
|
616
|
-
// The
|
|
974
|
+
// The collapsed bar's icon fades in as the bar shrinks around it.
|
|
617
975
|
protected readonly restoreEntrance = effect(() => {
|
|
618
|
-
const restore = this.
|
|
976
|
+
const restore = this.restoreButton();
|
|
619
977
|
if (restore && untracked(this.#animate)) {
|
|
620
978
|
fadeIn(restore.nativeElement, { duration: DURATION.slow });
|
|
621
979
|
}
|
|
622
980
|
});
|
|
623
981
|
|
|
982
|
+
/** Whether the dock searched when it last checked, to see it close. */
|
|
983
|
+
#wasSearching = false;
|
|
984
|
+
|
|
985
|
+
// A search that closes, by its route or by the app, takes the keyboard
|
|
986
|
+
// with it.
|
|
987
|
+
protected readonly closeKeyboard = effect(() => {
|
|
988
|
+
const searching = this.searching();
|
|
989
|
+
if (this.#wasSearching && !searching) {
|
|
990
|
+
untracked(() => this.searchField()?.blur());
|
|
991
|
+
}
|
|
992
|
+
this.#wasSearching = searching;
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// The search field's keyboard height says the keyboard moved: measure how
|
|
996
|
+
// far it reaches into the dock, which floats above it while it searches.
|
|
997
|
+
protected readonly followKeyboard = effect(() => {
|
|
998
|
+
const height = this.searchField()?.keyboardHeight() ?? 0;
|
|
999
|
+
const measuring = ++this.#measuring;
|
|
1000
|
+
const screen = height > 0 ? keyboardScreen(this.#injector) : null;
|
|
1001
|
+
if (!screen) {
|
|
1002
|
+
this.#keyboardOverlap.set(0);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
// The dock's host sits at the bottom of the screen whatever its padding,
|
|
1006
|
+
// so measuring it gives the same answer however far the dock has risen.
|
|
1007
|
+
measureForKeyboard(this.#renderer, this.#host.nativeElement).then(
|
|
1008
|
+
(measured) => {
|
|
1009
|
+
if (measuring !== this.#measuring || !measured) return;
|
|
1010
|
+
this.#keyboardOverlap.set(
|
|
1011
|
+
keyboardLift(measured.element, measured.page, height, screen, 0),
|
|
1012
|
+
);
|
|
1013
|
+
},
|
|
1014
|
+
);
|
|
1015
|
+
});
|
|
1016
|
+
|
|
624
1017
|
protected measure(event: Event): void {
|
|
625
1018
|
this.width.set((event as LayoutChangeEvent).detail.width);
|
|
626
1019
|
}
|
|
@@ -631,6 +1024,55 @@ export class UiDock {
|
|
|
631
1024
|
expand(): void {
|
|
632
1025
|
this.#state.minimized.set(false);
|
|
633
1026
|
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Opens the search field and gives it the keyboard, as pressing an action
|
|
1030
|
+
* without a route does.
|
|
1031
|
+
*/
|
|
1032
|
+
openSearch(): void {
|
|
1033
|
+
const field = this.searchField();
|
|
1034
|
+
if (!field) return;
|
|
1035
|
+
this.#state.searching.set(true);
|
|
1036
|
+
field.focus();
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Closes the search field: goes back to the last item while the search's
|
|
1041
|
+
* route is active, or else closes the search UiDockState holds open.
|
|
1042
|
+
*/
|
|
1043
|
+
closeSearch(): void {
|
|
1044
|
+
if (this.action()?.active()) {
|
|
1045
|
+
(this.#lastItem() ?? this.items()[0])?.navigate();
|
|
1046
|
+
}
|
|
1047
|
+
this.#state.searching.set(false);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Cancel closes the search, except that the search tab of iOS stays open
|
|
1052
|
+
* with its suggestions: there, Cancel clears the field and closes the
|
|
1053
|
+
* keyboard, and the bar's button goes back. Material's back arrow always
|
|
1054
|
+
* goes back.
|
|
1055
|
+
*/
|
|
1056
|
+
searchCanceled(): void {
|
|
1057
|
+
if (this.design() === 'material' || !this.action()?.active()) {
|
|
1058
|
+
this.closeSearch();
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* The action was pressed: one without a route opens the search.
|
|
1064
|
+
*/
|
|
1065
|
+
actionPressed(routed: boolean): void {
|
|
1066
|
+
if (!routed) this.openSearch();
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
protected onRestore(): void {
|
|
1070
|
+
if (this.searching()) {
|
|
1071
|
+
this.closeSearch();
|
|
1072
|
+
} else {
|
|
1073
|
+
this.expand();
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
634
1076
|
}
|
|
635
1077
|
|
|
636
1078
|
/**
|
|
@@ -749,6 +1191,7 @@ export class UiDockItem {
|
|
|
749
1191
|
// The host, not an inner view, shrinks on a press: it's the whole tap
|
|
750
1192
|
// target, and injecting it needs no view query.
|
|
751
1193
|
readonly #host = inject(ElementRef);
|
|
1194
|
+
readonly #link = inject(RouterLink);
|
|
752
1195
|
#pressAnim?: AnimationHandle;
|
|
753
1196
|
|
|
754
1197
|
protected readonly design = this.#dock.design;
|
|
@@ -846,6 +1289,14 @@ export class UiDockItem {
|
|
|
846
1289
|
),
|
|
847
1290
|
);
|
|
848
1291
|
|
|
1292
|
+
/**
|
|
1293
|
+
* Navigates to the item's route, with its RouterLink options, as a tap
|
|
1294
|
+
* does: the glass dock's collapsed bar goes back to it from the search.
|
|
1295
|
+
*/
|
|
1296
|
+
navigate(): void {
|
|
1297
|
+
this.#link.onClick(0, false, false, false, false);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
849
1300
|
protected onPressStart(): void {
|
|
850
1301
|
this.#pressAnim?.cancel();
|
|
851
1302
|
this.#pressAnim = pressDown(this.#host.nativeElement);
|
|
@@ -871,6 +1322,8 @@ export class UiDockItem {
|
|
|
871
1322
|
* <ui-dock-action route="/search" icon="search" label="Search" />
|
|
872
1323
|
* <ui-dock-action icon="plus" label="New message" (pressed)="compose()" />
|
|
873
1324
|
* ```
|
|
1325
|
+
*
|
|
1326
|
+
* With a `<ui-search-field>` in the dock, it opens the search (see UiDock).
|
|
874
1327
|
*/
|
|
875
1328
|
@Component({
|
|
876
1329
|
selector: 'ui-dock-action',
|
|
@@ -907,7 +1360,11 @@ export class UiDockItem {
|
|
|
907
1360
|
'(bindtouchcancel)': 'onPressEnd()',
|
|
908
1361
|
'(bindmouseup)': 'onPressEnd()',
|
|
909
1362
|
'(bindmouseleave)': 'onPressEnd()',
|
|
910
|
-
'(bindtap)': '
|
|
1363
|
+
'(bindtap)': 'onTap()',
|
|
1364
|
+
// Its icon fades while the button shrinks into the search field, so it
|
|
1365
|
+
// doesn't show through the field's glass. The content, not the glass.
|
|
1366
|
+
'[style.opacity]': 'fade().opacity',
|
|
1367
|
+
'[style.transition]': 'fade().transition',
|
|
911
1368
|
},
|
|
912
1369
|
template: `<svg
|
|
913
1370
|
[attr.content]="iconSvg()"
|
|
@@ -923,8 +1380,10 @@ export class UiDockAction {
|
|
|
923
1380
|
readonly pressed = output<void>();
|
|
924
1381
|
|
|
925
1382
|
readonly #dock = inject(UiDock);
|
|
1383
|
+
readonly #settings = inject(LynxAccessibilitySettings);
|
|
926
1384
|
readonly #resolveColor = injectIconColor();
|
|
927
1385
|
readonly #host = inject(ElementRef);
|
|
1386
|
+
readonly #link = inject(RouterLink);
|
|
928
1387
|
#pressAnim?: AnimationHandle;
|
|
929
1388
|
|
|
930
1389
|
/** Whether the button's route is active, as for UiDockItem.active. */
|
|
@@ -948,6 +1407,25 @@ export class UiDockAction {
|
|
|
948
1407
|
return iconSvg(this.icon(), this.#resolveColor(color));
|
|
949
1408
|
});
|
|
950
1409
|
|
|
1410
|
+
protected readonly fade = computed(() => {
|
|
1411
|
+
const hidden = this.#dock.actionHidden();
|
|
1412
|
+
return {
|
|
1413
|
+
opacity: hidden ? '0' : null,
|
|
1414
|
+
transition:
|
|
1415
|
+
hidden && !this.#settings.reduceMotion() ? ICON_OUT_TRANSITION : null,
|
|
1416
|
+
};
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* RouterLink navigates on the same tap through its own listener, when the
|
|
1421
|
+
* button has a route: one without a route opens the dock's search, if it
|
|
1422
|
+
* has one.
|
|
1423
|
+
*/
|
|
1424
|
+
protected onTap(): void {
|
|
1425
|
+
this.pressed.emit();
|
|
1426
|
+
this.#dock.actionPressed(this.#link.urlTree !== null);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
951
1429
|
protected onPressStart(): void {
|
|
952
1430
|
this.#pressAnim?.cancel();
|
|
953
1431
|
this.#pressAnim = pressDown(this.#host.nativeElement);
|