@blotch/dolan 22.2.6 → 22.2.8
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/install-packages.test.js +7 -2
- package/dist/commands/install-packages.test.js.map +1 -1
- package/dist/registry.js +8 -2
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +1 -0
- package/dist/registry.test.js.map +1 -1
- package/dist/ui/components/accordion/accordion.test.ts +24 -7
- package/dist/ui/components/accordion/accordion.ts +34 -44
- package/dist/ui/components/action-sheet/action-sheet.test.ts +18 -1
- package/dist/ui/components/action-sheet/action-sheet.ts +55 -60
- package/dist/ui/components/alert/alert.test.ts +17 -0
- package/dist/ui/components/alert/alert.ts +23 -14
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +19 -4
- package/dist/ui/components/alert-dialog/alert-dialog.ts +31 -20
- package/dist/ui/components/aspect-ratio/aspect-ratio.test.ts +38 -2
- package/dist/ui/components/aspect-ratio/aspect-ratio.ts +11 -8
- package/dist/ui/components/avatar/avatar.test.ts +7 -0
- package/dist/ui/components/avatar/avatar.ts +35 -30
- package/dist/ui/components/badge/badge.test.ts +9 -0
- package/dist/ui/components/badge/badge.ts +12 -10
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +7 -0
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +5 -0
- package/dist/ui/components/button/button.test.ts +59 -3
- package/dist/ui/components/button/button.ts +67 -62
- package/dist/ui/components/button-group/button-group.test.ts +37 -4
- package/dist/ui/components/button-group/button-group.ts +60 -51
- package/dist/ui/components/card/card.test.ts +35 -2
- package/dist/ui/components/card/card.ts +41 -53
- package/dist/ui/components/chart/chart-presets.test.ts +16 -2
- package/dist/ui/components/chart/chart-presets.ts +22 -3
- package/dist/ui/components/chart/chart.test.ts +35 -6
- package/dist/ui/components/chart/chart.ts +14 -3
- package/dist/ui/components/checkbox/checkbox.test.ts +11 -4
- package/dist/ui/components/checkbox/checkbox.ts +21 -14
- package/dist/ui/components/collapsible/collapsible.test.ts +20 -10
- package/dist/ui/components/collapsible/collapsible.ts +29 -40
- package/dist/ui/components/dialog/dialog.test.ts +17 -1
- package/dist/ui/components/dialog/dialog.ts +24 -17
- package/dist/ui/components/dock/dock.test.ts +30 -5
- package/dist/ui/components/dock/dock.ts +29 -27
- package/dist/ui/components/empty-state/empty-state.test.ts +9 -0
- package/dist/ui/components/empty-state/empty-state.ts +20 -11
- package/dist/ui/components/icon/icon.test.ts +134 -2
- package/dist/ui/components/icon/icon.ts +57 -18
- package/dist/ui/components/input/input.test.ts +55 -3
- package/dist/ui/components/input/input.ts +91 -65
- package/dist/ui/components/label/label.test.ts +15 -0
- package/dist/ui/components/label/label.ts +12 -4
- package/dist/ui/components/list/list.test.ts +13 -3
- package/dist/ui/components/list/list.ts +14 -2
- package/dist/ui/components/menu/menu.test.ts +8 -0
- package/dist/ui/components/menu/menu.ts +4 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +34 -21
- package/dist/ui/components/nav-drawer/nav-drawer.ts +60 -68
- package/dist/ui/components/navigation-bar/navigation-bar.test.ts +13 -0
- package/dist/ui/components/navigation-bar/navigation-bar.ts +5 -0
- package/dist/ui/components/pagination/pagination.test.ts +9 -0
- package/dist/ui/components/pagination/pagination.ts +52 -49
- package/dist/ui/components/popover/popover.test.ts +3 -0
- package/dist/ui/components/progress/progress.test.ts +7 -0
- package/dist/ui/components/progress/progress.ts +8 -6
- package/dist/ui/components/radio-group/radio-group.test.ts +8 -0
- package/dist/ui/components/radio-group/radio-group.ts +22 -19
- package/dist/ui/components/scroll-area/scroll-area.test.ts +14 -2
- package/dist/ui/components/scroll-area/scroll-area.ts +15 -3
- package/dist/ui/components/search-field/search-field.test.ts +25 -0
- package/dist/ui/components/search-field/search-field.ts +7 -1
- package/dist/ui/components/select/select.test.ts +32 -5
- package/dist/ui/components/select/select.ts +32 -11
- package/dist/ui/components/separator/separator.test.ts +3 -0
- package/dist/ui/components/sheet/sheet.test.ts +18 -0
- package/dist/ui/components/sheet/sheet.ts +24 -24
- package/dist/ui/components/skeleton/skeleton.test.ts +8 -0
- package/dist/ui/components/skeleton/skeleton.ts +13 -8
- package/dist/ui/components/slider/slider.test.ts +13 -1
- package/dist/ui/components/slider/slider.ts +16 -3
- package/dist/ui/components/spinner/spinner.test.ts +16 -1
- package/dist/ui/components/spinner/spinner.ts +15 -7
- package/dist/ui/components/switch/switch.test.ts +13 -2
- package/dist/ui/components/switch/switch.ts +15 -1
- package/dist/ui/components/tab-bar/tab-bar.test.ts +32 -6
- package/dist/ui/components/tab-bar/tab-bar.ts +28 -24
- package/dist/ui/components/tabs/tabs.test.ts +34 -32
- package/dist/ui/components/tabs/tabs.ts +56 -73
- package/dist/ui/components/textarea/textarea.test.ts +11 -1
- package/dist/ui/components/textarea/textarea.ts +77 -71
- package/dist/ui/components/toggle/toggle.test.ts +47 -16
- package/dist/ui/components/toggle/toggle.ts +78 -80
- package/dist/ui/components/typography/typography.test.ts +16 -0
- package/dist/ui/components/typography/typography.ts +12 -4
- package/package.json +1 -1
|
@@ -27,8 +27,14 @@ import { type IconName, ICONS } from '../icon/icons';
|
|
|
27
27
|
*/
|
|
28
28
|
@Component({
|
|
29
29
|
selector: 'ui-tab-bar',
|
|
30
|
-
imports: [LYNX_ELEMENTS],
|
|
31
30
|
encapsulation: ViewEncapsulation.None,
|
|
31
|
+
// The bar and each item are their host elements, styled through `host`,
|
|
32
|
+
// rather than a view inside the host. Angular gives a component's host
|
|
33
|
+
// every static attribute the app writes on it, so `<ui-tab-bar
|
|
34
|
+
// class="pt-2">` puts `pt-2` on the host whatever the component does with
|
|
35
|
+
// its `class` input: with a styled view inside as well, the padding applied
|
|
36
|
+
// twice.
|
|
37
|
+
//
|
|
32
38
|
// `pb-safe` pads the bar by the bottom safe-area inset, so the items sit
|
|
33
39
|
// above the iPhone's home indicator while the bar's background still runs
|
|
34
40
|
// to the bottom edge, as a native tab bar's does. Lynx reports no insets on
|
|
@@ -38,11 +44,11 @@ import { type IconName, ICONS } from '../icon/icons';
|
|
|
38
44
|
// native tab bar's container has: it tells VoiceOver that the buttons in it
|
|
39
45
|
// are tabs. Android reads a single trait and has no tab bar trait, so there
|
|
40
46
|
// the row stays a plain container.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
host: {
|
|
48
|
+
'accessibility-traits': 'tabbar',
|
|
49
|
+
'[class]': 'barClass()',
|
|
50
|
+
},
|
|
51
|
+
template: '<ng-content />',
|
|
46
52
|
})
|
|
47
53
|
export class UiTabBar {
|
|
48
54
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
@@ -97,7 +103,7 @@ export class UiTabBar {
|
|
|
97
103
|
// `flex-1` has to be on the host, the row's real child: on an inner view
|
|
98
104
|
// it would share out the host's width instead (same lesson as
|
|
99
105
|
// ui-nav-drawer-content).
|
|
100
|
-
class: '
|
|
106
|
+
'[class]': 'contentClass()',
|
|
101
107
|
// One accessibility element per tab, named by its label, so a screen
|
|
102
108
|
// reader reads "Inbox, 3, button" instead of an unlabeled SVG and a word.
|
|
103
109
|
// LynxRouterLinkActive adds `selected` to `button` while the route is
|
|
@@ -122,25 +128,23 @@ export class UiTabBar {
|
|
|
122
128
|
'(bindtap)': 'pressed.emit()',
|
|
123
129
|
},
|
|
124
130
|
template: `
|
|
125
|
-
<view
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
<view class="relative">
|
|
132
|
+
<svg [attr.content]="iconSvg()" style="width: 24px; height: 24px;" />
|
|
133
|
+
@if (badgeText()) {
|
|
134
|
+
<!-- Hangs off the icon's top-right corner, as on a native tab bar.
|
|
135
|
+
min-w-4 keeps a one-digit badge round; longer ones grow into
|
|
136
|
+
a pill. -->
|
|
137
|
+
<view
|
|
138
|
+
class="h-4 min-w-4 items-center rounded-full bg-destructive px-1 flex absolute -right-2.5 -top-1 justify-center"
|
|
139
|
+
>
|
|
140
|
+
<text
|
|
141
|
+
class="text-[10px] font-semibold leading-4 text-destructive-foreground"
|
|
142
|
+
>{{ badgeText() }}</text
|
|
134
143
|
>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
>{{ badgeText() }}</text
|
|
138
|
-
>
|
|
139
|
-
</view>
|
|
140
|
-
}
|
|
141
|
-
</view>
|
|
142
|
-
<text [class]="labelClass()">{{ label() }}</text>
|
|
144
|
+
</view>
|
|
145
|
+
}
|
|
143
146
|
</view>
|
|
147
|
+
<text [class]="labelClass()">{{ label() }}</text>
|
|
144
148
|
`,
|
|
145
149
|
})
|
|
146
150
|
export class UiTabBarItem {
|
|
@@ -57,11 +57,11 @@ class TabsHost {
|
|
|
57
57
|
const renderTabs = async () => {
|
|
58
58
|
const result = await render(TabsHost);
|
|
59
59
|
/**
|
|
60
|
-
* A trigger
|
|
60
|
+
* A trigger: its host, which holds the tap listener, around its label.
|
|
61
61
|
*/
|
|
62
62
|
const trigger = (label: string) => result.getByText(label).parentElement!;
|
|
63
63
|
/**
|
|
64
|
-
* A panel
|
|
64
|
+
* A panel: its host, around its projected text.
|
|
65
65
|
*/
|
|
66
66
|
const panel = (text: string) => result.getByText(text).parentElement!;
|
|
67
67
|
return {
|
|
@@ -69,9 +69,8 @@ const renderTabs = async () => {
|
|
|
69
69
|
host: result.componentRef.instance as TabsHost,
|
|
70
70
|
trigger,
|
|
71
71
|
panel,
|
|
72
|
-
/** The display of a panel
|
|
73
|
-
displayOf: (text: string) =>
|
|
74
|
-
(panel(text).parentElement as HTMLElement).style.display,
|
|
72
|
+
/** The display of a panel, which shows or hides it. */
|
|
73
|
+
displayOf: (text: string) => panel(text).style.display,
|
|
75
74
|
tap: async (label: string) => {
|
|
76
75
|
fireEvent.tap(trigger(label));
|
|
77
76
|
await waitForUpdate();
|
|
@@ -87,8 +86,6 @@ const CANCEL = 3;
|
|
|
87
86
|
describe('UiTabs', () => {
|
|
88
87
|
it("merges each part's class with its own", async () => {
|
|
89
88
|
const { container, trigger, panel } = await renderTabs();
|
|
90
|
-
// Each host element carries the given class too, so look for the view
|
|
91
|
-
// that has both its own and the given class.
|
|
92
89
|
expect(container.querySelector('.flex-col.w-80')).not.toBeNull();
|
|
93
90
|
expect(container.querySelector('.rounded-lg.mb-2')).not.toBeNull();
|
|
94
91
|
expect(
|
|
@@ -100,6 +97,21 @@ describe('UiTabs', () => {
|
|
|
100
97
|
expect(panel('Account panel').classList).toContain('gap-2');
|
|
101
98
|
});
|
|
102
99
|
|
|
100
|
+
// Angular gives a component's host every static attribute written on it,
|
|
101
|
+
// so a part that also styled a view inside its host applied the class
|
|
102
|
+
// twice (issue #181).
|
|
103
|
+
it('puts a class written on a part on that one element', async () => {
|
|
104
|
+
const { container, trigger, panel } = await renderTabs();
|
|
105
|
+
expect(container.querySelectorAll('.w-80')).toHaveLength(1);
|
|
106
|
+
expect(container.querySelectorAll('.mb-2')).toHaveLength(1);
|
|
107
|
+
expect([...container.querySelectorAll('.px-4')]).toEqual([
|
|
108
|
+
trigger('Account'),
|
|
109
|
+
]);
|
|
110
|
+
expect([...container.querySelectorAll('.gap-2')]).toEqual([
|
|
111
|
+
panel('Account panel'),
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
|
|
103
115
|
it("shows only the selected tab's panel, and highlights its trigger", async () => {
|
|
104
116
|
const { getByText, trigger, displayOf } = await renderTabs();
|
|
105
117
|
expect(displayOf('Account panel')).toBe('flex');
|
|
@@ -140,9 +152,8 @@ describe('UiTabs', () => {
|
|
|
140
152
|
|
|
141
153
|
it('clips a hidden panel, whose text iOS would draw from its corner', async () => {
|
|
142
154
|
const { panel } = await renderTabs();
|
|
143
|
-
|
|
144
|
-
expect(
|
|
145
|
-
expect(hostOf('Account panel').style.overflow).toBe('');
|
|
155
|
+
expect(panel('Billing panel').style.overflow).toBe('hidden');
|
|
156
|
+
expect(panel('Account panel').style.overflow).toBe('');
|
|
146
157
|
});
|
|
147
158
|
|
|
148
159
|
it('shows the tab its value is set to, without reporting a change', async () => {
|
|
@@ -287,10 +298,7 @@ describe('UiTabs', () => {
|
|
|
287
298
|
await waitForUpdate();
|
|
288
299
|
expect(animate).not.toHaveBeenCalled();
|
|
289
300
|
expect(
|
|
290
|
-
(
|
|
291
|
-
result.getByText('Billing panel').parentElement!
|
|
292
|
-
.parentElement as HTMLElement
|
|
293
|
-
).style.display,
|
|
301
|
+
result.getByText('Billing panel').parentElement!.style.display,
|
|
294
302
|
).toBe('flex');
|
|
295
303
|
});
|
|
296
304
|
|
|
@@ -385,14 +393,10 @@ describe('UiTabsList, segmented', () => {
|
|
|
385
393
|
providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
|
|
386
394
|
});
|
|
387
395
|
/**
|
|
388
|
-
* A trigger's
|
|
396
|
+
* A trigger: its host, the track's flex item, around its label.
|
|
389
397
|
*/
|
|
390
398
|
const trigger = (label: string) => result.getByText(label).parentElement!;
|
|
391
|
-
|
|
392
|
-
* A trigger's host, the track's flex item.
|
|
393
|
-
*/
|
|
394
|
-
const slot = (label: string) => trigger(label).parentElement!;
|
|
395
|
-
const track = () => slot('Photos').parentElement!;
|
|
399
|
+
const track = () => trigger('Photos').parentElement!;
|
|
396
400
|
/**
|
|
397
401
|
* The indicator's frame, which moves between the tabs.
|
|
398
402
|
*/
|
|
@@ -402,7 +406,6 @@ describe('UiTabsList, segmented', () => {
|
|
|
402
406
|
...result,
|
|
403
407
|
host: result.componentRef.instance as SegmentedTabsHost,
|
|
404
408
|
trigger,
|
|
405
|
-
slot,
|
|
406
409
|
track,
|
|
407
410
|
frame,
|
|
408
411
|
measure: async () => {
|
|
@@ -411,23 +414,22 @@ describe('UiTabsList, segmented', () => {
|
|
|
411
414
|
['Albums', 90],
|
|
412
415
|
['People', 180],
|
|
413
416
|
] as const) {
|
|
414
|
-
fireEvent.layoutchange(
|
|
417
|
+
fireEvent.layoutchange(trigger(label), { detail: { width } });
|
|
415
418
|
}
|
|
416
419
|
await waitForUpdate();
|
|
417
420
|
},
|
|
418
|
-
/** The display of a panel
|
|
421
|
+
/** The display of a panel, which shows or hides it. */
|
|
419
422
|
displayOf: (text: string) =>
|
|
420
|
-
|
|
421
|
-
.style.display,
|
|
423
|
+
result.getByText(text).parentElement!.style.display,
|
|
422
424
|
};
|
|
423
425
|
};
|
|
424
426
|
|
|
425
427
|
it("draws the platform's segmented control, filling its width, its tabs sharing it", async () => {
|
|
426
|
-
const { track,
|
|
428
|
+
const { track, trigger, frame, getByText } = await renderSegmented();
|
|
427
429
|
expect(track().classList).toContain('rounded-full');
|
|
428
430
|
expect(track().classList).not.toContain('self-start');
|
|
429
|
-
expect(
|
|
430
|
-
expect(
|
|
431
|
+
expect(trigger('Albums').classList).toContain('flex-1');
|
|
432
|
+
expect(trigger('People').classList).toContain('flex-[2]');
|
|
431
433
|
expect(trigger('Photos').classList).toContain('justify-center');
|
|
432
434
|
expect(getByText('Photos').classList).toContain('font-medium');
|
|
433
435
|
expect(getByText('Albums').classList).not.toContain('font-medium');
|
|
@@ -508,18 +510,18 @@ describe('UiTabsList, segmented', () => {
|
|
|
508
510
|
});
|
|
509
511
|
|
|
510
512
|
it('goes back to the default tabs when the variant changes', async () => {
|
|
511
|
-
const { host, track,
|
|
513
|
+
const { host, track, trigger, frame } = await renderSegmented();
|
|
512
514
|
host.variant.set('default');
|
|
513
515
|
await waitForUpdate();
|
|
514
516
|
expect(track().classList).toContain('self-start');
|
|
515
|
-
expect(
|
|
517
|
+
expect(trigger('Photos').classList).not.toContain('flex-1');
|
|
516
518
|
expect(frame()).toBeNull();
|
|
517
519
|
});
|
|
518
520
|
|
|
519
521
|
it('lets a trigger outside a list take touches as the default tabs do', async () => {
|
|
520
522
|
const result = await render(LoneTriggerHost);
|
|
521
523
|
const trigger = result.getByText('Alone').parentElement!;
|
|
522
|
-
fireEvent.layoutchange(trigger
|
|
524
|
+
fireEvent.layoutchange(trigger, { detail: { width: 80 } });
|
|
523
525
|
fireEvent.touchstart(trigger, { touches: [{ pageX: 10 }] });
|
|
524
526
|
fireEvent.touchmove(trigger, { touches: [{ pageX: 60 }] });
|
|
525
527
|
fireEvent.touchend(trigger);
|
|
@@ -528,6 +530,6 @@ describe('UiTabsList, segmented', () => {
|
|
|
528
530
|
fireEvent.tap(trigger);
|
|
529
531
|
await waitForUpdate();
|
|
530
532
|
expect(trigger.classList).toContain('bg-background');
|
|
531
|
-
expect(trigger.
|
|
533
|
+
expect(trigger.classList).not.toContain('flex-1');
|
|
532
534
|
});
|
|
533
535
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type ElementRef,
|
|
3
2
|
Component,
|
|
3
|
+
ElementRef,
|
|
4
4
|
ViewEncapsulation,
|
|
5
5
|
computed,
|
|
6
6
|
contentChildren,
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
model,
|
|
11
11
|
output,
|
|
12
12
|
signal,
|
|
13
|
-
viewChild,
|
|
14
13
|
} from '@angular/core';
|
|
15
14
|
import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
|
|
16
15
|
|
|
@@ -53,13 +52,16 @@ const tabsListHost = (): SegmentedHost => inject(UiTabsList).segmentedHost;
|
|
|
53
52
|
|
|
54
53
|
@Component({
|
|
55
54
|
selector: 'ui-tabs',
|
|
56
|
-
imports: [LYNX_ELEMENTS],
|
|
57
55
|
encapsulation: ViewEncapsulation.None,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
// Each part is its host element, styled through `host`, rather than a view
|
|
57
|
+
// inside the host. Angular gives a component's host every static attribute
|
|
58
|
+
// the app writes on it, so `<ui-tabs-content class="p-4">` puts `p-4` on
|
|
59
|
+
// the host whatever the component does with its `class` input: with a
|
|
60
|
+
// styled view inside as well, the padding applied twice. On the host alone,
|
|
61
|
+
// a class also acts where the part sits in its parent's layout (`flex-1`,
|
|
62
|
+
// margins).
|
|
63
|
+
host: { '[class]': 'containerClass()' },
|
|
64
|
+
template: '<ng-content />',
|
|
63
65
|
})
|
|
64
66
|
export class UiTabs {
|
|
65
67
|
readonly value = model<string>('');
|
|
@@ -139,7 +141,7 @@ export class UiTabs {
|
|
|
139
141
|
*/
|
|
140
142
|
@Component({
|
|
141
143
|
selector: 'ui-tabs-list',
|
|
142
|
-
imports: [
|
|
144
|
+
imports: [UiSegmentedIndicator],
|
|
143
145
|
encapsulation: ViewEncapsulation.None,
|
|
144
146
|
providers: [
|
|
145
147
|
UiSegmentedControl,
|
|
@@ -147,17 +149,16 @@ export class UiTabs {
|
|
|
147
149
|
],
|
|
148
150
|
// The mouse leaving the row lets go of an indicator dragged in the web
|
|
149
151
|
// preview, where a drag gets no touchcancel.
|
|
152
|
+
host: {
|
|
153
|
+
'accessibility-traits': 'tabbar',
|
|
154
|
+
'[class]': 'listClass()',
|
|
155
|
+
'(bindmouseleave)': 'segmentedControl.cancel()',
|
|
156
|
+
},
|
|
150
157
|
template: `
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
>
|
|
156
|
-
@if (segmentedControl.underlay()) {
|
|
157
|
-
<ui-segmented-indicator />
|
|
158
|
-
}
|
|
159
|
-
<ng-content />
|
|
160
|
-
</view>
|
|
158
|
+
@if (segmentedControl.underlay()) {
|
|
159
|
+
<ui-segmented-indicator />
|
|
160
|
+
}
|
|
161
|
+
<ng-content />
|
|
161
162
|
`,
|
|
162
163
|
})
|
|
163
164
|
export class UiTabsList {
|
|
@@ -208,9 +209,19 @@ export class UiTabsList {
|
|
|
208
209
|
imports: [LYNX_ELEMENTS],
|
|
209
210
|
encapsulation: ViewEncapsulation.None,
|
|
210
211
|
host: {
|
|
211
|
-
'[class]': '
|
|
212
|
+
'[class]': 'triggerClass()',
|
|
213
|
+
'[style]': 'triggerStyle()',
|
|
212
214
|
// The segmented control places its indicator from each tab's width.
|
|
213
215
|
'(bindlayoutchange)': 'measure($event)',
|
|
216
|
+
'(bindtap)': 'onTap()',
|
|
217
|
+
'(bindtouchstart)': 'press($event)',
|
|
218
|
+
'(bindtouchmove)': 'move($event)',
|
|
219
|
+
'(bindtouchend)': 'release()',
|
|
220
|
+
'(bindtouchcancel)': 'cancel()',
|
|
221
|
+
'(bindmousedown)': 'press($event)',
|
|
222
|
+
'(bindmousemove)': 'move($event)',
|
|
223
|
+
'(bindmouseup)': 'release()',
|
|
224
|
+
'(bindmouseleave)': 'leave()',
|
|
214
225
|
},
|
|
215
226
|
// The label is the accessibility element, a button (a tab, in the list's
|
|
216
227
|
// tab bar) named by its own text. A view would need a label of its own.
|
|
@@ -218,30 +229,15 @@ export class UiTabsList {
|
|
|
218
229
|
// lynxAccessibilityTraits adds the trait to `button` on iOS, and leaves
|
|
219
230
|
// Android, which reads one trait, with `button`.
|
|
220
231
|
template: `
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
(bindmousedown)="press($event)"
|
|
231
|
-
(bindmousemove)="move($event)"
|
|
232
|
-
(bindmouseup)="release()"
|
|
233
|
-
(bindmouseleave)="leave()"
|
|
234
|
-
>
|
|
235
|
-
@if (stateLayerClass(); as stateLayer) {
|
|
236
|
-
<view [class]="stateLayer"></view>
|
|
237
|
-
}
|
|
238
|
-
<text
|
|
239
|
-
accessibility-traits="button"
|
|
240
|
-
[lynxAccessibilityTraits]="{ selected: isActive() }"
|
|
241
|
-
[class]="textClass()"
|
|
242
|
-
><ng-content
|
|
243
|
-
/></text>
|
|
244
|
-
</view>
|
|
232
|
+
@if (stateLayerClass(); as stateLayer) {
|
|
233
|
+
<view [class]="stateLayer"></view>
|
|
234
|
+
}
|
|
235
|
+
<text
|
|
236
|
+
accessibility-traits="button"
|
|
237
|
+
[lynxAccessibilityTraits]="{ selected: isActive() }"
|
|
238
|
+
[class]="textClass()"
|
|
239
|
+
><ng-content
|
|
240
|
+
/></text>
|
|
245
241
|
`,
|
|
246
242
|
})
|
|
247
243
|
export class UiTabsTrigger {
|
|
@@ -254,7 +250,7 @@ export class UiTabsTrigger {
|
|
|
254
250
|
readonly triggerValue = input.required<string>({ alias: 'value' });
|
|
255
251
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
256
252
|
|
|
257
|
-
readonly
|
|
253
|
+
readonly #host = inject(ElementRef).nativeElement;
|
|
258
254
|
#anim?: AnimationHandle;
|
|
259
255
|
#previousActive?: boolean;
|
|
260
256
|
|
|
@@ -270,16 +266,6 @@ export class UiTabsTrigger {
|
|
|
270
266
|
this.#segmented?.enabled() ? this.#segmented : null,
|
|
271
267
|
);
|
|
272
268
|
|
|
273
|
-
/**
|
|
274
|
-
* A segment's host is the track's flex item, an equal share of it. Angular
|
|
275
|
-
* puts the app's static `class` on the host as well as on the trigger, so
|
|
276
|
-
* the two are merged here: `class="flex-[2]"` makes a wider tab.
|
|
277
|
-
*/
|
|
278
|
-
protected readonly hostClass = computed(() => {
|
|
279
|
-
const segment = this.#segment();
|
|
280
|
-
return segment ? cn(segment.segmentHostClass, this.userClass()) : '';
|
|
281
|
-
});
|
|
282
|
-
|
|
283
269
|
protected readonly triggerStyle = computed(
|
|
284
270
|
() => this.#segment()?.segmentStyle(this.triggerValue()) ?? null,
|
|
285
271
|
);
|
|
@@ -296,7 +282,7 @@ export class UiTabsTrigger {
|
|
|
296
282
|
/** Animates the trigger's background when it's selected or deselected. */
|
|
297
283
|
protected readonly animatingSelection = effect(() => {
|
|
298
284
|
const active = this.isActive();
|
|
299
|
-
const el = this
|
|
285
|
+
const el = this.#host;
|
|
300
286
|
const wasActive = this.#previousActive;
|
|
301
287
|
this.#previousActive = active;
|
|
302
288
|
// The first run only records the starting state, and only a change has
|
|
@@ -348,8 +334,12 @@ export class UiTabsTrigger {
|
|
|
348
334
|
const segment = this.#segment();
|
|
349
335
|
if (segment) {
|
|
350
336
|
// The list provides the segmented control, so a segment is in one.
|
|
337
|
+
// The host is both the segment's slot in the track (an equal share of
|
|
338
|
+
// it) and its own view, so an app's `class="flex-[2]"` makes a wider
|
|
339
|
+
// tab.
|
|
351
340
|
const place = segmentPlace(this.#list!.triggers(), this);
|
|
352
341
|
return cn(
|
|
342
|
+
segment.segmentHostClass,
|
|
353
343
|
segment.segmentClass(this.triggerValue(), place),
|
|
354
344
|
this.userClass(),
|
|
355
345
|
);
|
|
@@ -420,34 +410,28 @@ export class UiTabsTrigger {
|
|
|
420
410
|
|
|
421
411
|
@Component({
|
|
422
412
|
selector: 'ui-tabs-content',
|
|
423
|
-
imports: [LYNX_ELEMENTS],
|
|
424
413
|
encapsulation: ViewEncapsulation.None,
|
|
425
414
|
host: {
|
|
415
|
+
'[class]': 'contentClass()',
|
|
426
416
|
// Hide the WHOLE host when inactive so it reserves no layout space.
|
|
427
417
|
//
|
|
428
418
|
// The caller writes `class="flex-1"` on <ui-tabs-content>, and Angular
|
|
429
419
|
// applies that to the host element. All three content hosts live as siblings
|
|
430
420
|
// inside the flex-col <ui-tabs>, so three flex-1 hosts each grow to fill a
|
|
431
421
|
// third of the available height — pushing each panel lower than the last
|
|
432
|
-
// (the "Done tab starts at the very bottom" stacking).
|
|
433
|
-
// view doesn't help: the flex-1 host still claims its third.
|
|
422
|
+
// (the "Done tab starts at the very bottom" stacking).
|
|
434
423
|
//
|
|
435
424
|
// Toggling the host's own `display` collapses inactive hosts to 0×0 (Lynx
|
|
436
425
|
// `display: none`) so they claim no space at all, while the single active
|
|
437
|
-
// host keeps its flex-1 and fills the tabs area.
|
|
438
|
-
//
|
|
426
|
+
// host keeps its flex-1 and fills the tabs area. The inline display wins
|
|
427
|
+
// over the `flex` in the host's classes.
|
|
439
428
|
'[style.display]': "isActive() ? 'flex' : 'none'",
|
|
440
|
-
'[style.flexDirection]': "'column'",
|
|
441
429
|
// A hidden panel's 0×0 doesn't stop iOS drawing its text: each line is
|
|
442
430
|
// laid out at 0×0 too, and its glyphs spill out from the panel's corner,
|
|
443
431
|
// over the tab list. Clipping the hidden host to its 0×0 hides them.
|
|
444
432
|
'[style.overflow]': "isActive() ? null : 'hidden'",
|
|
445
433
|
},
|
|
446
|
-
template:
|
|
447
|
-
<view #content [class]="contentClass()">
|
|
448
|
-
<ng-content />
|
|
449
|
-
</view>
|
|
450
|
-
`,
|
|
434
|
+
template: '<ng-content />',
|
|
451
435
|
})
|
|
452
436
|
export class UiTabsContent {
|
|
453
437
|
readonly #tabs = inject(UiTabs);
|
|
@@ -456,7 +440,7 @@ export class UiTabsContent {
|
|
|
456
440
|
readonly contentValue = input.required<string>({ alias: 'value' });
|
|
457
441
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
458
442
|
|
|
459
|
-
readonly
|
|
443
|
+
readonly #host = inject(ElementRef).nativeElement;
|
|
460
444
|
#anim?: AnimationHandle;
|
|
461
445
|
#isFirstRender = true;
|
|
462
446
|
|
|
@@ -476,8 +460,7 @@ export class UiTabsContent {
|
|
|
476
460
|
* that vanished and never came back. Keeping every panel mounted and only
|
|
477
461
|
* toggling `display` never destroys or re-parents anything: inactive panels
|
|
478
462
|
* collapse to 0×0 (Lynx `display: none`) so they take no space, and the
|
|
479
|
-
* active one simply reappears in place.
|
|
480
|
-
* always exists, so `contentRef` is required.
|
|
463
|
+
* active one simply reappears in place.
|
|
481
464
|
*/
|
|
482
465
|
protected readonly revealing = effect(() => {
|
|
483
466
|
if (!this.isActive()) return;
|
|
@@ -485,7 +468,7 @@ export class UiTabsContent {
|
|
|
485
468
|
setTimeout(() => {
|
|
486
469
|
// Under Reduce Motion the panel just appears.
|
|
487
470
|
if (this.#settings.reduceMotion()) return;
|
|
488
|
-
const el = this
|
|
471
|
+
const el = this.#host;
|
|
489
472
|
|
|
490
473
|
this.#anim?.cancel();
|
|
491
474
|
|
|
@@ -504,8 +487,8 @@ export class UiTabsContent {
|
|
|
504
487
|
}, 0);
|
|
505
488
|
});
|
|
506
489
|
|
|
507
|
-
// Visibility is controlled
|
|
508
|
-
//
|
|
490
|
+
// Visibility is controlled by the host's inline display (see `host`); its
|
|
491
|
+
// classes make it a flex column when shown.
|
|
509
492
|
protected readonly contentClass = computed(() =>
|
|
510
493
|
cn('mt-2 flex-col flex', this.userClass()),
|
|
511
494
|
);
|
|
@@ -56,7 +56,7 @@ const renderTextarea = async () => {
|
|
|
56
56
|
...result,
|
|
57
57
|
host: result.componentRef.instance as TextareaHost,
|
|
58
58
|
field: () => result.container.querySelector('textarea')!,
|
|
59
|
-
// The field's
|
|
59
|
+
// The field's column: its host.
|
|
60
60
|
column: () => result.container.querySelector('.flex-col')!,
|
|
61
61
|
wrapper: () => result.container.querySelector('.bg-muted') as HTMLElement,
|
|
62
62
|
// The layer that draws the focus ring around the field.
|
|
@@ -84,6 +84,16 @@ describe('UiTextarea', () => {
|
|
|
84
84
|
expect(queryByText('Add a note')).toBeNull();
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
// Angular gives a component's host every static attribute written on it
|
|
88
|
+
// (issue #181): the class styled the column twice, and the placeholder made
|
|
89
|
+
// a second match for getByPlaceholderText.
|
|
90
|
+
it('puts its class and placeholder on one element each', async () => {
|
|
91
|
+
const { container, field, getAllByPlaceholderText } =
|
|
92
|
+
await renderTextarea();
|
|
93
|
+
expect(container.querySelectorAll('.w-full')).toHaveLength(1);
|
|
94
|
+
expect(getAllByPlaceholderText('Add a note')).toEqual([field()]);
|
|
95
|
+
});
|
|
96
|
+
|
|
87
97
|
it('shows its label', async () => {
|
|
88
98
|
const { host, getByText } = await renderTextarea();
|
|
89
99
|
host.label.set('Notes');
|