@sonny-ui/core 0.1.0-alpha.1 → 0.1.0-alpha.11
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/README.md +101 -32
- package/fesm2022/sonny-ui-core.mjs +3031 -42
- package/fesm2022/sonny-ui-core.mjs.map +1 -1
- package/package.json +8 -5
- package/schematics/ng-add/schema.json +1 -1
- package/schematics/ng-generate/component/index.js +182 -1
- package/schematics/ng-generate/component/schema.json +2 -2
- package/src/lib/accordion/accordion.directives.spec.ts +173 -0
- package/src/lib/accordion/accordion.directives.ts +147 -0
- package/src/lib/accordion/index.ts +8 -0
- package/src/lib/alert/alert.directives.spec.ts +154 -0
- package/src/lib/alert/alert.directives.ts +70 -0
- package/src/lib/alert/alert.variants.ts +25 -0
- package/src/lib/alert/index.ts +6 -0
- package/src/lib/avatar/avatar.component.spec.ts +75 -0
- package/src/lib/avatar/avatar.component.ts +44 -0
- package/src/lib/avatar/avatar.variants.ts +26 -0
- package/src/lib/avatar/index.ts +2 -0
- package/src/lib/badge/badge.directive.spec.ts +74 -0
- package/src/lib/badge/badge.directive.ts +18 -0
- package/src/lib/badge/badge.variants.ts +29 -0
- package/src/lib/badge/index.ts +2 -0
- package/src/lib/breadcrumb/breadcrumb.directives.spec.ts +80 -0
- package/src/lib/breadcrumb/breadcrumb.directives.ts +84 -0
- package/src/lib/breadcrumb/index.ts +8 -0
- package/src/lib/button/button.directive.spec.ts +92 -0
- package/src/lib/button/button.directive.ts +29 -0
- package/src/lib/button/button.variants.ts +30 -0
- package/src/lib/button/index.ts +2 -0
- package/src/lib/button-group/button-group.directive.spec.ts +46 -0
- package/src/lib/button-group/button-group.directive.ts +20 -0
- package/src/lib/button-group/button-group.variants.ts +18 -0
- package/src/lib/button-group/index.ts +2 -0
- package/src/lib/calendar/calendar.component.spec.ts +105 -0
- package/src/lib/calendar/calendar.component.ts +231 -0
- package/src/lib/calendar/index.ts +1 -0
- package/src/lib/card/card.directives.spec.ts +104 -0
- package/src/lib/card/card.directives.ts +78 -0
- package/src/lib/card/card.variants.ts +28 -0
- package/src/lib/card/index.ts +9 -0
- package/src/lib/carousel/carousel.directives.spec.ts +85 -0
- package/src/lib/carousel/carousel.directives.ts +164 -0
- package/src/lib/carousel/index.ts +8 -0
- package/src/lib/chat-bubble/chat-bubble.directives.spec.ts +52 -0
- package/src/lib/chat-bubble/chat-bubble.directives.ts +102 -0
- package/src/lib/chat-bubble/index.ts +11 -0
- package/src/lib/checkbox/checkbox.directive.spec.ts +57 -0
- package/src/lib/checkbox/checkbox.directive.ts +17 -0
- package/src/lib/checkbox/checkbox.variants.ts +19 -0
- package/src/lib/checkbox/index.ts +2 -0
- package/src/lib/combobox/combobox.component.spec.ts +151 -0
- package/src/lib/combobox/combobox.component.ts +279 -0
- package/src/lib/combobox/combobox.variants.ts +19 -0
- package/src/lib/combobox/index.ts +2 -0
- package/src/lib/diff/diff.component.spec.ts +47 -0
- package/src/lib/diff/diff.component.ts +83 -0
- package/src/lib/diff/index.ts +1 -0
- package/src/lib/divider/divider.component.spec.ts +48 -0
- package/src/lib/divider/divider.component.ts +52 -0
- package/src/lib/divider/divider.variants.ts +22 -0
- package/src/lib/divider/index.ts +2 -0
- package/src/lib/dock/dock.directives.spec.ts +85 -0
- package/src/lib/dock/dock.directives.ts +83 -0
- package/src/lib/dock/index.ts +1 -0
- package/src/lib/drawer/drawer.directives.spec.ts +62 -0
- package/src/lib/drawer/drawer.directives.ts +83 -0
- package/src/lib/drawer/index.ts +8 -0
- package/src/lib/dropdown/dropdown.directives.spec.ts +106 -0
- package/src/lib/dropdown/dropdown.directives.ts +143 -0
- package/src/lib/dropdown/dropdown.variants.ts +27 -0
- package/src/lib/dropdown/index.ts +15 -0
- package/src/lib/fab/fab.directives.spec.ts +60 -0
- package/src/lib/fab/fab.directives.ts +80 -0
- package/src/lib/fab/index.ts +8 -0
- package/src/lib/fieldset/fieldset.directives.spec.ts +74 -0
- package/src/lib/fieldset/fieldset.directives.ts +52 -0
- package/src/lib/fieldset/fieldset.variants.ts +15 -0
- package/src/lib/fieldset/index.ts +6 -0
- package/src/lib/file-input/file-input.component.spec.ts +114 -0
- package/src/lib/file-input/file-input.component.ts +168 -0
- package/src/lib/file-input/file-input.variants.ts +25 -0
- package/src/lib/file-input/index.ts +6 -0
- package/src/lib/indicator/index.ts +6 -0
- package/src/lib/indicator/indicator.directives.spec.ts +64 -0
- package/src/lib/indicator/indicator.directives.ts +61 -0
- package/src/lib/input/index.ts +3 -0
- package/src/lib/input/input.directive.spec.ts +103 -0
- package/src/lib/input/input.directive.ts +26 -0
- package/src/lib/input/input.variants.ts +42 -0
- package/src/lib/input/label.directive.ts +17 -0
- package/src/lib/kbd/index.ts +2 -0
- package/src/lib/kbd/kbd.directive.spec.ts +42 -0
- package/src/lib/kbd/kbd.directive.ts +19 -0
- package/src/lib/kbd/kbd.variants.ts +19 -0
- package/src/lib/link/index.ts +2 -0
- package/src/lib/link/link.directive.spec.ts +41 -0
- package/src/lib/link/link.directive.ts +19 -0
- package/src/lib/link/link.variants.ts +20 -0
- package/src/lib/list/index.ts +8 -0
- package/src/lib/list/list.directives.spec.ts +65 -0
- package/src/lib/list/list.directives.ts +86 -0
- package/src/lib/loader/index.ts +2 -0
- package/src/lib/loader/loader.component.spec.ts +58 -0
- package/src/lib/loader/loader.component.ts +48 -0
- package/src/lib/loader/loader.variants.ts +21 -0
- package/src/lib/modal/dialog-ref.ts +19 -0
- package/src/lib/modal/dialog.directives.ts +90 -0
- package/src/lib/modal/dialog.service.spec.ts +52 -0
- package/src/lib/modal/dialog.service.ts +61 -0
- package/src/lib/modal/dialog.types.ts +16 -0
- package/src/lib/modal/index.ts +11 -0
- package/src/lib/navbar/index.ts +7 -0
- package/src/lib/navbar/navbar.directives.spec.ts +59 -0
- package/src/lib/navbar/navbar.directives.ts +61 -0
- package/src/lib/pagination/index.ts +6 -0
- package/src/lib/pagination/pagination.component.spec.ts +59 -0
- package/src/lib/pagination/pagination.component.ts +144 -0
- package/src/lib/pagination/pagination.variants.ts +31 -0
- package/src/lib/progress/index.ts +7 -0
- package/src/lib/progress/progress.component.spec.ts +117 -0
- package/src/lib/progress/progress.component.ts +65 -0
- package/src/lib/progress/progress.variants.ts +43 -0
- package/src/lib/radial-progress/index.ts +5 -0
- package/src/lib/radial-progress/radial-progress.component.spec.ts +41 -0
- package/src/lib/radial-progress/radial-progress.component.ts +71 -0
- package/src/lib/radio/index.ts +2 -0
- package/src/lib/radio/radio.directive.spec.ts +46 -0
- package/src/lib/radio/radio.directive.ts +17 -0
- package/src/lib/radio/radio.variants.ts +19 -0
- package/src/lib/rating/index.ts +2 -0
- package/src/lib/rating/rating.component.spec.ts +157 -0
- package/src/lib/rating/rating.component.ts +171 -0
- package/src/lib/rating/rating.variants.ts +20 -0
- package/src/lib/select/index.ts +2 -0
- package/src/lib/select/select.component.spec.ts +112 -0
- package/src/lib/select/select.component.ts +250 -0
- package/src/lib/select/select.variants.ts +19 -0
- package/src/lib/sheet/index.ts +10 -0
- package/src/lib/sheet/sheet-ref.ts +18 -0
- package/src/lib/sheet/sheet.component.spec.ts +67 -0
- package/src/lib/sheet/sheet.directives.ts +75 -0
- package/src/lib/sheet/sheet.service.ts +100 -0
- package/src/lib/sheet/sheet.types.ts +23 -0
- package/src/lib/skeleton/index.ts +2 -0
- package/src/lib/skeleton/skeleton.directive.spec.ts +63 -0
- package/src/lib/skeleton/skeleton.directive.ts +22 -0
- package/src/lib/skeleton/skeleton.variants.ts +27 -0
- package/src/lib/slider/index.ts +2 -0
- package/src/lib/slider/slider.component.spec.ts +104 -0
- package/src/lib/slider/slider.component.ts +188 -0
- package/src/lib/slider/slider.variants.ts +25 -0
- package/src/lib/stat/index.ts +8 -0
- package/src/lib/stat/stat.directives.spec.ts +60 -0
- package/src/lib/stat/stat.directives.ts +84 -0
- package/src/lib/status/index.ts +2 -0
- package/src/lib/status/status.directive.spec.ts +43 -0
- package/src/lib/status/status.directive.ts +38 -0
- package/src/lib/status/status.variants.ts +26 -0
- package/src/lib/steps/index.ts +8 -0
- package/src/lib/steps/steps.directives.spec.ts +52 -0
- package/src/lib/steps/steps.directives.ts +80 -0
- package/src/lib/switch/index.ts +2 -0
- package/src/lib/switch/switch.component.spec.ts +98 -0
- package/src/lib/switch/switch.component.ts +84 -0
- package/src/lib/switch/switch.variants.ts +31 -0
- package/src/lib/table/index.ts +12 -0
- package/src/lib/table/table.directives.spec.ts +111 -0
- package/src/lib/table/table.directives.ts +134 -0
- package/src/lib/table/table.variants.ts +36 -0
- package/src/lib/tabs/index.ts +8 -0
- package/src/lib/tabs/tabs.directives.spec.ts +136 -0
- package/src/lib/tabs/tabs.directives.ts +130 -0
- package/src/lib/tabs/tabs.variants.ts +17 -0
- package/src/lib/textarea/index.ts +7 -0
- package/src/lib/textarea/textarea.directive.spec.ts +84 -0
- package/src/lib/textarea/textarea.directive.ts +72 -0
- package/src/lib/textarea/textarea.variants.ts +34 -0
- package/src/lib/timeline/index.ts +11 -0
- package/src/lib/timeline/timeline.directives.spec.ts +55 -0
- package/src/lib/timeline/timeline.directives.ts +90 -0
- package/src/lib/toast/index.ts +3 -0
- package/src/lib/toast/toast.service.spec.ts +71 -0
- package/src/lib/toast/toast.service.ts +60 -0
- package/src/lib/toast/toast.variants.ts +38 -0
- package/src/lib/toast/toaster.component.spec.ts +38 -0
- package/src/lib/toast/toaster.component.ts +82 -0
- package/src/lib/toggle/index.ts +2 -0
- package/src/lib/toggle/toggle.directive.spec.ts +100 -0
- package/src/lib/toggle/toggle.directive.ts +73 -0
- package/src/lib/toggle/toggle.variants.ts +25 -0
- package/src/lib/tooltip/index.ts +2 -0
- package/src/lib/tooltip/tooltip.directive.spec.ts +113 -0
- package/src/lib/tooltip/tooltip.directive.ts +131 -0
- package/src/lib/tooltip/tooltip.variants.ts +20 -0
- package/src/lib/validator/index.ts +5 -0
- package/src/lib/validator/validator.directives.spec.ts +47 -0
- package/src/lib/validator/validator.directives.ts +52 -0
- package/types/sonny-ui-core.d.ts +878 -11
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Directive, ElementRef, HostListener, InjectionToken, computed, inject, input, signal } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
import {
|
|
4
|
+
dropdownContentVariants,
|
|
5
|
+
dropdownItemVariants,
|
|
6
|
+
type DropdownItemVariant,
|
|
7
|
+
} from './dropdown.variants';
|
|
8
|
+
|
|
9
|
+
export const SNY_DROPDOWN = new InjectionToken<SnyDropdownDirective>('SnyDropdown');
|
|
10
|
+
|
|
11
|
+
@Directive({
|
|
12
|
+
selector: '[snyDropdown]',
|
|
13
|
+
standalone: true,
|
|
14
|
+
exportAs: 'snyDropdown',
|
|
15
|
+
providers: [{ provide: SNY_DROPDOWN, useExisting: SnyDropdownDirective }],
|
|
16
|
+
host: {
|
|
17
|
+
'[class]': '"relative inline-block"',
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
export class SnyDropdownDirective {
|
|
21
|
+
private readonly elementRef = inject(ElementRef);
|
|
22
|
+
readonly isOpen = signal(false);
|
|
23
|
+
|
|
24
|
+
toggle(): void { this.isOpen.update((v) => !v); }
|
|
25
|
+
open(): void { this.isOpen.set(true); }
|
|
26
|
+
close(): void { this.isOpen.set(false); }
|
|
27
|
+
|
|
28
|
+
@HostListener('document:click', ['$event'])
|
|
29
|
+
onDocumentClick(event: MouseEvent): void {
|
|
30
|
+
if (!this.elementRef.nativeElement.contains(event.target)) {
|
|
31
|
+
this.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@HostListener('keydown.escape')
|
|
36
|
+
onEscape(): void {
|
|
37
|
+
this.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Directive({
|
|
42
|
+
selector: '[snyDropdownTrigger]',
|
|
43
|
+
standalone: true,
|
|
44
|
+
host: {
|
|
45
|
+
'(click)': 'dropdown.toggle()',
|
|
46
|
+
'[attr.aria-expanded]': 'dropdown.isOpen()',
|
|
47
|
+
'[attr.aria-haspopup]': '"menu"',
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
export class SnyDropdownTriggerDirective {
|
|
51
|
+
readonly dropdown = inject(SNY_DROPDOWN);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Directive({
|
|
55
|
+
selector: '[snyDropdownContent]',
|
|
56
|
+
standalone: true,
|
|
57
|
+
host: {
|
|
58
|
+
'role': 'menu',
|
|
59
|
+
'[class]': 'computedClass()',
|
|
60
|
+
'[style.display]': 'dropdown.isOpen() ? "" : "none"',
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
export class SnyDropdownContentDirective {
|
|
64
|
+
readonly dropdown = inject(SNY_DROPDOWN);
|
|
65
|
+
readonly class = input<string>('');
|
|
66
|
+
|
|
67
|
+
protected readonly computedClass = computed(() =>
|
|
68
|
+
cn(
|
|
69
|
+
dropdownContentVariants(),
|
|
70
|
+
'absolute mt-1 left-0 animate-in fade-in-0 zoom-in-95',
|
|
71
|
+
this.class()
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Directive({
|
|
77
|
+
selector: '[snyMenuContent]',
|
|
78
|
+
standalone: true,
|
|
79
|
+
host: {
|
|
80
|
+
'[class]': 'computedClass()',
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
export class SnyMenuContentDirective {
|
|
84
|
+
readonly class = input<string>('');
|
|
85
|
+
|
|
86
|
+
protected readonly computedClass = computed(() =>
|
|
87
|
+
cn(dropdownContentVariants(), this.class())
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Directive({
|
|
92
|
+
selector: '[snyMenuItem]',
|
|
93
|
+
standalone: true,
|
|
94
|
+
host: {
|
|
95
|
+
'role': 'menuitem',
|
|
96
|
+
'[class]': 'computedClass()',
|
|
97
|
+
'(click)': 'onClick()',
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
export class SnyMenuItemDirective {
|
|
101
|
+
private readonly dropdown = inject(SNY_DROPDOWN, { optional: true });
|
|
102
|
+
readonly variant = input<DropdownItemVariant>('default');
|
|
103
|
+
readonly class = input<string>('');
|
|
104
|
+
|
|
105
|
+
protected readonly computedClass = computed(() =>
|
|
106
|
+
cn(dropdownItemVariants({ variant: this.variant() }), 'cursor-pointer hover:bg-accent hover:text-accent-foreground', this.class())
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
onClick(): void {
|
|
110
|
+
this.dropdown?.close();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Directive({
|
|
115
|
+
selector: '[snyMenuSeparator]',
|
|
116
|
+
standalone: true,
|
|
117
|
+
host: {
|
|
118
|
+
'role': 'separator',
|
|
119
|
+
'[class]': 'computedClass()',
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
export class SnyMenuSeparatorDirective {
|
|
123
|
+
readonly class = input<string>('');
|
|
124
|
+
|
|
125
|
+
protected readonly computedClass = computed(() =>
|
|
126
|
+
cn('-mx-1 my-1 h-px bg-muted', this.class())
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@Directive({
|
|
131
|
+
selector: '[snyMenuLabel]',
|
|
132
|
+
standalone: true,
|
|
133
|
+
host: {
|
|
134
|
+
'[class]': 'computedClass()',
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
export class SnyMenuLabelDirective {
|
|
138
|
+
readonly class = input<string>('');
|
|
139
|
+
|
|
140
|
+
protected readonly computedClass = computed(() =>
|
|
141
|
+
cn('px-2 py-1.5 text-sm font-semibold', this.class())
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
export const dropdownContentVariants = cva(
|
|
4
|
+
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
|
|
5
|
+
{
|
|
6
|
+
variants: {},
|
|
7
|
+
defaultVariants: {},
|
|
8
|
+
}
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export const dropdownItemVariants = cva(
|
|
12
|
+
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[active]:bg-accent data-[active]:text-accent-foreground',
|
|
13
|
+
{
|
|
14
|
+
variants: {
|
|
15
|
+
variant: {
|
|
16
|
+
default: '',
|
|
17
|
+
destructive:
|
|
18
|
+
'text-destructive data-[active]:bg-destructive/10 data-[active]:text-destructive',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
variant: 'default',
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export type DropdownItemVariant = 'default' | 'destructive';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
SnyDropdownDirective,
|
|
3
|
+
SnyDropdownTriggerDirective,
|
|
4
|
+
SnyDropdownContentDirective,
|
|
5
|
+
SnyMenuContentDirective,
|
|
6
|
+
SnyMenuItemDirective,
|
|
7
|
+
SnyMenuSeparatorDirective,
|
|
8
|
+
SnyMenuLabelDirective,
|
|
9
|
+
SNY_DROPDOWN,
|
|
10
|
+
} from './dropdown.directives';
|
|
11
|
+
export {
|
|
12
|
+
dropdownContentVariants,
|
|
13
|
+
dropdownItemVariants,
|
|
14
|
+
type DropdownItemVariant,
|
|
15
|
+
} from './dropdown.variants';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Component, viewChild } from '@angular/core';
|
|
2
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { SnyFabDirective, SnyFabTriggerDirective, SnyFabActionDirective } from './fab.directives';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
standalone: true,
|
|
7
|
+
imports: [SnyFabDirective, SnyFabTriggerDirective, SnyFabActionDirective],
|
|
8
|
+
template: `
|
|
9
|
+
<div snyFab>
|
|
10
|
+
<button snyFabAction ariaLabel="Edit item">Action 1</button>
|
|
11
|
+
<button snyFabAction>Action 2</button>
|
|
12
|
+
<button snyFabTrigger>+</button>
|
|
13
|
+
</div>
|
|
14
|
+
`,
|
|
15
|
+
})
|
|
16
|
+
class TestHostComponent {
|
|
17
|
+
fab = viewChild(SnyFabDirective);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('SnyFabDirective', () => {
|
|
21
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
|
|
25
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
26
|
+
fixture.detectChanges();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should be closed by default', () => {
|
|
30
|
+
const actions = fixture.nativeElement.querySelectorAll('[snyFabAction]');
|
|
31
|
+
actions.forEach((a: HTMLElement) => expect(a.className).toContain('scale-0'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should open on trigger click', () => {
|
|
35
|
+
const trigger = fixture.nativeElement.querySelector('[snyFabTrigger]');
|
|
36
|
+
trigger.click();
|
|
37
|
+
fixture.detectChanges();
|
|
38
|
+
const actions = fixture.nativeElement.querySelectorAll('[snyFabAction]');
|
|
39
|
+
actions.forEach((a: HTMLElement) => expect(a.className).toContain('scale-100'));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should have aria-expanded on trigger', () => {
|
|
43
|
+
const trigger = fixture.nativeElement.querySelector('[snyFabTrigger]');
|
|
44
|
+
expect(trigger.getAttribute('aria-expanded')).toBe('false');
|
|
45
|
+
trigger.click();
|
|
46
|
+
fixture.detectChanges();
|
|
47
|
+
expect(trigger.getAttribute('aria-expanded')).toBe('true');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should have menuitem role on actions', () => {
|
|
51
|
+
const actions = fixture.nativeElement.querySelectorAll('[snyFabAction]');
|
|
52
|
+
actions.forEach((a: HTMLElement) => expect(a.getAttribute('role')).toBe('menuitem'));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should set aria-label on action when ariaLabel input is provided', () => {
|
|
56
|
+
const actions = fixture.nativeElement.querySelectorAll('[snyFabAction]');
|
|
57
|
+
expect(actions[0].getAttribute('aria-label')).toBe('Edit item');
|
|
58
|
+
expect(actions[1].getAttribute('aria-label')).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Directive, InjectionToken, computed, inject, input, signal } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
|
|
4
|
+
export const SNY_FAB = new InjectionToken<SnyFabDirective>('SnyFab');
|
|
5
|
+
export type FabPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
6
|
+
export type FabDirection = 'up' | 'down' | 'left' | 'right';
|
|
7
|
+
|
|
8
|
+
const positionMap: Record<FabPosition, string> = {
|
|
9
|
+
'bottom-right': 'fixed bottom-6 right-6',
|
|
10
|
+
'bottom-left': 'fixed bottom-6 left-6',
|
|
11
|
+
'top-right': 'fixed top-6 right-6',
|
|
12
|
+
'top-left': 'fixed top-6 left-6',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
@Directive({
|
|
16
|
+
selector: '[snyFab]',
|
|
17
|
+
standalone: true,
|
|
18
|
+
exportAs: 'snyFab',
|
|
19
|
+
providers: [{ provide: SNY_FAB, useExisting: SnyFabDirective }],
|
|
20
|
+
host: { '[class]': 'computedClass()' },
|
|
21
|
+
})
|
|
22
|
+
export class SnyFabDirective {
|
|
23
|
+
readonly position = input<FabPosition>('bottom-right');
|
|
24
|
+
readonly direction = input<FabDirection>('up');
|
|
25
|
+
readonly class = input<string>('');
|
|
26
|
+
readonly isOpen = signal(false);
|
|
27
|
+
|
|
28
|
+
toggle(): void { this.isOpen.update((v) => !v); }
|
|
29
|
+
open(): void { this.isOpen.set(true); }
|
|
30
|
+
close(): void { this.isOpen.set(false); }
|
|
31
|
+
|
|
32
|
+
protected readonly computedClass = computed(() =>
|
|
33
|
+
cn('z-50 flex flex-col items-center gap-2', positionMap[this.position()], this.class())
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Directive({
|
|
38
|
+
selector: '[snyFabTrigger]',
|
|
39
|
+
standalone: true,
|
|
40
|
+
host: {
|
|
41
|
+
'(click)': 'fab.toggle()',
|
|
42
|
+
'[attr.aria-expanded]': 'fab.isOpen()',
|
|
43
|
+
'[attr.aria-haspopup]': '"menu"',
|
|
44
|
+
'aria-label': 'Quick actions',
|
|
45
|
+
'[class]': 'computedClass()',
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
export class SnyFabTriggerDirective {
|
|
49
|
+
readonly fab = inject(SNY_FAB);
|
|
50
|
+
readonly class = input<string>('');
|
|
51
|
+
protected readonly computedClass = computed(() =>
|
|
52
|
+
cn(
|
|
53
|
+
'inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg h-14 w-14 hover:bg-primary/90 transition-transform',
|
|
54
|
+
this.fab.isOpen() && 'rotate-45',
|
|
55
|
+
this.class()
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Directive({
|
|
61
|
+
selector: '[snyFabAction]',
|
|
62
|
+
standalone: true,
|
|
63
|
+
host: {
|
|
64
|
+
'role': 'menuitem',
|
|
65
|
+
'[attr.aria-label]': 'ariaLabel() || null',
|
|
66
|
+
'[class]': 'computedClass()',
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
export class SnyFabActionDirective {
|
|
70
|
+
readonly fab = inject(SNY_FAB);
|
|
71
|
+
readonly ariaLabel = input<string>('');
|
|
72
|
+
readonly class = input<string>('');
|
|
73
|
+
protected readonly computedClass = computed(() =>
|
|
74
|
+
cn(
|
|
75
|
+
'inline-flex items-center justify-center rounded-full bg-secondary text-secondary-foreground shadow-md h-10 w-10 transition-all',
|
|
76
|
+
this.fab.isOpen() ? 'scale-100 opacity-100' : 'scale-0 opacity-0',
|
|
77
|
+
this.class()
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import {
|
|
4
|
+
SnyFieldsetDirective,
|
|
5
|
+
SnyFieldsetLegendDirective,
|
|
6
|
+
SnyFieldsetContentDirective,
|
|
7
|
+
} from './fieldset.directives';
|
|
8
|
+
import type { FieldsetVariant } from './fieldset.variants';
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
standalone: true,
|
|
12
|
+
imports: [SnyFieldsetDirective, SnyFieldsetLegendDirective, SnyFieldsetContentDirective],
|
|
13
|
+
template: `
|
|
14
|
+
<fieldset snyFieldset [variant]="variant()" [disabled]="disabled()">
|
|
15
|
+
<legend snyFieldsetLegend>Personal Info</legend>
|
|
16
|
+
<div snyFieldsetContent>
|
|
17
|
+
<input type="text" />
|
|
18
|
+
</div>
|
|
19
|
+
</fieldset>
|
|
20
|
+
`,
|
|
21
|
+
})
|
|
22
|
+
class TestHostComponent {
|
|
23
|
+
variant = signal<FieldsetVariant>('default');
|
|
24
|
+
disabled = signal(false);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('SnyFieldsetDirective', () => {
|
|
28
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
29
|
+
let fieldset: HTMLFieldSetElement;
|
|
30
|
+
|
|
31
|
+
beforeEach(async () => {
|
|
32
|
+
await TestBed.configureTestingModule({
|
|
33
|
+
imports: [TestHostComponent],
|
|
34
|
+
}).compileComponents();
|
|
35
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
36
|
+
fixture.detectChanges();
|
|
37
|
+
fieldset = fixture.nativeElement.querySelector('fieldset');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should apply default classes', () => {
|
|
41
|
+
expect(fieldset.className).toContain('space-y-4');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should apply bordered variant', () => {
|
|
45
|
+
fixture.componentInstance.variant.set('bordered');
|
|
46
|
+
fixture.detectChanges();
|
|
47
|
+
expect(fieldset.className).toContain('border');
|
|
48
|
+
expect(fieldset.className).toContain('rounded-lg');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should set disabled attribute', () => {
|
|
52
|
+
expect(fieldset.disabled).toBe(false);
|
|
53
|
+
fixture.componentInstance.disabled.set(true);
|
|
54
|
+
fixture.detectChanges();
|
|
55
|
+
expect(fieldset.disabled).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should apply legend classes', () => {
|
|
59
|
+
const legend = fixture.nativeElement.querySelector('legend');
|
|
60
|
+
expect(legend.className).toContain('font-medium');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should apply content classes', () => {
|
|
64
|
+
const content = fixture.nativeElement.querySelector('[snyFieldsetContent]');
|
|
65
|
+
expect(content.className).toContain('space-y-2');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should set aria-disabled when disabled', () => {
|
|
69
|
+
expect(fieldset.getAttribute('aria-disabled')).toBeNull();
|
|
70
|
+
fixture.componentInstance.disabled.set(true);
|
|
71
|
+
fixture.detectChanges();
|
|
72
|
+
expect(fieldset.getAttribute('aria-disabled')).toBe('true');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Directive, computed, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
import { fieldsetVariants, type FieldsetVariant } from './fieldset.variants';
|
|
4
|
+
|
|
5
|
+
@Directive({
|
|
6
|
+
selector: 'fieldset[snyFieldset]',
|
|
7
|
+
standalone: true,
|
|
8
|
+
host: {
|
|
9
|
+
'[class]': 'computedClass()',
|
|
10
|
+
'[attr.disabled]': 'disabled() || null',
|
|
11
|
+
'[attr.aria-disabled]': 'disabled() || null',
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
export class SnyFieldsetDirective {
|
|
15
|
+
readonly variant = input<FieldsetVariant>('default');
|
|
16
|
+
readonly disabled = input(false);
|
|
17
|
+
readonly class = input<string>('');
|
|
18
|
+
|
|
19
|
+
protected readonly computedClass = computed(() =>
|
|
20
|
+
cn(fieldsetVariants({ variant: this.variant() }), this.class())
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Directive({
|
|
25
|
+
selector: 'legend[snyFieldsetLegend]',
|
|
26
|
+
standalone: true,
|
|
27
|
+
host: {
|
|
28
|
+
'[class]': 'computedClass()',
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
export class SnyFieldsetLegendDirective {
|
|
32
|
+
readonly class = input<string>('');
|
|
33
|
+
|
|
34
|
+
protected readonly computedClass = computed(() =>
|
|
35
|
+
cn('text-sm font-medium leading-none', this.class())
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Directive({
|
|
40
|
+
selector: '[snyFieldsetContent]',
|
|
41
|
+
standalone: true,
|
|
42
|
+
host: {
|
|
43
|
+
'[class]': 'computedClass()',
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
export class SnyFieldsetContentDirective {
|
|
47
|
+
readonly class = input<string>('');
|
|
48
|
+
|
|
49
|
+
protected readonly computedClass = computed(() =>
|
|
50
|
+
cn('space-y-2', this.class())
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
export const fieldsetVariants = cva('space-y-4', {
|
|
4
|
+
variants: {
|
|
5
|
+
variant: {
|
|
6
|
+
default: '',
|
|
7
|
+
bordered: 'rounded-lg border border-border p-4',
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
defaultVariants: {
|
|
11
|
+
variant: 'default',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type FieldsetVariant = 'default' | 'bordered';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Component, signal, viewChild } from '@angular/core';
|
|
2
|
+
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
|
3
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
4
|
+
import { SnyFileInputComponent } from './file-input.component';
|
|
5
|
+
import type { FileInputVariant, FileInputSize } from './file-input.variants';
|
|
6
|
+
|
|
7
|
+
@Component({
|
|
8
|
+
standalone: true,
|
|
9
|
+
imports: [SnyFileInputComponent],
|
|
10
|
+
template: `
|
|
11
|
+
<sny-file-input
|
|
12
|
+
[variant]="variant()"
|
|
13
|
+
[size]="size()"
|
|
14
|
+
[disabled]="disabled()"
|
|
15
|
+
[placeholder]="placeholder()"
|
|
16
|
+
[maxSize]="maxSize()"
|
|
17
|
+
(error)="lastError = $event"
|
|
18
|
+
/>
|
|
19
|
+
`,
|
|
20
|
+
})
|
|
21
|
+
class TestHostComponent {
|
|
22
|
+
variant = signal<FileInputVariant>('default');
|
|
23
|
+
size = signal<FileInputSize>('md');
|
|
24
|
+
disabled = signal(false);
|
|
25
|
+
placeholder = signal('Choose file...');
|
|
26
|
+
maxSize = signal(0);
|
|
27
|
+
lastError = '';
|
|
28
|
+
fileInput = viewChild(SnyFileInputComponent);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('SnyFileInputComponent', () => {
|
|
32
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
33
|
+
let host: HTMLElement;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
await TestBed.configureTestingModule({
|
|
37
|
+
imports: [TestHostComponent],
|
|
38
|
+
}).compileComponents();
|
|
39
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
40
|
+
fixture.detectChanges();
|
|
41
|
+
host = fixture.nativeElement.querySelector('sny-file-input');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should show placeholder text', () => {
|
|
45
|
+
expect(host.textContent).toContain('Choose file...');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should apply default variant classes', () => {
|
|
49
|
+
const label = host.querySelector('label');
|
|
50
|
+
expect(label!.className).toContain('border-input');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should apply error variant classes', () => {
|
|
54
|
+
fixture.componentInstance.variant.set('error');
|
|
55
|
+
fixture.detectChanges();
|
|
56
|
+
const label = host.querySelector('label');
|
|
57
|
+
expect(label!.className).toContain('border-destructive');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should have file input with sr-only class', () => {
|
|
61
|
+
const input = host.querySelector('input[type="file"]') as HTMLInputElement;
|
|
62
|
+
expect(input).not.toBeNull();
|
|
63
|
+
expect(input.className).toContain('sr-only');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should disable the input', () => {
|
|
67
|
+
fixture.componentInstance.disabled.set(true);
|
|
68
|
+
fixture.detectChanges();
|
|
69
|
+
const input = host.querySelector('input[type="file"]') as HTMLInputElement;
|
|
70
|
+
expect(input.disabled).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should clear the selection', () => {
|
|
74
|
+
const comp = fixture.componentInstance.fileInput();
|
|
75
|
+
comp!.clear();
|
|
76
|
+
fixture.detectChanges();
|
|
77
|
+
expect(host.textContent).toContain('Choose file...');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
@Component({
|
|
82
|
+
standalone: true,
|
|
83
|
+
imports: [ReactiveFormsModule, SnyFileInputComponent],
|
|
84
|
+
template: `<sny-file-input [formControl]="ctrl" />`,
|
|
85
|
+
})
|
|
86
|
+
class ReactiveFormHost {
|
|
87
|
+
ctrl = new FormControl<FileList | null>(null);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe('SnyFileInputComponent — Reactive Forms', () => {
|
|
91
|
+
let fixture: ComponentFixture<ReactiveFormHost>;
|
|
92
|
+
let host: HTMLElement;
|
|
93
|
+
|
|
94
|
+
beforeEach(async () => {
|
|
95
|
+
await TestBed.configureTestingModule({
|
|
96
|
+
imports: [ReactiveFormHost],
|
|
97
|
+
}).compileComponents();
|
|
98
|
+
fixture = TestBed.createComponent(ReactiveFormHost);
|
|
99
|
+
fixture.detectChanges();
|
|
100
|
+
host = fixture.nativeElement.querySelector('sny-file-input');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should render with FormControl (writeValue)', () => {
|
|
104
|
+
expect(host).toBeTruthy();
|
|
105
|
+
expect(host.textContent).toContain('Choose file...');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should disable via FormControl.disable() (setDisabledState)', () => {
|
|
109
|
+
fixture.componentInstance.ctrl.disable();
|
|
110
|
+
fixture.detectChanges();
|
|
111
|
+
const input = host.querySelector('input[type="file"]') as HTMLInputElement;
|
|
112
|
+
expect(input.disabled).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
});
|