@blotch/dolan 22.2.6 → 22.2.7
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 +29 -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 +17 -0
- package/dist/ui/components/icon/icon.ts +19 -17
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NgTemplateOutlet } from '@angular/common';
|
|
2
2
|
import {
|
|
3
|
-
type ElementRef,
|
|
4
3
|
Component,
|
|
4
|
+
ElementRef,
|
|
5
5
|
Injector,
|
|
6
6
|
ViewEncapsulation,
|
|
7
7
|
booleanAttribute,
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
input,
|
|
12
12
|
output,
|
|
13
13
|
signal,
|
|
14
|
-
viewChild,
|
|
15
14
|
} from '@angular/core';
|
|
16
15
|
import {
|
|
17
16
|
LYNX_ELEMENTS,
|
|
@@ -233,63 +232,69 @@ const buttonIconColor = () => inject(UiButton).foreground;
|
|
|
233
232
|
// inside the template provides one too, but only for the template's own
|
|
234
233
|
// nodes.
|
|
235
234
|
providers: [{ provide: UI_ICON_COLOR, useFactory: buttonIconColor }],
|
|
235
|
+
// The host is the button's box. Angular gives a component's host every
|
|
236
|
+
// static attribute the app writes on it, so a class on <ui-button> lands
|
|
237
|
+
// there whatever the component does: on a box inside the host as well, a
|
|
238
|
+
// margin or padding applied twice. An accessibility-label lands there too,
|
|
239
|
+
// which is where it belongs: the box is the button a screen reader reads.
|
|
240
|
+
//
|
|
241
|
+
// Press feedback binds BOTH touch and mouse events on purpose. Native
|
|
242
|
+
// (iOS/Android) only ever fires touch; Lynx web runs in the browser, where a
|
|
243
|
+
// cursor press fires mousedown/up and NEVER touchstart — so touch-only
|
|
244
|
+
// bindings gave zero visual feedback on web while (bindtap) still fired the
|
|
245
|
+
// action (a "the button works but doesn't react" split). Binding both makes
|
|
246
|
+
// the press-and-hold identical on every platform; mouse events are inert on
|
|
247
|
+
// touch devices, so there's no double-fire. The scale animation itself lives
|
|
248
|
+
// in pressDown/pressRelease — see their JSDoc for why it's a CSS transition
|
|
249
|
+
// (which serializes to the DOM on Lynx web) and not el.animate() (whose Web
|
|
250
|
+
// Animation runs on web-core's offscreen node and never reaches the screen).
|
|
251
|
+
//
|
|
252
|
+
// An icon-only button has no text for a screen reader to read, so with an
|
|
253
|
+
// accessibility-label the whole button becomes one element that says it.
|
|
254
|
+
// Without one, the label's own text is read, as a button. The attributes
|
|
255
|
+
// are attribute bindings: a property binding on a component's host is
|
|
256
|
+
// checked against the DOM schema, which has no `accessibility-label`.
|
|
257
|
+
host: {
|
|
258
|
+
'[class]': 'containerClass()',
|
|
259
|
+
'[attr.accessibility-element]': "accessibilityLabel() ? 'true' : null",
|
|
260
|
+
'[attr.accessibility-traits]': "accessibilityLabel() ? 'button' : null",
|
|
261
|
+
'[attr.accessibility-label]': 'accessibilityLabel() || null',
|
|
262
|
+
'(bindtouchstart)': 'onPressStart()',
|
|
263
|
+
'(bindtouchend)': 'onPressEnd()',
|
|
264
|
+
'(bindtouchcancel)': 'onPressCancel()',
|
|
265
|
+
'(bindmousedown)': 'onPressStart()',
|
|
266
|
+
'(bindmouseup)': 'onPressEnd()',
|
|
267
|
+
'(bindmouseleave)': 'onPressCancel()',
|
|
268
|
+
'(bindtap)': 'onTap()',
|
|
269
|
+
},
|
|
236
270
|
template: `
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
(
|
|
260
|
-
|
|
261
|
-
(bindmouseup)="onPressEnd()"
|
|
262
|
-
(bindmouseleave)="onPressCancel()"
|
|
263
|
-
(bindtap)="onTap()"
|
|
264
|
-
>
|
|
265
|
-
@if (look() === 'glass') {
|
|
266
|
-
<!-- The container is the button's box, and the glass fills it. Under
|
|
267
|
-
clear glass the container is the dimming layer (see dimmed).
|
|
268
|
-
The content sits in the glass, where iOS's interactive glass
|
|
269
|
-
reacts to touches on it, and is what dims when disabled, as a
|
|
270
|
-
disabled SwiftUI glass button dims its label: glass under a
|
|
271
|
-
faded parent turns into a washed-out patch on iOS. -->
|
|
272
|
-
<blur-view
|
|
273
|
-
ui-glass
|
|
274
|
-
[variant]="variant() === 'glass-clear' ? 'clear' : 'regular'"
|
|
275
|
-
material="thin"
|
|
276
|
-
[tint]="
|
|
277
|
-
variant() === 'glass-prominent' ? 'var(--glass-tint)' : undefined
|
|
278
|
-
"
|
|
279
|
-
[interactive]="interactive()"
|
|
280
|
-
[class]="glassClass()"
|
|
281
|
-
>
|
|
282
|
-
<view [class]="glassContentClass()">
|
|
283
|
-
<ng-container [ngTemplateOutlet]="content" />
|
|
284
|
-
</view>
|
|
285
|
-
</blur-view>
|
|
286
|
-
} @else {
|
|
287
|
-
@if (stateLayerClass(); as layer) {
|
|
288
|
-
<view [class]="layer"></view>
|
|
289
|
-
}
|
|
290
|
-
<ng-container [ngTemplateOutlet]="content" />
|
|
271
|
+
@if (look() === 'glass') {
|
|
272
|
+
<!-- The host is the button's box, and the glass fills it. Under
|
|
273
|
+
clear glass the host is the dimming layer (see dimmed).
|
|
274
|
+
The content sits in the glass, where iOS's interactive glass
|
|
275
|
+
reacts to touches on it, and is what dims when disabled, as a
|
|
276
|
+
disabled SwiftUI glass button dims its label: glass under a
|
|
277
|
+
faded parent turns into a washed-out patch on iOS. -->
|
|
278
|
+
<blur-view
|
|
279
|
+
ui-glass
|
|
280
|
+
[variant]="variant() === 'glass-clear' ? 'clear' : 'regular'"
|
|
281
|
+
material="thin"
|
|
282
|
+
[tint]="
|
|
283
|
+
variant() === 'glass-prominent' ? 'var(--glass-tint)' : undefined
|
|
284
|
+
"
|
|
285
|
+
[interactive]="interactive()"
|
|
286
|
+
[class]="glassClass()"
|
|
287
|
+
>
|
|
288
|
+
<view [class]="glassContentClass()">
|
|
289
|
+
<ng-container [ngTemplateOutlet]="content" />
|
|
290
|
+
</view>
|
|
291
|
+
</blur-view>
|
|
292
|
+
} @else {
|
|
293
|
+
@if (stateLayerClass(); as layer) {
|
|
294
|
+
<view [class]="layer"></view>
|
|
291
295
|
}
|
|
292
|
-
|
|
296
|
+
<ng-container [ngTemplateOutlet]="content" />
|
|
297
|
+
}
|
|
293
298
|
|
|
294
299
|
<!-- One copy of the content, for whichever form the button takes: Angular
|
|
295
300
|
projects content once, so it can't be in both branches above.
|
|
@@ -340,13 +345,13 @@ export class UiButton {
|
|
|
340
345
|
|
|
341
346
|
readonly pressed = output<void>();
|
|
342
347
|
|
|
343
|
-
readonly containerRef = viewChild<ElementRef>('container');
|
|
344
348
|
/**
|
|
345
349
|
* A `<ui-icon>` projected into the button, which sits before the label, or
|
|
346
350
|
* after it when it's marked `trailing`.
|
|
347
351
|
*/
|
|
348
352
|
readonly projectedIcon = contentChild(iconType());
|
|
349
353
|
|
|
354
|
+
readonly #host = inject(ElementRef).nativeElement;
|
|
350
355
|
readonly #settings = inject(LynxAccessibilitySettings);
|
|
351
356
|
readonly #design = injectGlassDesign();
|
|
352
357
|
readonly #systemDrawsGlass = injectSystemDrawsGlass();
|
|
@@ -417,7 +422,7 @@ export class UiButton {
|
|
|
417
422
|
/**
|
|
418
423
|
* Clear glass sits on a dark layer at about a third, so its white label
|
|
419
424
|
* stays legible over bright photos and video: Apple's rule for clear
|
|
420
|
-
* glass. The layer is the
|
|
425
|
+
* glass. The layer is the host, under the glass.
|
|
421
426
|
*/
|
|
422
427
|
readonly #dimmed = computed(
|
|
423
428
|
() => this.variant() === 'glass-clear' && this.#seeThrough(),
|
|
@@ -534,9 +539,9 @@ export class UiButton {
|
|
|
534
539
|
this.pressing.set(true);
|
|
535
540
|
if (this.#feedback() !== 'scale') return;
|
|
536
541
|
// pressDown/pressRelease drive el.setStyle() based CSS transitions on the
|
|
537
|
-
//
|
|
542
|
+
// host (see pressDown for why it isn't el.animate()).
|
|
538
543
|
this.#pressAnim?.cancel();
|
|
539
|
-
this.#pressAnim = pressDown(this
|
|
544
|
+
this.#pressAnim = pressDown(this.#host);
|
|
540
545
|
}
|
|
541
546
|
|
|
542
547
|
protected onPressEnd(): void {
|
|
@@ -560,6 +565,6 @@ export class UiButton {
|
|
|
560
565
|
this.pressing.set(false);
|
|
561
566
|
if (this.#feedback() !== 'scale') return;
|
|
562
567
|
this.#pressAnim?.cancel();
|
|
563
|
-
this.#pressAnim = pressRelease(this
|
|
568
|
+
this.#pressAnim = pressRelease(this.#host);
|
|
564
569
|
}
|
|
565
570
|
}
|
|
@@ -61,6 +61,18 @@ class ButtonGroupHost {
|
|
|
61
61
|
readonly presses: string[] = [];
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
@Component({
|
|
65
|
+
imports: [UiButtonGroup, UiButtonGroupItem, UiIcon],
|
|
66
|
+
template: `
|
|
67
|
+
<ui-button-group size="icon">
|
|
68
|
+
<ui-button-group-item accessibility-label="Copy">
|
|
69
|
+
<ui-icon name="copy" />
|
|
70
|
+
</ui-button-group-item>
|
|
71
|
+
</ui-button-group>
|
|
72
|
+
`,
|
|
73
|
+
})
|
|
74
|
+
class StaticLabelHost {}
|
|
75
|
+
|
|
64
76
|
/** Accessibility settings a test can change, as the host app would. */
|
|
65
77
|
const settings = {
|
|
66
78
|
reduceMotion: signal(false),
|
|
@@ -87,16 +99,15 @@ const renderGroup = async (
|
|
|
87
99
|
...result,
|
|
88
100
|
host: result.componentRef.instance as ButtonGroupHost,
|
|
89
101
|
/**
|
|
90
|
-
* The group's
|
|
102
|
+
* The group's box, its host, around the items: the bar of the solid
|
|
91
103
|
* variants, Material's connected group, or the glass's frame.
|
|
92
104
|
*/
|
|
93
105
|
group: (): HTMLElement => {
|
|
94
|
-
const parent =
|
|
95
|
-
result.getByText('Paste').parentElement!.parentElement!.parentElement!;
|
|
106
|
+
const parent = result.getByText('Paste').parentElement!.parentElement!;
|
|
96
107
|
return parent.localName === 'blur-view' ? parent.parentElement! : parent;
|
|
97
108
|
},
|
|
98
109
|
glass: () => result.container.querySelector<HTMLElement>('blur-view')!,
|
|
99
|
-
// An item
|
|
110
|
+
// An item (its host, which takes presses) holds its label.
|
|
100
111
|
item: (label: string) => result.getByText(label).parentElement!,
|
|
101
112
|
icon: () => result.container.querySelector('svg')!,
|
|
102
113
|
update: async (change: () => void) => {
|
|
@@ -141,6 +152,28 @@ afterEach(() => {
|
|
|
141
152
|
});
|
|
142
153
|
|
|
143
154
|
describe('UiButtonGroup', () => {
|
|
155
|
+
// Angular gives a component's host every static attribute written on it,
|
|
156
|
+
// so a group or item that also styled a view inside its host applied the
|
|
157
|
+
// class twice, and the label named two elements (issue #181).
|
|
158
|
+
it('puts a class written on the group or an item on that one element', async () => {
|
|
159
|
+
const { container, group, item } = await renderGroup();
|
|
160
|
+
expect([...container.querySelectorAll('.w-full')]).toEqual([group()]);
|
|
161
|
+
expect([...container.querySelectorAll('.flex-1')]).toEqual([item('Copy')]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('names one element with an accessibility label written on an item', async () => {
|
|
165
|
+
vi.stubGlobal('SystemInfo', undefined);
|
|
166
|
+
const { container, getAllByAccessibilityLabel } = await render(
|
|
167
|
+
StaticLabelHost,
|
|
168
|
+
{
|
|
169
|
+
providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
expect(getAllByAccessibilityLabel('Copy')).toEqual([
|
|
173
|
+
container.querySelector('.h-10'),
|
|
174
|
+
]);
|
|
175
|
+
});
|
|
176
|
+
|
|
144
177
|
it('lays its items out in a row, or in a column when vertical', async () => {
|
|
145
178
|
const { host, group } = await renderGroup();
|
|
146
179
|
expect(group().classList).toContain('flex-row');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NgTemplateOutlet } from '@angular/common';
|
|
2
2
|
import {
|
|
3
|
-
type ElementRef,
|
|
4
3
|
Component,
|
|
4
|
+
ElementRef,
|
|
5
5
|
ViewEncapsulation,
|
|
6
6
|
booleanAttribute,
|
|
7
7
|
computed,
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
input,
|
|
12
12
|
output,
|
|
13
13
|
signal,
|
|
14
|
-
viewChild,
|
|
15
14
|
} from '@angular/core';
|
|
16
15
|
import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
|
|
17
16
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
@@ -174,28 +173,37 @@ const groupIconColor = () => inject(UiButtonGroup).foreground;
|
|
|
174
173
|
// Icons projected into the items inject this: they're declared inside the
|
|
175
174
|
// group, so the group's element injector is one of theirs.
|
|
176
175
|
providers: [{ provide: UI_ICON_COLOR, useFactory: groupIconColor }],
|
|
176
|
+
// The group and each item are their host elements, styled through `host`,
|
|
177
|
+
// rather than a view inside the host. Angular gives a component's host
|
|
178
|
+
// every static attribute the app writes on it, so `<ui-button-group
|
|
179
|
+
// class="mt-4">` puts `mt-4` on the host whatever the component does with
|
|
180
|
+
// its `class` input: with a styled view inside as well, the margin applied
|
|
181
|
+
// twice. On the host alone, a class also acts where the group sits in its
|
|
182
|
+
// parent's layout (`w-full`), and an item's accessibility-label names one
|
|
183
|
+
// element.
|
|
184
|
+
//
|
|
185
|
+
// Under glass the host is the frame: the group's box and, under clear
|
|
186
|
+
// glass, its dimming layer, in the glass's shape. Before iOS 26 the glass
|
|
187
|
+
// is the thin material, as the buttons' is.
|
|
188
|
+
host: {
|
|
189
|
+
'[class]': "look() === 'glass' ? frameClass() : containerClass()",
|
|
190
|
+
'[style.border-radius.px]': "look() === 'glass' ? cornerRadius() : null",
|
|
191
|
+
},
|
|
177
192
|
template: `
|
|
178
193
|
@if (look() === 'glass') {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
material="thin"
|
|
189
|
-
[interactive]="interactive()"
|
|
190
|
-
[class]="glassClass()"
|
|
191
|
-
>
|
|
192
|
-
<ng-container [ngTemplateOutlet]="items" />
|
|
193
|
-
</blur-view>
|
|
194
|
-
</view>
|
|
195
|
-
} @else {
|
|
196
|
-
<view [class]="containerClass()">
|
|
194
|
+
<blur-view
|
|
195
|
+
ui-glass
|
|
196
|
+
[shape]="capsule() ? 'capsule' : 'rounded'"
|
|
197
|
+
[radius]="radius()"
|
|
198
|
+
[variant]="variant() === 'glass-clear' ? 'clear' : 'regular'"
|
|
199
|
+
material="thin"
|
|
200
|
+
[interactive]="interactive()"
|
|
201
|
+
[class]="glassClass()"
|
|
202
|
+
>
|
|
197
203
|
<ng-container [ngTemplateOutlet]="items" />
|
|
198
|
-
</view>
|
|
204
|
+
</blur-view>
|
|
205
|
+
} @else {
|
|
206
|
+
<ng-container [ngTemplateOutlet]="items" />
|
|
199
207
|
}
|
|
200
208
|
|
|
201
209
|
<!-- One copy of the items, for whichever form the group takes: Angular
|
|
@@ -356,34 +364,35 @@ export class UiButtonGroup {
|
|
|
356
364
|
selector: 'ui-button-group-item',
|
|
357
365
|
imports: [LYNX_ELEMENTS],
|
|
358
366
|
encapsulation: ViewEncapsulation.None,
|
|
367
|
+
// The accessibility attributes are attribute bindings: a property binding
|
|
368
|
+
// on a component's host is checked against the DOM schema, which has no
|
|
369
|
+
// `accessibility-label`.
|
|
370
|
+
host: {
|
|
371
|
+
'[class]': 'containerClass()',
|
|
372
|
+
'[style]': 'cornerStyle()',
|
|
373
|
+
'[attr.accessibility-element]': "accessibilityLabel() ? 'true' : null",
|
|
374
|
+
'[attr.accessibility-traits]': "accessibilityLabel() ? 'button' : null",
|
|
375
|
+
'[attr.accessibility-label]': 'accessibilityLabel() || null',
|
|
376
|
+
'(bindtouchstart)': 'onPressStart()',
|
|
377
|
+
'(bindtouchend)': 'onPressEnd()',
|
|
378
|
+
'(bindtouchcancel)': 'onPressCancel()',
|
|
379
|
+
'(bindmousedown)': 'onPressStart()',
|
|
380
|
+
'(bindmouseup)': 'onPressEnd()',
|
|
381
|
+
'(bindmouseleave)': 'onPressCancel()',
|
|
382
|
+
'(bindtap)': 'onTap()',
|
|
383
|
+
},
|
|
359
384
|
template: `
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
(bindmouseup)="onPressEnd()"
|
|
372
|
-
(bindmouseleave)="onPressCancel()"
|
|
373
|
-
(bindtap)="onTap()"
|
|
374
|
-
>
|
|
375
|
-
@if (highlightClass(); as highlight) {
|
|
376
|
-
<view [class]="highlight" [style.border-radius.px]="radius()"></view>
|
|
377
|
-
}
|
|
378
|
-
<ng-content select="ui-icon" />
|
|
379
|
-
@if (showLabel()) {
|
|
380
|
-
<text
|
|
381
|
-
[attr.accessibility-traits]="accessibilityLabel() ? null : 'button'"
|
|
382
|
-
[class]="labelClass()"
|
|
383
|
-
><ng-content
|
|
384
|
-
/></text>
|
|
385
|
-
}
|
|
386
|
-
</view>
|
|
385
|
+
@if (highlightClass(); as highlight) {
|
|
386
|
+
<view [class]="highlight" [style.border-radius.px]="radius()"></view>
|
|
387
|
+
}
|
|
388
|
+
<ng-content select="ui-icon" />
|
|
389
|
+
@if (showLabel()) {
|
|
390
|
+
<text
|
|
391
|
+
[attr.accessibility-traits]="accessibilityLabel() ? null : 'button'"
|
|
392
|
+
[class]="labelClass()"
|
|
393
|
+
><ng-content
|
|
394
|
+
/></text>
|
|
395
|
+
}
|
|
387
396
|
`,
|
|
388
397
|
})
|
|
389
398
|
export class UiButtonGroupItem {
|
|
@@ -402,7 +411,7 @@ export class UiButtonGroupItem {
|
|
|
402
411
|
|
|
403
412
|
readonly pressed = output<void>();
|
|
404
413
|
|
|
405
|
-
readonly
|
|
414
|
+
readonly #host = inject(ElementRef).nativeElement;
|
|
406
415
|
/** A `<ui-icon>` projected into the item, which sits before the label. */
|
|
407
416
|
readonly projectedIcon = contentChild(iconType());
|
|
408
417
|
#pressAnim?: AnimationHandle;
|
|
@@ -531,7 +540,7 @@ export class UiButtonGroupItem {
|
|
|
531
540
|
if (!this.#scales()) return;
|
|
532
541
|
// See UiButton: press feedback is an el.setStyle()-driven CSS transition.
|
|
533
542
|
this.#pressAnim?.cancel();
|
|
534
|
-
this.#pressAnim = pressDown(this
|
|
543
|
+
this.#pressAnim = pressDown(this.#host);
|
|
535
544
|
}
|
|
536
545
|
|
|
537
546
|
protected onPressEnd(): void {
|
|
@@ -562,6 +571,6 @@ export class UiButtonGroupItem {
|
|
|
562
571
|
this.pressing.set(false);
|
|
563
572
|
if (!this.#scales()) return;
|
|
564
573
|
this.#pressAnim?.cancel();
|
|
565
|
-
this.#pressAnim = pressRelease(this
|
|
574
|
+
this.#pressAnim = pressRelease(this.#host);
|
|
566
575
|
}
|
|
567
576
|
}
|
|
@@ -46,9 +46,19 @@ class CardHost {
|
|
|
46
46
|
})
|
|
47
47
|
class BarePressableHost {}
|
|
48
48
|
|
|
49
|
+
@Component({
|
|
50
|
+
imports: [UiCardContent, UiCardTitle, LYNX_ELEMENTS],
|
|
51
|
+
template: `
|
|
52
|
+
<ui-card-content class="pt-6"><text>Body</text></ui-card-content>
|
|
53
|
+
<ui-card-content class="p-2"><text>Tight</text></ui-card-content>
|
|
54
|
+
<ui-card-title class="mb-2">Title</ui-card-title>
|
|
55
|
+
`,
|
|
56
|
+
})
|
|
57
|
+
class StaticClassHost {}
|
|
58
|
+
|
|
49
59
|
/**
|
|
50
|
-
* The card
|
|
51
|
-
*
|
|
60
|
+
* The card: its host, found by the card's class rather than by being the
|
|
61
|
+
* first view (the test host renders as a view too).
|
|
52
62
|
*/
|
|
53
63
|
const cardView = (container: Element) => container.querySelector('.bg-card')!;
|
|
54
64
|
|
|
@@ -88,6 +98,29 @@ describe('UiCard', () => {
|
|
|
88
98
|
expect(getByText('Desc').getAttribute('class')).toContain('italic');
|
|
89
99
|
});
|
|
90
100
|
|
|
101
|
+
// Angular gives a component's host every static attribute written on it,
|
|
102
|
+
// so a part that also styled a view inside its host applied the class
|
|
103
|
+
// twice: double the padding (issue #181).
|
|
104
|
+
it('puts a class written on a part on that one element', async () => {
|
|
105
|
+
const { container, getByText } = await render(StaticClassHost);
|
|
106
|
+
expect(container.querySelectorAll('.pt-6')).toHaveLength(1);
|
|
107
|
+
expect(container.querySelectorAll('.mb-2')).toHaveLength(1);
|
|
108
|
+
// The title is a single <text>, with the class.
|
|
109
|
+
expect(getByText('Title').tagName.toLowerCase()).toBe('text');
|
|
110
|
+
expect(getByText('Title').getAttribute('class')).toContain('mb-2');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("lets a part's class override its own", async () => {
|
|
114
|
+
const { getByText } = await render(StaticClassHost);
|
|
115
|
+
const tight = getByText('Tight').parentElement!;
|
|
116
|
+
expect(tight.getAttribute('class')).toContain('p-2');
|
|
117
|
+
expect(tight.getAttribute('class')).not.toContain('p-6');
|
|
118
|
+
const body = getByText('Body').parentElement!;
|
|
119
|
+
expect(body.getAttribute('class')).toContain('p-6');
|
|
120
|
+
expect(body.getAttribute('class')).toContain('pt-6');
|
|
121
|
+
expect(body.getAttribute('class')).not.toContain('pt-0');
|
|
122
|
+
});
|
|
123
|
+
|
|
91
124
|
it("doesn't respond to presses unless it's pressable", async () => {
|
|
92
125
|
const { container } = await render(CardHost);
|
|
93
126
|
const view = cardView(container);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type ElementRef,
|
|
3
2
|
Component,
|
|
3
|
+
ElementRef,
|
|
4
4
|
ViewEncapsulation,
|
|
5
5
|
booleanAttribute,
|
|
6
6
|
computed,
|
|
7
|
+
inject,
|
|
7
8
|
input,
|
|
8
|
-
viewChild,
|
|
9
9
|
} from '@angular/core';
|
|
10
|
-
import {
|
|
10
|
+
import { registerHostElement } from '@blotch/angular-lynx';
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
type AnimationHandle,
|
|
@@ -17,25 +17,29 @@ import {
|
|
|
17
17
|
} from '../../utils/animate';
|
|
18
18
|
import { cn } from '../../utils/cn';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Each part is its host element, styled through `host`, rather than a view
|
|
22
|
+
* (or text) inside the host. Angular gives a component's host every static
|
|
23
|
+
* attribute the app writes on it, so `<ui-card-content class="pt-6">` puts
|
|
24
|
+
* `pt-6` on the host whatever the component does with its `class` input: with
|
|
25
|
+
* a styled view inside as well, the padding applied twice. On the host alone,
|
|
26
|
+
* a class also acts where the part sits in its parent's layout (`flex-1`,
|
|
27
|
+
* margins). The title and description are text, so their hosts are `<text>`
|
|
28
|
+
* elements (registerHostElement, below).
|
|
29
|
+
*/
|
|
20
30
|
@Component({
|
|
21
31
|
selector: 'ui-card',
|
|
22
|
-
standalone: true,
|
|
23
|
-
imports: [LYNX_ELEMENTS],
|
|
24
32
|
encapsulation: ViewEncapsulation.None,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
>
|
|
36
|
-
<ng-content />
|
|
37
|
-
</view>
|
|
38
|
-
`,
|
|
33
|
+
host: {
|
|
34
|
+
'[class]': 'containerClass()',
|
|
35
|
+
'(bindtouchstart)': 'onPressStart()',
|
|
36
|
+
'(bindtouchend)': 'onPressEnd()',
|
|
37
|
+
'(bindtouchcancel)': 'onPressCancel()',
|
|
38
|
+
'(bindmousedown)': 'onPressStart()',
|
|
39
|
+
'(bindmouseup)': 'onPressEnd()',
|
|
40
|
+
'(bindmouseleave)': 'onPressCancel()',
|
|
41
|
+
},
|
|
42
|
+
template: '<ng-content />',
|
|
39
43
|
})
|
|
40
44
|
export class UiCard {
|
|
41
45
|
/**
|
|
@@ -44,7 +48,7 @@ export class UiCard {
|
|
|
44
48
|
readonly pressable = input(false, { transform: booleanAttribute });
|
|
45
49
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
46
50
|
|
|
47
|
-
readonly
|
|
51
|
+
readonly #host = inject(ElementRef).nativeElement;
|
|
48
52
|
#pressAnim?: AnimationHandle;
|
|
49
53
|
|
|
50
54
|
protected readonly containerClass = computed(() =>
|
|
@@ -54,16 +58,13 @@ export class UiCard {
|
|
|
54
58
|
protected onPressStart(): void {
|
|
55
59
|
if (!this.pressable()) return;
|
|
56
60
|
this.#pressAnim?.cancel();
|
|
57
|
-
this.#pressAnim = pressDown(
|
|
58
|
-
this.containerRef()?.nativeElement,
|
|
59
|
-
SCALE.pressDownLight,
|
|
60
|
-
);
|
|
61
|
+
this.#pressAnim = pressDown(this.#host, SCALE.pressDownLight);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
protected onPressEnd(): void {
|
|
64
65
|
if (!this.pressable()) return;
|
|
65
66
|
this.#pressAnim?.cancel();
|
|
66
|
-
this.#pressAnim = pressRelease(this
|
|
67
|
+
this.#pressAnim = pressRelease(this.#host);
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
protected onPressCancel(): void {
|
|
@@ -71,20 +72,15 @@ export class UiCard {
|
|
|
71
72
|
// mid-press would leave the card stuck in its pressed-down state.
|
|
72
73
|
if (!this.pressable()) return;
|
|
73
74
|
this.#pressAnim?.cancel();
|
|
74
|
-
this.#pressAnim = pressRelease(this
|
|
75
|
+
this.#pressAnim = pressRelease(this.#host);
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
@Component({
|
|
79
80
|
selector: 'ui-card-header',
|
|
80
|
-
standalone: true,
|
|
81
|
-
imports: [LYNX_ELEMENTS],
|
|
82
81
|
encapsulation: ViewEncapsulation.None,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<ng-content />
|
|
86
|
-
</view>
|
|
87
|
-
`,
|
|
82
|
+
host: { '[class]': 'containerClass()' },
|
|
83
|
+
template: '<ng-content />',
|
|
88
84
|
})
|
|
89
85
|
export class UiCardHeader {
|
|
90
86
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
@@ -96,10 +92,9 @@ export class UiCardHeader {
|
|
|
96
92
|
|
|
97
93
|
@Component({
|
|
98
94
|
selector: 'ui-card-title',
|
|
99
|
-
standalone: true,
|
|
100
|
-
imports: [LYNX_ELEMENTS],
|
|
101
95
|
encapsulation: ViewEncapsulation.None,
|
|
102
|
-
|
|
96
|
+
host: { '[class]': 'textClass()' },
|
|
97
|
+
template: '<ng-content />',
|
|
103
98
|
})
|
|
104
99
|
export class UiCardTitle {
|
|
105
100
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
@@ -109,12 +104,13 @@ export class UiCardTitle {
|
|
|
109
104
|
);
|
|
110
105
|
}
|
|
111
106
|
|
|
107
|
+
registerHostElement(UiCardTitle, 'text');
|
|
108
|
+
|
|
112
109
|
@Component({
|
|
113
110
|
selector: 'ui-card-description',
|
|
114
|
-
standalone: true,
|
|
115
|
-
imports: [LYNX_ELEMENTS],
|
|
116
111
|
encapsulation: ViewEncapsulation.None,
|
|
117
|
-
|
|
112
|
+
host: { '[class]': 'textClass()' },
|
|
113
|
+
template: '<ng-content />',
|
|
118
114
|
})
|
|
119
115
|
export class UiCardDescription {
|
|
120
116
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
@@ -124,16 +120,13 @@ export class UiCardDescription {
|
|
|
124
120
|
);
|
|
125
121
|
}
|
|
126
122
|
|
|
123
|
+
registerHostElement(UiCardDescription, 'text');
|
|
124
|
+
|
|
127
125
|
@Component({
|
|
128
126
|
selector: 'ui-card-content',
|
|
129
|
-
standalone: true,
|
|
130
|
-
imports: [LYNX_ELEMENTS],
|
|
131
127
|
encapsulation: ViewEncapsulation.None,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
<ng-content />
|
|
135
|
-
</view>
|
|
136
|
-
`,
|
|
128
|
+
host: { '[class]': 'containerClass()' },
|
|
129
|
+
template: '<ng-content />',
|
|
137
130
|
})
|
|
138
131
|
export class UiCardContent {
|
|
139
132
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
@@ -145,14 +138,9 @@ export class UiCardContent {
|
|
|
145
138
|
|
|
146
139
|
@Component({
|
|
147
140
|
selector: 'ui-card-footer',
|
|
148
|
-
standalone: true,
|
|
149
|
-
imports: [LYNX_ELEMENTS],
|
|
150
141
|
encapsulation: ViewEncapsulation.None,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<ng-content />
|
|
154
|
-
</view>
|
|
155
|
-
`,
|
|
142
|
+
host: { '[class]': 'containerClass()' },
|
|
143
|
+
template: '<ng-content />',
|
|
156
144
|
})
|
|
157
145
|
export class UiCardFooter {
|
|
158
146
|
readonly userClass = input<string>('', { alias: 'class' });
|