@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.
- package/fesm2022/kouji-ui-core.mjs +26789 -0
- package/fesm2022/kouji-ui-core.mjs.map +1 -0
- package/icon/icon.css +40 -0
- package/package.json +20 -9
- package/types/kouji-ui-core.d.ts +13545 -0
- package/typography/prose.css +312 -0
- package/.turbo/turbo-build.log +0 -27
- package/CHANGELOG.md +0 -21
- package/ng-package.json +0 -7
- package/src/a11y/.gitkeep +0 -0
- package/src/a11y/aria-describedby.spec.ts +0 -55
- package/src/a11y/aria-describedby.ts +0 -33
- package/src/a11y/focus-trap.spec.ts +0 -86
- package/src/a11y/focus-trap.ts +0 -70
- package/src/a11y/index.ts +0 -9
- package/src/a11y/live-region.spec.ts +0 -58
- package/src/a11y/live-region.ts +0 -48
- package/src/a11y/roving-tabindex.spec.ts +0 -62
- package/src/a11y/roving-tabindex.ts +0 -104
- package/src/a11y/visually-hidden.spec.ts +0 -34
- package/src/a11y/visually-hidden.ts +0 -24
- package/src/accordion/accordion.context.ts +0 -22
- package/src/accordion/accordion.spec.ts +0 -32
- package/src/accordion/accordion.ts +0 -129
- package/src/accordion/index.ts +0 -2
- package/src/avatar/avatar.spec.ts +0 -18
- package/src/avatar/avatar.ts +0 -26
- package/src/avatar/index.ts +0 -1
- package/src/badge/badge.spec.ts +0 -20
- package/src/badge/badge.ts +0 -14
- package/src/badge/index.ts +0 -1
- package/src/button/button.example.ts +0 -30
- package/src/button/button.finance.example.ts +0 -37
- package/src/button/button.retro.example.ts +0 -47
- package/src/button/button.sizes.example.ts +0 -25
- package/src/button/button.spec.ts +0 -32
- package/src/button/button.ts +0 -44
- package/src/button/index.ts +0 -1
- package/src/chart/chart.spec.ts +0 -27
- package/src/chart/chart.ts +0 -48
- package/src/chart/index.ts +0 -1
- package/src/checkbox/checkbox.spec.ts +0 -35
- package/src/checkbox/checkbox.ts +0 -82
- package/src/checkbox/index.ts +0 -1
- package/src/dialog/dialog.confirm.example.ts +0 -57
- package/src/dialog/dialog.context.ts +0 -11
- package/src/dialog/dialog.example.ts +0 -40
- package/src/dialog/dialog.finance.example.ts +0 -47
- package/src/dialog/dialog.retro.example.ts +0 -46
- package/src/dialog/dialog.service.ts +0 -176
- package/src/dialog/dialog.spec.ts +0 -122
- package/src/dialog/dialog.ts +0 -201
- package/src/dialog/index.ts +0 -9
- package/src/example-components.ts +0 -33
- package/src/form/form.context.ts +0 -3
- package/src/form/form.spec.ts +0 -25
- package/src/form/form.ts +0 -46
- package/src/form/index.ts +0 -2
- package/src/input/index.ts +0 -1
- package/src/input/input.spec.ts +0 -43
- package/src/input/input.ts +0 -80
- package/src/menu/index.ts +0 -2
- package/src/menu/menu.context.ts +0 -10
- package/src/menu/menu.example.ts +0 -39
- package/src/menu/menu.spec.ts +0 -168
- package/src/menu/menu.ts +0 -176
- package/src/popover/index.ts +0 -12
- package/src/popover/popover.context.ts +0 -18
- package/src/popover/popover.example.ts +0 -50
- package/src/popover/popover.finance.example.ts +0 -52
- package/src/popover/popover.retro.example.ts +0 -53
- package/src/popover/popover.spec.ts +0 -52
- package/src/popover/popover.ts +0 -124
- package/src/primitives/.gitkeep +0 -0
- package/src/primitives/forms/form-control.spec.ts +0 -59
- package/src/primitives/forms/form-control.ts +0 -90
- package/src/primitives/forms/index.ts +0 -1
- package/src/primitives/index.ts +0 -3
- package/src/primitives/interaction/disabled.spec.ts +0 -47
- package/src/primitives/interaction/disabled.ts +0 -24
- package/src/primitives/interaction/focus-ring.spec.ts +0 -62
- package/src/primitives/interaction/focus-ring.ts +0 -57
- package/src/primitives/interaction/index.ts +0 -2
- package/src/primitives/overlay/index.ts +0 -1
- package/src/primitives/overlay/overlay.spec.ts +0 -90
- package/src/primitives/overlay/overlay.ts +0 -106
- package/src/public-api.ts +0 -35
- package/src/radio/index.ts +0 -2
- package/src/radio/radio.context.ts +0 -3
- package/src/radio/radio.spec.ts +0 -36
- package/src/radio/radio.ts +0 -55
- package/src/select/index.ts +0 -2
- package/src/select/select.context.ts +0 -3
- package/src/select/select.spec.ts +0 -145
- package/src/select/select.ts +0 -250
- package/src/styles/docs-themes.css +0 -82
- package/src/table/index.ts +0 -1
- package/src/table/table.spec.ts +0 -113
- package/src/table/table.ts +0 -126
- package/src/tabs/index.ts +0 -2
- package/src/tabs/tabs.context.ts +0 -9
- package/src/tabs/tabs.spec.ts +0 -40
- package/src/tabs/tabs.ts +0 -130
- package/src/test-setup.ts +0 -5
- package/src/toast/index.ts +0 -21
- package/src/toast/toast.example.ts +0 -134
- package/src/toast/toast.finance.example.ts +0 -68
- package/src/toast/toast.retro.example.ts +0 -69
- package/src/toast/toast.service.ts +0 -153
- package/src/toast/toast.spec.ts +0 -95
- package/src/toast/toast.strategy.ts +0 -102
- package/src/toast/toast.ts +0 -307
- package/src/toast/toast.types.ts +0 -5
- package/src/toggle/index.ts +0 -1
- package/src/toggle/toggle.spec.ts +0 -24
- package/src/toggle/toggle.ts +0 -40
- package/src/tooltip/index.ts +0 -5
- package/src/tooltip/tooltip.example.ts +0 -46
- package/src/tooltip/tooltip.finance.example.ts +0 -54
- package/src/tooltip/tooltip.placements.example.ts +0 -56
- package/src/tooltip/tooltip.retro.example.ts +0 -65
- package/src/tooltip/tooltip.spec.ts +0 -104
- package/src/tooltip/tooltip.ts +0 -129
- package/tsconfig.lib.json +0 -13
- package/tsconfig.lib.prod.json +0 -11
- package/tsconfig.spec.json +0 -10
- 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
|
-
});
|
package/src/dialog/dialog.ts
DELETED
|
@@ -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
|
-
}
|
package/src/dialog/index.ts
DELETED
|
@@ -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';
|
package/src/form/form.context.ts
DELETED
package/src/form/form.spec.ts
DELETED
|
@@ -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
package/src/input/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { KjInput } from './input';
|
package/src/input/input.spec.ts
DELETED
|
@@ -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
|
-
});
|