@memberjunction/ng-ui-components 5.40.2 → 5.42.0
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/dist/lib/button/button.directive.d.ts +26 -3
- package/dist/lib/button/button.directive.d.ts.map +1 -1
- package/dist/lib/button/button.directive.js +54 -4
- package/dist/lib/button/button.directive.js.map +1 -1
- package/dist/lib/clickable/clickable.directive.d.ts +68 -0
- package/dist/lib/clickable/clickable.directive.d.ts.map +1 -0
- package/dist/lib/clickable/clickable.directive.js +128 -0
- package/dist/lib/clickable/clickable.directive.js.map +1 -0
- package/dist/public-api.d.ts +1 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +1 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventEmitter } from '@angular/core';
|
|
1
|
+
import { EventEmitter, ElementRef, AfterContentInit } from '@angular/core';
|
|
2
2
|
import * as i0 from "@angular/core";
|
|
3
3
|
export type MjButtonVariant = 'primary' | 'secondary' | 'outline' | 'flat' | 'danger' | 'icon' | 'success' | 'warning';
|
|
4
4
|
export type MjButtonSize = 'sm' | 'md' | 'lg';
|
|
@@ -12,19 +12,36 @@ export type MjButtonSize = 'sm' | 'md' | 'lg';
|
|
|
12
12
|
* Inputs use lowercase (not PascalCase) per the Phase 2 plan exception for
|
|
13
13
|
* directives — matches HTML attribute convention.
|
|
14
14
|
*
|
|
15
|
+
* Accessibility: an **icon-only** button (`variant="icon"`, no visible text) has no accessible
|
|
16
|
+
* name unless you supply one — set `[ariaLabel]` (or a native `title`). Without a name, screen
|
|
17
|
+
* readers announce it as an unlabeled "button" and DOM/accessibility-tree agents (e.g. computer-use)
|
|
18
|
+
* can't identify it. In dev mode the directive logs a warning for an icon button that ends up with
|
|
19
|
+
* no accessible name.
|
|
20
|
+
*
|
|
15
21
|
* @example
|
|
16
22
|
* ```html
|
|
17
23
|
* <button mjButton variant="primary" (click)="save()">Save</button>
|
|
18
24
|
* <button mjButton variant="outline" size="sm">Cancel</button>
|
|
25
|
+
* <button mjButton variant="icon" ariaLabel="Remove" (click)="remove()"><i class="fa-solid fa-xmark"></i></button>
|
|
19
26
|
* <button mjButton variant="flat" [toggleable]="true" [(selected)]="isActive">Toggle</button>
|
|
20
27
|
* ```
|
|
21
28
|
*/
|
|
22
|
-
export declare class MJButtonDirective {
|
|
29
|
+
export declare class MJButtonDirective implements AfterContentInit {
|
|
30
|
+
private readonly host;
|
|
23
31
|
variant: MjButtonVariant;
|
|
24
32
|
size: MjButtonSize;
|
|
25
33
|
toggleable: boolean;
|
|
26
34
|
selected: boolean;
|
|
27
35
|
selectedChange: EventEmitter<boolean>;
|
|
36
|
+
constructor(host: ElementRef<HTMLElement>);
|
|
37
|
+
/**
|
|
38
|
+
* Accessible name for the button, surfaced as `aria-label`. Primarily for **icon-only** buttons
|
|
39
|
+
* (which otherwise have no name). Applied via `setAttribute` only when set, so a directly-authored
|
|
40
|
+
* `aria-label="..."` on the element is never clobbered.
|
|
41
|
+
*/
|
|
42
|
+
set ariaLabel(value: string | null | undefined);
|
|
43
|
+
get ariaLabel(): string | undefined;
|
|
44
|
+
private _ariaLabel?;
|
|
28
45
|
readonly baseClass = true;
|
|
29
46
|
get isPrimary(): boolean;
|
|
30
47
|
get isSecondary(): boolean;
|
|
@@ -39,7 +56,13 @@ export declare class MJButtonDirective {
|
|
|
39
56
|
get isSelected(): boolean;
|
|
40
57
|
get ariaPressed(): string | null;
|
|
41
58
|
OnClick(): void;
|
|
59
|
+
/**
|
|
60
|
+
* Dev-only guard: an icon-variant button with no accessible name (no `ariaLabel`/`aria-label`/
|
|
61
|
+
* `aria-labelledby`/`title` and no visible text) is invisible-by-name to screen readers and DOM
|
|
62
|
+
* agents. Warn so it gets a name. No-op in production builds.
|
|
63
|
+
*/
|
|
64
|
+
ngAfterContentInit(): void;
|
|
42
65
|
static ɵfac: i0.ɵɵFactoryDeclaration<MJButtonDirective, never>;
|
|
43
|
-
static ɵdir: i0.ɵɵDirectiveDeclaration<MJButtonDirective, "button[mjButton], a[mjButton]", never, { "variant": { "alias": "variant"; "required": false; }; "size": { "alias": "size"; "required": false; }; "toggleable": { "alias": "toggleable"; "required": false; }; "selected": { "alias": "selected"; "required": false; }; }, { "selectedChange": "selectedChange"; }, never, never, true, never>;
|
|
66
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<MJButtonDirective, "button[mjButton], a[mjButton]", never, { "variant": { "alias": "variant"; "required": false; }; "size": { "alias": "size"; "required": false; }; "toggleable": { "alias": "toggleable"; "required": false; }; "selected": { "alias": "selected"; "required": false; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; }; }, { "selectedChange": "selectedChange"; }, never, never, true, never>;
|
|
44
67
|
}
|
|
45
68
|
//# sourceMappingURL=button.directive.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.directive.d.ts","sourceRoot":"","sources":["../../../src/lib/button/button.directive.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,YAAY,
|
|
1
|
+
{"version":3,"file":"button.directive.d.ts","sourceRoot":"","sources":["../../../src/lib/button/button.directive.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,YAAY,EAGZ,UAAU,EACV,gBAAgB,EAEjB,MAAM,eAAe,CAAC;;AAEvB,MAAM,MAAM,eAAe,GACvB,SAAS,GACT,WAAW,GACX,SAAS,GACT,MAAM,GACN,QAAQ,GACR,MAAM,GACN,SAAS,GACT,SAAS,CAAC;AAEd,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAIa,iBAAkB,YAAW,gBAAgB;IAS5C,OAAO,CAAC,QAAQ,CAAC,IAAI;IARxB,OAAO,EAAE,eAAe,CAAe;IACvC,IAAI,EAAE,YAAY,CAAQ;IAC1B,UAAU,UAAS;IACnB,QAAQ,UAAS;IAChB,cAAc,wBAA+B;gBAI1B,IAAI,EAAE,UAAU,CAAC,WAAW,CAAC;IAE1D;;;;OAIG;IACH,IACI,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAK7C;IACD,IAAI,SAAS,IAAI,MAAM,GAAG,SAAS,CAElC;IACD,OAAO,CAAC,UAAU,CAAC,CAAS;IAEC,QAAQ,CAAC,SAAS,QAAQ;IAEvD,IAA0C,SAAS,YAAyC;IAC5F,IAA4C,WAAW,YAA2C;IAClG,IAA0C,SAAS,YAAyC;IAC5F,IAAuC,MAAM,YAAsC;IACnF,IAAyC,QAAQ,YAAwC;IACzF,IAAuC,MAAM,YAAsC;IACnF,IAA0C,SAAS,YAAyC;IAC5F,IAA0C,SAAS,YAAyC;IAE5F,IAAqC,IAAI,YAAiC;IAC1E,IAAqC,IAAI,YAAiC;IAE1E,IAA2C,UAAU,YAA+C;IACpG,IAAsC,WAAW,IAAI,MAAM,GAAG,IAAI,CAEjE;IAGD,OAAO,IAAI,IAAI;IAOf;;;;OAIG;IACH,kBAAkB,IAAI,IAAI;yCA5Df,iBAAiB;2CAAjB,iBAAiB;CAgF7B"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Directive, Input, Output, EventEmitter, HostBinding, HostListener } from '@angular/core';
|
|
1
|
+
import { Directive, Input, Output, EventEmitter, HostBinding, HostListener, isDevMode } from '@angular/core';
|
|
2
2
|
import * as i0 from "@angular/core";
|
|
3
3
|
/**
|
|
4
4
|
* mjButton — Attribute directive that styles a native `<button>` or `<a>` element
|
|
@@ -10,19 +10,47 @@ import * as i0 from "@angular/core";
|
|
|
10
10
|
* Inputs use lowercase (not PascalCase) per the Phase 2 plan exception for
|
|
11
11
|
* directives — matches HTML attribute convention.
|
|
12
12
|
*
|
|
13
|
+
* Accessibility: an **icon-only** button (`variant="icon"`, no visible text) has no accessible
|
|
14
|
+
* name unless you supply one — set `[ariaLabel]` (or a native `title`). Without a name, screen
|
|
15
|
+
* readers announce it as an unlabeled "button" and DOM/accessibility-tree agents (e.g. computer-use)
|
|
16
|
+
* can't identify it. In dev mode the directive logs a warning for an icon button that ends up with
|
|
17
|
+
* no accessible name.
|
|
18
|
+
*
|
|
13
19
|
* @example
|
|
14
20
|
* ```html
|
|
15
21
|
* <button mjButton variant="primary" (click)="save()">Save</button>
|
|
16
22
|
* <button mjButton variant="outline" size="sm">Cancel</button>
|
|
23
|
+
* <button mjButton variant="icon" ariaLabel="Remove" (click)="remove()"><i class="fa-solid fa-xmark"></i></button>
|
|
17
24
|
* <button mjButton variant="flat" [toggleable]="true" [(selected)]="isActive">Toggle</button>
|
|
18
25
|
* ```
|
|
19
26
|
*/
|
|
20
27
|
export class MJButtonDirective {
|
|
28
|
+
host;
|
|
21
29
|
variant = 'secondary';
|
|
22
30
|
size = 'md';
|
|
23
31
|
toggleable = false;
|
|
24
32
|
selected = false;
|
|
25
33
|
selectedChange = new EventEmitter();
|
|
34
|
+
// Constructor injection (not inject()) so the directive can be unit-tested via direct
|
|
35
|
+
// instantiation with a stub ElementRef — the established pattern in this package's specs.
|
|
36
|
+
constructor(host) {
|
|
37
|
+
this.host = host;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Accessible name for the button, surfaced as `aria-label`. Primarily for **icon-only** buttons
|
|
41
|
+
* (which otherwise have no name). Applied via `setAttribute` only when set, so a directly-authored
|
|
42
|
+
* `aria-label="..."` on the element is never clobbered.
|
|
43
|
+
*/
|
|
44
|
+
set ariaLabel(value) {
|
|
45
|
+
this._ariaLabel = value ?? undefined;
|
|
46
|
+
if (this._ariaLabel) {
|
|
47
|
+
this.host.nativeElement.setAttribute('aria-label', this._ariaLabel);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
get ariaLabel() {
|
|
51
|
+
return this._ariaLabel;
|
|
52
|
+
}
|
|
53
|
+
_ariaLabel;
|
|
26
54
|
baseClass = true;
|
|
27
55
|
get isPrimary() { return this.variant === 'primary'; }
|
|
28
56
|
get isSecondary() { return this.variant === 'secondary'; }
|
|
@@ -44,13 +72,33 @@ export class MJButtonDirective {
|
|
|
44
72
|
this.selectedChange.emit(this.selected);
|
|
45
73
|
}
|
|
46
74
|
}
|
|
47
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Dev-only guard: an icon-variant button with no accessible name (no `ariaLabel`/`aria-label`/
|
|
77
|
+
* `aria-labelledby`/`title` and no visible text) is invisible-by-name to screen readers and DOM
|
|
78
|
+
* agents. Warn so it gets a name. No-op in production builds.
|
|
79
|
+
*/
|
|
80
|
+
ngAfterContentInit() {
|
|
81
|
+
if (!isDevMode() || this.variant !== 'icon') {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const el = this.host.nativeElement;
|
|
85
|
+
const hasAccessibleName = !!(this._ariaLabel ||
|
|
86
|
+
el.getAttribute('aria-label') ||
|
|
87
|
+
el.getAttribute('aria-labelledby') ||
|
|
88
|
+
el.getAttribute('title') ||
|
|
89
|
+
(el.textContent ?? '').trim().length > 0);
|
|
90
|
+
if (!hasAccessibleName) {
|
|
91
|
+
console.warn('[mjButton] icon-only button has no accessible name — add [ariaLabel]="…" (or a title) so ' +
|
|
92
|
+
'screen readers and computer-use agents can identify it.', el);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
static ɵfac = function MJButtonDirective_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MJButtonDirective)(i0.ɵɵdirectiveInject(i0.ElementRef)); };
|
|
48
96
|
static ɵdir = /*@__PURE__*/ i0.ɵɵdefineDirective({ type: MJButtonDirective, selectors: [["button", "mjButton", ""], ["a", "mjButton", ""]], hostVars: 25, hostBindings: function MJButtonDirective_HostBindings(rf, ctx) { if (rf & 1) {
|
|
49
97
|
i0.ɵɵlistener("click", function MJButtonDirective_click_HostBindingHandler() { return ctx.OnClick(); });
|
|
50
98
|
} if (rf & 2) {
|
|
51
99
|
i0.ɵɵattribute("aria-pressed", ctx.ariaPressed);
|
|
52
100
|
i0.ɵɵclassProp("mj-btn", ctx.baseClass)("mj-btn--primary", ctx.isPrimary)("mj-btn--secondary", ctx.isSecondary)("mj-btn--outline", ctx.isOutline)("mj-btn--flat", ctx.isFlat)("mj-btn--danger", ctx.isDanger)("mj-btn--icon", ctx.isIcon)("mj-btn--success", ctx.isSuccess)("mj-btn--warning", ctx.isWarning)("mj-btn--sm", ctx.isSm)("mj-btn--lg", ctx.isLg)("mj-btn--selected", ctx.isSelected);
|
|
53
|
-
} }, inputs: { variant: "variant", size: "size", toggleable: "toggleable", selected: "selected" }, outputs: { selectedChange: "selectedChange" } });
|
|
101
|
+
} }, inputs: { variant: "variant", size: "size", toggleable: "toggleable", selected: "selected", ariaLabel: "ariaLabel" }, outputs: { selectedChange: "selectedChange" } });
|
|
54
102
|
}
|
|
55
103
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MJButtonDirective, [{
|
|
56
104
|
type: Directive,
|
|
@@ -58,7 +106,7 @@ export class MJButtonDirective {
|
|
|
58
106
|
selector: 'button[mjButton], a[mjButton]',
|
|
59
107
|
standalone: true
|
|
60
108
|
}]
|
|
61
|
-
}],
|
|
109
|
+
}], () => [{ type: i0.ElementRef }], { variant: [{
|
|
62
110
|
type: Input
|
|
63
111
|
}], size: [{
|
|
64
112
|
type: Input
|
|
@@ -68,6 +116,8 @@ export class MJButtonDirective {
|
|
|
68
116
|
type: Input
|
|
69
117
|
}], selectedChange: [{
|
|
70
118
|
type: Output
|
|
119
|
+
}], ariaLabel: [{
|
|
120
|
+
type: Input
|
|
71
121
|
}], baseClass: [{
|
|
72
122
|
type: HostBinding,
|
|
73
123
|
args: ['class.mj-btn']
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.directive.js","sourceRoot":"","sources":["../../../src/lib/button/button.directive.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,KAAK,EACL,MAAM,EACN,YAAY,EACZ,WAAW,EACX,YAAY,
|
|
1
|
+
{"version":3,"file":"button.directive.js","sourceRoot":"","sources":["../../../src/lib/button/button.directive.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Directive,\n Input,\n Output,\n EventEmitter,\n HostBinding,\n HostListener,\n ElementRef,\n AfterContentInit,\n isDevMode\n} from '@angular/core';\n\nexport type MjButtonVariant =\n | 'primary'\n | 'secondary'\n | 'outline'\n | 'flat'\n | 'danger'\n | 'icon'\n | 'success'\n | 'warning';\n\nexport type MjButtonSize = 'sm' | 'md' | 'lg';\n\n/**\n * mjButton — Attribute directive that styles a native `<button>` or `<a>` element\n * as an MJ-branded button.\n *\n * Uses `--mj-*` design tokens for all colors, ensuring dark-mode support\n * with zero extra CSS.\n *\n * Inputs use lowercase (not PascalCase) per the Phase 2 plan exception for\n * directives — matches HTML attribute convention.\n *\n * Accessibility: an **icon-only** button (`variant=\"icon\"`, no visible text) has no accessible\n * name unless you supply one — set `[ariaLabel]` (or a native `title`). Without a name, screen\n * readers announce it as an unlabeled \"button\" and DOM/accessibility-tree agents (e.g. computer-use)\n * can't identify it. In dev mode the directive logs a warning for an icon button that ends up with\n * no accessible name.\n *\n * @example\n * ```html\n * <button mjButton variant=\"primary\" (click)=\"save()\">Save</button>\n * <button mjButton variant=\"outline\" size=\"sm\">Cancel</button>\n * <button mjButton variant=\"icon\" ariaLabel=\"Remove\" (click)=\"remove()\"><i class=\"fa-solid fa-xmark\"></i></button>\n * <button mjButton variant=\"flat\" [toggleable]=\"true\" [(selected)]=\"isActive\">Toggle</button>\n * ```\n */\n@Directive({\n selector: 'button[mjButton], a[mjButton]',\n standalone: true\n})\nexport class MJButtonDirective implements AfterContentInit {\n @Input() variant: MjButtonVariant = 'secondary';\n @Input() size: MjButtonSize = 'md';\n @Input() toggleable = false;\n @Input() selected = false;\n @Output() selectedChange = new EventEmitter<boolean>();\n\n // Constructor injection (not inject()) so the directive can be unit-tested via direct\n // instantiation with a stub ElementRef — the established pattern in this package's specs.\n constructor(private readonly host: ElementRef<HTMLElement>) {}\n\n /**\n * Accessible name for the button, surfaced as `aria-label`. Primarily for **icon-only** buttons\n * (which otherwise have no name). Applied via `setAttribute` only when set, so a directly-authored\n * `aria-label=\"...\"` on the element is never clobbered.\n */\n @Input()\n set ariaLabel(value: string | null | undefined) {\n this._ariaLabel = value ?? undefined;\n if (this._ariaLabel) {\n this.host.nativeElement.setAttribute('aria-label', this._ariaLabel);\n }\n }\n get ariaLabel(): string | undefined {\n return this._ariaLabel;\n }\n private _ariaLabel?: string;\n\n @HostBinding('class.mj-btn') readonly baseClass = true;\n\n @HostBinding('class.mj-btn--primary') get isPrimary() { return this.variant === 'primary'; }\n @HostBinding('class.mj-btn--secondary') get isSecondary() { return this.variant === 'secondary'; }\n @HostBinding('class.mj-btn--outline') get isOutline() { return this.variant === 'outline'; }\n @HostBinding('class.mj-btn--flat') get isFlat() { return this.variant === 'flat'; }\n @HostBinding('class.mj-btn--danger') get isDanger() { return this.variant === 'danger'; }\n @HostBinding('class.mj-btn--icon') get isIcon() { return this.variant === 'icon'; }\n @HostBinding('class.mj-btn--success') get isSuccess() { return this.variant === 'success'; }\n @HostBinding('class.mj-btn--warning') get isWarning() { return this.variant === 'warning'; }\n\n @HostBinding('class.mj-btn--sm') get isSm() { return this.size === 'sm'; }\n @HostBinding('class.mj-btn--lg') get isLg() { return this.size === 'lg'; }\n\n @HostBinding('class.mj-btn--selected') get isSelected() { return this.toggleable && this.selected; }\n @HostBinding('attr.aria-pressed') get ariaPressed(): string | null {\n return this.toggleable ? String(this.selected) : null;\n }\n\n @HostListener('click')\n OnClick(): void {\n if (this.toggleable) {\n this.selected = !this.selected;\n this.selectedChange.emit(this.selected);\n }\n }\n\n /**\n * Dev-only guard: an icon-variant button with no accessible name (no `ariaLabel`/`aria-label`/\n * `aria-labelledby`/`title` and no visible text) is invisible-by-name to screen readers and DOM\n * agents. Warn so it gets a name. No-op in production builds.\n */\n ngAfterContentInit(): void {\n if (!isDevMode() || this.variant !== 'icon') {\n return;\n }\n const el = this.host.nativeElement;\n const hasAccessibleName = !!(\n this._ariaLabel ||\n el.getAttribute('aria-label') ||\n el.getAttribute('aria-labelledby') ||\n el.getAttribute('title') ||\n (el.textContent ?? '').trim().length > 0\n );\n if (!hasAccessibleName) {\n console.warn(\n '[mjButton] icon-only button has no accessible name — add [ariaLabel]=\"…\" (or a title) so ' +\n 'screen readers and computer-use agents can identify it.',\n el\n );\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ElementRef, AfterContentInit } from '@angular/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
export type MjClickableRole = 'button' | 'link';
|
|
4
|
+
/**
|
|
5
|
+
* mjClickable — Attribute directive that upgrades a non-semantic element (a `<div>`/`<span>` that
|
|
6
|
+
* has a `(click)` handler) into a proper, accessible, keyboard-operable control **without changing
|
|
7
|
+
* its tag or styling**.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this exists
|
|
10
|
+
* A `<div (click)="…">` is invisible as a control to anyone (and anything) that consumes the
|
|
11
|
+
* **accessibility tree** rather than pixels:
|
|
12
|
+
* - screen-reader and keyboard users can't reach or operate it (no role, no focus, no Enter/Space);
|
|
13
|
+
* - DOM/accessibility-tree AI agents (e.g. computer-use driving a live browser) can't enumerate it
|
|
14
|
+
* as a clickable or find it by name — it resolves to `role=generic` with no accessible name.
|
|
15
|
+
*
|
|
16
|
+
* Converting such a `<div>` to a native `<button>` is the ideal fix, but often forces CSS churn for
|
|
17
|
+
* already-styled tiles/rows. `mjClickable` is the low-risk alternative: it keeps the element and its
|
|
18
|
+
* styles, and adds the missing semantics — `role`, `tabindex`, an accessible name (`aria-label`),
|
|
19
|
+
* Enter/Space activation (which dispatches a native `click`, so your existing `(click)` handler runs
|
|
20
|
+
* for both mouse and keyboard), and an optional stable `data-testid` automation hook.
|
|
21
|
+
*
|
|
22
|
+
* Prefer a real `<button mjButton>`/`<a>` for NEW markup; reach for `mjClickable` to retrofit existing
|
|
23
|
+
* clickable `<div>`/`<span>` widgets (nav tiles, list rows, cards) cheaply.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```html
|
|
27
|
+
* <!-- Retrofit a clickable app tile: stays a styled <div>, becomes a named, keyboard-operable button -->
|
|
28
|
+
* <div class="app-card" [mjClickable]="app.Name" [testId]="'app-tile-' + app.Name" (click)="open(app)">
|
|
29
|
+
* …icon + name + description…
|
|
30
|
+
* </div>
|
|
31
|
+
*
|
|
32
|
+
* <!-- A navigation-style row -->
|
|
33
|
+
* <div class="nav-item" [mjClickable]="item.Label" role="link" (click)="navigate(item)">…</div>
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare class MJClickableDirective implements AfterContentInit {
|
|
37
|
+
private readonly host;
|
|
38
|
+
/**
|
|
39
|
+
* Accessible name for the control, surfaced as `aria-label`. Pass the human-meaningful label
|
|
40
|
+
* (e.g. the app/nav-item name). Strongly recommended — without it the control is operable but
|
|
41
|
+
* unnamed, so agents/AT can reach it but can't identify it. When omitted, the element's own
|
|
42
|
+
* visible text becomes the name (fine when the text alone is unambiguous).
|
|
43
|
+
*/
|
|
44
|
+
label: string | null | undefined;
|
|
45
|
+
/** ARIA role to expose. `'button'` (default) for actions, `'link'` for navigation-like targets. */
|
|
46
|
+
role: MjClickableRole;
|
|
47
|
+
/** Optional stable automation/test hook emitted as `data-testid` (no convention enforced here). */
|
|
48
|
+
testId?: string;
|
|
49
|
+
/** Set `false` to skip making the element keyboard-focusable (rare; e.g. focus handled elsewhere). */
|
|
50
|
+
focusable: boolean;
|
|
51
|
+
constructor(host: ElementRef<HTMLElement>);
|
|
52
|
+
get roleAttr(): string;
|
|
53
|
+
get tabIndexAttr(): string | null;
|
|
54
|
+
get ariaLabelAttr(): string | null;
|
|
55
|
+
get testIdAttr(): string | null;
|
|
56
|
+
/**
|
|
57
|
+
* Keyboard activation: Enter / Space activate a `role="button"`/`"link"` exactly as they would a
|
|
58
|
+
* native control. We `preventDefault` (Space would otherwise scroll the page) and dispatch a native
|
|
59
|
+
* `click`, so the element's existing `(click)` binding handles both mouse and keyboard with no
|
|
60
|
+
* duplicate wiring.
|
|
61
|
+
*/
|
|
62
|
+
onKeydown(event: KeyboardEvent): void;
|
|
63
|
+
/** Dev-only guard: warn when the control ends up with no accessible name. No-op in production. */
|
|
64
|
+
ngAfterContentInit(): void;
|
|
65
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<MJClickableDirective, never>;
|
|
66
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<MJClickableDirective, "[mjClickable]", never, { "label": { "alias": "mjClickable"; "required": false; }; "role": { "alias": "role"; "required": false; }; "testId": { "alias": "testId"; "required": false; }; "focusable": { "alias": "focusable"; "required": false; }; }, {}, never, never, true, never>;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=clickable.directive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clickable.directive.d.ts","sourceRoot":"","sources":["../../../src/lib/clickable/clickable.directive.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,UAAU,EACV,gBAAgB,EAEjB,MAAM,eAAe,CAAC;;AAEvB,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,qBAIa,oBAAqB,YAAW,gBAAgB;IAoB/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IAnBjC;;;;;OAKG;IACmB,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAM;IAE5D,mGAAmG;IAC1F,IAAI,EAAE,eAAe,CAAY;IAE1C,mGAAmG;IAC1F,MAAM,CAAC,EAAE,MAAM,CAAC;IAEzB,sGAAsG;IAC7F,SAAS,UAAQ;gBAIG,IAAI,EAAE,UAAU,CAAC,WAAW,CAAC;IAE1D,IAA8B,QAAQ,IAAI,MAAM,CAE/C;IACD,IAAkC,YAAY,IAAI,MAAM,GAAG,IAAI,CAE9D;IACD,IAAoC,aAAa,IAAI,MAAM,GAAG,IAAI,CAEjE;IACD,IAAqC,UAAU,IAAI,MAAM,GAAG,IAAI,CAE/D;IAED;;;;;OAKG;IAEH,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAOrC,kGAAkG;IAClG,kBAAkB,IAAI,IAAI;yCAlDf,oBAAoB;2CAApB,oBAAoB;CA+DhC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Directive, Input, HostBinding, HostListener, isDevMode } from '@angular/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
/**
|
|
4
|
+
* mjClickable — Attribute directive that upgrades a non-semantic element (a `<div>`/`<span>` that
|
|
5
|
+
* has a `(click)` handler) into a proper, accessible, keyboard-operable control **without changing
|
|
6
|
+
* its tag or styling**.
|
|
7
|
+
*
|
|
8
|
+
* ## Why this exists
|
|
9
|
+
* A `<div (click)="…">` is invisible as a control to anyone (and anything) that consumes the
|
|
10
|
+
* **accessibility tree** rather than pixels:
|
|
11
|
+
* - screen-reader and keyboard users can't reach or operate it (no role, no focus, no Enter/Space);
|
|
12
|
+
* - DOM/accessibility-tree AI agents (e.g. computer-use driving a live browser) can't enumerate it
|
|
13
|
+
* as a clickable or find it by name — it resolves to `role=generic` with no accessible name.
|
|
14
|
+
*
|
|
15
|
+
* Converting such a `<div>` to a native `<button>` is the ideal fix, but often forces CSS churn for
|
|
16
|
+
* already-styled tiles/rows. `mjClickable` is the low-risk alternative: it keeps the element and its
|
|
17
|
+
* styles, and adds the missing semantics — `role`, `tabindex`, an accessible name (`aria-label`),
|
|
18
|
+
* Enter/Space activation (which dispatches a native `click`, so your existing `(click)` handler runs
|
|
19
|
+
* for both mouse and keyboard), and an optional stable `data-testid` automation hook.
|
|
20
|
+
*
|
|
21
|
+
* Prefer a real `<button mjButton>`/`<a>` for NEW markup; reach for `mjClickable` to retrofit existing
|
|
22
|
+
* clickable `<div>`/`<span>` widgets (nav tiles, list rows, cards) cheaply.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```html
|
|
26
|
+
* <!-- Retrofit a clickable app tile: stays a styled <div>, becomes a named, keyboard-operable button -->
|
|
27
|
+
* <div class="app-card" [mjClickable]="app.Name" [testId]="'app-tile-' + app.Name" (click)="open(app)">
|
|
28
|
+
* …icon + name + description…
|
|
29
|
+
* </div>
|
|
30
|
+
*
|
|
31
|
+
* <!-- A navigation-style row -->
|
|
32
|
+
* <div class="nav-item" [mjClickable]="item.Label" role="link" (click)="navigate(item)">…</div>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class MJClickableDirective {
|
|
36
|
+
host;
|
|
37
|
+
/**
|
|
38
|
+
* Accessible name for the control, surfaced as `aria-label`. Pass the human-meaningful label
|
|
39
|
+
* (e.g. the app/nav-item name). Strongly recommended — without it the control is operable but
|
|
40
|
+
* unnamed, so agents/AT can reach it but can't identify it. When omitted, the element's own
|
|
41
|
+
* visible text becomes the name (fine when the text alone is unambiguous).
|
|
42
|
+
*/
|
|
43
|
+
label = '';
|
|
44
|
+
/** ARIA role to expose. `'button'` (default) for actions, `'link'` for navigation-like targets. */
|
|
45
|
+
role = 'button';
|
|
46
|
+
/** Optional stable automation/test hook emitted as `data-testid` (no convention enforced here). */
|
|
47
|
+
testId;
|
|
48
|
+
/** Set `false` to skip making the element keyboard-focusable (rare; e.g. focus handled elsewhere). */
|
|
49
|
+
focusable = true;
|
|
50
|
+
// Constructor injection (not inject()) so the directive can be unit-tested via direct
|
|
51
|
+
// instantiation with a stub ElementRef — the established pattern in this package's specs.
|
|
52
|
+
constructor(host) {
|
|
53
|
+
this.host = host;
|
|
54
|
+
}
|
|
55
|
+
get roleAttr() {
|
|
56
|
+
return this.role;
|
|
57
|
+
}
|
|
58
|
+
get tabIndexAttr() {
|
|
59
|
+
return this.focusable ? '0' : null;
|
|
60
|
+
}
|
|
61
|
+
get ariaLabelAttr() {
|
|
62
|
+
return this.label?.trim() ? this.label.trim() : null;
|
|
63
|
+
}
|
|
64
|
+
get testIdAttr() {
|
|
65
|
+
return this.testId ?? null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Keyboard activation: Enter / Space activate a `role="button"`/`"link"` exactly as they would a
|
|
69
|
+
* native control. We `preventDefault` (Space would otherwise scroll the page) and dispatch a native
|
|
70
|
+
* `click`, so the element's existing `(click)` binding handles both mouse and keyboard with no
|
|
71
|
+
* duplicate wiring.
|
|
72
|
+
*/
|
|
73
|
+
onKeydown(event) {
|
|
74
|
+
if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
this.host.nativeElement.click();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Dev-only guard: warn when the control ends up with no accessible name. No-op in production. */
|
|
80
|
+
ngAfterContentInit() {
|
|
81
|
+
if (!isDevMode()) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const hasName = !!(this.label?.trim() || (this.host.nativeElement.textContent ?? '').trim());
|
|
85
|
+
if (!hasName) {
|
|
86
|
+
console.warn('[mjClickable] element has no accessible name — pass mjClickable="<name>" so screen readers ' +
|
|
87
|
+
'and computer-use agents can identify it.', this.host.nativeElement);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
static ɵfac = function MJClickableDirective_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MJClickableDirective)(i0.ɵɵdirectiveInject(i0.ElementRef)); };
|
|
91
|
+
static ɵdir = /*@__PURE__*/ i0.ɵɵdefineDirective({ type: MJClickableDirective, selectors: [["", "mjClickable", ""]], hostVars: 4, hostBindings: function MJClickableDirective_HostBindings(rf, ctx) { if (rf & 1) {
|
|
92
|
+
i0.ɵɵlistener("keydown", function MJClickableDirective_keydown_HostBindingHandler($event) { return ctx.onKeydown($event); });
|
|
93
|
+
} if (rf & 2) {
|
|
94
|
+
i0.ɵɵattribute("role", ctx.roleAttr)("tabindex", ctx.tabIndexAttr)("aria-label", ctx.ariaLabelAttr)("data-testid", ctx.testIdAttr);
|
|
95
|
+
} }, inputs: { label: [0, "mjClickable", "label"], role: "role", testId: "testId", focusable: "focusable" } });
|
|
96
|
+
}
|
|
97
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MJClickableDirective, [{
|
|
98
|
+
type: Directive,
|
|
99
|
+
args: [{
|
|
100
|
+
selector: '[mjClickable]',
|
|
101
|
+
standalone: true
|
|
102
|
+
}]
|
|
103
|
+
}], () => [{ type: i0.ElementRef }], { label: [{
|
|
104
|
+
type: Input,
|
|
105
|
+
args: ['mjClickable']
|
|
106
|
+
}], role: [{
|
|
107
|
+
type: Input
|
|
108
|
+
}], testId: [{
|
|
109
|
+
type: Input
|
|
110
|
+
}], focusable: [{
|
|
111
|
+
type: Input
|
|
112
|
+
}], roleAttr: [{
|
|
113
|
+
type: HostBinding,
|
|
114
|
+
args: ['attr.role']
|
|
115
|
+
}], tabIndexAttr: [{
|
|
116
|
+
type: HostBinding,
|
|
117
|
+
args: ['attr.tabindex']
|
|
118
|
+
}], ariaLabelAttr: [{
|
|
119
|
+
type: HostBinding,
|
|
120
|
+
args: ['attr.aria-label']
|
|
121
|
+
}], testIdAttr: [{
|
|
122
|
+
type: HostBinding,
|
|
123
|
+
args: ['attr.data-testid']
|
|
124
|
+
}], onKeydown: [{
|
|
125
|
+
type: HostListener,
|
|
126
|
+
args: ['keydown', ['$event']]
|
|
127
|
+
}] }); })();
|
|
128
|
+
//# sourceMappingURL=clickable.directive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clickable.directive.js","sourceRoot":"","sources":["../../../src/lib/clickable/clickable.directive.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,KAAK,EACL,WAAW,EACX,YAAY,EAGZ,SAAS,EACV,MAAM,eAAe,CAAC;;AAIvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAKH,MAAM,OAAO,oBAAoB;IAoBF;IAnB7B;;;;;OAKG;IACmB,KAAK,GAA8B,EAAE,CAAC;IAE5D,mGAAmG;IAC1F,IAAI,GAAoB,QAAQ,CAAC;IAE1C,mGAAmG;IAC1F,MAAM,CAAU;IAEzB,sGAAsG;IAC7F,SAAS,GAAG,IAAI,CAAC;IAE1B,sFAAsF;IACtF,0FAA0F;IAC1F,YAA6B,IAA6B;QAA7B,SAAI,GAAJ,IAAI,CAAyB;IAAG,CAAC;IAE9D,IAA8B,QAAQ;QACpC,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IACD,IAAkC,YAAY;QAC5C,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IACrC,CAAC;IACD,IAAoC,aAAa;QAC/C,OAAO,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACvD,CAAC;IACD,IAAqC,UAAU;QAC7C,OAAO,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC;IAC7B,CAAC;IAED;;;;;OAKG;IAEH,SAAS,CAAC,KAAoB;QAC5B,IAAI,KAAK,CAAC,GAAG,KAAK,OAAO,IAAI,KAAK,CAAC,GAAG,KAAK,GAAG,IAAI,KAAK,CAAC,GAAG,KAAK,UAAU,EAAE,CAAC;YAC3E,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC;QAClC,CAAC;IACH,CAAC;IAED,kGAAkG;IAClG,kBAAkB;QAChB,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7F,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CACV,6FAA6F;gBAC3F,0CAA0C,EAC5C,IAAI,CAAC,IAAI,CAAC,aAAa,CACxB,CAAC;QACJ,CAAC;IACH,CAAC;8GA9DU,oBAAoB;6DAApB,oBAAoB;YAApB,mGAAA,qBAAiB,IAAG;;;;;iFAApB,oBAAoB;cAJhC,SAAS;eAAC;gBACT,QAAQ,EAAE,eAAe;gBACzB,UAAU,EAAE,IAAI;aACjB;;kBAQE,KAAK;mBAAC,aAAa;;kBAGnB,KAAK;;kBAGL,KAAK;;kBAGL,KAAK;;kBAML,WAAW;mBAAC,WAAW;;kBAGvB,WAAW;mBAAC,eAAe;;kBAG3B,WAAW;mBAAC,iBAAiB;;kBAG7B,WAAW;mBAAC,kBAAkB;;kBAU9B,YAAY;mBAAC,SAAS,EAAE,CAAC,QAAQ,CAAC","sourcesContent":["import {\n Directive,\n Input,\n HostBinding,\n HostListener,\n ElementRef,\n AfterContentInit,\n isDevMode\n} from '@angular/core';\n\nexport type MjClickableRole = 'button' | 'link';\n\n/**\n * mjClickable — Attribute directive that upgrades a non-semantic element (a `<div>`/`<span>` that\n * has a `(click)` handler) into a proper, accessible, keyboard-operable control **without changing\n * its tag or styling**.\n *\n * ## Why this exists\n * A `<div (click)=\"…\">` is invisible as a control to anyone (and anything) that consumes the\n * **accessibility tree** rather than pixels:\n * - screen-reader and keyboard users can't reach or operate it (no role, no focus, no Enter/Space);\n * - DOM/accessibility-tree AI agents (e.g. computer-use driving a live browser) can't enumerate it\n * as a clickable or find it by name — it resolves to `role=generic` with no accessible name.\n *\n * Converting such a `<div>` to a native `<button>` is the ideal fix, but often forces CSS churn for\n * already-styled tiles/rows. `mjClickable` is the low-risk alternative: it keeps the element and its\n * styles, and adds the missing semantics — `role`, `tabindex`, an accessible name (`aria-label`),\n * Enter/Space activation (which dispatches a native `click`, so your existing `(click)` handler runs\n * for both mouse and keyboard), and an optional stable `data-testid` automation hook.\n *\n * Prefer a real `<button mjButton>`/`<a>` for NEW markup; reach for `mjClickable` to retrofit existing\n * clickable `<div>`/`<span>` widgets (nav tiles, list rows, cards) cheaply.\n *\n * @example\n * ```html\n * <!-- Retrofit a clickable app tile: stays a styled <div>, becomes a named, keyboard-operable button -->\n * <div class=\"app-card\" [mjClickable]=\"app.Name\" [testId]=\"'app-tile-' + app.Name\" (click)=\"open(app)\">\n * …icon + name + description…\n * </div>\n *\n * <!-- A navigation-style row -->\n * <div class=\"nav-item\" [mjClickable]=\"item.Label\" role=\"link\" (click)=\"navigate(item)\">…</div>\n * ```\n */\n@Directive({\n selector: '[mjClickable]',\n standalone: true\n})\nexport class MJClickableDirective implements AfterContentInit {\n /**\n * Accessible name for the control, surfaced as `aria-label`. Pass the human-meaningful label\n * (e.g. the app/nav-item name). Strongly recommended — without it the control is operable but\n * unnamed, so agents/AT can reach it but can't identify it. When omitted, the element's own\n * visible text becomes the name (fine when the text alone is unambiguous).\n */\n @Input('mjClickable') label: string | null | undefined = '';\n\n /** ARIA role to expose. `'button'` (default) for actions, `'link'` for navigation-like targets. */\n @Input() role: MjClickableRole = 'button';\n\n /** Optional stable automation/test hook emitted as `data-testid` (no convention enforced here). */\n @Input() testId?: string;\n\n /** Set `false` to skip making the element keyboard-focusable (rare; e.g. focus handled elsewhere). */\n @Input() focusable = true;\n\n // Constructor injection (not inject()) so the directive can be unit-tested via direct\n // instantiation with a stub ElementRef — the established pattern in this package's specs.\n constructor(private readonly host: ElementRef<HTMLElement>) {}\n\n @HostBinding('attr.role') get roleAttr(): string {\n return this.role;\n }\n @HostBinding('attr.tabindex') get tabIndexAttr(): string | null {\n return this.focusable ? '0' : null;\n }\n @HostBinding('attr.aria-label') get ariaLabelAttr(): string | null {\n return this.label?.trim() ? this.label.trim() : null;\n }\n @HostBinding('attr.data-testid') get testIdAttr(): string | null {\n return this.testId ?? null;\n }\n\n /**\n * Keyboard activation: Enter / Space activate a `role=\"button\"`/`\"link\"` exactly as they would a\n * native control. We `preventDefault` (Space would otherwise scroll the page) and dispatch a native\n * `click`, so the element's existing `(click)` binding handles both mouse and keyboard with no\n * duplicate wiring.\n */\n @HostListener('keydown', ['$event'])\n onKeydown(event: KeyboardEvent): void {\n if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {\n event.preventDefault();\n this.host.nativeElement.click();\n }\n }\n\n /** Dev-only guard: warn when the control ends up with no accessible name. No-op in production. */\n ngAfterContentInit(): void {\n if (!isDevMode()) {\n return;\n }\n const hasName = !!(this.label?.trim() || (this.host.nativeElement.textContent ?? '').trim());\n if (!hasName) {\n console.warn(\n '[mjClickable] element has no accessible name — pass mjClickable=\"<name>\" so screen readers ' +\n 'and computer-use agents can identify it.',\n this.host.nativeElement\n );\n }\n }\n}\n"]}
|
package/dist/public-api.d.ts
CHANGED
package/dist/public-api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public-api.d.ts","sourceRoot":"","sources":["../src/public-api.ts"],"names":[],"mappings":"AAGA,cAAc,+BAA+B,CAAC;AAC9C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mCAAmC,CAAC;AAClD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6CAA6C,CAAC;AAC5D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,uCAAuC,CAAC;AACtD,cAAc,mCAAmC,CAAC;AAClD,cAAc,qCAAqC,CAAC;AACpD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,yCAAyC,CAAC;AACxD,cAAc,mCAAmC,CAAC;AAClD,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,qCAAqC,CAAC;AACpD,cAAc,uCAAuC,CAAC;AACtD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,iCAAiC,CAAC;AAChD,cAAc,yCAAyC,CAAC;AACxD,cAAc,mCAAmC,CAAC;AAClD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2DAA2D,CAAC;AAC1E,cAAc,uDAAuD,CAAC"}
|
|
1
|
+
{"version":3,"file":"public-api.d.ts","sourceRoot":"","sources":["../src/public-api.ts"],"names":[],"mappings":"AAGA,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qCAAqC,CAAC;AACpD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mCAAmC,CAAC;AAClD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6CAA6C,CAAC;AAC5D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,uCAAuC,CAAC;AACtD,cAAc,mCAAmC,CAAC;AAClD,cAAc,qCAAqC,CAAC;AACpD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,yCAAyC,CAAC;AACxD,cAAc,mCAAmC,CAAC;AAClD,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,qCAAqC,CAAC;AACpD,cAAc,uCAAuC,CAAC;AACtD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,iCAAiC,CAAC;AAChD,cAAc,yCAAyC,CAAC;AACxD,cAAc,mCAAmC,CAAC;AAClD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2DAA2D,CAAC;AAC1E,cAAc,uDAAuD,CAAC"}
|
package/dist/public-api.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @memberjunction/ng-ui-components
|
|
2
2
|
// Reusable standalone Angular UI components for MemberJunction
|
|
3
3
|
export * from './lib/button/button.directive';
|
|
4
|
+
export * from './lib/clickable/clickable.directive';
|
|
4
5
|
export * from './lib/dialog/dialog.component';
|
|
5
6
|
export * from './lib/dialog/dialog.service';
|
|
6
7
|
export * from './lib/dropdown/dropdown.component';
|
package/dist/public-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public-api.js","sourceRoot":"","sources":["../src/public-api.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,+DAA+D;AAE/D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mCAAmC,CAAC;AAClD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6CAA6C,CAAC;AAC5D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,uCAAuC,CAAC;AACtD,cAAc,mCAAmC,CAAC;AAClD,cAAc,qCAAqC,CAAC;AACpD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,yCAAyC,CAAC;AACxD,cAAc,mCAAmC,CAAC;AAClD,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,qCAAqC,CAAC;AACpD,cAAc,uCAAuC,CAAC;AACtD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,iCAAiC,CAAC;AAChD,cAAc,yCAAyC,CAAC;AACxD,cAAc,mCAAmC,CAAC;AAClD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2DAA2D,CAAC;AAC1E,cAAc,uDAAuD,CAAC","sourcesContent":["// @memberjunction/ng-ui-components\n// Reusable standalone Angular UI components for MemberJunction\n\nexport * from './lib/button/button.directive';\nexport * from './lib/dialog/dialog.component';\nexport * from './lib/dialog/dialog.service';\nexport * from './lib/dropdown/dropdown.component';\nexport * from './lib/switch/switch.component';\nexport * from './lib/numeric-input/numeric-input.component';\nexport * from './lib/progress-bar/progress-bar.component';\nexport * from './lib/calendar/calendar-utils';\nexport * from './lib/datepicker/datepicker.component';\nexport * from './lib/splitter/splitter.component';\nexport * from './lib/accordion/accordion.component';\nexport * from './lib/window/window.component';\nexport * from './lib/slide-panel/slide-panel.component';\nexport * from './lib/combobox/combobox.component';\nexport * from './lib/page-header/page-header.component';\nexport * from './lib/page-layout/page-layout.component';\nexport * from './lib/page-body/page-body.component';\nexport * from './lib/stat-badge/stat-badge.component';\nexport * from './lib/refresh-button/refresh-button.component';\nexport * from './lib/filter-popover/filter-popover.component';\nexport * from './lib/page-search/page-search.component';\nexport * from './lib/filter-chip/filter-chip.component';\nexport * from './lib/filter-field/filter-field.component';\nexport * from './lib/filter-panel/filter-panel.component';\nexport * from './lib/tab-nav/tab-nav.component';\nexport * from './lib/view-toggle/view-toggle.component';\nexport * from './lib/left-nav/left-nav.component';\nexport * from './lib/left-nav/left-nav-content.component';\nexport * from './lib/page-header-interior/page-header-interior.component';\nexport * from './lib/page-body-interior/page-body-interior.component';\n"]}
|
|
1
|
+
{"version":3,"file":"public-api.js","sourceRoot":"","sources":["../src/public-api.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,+DAA+D;AAE/D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qCAAqC,CAAC;AACpD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mCAAmC,CAAC;AAClD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6CAA6C,CAAC;AAC5D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,uCAAuC,CAAC;AACtD,cAAc,mCAAmC,CAAC;AAClD,cAAc,qCAAqC,CAAC;AACpD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,yCAAyC,CAAC;AACxD,cAAc,mCAAmC,CAAC;AAClD,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,qCAAqC,CAAC;AACpD,cAAc,uCAAuC,CAAC;AACtD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,iCAAiC,CAAC;AAChD,cAAc,yCAAyC,CAAC;AACxD,cAAc,mCAAmC,CAAC;AAClD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2DAA2D,CAAC;AAC1E,cAAc,uDAAuD,CAAC","sourcesContent":["// @memberjunction/ng-ui-components\n// Reusable standalone Angular UI components for MemberJunction\n\nexport * from './lib/button/button.directive';\nexport * from './lib/clickable/clickable.directive';\nexport * from './lib/dialog/dialog.component';\nexport * from './lib/dialog/dialog.service';\nexport * from './lib/dropdown/dropdown.component';\nexport * from './lib/switch/switch.component';\nexport * from './lib/numeric-input/numeric-input.component';\nexport * from './lib/progress-bar/progress-bar.component';\nexport * from './lib/calendar/calendar-utils';\nexport * from './lib/datepicker/datepicker.component';\nexport * from './lib/splitter/splitter.component';\nexport * from './lib/accordion/accordion.component';\nexport * from './lib/window/window.component';\nexport * from './lib/slide-panel/slide-panel.component';\nexport * from './lib/combobox/combobox.component';\nexport * from './lib/page-header/page-header.component';\nexport * from './lib/page-layout/page-layout.component';\nexport * from './lib/page-body/page-body.component';\nexport * from './lib/stat-badge/stat-badge.component';\nexport * from './lib/refresh-button/refresh-button.component';\nexport * from './lib/filter-popover/filter-popover.component';\nexport * from './lib/page-search/page-search.component';\nexport * from './lib/filter-chip/filter-chip.component';\nexport * from './lib/filter-field/filter-field.component';\nexport * from './lib/filter-panel/filter-panel.component';\nexport * from './lib/tab-nav/tab-nav.component';\nexport * from './lib/view-toggle/view-toggle.component';\nexport * from './lib/left-nav/left-nav.component';\nexport * from './lib/left-nav/left-nav-content.component';\nexport * from './lib/page-header-interior/page-header-interior.component';\nexport * from './lib/page-body-interior/page-body-interior.component';\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ng-ui-components",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.42.0",
|
|
4
4
|
"description": "MemberJunction: Angular UI Components - reusable standalone components replacing Kendo UI dependencies.",
|
|
5
5
|
"main": "./dist/public-api.js",
|
|
6
6
|
"typings": "./dist/public-api.d.ts",
|