@signality/cdk-interop 0.0.1-alpha.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/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @signality/cdk-interop
2
+
3
+ Signal-based utilities for Angular CDK. Reactive wrappers around CDK's accessibility features.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @signality/cdk-interop @angular/cdk
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **`focusMonitor()`** — Reactive focus tracking with origin detection (keyboard, mouse, touch, program)
14
+ - **`inputModality()`** — Reactive input method detection (keyboard, mouse, touch)
15
+ - **`liveAnnouncer()`** — Reactive screen reader announcements with message tracking
16
+
17
+ ## Usage
18
+
19
+ ```typescript
20
+ import { focusMonitor, inputModality, liveAnnouncer } from '@signality/cdk-interop';
21
+
22
+ // Focus monitoring with origin
23
+ const btn = viewChild<ElementRef<HTMLButtonElement>>('btn');
24
+ const focus = focusMonitor(btn);
25
+ // In template: Focused: {{ focus.isFocused() }}, Origin: {{ focus.origin() }}
26
+
27
+ // Input modality detection
28
+ const modality = inputModality();
29
+ // In template: Current input: {{ modality() ?? 'none' }}
30
+
31
+ // Screen reader announcements
32
+ const announcer = liveAnnouncer();
33
+ announcer.announce('Item added to cart');
34
+ // In template: Last: {{ announcer.lastMessage() }}
35
+ ```
36
+
37
+ ## Documentation
38
+
39
+ Full documentation available at [signality.dev/cdk-interop](https://signality.dev/cdk-interop/focus-monitor)
40
+
@@ -0,0 +1,92 @@
1
+ import { inject, signal, afterRenderEffect } from '@angular/core';
2
+ import { FocusMonitor } from '@angular/cdk/a11y';
3
+ import { onDisconnect } from '@signality/core';
4
+ import { setupContext, NOOP_FN, constSignal, toElement } from '@signality/core/internal';
5
+
6
+ /**
7
+ * Signal-based wrapper around the [Angular CDK](https://material.angular.io/cdk/a11y/overview) FocusMonitor.
8
+ *
9
+ * @param target - Target element to monitor
10
+ * @param options - Optional configuration
11
+ * @returns A FocusMonitorRef with focus state signals and control methods
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * @Component({
16
+ * template: `
17
+ * <button #btn [class.focused]="focus.isFocused()">
18
+ * Click me
19
+ * @if (focus.origin(); as origin) {
20
+ * <span>({{ origin }})</span>
21
+ * }
22
+ * </button>
23
+ * <button (click)="focus.focusVia('program')">Focus programmatically</button>
24
+ * `
25
+ * })
26
+ * class FocusComponent {
27
+ * readonly btn = viewChild<ElementRef>('btn');
28
+ * readonly focus = focusMonitor(this.btn);
29
+ * }
30
+ * ```
31
+ */
32
+ function focusMonitor(target, options) {
33
+ const { runInContext } = setupContext(options?.injector, focusMonitor);
34
+ return runInContext(({ isServer, onCleanup }) => {
35
+ if (isServer) {
36
+ return {
37
+ origin: constSignal(null),
38
+ isFocused: constSignal(false),
39
+ focusVia: NOOP_FN,
40
+ };
41
+ }
42
+ const cdkMonitor = inject(FocusMonitor);
43
+ const origin = signal(null, ...(ngDevMode ? [{ debugName: "origin" }] : []));
44
+ const isFocused = signal(false, ...(ngDevMode ? [{ debugName: "isFocused" }] : []));
45
+ let subscription;
46
+ const startMonitoring = (el) => {
47
+ subscription?.unsubscribe();
48
+ subscription = cdkMonitor.monitor(el, options?.checkChildren).subscribe(focusOrigin => {
49
+ origin.set(focusOrigin);
50
+ isFocused.set(focusOrigin !== null);
51
+ });
52
+ };
53
+ const stopMonitoring = (el) => {
54
+ origin.set(null);
55
+ isFocused.set(false);
56
+ subscription?.unsubscribe();
57
+ cdkMonitor.stopMonitoring(el);
58
+ };
59
+ const focusVia = (origin, options) => {
60
+ const el = toElement(target);
61
+ if (!el) {
62
+ if (ngDevMode) {
63
+ console.warn('[focusMonitor] Cannot focus: element is not available');
64
+ }
65
+ return;
66
+ }
67
+ cdkMonitor.focusVia(el, origin, options);
68
+ };
69
+ const setupMonitoring = (onCleanup) => {
70
+ const el = toElement(target);
71
+ if (el) {
72
+ startMonitoring(el);
73
+ onCleanup(() => stopMonitoring(el));
74
+ }
75
+ };
76
+ onCleanup(() => subscription?.unsubscribe());
77
+ afterRenderEffect({ read: setupMonitoring });
78
+ onDisconnect(target, stopMonitoring);
79
+ return {
80
+ origin: origin.asReadonly(),
81
+ isFocused: isFocused.asReadonly(),
82
+ focusVia,
83
+ };
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Generated bundle index. Do not edit.
89
+ */
90
+
91
+ export { focusMonitor };
92
+ //# sourceMappingURL=signality-cdk-interop-focus-monitor.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signality-cdk-interop-focus-monitor.mjs","sources":["../../../projects/cdk-interop/focus-monitor/index.ts","../../../projects/cdk-interop/focus-monitor/signality-cdk-interop-focus-monitor.ts"],"sourcesContent":["import {\n afterRenderEffect,\n type EffectCleanupRegisterFn,\n inject,\n type Signal,\n signal,\n} from '@angular/core';\nimport { FocusMonitor, type FocusOrigin } from '@angular/cdk/a11y';\nimport type { Subscription } from 'rxjs';\nimport { MaybeElementSignal, onDisconnect, WithInjector } from '@signality/core';\nimport { constSignal, NOOP_FN, setupContext, toElement } from '@signality/core/internal';\n\nexport interface FocusMonitorOptions extends WithInjector {\n /**\n * Also monitor focus within children.\n * @default false\n */\n readonly checkChildren?: boolean;\n}\n\nexport interface FocusMonitorRef {\n /** Whether element is focused */\n readonly isFocused: Signal<boolean>;\n\n /** Focus origin: 'keyboard', 'mouse', 'touch', 'program', or null */\n readonly origin: Signal<FocusOrigin>;\n\n /** Focus element with specific origin */\n readonly focusVia: (origin: FocusOrigin, options?: FocusOptions) => void;\n}\n\n/**\n * Signal-based wrapper around the [Angular CDK](https://material.angular.io/cdk/a11y/overview) FocusMonitor.\n *\n * @param target - Target element to monitor\n * @param options - Optional configuration\n * @returns A FocusMonitorRef with focus state signals and control methods\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <button #btn [class.focused]=\"focus.isFocused()\">\n * Click me\n * @if (focus.origin(); as origin) {\n * <span>({{ origin }})</span>\n * }\n * </button>\n * <button (click)=\"focus.focusVia('program')\">Focus programmatically</button>\n * `\n * })\n * class FocusComponent {\n * readonly btn = viewChild<ElementRef>('btn');\n * readonly focus = focusMonitor(this.btn);\n * }\n * ```\n */\nexport function focusMonitor(\n target: MaybeElementSignal<HTMLElement>,\n options?: FocusMonitorOptions\n): FocusMonitorRef {\n const { runInContext } = setupContext(options?.injector, focusMonitor);\n\n return runInContext(({ isServer, onCleanup }) => {\n if (isServer) {\n return {\n origin: constSignal(null),\n isFocused: constSignal(false),\n focusVia: NOOP_FN,\n };\n }\n\n const cdkMonitor = inject(FocusMonitor);\n\n const origin = signal<FocusOrigin>(null);\n const isFocused = signal(false);\n\n let subscription: Subscription | undefined;\n\n const startMonitoring = (el: HTMLElement) => {\n subscription?.unsubscribe();\n\n subscription = cdkMonitor.monitor(el, options?.checkChildren).subscribe(focusOrigin => {\n origin.set(focusOrigin);\n isFocused.set(focusOrigin !== null);\n });\n };\n\n const stopMonitoring = (el: HTMLElement) => {\n origin.set(null);\n isFocused.set(false);\n subscription?.unsubscribe();\n cdkMonitor.stopMonitoring(el);\n };\n\n const focusVia = (origin: FocusOrigin, options?: FocusOptions) => {\n const el = toElement(target);\n\n if (!el) {\n if (ngDevMode) {\n console.warn('[focusMonitor] Cannot focus: element is not available');\n }\n return;\n }\n\n cdkMonitor.focusVia(el, origin, options);\n };\n\n const setupMonitoring = (onCleanup: EffectCleanupRegisterFn) => {\n const el = toElement(target);\n\n if (el) {\n startMonitoring(el);\n onCleanup(() => stopMonitoring(el));\n }\n };\n\n onCleanup(() => subscription?.unsubscribe());\n\n afterRenderEffect({ read: setupMonitoring });\n\n onDisconnect(target, stopMonitoring);\n\n return {\n origin: origin.asReadonly(),\n isFocused: isFocused.asReadonly(),\n focusVia,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;AA+BA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBG;AACG,SAAU,YAAY,CAC1B,MAAuC,EACvC,OAA6B,EAAA;AAE7B,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,YAAY,CAAC;IAEtE,OAAO,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAI;QAC9C,IAAI,QAAQ,EAAE;YACZ,OAAO;AACL,gBAAA,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC;AACzB,gBAAA,SAAS,EAAE,WAAW,CAAC,KAAK,CAAC;AAC7B,gBAAA,QAAQ,EAAE,OAAO;aAClB;QACH;AAEA,QAAA,MAAM,UAAU,GAAG,MAAM,CAAC,YAAY,CAAC;AAEvC,QAAA,MAAM,MAAM,GAAG,MAAM,CAAc,IAAI,kDAAC;AACxC,QAAA,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,qDAAC;AAE/B,QAAA,IAAI,YAAsC;AAE1C,QAAA,MAAM,eAAe,GAAG,CAAC,EAAe,KAAI;YAC1C,YAAY,EAAE,WAAW,EAAE;AAE3B,YAAA,YAAY,GAAG,UAAU,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC,SAAS,CAAC,WAAW,IAAG;AACpF,gBAAA,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC;AACvB,gBAAA,SAAS,CAAC,GAAG,CAAC,WAAW,KAAK,IAAI,CAAC;AACrC,YAAA,CAAC,CAAC;AACJ,QAAA,CAAC;AAED,QAAA,MAAM,cAAc,GAAG,CAAC,EAAe,KAAI;AACzC,YAAA,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;AAChB,YAAA,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;YACpB,YAAY,EAAE,WAAW,EAAE;AAC3B,YAAA,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;AAC/B,QAAA,CAAC;AAED,QAAA,MAAM,QAAQ,GAAG,CAAC,MAAmB,EAAE,OAAsB,KAAI;AAC/D,YAAA,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC;YAE5B,IAAI,CAAC,EAAE,EAAE;gBACP,IAAI,SAAS,EAAE;AACb,oBAAA,OAAO,CAAC,IAAI,CAAC,uDAAuD,CAAC;gBACvE;gBACA;YACF;YAEA,UAAU,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC;AAC1C,QAAA,CAAC;AAED,QAAA,MAAM,eAAe,GAAG,CAAC,SAAkC,KAAI;AAC7D,YAAA,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC;YAE5B,IAAI,EAAE,EAAE;gBACN,eAAe,CAAC,EAAE,CAAC;gBACnB,SAAS,CAAC,MAAM,cAAc,CAAC,EAAE,CAAC,CAAC;YACrC;AACF,QAAA,CAAC;QAED,SAAS,CAAC,MAAM,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5C,QAAA,iBAAiB,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC;AAE5C,QAAA,YAAY,CAAC,MAAM,EAAE,cAAc,CAAC;QAEpC,OAAO;AACL,YAAA,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE;AAC3B,YAAA,SAAS,EAAE,SAAS,CAAC,UAAU,EAAE;YACjC,QAAQ;SACT;AACH,IAAA,CAAC,CAAC;AACJ;;ACjIA;;AAEG;;;;"}
@@ -0,0 +1,47 @@
1
+ import { inject, signal } from '@angular/core';
2
+ import { InputModalityDetector } from '@angular/cdk/a11y';
3
+ import { setupContext, constSignal } from '@signality/core/internal';
4
+
5
+ /**
6
+ * Signal-based wrapper around the [Angular CDK](https://material.angular.io/cdk/a11y/overview) InputModalityDetector.
7
+ *
8
+ * @param options - Optional configuration including signal options and injector
9
+ * @returns A signal containing the current input modality: `'keyboard'`, `'mouse'`, `'touch'`, or `null`
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * @Component({
14
+ * template: `
15
+ * <div [class.keyboard]="modality() === 'keyboard'">
16
+ * <p>Current input: {{ modality() ?? 'none' }}</p>
17
+ * <button>Button with conditional focus ring</button>
18
+ * </div>
19
+ * `
20
+ * })
21
+ * class ModalityComponent {
22
+ * readonly modality = inputModality();
23
+ * }
24
+ * ```
25
+ */
26
+ function inputModality(options) {
27
+ const { runInContext } = setupContext(options?.injector, inputModality);
28
+ return runInContext(({ isServer, onCleanup }) => {
29
+ if (isServer) {
30
+ return constSignal(null);
31
+ }
32
+ const cdkDetector = inject(InputModalityDetector);
33
+ const current = signal(cdkDetector.mostRecentModality, options);
34
+ const subscription = cdkDetector.modalityChanged.subscribe(modality => {
35
+ current.set(modality);
36
+ });
37
+ onCleanup(subscription.unsubscribe.bind(subscription));
38
+ return current.asReadonly();
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Generated bundle index. Do not edit.
44
+ */
45
+
46
+ export { inputModality };
47
+ //# sourceMappingURL=signality-cdk-interop-input-modality.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signality-cdk-interop-input-modality.mjs","sources":["../../../projects/cdk-interop/input-modality/index.ts","../../../projects/cdk-interop/input-modality/signality-cdk-interop-input-modality.ts"],"sourcesContent":["import { type CreateSignalOptions, inject, type Signal, signal } from '@angular/core';\nimport { type InputModality, InputModalityDetector } from '@angular/cdk/a11y';\nimport { WithInjector } from '@signality/core';\nimport { constSignal, setupContext } from '@signality/core/internal';\n\nexport type InputModalityOptions = CreateSignalOptions<InputModality> & WithInjector;\n\n/**\n * Signal-based wrapper around the [Angular CDK](https://material.angular.io/cdk/a11y/overview) InputModalityDetector.\n *\n * @param options - Optional configuration including signal options and injector\n * @returns A signal containing the current input modality: `'keyboard'`, `'mouse'`, `'touch'`, or `null`\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <div [class.keyboard]=\"modality() === 'keyboard'\">\n * <p>Current input: {{ modality() ?? 'none' }}</p>\n * <button>Button with conditional focus ring</button>\n * </div>\n * `\n * })\n * class ModalityComponent {\n * readonly modality = inputModality();\n * }\n * ```\n */\nexport function inputModality(options?: InputModalityOptions): Signal<InputModality> {\n const { runInContext } = setupContext(options?.injector, inputModality);\n\n return runInContext(({ isServer, onCleanup }) => {\n if (isServer) {\n return constSignal(null);\n }\n\n const cdkDetector = inject(InputModalityDetector);\n\n const current = signal<InputModality>(cdkDetector.mostRecentModality, options);\n\n const subscription = cdkDetector.modalityChanged.subscribe(modality => {\n current.set(modality);\n });\n\n onCleanup(subscription.unsubscribe.bind(subscription));\n\n return current.asReadonly();\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAOA;;;;;;;;;;;;;;;;;;;;AAoBG;AACG,SAAU,aAAa,CAAC,OAA8B,EAAA;AAC1D,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,aAAa,CAAC;IAEvE,OAAO,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAI;QAC9C,IAAI,QAAQ,EAAE;AACZ,YAAA,OAAO,WAAW,CAAC,IAAI,CAAC;QAC1B;AAEA,QAAA,MAAM,WAAW,GAAG,MAAM,CAAC,qBAAqB,CAAC;QAEjD,MAAM,OAAO,GAAG,MAAM,CAAgB,WAAW,CAAC,kBAAkB,EAAE,OAAO,CAAC;QAE9E,MAAM,YAAY,GAAG,WAAW,CAAC,eAAe,CAAC,SAAS,CAAC,QAAQ,IAAG;AACpE,YAAA,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;AACvB,QAAA,CAAC,CAAC;QAEF,SAAS,CAAC,YAAY,CAAC,WAAW,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AAEtD,QAAA,OAAO,OAAO,CAAC,UAAU,EAAE;AAC7B,IAAA,CAAC,CAAC;AACJ;;AChDA;;AAEG;;;;"}
@@ -0,0 +1,72 @@
1
+ import { inject, signal } from '@angular/core';
2
+ import { LiveAnnouncer } from '@angular/cdk/a11y';
3
+ import { setupContext, NOOP_FN, constSignal } from '@signality/core/internal';
4
+
5
+ /**
6
+ * Signal-based wrapper around the [Angular CDK](https://material.angular.io/cdk/a11y/overview) LiveAnnouncer.
7
+ *
8
+ * @param options - Optional configuration
9
+ * @returns A LiveAnnouncerRef with announcement methods and last message signal
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * @Component({
14
+ * template: `
15
+ * <div>
16
+ * <button (click)="addToCart()">Add to cart</button>
17
+ * <button (click)="announcer.clear()">Clear Announcements</button>
18
+ * @if (announcer.lastMessage(); as message) {
19
+ * <p>Last: {{ message }}</p>
20
+ * }
21
+ * </div>
22
+ * `
23
+ * })
24
+ * class ShoppingComponent {
25
+ * readonly announcer = liveAnnouncer();
26
+ *
27
+ * addToCart() {
28
+ * this.announcer.announce('Item added to cart', 'polite');
29
+ * }
30
+ * }
31
+ * ```
32
+ */
33
+ function liveAnnouncer(options) {
34
+ const { runInContext } = setupContext(options?.injector, liveAnnouncer);
35
+ return runInContext(({ isServer }) => {
36
+ if (isServer) {
37
+ return {
38
+ lastMessage: constSignal(null),
39
+ announce: NOOP_FN,
40
+ clear: NOOP_FN,
41
+ };
42
+ }
43
+ const cdkLiveAnnouncer = inject(LiveAnnouncer);
44
+ const defaultPoliteness = options?.defaultPoliteness ?? 'polite';
45
+ const lastMessage = signal(null, ...(ngDevMode ? [{ debugName: "lastMessage" }] : []));
46
+ const announce = (message, politeness) => {
47
+ const politenessLevel = politeness ?? defaultPoliteness;
48
+ if (politenessLevel === 'off') {
49
+ return;
50
+ }
51
+ cdkLiveAnnouncer.announce(message, politenessLevel).then(() => {
52
+ lastMessage.set(message);
53
+ });
54
+ };
55
+ const clear = () => {
56
+ cdkLiveAnnouncer.clear();
57
+ lastMessage.set(null);
58
+ };
59
+ return {
60
+ lastMessage: lastMessage.asReadonly(),
61
+ announce,
62
+ clear,
63
+ };
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Generated bundle index. Do not edit.
69
+ */
70
+
71
+ export { liveAnnouncer };
72
+ //# sourceMappingURL=signality-cdk-interop-live-announcer.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signality-cdk-interop-live-announcer.mjs","sources":["../../../projects/cdk-interop/live-announcer/index.ts","../../../projects/cdk-interop/live-announcer/signality-cdk-interop-live-announcer.ts"],"sourcesContent":["import { inject, signal, type Signal } from '@angular/core';\nimport { LiveAnnouncer } from '@angular/cdk/a11y';\nimport { WithInjector } from '@signality/core';\nimport { constSignal, NOOP_FN, setupContext } from '@signality/core/internal';\n\nexport type AriaLivePoliteness = 'polite' | 'assertive' | 'off';\n\nexport interface LiveAnnouncerOptions extends WithInjector {\n /**\n * Default politeness level for announcements.\n * @default 'polite'\n */\n readonly defaultPoliteness?: AriaLivePoliteness;\n}\n\nexport interface LiveAnnouncerRef {\n /** Last announced message */\n readonly lastMessage: Signal<string | null>;\n\n /** Announce a message to screen readers */\n readonly announce: (message: string, politeness?: AriaLivePoliteness) => void;\n\n /** Clear all announcements */\n readonly clear: () => void;\n}\n\n/**\n * Signal-based wrapper around the [Angular CDK](https://material.angular.io/cdk/a11y/overview) LiveAnnouncer.\n *\n * @param options - Optional configuration\n * @returns A LiveAnnouncerRef with announcement methods and last message signal\n *\n * @example\n * ```typescript\n * @Component({\n * template: `\n * <div>\n * <button (click)=\"addToCart()\">Add to cart</button>\n * <button (click)=\"announcer.clear()\">Clear Announcements</button>\n * @if (announcer.lastMessage(); as message) {\n * <p>Last: {{ message }}</p>\n * }\n * </div>\n * `\n * })\n * class ShoppingComponent {\n * readonly announcer = liveAnnouncer();\n *\n * addToCart() {\n * this.announcer.announce('Item added to cart', 'polite');\n * }\n * }\n * ```\n */\nexport function liveAnnouncer(options?: LiveAnnouncerOptions): LiveAnnouncerRef {\n const { runInContext } = setupContext(options?.injector, liveAnnouncer);\n\n return runInContext(({ isServer }) => {\n if (isServer) {\n return {\n lastMessage: constSignal(null),\n announce: NOOP_FN,\n clear: NOOP_FN,\n };\n }\n\n const cdkLiveAnnouncer = inject(LiveAnnouncer);\n const defaultPoliteness = options?.defaultPoliteness ?? 'polite';\n\n const lastMessage = signal<string | null>(null);\n\n const announce = (message: string, politeness?: AriaLivePoliteness) => {\n const politenessLevel = politeness ?? defaultPoliteness;\n\n if (politenessLevel === 'off') {\n return;\n }\n\n cdkLiveAnnouncer.announce(message, politenessLevel as 'polite' | 'assertive').then(() => {\n lastMessage.set(message);\n });\n };\n\n const clear = () => {\n cdkLiveAnnouncer.clear();\n lastMessage.set(null);\n };\n\n return {\n lastMessage: lastMessage.asReadonly(),\n announce,\n clear,\n };\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AA0BA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BG;AACG,SAAU,aAAa,CAAC,OAA8B,EAAA;AAC1D,IAAA,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,aAAa,CAAC;AAEvE,IAAA,OAAO,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAI;QACnC,IAAI,QAAQ,EAAE;YACZ,OAAO;AACL,gBAAA,WAAW,EAAE,WAAW,CAAC,IAAI,CAAC;AAC9B,gBAAA,QAAQ,EAAE,OAAO;AACjB,gBAAA,KAAK,EAAE,OAAO;aACf;QACH;AAEA,QAAA,MAAM,gBAAgB,GAAG,MAAM,CAAC,aAAa,CAAC;AAC9C,QAAA,MAAM,iBAAiB,GAAG,OAAO,EAAE,iBAAiB,IAAI,QAAQ;AAEhE,QAAA,MAAM,WAAW,GAAG,MAAM,CAAgB,IAAI,uDAAC;AAE/C,QAAA,MAAM,QAAQ,GAAG,CAAC,OAAe,EAAE,UAA+B,KAAI;AACpE,YAAA,MAAM,eAAe,GAAG,UAAU,IAAI,iBAAiB;AAEvD,YAAA,IAAI,eAAe,KAAK,KAAK,EAAE;gBAC7B;YACF;YAEA,gBAAgB,CAAC,QAAQ,CAAC,OAAO,EAAE,eAAyC,CAAC,CAAC,IAAI,CAAC,MAAK;AACtF,gBAAA,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC;AAC1B,YAAA,CAAC,CAAC;AACJ,QAAA,CAAC;QAED,MAAM,KAAK,GAAG,MAAK;YACjB,gBAAgB,CAAC,KAAK,EAAE;AACxB,YAAA,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC;AACvB,QAAA,CAAC;QAED,OAAO;AACL,YAAA,WAAW,EAAE,WAAW,CAAC,UAAU,EAAE;YACrC,QAAQ;YACR,KAAK;SACN;AACH,IAAA,CAAC,CAAC;AACJ;;AC9FA;;AAEG;;;;"}
@@ -0,0 +1,8 @@
1
+ export * from '@signality/cdk-interop/focus-monitor';
2
+ export * from '@signality/cdk-interop/input-modality';
3
+ export * from '@signality/cdk-interop/live-announcer';
4
+
5
+ /**
6
+ * Generated bundle index. Do not edit.
7
+ */
8
+ //# sourceMappingURL=signality-cdk-interop.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signality-cdk-interop.mjs","sources":["../../../projects/cdk-interop/signality-cdk-interop.ts"],"sourcesContent":["/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAAA;;AAEG"}
@@ -0,0 +1,45 @@
1
+ import { type Signal } from '@angular/core';
2
+ import { type FocusOrigin } from '@angular/cdk/a11y';
3
+ import { MaybeElementSignal, WithInjector } from '@signality/core';
4
+ export interface FocusMonitorOptions extends WithInjector {
5
+ /**
6
+ * Also monitor focus within children.
7
+ * @default false
8
+ */
9
+ readonly checkChildren?: boolean;
10
+ }
11
+ export interface FocusMonitorRef {
12
+ /** Whether element is focused */
13
+ readonly isFocused: Signal<boolean>;
14
+ /** Focus origin: 'keyboard', 'mouse', 'touch', 'program', or null */
15
+ readonly origin: Signal<FocusOrigin>;
16
+ /** Focus element with specific origin */
17
+ readonly focusVia: (origin: FocusOrigin, options?: FocusOptions) => void;
18
+ }
19
+ /**
20
+ * Signal-based wrapper around the [Angular CDK](https://material.angular.io/cdk/a11y/overview) FocusMonitor.
21
+ *
22
+ * @param target - Target element to monitor
23
+ * @param options - Optional configuration
24
+ * @returns A FocusMonitorRef with focus state signals and control methods
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * @Component({
29
+ * template: `
30
+ * <button #btn [class.focused]="focus.isFocused()">
31
+ * Click me
32
+ * @if (focus.origin(); as origin) {
33
+ * <span>({{ origin }})</span>
34
+ * }
35
+ * </button>
36
+ * <button (click)="focus.focusVia('program')">Focus programmatically</button>
37
+ * `
38
+ * })
39
+ * class FocusComponent {
40
+ * readonly btn = viewChild<ElementRef>('btn');
41
+ * readonly focus = focusMonitor(this.btn);
42
+ * }
43
+ * ```
44
+ */
45
+ export declare function focusMonitor(target: MaybeElementSignal<HTMLElement>, options?: FocusMonitorOptions): FocusMonitorRef;
package/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from '@signality/cdk-interop/focus-monitor';
2
+ export * from '@signality/cdk-interop/input-modality';
3
+ export * from '@signality/cdk-interop/live-announcer';
@@ -0,0 +1,26 @@
1
+ import { type CreateSignalOptions, type Signal } from '@angular/core';
2
+ import { type InputModality } from '@angular/cdk/a11y';
3
+ import { WithInjector } from '@signality/core';
4
+ export type InputModalityOptions = CreateSignalOptions<InputModality> & WithInjector;
5
+ /**
6
+ * Signal-based wrapper around the [Angular CDK](https://material.angular.io/cdk/a11y/overview) InputModalityDetector.
7
+ *
8
+ * @param options - Optional configuration including signal options and injector
9
+ * @returns A signal containing the current input modality: `'keyboard'`, `'mouse'`, `'touch'`, or `null`
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * @Component({
14
+ * template: `
15
+ * <div [class.keyboard]="modality() === 'keyboard'">
16
+ * <p>Current input: {{ modality() ?? 'none' }}</p>
17
+ * <button>Button with conditional focus ring</button>
18
+ * </div>
19
+ * `
20
+ * })
21
+ * class ModalityComponent {
22
+ * readonly modality = inputModality();
23
+ * }
24
+ * ```
25
+ */
26
+ export declare function inputModality(options?: InputModalityOptions): Signal<InputModality>;
@@ -0,0 +1,47 @@
1
+ import { type Signal } from '@angular/core';
2
+ import { WithInjector } from '@signality/core';
3
+ export type AriaLivePoliteness = 'polite' | 'assertive' | 'off';
4
+ export interface LiveAnnouncerOptions extends WithInjector {
5
+ /**
6
+ * Default politeness level for announcements.
7
+ * @default 'polite'
8
+ */
9
+ readonly defaultPoliteness?: AriaLivePoliteness;
10
+ }
11
+ export interface LiveAnnouncerRef {
12
+ /** Last announced message */
13
+ readonly lastMessage: Signal<string | null>;
14
+ /** Announce a message to screen readers */
15
+ readonly announce: (message: string, politeness?: AriaLivePoliteness) => void;
16
+ /** Clear all announcements */
17
+ readonly clear: () => void;
18
+ }
19
+ /**
20
+ * Signal-based wrapper around the [Angular CDK](https://material.angular.io/cdk/a11y/overview) LiveAnnouncer.
21
+ *
22
+ * @param options - Optional configuration
23
+ * @returns A LiveAnnouncerRef with announcement methods and last message signal
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * @Component({
28
+ * template: `
29
+ * <div>
30
+ * <button (click)="addToCart()">Add to cart</button>
31
+ * <button (click)="announcer.clear()">Clear Announcements</button>
32
+ * @if (announcer.lastMessage(); as message) {
33
+ * <p>Last: {{ message }}</p>
34
+ * }
35
+ * </div>
36
+ * `
37
+ * })
38
+ * class ShoppingComponent {
39
+ * readonly announcer = liveAnnouncer();
40
+ *
41
+ * addToCart() {
42
+ * this.announcer.announce('Item added to cart', 'polite');
43
+ * }
44
+ * }
45
+ * ```
46
+ */
47
+ export declare function liveAnnouncer(options?: LiveAnnouncerOptions): LiveAnnouncerRef;
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@signality/cdk-interop",
3
+ "version": "0.0.1-alpha.2",
4
+ "license": "MIT",
5
+ "author": "Vyacheslav Borodin <https://github.com/vs-borodin>",
6
+ "description": "Signal-based utilities for Angular CDK",
7
+ "keywords": [
8
+ "signality",
9
+ "angular",
10
+ "cdk",
11
+ "signals",
12
+ "reactive",
13
+ "accessibility",
14
+ "a11y"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/signalityjs/signality"
19
+ },
20
+ "bugs": "https://github.com/signalityjs/signality/issues",
21
+ "homepage": "https://signality.dev",
22
+ "sideEffects": false,
23
+ "peerDependencies": {
24
+ "@angular/core": ">=19.2.0",
25
+ "@angular/cdk": ">=19.2.0",
26
+ "@signality/core": ">=0.0.1-alpha.1",
27
+ "rxjs": ">=7.8.1"
28
+ },
29
+ "module": "fesm2022/signality-cdk-interop.mjs",
30
+ "typings": "index.d.ts",
31
+ "exports": {
32
+ "./package.json": {
33
+ "default": "./package.json"
34
+ },
35
+ ".": {
36
+ "types": "./index.d.ts",
37
+ "default": "./fesm2022/signality-cdk-interop.mjs"
38
+ },
39
+ "./focus-monitor": {
40
+ "types": "./focus-monitor/index.d.ts",
41
+ "default": "./fesm2022/signality-cdk-interop-focus-monitor.mjs"
42
+ },
43
+ "./input-modality": {
44
+ "types": "./input-modality/index.d.ts",
45
+ "default": "./fesm2022/signality-cdk-interop-input-modality.mjs"
46
+ },
47
+ "./live-announcer": {
48
+ "types": "./live-announcer/index.d.ts",
49
+ "default": "./fesm2022/signality-cdk-interop-live-announcer.mjs"
50
+ }
51
+ },
52
+ "dependencies": {
53
+ "tslib": "^2.3.0"
54
+ }
55
+ }