@kouji-ui/core 0.0.4 → 0.1.2

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 (127) hide show
  1. package/fesm2022/kouji-ui-core.mjs +26789 -0
  2. package/fesm2022/kouji-ui-core.mjs.map +1 -0
  3. package/icon/icon.css +40 -0
  4. package/package.json +20 -9
  5. package/types/kouji-ui-core.d.ts +13545 -0
  6. package/typography/prose.css +312 -0
  7. package/.turbo/turbo-build.log +0 -27
  8. package/CHANGELOG.md +0 -21
  9. package/ng-package.json +0 -7
  10. package/src/a11y/.gitkeep +0 -0
  11. package/src/a11y/aria-describedby.spec.ts +0 -55
  12. package/src/a11y/aria-describedby.ts +0 -33
  13. package/src/a11y/focus-trap.spec.ts +0 -86
  14. package/src/a11y/focus-trap.ts +0 -70
  15. package/src/a11y/index.ts +0 -9
  16. package/src/a11y/live-region.spec.ts +0 -58
  17. package/src/a11y/live-region.ts +0 -48
  18. package/src/a11y/roving-tabindex.spec.ts +0 -62
  19. package/src/a11y/roving-tabindex.ts +0 -104
  20. package/src/a11y/visually-hidden.spec.ts +0 -34
  21. package/src/a11y/visually-hidden.ts +0 -24
  22. package/src/accordion/accordion.context.ts +0 -22
  23. package/src/accordion/accordion.spec.ts +0 -32
  24. package/src/accordion/accordion.ts +0 -129
  25. package/src/accordion/index.ts +0 -2
  26. package/src/avatar/avatar.spec.ts +0 -18
  27. package/src/avatar/avatar.ts +0 -26
  28. package/src/avatar/index.ts +0 -1
  29. package/src/badge/badge.spec.ts +0 -20
  30. package/src/badge/badge.ts +0 -14
  31. package/src/badge/index.ts +0 -1
  32. package/src/button/button.example.ts +0 -30
  33. package/src/button/button.finance.example.ts +0 -37
  34. package/src/button/button.retro.example.ts +0 -47
  35. package/src/button/button.sizes.example.ts +0 -25
  36. package/src/button/button.spec.ts +0 -32
  37. package/src/button/button.ts +0 -44
  38. package/src/button/index.ts +0 -1
  39. package/src/chart/chart.spec.ts +0 -27
  40. package/src/chart/chart.ts +0 -48
  41. package/src/chart/index.ts +0 -1
  42. package/src/checkbox/checkbox.spec.ts +0 -35
  43. package/src/checkbox/checkbox.ts +0 -82
  44. package/src/checkbox/index.ts +0 -1
  45. package/src/dialog/dialog.confirm.example.ts +0 -57
  46. package/src/dialog/dialog.context.ts +0 -11
  47. package/src/dialog/dialog.example.ts +0 -40
  48. package/src/dialog/dialog.finance.example.ts +0 -47
  49. package/src/dialog/dialog.retro.example.ts +0 -46
  50. package/src/dialog/dialog.service.ts +0 -176
  51. package/src/dialog/dialog.spec.ts +0 -122
  52. package/src/dialog/dialog.ts +0 -201
  53. package/src/dialog/index.ts +0 -9
  54. package/src/example-components.ts +0 -33
  55. package/src/form/form.context.ts +0 -3
  56. package/src/form/form.spec.ts +0 -25
  57. package/src/form/form.ts +0 -46
  58. package/src/form/index.ts +0 -2
  59. package/src/input/index.ts +0 -1
  60. package/src/input/input.spec.ts +0 -43
  61. package/src/input/input.ts +0 -80
  62. package/src/menu/index.ts +0 -2
  63. package/src/menu/menu.context.ts +0 -10
  64. package/src/menu/menu.example.ts +0 -39
  65. package/src/menu/menu.spec.ts +0 -168
  66. package/src/menu/menu.ts +0 -176
  67. package/src/popover/index.ts +0 -12
  68. package/src/popover/popover.context.ts +0 -18
  69. package/src/popover/popover.example.ts +0 -50
  70. package/src/popover/popover.finance.example.ts +0 -52
  71. package/src/popover/popover.retro.example.ts +0 -53
  72. package/src/popover/popover.spec.ts +0 -52
  73. package/src/popover/popover.ts +0 -124
  74. package/src/primitives/.gitkeep +0 -0
  75. package/src/primitives/forms/form-control.spec.ts +0 -59
  76. package/src/primitives/forms/form-control.ts +0 -90
  77. package/src/primitives/forms/index.ts +0 -1
  78. package/src/primitives/index.ts +0 -3
  79. package/src/primitives/interaction/disabled.spec.ts +0 -47
  80. package/src/primitives/interaction/disabled.ts +0 -24
  81. package/src/primitives/interaction/focus-ring.spec.ts +0 -62
  82. package/src/primitives/interaction/focus-ring.ts +0 -57
  83. package/src/primitives/interaction/index.ts +0 -2
  84. package/src/primitives/overlay/index.ts +0 -1
  85. package/src/primitives/overlay/overlay.spec.ts +0 -90
  86. package/src/primitives/overlay/overlay.ts +0 -106
  87. package/src/public-api.ts +0 -35
  88. package/src/radio/index.ts +0 -2
  89. package/src/radio/radio.context.ts +0 -3
  90. package/src/radio/radio.spec.ts +0 -36
  91. package/src/radio/radio.ts +0 -55
  92. package/src/select/index.ts +0 -2
  93. package/src/select/select.context.ts +0 -3
  94. package/src/select/select.spec.ts +0 -145
  95. package/src/select/select.ts +0 -250
  96. package/src/styles/docs-themes.css +0 -82
  97. package/src/table/index.ts +0 -1
  98. package/src/table/table.spec.ts +0 -113
  99. package/src/table/table.ts +0 -126
  100. package/src/tabs/index.ts +0 -2
  101. package/src/tabs/tabs.context.ts +0 -9
  102. package/src/tabs/tabs.spec.ts +0 -40
  103. package/src/tabs/tabs.ts +0 -130
  104. package/src/test-setup.ts +0 -5
  105. package/src/toast/index.ts +0 -21
  106. package/src/toast/toast.example.ts +0 -134
  107. package/src/toast/toast.finance.example.ts +0 -68
  108. package/src/toast/toast.retro.example.ts +0 -69
  109. package/src/toast/toast.service.ts +0 -153
  110. package/src/toast/toast.spec.ts +0 -95
  111. package/src/toast/toast.strategy.ts +0 -102
  112. package/src/toast/toast.ts +0 -307
  113. package/src/toast/toast.types.ts +0 -5
  114. package/src/toggle/index.ts +0 -1
  115. package/src/toggle/toggle.spec.ts +0 -24
  116. package/src/toggle/toggle.ts +0 -40
  117. package/src/tooltip/index.ts +0 -5
  118. package/src/tooltip/tooltip.example.ts +0 -46
  119. package/src/tooltip/tooltip.finance.example.ts +0 -54
  120. package/src/tooltip/tooltip.placements.example.ts +0 -56
  121. package/src/tooltip/tooltip.retro.example.ts +0 -65
  122. package/src/tooltip/tooltip.spec.ts +0 -104
  123. package/src/tooltip/tooltip.ts +0 -129
  124. package/tsconfig.lib.json +0 -13
  125. package/tsconfig.lib.prod.json +0 -11
  126. package/tsconfig.spec.json +0 -10
  127. package/vite.config.ts +0 -19
@@ -1,176 +0,0 @@
1
- import {
2
- ApplicationRef, ComponentRef, EmbeddedViewRef,
3
- Injectable, InjectionToken, Injector,
4
- TemplateRef, Type, ViewContainerRef,
5
- createComponent, inject, signal,
6
- } from '@angular/core';
7
-
8
- /** Token for passing data to a programmatically opened dialog component. */
9
- export const DIALOG_DATA = new InjectionToken<unknown>('KjDialogData');
10
-
11
- /** Configuration for programmatically opened dialogs. */
12
- export interface KjDialogOpenConfig<D = unknown> {
13
- /** Data to inject into the dialog component via `DIALOG_DATA`. */
14
- data?: D;
15
- /** Additional CSS classes for the dialog panel element. */
16
- panelClass?: string | string[];
17
- /** Where to focus when the dialog opens. Defaults to the first focusable element. */
18
- autoFocus?: boolean | string;
19
- /** Whether to restore focus to the trigger after the dialog closes. Defaults to `true`. */
20
- restoreFocus?: boolean | string;
21
- /** Panel width. */
22
- width?: string;
23
- /** Maximum panel width. */
24
- maxWidth?: string;
25
- /** Panel height. */
26
- height?: string;
27
- /** When `true`, clicking the backdrop or pressing Escape won't close the dialog. */
28
- disableClose?: boolean;
29
- }
30
-
31
- /**
32
- * Reference returned by `KjDialogService.open()`.
33
- * Use `close(result?)` to dismiss the dialog and emit the result.
34
- */
35
- export class KjDialogRef<R = unknown> {
36
- private readonly _closed = signal<R | undefined>(undefined);
37
- private _closedCallbacks: Array<(result: R | undefined) => void> = [];
38
-
39
- /** @internal */
40
- constructor(
41
- private readonly container: HTMLElement,
42
- private readonly viewRef: EmbeddedViewRef<unknown> | ComponentRef<unknown>,
43
- private readonly appRef: ApplicationRef,
44
- private readonly previouslyFocused: Element | null,
45
- private readonly restoreFocus: boolean,
46
- ) {}
47
-
48
- /** Closes the dialog and emits `result` to subscribers. */
49
- close(result?: R): void {
50
- this.appRef.detachView(
51
- this.viewRef instanceof ComponentRef
52
- ? this.viewRef.hostView as EmbeddedViewRef<unknown>
53
- : this.viewRef,
54
- );
55
- this.viewRef.destroy();
56
- this.container.remove();
57
-
58
- if (this.restoreFocus && this.previouslyFocused instanceof HTMLElement) {
59
- this.previouslyFocused.focus();
60
- }
61
-
62
- this._closedCallbacks.forEach(cb => cb(result));
63
- this._closedCallbacks = [];
64
- }
65
-
66
- /**
67
- * Registers a callback invoked when the dialog closes.
68
- * @param cb - Callback receiving the close result.
69
- */
70
- afterClosed(cb: (result: R | undefined) => void): this {
71
- this._closedCallbacks.push(cb);
72
- return this;
73
- }
74
- }
75
-
76
- /**
77
- * Programmatic service for opening dialogs with components or templates.
78
- * The opened component can inject `DIALOG_DATA` for typed data and `KjDialogRef` to close itself.
79
- *
80
- * @example
81
- * ```ts
82
- * // Open a component as a dialog
83
- * private readonly dialog = inject(KjDialogService);
84
- *
85
- * openConfirm() {
86
- * const ref = this.dialog.open(ConfirmDialogComponent, {
87
- * data: { message: 'Delete this item?' },
88
- * });
89
- * ref.afterClosed(result => {
90
- * if (result) this.doDelete();
91
- * });
92
- * }
93
- * ```
94
- *
95
- * ```ts
96
- * // Inside ConfirmDialogComponent
97
- * readonly data = inject<{ message: string }>(DIALOG_DATA);
98
- * readonly ref = inject(KjDialogRef);
99
- * confirm() { this.ref.close(true); }
100
- * cancel() { this.ref.close(false); }
101
- * ```
102
- * @category Core/Overlays
103
- */
104
- @Injectable({ providedIn: 'root' })
105
- export class KjDialogService {
106
- private readonly appRef = inject(ApplicationRef);
107
- private readonly injector = inject(Injector);
108
-
109
- /**
110
- * Opens a component as a modal dialog.
111
- * @param content - Angular component type to render inside the dialog.
112
- * @param config - Optional configuration: data, panelClass, width, autoFocus, etc.
113
- * @returns A `KjDialogRef` whose `afterClosed()` fires when the dialog closes.
114
- */
115
- open<R = unknown, D = unknown, C = unknown>(
116
- content: Type<C> | TemplateRef<C>,
117
- config?: KjDialogOpenConfig<D>,
118
- ): KjDialogRef<R> {
119
- const container = document.createElement('div');
120
- container.setAttribute('data-kj-dialog-container', '');
121
-
122
- const previouslyFocused = document.activeElement;
123
- const restoreFocus = config?.restoreFocus !== false;
124
-
125
- const dialogRef = new KjDialogRef<R>(
126
- container,
127
- null as never, // filled below
128
- this.appRef,
129
- previouslyFocused,
130
- restoreFocus,
131
- );
132
-
133
- const childInjector = Injector.create({
134
- parent: this.injector,
135
- providers: [
136
- { provide: DIALOG_DATA, useValue: config?.data },
137
- { provide: KjDialogRef, useValue: dialogRef },
138
- ],
139
- });
140
-
141
- let viewRef: EmbeddedViewRef<unknown> | ComponentRef<unknown>;
142
-
143
- if (content instanceof TemplateRef) {
144
- const vcr = this.appRef.components[0]?.injector.get(ViewContainerRef, null);
145
- if (!vcr) throw new Error('KjDialogService: no root ViewContainerRef available');
146
- viewRef = vcr.createEmbeddedView(content as TemplateRef<unknown>, {}, { injector: childInjector });
147
- this.appRef.attachView(viewRef as EmbeddedViewRef<unknown>);
148
- (viewRef as EmbeddedViewRef<unknown>).rootNodes.forEach(n => container.appendChild(n));
149
- } else {
150
- const compRef = createComponent(content as Type<unknown>, {
151
- environmentInjector: this.appRef.injector,
152
- elementInjector: childInjector,
153
- });
154
- this.appRef.attachView(compRef.hostView);
155
- container.appendChild((compRef.hostView as EmbeddedViewRef<unknown>).rootNodes[0]);
156
- viewRef = compRef;
157
- }
158
-
159
- // Patch the viewRef into the dialogRef (bypass readonly via cast)
160
- (dialogRef as unknown as { viewRef: typeof viewRef }).viewRef = viewRef;
161
-
162
- document.body.appendChild(container);
163
-
164
- // Auto-focus first tabbable element
165
- if (config?.autoFocus !== false) {
166
- const target = typeof config?.autoFocus === 'string'
167
- ? container.querySelector<HTMLElement>(config.autoFocus)
168
- : container.querySelector<HTMLElement>(
169
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
170
- );
171
- target?.focus();
172
- }
173
-
174
- return dialogRef;
175
- }
176
- }
@@ -1,122 +0,0 @@
1
- import { fireEvent } from '@testing-library/angular';
2
- import { render } from '@testing-library/angular';
3
- import { axe, toHaveNoViolations } from 'jest-axe';
4
- import {
5
- KjDialogTrigger, KjDialog, KjDialogOverlay,
6
- KjDialogTitle, KjDialogClose,
7
- } from './dialog';
8
-
9
- expect.extend(toHaveNoViolations);
10
-
11
- const imports = [
12
- KjDialogTrigger, KjDialog, KjDialogOverlay,
13
- KjDialogTitle, KjDialogClose,
14
- ];
15
-
16
- describe('KjDialogTrigger', () => {
17
- it('renders trigger button', async () => {
18
- const { getByRole } = await render(
19
- `<button [kjDialogTrigger]="dlg">Open</button>
20
- <ng-template #dlg>
21
- <div kjDialogOverlay><div kjDialog><h2 kjDialogTitle>T</h2></div></div>
22
- </ng-template>`,
23
- { imports },
24
- );
25
- expect(getByRole('button', { name: 'Open' })).toBeInTheDocument();
26
- });
27
-
28
- it('trigger has aria-haspopup=dialog', async () => {
29
- const { getByRole } = await render(
30
- `<button [kjDialogTrigger]="dlg">Open</button>
31
- <ng-template #dlg>
32
- <div kjDialogOverlay><div kjDialog><h2 kjDialogTitle>T</h2></div></div>
33
- </ng-template>`,
34
- { imports },
35
- );
36
- expect(getByRole('button', { name: 'Open' })).toHaveAttribute('aria-haspopup', 'dialog');
37
- });
38
-
39
- it('trigger has aria-expanded=false initially', async () => {
40
- const { getByRole } = await render(
41
- `<button [kjDialogTrigger]="dlg">Open</button>
42
- <ng-template #dlg>
43
- <div kjDialogOverlay><div kjDialog><h2 kjDialogTitle>T</h2></div></div>
44
- </ng-template>`,
45
- { imports },
46
- );
47
- expect(getByRole('button', { name: 'Open' })).toHaveAttribute('aria-expanded', 'false');
48
- });
49
-
50
- it('opens dialog and sets aria-expanded=true on click', async () => {
51
- const { getByRole } = await render(
52
- `<button [kjDialogTrigger]="dlg">Open</button>
53
- <ng-template #dlg>
54
- <div kjDialogOverlay><div kjDialog><h2 kjDialogTitle>Settings</h2></div></div>
55
- </ng-template>`,
56
- { imports },
57
- );
58
- const trigger = getByRole('button', { name: 'Open' });
59
- fireEvent.click(trigger);
60
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
61
- });
62
-
63
- it('mounts dialog content in document.body on open', async () => {
64
- const { getByRole } = await render(
65
- `<button [kjDialogTrigger]="dlg">Open</button>
66
- <ng-template #dlg>
67
- <div kjDialogOverlay>
68
- <div kjDialog><h2 kjDialogTitle>My Dialog</h2></div>
69
- </div>
70
- </ng-template>`,
71
- { imports },
72
- );
73
- fireEvent.click(getByRole('button', { name: 'Open' }));
74
- expect(document.body.querySelector('[data-kj-dialog-container]')).toBeTruthy();
75
- });
76
-
77
- it('closes dialog on overlay click (close on backdrop enabled by default)', async () => {
78
- const { getByRole } = await render(
79
- `<button [kjDialogTrigger]="dlg">Open</button>
80
- <ng-template #dlg>
81
- <div kjDialogOverlay>
82
- <div kjDialog><h2 kjDialogTitle>T</h2></div>
83
- </div>
84
- </ng-template>`,
85
- { imports },
86
- );
87
- const trigger = getByRole('button', { name: 'Open' });
88
- fireEvent.click(trigger);
89
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
90
-
91
- const overlay = document.querySelector('[kjDialogOverlay]') ?? document.body.querySelector('div[data-kj-dialog-container] > div');
92
- if (overlay) fireEvent.click(overlay);
93
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
94
- });
95
-
96
- it('closes dialog on Escape key when closeOnEscape is true', async () => {
97
- const { getByRole } = await render(
98
- `<button [kjDialogTrigger]="dlg">Open</button>
99
- <ng-template #dlg>
100
- <div kjDialogOverlay><div kjDialog><h2 kjDialogTitle>T</h2></div></div>
101
- </ng-template>`,
102
- { imports },
103
- );
104
- const trigger = getByRole('button', { name: 'Open' });
105
- fireEvent.click(trigger);
106
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
107
-
108
- fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' });
109
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
110
- });
111
-
112
- it('passes axe audit on trigger', async () => {
113
- const { container } = await render(
114
- `<button [kjDialogTrigger]="dlg">Open dialog</button>
115
- <ng-template #dlg>
116
- <div kjDialogOverlay><div kjDialog><h2 kjDialogTitle>Settings</h2></div></div>
117
- </ng-template>`,
118
- { imports },
119
- );
120
- expect(await axe(container)).toHaveNoViolations();
121
- });
122
- });
@@ -1,201 +0,0 @@
1
- import {
2
- ApplicationRef, Directive, DestroyRef, EmbeddedViewRef,
3
- HostListener, Injector, TemplateRef,
4
- computed, inject, input, output, signal,
5
- PLATFORM_ID,
6
- } from '@angular/core';
7
- import { isPlatformBrowser } from '@angular/common';
8
- import { KJ_DIALOG, KjDialogContext } from './dialog.context';
9
-
10
- let _dialogIdCounter = 0;
11
-
12
- /**
13
- * Trigger that opens the dialog. Place on any button or interactive element.
14
- * Takes a `TemplateRef` input that defines the full overlay structure.
15
- *
16
- * The template should contain `[kjDialogOverlay]` (backdrop) wrapping `[kjDialog]` (panel).
17
- * Inside the panel use `[kjDialogTitle]` and `[kjDialogClose]`.
18
- * Export `kjDialog` as a template ref (`#dlg="kjDialog"`) to call `dlg.close(result?)`.
19
- *
20
- * @example
21
- * ```html
22
- * <button kjButton [kjDialogTrigger]="myDialog" (kjDialogClosed)="onResult($event)">Open</button>
23
- *
24
- * <ng-template #myDialog>
25
- * <div kjDialogOverlay>
26
- * <div kjDialog #dlg="kjDialog">
27
- * <h2 kjDialogTitle>Title</h2>
28
- * <button kjDialogClose>Cancel</button>
29
- * <button (click)="dlg.close('saved')">Save</button>
30
- * </div>
31
- * </div>
32
- * </ng-template>
33
- * ```
34
- * @doc
35
- * @doc-example Basic
36
- * @doc-theme default
37
- * @doc-file dialog.example.ts
38
- * @doc-theme retro
39
- * @doc-file dialog.retro.example.ts
40
- * @doc-theme finance
41
- * @doc-file dialog.finance.example.ts
42
- * @doc-example Confirmation
43
- * @doc-file dialog.confirm.example.ts
44
- * @category Core/Overlays
45
- */
46
- @Directive({
47
- selector: '[kjDialogTrigger]',
48
- standalone: true,
49
- providers: [{ provide: KJ_DIALOG, useExisting: KjDialogTrigger }],
50
- host: {
51
- '[attr.aria-haspopup]': '"dialog"',
52
- '[attr.aria-expanded]': 'open().toString()',
53
- '(click)': 'openDialog()',
54
- },
55
- })
56
- export class KjDialogTrigger implements KjDialogContext {
57
- private readonly appRef = inject(ApplicationRef);
58
- private readonly injector = inject(Injector);
59
- private readonly destroyRef = inject(DestroyRef);
60
- private readonly platformId = inject(PLATFORM_ID);
61
-
62
- readonly kjDialogTrigger = input.required<TemplateRef<unknown>>();
63
- readonly kjDialogCloseOnEscape = input<boolean>(true);
64
- readonly kjDialogCloseOnBackdrop = input<boolean>(true);
65
- readonly kjDialogClosed = output<unknown>();
66
-
67
- readonly dialogId = `kj-dialog-${++_dialogIdCounter}`;
68
-
69
- private readonly _open = signal(false);
70
- readonly open = this._open.asReadonly();
71
- readonly closeOnEscape = computed(() => this.kjDialogCloseOnEscape());
72
- readonly closeOnBackdrop = computed(() => this.kjDialogCloseOnBackdrop());
73
-
74
- private viewRef?: EmbeddedViewRef<unknown>;
75
- private containerEl?: HTMLElement;
76
-
77
- constructor() {
78
- this.destroyRef.onDestroy(() => this.close());
79
- }
80
-
81
- openDialog(): void {
82
- if (this._open() || !isPlatformBrowser(this.platformId)) return;
83
-
84
- // Use TemplateRef.createEmbeddedView directly (not vcr.createEmbeddedView) so
85
- // the view is NOT pre-attached to any ViewContainer — ApplicationRef can own it.
86
- // Pass the trigger's injector so KJ_DIALOG is resolvable within the template.
87
- this.viewRef = this.kjDialogTrigger().createEmbeddedView(undefined as never, this.injector);
88
- this.appRef.attachView(this.viewRef);
89
-
90
- // Mount in a fixed container appended to body
91
- this.containerEl = document.createElement('div');
92
- this.containerEl.setAttribute('data-kj-dialog-container', '');
93
- this.viewRef.rootNodes.forEach(node => this.containerEl!.appendChild(node));
94
- document.body.appendChild(this.containerEl);
95
-
96
- this._open.set(true);
97
- }
98
-
99
- close(result?: unknown): void {
100
- if (!this._open()) return;
101
- this._open.set(false);
102
-
103
- if (this.viewRef) {
104
- this.appRef.detachView(this.viewRef);
105
- this.viewRef.destroy();
106
- this.viewRef = undefined;
107
- }
108
- this.containerEl?.remove();
109
- this.containerEl = undefined;
110
-
111
- this.kjDialogClosed.emit(result);
112
- }
113
-
114
- @HostListener('document:keydown.escape')
115
- onEscape(): void {
116
- if (this._open() && this.closeOnEscape()) this.close();
117
- }
118
- }
119
-
120
- /**
121
- * Panel container for the dialog. Place inside the template provided to `[kjDialogTrigger]`.
122
- * Auto-sets `role="dialog"`, `aria-modal`, and `aria-labelledby` (requires `[kjDialogTitle]` inside).
123
- * Export as `#dlg="kjDialog"` to call `dlg.close(result?)` from event handlers.
124
- *
125
- * @category Core/Overlays
126
- */
127
- @Directive({
128
- selector: '[kjDialog]',
129
- standalone: true,
130
- exportAs: 'kjDialog',
131
- host: {
132
- '[attr.role]': '"dialog"',
133
- '[attr.aria-modal]': '"true"',
134
- '[attr.aria-labelledby]': 'ctx.dialogId + "-title"',
135
- '(click)': '$event.stopPropagation()',
136
- },
137
- })
138
- export class KjDialog {
139
- readonly ctx = inject(KJ_DIALOG);
140
-
141
- close(result?: unknown): void {
142
- this.ctx.close(result);
143
- }
144
- }
145
-
146
- /**
147
- * Backdrop/overlay element. Place inside the template wrapping `[kjDialog]`.
148
- * Closes the dialog on click when `[kjDialogOverlayCloseOnClick]` is true (default).
149
- *
150
- * @category Core/Overlays
151
- */
152
- @Directive({
153
- selector: '[kjDialogOverlay]',
154
- standalone: true,
155
- host: {
156
- '(click)': 'onOverlayClick()',
157
- },
158
- })
159
- export class KjDialogOverlay {
160
- private readonly ctx = inject(KJ_DIALOG);
161
- readonly kjDialogOverlayCloseOnClick = input<boolean>(true);
162
-
163
- onOverlayClick(): void {
164
- if (this.ctx.closeOnBackdrop() && this.kjDialogOverlayCloseOnClick()) {
165
- this.ctx.close();
166
- }
167
- }
168
- }
169
-
170
- /**
171
- * Marks the dialog title. Sets `id` for `aria-labelledby` wiring on `[kjDialog]`.
172
- *
173
- * @category Core/Overlays
174
- */
175
- @Directive({
176
- selector: '[kjDialogTitle]',
177
- standalone: true,
178
- host: {
179
- '[attr.id]': 'ctx.dialogId + "-title"',
180
- },
181
- })
182
- export class KjDialogTitle {
183
- readonly ctx = inject(KJ_DIALOG);
184
- }
185
-
186
- /**
187
- * Closes the dialog on click without a result value.
188
- * For closing with a result, use `#dlg="kjDialog"` then `(click)="dlg.close(value)"`.
189
- *
190
- * @category Core/Overlays
191
- */
192
- @Directive({
193
- selector: '[kjDialogClose]',
194
- standalone: true,
195
- host: {
196
- '(click)': 'ctx.close()',
197
- },
198
- })
199
- export class KjDialogClose {
200
- readonly ctx = inject(KJ_DIALOG);
201
- }
@@ -1,9 +0,0 @@
1
- export {
2
- KjDialogTrigger,
3
- KjDialog,
4
- KjDialogOverlay,
5
- KjDialogClose,
6
- KjDialogTitle,
7
- } from './dialog';
8
- export { KJ_DIALOG, type KjDialogContext } from './dialog.context';
9
- export { KjDialogService, KjDialogRef, DIALOG_DATA, type KjDialogOpenConfig } from './dialog.service';
@@ -1,33 +0,0 @@
1
- // AOT-compiled example components for documentation.
2
- // One entry per directive × theme combination.
3
-
4
- // Button
5
- export { ButtonExample } from './button/button.example';
6
- export { ButtonRetroExample } from './button/button.retro.example';
7
- export { ButtonFinanceExample } from './button/button.finance.example';
8
- export { ButtonSizesExample } from './button/button.sizes.example';
9
-
10
- // Dialog
11
- export { DialogBasicExample } from './dialog/dialog.example';
12
- export { DialogRetroExample } from './dialog/dialog.retro.example';
13
- export { DialogFinanceExample } from './dialog/dialog.finance.example';
14
- export { DialogConfirmExample } from './dialog/dialog.confirm.example';
15
-
16
- // Tooltip
17
- export { TooltipBasicExample } from './tooltip/tooltip.example';
18
- export { TooltipRetroExample } from './tooltip/tooltip.retro.example';
19
- export { TooltipFinanceExample } from './tooltip/tooltip.finance.example';
20
- export { TooltipPlacementsExample } from './tooltip/tooltip.placements.example';
21
-
22
- // Popover
23
- export { PopoverBasicExample } from './popover/popover.example';
24
- export { PopoverRetroExample } from './popover/popover.retro.example';
25
- export { PopoverFinanceExample } from './popover/popover.finance.example';
26
-
27
- // Menu
28
- export { MenuBasicExample } from './menu/menu.example';
29
-
30
- // Toast
31
- export { ToastBasicExample } from './toast/toast.example';
32
- export { ToastRetroExample } from './toast/toast.retro.example';
33
- export { ToastFinanceExample } from './toast/toast.finance.example';
@@ -1,3 +0,0 @@
1
- import { InjectionToken, Signal } from '@angular/core';
2
- export interface KjFormFieldContext { invalid: Signal<boolean>; }
3
- export const KJ_FORM_FIELD = new InjectionToken<KjFormFieldContext>('KjFormField');
@@ -1,25 +0,0 @@
1
- import { render } from '@testing-library/angular';
2
- import { axe, toHaveNoViolations } from 'jest-axe';
3
- import { KjFormField, KjFormError, KjFormLabel } from './form';
4
-
5
- expect.extend(toHaveNoViolations);
6
- const imports = [KjFormField, KjFormError, KjFormLabel];
7
-
8
- describe('KjFormField', () => {
9
- it('sets data-invalid when invalid', async () => {
10
- const { container } = await render(`<div kjFormField [kjFieldInvalid]="true"><label kjFormLabel for="f">Name</label><input id="f" /><span kjFormError>Required</span></div>`, { imports });
11
- expect(container.querySelector('[kjFormField]')).toHaveAttribute('data-invalid', '');
12
- });
13
- it('error visible when invalid', async () => {
14
- const { getByText } = await render(`<div kjFormField [kjFieldInvalid]="true"><label kjFormLabel for="f">Name</label><input id="f" /><span kjFormError>Required</span></div>`, { imports });
15
- expect(getByText('Required')).not.toHaveAttribute('hidden');
16
- });
17
- it('error hidden when valid', async () => {
18
- const { getByText } = await render(`<div kjFormField [kjFieldInvalid]="false"><label kjFormLabel for="f">Name</label><input id="f" /><span kjFormError>Required</span></div>`, { imports });
19
- expect(getByText('Required').closest('[kjFormError]')).toHaveAttribute('hidden', '');
20
- });
21
- it('passes axe audit', async () => {
22
- const { container } = await render(`<div kjFormField [kjFieldInvalid]="false"><label kjFormLabel for="e">Email</label><input id="e" type="email" /></div>`, { imports });
23
- expect(await axe(container)).toHaveNoViolations();
24
- });
25
- });
package/src/form/form.ts DELETED
@@ -1,46 +0,0 @@
1
- import { Directive, computed, inject, input } from '@angular/core';
2
- import { KJ_FORM_FIELD, KjFormFieldContext } from './form.context';
3
-
4
- /**
5
- * Container for a form field. Groups label, input, and error message.
6
- * @example
7
- * ```html
8
- * <div kjFormField [kjFieldInvalid]="ctrl.invalid && ctrl.touched">
9
- * <label kjFormLabel for="name">Name</label>
10
- * <input id="name" kjInput />
11
- * <span kjFormError>Name is required</span>
12
- * </div>
13
- * ```
14
- * @category Core/Inputs
15
- */
16
- @Directive({
17
- selector: '[kjFormField]', standalone: true,
18
- providers: [{ provide: KJ_FORM_FIELD, useExisting: KjFormField }],
19
- host: { '[attr.data-invalid]': 'kjFieldInvalid() ? "" : null' },
20
- })
21
- export class KjFormField implements KjFormFieldContext {
22
- /** Whether the field is in an invalid state. */
23
- kjFieldInvalid = input<boolean>(false);
24
- readonly invalid = computed(() => this.kjFieldInvalid());
25
- }
26
-
27
- /**
28
- * Label element within a form field.
29
- * @category Core/Inputs
30
- */
31
- @Directive({
32
- selector: '[kjFormLabel]', standalone: true,
33
- host: { '[attr.data-invalid]': 'ctx.invalid() ? "" : null' },
34
- })
35
- export class KjFormLabel { readonly ctx = inject(KJ_FORM_FIELD); }
36
-
37
- /**
38
- * Error message element. Visible only when field is invalid.
39
- * @example `<span kjFormError>Email is invalid</span>`
40
- * @category Core/Inputs
41
- */
42
- @Directive({
43
- selector: '[kjFormError]', standalone: true,
44
- host: { role: 'alert', 'aria-live': 'polite', '[attr.hidden]': '!ctx.invalid() ? "" : null' },
45
- })
46
- export class KjFormError { readonly ctx = inject(KJ_FORM_FIELD); }
package/src/form/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export { KjFormField, KjFormLabel, KjFormError } from './form';
2
- export { KJ_FORM_FIELD, type KjFormFieldContext } from './form.context';
@@ -1 +0,0 @@
1
- export { KjInput } from './input';
@@ -1,43 +0,0 @@
1
- import { render } from '@testing-library/angular';
2
- import { FormControl, ReactiveFormsModule } from '@angular/forms';
3
- import { axe, toHaveNoViolations } from 'jest-axe';
4
- import { KjInput } from './input';
5
-
6
- expect.extend(toHaveNoViolations);
7
-
8
- describe('KjInput', () => {
9
- it('renders without error', async () => {
10
- const { container } = await render(`<input kjInput type="text" />`, { imports: [KjInput] });
11
- expect(container.querySelector('input')).toBeInTheDocument();
12
- });
13
-
14
- it('sets aria-invalid and data-invalid when touched + invalid', async () => {
15
- const ctrl = new FormControl('', { updateOn: 'blur' });
16
- const { container } = await render(
17
- `<input kjInput [formControl]="ctrl" [kjInvalid]="ctrl.invalid" />`,
18
- { imports: [KjInput, ReactiveFormsModule], componentProperties: { ctrl } },
19
- );
20
- const input = container.querySelector('input')!;
21
- input.dispatchEvent(new Event('blur'));
22
- await new Promise(r => setTimeout(r, 0));
23
- // touched is set, but ctrl is not invalid by default with no validators
24
- expect(input).toBeInTheDocument();
25
- });
26
-
27
- it('works with formControl binding', async () => {
28
- const ctrl = new FormControl('hello');
29
- const { container } = await render(
30
- `<input kjInput [formControl]="ctrl" />`,
31
- { imports: [KjInput, ReactiveFormsModule], componentProperties: { ctrl } },
32
- );
33
- expect((container.querySelector('input') as HTMLInputElement).value).toBe('hello');
34
- });
35
-
36
- it('passes axe audit', async () => {
37
- const { container } = await render(
38
- `<label for="n">Name</label><input id="n" kjInput type="text" />`,
39
- { imports: [KjInput] },
40
- );
41
- expect(await axe(container)).toHaveNoViolations();
42
- });
43
- });