@kouji-ui/core 0.0.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/.turbo/turbo-build.log +27 -0
- package/CHANGELOG.md +7 -0
- package/README.md +64 -0
- package/ng-package.json +7 -0
- package/package.json +39 -0
- package/src/a11y/.gitkeep +0 -0
- package/src/a11y/aria-describedby.spec.ts +55 -0
- package/src/a11y/aria-describedby.ts +33 -0
- package/src/a11y/focus-trap.spec.ts +86 -0
- package/src/a11y/focus-trap.ts +70 -0
- package/src/a11y/index.ts +9 -0
- package/src/a11y/live-region.spec.ts +58 -0
- package/src/a11y/live-region.ts +48 -0
- package/src/a11y/roving-tabindex.spec.ts +62 -0
- package/src/a11y/roving-tabindex.ts +104 -0
- package/src/a11y/visually-hidden.spec.ts +34 -0
- package/src/a11y/visually-hidden.ts +24 -0
- package/src/accordion/accordion.context.ts +22 -0
- package/src/accordion/accordion.spec.ts +32 -0
- package/src/accordion/accordion.ts +129 -0
- package/src/accordion/index.ts +2 -0
- package/src/avatar/avatar.spec.ts +18 -0
- package/src/avatar/avatar.ts +26 -0
- package/src/avatar/index.ts +1 -0
- package/src/badge/badge.spec.ts +20 -0
- package/src/badge/badge.ts +14 -0
- package/src/badge/index.ts +1 -0
- package/src/button/button.example.ts +30 -0
- package/src/button/button.finance.example.ts +37 -0
- package/src/button/button.retro.example.ts +47 -0
- package/src/button/button.sizes.example.ts +25 -0
- package/src/button/button.spec.ts +32 -0
- package/src/button/button.ts +44 -0
- package/src/button/index.ts +1 -0
- package/src/chart/chart.spec.ts +27 -0
- package/src/chart/chart.ts +48 -0
- package/src/chart/index.ts +1 -0
- package/src/checkbox/checkbox.spec.ts +35 -0
- package/src/checkbox/checkbox.ts +82 -0
- package/src/checkbox/index.ts +1 -0
- package/src/dialog/dialog.confirm.example.ts +57 -0
- package/src/dialog/dialog.context.ts +11 -0
- package/src/dialog/dialog.example.ts +40 -0
- package/src/dialog/dialog.finance.example.ts +47 -0
- package/src/dialog/dialog.retro.example.ts +46 -0
- package/src/dialog/dialog.service.ts +176 -0
- package/src/dialog/dialog.spec.ts +122 -0
- package/src/dialog/dialog.ts +201 -0
- package/src/dialog/index.ts +9 -0
- package/src/example-components.ts +33 -0
- package/src/form/form.context.ts +3 -0
- package/src/form/form.spec.ts +25 -0
- package/src/form/form.ts +46 -0
- package/src/form/index.ts +2 -0
- package/src/input/index.ts +1 -0
- package/src/input/input.spec.ts +43 -0
- package/src/input/input.ts +80 -0
- package/src/menu/index.ts +2 -0
- package/src/menu/menu.context.ts +10 -0
- package/src/menu/menu.example.ts +39 -0
- package/src/menu/menu.spec.ts +168 -0
- package/src/menu/menu.ts +176 -0
- package/src/popover/index.ts +12 -0
- package/src/popover/popover.context.ts +18 -0
- package/src/popover/popover.example.ts +50 -0
- package/src/popover/popover.finance.example.ts +52 -0
- package/src/popover/popover.retro.example.ts +53 -0
- package/src/popover/popover.spec.ts +52 -0
- package/src/popover/popover.ts +124 -0
- package/src/primitives/.gitkeep +0 -0
- package/src/primitives/forms/form-control.spec.ts +59 -0
- package/src/primitives/forms/form-control.ts +90 -0
- package/src/primitives/forms/index.ts +1 -0
- package/src/primitives/index.ts +3 -0
- package/src/primitives/interaction/disabled.spec.ts +47 -0
- package/src/primitives/interaction/disabled.ts +24 -0
- package/src/primitives/interaction/focus-ring.spec.ts +62 -0
- package/src/primitives/interaction/focus-ring.ts +57 -0
- package/src/primitives/interaction/index.ts +2 -0
- package/src/primitives/overlay/index.ts +1 -0
- package/src/primitives/overlay/overlay.spec.ts +90 -0
- package/src/primitives/overlay/overlay.ts +102 -0
- package/src/public-api.ts +35 -0
- package/src/radio/index.ts +2 -0
- package/src/radio/radio.context.ts +3 -0
- package/src/radio/radio.spec.ts +36 -0
- package/src/radio/radio.ts +55 -0
- package/src/select/index.ts +2 -0
- package/src/select/select.context.ts +3 -0
- package/src/select/select.spec.ts +145 -0
- package/src/select/select.ts +250 -0
- package/src/styles/docs-themes.css +82 -0
- package/src/table/index.ts +1 -0
- package/src/table/table.spec.ts +113 -0
- package/src/table/table.ts +126 -0
- package/src/tabs/index.ts +2 -0
- package/src/tabs/tabs.context.ts +9 -0
- package/src/tabs/tabs.spec.ts +40 -0
- package/src/tabs/tabs.ts +130 -0
- package/src/test-setup.ts +5 -0
- package/src/toast/index.ts +21 -0
- package/src/toast/toast.example.ts +134 -0
- package/src/toast/toast.finance.example.ts +68 -0
- package/src/toast/toast.retro.example.ts +69 -0
- package/src/toast/toast.service.ts +153 -0
- package/src/toast/toast.spec.ts +94 -0
- package/src/toast/toast.strategy.ts +102 -0
- package/src/toast/toast.ts +307 -0
- package/src/toast/toast.types.ts +5 -0
- package/src/toggle/index.ts +1 -0
- package/src/toggle/toggle.spec.ts +24 -0
- package/src/toggle/toggle.ts +40 -0
- package/src/tooltip/index.ts +5 -0
- package/src/tooltip/tooltip.example.ts +46 -0
- package/src/tooltip/tooltip.finance.example.ts +54 -0
- package/src/tooltip/tooltip.placements.example.ts +56 -0
- package/src/tooltip/tooltip.retro.example.ts +65 -0
- package/src/tooltip/tooltip.spec.ts +104 -0
- package/src/tooltip/tooltip.ts +129 -0
- package/tsconfig.lib.json +13 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +10 -0
- package/vite.config.ts +19 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/angular';
|
|
2
|
+
import { axe, toHaveNoViolations } from 'jest-axe';
|
|
3
|
+
import { KjTooltipContent, KjTooltipTrigger } from './tooltip';
|
|
4
|
+
|
|
5
|
+
expect.extend(toHaveNoViolations);
|
|
6
|
+
const imports = [KjTooltipTrigger, KjTooltipContent];
|
|
7
|
+
|
|
8
|
+
// [kjTooltipTrigger]="t" is a property binding — it does not appear as a DOM attribute.
|
|
9
|
+
// Select the trigger element by its tag or by aria-describedby (always set per WAI-ARIA spec).
|
|
10
|
+
// Select the content element by [kjTooltipContent] which IS a static directive attribute.
|
|
11
|
+
|
|
12
|
+
describe('KjTooltipContent + KjTooltipTrigger', () => {
|
|
13
|
+
it('content hidden by default', async () => {
|
|
14
|
+
const { container } = await render(
|
|
15
|
+
`<button [kjTooltipTrigger]="t">Hover</button>
|
|
16
|
+
<span #t kjTooltipContent [kjTooltipDelay]="0" [kjTooltipHideDelay]="0">Tip</span>`,
|
|
17
|
+
{ imports },
|
|
18
|
+
);
|
|
19
|
+
expect(container.querySelector('[kjTooltipContent]')).toHaveAttribute('hidden', '');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('content has role=tooltip by default', async () => {
|
|
23
|
+
const { container } = await render(
|
|
24
|
+
`<button [kjTooltipTrigger]="t">Hover</button>
|
|
25
|
+
<span #t kjTooltipContent [kjTooltipDelay]="0">Tip</span>`,
|
|
26
|
+
{ imports },
|
|
27
|
+
);
|
|
28
|
+
expect(container.querySelector('[kjTooltipContent]')).toHaveAttribute('role', 'tooltip');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('shows on mouseenter (no delay)', async () => {
|
|
32
|
+
const { container } = await render(
|
|
33
|
+
`<button [kjTooltipTrigger]="t">Hover</button>
|
|
34
|
+
<span #t kjTooltipContent [kjTooltipDelay]="0" [kjTooltipHideDelay]="0">Tip</span>`,
|
|
35
|
+
{ imports },
|
|
36
|
+
);
|
|
37
|
+
fireEvent.mouseEnter(container.querySelector('button')!);
|
|
38
|
+
expect(container.querySelector('[kjTooltipContent]')).not.toHaveAttribute('hidden');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('hides on mouseleave (no delay)', async () => {
|
|
42
|
+
const { container } = await render(
|
|
43
|
+
`<button [kjTooltipTrigger]="t">Hover</button>
|
|
44
|
+
<span #t kjTooltipContent [kjTooltipDelay]="0" [kjTooltipHideDelay]="0">Tip</span>`,
|
|
45
|
+
{ imports },
|
|
46
|
+
);
|
|
47
|
+
fireEvent.mouseEnter(container.querySelector('button')!);
|
|
48
|
+
fireEvent.mouseLeave(container.querySelector('button')!);
|
|
49
|
+
expect(container.querySelector('[kjTooltipContent]')).toHaveAttribute('hidden', '');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('hides on Escape key', async () => {
|
|
53
|
+
const { container } = await render(
|
|
54
|
+
`<button [kjTooltipTrigger]="t">Hover</button>
|
|
55
|
+
<span #t kjTooltipContent [kjTooltipDelay]="0">Tip</span>`,
|
|
56
|
+
{ imports },
|
|
57
|
+
);
|
|
58
|
+
fireEvent.mouseEnter(container.querySelector('button')!);
|
|
59
|
+
fireEvent.keyDown(container.querySelector('button')!, { key: 'Escape' });
|
|
60
|
+
expect(container.querySelector('[kjTooltipContent]')).toHaveAttribute('hidden', '');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('hides on blur', async () => {
|
|
64
|
+
const { container } = await render(
|
|
65
|
+
`<button [kjTooltipTrigger]="t">Hover</button>
|
|
66
|
+
<span #t kjTooltipContent [kjTooltipDelay]="0" [kjTooltipHideDelay]="0">Tip</span>`,
|
|
67
|
+
{ imports },
|
|
68
|
+
);
|
|
69
|
+
fireEvent.focus(container.querySelector('button')!);
|
|
70
|
+
fireEvent.blur(container.querySelector('button')!);
|
|
71
|
+
expect(container.querySelector('[kjTooltipContent]')).toHaveAttribute('hidden', '');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('trigger has aria-describedby wired to content id always', async () => {
|
|
75
|
+
const { container } = await render(
|
|
76
|
+
`<button [kjTooltipTrigger]="t">Info</button>
|
|
77
|
+
<span #t kjTooltipContent [kjTooltipDelay]="0">Info text</span>`,
|
|
78
|
+
{ imports },
|
|
79
|
+
);
|
|
80
|
+
const trigger = container.querySelector('button')!;
|
|
81
|
+
const content = container.querySelector('[kjTooltipContent]')!;
|
|
82
|
+
expect(trigger.getAttribute('aria-describedby')).toBeTruthy();
|
|
83
|
+
expect(trigger.getAttribute('aria-describedby')).toBe(content.id);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('role can be customised', async () => {
|
|
87
|
+
const { container } = await render(
|
|
88
|
+
`<button [kjTooltipTrigger]="t">X</button>
|
|
89
|
+
<span #t kjTooltipContent kjTooltipRole="note" [kjTooltipDelay]="0">Note</span>`,
|
|
90
|
+
{ imports },
|
|
91
|
+
);
|
|
92
|
+
expect(container.querySelector('[kjTooltipContent]')).toHaveAttribute('role', 'note');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('passes axe audit when shown', async () => {
|
|
96
|
+
const { container } = await render(
|
|
97
|
+
`<button [kjTooltipTrigger]="t">Info</button>
|
|
98
|
+
<span #t kjTooltipContent [kjTooltipDelay]="0">Info text</span>`,
|
|
99
|
+
{ imports },
|
|
100
|
+
);
|
|
101
|
+
fireEvent.mouseEnter(container.querySelector('button')!);
|
|
102
|
+
expect(await axe(container)).toHaveNoViolations();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Directive, ElementRef, inject, input, signal } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/** Preferred side for the tooltip relative to its trigger. */
|
|
4
|
+
export type KjTooltipSide = 'top' | 'bottom' | 'left' | 'right';
|
|
5
|
+
|
|
6
|
+
let _tooltipIdCounter = 0;
|
|
7
|
+
|
|
8
|
+
/** Maps a tooltip content element to its directive instance. */
|
|
9
|
+
const _tooltipRegistry = new WeakMap<Element, KjTooltipContent>();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Tooltip content panel. Owns all configuration and visibility state.
|
|
13
|
+
* Pair with `[kjTooltipTrigger]` — pass the native element reference using `#myTip` (no `exportAs` needed).
|
|
14
|
+
*
|
|
15
|
+
* Automatically sets `role="tooltip"` (customisable via `kjTooltipRole`).
|
|
16
|
+
* Note: `role="tooltip"` prohibits `aria-label` and `aria-labelledby` per WAI-ARIA.
|
|
17
|
+
*
|
|
18
|
+
* @doc
|
|
19
|
+
* @doc-example Basic
|
|
20
|
+
* @doc-theme default
|
|
21
|
+
* @doc-file tooltip.example.ts
|
|
22
|
+
* @doc-theme retro
|
|
23
|
+
* @doc-file tooltip.retro.example.ts
|
|
24
|
+
* @doc-theme finance
|
|
25
|
+
* @doc-file tooltip.finance.example.ts
|
|
26
|
+
* @doc-example Placements
|
|
27
|
+
* @doc-file tooltip.placements.example.ts
|
|
28
|
+
* @category Core/Overlays
|
|
29
|
+
* @required
|
|
30
|
+
*/
|
|
31
|
+
@Directive({
|
|
32
|
+
selector: '[kjTooltipContent]',
|
|
33
|
+
standalone: true,
|
|
34
|
+
host: {
|
|
35
|
+
'[attr.id]': 'tooltipId',
|
|
36
|
+
'[attr.role]': 'kjTooltipRole()',
|
|
37
|
+
'[attr.data-side]':'kjTooltipSide()',
|
|
38
|
+
'[attr.hidden]': '!visible() ? "" : null',
|
|
39
|
+
'(mouseenter)': 'cancelHide()',
|
|
40
|
+
'(mouseleave)': 'startHide()',
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
export class KjTooltipContent {
|
|
44
|
+
private readonly el = inject(ElementRef<HTMLElement>);
|
|
45
|
+
|
|
46
|
+
/** Preferred placement relative to the trigger. One of `'top'`, `'bottom'`, `'left'`, `'right'`. Defaults to `'top'`. */
|
|
47
|
+
readonly kjTooltipSide = input<KjTooltipSide>('top');
|
|
48
|
+
|
|
49
|
+
/** ARIA role for the tooltip element. Defaults to `'tooltip'`. Note: `role="tooltip"` prohibits `aria-label` and `aria-labelledby`. */
|
|
50
|
+
readonly kjTooltipRole = input<string>('tooltip');
|
|
51
|
+
|
|
52
|
+
/** Delay in ms before the tooltip appears on hover. Defaults to `600`. Set to `0` for instant. */
|
|
53
|
+
readonly kjTooltipDelay = input<number>(600);
|
|
54
|
+
|
|
55
|
+
/** Delay in ms before the tooltip disappears after hover ends. Defaults to `200`. */
|
|
56
|
+
readonly kjTooltipHideDelay = input<number>(200);
|
|
57
|
+
|
|
58
|
+
/** Auto-generated `id` wired to the trigger's `aria-describedby`. Always present — set before first display per WAI-ARIA spec. */
|
|
59
|
+
readonly tooltipId = `kj-tooltip-${++_tooltipIdCounter}`;
|
|
60
|
+
|
|
61
|
+
readonly visible = signal(false);
|
|
62
|
+
|
|
63
|
+
private showTimer?: ReturnType<typeof setTimeout>;
|
|
64
|
+
private hideTimer?: ReturnType<typeof setTimeout>;
|
|
65
|
+
|
|
66
|
+
constructor() {
|
|
67
|
+
// Register synchronously so KjTooltipTrigger's host binding can resolve immediately
|
|
68
|
+
_tooltipRegistry.set(this.el.nativeElement, this);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
show(): void {
|
|
72
|
+
clearTimeout(this.hideTimer);
|
|
73
|
+
const delay = this.kjTooltipDelay();
|
|
74
|
+
if (delay <= 0) { this.visible.set(true); return; }
|
|
75
|
+
this.showTimer = setTimeout(() => this.visible.set(true), delay);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
startHide(): void {
|
|
79
|
+
clearTimeout(this.showTimer);
|
|
80
|
+
const delay = this.kjTooltipHideDelay();
|
|
81
|
+
if (delay <= 0) { this.visible.set(false); return; }
|
|
82
|
+
this.hideTimer = setTimeout(() => this.visible.set(false), delay);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
cancelHide(): void {
|
|
86
|
+
clearTimeout(this.hideTimer);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
forceHide(): void {
|
|
90
|
+
clearTimeout(this.showTimer);
|
|
91
|
+
clearTimeout(this.hideTimer);
|
|
92
|
+
this.visible.set(false);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Trigger element for the tooltip. Pass the `[kjTooltipContent]` native element reference via `[kjTooltipTrigger]`.
|
|
98
|
+
*
|
|
99
|
+
* - Automatically wires `aria-describedby` to the content's `id` — always set before first display per WAI-ARIA spec
|
|
100
|
+
* - Shows on hover and focus, hides on leave, blur, or Escape
|
|
101
|
+
*
|
|
102
|
+
* @category Core/Overlays
|
|
103
|
+
* @required
|
|
104
|
+
*/
|
|
105
|
+
@Directive({
|
|
106
|
+
selector: '[kjTooltipTrigger]',
|
|
107
|
+
standalone: true,
|
|
108
|
+
host: {
|
|
109
|
+
'[attr.aria-describedby]': '_content?.tooltipId',
|
|
110
|
+
'(mouseenter)': '_show()',
|
|
111
|
+
'(mouseleave)': '_startHide()',
|
|
112
|
+
'(focus)': '_show()',
|
|
113
|
+
'(blur)': '_forceHide()',
|
|
114
|
+
'(keydown.escape)': '_forceHide()',
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
export class KjTooltipTrigger {
|
|
118
|
+
/** Native element of the `[kjTooltipContent]` to associate. Use `#myTip` *(no `exportAs`)* in the template. */
|
|
119
|
+
readonly kjTooltipTrigger = input.required<HTMLElement>();
|
|
120
|
+
|
|
121
|
+
/** Resolved `KjTooltipContent` instance looked up from the registry. */
|
|
122
|
+
protected get _content(): KjTooltipContent | undefined {
|
|
123
|
+
return _tooltipRegistry.get(this.kjTooltipTrigger());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
protected _show(): void { this._content?.show(); }
|
|
127
|
+
protected _startHide(): void { this._content?.startHide(); }
|
|
128
|
+
protected _forceHide(): void { this._content?.forceHide(); }
|
|
129
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "../../tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "../../out-tsc/lib",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"types": []
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*.ts"],
|
|
12
|
+
"exclude": ["**/*.spec.ts"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "./tsconfig.lib.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"declarationMap": false
|
|
7
|
+
},
|
|
8
|
+
"angularCompilerOptions": {
|
|
9
|
+
"compilationMode": "partial"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "../../tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "../../out-tsc/spec",
|
|
7
|
+
"types": ["vitest/globals"]
|
|
8
|
+
},
|
|
9
|
+
"include": ["src/**/*.d.ts", "src/**/*.spec.ts"]
|
|
10
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineProject } from 'vitest/config';
|
|
2
|
+
import angular from '@analogjs/vite-plugin-angular';
|
|
3
|
+
|
|
4
|
+
export default defineProject({
|
|
5
|
+
plugins: [angular()],
|
|
6
|
+
test: {
|
|
7
|
+
globals: true,
|
|
8
|
+
environment: 'jsdom',
|
|
9
|
+
setupFiles: ['src/test-setup.ts'],
|
|
10
|
+
include: ['src/**/*.spec.ts'],
|
|
11
|
+
reporters: ['default'],
|
|
12
|
+
coverage: {
|
|
13
|
+
provider: 'v8',
|
|
14
|
+
reporter: ['text', 'json'],
|
|
15
|
+
include: ['src/**/*.ts'],
|
|
16
|
+
exclude: ['src/**/*.spec.ts', 'src/test-setup.ts'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|