@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
package/src/input/input.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { Directive, ElementRef, effect, inject, input } from '@angular/core';
|
|
2
|
-
import { KjDisabled, KjFocusRing, KjFormControl } from '../primitives';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Enhances a native `<input>` with Angular forms integration, disabled/invalid state, and focus-ring.
|
|
6
|
-
* Supports `formControl`, `formControlName`, and `ngModel` bindings.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```html
|
|
10
|
-
* <input kjInput type="email" [formControl]="emailCtrl" [kjInvalid]="emailCtrl.invalid" />
|
|
11
|
-
* ```
|
|
12
|
-
* @doc
|
|
13
|
-
* @doc-file input.example.ts
|
|
14
|
-
* ```typescript
|
|
15
|
-
* import { Component } from '@angular/core';
|
|
16
|
-
* import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
17
|
-
* import { KjInput } from '@kouji-ui/core';
|
|
18
|
-
*
|
|
19
|
-
* @Component({
|
|
20
|
-
* standalone: true,
|
|
21
|
-
* imports: [KjInput, ReactiveFormsModule],
|
|
22
|
-
* styles: [`
|
|
23
|
-
* .form { padding: 2rem; background: #0c0c0c; display: flex; flex-direction: column; gap: 1rem; max-width: 400px; }
|
|
24
|
-
* [kjInput] { padding: 0.625rem 0.875rem; background: #111; border: 1px solid #222; color: #f0ede6; font-family: 'JetBrains Mono', monospace; font-size: 0.875rem; width: 100%; }
|
|
25
|
-
* [kjInput]:focus { outline: none; border-color: #b8f500; }
|
|
26
|
-
* [aria-invalid="true"] { border-color: #ef4444; }
|
|
27
|
-
* .error { color: #ef4444; font-size: 0.75rem; font-family: 'JetBrains Mono', monospace; }
|
|
28
|
-
* label { color: #888; font-size: 0.75rem; font-family: 'JetBrains Mono', monospace; }
|
|
29
|
-
* `],
|
|
30
|
-
* template: `
|
|
31
|
-
* <div class="form">
|
|
32
|
-
* <label for="email">Email</label>
|
|
33
|
-
* <input id="email" kjInput type="email" [formControl]="emailCtrl"
|
|
34
|
-
* [kjInvalid]="emailCtrl.invalid && emailCtrl.touched"
|
|
35
|
-
* placeholder="you@example.com" />
|
|
36
|
-
* @if (emailCtrl.invalid && emailCtrl.touched) {
|
|
37
|
-
* <span class="error">Please enter a valid email.</span>
|
|
38
|
-
* }
|
|
39
|
-
* <label for="disabled">Disabled input</label>
|
|
40
|
-
* <input id="disabled" kjInput type="text" [kjDisabled]="true" value="Not editable" />
|
|
41
|
-
* </div>
|
|
42
|
-
* `,
|
|
43
|
-
* })
|
|
44
|
-
* export class InputExampleComponent {
|
|
45
|
-
* emailCtrl = new FormControl('', [Validators.email, Validators.required]);
|
|
46
|
-
* }
|
|
47
|
-
* ```
|
|
48
|
-
* @category Core/Inputs
|
|
49
|
-
*/
|
|
50
|
-
@Directive({
|
|
51
|
-
selector: '[kjInput]',
|
|
52
|
-
standalone: true,
|
|
53
|
-
hostDirectives: [
|
|
54
|
-
{ directive: KjDisabled, inputs: ['kjDisabled'] },
|
|
55
|
-
KjFocusRing,
|
|
56
|
-
KjFormControl,
|
|
57
|
-
],
|
|
58
|
-
host: {
|
|
59
|
-
'[attr.aria-invalid]': 'formCtrl.touched() && kjInvalid() ? "true" : null',
|
|
60
|
-
'[attr.data-invalid]': 'formCtrl.touched() && kjInvalid() ? "" : null',
|
|
61
|
-
'[attr.disabled]': 'formCtrl.disabled() ? "" : null',
|
|
62
|
-
'(input)': 'formCtrl.notifyChange($any($event.target).value)',
|
|
63
|
-
'(blur)': 'formCtrl.notifyTouched()',
|
|
64
|
-
},
|
|
65
|
-
})
|
|
66
|
-
export class KjInput {
|
|
67
|
-
readonly formCtrl = inject(KjFormControl);
|
|
68
|
-
private readonly el = inject<ElementRef<HTMLInputElement>>(ElementRef);
|
|
69
|
-
|
|
70
|
-
/** Whether the input is in an invalid state. Combined with `touched` for ARIA. */
|
|
71
|
-
kjInvalid = input<boolean>(false);
|
|
72
|
-
|
|
73
|
-
constructor() {
|
|
74
|
-
// Reflect the CVA value signal back to the native input element.
|
|
75
|
-
effect(() => {
|
|
76
|
-
const val = this.formCtrl.value();
|
|
77
|
-
this.el.nativeElement.value = val == null ? '' : String(val);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
package/src/menu/index.ts
DELETED
package/src/menu/menu.context.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { InjectionToken, Signal } from '@angular/core';
|
|
2
|
-
|
|
3
|
-
/** Context shared between menu directives. */
|
|
4
|
-
export interface KjMenuContext {
|
|
5
|
-
readonly open: Signal<boolean>;
|
|
6
|
-
toggle(): void;
|
|
7
|
-
close(): void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const KJ_MENU = new InjectionToken<KjMenuContext>('KjMenu');
|
package/src/menu/menu.example.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Component } from '@angular/core';
|
|
2
|
-
import { KjMenu, KjMenuTrigger, KjMenuContent, KjMenuItem } from './menu';
|
|
3
|
-
import { KjButton } from '../button/button';
|
|
4
|
-
|
|
5
|
-
@Component({
|
|
6
|
-
selector: 'kj-example-menu-basic',
|
|
7
|
-
standalone: true,
|
|
8
|
-
imports: [KjMenu, KjMenuTrigger, KjMenuContent, KjMenuItem, KjButton],
|
|
9
|
-
styles: [`
|
|
10
|
-
:host { display: flex; align-items: flex-start; justify-content: center; padding: 3rem 2rem 10rem; background: #0c0c0c; font-family: 'JetBrains Mono', monospace; min-height: 220px; }
|
|
11
|
-
.anchor { position: relative; display: inline-block; }
|
|
12
|
-
button[kjButton] { padding: 0.5rem 1.25rem; border: 1px solid #444; background: transparent; color: #f0ede6; cursor: pointer; font-family: inherit; font-size: 0.875rem; }
|
|
13
|
-
button[kjButton]:hover { border-color: #b8f500; color: #b8f500; }
|
|
14
|
-
[kjMenuContent] {
|
|
15
|
-
position: absolute; top: calc(100% + 4px); left: 0; z-index: 20;
|
|
16
|
-
background: #1a1a1a; border: 1px solid #333; padding: 0.25rem 0; min-width: 10rem;
|
|
17
|
-
display: flex; flex-direction: column;
|
|
18
|
-
}
|
|
19
|
-
[kjMenuContent][hidden] { display: none; }
|
|
20
|
-
[kjMenuItem] {
|
|
21
|
-
background: transparent; border: none; color: #f0ede6; font-family: inherit;
|
|
22
|
-
font-size: 0.875rem; padding: 0.5rem 1rem; text-align: left; cursor: pointer; width: 100%;
|
|
23
|
-
}
|
|
24
|
-
[kjMenuItem]:hover, [kjMenuItem]:focus { background: #252525; color: #b8f500; outline: none; }
|
|
25
|
-
[kjMenuItem][disabled] { color: #555; cursor: not-allowed; }
|
|
26
|
-
`],
|
|
27
|
-
template: `
|
|
28
|
-
<div class="anchor" kjMenu>
|
|
29
|
-
<button kjButton kjMenuTrigger>Actions</button>
|
|
30
|
-
<div kjMenuContent>
|
|
31
|
-
<button kjMenuItem>Edit</button>
|
|
32
|
-
<button kjMenuItem>Duplicate</button>
|
|
33
|
-
<button kjMenuItem>Archive</button>
|
|
34
|
-
<button kjMenuItem disabled>Delete</button>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
37
|
-
`,
|
|
38
|
-
})
|
|
39
|
-
export class MenuBasicExample {}
|
package/src/menu/menu.spec.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { render, fireEvent } from '@testing-library/angular';
|
|
2
|
-
import { axe, toHaveNoViolations } from 'jest-axe';
|
|
3
|
-
import { KjMenu, KjMenuTrigger, KjMenuContent, KjMenuItem, KjMenuClose } from './menu';
|
|
4
|
-
|
|
5
|
-
expect.extend(toHaveNoViolations);
|
|
6
|
-
|
|
7
|
-
const imports = [KjMenu, KjMenuTrigger, KjMenuContent, KjMenuItem, KjMenuClose];
|
|
8
|
-
|
|
9
|
-
const template = `
|
|
10
|
-
<div kjMenu>
|
|
11
|
-
<button kjMenuTrigger>Actions</button>
|
|
12
|
-
<div kjMenuContent>
|
|
13
|
-
<button kjMenuItem>Edit</button>
|
|
14
|
-
<button kjMenuItem>Delete</button>
|
|
15
|
-
</div>
|
|
16
|
-
</div>`;
|
|
17
|
-
|
|
18
|
-
describe('KjMenu', () => {
|
|
19
|
-
it('renders trigger button', async () => {
|
|
20
|
-
const { getByRole } = await render(template, { imports });
|
|
21
|
-
expect(getByRole('button', { name: 'Actions' })).toBeInTheDocument();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('trigger has aria-haspopup="menu"', async () => {
|
|
25
|
-
const { getByRole } = await render(template, { imports });
|
|
26
|
-
expect(getByRole('button', { name: 'Actions' })).toHaveAttribute('aria-haspopup', 'menu');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('trigger has aria-expanded="false" initially', async () => {
|
|
30
|
-
const { getByRole } = await render(template, { imports });
|
|
31
|
-
expect(getByRole('button', { name: 'Actions' })).toHaveAttribute('aria-expanded', 'false');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('menu content is hidden initially', async () => {
|
|
35
|
-
const { getByRole } = await render(template, { imports });
|
|
36
|
-
const menu = getByRole('menu', { hidden: true });
|
|
37
|
-
expect(menu).toHaveAttribute('hidden');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('opens on trigger click', async () => {
|
|
41
|
-
const { getByRole } = await render(template, { imports });
|
|
42
|
-
fireEvent.click(getByRole('button', { name: 'Actions' }));
|
|
43
|
-
const menu = getByRole('menu');
|
|
44
|
-
expect(menu).not.toHaveAttribute('hidden');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('sets aria-expanded="true" when open', async () => {
|
|
48
|
-
const { getByRole } = await render(template, { imports });
|
|
49
|
-
fireEvent.click(getByRole('button', { name: 'Actions' }));
|
|
50
|
-
expect(getByRole('button', { name: 'Actions' })).toHaveAttribute('aria-expanded', 'true');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('closes on second trigger click (toggle)', async () => {
|
|
54
|
-
const { getByRole } = await render(template, { imports });
|
|
55
|
-
const trigger = getByRole('button', { name: 'Actions' });
|
|
56
|
-
fireEvent.click(trigger);
|
|
57
|
-
fireEvent.click(trigger);
|
|
58
|
-
const menu = getByRole('menu', { hidden: true });
|
|
59
|
-
expect(menu).toHaveAttribute('hidden');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('closes on Escape key', async () => {
|
|
63
|
-
const { getByRole } = await render(template, { imports });
|
|
64
|
-
fireEvent.click(getByRole('button', { name: 'Actions' }));
|
|
65
|
-
fireEvent.keyDown(document, { key: 'Escape' });
|
|
66
|
-
const menu = getByRole('menu', { hidden: true });
|
|
67
|
-
expect(menu).toHaveAttribute('hidden');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('closes on click outside', async () => {
|
|
71
|
-
const { getByRole } = await render(template, { imports });
|
|
72
|
-
fireEvent.click(getByRole('button', { name: 'Actions' }));
|
|
73
|
-
fireEvent.click(document.body);
|
|
74
|
-
const menu = getByRole('menu', { hidden: true });
|
|
75
|
-
expect(menu).toHaveAttribute('hidden');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('closes when a menu item is clicked', async () => {
|
|
79
|
-
const { getByRole } = await render(template, { imports });
|
|
80
|
-
fireEvent.click(getByRole('button', { name: 'Actions' }));
|
|
81
|
-
fireEvent.click(getByRole('menuitem', { name: 'Edit' }));
|
|
82
|
-
const menu = getByRole('menu', { hidden: true });
|
|
83
|
-
expect(menu).toHaveAttribute('hidden');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('menu items have role="menuitem"', async () => {
|
|
87
|
-
const { getAllByRole } = await render(template, { imports });
|
|
88
|
-
fireEvent.click(getAllByRole('button')[0]);
|
|
89
|
-
const items = getAllByRole('menuitem');
|
|
90
|
-
expect(items).toHaveLength(2);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('navigates down with ArrowDown', async () => {
|
|
94
|
-
const { getByRole, getAllByRole } = await render(template, { imports });
|
|
95
|
-
fireEvent.click(getByRole('button', { name: 'Actions' }));
|
|
96
|
-
const menu = getByRole('menu');
|
|
97
|
-
const items = getAllByRole('menuitem');
|
|
98
|
-
items[0].focus();
|
|
99
|
-
fireEvent.keyDown(menu, { key: 'ArrowDown' });
|
|
100
|
-
expect(document.activeElement).toBe(items[1]);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('navigates up with ArrowUp', async () => {
|
|
104
|
-
const { getByRole, getAllByRole } = await render(template, { imports });
|
|
105
|
-
fireEvent.click(getByRole('button', { name: 'Actions' }));
|
|
106
|
-
const menu = getByRole('menu');
|
|
107
|
-
const items = getAllByRole('menuitem');
|
|
108
|
-
items[1].focus();
|
|
109
|
-
fireEvent.keyDown(menu, { key: 'ArrowUp' });
|
|
110
|
-
expect(document.activeElement).toBe(items[0]);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('wraps around from last to first on ArrowDown', async () => {
|
|
114
|
-
const { getByRole, getAllByRole } = await render(template, { imports });
|
|
115
|
-
fireEvent.click(getByRole('button', { name: 'Actions' }));
|
|
116
|
-
const menu = getByRole('menu');
|
|
117
|
-
const items = getAllByRole('menuitem');
|
|
118
|
-
items[items.length - 1].focus();
|
|
119
|
-
fireEvent.keyDown(menu, { key: 'ArrowDown' });
|
|
120
|
-
expect(document.activeElement).toBe(items[0]);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('focuses first item on Home key', async () => {
|
|
124
|
-
const { getByRole, getAllByRole } = await render(template, { imports });
|
|
125
|
-
fireEvent.click(getByRole('button', { name: 'Actions' }));
|
|
126
|
-
const menu = getByRole('menu');
|
|
127
|
-
const items = getAllByRole('menuitem');
|
|
128
|
-
items[1].focus();
|
|
129
|
-
fireEvent.keyDown(menu, { key: 'Home' });
|
|
130
|
-
expect(document.activeElement).toBe(items[0]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('focuses last item on End key', async () => {
|
|
134
|
-
const { getByRole, getAllByRole } = await render(template, { imports });
|
|
135
|
-
fireEvent.click(getByRole('button', { name: 'Actions' }));
|
|
136
|
-
const menu = getByRole('menu');
|
|
137
|
-
const items = getAllByRole('menuitem');
|
|
138
|
-
items[0].focus();
|
|
139
|
-
fireEvent.keyDown(menu, { key: 'End' });
|
|
140
|
-
expect(document.activeElement).toBe(items[items.length - 1]);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('kjMenuClose closes the menu on click', async () => {
|
|
144
|
-
const closeTemplate = `
|
|
145
|
-
<div kjMenu>
|
|
146
|
-
<button kjMenuTrigger>Open</button>
|
|
147
|
-
<div kjMenuContent>
|
|
148
|
-
<button kjMenuClose>Cancel</button>
|
|
149
|
-
</div>
|
|
150
|
-
</div>`;
|
|
151
|
-
const { getByRole } = await render(closeTemplate, { imports });
|
|
152
|
-
fireEvent.click(getByRole('button', { name: 'Open' }));
|
|
153
|
-
fireEvent.click(getByRole('button', { name: 'Cancel' }));
|
|
154
|
-
const menu = getByRole('menu', { hidden: true });
|
|
155
|
-
expect(menu).toHaveAttribute('hidden');
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('passes axe audit when closed', async () => {
|
|
159
|
-
const { container } = await render(template, { imports });
|
|
160
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('passes axe audit when open', async () => {
|
|
164
|
-
const { container, getByRole } = await render(template, { imports });
|
|
165
|
-
fireEvent.click(getByRole('button', { name: 'Actions' }));
|
|
166
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
167
|
-
});
|
|
168
|
-
});
|
package/src/menu/menu.ts
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { Directive, ElementRef, inject, signal } from '@angular/core';
|
|
2
|
-
import { KJ_MENU, KjMenuContext } from './menu.context';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Root menu container. Manages open/close state and provides context to child directives.
|
|
6
|
-
* Pairs with `[kjMenuTrigger]`, `[kjMenuContent]`, and `[kjMenuItem]`.
|
|
7
|
-
*
|
|
8
|
-
* Click outside or Escape closes the menu. Clicking the trigger toggles it.
|
|
9
|
-
* Arrow-key navigation, Home/End, and typeahead are handled by `[kjMenuContent]`.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```html
|
|
13
|
-
* <div kjMenu>
|
|
14
|
-
* <button kjMenuTrigger>Actions</button>
|
|
15
|
-
* <div kjMenuContent>
|
|
16
|
-
* <button kjMenuItem>Edit</button>
|
|
17
|
-
* <button kjMenuItem>Delete</button>
|
|
18
|
-
* </div>
|
|
19
|
-
* </div>
|
|
20
|
-
* ```
|
|
21
|
-
* @doc
|
|
22
|
-
* @doc-example Basic
|
|
23
|
-
* @doc-theme default
|
|
24
|
-
* @doc-file menu.example.ts
|
|
25
|
-
* @category Core/Navigation
|
|
26
|
-
*/
|
|
27
|
-
@Directive({
|
|
28
|
-
selector: '[kjMenu]',
|
|
29
|
-
standalone: true,
|
|
30
|
-
providers: [{ provide: KJ_MENU, useExisting: KjMenu }],
|
|
31
|
-
exportAs: 'kjMenu',
|
|
32
|
-
})
|
|
33
|
-
export class KjMenu implements KjMenuContext {
|
|
34
|
-
private readonly _open = signal(false);
|
|
35
|
-
readonly open = this._open.asReadonly();
|
|
36
|
-
|
|
37
|
-
toggle(): void { this._open.update(v => !v); }
|
|
38
|
-
close(): void { this._open.set(false); }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Trigger element that toggles the parent menu.
|
|
43
|
-
* Automatically sets `aria-haspopup` and `aria-expanded` on the host element.
|
|
44
|
-
*
|
|
45
|
-
* @category Core/Navigation
|
|
46
|
-
*/
|
|
47
|
-
@Directive({
|
|
48
|
-
selector: '[kjMenuTrigger]',
|
|
49
|
-
standalone: true,
|
|
50
|
-
host: {
|
|
51
|
-
'[attr.aria-haspopup]': '"menu"',
|
|
52
|
-
'[attr.aria-expanded]': 'ctx.open().toString()',
|
|
53
|
-
// stopPropagation prevents the document:click on kjMenuContent from immediately closing
|
|
54
|
-
'(click)': '$event.stopPropagation(); ctx.toggle()',
|
|
55
|
-
},
|
|
56
|
-
})
|
|
57
|
-
export class KjMenuTrigger {
|
|
58
|
-
readonly ctx = inject(KJ_MENU);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Menu content panel. Hidden when closed via the `hidden` attribute.
|
|
63
|
-
* Provides keyboard navigation: Arrow keys, Home/End, Escape, and typeahead.
|
|
64
|
-
* Click-outside dismissal is handled via a document-level click listener.
|
|
65
|
-
*
|
|
66
|
-
* @category Core/Navigation
|
|
67
|
-
*/
|
|
68
|
-
@Directive({
|
|
69
|
-
selector: '[kjMenuContent]',
|
|
70
|
-
standalone: true,
|
|
71
|
-
host: {
|
|
72
|
-
'role': 'menu',
|
|
73
|
-
'[attr.hidden]': '!ctx.open() ? "" : null',
|
|
74
|
-
// Prevent clicks inside from propagating to document click handler
|
|
75
|
-
'(click)': '$event.stopPropagation()',
|
|
76
|
-
// Keyboard navigation within the menu
|
|
77
|
-
'(keydown)': 'onKeydown($event)',
|
|
78
|
-
// Close on Escape key
|
|
79
|
-
'(document:keydown.escape)': 'ctx.open() && ctx.close()',
|
|
80
|
-
// Close on any click outside (trigger stopPropagation prevents self-close)
|
|
81
|
-
'(document:click)': 'ctx.open() && ctx.close()',
|
|
82
|
-
},
|
|
83
|
-
})
|
|
84
|
-
export class KjMenuContent {
|
|
85
|
-
private readonly el = inject(ElementRef<HTMLElement>);
|
|
86
|
-
readonly ctx = inject(KJ_MENU);
|
|
87
|
-
|
|
88
|
-
onKeydown(e: KeyboardEvent): void {
|
|
89
|
-
const items = this.getFocusableItems();
|
|
90
|
-
if (!items.length) return;
|
|
91
|
-
const current = items.indexOf(document.activeElement as HTMLElement);
|
|
92
|
-
|
|
93
|
-
switch (e.key) {
|
|
94
|
-
case 'ArrowDown':
|
|
95
|
-
e.preventDefault();
|
|
96
|
-
items[(current + 1) % items.length].focus();
|
|
97
|
-
break;
|
|
98
|
-
case 'ArrowUp':
|
|
99
|
-
e.preventDefault();
|
|
100
|
-
items[(current - 1 + items.length) % items.length].focus();
|
|
101
|
-
break;
|
|
102
|
-
case 'Home':
|
|
103
|
-
e.preventDefault();
|
|
104
|
-
items[0].focus();
|
|
105
|
-
break;
|
|
106
|
-
case 'End':
|
|
107
|
-
e.preventDefault();
|
|
108
|
-
items[items.length - 1].focus();
|
|
109
|
-
break;
|
|
110
|
-
default: {
|
|
111
|
-
// Type-ahead: jump to next item starting with pressed character
|
|
112
|
-
const char = e.key.length === 1 ? e.key.toLowerCase() : null;
|
|
113
|
-
if (char) {
|
|
114
|
-
const match =
|
|
115
|
-
items.find((item, i) =>
|
|
116
|
-
i > current && item.textContent?.trim().toLowerCase().startsWith(char)
|
|
117
|
-
) ??
|
|
118
|
-
items.find(item =>
|
|
119
|
-
item.textContent?.trim().toLowerCase().startsWith(char)
|
|
120
|
-
);
|
|
121
|
-
match?.focus();
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
private getFocusableItems(): HTMLElement[] {
|
|
128
|
-
const nodes = this.el.nativeElement.querySelectorAll('[kjMenuItem]');
|
|
129
|
-
return (Array.from(nodes) as HTMLElement[]).filter(el => !el.hasAttribute('disabled'));
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Individual menu item. Closes the menu on click or keyboard activation (Enter/Space).
|
|
135
|
-
*
|
|
136
|
-
* @example
|
|
137
|
-
* ```html
|
|
138
|
-
* <button kjMenuItem>Delete</button>
|
|
139
|
-
* ```
|
|
140
|
-
* @category Core/Navigation
|
|
141
|
-
*/
|
|
142
|
-
@Directive({
|
|
143
|
-
selector: '[kjMenuItem]',
|
|
144
|
-
standalone: true,
|
|
145
|
-
host: {
|
|
146
|
-
'role': 'menuitem',
|
|
147
|
-
'[attr.tabindex]': '"0"',
|
|
148
|
-
'(click)': 'ctx.close()',
|
|
149
|
-
'(keydown.enter)': '$event.preventDefault(); ctx.close()',
|
|
150
|
-
'(keydown.space)': '$event.preventDefault(); ctx.close()',
|
|
151
|
-
},
|
|
152
|
-
})
|
|
153
|
-
export class KjMenuItem {
|
|
154
|
-
readonly ctx = inject(KJ_MENU);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Closes the parent menu when clicked. Convenience directive for non-item close buttons.
|
|
159
|
-
* Place anywhere inside `[kjMenu]`.
|
|
160
|
-
*
|
|
161
|
-
* @example
|
|
162
|
-
* ```html
|
|
163
|
-
* <button kjMenuClose>Cancel</button>
|
|
164
|
-
* ```
|
|
165
|
-
* @category Core/Navigation
|
|
166
|
-
*/
|
|
167
|
-
@Directive({
|
|
168
|
-
selector: '[kjMenuClose]',
|
|
169
|
-
standalone: true,
|
|
170
|
-
host: {
|
|
171
|
-
'(click)': 'ctx.close()',
|
|
172
|
-
},
|
|
173
|
-
})
|
|
174
|
-
export class KjMenuClose {
|
|
175
|
-
readonly ctx = inject(KJ_MENU);
|
|
176
|
-
}
|
package/src/popover/index.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { InjectionToken, Signal, InputSignal } from '@angular/core';
|
|
2
|
-
|
|
3
|
-
/** Preferred side for the popover relative to its trigger. */
|
|
4
|
-
export type KjPopoverSide = 'top' | 'bottom' | 'left' | 'right';
|
|
5
|
-
/** Alignment of the popover along the cross-axis. */
|
|
6
|
-
export type KjPopoverAlign = 'start' | 'center' | 'end';
|
|
7
|
-
|
|
8
|
-
/** Context shared between popover directives. */
|
|
9
|
-
export interface KjPopoverContext {
|
|
10
|
-
readonly open: Signal<boolean>;
|
|
11
|
-
readonly popoverId: string;
|
|
12
|
-
kjPopoverSide: Signal<KjPopoverSide> | InputSignal<KjPopoverSide>;
|
|
13
|
-
kjPopoverAlign: Signal<KjPopoverAlign> | InputSignal<KjPopoverAlign>;
|
|
14
|
-
toggle: () => void;
|
|
15
|
-
hide: () => void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const KJ_POPOVER = new InjectionToken<KjPopoverContext>('KjPopover');
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { Component } from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
KjPopover, KjPopoverTrigger,
|
|
4
|
-
KjPopoverContent, KjPopoverClose,
|
|
5
|
-
} from './popover';
|
|
6
|
-
import { KjButton } from '../button/button';
|
|
7
|
-
|
|
8
|
-
@Component({
|
|
9
|
-
selector: 'kj-example-popover-basic',
|
|
10
|
-
standalone: true,
|
|
11
|
-
imports: [
|
|
12
|
-
KjPopover, KjPopoverTrigger,
|
|
13
|
-
KjPopoverContent, KjPopoverClose,
|
|
14
|
-
KjButton,
|
|
15
|
-
],
|
|
16
|
-
styleUrls: ['../styles/docs-themes.css'],
|
|
17
|
-
styles: [`
|
|
18
|
-
:host { display: flex; align-items: flex-start; justify-content: center; padding: 3rem 2rem 10rem; background: var(--kj-bg); font-family: var(--kj-font); min-height: 220px; }
|
|
19
|
-
.anchor { position: relative; display: inline-block; }
|
|
20
|
-
button[kjButton] { padding: 0.5rem 1.25rem; border: 1px solid var(--kj-border); background: transparent; color: var(--kj-text); cursor: pointer; font-family: inherit; font-size: 0.875rem; }
|
|
21
|
-
button[kjButton]:hover { border-color: var(--kj-accent); color: var(--kj-accent); }
|
|
22
|
-
[kjPopoverContent] {
|
|
23
|
-
position: absolute; top: calc(100% + 8px); left: 0; z-index: 20;
|
|
24
|
-
background: var(--kj-surface); border: 1px solid var(--kj-border); padding: 1rem; min-width: 14rem;
|
|
25
|
-
color: var(--kj-text);
|
|
26
|
-
}
|
|
27
|
-
[kjPopoverContent][hidden] { display: none; }
|
|
28
|
-
[kjPopoverContent][data-align="end"] { left: auto; right: 0; }
|
|
29
|
-
.popover-title { font-size: 0.875rem; font-weight: 600; margin: 0 0 0.5rem; color: var(--kj-accent); }
|
|
30
|
-
.popover-body { font-size: 0.8125rem; color: var(--kj-text-muted); margin: 0 0 1rem; line-height: 1.5; }
|
|
31
|
-
.popover-footer { display: flex; justify-content: flex-end; gap: 0.5rem; }
|
|
32
|
-
.popover-footer button[kjButton] { padding: 0.35rem 0.875rem; font-size: 0.8125rem; }
|
|
33
|
-
[data-variant="default"] { border-color: var(--kj-accent); color: var(--kj-accent-on); background: var(--kj-accent); }
|
|
34
|
-
[data-variant="ghost"] { border-color: transparent; background: transparent; color: var(--kj-text-muted); }
|
|
35
|
-
`],
|
|
36
|
-
template: `
|
|
37
|
-
<div class="anchor" kjPopover>
|
|
38
|
-
<button kjButton kjPopoverTrigger>Open Popover</button>
|
|
39
|
-
<div kjPopoverContent>
|
|
40
|
-
<p class="popover-title">Notification Settings</p>
|
|
41
|
-
<p class="popover-body">Control how and when you receive notifications from this app.</p>
|
|
42
|
-
<div class="popover-footer">
|
|
43
|
-
<button kjButton kjPopoverClose [kjVariant]="'ghost'">Cancel</button>
|
|
44
|
-
<button kjButton kjPopoverClose [kjVariant]="'default'">Save</button>
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
`,
|
|
49
|
-
})
|
|
50
|
-
export class PopoverBasicExample {}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { Component } from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
KjPopover, KjPopoverTrigger,
|
|
4
|
-
KjPopoverContent, KjPopoverClose,
|
|
5
|
-
} from './popover';
|
|
6
|
-
|
|
7
|
-
@Component({
|
|
8
|
-
selector: 'kj-example-popover-finance',
|
|
9
|
-
standalone: true,
|
|
10
|
-
imports: [
|
|
11
|
-
KjPopover, KjPopoverTrigger,
|
|
12
|
-
KjPopoverContent, KjPopoverClose,
|
|
13
|
-
],
|
|
14
|
-
styleUrls: ['../styles/docs-themes.css'],
|
|
15
|
-
host: { class: 'kj-theme-finance' },
|
|
16
|
-
styles: [`
|
|
17
|
-
:host { display: flex; align-items: flex-start; justify-content: center; padding: 3rem 2rem 10rem; background: var(--kj-bg); font-family: var(--kj-font); min-height: 220px; }
|
|
18
|
-
.anchor { position: relative; display: inline-block; }
|
|
19
|
-
button {
|
|
20
|
-
padding: 0.4rem 1rem; background: var(--kj-accent); color: var(--kj-accent-on); border: 1px solid var(--kj-accent);
|
|
21
|
-
border-radius: var(--kj-radius-md); cursor: pointer; font-family: inherit; font-size: 0.875rem; font-weight: 500;
|
|
22
|
-
}
|
|
23
|
-
button:hover { background: #2563eb; }
|
|
24
|
-
[kjPopoverContent] {
|
|
25
|
-
position: absolute; top: calc(100% + 8px); left: 0; z-index: 20;
|
|
26
|
-
background: var(--kj-surface); border: 1px solid var(--kj-border); border-radius: var(--kj-radius-lg);
|
|
27
|
-
box-shadow: var(--kj-shadow-md); padding: 1rem; min-width: 14rem;
|
|
28
|
-
}
|
|
29
|
-
[kjPopoverContent][hidden] { display: none; }
|
|
30
|
-
.popover-title { font-size: 0.875rem; font-weight: 600; color: var(--kj-text); margin: 0 0 0.375rem; }
|
|
31
|
-
.popover-body { font-size: 0.8125rem; color: var(--kj-text-muted); margin: 0 0 1rem; line-height: 1.5; }
|
|
32
|
-
.popover-footer { display: flex; justify-content: flex-end; gap: 0.5rem; }
|
|
33
|
-
.btn-cancel { padding: 0.35rem 0.875rem; background: var(--kj-surface); color: #374151; border: 1px solid var(--kj-border); border-radius: var(--kj-radius-sm); cursor: pointer; font-size: 0.8125rem; font-weight: 500; }
|
|
34
|
-
.btn-cancel:hover { background: #f3f4f6; }
|
|
35
|
-
.btn-primary { padding: 0.35rem 0.875rem; background: var(--kj-accent); color: var(--kj-accent-on); border: 1px solid var(--kj-accent); border-radius: var(--kj-radius-sm); cursor: pointer; font-size: 0.8125rem; font-weight: 500; }
|
|
36
|
-
.btn-primary:hover { background: #2563eb; }
|
|
37
|
-
`],
|
|
38
|
-
template: `
|
|
39
|
-
<div class="anchor" kjPopover>
|
|
40
|
-
<button kjPopoverTrigger>Preferences</button>
|
|
41
|
-
<div kjPopoverContent>
|
|
42
|
-
<p class="popover-title">Notification Preferences</p>
|
|
43
|
-
<p class="popover-body">Choose how you receive alerts and account notifications.</p>
|
|
44
|
-
<div class="popover-footer">
|
|
45
|
-
<button class="btn-cancel" kjPopoverClose>Cancel</button>
|
|
46
|
-
<button class="btn-primary" kjPopoverClose>Apply</button>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
`,
|
|
51
|
-
})
|
|
52
|
-
export class PopoverFinanceExample {}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { Component } from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
KjPopover, KjPopoverTrigger,
|
|
4
|
-
KjPopoverContent, KjPopoverClose,
|
|
5
|
-
} from './popover';
|
|
6
|
-
|
|
7
|
-
@Component({
|
|
8
|
-
selector: 'kj-example-popover-retro',
|
|
9
|
-
standalone: true,
|
|
10
|
-
imports: [
|
|
11
|
-
KjPopover, KjPopoverTrigger,
|
|
12
|
-
KjPopoverContent, KjPopoverClose,
|
|
13
|
-
],
|
|
14
|
-
styleUrls: ['../styles/docs-themes.css'],
|
|
15
|
-
host: { class: 'kj-theme-retro' },
|
|
16
|
-
styles: [`
|
|
17
|
-
:host { display: flex; align-items: flex-start; justify-content: center; padding: 3rem 2rem 10rem; background: var(--kj-bg); font-family: var(--kj-font); min-height: 220px; color: var(--kj-text); }
|
|
18
|
-
.anchor { position: relative; display: inline-block; }
|
|
19
|
-
button {
|
|
20
|
-
padding: 0.4rem 1rem; font-family: var(--kj-font); font-size: 0.8rem; font-weight: 700;
|
|
21
|
-
letter-spacing: 0.06em; text-transform: uppercase; background: var(--kj-surface); color: var(--kj-text);
|
|
22
|
-
border: var(--kj-btn-border); border-radius: 0; cursor: pointer;
|
|
23
|
-
box-shadow: var(--kj-shadow-sm); transition: var(--kj-transition);
|
|
24
|
-
}
|
|
25
|
-
button:hover { transform: translate(-1px, -1px); box-shadow: var(--kj-shadow-md); }
|
|
26
|
-
[kjPopoverContent] {
|
|
27
|
-
position: absolute; top: calc(100% + 8px); left: 0; z-index: 20;
|
|
28
|
-
background: var(--kj-bg); border: var(--kj-btn-border); box-shadow: var(--kj-shadow-hard);
|
|
29
|
-
padding: 1rem; min-width: 14rem; font-family: var(--kj-font);
|
|
30
|
-
}
|
|
31
|
-
[kjPopoverContent][hidden] { display: none; }
|
|
32
|
-
.popover-title { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; margin: 0 0 0.5rem; border-bottom: var(--kj-btn-border); padding-bottom: 0.375rem; color: var(--kj-text); }
|
|
33
|
-
.popover-body { font-size: 0.75rem; color: var(--kj-text-muted); margin: 0.5rem 0 1rem; line-height: 1.6; }
|
|
34
|
-
.popover-footer { display: flex; justify-content: flex-end; gap: 0.5rem; }
|
|
35
|
-
.popover-footer button { padding: 0.3rem 0.75rem; font-size: 0.7rem; box-shadow: var(--kj-shadow-sm); }
|
|
36
|
-
.popover-footer button:hover { box-shadow: var(--kj-shadow-md); }
|
|
37
|
-
.btn-primary { background: var(--kj-accent); color: var(--kj-accent-on); border-color: var(--kj-border); }
|
|
38
|
-
`],
|
|
39
|
-
template: `
|
|
40
|
-
<div class="anchor" kjPopover>
|
|
41
|
-
<button kjPopoverTrigger>Settings</button>
|
|
42
|
-
<div kjPopoverContent>
|
|
43
|
-
<p class="popover-title">Notifications</p>
|
|
44
|
-
<p class="popover-body">Configure notification frequency and delivery preferences.</p>
|
|
45
|
-
<div class="popover-footer">
|
|
46
|
-
<button kjPopoverClose>Cancel</button>
|
|
47
|
-
<button kjPopoverClose class="btn-primary">Save</button>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
`,
|
|
52
|
-
})
|
|
53
|
-
export class PopoverRetroExample {}
|