@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,52 +0,0 @@
1
- import { render, fireEvent } from '@testing-library/angular';
2
- import { axe, toHaveNoViolations } from 'jest-axe';
3
- import {
4
- KjPopover, KjPopoverTrigger,
5
- KjPopoverContent, KjPopoverClose,
6
- } from './popover';
7
-
8
- expect.extend(toHaveNoViolations);
9
- const imports = [KjPopover, KjPopoverTrigger, KjPopoverContent, KjPopoverClose];
10
- const template = `<div kjPopover><button kjPopoverTrigger>Open</button><div kjPopoverContent role="dialog" aria-label="Options">Content</div></div>`;
11
-
12
- describe('KjPopover', () => {
13
- it('content hidden by default', async () => {
14
- const { container } = await render(template, { imports });
15
- expect(container.querySelector('[kjPopoverContent]')).toHaveAttribute('hidden', '');
16
- });
17
-
18
- it('trigger toggles content', async () => {
19
- const { container } = await render(template, { imports });
20
- fireEvent.click(container.querySelector('[kjPopoverTrigger]')!);
21
- expect(container.querySelector('[kjPopoverContent]')).not.toHaveAttribute('hidden');
22
- });
23
-
24
- it('trigger sets aria-expanded', async () => {
25
- const { container } = await render(template, { imports });
26
- const t = container.querySelector('[kjPopoverTrigger]')!;
27
- expect(t).toHaveAttribute('aria-expanded', 'false');
28
- fireEvent.click(t);
29
- expect(t).toHaveAttribute('aria-expanded', 'true');
30
- });
31
-
32
- it('trigger sets aria-controls pointing to content id', async () => {
33
- const { container } = await render(template, { imports });
34
- const t = container.querySelector('[kjPopoverTrigger]')!;
35
- const c = container.querySelector('[kjPopoverContent]')!;
36
- expect(t.getAttribute('aria-controls')).toBe(c.id);
37
- });
38
-
39
- it('kjPopoverClose closes the popover', async () => {
40
- const closeTemplate = `<div kjPopover><button kjPopoverTrigger>Open</button><div kjPopoverContent role="dialog" aria-label="Options"><button kjPopoverClose>Close</button></div></div>`;
41
- const { container } = await render(closeTemplate, { imports });
42
- fireEvent.click(container.querySelector('[kjPopoverTrigger]')!);
43
- expect(container.querySelector('[kjPopoverContent]')).not.toHaveAttribute('hidden');
44
- fireEvent.click(container.querySelector('[kjPopoverClose]')!);
45
- expect(container.querySelector('[kjPopoverContent]')).toHaveAttribute('hidden', '');
46
- });
47
-
48
- it('passes axe audit', async () => {
49
- const { container } = await render(template, { imports });
50
- expect(await axe(container)).toHaveNoViolations();
51
- });
52
- });
@@ -1,124 +0,0 @@
1
- import { Directive, inject, input, signal } from '@angular/core';
2
- import { KJ_POPOVER, KjPopoverContext, type KjPopoverSide, type KjPopoverAlign } from './popover.context';
3
-
4
- let _popoverIdCounter = 0;
5
-
6
- /**
7
- * Root popover container. Manages open state, ARIA wiring, and click-outside dismissal.
8
- * Pairs with `[kjPopoverTrigger]` and `[kjPopoverContent]`.
9
- *
10
- * Click outside or Escape closes the popover. Clicking the trigger toggles it.
11
- * `aria-expanded` and `aria-controls` are automatically set on the trigger.
12
- *
13
- * @example
14
- * ```html
15
- * <div kjPopover>
16
- * <button kjPopoverTrigger>Options</button>
17
- * <div kjPopoverContent>
18
- * <p>Popover body</p>
19
- * <button kjPopoverClose>Done</button>
20
- * </div>
21
- * </div>
22
- * ```
23
- * @doc
24
- * @doc-example Basic
25
- * @doc-theme default
26
- * @doc-file popover.example.ts
27
- * @doc-theme retro
28
- * @doc-file popover.retro.example.ts
29
- * @doc-theme finance
30
- * @doc-file popover.finance.example.ts
31
- * @category Core/Overlays
32
- */
33
- @Directive({
34
- selector: '[kjPopover]',
35
- standalone: true,
36
- providers: [{ provide: KJ_POPOVER, useExisting: KjPopover }],
37
- exportAs: 'kjPopover',
38
- })
39
- export class KjPopover implements KjPopoverContext {
40
- /** Preferred side for the popover panel. Used via `data-side` for CSS. Defaults to `'bottom'`. */
41
- kjPopoverSide = input<KjPopoverSide>('bottom');
42
- /** Alignment along the cross-axis. Used via `data-align` for CSS. Defaults to `'start'`. */
43
- kjPopoverAlign = input<KjPopoverAlign>('start');
44
-
45
- readonly open = signal(false);
46
- readonly popoverId = `kj-popover-${++_popoverIdCounter}`;
47
-
48
- toggle(): void {
49
- this.open.update(v => !v);
50
- }
51
-
52
- hide(): void {
53
- this.open.set(false);
54
- }
55
- }
56
-
57
- /**
58
- * Trigger button that toggles the popover. Automatically sets `aria-expanded`
59
- * and `aria-controls` on the host element.
60
- *
61
- * @category Core/Overlays
62
- */
63
- @Directive({
64
- selector: '[kjPopoverTrigger]',
65
- standalone: true,
66
- host: {
67
- '[attr.aria-expanded]': 'ctx.open().toString()',
68
- '[attr.aria-haspopup]': '"dialog"',
69
- '[attr.aria-controls]': 'ctx.popoverId',
70
- // stopPropagation prevents the document:click on kjPopoverContent from immediately closing
71
- '(click)': '$event.stopPropagation(); ctx.toggle()',
72
- },
73
- })
74
- export class KjPopoverTrigger {
75
- readonly ctx = inject(KJ_POPOVER);
76
- }
77
-
78
- /**
79
- * Popover content panel. Hidden when closed. Uses `hidden` attribute for
80
- * accessibility-compliant visibility. Click-outside and Escape both dismiss it.
81
- *
82
- * Clicks inside the panel are stopped so they don't bubble to the document listener.
83
- *
84
- * @category Core/Overlays
85
- */
86
- @Directive({
87
- selector: '[kjPopoverContent]',
88
- standalone: true,
89
- host: {
90
- '[attr.id]': 'ctx.popoverId',
91
- '[attr.hidden]': '!ctx.open() ? "" : null',
92
- '[attr.data-side]': 'ctx.kjPopoverSide()',
93
- '[attr.data-align]': 'ctx.kjPopoverAlign()',
94
- // Prevent clicks inside from closing the popover
95
- '(click)': '$event.stopPropagation()',
96
- // Close on Escape
97
- '(document:keydown.escape)': 'ctx.open() && ctx.hide()',
98
- // Close on any click outside (trigger stopPropagation prevents self-close)
99
- '(document:click)': 'ctx.open() && ctx.hide()',
100
- },
101
- })
102
- export class KjPopoverContent {
103
- readonly ctx = inject(KJ_POPOVER);
104
- }
105
-
106
- /**
107
- * Closes the parent popover when clicked. Place inside `[kjPopoverContent]`.
108
- *
109
- * @example
110
- * ```html
111
- * <button kjPopoverClose>Done</button>
112
- * ```
113
- * @category Core/Overlays
114
- */
115
- @Directive({
116
- selector: '[kjPopoverClose]',
117
- standalone: true,
118
- host: {
119
- '(click)': 'ctx.hide()',
120
- },
121
- })
122
- export class KjPopoverClose {
123
- readonly ctx = inject(KJ_POPOVER);
124
- }
File without changes
@@ -1,59 +0,0 @@
1
- import { Component } from '@angular/core';
2
- import { FormControl, ReactiveFormsModule } from '@angular/forms';
3
- import { render } from '@testing-library/angular';
4
- import { toHaveNoViolations } from 'jest-axe';
5
- import { KjFormControl } from './form-control';
6
-
7
- expect.extend(toHaveNoViolations);
8
-
9
- @Component({
10
- standalone: true,
11
- imports: [KjFormControl, ReactiveFormsModule],
12
- template: `<input kjFormControl [formControl]="ctrl" (input)="onInput($event)" (blur)="onBlur()" />`,
13
- })
14
- class TestHostComponent {
15
- ctrl = new FormControl('');
16
- formCtrl!: KjFormControl;
17
- onInput(_e: Event): void {
18
- // will be wired by the directive consumer pattern
19
- }
20
- onBlur(): void {}
21
- }
22
-
23
- describe('KjFormControl', () => {
24
- it('registers as NG_VALUE_ACCESSOR so formControl binding works', async () => {
25
- const { container } = await render(TestHostComponent);
26
- expect(container.querySelector('input')).toBeInTheDocument();
27
- });
28
-
29
- it('writeValue updates the value signal', async () => {
30
- const { fixture } = await render(TestHostComponent);
31
- const directive = fixture.debugElement.children[0].injector.get(KjFormControl);
32
- directive.writeValue('hello');
33
- expect(directive.value()).toBe('hello');
34
- });
35
-
36
- it('setDisabledState updates the disabled signal', async () => {
37
- const { fixture } = await render(TestHostComponent);
38
- const directive = fixture.debugElement.children[0].injector.get(KjFormControl);
39
- directive.setDisabledState(true);
40
- expect(directive.disabled()).toBe(true);
41
- });
42
-
43
- it('notifyChange updates value and calls onChange', async () => {
44
- const { fixture } = await render(TestHostComponent);
45
- const directive = fixture.debugElement.children[0].injector.get(KjFormControl);
46
- let emitted: unknown;
47
- directive.registerOnChange((v) => { emitted = v; });
48
- directive.notifyChange('test');
49
- expect(directive.value()).toBe('test');
50
- expect(emitted).toBe('test');
51
- });
52
-
53
- it('notifyTouched marks touched', async () => {
54
- const { fixture } = await render(TestHostComponent);
55
- const directive = fixture.debugElement.children[0].injector.get(KjFormControl);
56
- directive.notifyTouched();
57
- expect(directive.touched()).toBe(true);
58
- });
59
- });
@@ -1,90 +0,0 @@
1
- import { Directive, forwardRef, signal } from '@angular/core';
2
- import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
3
-
4
- /**
5
- * Shared ControlValueAccessor primitive. Compose via `hostDirectives` on any
6
- * form input directive (KjInput, KjCheckbox, etc.) to wire
7
- * Angular reactive forms and template-driven forms automatically.
8
- *
9
- * The host directive calls `notifyChange(value)` on user input and
10
- * `notifyTouched()` on blur. The directive handles all CVA plumbing.
11
- *
12
- * @example
13
- * ```ts
14
- * @Directive({
15
- * selector: '[kjInput]',
16
- * hostDirectives: [KjFormControl],
17
- * host: {
18
- * '(input)': 'formCtrl.notifyChange($event.target.value)',
19
- * '(blur)': 'formCtrl.notifyTouched()',
20
- * },
21
- * })
22
- * export class KjInput {
23
- * readonly formCtrl = inject(KjFormControl);
24
- * }
25
- * ```
26
- * @category Core/Primitives
27
- */
28
- @Directive({
29
- selector: '[kjFormControl]',
30
- standalone: true,
31
- providers: [
32
- {
33
- provide: NG_VALUE_ACCESSOR,
34
- useExisting: forwardRef(() => KjFormControl),
35
- multi: true,
36
- },
37
- ],
38
- })
39
- export class KjFormControl implements ControlValueAccessor {
40
- /** The current form value, kept in sync with the Angular forms model. */
41
- readonly value = signal<unknown>(undefined);
42
-
43
- /** Whether the control is disabled by the Angular forms layer. */
44
- readonly disabled = signal<boolean>(false);
45
-
46
- /** Whether the control has been touched (blurred at least once). */
47
- readonly touched = signal<boolean>(false);
48
-
49
- private _onChange?: (value: unknown) => void;
50
- private _onTouched?: () => void;
51
-
52
- /** @internal — called by Angular forms */
53
- writeValue(val: unknown): void {
54
- this.value.set(val);
55
- }
56
-
57
- /** @internal — called by Angular forms */
58
- registerOnChange(fn: (value: unknown) => void): void {
59
- this._onChange = fn;
60
- }
61
-
62
- /** @internal — called by Angular forms */
63
- registerOnTouched(fn: () => void): void {
64
- this._onTouched = fn;
65
- }
66
-
67
- /** @internal — called by Angular forms */
68
- setDisabledState(isDisabled: boolean): void {
69
- this.disabled.set(isDisabled);
70
- }
71
-
72
- /**
73
- * Call this when the user changes the control's value.
74
- * Updates the value signal and notifies Angular forms.
75
- * @param val - The new value.
76
- */
77
- notifyChange(val: unknown): void {
78
- this.value.set(val);
79
- this._onChange?.(val);
80
- }
81
-
82
- /**
83
- * Call this when the user blurs the control.
84
- * Marks the control as touched.
85
- */
86
- notifyTouched(): void {
87
- this.touched.set(true);
88
- this._onTouched?.();
89
- }
90
- }
@@ -1 +0,0 @@
1
- export { KjFormControl } from './form-control';
@@ -1,3 +0,0 @@
1
- export * from './interaction/index';
2
- export * from './forms/index';
3
- export * from './overlay/index';
@@ -1,47 +0,0 @@
1
- import { render } from '@testing-library/angular';
2
- import { axe, toHaveNoViolations } from 'jest-axe';
3
- import { KjDisabled } from './disabled';
4
-
5
- expect.extend(toHaveNoViolations);
6
-
7
- describe('KjDisabled', () => {
8
- it('sets aria-disabled="true" when disabled', async () => {
9
- const { container } = await render(
10
- `<button kjDisabled [kjDisabled]="true">Submit</button>`,
11
- { imports: [KjDisabled] },
12
- );
13
- expect(container.querySelector('button')).toHaveAttribute('aria-disabled', 'true');
14
- });
15
-
16
- it('sets data-disabled attribute when disabled', async () => {
17
- const { container } = await render(
18
- `<button kjDisabled [kjDisabled]="true">Submit</button>`,
19
- { imports: [KjDisabled] },
20
- );
21
- expect(container.querySelector('button')).toHaveAttribute('data-disabled', '');
22
- });
23
-
24
- it('removes aria-disabled when not disabled', async () => {
25
- const { container } = await render(
26
- `<button kjDisabled [kjDisabled]="false">Submit</button>`,
27
- { imports: [KjDisabled] },
28
- );
29
- expect(container.querySelector('button')).not.toHaveAttribute('aria-disabled');
30
- });
31
-
32
- it('defaults to not disabled', async () => {
33
- const { container } = await render(
34
- `<button kjDisabled>Submit</button>`,
35
- { imports: [KjDisabled] },
36
- );
37
- expect(container.querySelector('button')).not.toHaveAttribute('aria-disabled');
38
- });
39
-
40
- it('passes axe accessibility audit', async () => {
41
- const { container } = await render(
42
- `<button kjDisabled [kjDisabled]="true">Submit</button>`,
43
- { imports: [KjDisabled] },
44
- );
45
- expect(await axe(container)).toHaveNoViolations();
46
- });
47
- });
@@ -1,24 +0,0 @@
1
- import { Directive, input } from '@angular/core';
2
-
3
- /**
4
- * Applies disabled state to any element via ARIA and data attributes.
5
- * Compose via `hostDirectives` on other directives to add disabled behavior.
6
- *
7
- * @example
8
- * ```html
9
- * <button kjButton [kjDisabled]="isLoading()">Submit</button>
10
- * ```
11
- * @category Core/Primitives
12
- */
13
- @Directive({
14
- selector: '[kjDisabled]',
15
- standalone: true,
16
- host: {
17
- '[attr.aria-disabled]': 'disabled() ? "true" : null',
18
- '[attr.data-disabled]': 'disabled() ? "" : null',
19
- },
20
- })
21
- export class KjDisabled {
22
- /** Whether the element is disabled. Reflects via `aria-disabled` and `data-disabled`. */
23
- readonly disabled = input<boolean>(false, { alias: 'kjDisabled' });
24
- }
@@ -1,62 +0,0 @@
1
- import { render, fireEvent } from '@testing-library/angular';
2
- import { axe, toHaveNoViolations } from 'jest-axe';
3
- import { KjFocusRing } from './focus-ring';
4
-
5
- expect.extend(toHaveNoViolations);
6
-
7
- describe('KjFocusRing', () => {
8
- it('adds data-focus-visible on focus when last interaction was keyboard', async () => {
9
- const { container } = await render(
10
- `<button kjFocusRing>Click me</button>`,
11
- { imports: [KjFocusRing] },
12
- );
13
- const btn = container.querySelector('button')!;
14
- // No prior pointerdown — _lastWasPointer is false, so focus should be visible.
15
- fireEvent.focus(btn);
16
- expect(btn).toHaveAttribute('data-focus-visible', '');
17
- });
18
-
19
- it('does not add data-focus-visible on focus after pointer interaction', async () => {
20
- const { container } = await render(
21
- `<button kjFocusRing>Click me</button>`,
22
- { imports: [KjFocusRing] },
23
- );
24
- const btn = container.querySelector('button')!;
25
- // Simulate pointer interaction, then focus.
26
- fireEvent.pointerDown(document);
27
- fireEvent.focus(btn);
28
- expect(btn).not.toHaveAttribute('data-focus-visible');
29
- });
30
-
31
- it('removes data-focus-visible on blur', async () => {
32
- const { container } = await render(
33
- `<button kjFocusRing>Click me</button>`,
34
- { imports: [KjFocusRing] },
35
- );
36
- const btn = container.querySelector('button')!;
37
- fireEvent.focus(btn);
38
- fireEvent.blur(btn);
39
- expect(btn).not.toHaveAttribute('data-focus-visible');
40
- });
41
-
42
- it('restores data-focus-visible after keydown followed by focus', async () => {
43
- const { container } = await render(
44
- `<button kjFocusRing>Click me</button>`,
45
- { imports: [KjFocusRing] },
46
- );
47
- const btn = container.querySelector('button')!;
48
- // First simulate pointer (marks _lastWasPointer = true), then keyboard (resets it).
49
- fireEvent.pointerDown(document);
50
- fireEvent.keyDown(document);
51
- fireEvent.focus(btn);
52
- expect(btn).toHaveAttribute('data-focus-visible', '');
53
- });
54
-
55
- it('passes axe accessibility audit', async () => {
56
- const { container } = await render(
57
- `<button kjFocusRing>Click me</button>`,
58
- { imports: [KjFocusRing] },
59
- );
60
- expect(await axe(container)).toHaveNoViolations();
61
- });
62
- });
@@ -1,57 +0,0 @@
1
- import { Directive, DestroyRef, ElementRef, afterNextRender, inject, signal } from '@angular/core';
2
- import { PLATFORM_ID } from '@angular/core';
3
- import { isPlatformBrowser } from '@angular/common';
4
-
5
- /**
6
- * Tracks focus-visible state using native PointerEvents and keyboard interaction.
7
- * Sets `data-focus-visible` only when the element receives focus via keyboard.
8
- * Compose via `hostDirectives` to add focus-ring behavior to interactive elements.
9
- *
10
- * @example
11
- * ```html
12
- * <button kjFocusRing>Focusable button</button>
13
- * ```
14
- * @category Core/Primitives
15
- */
16
- @Directive({
17
- selector: '[kjFocusRing]',
18
- standalone: true,
19
- host: {
20
- '[attr.data-focus-visible]': 'focusVisible() ? "" : null',
21
- },
22
- })
23
- export class KjFocusRing {
24
- private readonly el = inject<ElementRef<HTMLElement>>(ElementRef);
25
- private readonly destroyRef = inject(DestroyRef);
26
- private readonly platformId = inject(PLATFORM_ID);
27
-
28
- private readonly _focusVisible = signal(false);
29
-
30
- /** Whether the element currently has visible focus (keyboard-initiated). */
31
- readonly focusVisible = this._focusVisible.asReadonly();
32
-
33
- private _lastWasPointer = false;
34
-
35
- constructor() {
36
- afterNextRender(() => {
37
- if (!isPlatformBrowser(this.platformId)) return;
38
-
39
- const onKeydown = () => { this._lastWasPointer = false; };
40
- const onPointerdown = () => { this._lastWasPointer = true; };
41
- const onFocus = () => { this._focusVisible.set(!this._lastWasPointer); };
42
- const onBlur = () => { this._focusVisible.set(false); };
43
-
44
- document.addEventListener('keydown', onKeydown, true);
45
- document.addEventListener('pointerdown', onPointerdown, true);
46
- this.el.nativeElement.addEventListener('focus', onFocus);
47
- this.el.nativeElement.addEventListener('blur', onBlur);
48
-
49
- this.destroyRef.onDestroy(() => {
50
- document.removeEventListener('keydown', onKeydown, true);
51
- document.removeEventListener('pointerdown', onPointerdown, true);
52
- this.el.nativeElement.removeEventListener('focus', onFocus);
53
- this.el.nativeElement.removeEventListener('blur', onBlur);
54
- });
55
- });
56
- }
57
- }
@@ -1,2 +0,0 @@
1
- export { KjDisabled } from './disabled';
2
- export { KjFocusRing } from './focus-ring';
@@ -1 +0,0 @@
1
- export { KjOverlayService, KjOverlayRef } from './overlay';
@@ -1,90 +0,0 @@
1
- import { Component, TemplateRef, ViewChild, ViewContainerRef, inject, OnInit } from '@angular/core';
2
- import { render } from '@testing-library/angular';
3
- import { KjOverlayService, KjOverlayRef } from './overlay';
4
-
5
- describe('KjOverlayService', () => {
6
- it('can be injected', async () => {
7
- @Component({ standalone: true, template: '' })
8
- class TestComponent {
9
- readonly svc = inject(KjOverlayService);
10
- }
11
- const { fixture } = await render(TestComponent);
12
- expect(fixture.componentInstance.svc).toBeTruthy();
13
- });
14
-
15
- it('createFromTemplate returns a KjOverlayRef with isOpen=false', async () => {
16
- @Component({
17
- standalone: true,
18
- template: '<ng-template #tpl><div>overlay content</div></ng-template>',
19
- })
20
- class TestComponent implements OnInit {
21
- @ViewChild('tpl', { static: true }) tpl!: TemplateRef<unknown>;
22
- readonly svc = inject(KjOverlayService);
23
- readonly vcr = inject(ViewContainerRef);
24
- ref!: KjOverlayRef;
25
- ngOnInit() { this.ref = this.svc.createFromTemplate(this.tpl, this.vcr); }
26
- }
27
- const { fixture } = await render(TestComponent);
28
- expect(fixture.componentInstance.ref.isOpen()).toBe(false);
29
- fixture.componentInstance.ref.dispose();
30
- });
31
-
32
- it('open() sets isOpen to true', async () => {
33
- @Component({
34
- standalone: true,
35
- template: '<ng-template #tpl><div>overlay</div></ng-template>',
36
- })
37
- class TestComponent implements OnInit {
38
- @ViewChild('tpl', { static: true }) tpl!: TemplateRef<unknown>;
39
- readonly svc = inject(KjOverlayService);
40
- readonly vcr = inject(ViewContainerRef);
41
- ref!: KjOverlayRef;
42
- ngOnInit() { this.ref = this.svc.createFromTemplate(this.tpl, this.vcr); }
43
- }
44
- const { fixture } = await render(TestComponent);
45
- const ref = fixture.componentInstance.ref;
46
- ref.open();
47
- expect(ref.isOpen()).toBe(true);
48
- ref.dispose();
49
- });
50
-
51
- it('close() sets isOpen to false', async () => {
52
- @Component({
53
- standalone: true,
54
- template: '<ng-template #tpl><div>overlay</div></ng-template>',
55
- })
56
- class TestComponent implements OnInit {
57
- @ViewChild('tpl', { static: true }) tpl!: TemplateRef<unknown>;
58
- readonly svc = inject(KjOverlayService);
59
- readonly vcr = inject(ViewContainerRef);
60
- ref!: KjOverlayRef;
61
- ngOnInit() { this.ref = this.svc.createFromTemplate(this.tpl, this.vcr); }
62
- }
63
- const { fixture } = await render(TestComponent);
64
- const ref = fixture.componentInstance.ref;
65
- ref.open();
66
- ref.close();
67
- expect(ref.isOpen()).toBe(false);
68
- ref.dispose();
69
- });
70
-
71
- it('dispose() removes the overlay container from document.body', async () => {
72
- @Component({
73
- standalone: true,
74
- template: '<ng-template #tpl><div>overlay</div></ng-template>',
75
- })
76
- class TestComponent implements OnInit {
77
- @ViewChild('tpl', { static: true }) tpl!: TemplateRef<unknown>;
78
- readonly svc = inject(KjOverlayService);
79
- readonly vcr = inject(ViewContainerRef);
80
- ref!: KjOverlayRef;
81
- ngOnInit() { this.ref = this.svc.createFromTemplate(this.tpl, this.vcr); }
82
- }
83
- const { fixture } = await render(TestComponent);
84
- const ref = fixture.componentInstance.ref;
85
- ref.open();
86
- expect(document.body.querySelector('[data-kj-overlay]')).toBeTruthy();
87
- ref.dispose();
88
- expect(document.body.querySelector('[data-kj-overlay]')).toBeNull();
89
- });
90
- });