@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.
Files changed (91) hide show
  1. package/dist/commands/install-packages.test.js +7 -2
  2. package/dist/commands/install-packages.test.js.map +1 -1
  3. package/dist/registry.js +8 -2
  4. package/dist/registry.js.map +1 -1
  5. package/dist/registry.test.js +1 -0
  6. package/dist/registry.test.js.map +1 -1
  7. package/dist/ui/components/accordion/accordion.test.ts +24 -7
  8. package/dist/ui/components/accordion/accordion.ts +34 -44
  9. package/dist/ui/components/action-sheet/action-sheet.test.ts +18 -1
  10. package/dist/ui/components/action-sheet/action-sheet.ts +55 -60
  11. package/dist/ui/components/alert/alert.test.ts +17 -0
  12. package/dist/ui/components/alert/alert.ts +23 -14
  13. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +19 -4
  14. package/dist/ui/components/alert-dialog/alert-dialog.ts +31 -20
  15. package/dist/ui/components/aspect-ratio/aspect-ratio.test.ts +38 -2
  16. package/dist/ui/components/aspect-ratio/aspect-ratio.ts +11 -8
  17. package/dist/ui/components/avatar/avatar.test.ts +7 -0
  18. package/dist/ui/components/avatar/avatar.ts +35 -30
  19. package/dist/ui/components/badge/badge.test.ts +9 -0
  20. package/dist/ui/components/badge/badge.ts +12 -10
  21. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +7 -0
  22. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +5 -0
  23. package/dist/ui/components/button/button.test.ts +29 -3
  24. package/dist/ui/components/button/button.ts +67 -62
  25. package/dist/ui/components/button-group/button-group.test.ts +37 -4
  26. package/dist/ui/components/button-group/button-group.ts +60 -51
  27. package/dist/ui/components/card/card.test.ts +35 -2
  28. package/dist/ui/components/card/card.ts +41 -53
  29. package/dist/ui/components/chart/chart-presets.test.ts +16 -2
  30. package/dist/ui/components/chart/chart-presets.ts +22 -3
  31. package/dist/ui/components/chart/chart.test.ts +35 -6
  32. package/dist/ui/components/chart/chart.ts +14 -3
  33. package/dist/ui/components/checkbox/checkbox.test.ts +11 -4
  34. package/dist/ui/components/checkbox/checkbox.ts +21 -14
  35. package/dist/ui/components/collapsible/collapsible.test.ts +20 -10
  36. package/dist/ui/components/collapsible/collapsible.ts +29 -40
  37. package/dist/ui/components/dialog/dialog.test.ts +17 -1
  38. package/dist/ui/components/dialog/dialog.ts +24 -17
  39. package/dist/ui/components/dock/dock.test.ts +30 -5
  40. package/dist/ui/components/dock/dock.ts +29 -27
  41. package/dist/ui/components/empty-state/empty-state.test.ts +9 -0
  42. package/dist/ui/components/empty-state/empty-state.ts +20 -11
  43. package/dist/ui/components/icon/icon.test.ts +17 -0
  44. package/dist/ui/components/icon/icon.ts +19 -17
  45. package/dist/ui/components/input/input.test.ts +55 -3
  46. package/dist/ui/components/input/input.ts +91 -65
  47. package/dist/ui/components/label/label.test.ts +15 -0
  48. package/dist/ui/components/label/label.ts +12 -4
  49. package/dist/ui/components/list/list.test.ts +13 -3
  50. package/dist/ui/components/list/list.ts +14 -2
  51. package/dist/ui/components/menu/menu.test.ts +8 -0
  52. package/dist/ui/components/menu/menu.ts +4 -0
  53. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +34 -21
  54. package/dist/ui/components/nav-drawer/nav-drawer.ts +60 -68
  55. package/dist/ui/components/navigation-bar/navigation-bar.test.ts +13 -0
  56. package/dist/ui/components/navigation-bar/navigation-bar.ts +5 -0
  57. package/dist/ui/components/pagination/pagination.test.ts +9 -0
  58. package/dist/ui/components/pagination/pagination.ts +52 -49
  59. package/dist/ui/components/popover/popover.test.ts +3 -0
  60. package/dist/ui/components/progress/progress.test.ts +7 -0
  61. package/dist/ui/components/progress/progress.ts +8 -6
  62. package/dist/ui/components/radio-group/radio-group.test.ts +8 -0
  63. package/dist/ui/components/radio-group/radio-group.ts +22 -19
  64. package/dist/ui/components/scroll-area/scroll-area.test.ts +14 -2
  65. package/dist/ui/components/scroll-area/scroll-area.ts +15 -3
  66. package/dist/ui/components/search-field/search-field.test.ts +25 -0
  67. package/dist/ui/components/search-field/search-field.ts +7 -1
  68. package/dist/ui/components/select/select.test.ts +32 -5
  69. package/dist/ui/components/select/select.ts +32 -11
  70. package/dist/ui/components/separator/separator.test.ts +3 -0
  71. package/dist/ui/components/sheet/sheet.test.ts +18 -0
  72. package/dist/ui/components/sheet/sheet.ts +24 -24
  73. package/dist/ui/components/skeleton/skeleton.test.ts +8 -0
  74. package/dist/ui/components/skeleton/skeleton.ts +13 -8
  75. package/dist/ui/components/slider/slider.test.ts +13 -1
  76. package/dist/ui/components/slider/slider.ts +16 -3
  77. package/dist/ui/components/spinner/spinner.test.ts +16 -1
  78. package/dist/ui/components/spinner/spinner.ts +15 -7
  79. package/dist/ui/components/switch/switch.test.ts +13 -2
  80. package/dist/ui/components/switch/switch.ts +15 -1
  81. package/dist/ui/components/tab-bar/tab-bar.test.ts +32 -6
  82. package/dist/ui/components/tab-bar/tab-bar.ts +28 -24
  83. package/dist/ui/components/tabs/tabs.test.ts +34 -32
  84. package/dist/ui/components/tabs/tabs.ts +56 -73
  85. package/dist/ui/components/textarea/textarea.test.ts +11 -1
  86. package/dist/ui/components/textarea/textarea.ts +77 -71
  87. package/dist/ui/components/toggle/toggle.test.ts +47 -16
  88. package/dist/ui/components/toggle/toggle.ts +78 -80
  89. package/dist/ui/components/typography/typography.test.ts +16 -0
  90. package/dist/ui/components/typography/typography.ts +12 -4
  91. package/package.json +1 -1
@@ -1,16 +1,16 @@
1
1
  import {
2
- type ElementRef,
3
2
  Component,
3
+ ElementRef,
4
4
  ViewEncapsulation,
5
5
  booleanAttribute,
6
6
  computed,
7
7
  contentChildren,
8
+ effect,
8
9
  inject,
9
10
  input,
10
11
  model,
11
- viewChild,
12
12
  } from '@angular/core';
13
- import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
13
+ import { type BaseLynxElement, LYNX_ELEMENTS } from '@blotch/angular-lynx';
14
14
  import { cva, type VariantProps } from 'class-variance-authority';
15
15
 
16
16
  import {
@@ -86,23 +86,25 @@ const itemIconColor = () => {
86
86
 
87
87
  @Component({
88
88
  selector: 'ui-toggle',
89
- imports: [LYNX_ELEMENTS],
90
89
  encapsulation: ViewEncapsulation.None,
91
- template: `
92
- <view
93
- #container
94
- [class]="containerClass()"
95
- (bindtouchstart)="onPressStart()"
96
- (bindtouchend)="onPressEnd()"
97
- (bindtouchcancel)="onPressCancel()"
98
- (bindmousedown)="onPressStart()"
99
- (bindmouseup)="onPressEnd()"
100
- (bindmouseleave)="onPressCancel()"
101
- (bindtap)="onTap()"
102
- >
103
- <ng-content />
104
- </view>
105
- `,
90
+ // Each toggle, group and item is its host element, styled through `host`,
91
+ // rather than a view inside the host. Angular gives a component's host
92
+ // every static attribute the app writes on it, so `<ui-toggle
93
+ // class="px-6">` puts `px-6` on the host whatever the component does with
94
+ // its `class` input: with a styled view inside as well, the padding applied
95
+ // twice. On the host alone, a class also acts where the toggle sits in its
96
+ // parent's layout, and an item's accessibility-label names one element.
97
+ host: {
98
+ '[class]': 'containerClass()',
99
+ '(bindtouchstart)': 'onPressStart()',
100
+ '(bindtouchend)': 'onPressEnd()',
101
+ '(bindtouchcancel)': 'onPressCancel()',
102
+ '(bindmousedown)': 'onPressStart()',
103
+ '(bindmouseup)': 'onPressEnd()',
104
+ '(bindmouseleave)': 'onPressCancel()',
105
+ '(bindtap)': 'onTap()',
106
+ },
107
+ template: '<ng-content />',
106
108
  })
107
109
  export class UiToggle {
108
110
  readonly pressed = model(false);
@@ -111,7 +113,7 @@ export class UiToggle {
111
113
  readonly disabled = input(false, { transform: booleanAttribute });
112
114
  readonly userClass = input<string>('', { alias: 'class' });
113
115
 
114
- readonly containerRef = viewChild<ElementRef>('container');
116
+ readonly #host = inject(ElementRef).nativeElement;
115
117
  #pressAnim?: AnimationHandle;
116
118
 
117
119
  protected readonly containerClass = computed(() =>
@@ -126,18 +128,18 @@ export class UiToggle {
126
128
  protected onPressStart(): void {
127
129
  if (this.disabled()) return;
128
130
  this.#pressAnim?.cancel();
129
- this.#pressAnim = pressDown(this.containerRef()?.nativeElement);
131
+ this.#pressAnim = pressDown(this.#host);
130
132
  }
131
133
 
132
134
  protected onPressEnd(): void {
133
135
  if (this.disabled()) return;
134
136
  this.#pressAnim?.cancel();
135
- this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
137
+ this.#pressAnim = pressRelease(this.#host);
136
138
  }
137
139
 
138
140
  protected onPressCancel(): void {
139
141
  this.#pressAnim?.cancel();
140
- this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
142
+ this.#pressAnim = pressRelease(this.#host);
141
143
  }
142
144
 
143
145
  protected onTap(): void {
@@ -163,7 +165,7 @@ export class UiToggle {
163
165
  */
164
166
  @Component({
165
167
  selector: 'ui-toggle-group',
166
- imports: [LYNX_ELEMENTS, UiSegmentedIndicator],
168
+ imports: [UiSegmentedIndicator],
167
169
  encapsulation: ViewEncapsulation.None,
168
170
  providers: [
169
171
  UiSegmentedControl,
@@ -171,16 +173,15 @@ export class UiToggle {
171
173
  ],
172
174
  // The mouse leaving the row lets go of an indicator dragged in the web
173
175
  // preview, where a drag gets no touchcancel.
176
+ host: {
177
+ '[class]': 'containerClass()',
178
+ '(bindmouseleave)': 'segmentedControl.cancel()',
179
+ },
174
180
  template: `
175
- <view
176
- [class]="containerClass()"
177
- (bindmouseleave)="segmentedControl.cancel()"
178
- >
179
- @if (segmentedControl.underlay()) {
180
- <ui-segmented-indicator />
181
- }
182
- <ng-content />
183
- </view>
181
+ @if (segmentedControl.underlay()) {
182
+ <ui-segmented-indicator />
183
+ }
184
+ <ng-content />
184
185
  `,
185
186
  })
186
187
  export class UiToggleGroup implements SegmentedHost {
@@ -254,38 +255,32 @@ export class UiToggleGroup implements SegmentedHost {
254
255
  encapsulation: ViewEncapsulation.None,
255
256
  // Icons projected into the item inject this: they're declared inside it.
256
257
  providers: [{ provide: UI_ICON_COLOR, useFactory: itemIconColor }],
258
+ // The accessibility attributes are attribute bindings: a property binding
259
+ // on a component's host is checked against the DOM schema, which has no
260
+ // `accessibility-label`.
257
261
  host: {
258
- '[class]': 'hostClass()',
262
+ '[class]': 'containerClass()',
263
+ '[style]': 'containerStyle()',
264
+ '[attr.accessibility-element]': "accessibilityLabel() ? 'true' : null",
265
+ '[attr.accessibility-traits]': "accessibilityLabel() ? 'button' : null",
266
+ '[attr.accessibility-label]': 'accessibilityLabel() || null',
259
267
  // The segmented control places its indicator from each item's width.
260
268
  '(bindlayoutchange)': 'measure($event)',
269
+ '(bindtouchstart)': 'onPressStart($event)',
270
+ '(bindtouchmove)': 'onPressMove($event)',
271
+ '(bindtouchend)': 'onPressEnd()',
272
+ '(bindtouchcancel)': 'onPressCancel()',
273
+ '(bindmousedown)': 'onPressStart($event)',
274
+ '(bindmousemove)': 'onPressMove($event)',
275
+ '(bindmouseup)': 'onPressEnd()',
276
+ '(bindmouseleave)': 'onPointerLeave()',
277
+ '(bindtap)': 'onTap()',
261
278
  },
262
279
  template: `
263
- <!-- A labeled item is selected to a screen reader while pressed, which
264
- iOS reads. lynxAccessibilityTraits adds the trait to button on iOS,
265
- and leaves Android, which reads one trait, with button. -->
266
- <view
267
- #container
268
- [class]="containerClass()"
269
- [style]="containerStyle()"
270
- [attr.accessibility-element]="accessibilityLabel() ? 'true' : null"
271
- [attr.accessibility-traits]="accessibilityLabel() ? 'button' : null"
272
- [attr.accessibility-label]="accessibilityLabel() || null"
273
- [lynxAccessibilityTraits]="{ selected: selectedTrait() }"
274
- (bindtouchstart)="onPressStart($event)"
275
- (bindtouchmove)="onPressMove($event)"
276
- (bindtouchend)="onPressEnd()"
277
- (bindtouchcancel)="onPressCancel()"
278
- (bindmousedown)="onPressStart($event)"
279
- (bindmousemove)="onPressMove($event)"
280
- (bindmouseup)="onPressEnd()"
281
- (bindmouseleave)="onPointerLeave()"
282
- (bindtap)="onTap()"
283
- >
284
- @if (stateLayerClass(); as stateLayer) {
285
- <view [class]="stateLayer"></view>
286
- }
287
- <ng-content />
288
- </view>
280
+ @if (stateLayerClass(); as stateLayer) {
281
+ <view [class]="stateLayer"></view>
282
+ }
283
+ <ng-content />
289
284
  `,
290
285
  })
291
286
  export class UiToggleGroupItem {
@@ -303,17 +298,27 @@ export class UiToggleGroupItem {
303
298
  });
304
299
  readonly userClass = input<string>('', { alias: 'class' });
305
300
 
306
- // Required: always in the template.
307
- readonly containerRef = viewChild.required<ElementRef>('container');
301
+ readonly #host: BaseLynxElement = inject(ElementRef).nativeElement;
308
302
  #pressAnim?: AnimationHandle;
309
303
 
310
304
  protected readonly isSelected = computed(() =>
311
305
  this.#group.isSelected(this.itemValue()),
312
306
  );
313
- /** Only a labeled item is an accessibility element to be `selected`. */
314
- protected readonly selectedTrait = computed(
315
- () => !!this.accessibilityLabel() && this.isSelected(),
316
- );
307
+
308
+ /**
309
+ * A labeled item is `selected` to a screen reader while pressed, which iOS
310
+ * reads. The trait is added to the host's own `button`, as
311
+ * lynxAccessibilityTraits does on an element in a template: on iOS it
312
+ * reads `button,selected`, and Android, which reads one trait, keeps
313
+ * `button`. Only a labeled item is an accessibility element to be
314
+ * `selected`.
315
+ */
316
+ protected readonly markingSelected = effect(() => {
317
+ this.#host.setAccessibilityTrait(
318
+ 'selected',
319
+ !!this.accessibilityLabel() && this.isSelected(),
320
+ );
321
+ });
317
322
 
318
323
  /** Whether the item is a segment of a segmented control. */
319
324
  readonly #segment = computed(() => this.#segmented.enabled());
@@ -327,20 +332,13 @@ export class UiToggleGroupItem {
327
332
  this.#segment() ? this.#segmented.iconColor(this.itemValue()) : undefined,
328
333
  );
329
334
 
330
- /**
331
- * A segment's host is the track's flex item, an equal share of it. Angular
332
- * puts the app's static `class` on the host as well as on the item, so the
333
- * two are merged here: `class="flex-[2]"` makes a wider segment.
334
- */
335
- protected readonly hostClass = computed(() =>
336
- this.#segment()
337
- ? cn(this.#segmented.segmentHostClass, this.userClass())
338
- : '',
339
- );
340
-
341
335
  protected readonly containerClass = computed(() => {
342
336
  if (this.#segment()) {
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
+ // segment.
343
340
  return cn(
341
+ this.#segmented.segmentHostClass,
344
342
  this.#segmented.segmentClass(
345
343
  this.itemValue(),
346
344
  segmentPlace(this.#group.items(), this),
@@ -384,7 +382,7 @@ export class UiToggleGroupItem {
384
382
  return;
385
383
  }
386
384
  this.#pressAnim?.cancel();
387
- this.#pressAnim = pressDown(this.containerRef().nativeElement);
385
+ this.#pressAnim = pressDown(this.#host);
388
386
  }
389
387
 
390
388
  protected onPressMove(event: unknown): void {
@@ -398,7 +396,7 @@ export class UiToggleGroupItem {
398
396
  return;
399
397
  }
400
398
  this.#pressAnim?.cancel();
401
- this.#pressAnim = pressRelease(this.containerRef().nativeElement);
399
+ this.#pressAnim = pressRelease(this.#host);
402
400
  }
403
401
 
404
402
  protected onPressCancel(): void {
@@ -428,6 +426,6 @@ export class UiToggleGroupItem {
428
426
 
429
427
  #release(): void {
430
428
  this.#pressAnim?.cancel();
431
- this.#pressAnim = pressRelease(this.containerRef().nativeElement);
429
+ this.#pressAnim = pressRelease(this.#host);
432
430
  }
433
431
  }
@@ -22,6 +22,12 @@ class TypographyHost {
22
22
  | 'muted' = 'p';
23
23
  }
24
24
 
25
+ @Component({
26
+ imports: [UiText],
27
+ template: `<ui-text variant="muted" class="mb-4 text-center">Note</ui-text>`,
28
+ })
29
+ class StaticClassHost {}
30
+
25
31
  describe('typographyVariants', () => {
26
32
  it('returns default variant classes', () => {
27
33
  expect(typographyVariants()).toContain('text-base');
@@ -52,4 +58,14 @@ describe('UiText', () => {
52
58
  const { getByText } = await render(UiText);
53
59
  expect(getByText).toBeTruthy();
54
60
  });
61
+
62
+ // A class on a component lands on its host too (issue #181): the text is
63
+ // one <text>, so the class styles one element.
64
+ it('is one text, with the class written on it', async () => {
65
+ const { container, getByText } = await render(StaticClassHost);
66
+ const text = getByText('Note');
67
+ expect(text.tagName.toLowerCase()).toBe('text');
68
+ expect(text.classList).toContain('text-muted-foreground');
69
+ expect([...container.querySelectorAll('.mb-4')]).toEqual([text]);
70
+ });
55
71
  });
@@ -1,5 +1,5 @@
1
1
  import { Component, ViewEncapsulation, computed, input } from '@angular/core';
2
- import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
2
+ import { registerHostElement } from '@blotch/angular-lynx';
3
3
  import { cva, type VariantProps } from 'class-variance-authority';
4
4
 
5
5
  import { cn } from '../../utils/cn';
@@ -27,12 +27,18 @@ export type TypographyVariant = NonNullable<
27
27
  VariantProps<typeof typographyVariants>['variant']
28
28
  >;
29
29
 
30
+ /**
31
+ * The text is its host, a `<text>` (registerHostElement, below), styled
32
+ * through `host`. Angular gives a component's host every static attribute the
33
+ * app writes on it, so `<ui-text class="mb-4">` puts `mb-4` on the host
34
+ * whatever the component does: with a styled `<text>` inside a view host as
35
+ * well, the margin applied twice.
36
+ */
30
37
  @Component({
31
38
  selector: 'ui-text',
32
- standalone: true,
33
- imports: [LYNX_ELEMENTS],
34
39
  encapsulation: ViewEncapsulation.None,
35
- template: `<text [class]="textClass()"><ng-content /></text>`,
40
+ host: { '[class]': 'textClass()' },
41
+ template: '<ng-content />',
36
42
  })
37
43
  export class UiText {
38
44
  readonly variant = input<TypographyVariant>('p');
@@ -42,3 +48,5 @@ export class UiText {
42
48
  cn(typographyVariants({ variant: this.variant() }), this.userClass()),
43
49
  );
44
50
  }
51
+
52
+ registerHostElement(UiText, 'text');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blotch/dolan",
3
- "version": "22.2.6",
3
+ "version": "22.2.7",
4
4
  "description": "CLI for adding @blotch/ui components to your project",
5
5
  "homepage": "https://github.com/Blotch-Smart-Frames/AngularLynx#readme",
6
6
  "bugs": {