@lifecorner/angular-ui-kit 3.34.0 → 3.35.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/fesm2022/lifecorner-angular-ui-kit-alert.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-alert.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-bottom-nav.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-combobox.mjs +5 -2
- package/fesm2022/lifecorner-angular-ui-kit-combobox.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-drawer.mjs +30 -2
- package/fesm2022/lifecorner-angular-ui-kit-drawer.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-internal-overlay.mjs +6 -1
- package/fesm2022/lifecorner-angular-ui-kit-internal-overlay.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-modal.mjs +2 -4
- package/fesm2022/lifecorner-angular-ui-kit-modal.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-sidenav.mjs +41 -1
- package/fesm2022/lifecorner-angular-ui-kit-sidenav.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-textarea.mjs +3 -0
- package/fesm2022/lifecorner-angular-ui-kit-textarea.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-toast.mjs.map +1 -1
- package/package.json +1 -1
- package/src/styles/theme-contrast.spec.ts +2 -0
- package/types/lifecorner-angular-ui-kit-alert.d.ts +12 -2
- package/types/lifecorner-angular-ui-kit-bottom-nav.d.ts +5 -1
- package/types/lifecorner-angular-ui-kit-combobox.d.ts +21 -0
- package/types/lifecorner-angular-ui-kit-drawer.d.ts +37 -1
- package/types/lifecorner-angular-ui-kit-gallery.d.ts +3 -1
- package/types/lifecorner-angular-ui-kit-internal-overlay.d.ts +9 -1
- package/types/lifecorner-angular-ui-kit-sidenav.d.ts +42 -1
- package/types/lifecorner-angular-ui-kit-textarea.d.ts +17 -0
- package/types/lifecorner-angular-ui-kit-toast.d.ts +4 -2
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as i0 from "@angular/core";
|
|
2
|
-
import { ChangeDetectionStrategy, Component, HostListener, computed, effect, inject, input, output, signal, untracked } from "@angular/core";
|
|
2
|
+
import { ChangeDetectionStrategy, Component, EnvironmentInjector, HostListener, afterNextRender, computed, effect, inject, input, output, signal, untracked } from "@angular/core";
|
|
3
3
|
import * as i1 from "@angular/cdk/a11y";
|
|
4
4
|
import { A11yModule } from "@angular/cdk/a11y";
|
|
5
5
|
import { IconComponent, ɵprovideKitIcons } from "@lifecorner/angular-ui-kit/icon";
|
|
6
6
|
import { ICON_X_MARK } from "@lifecorner/angular-ui-kit/icons";
|
|
7
|
-
import { OverlayStackService } from "@lifecorner/angular-ui-kit/internal/overlay";
|
|
7
|
+
import { OverlayStackService, focusReturnTarget } from "@lifecorner/angular-ui-kit/internal/overlay";
|
|
8
8
|
const CLOSE_ANIMATION_MS = 250;
|
|
9
9
|
var DrawerComponent = class DrawerComponent {
|
|
10
10
|
static nextId = 0;
|
|
@@ -17,6 +17,7 @@ var DrawerComponent = class DrawerComponent {
|
|
|
17
17
|
closeOnEscape = input(true, ...ngDevMode ? [{ debugName: "closeOnEscape" }] : /* istanbul ignore next */ []);
|
|
18
18
|
ariaLabel = input("", ...ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []);
|
|
19
19
|
closeAriaLabel = input("Close drawer", ...ngDevMode ? [{ debugName: "closeAriaLabel" }] : /* istanbul ignore next */ []);
|
|
20
|
+
returnFocusTo = input(null, ...ngDevMode ? [{ debugName: "returnFocusTo" }] : /* istanbul ignore next */ []);
|
|
20
21
|
closed = output();
|
|
21
22
|
_visible = signal(false, ...ngDevMode ? [{ debugName: "_visible" }] : /* istanbul ignore next */ []);
|
|
22
23
|
widthValue = computed(() => {
|
|
@@ -36,6 +37,8 @@ var DrawerComponent = class DrawerComponent {
|
|
|
36
37
|
drawerId = `lc-drawer-${++DrawerComponent.nextId}`;
|
|
37
38
|
closeTimer;
|
|
38
39
|
originalOverflow;
|
|
40
|
+
environmentInjector = inject(EnvironmentInjector);
|
|
41
|
+
destroyed = false;
|
|
39
42
|
constructor() {
|
|
40
43
|
effect(() => {
|
|
41
44
|
const isOpen = this.open();
|
|
@@ -51,15 +54,25 @@ var DrawerComponent = class DrawerComponent {
|
|
|
51
54
|
this.closeTimer = void 0;
|
|
52
55
|
this._visible.set(false);
|
|
53
56
|
this.unlockScroll();
|
|
57
|
+
if (untracked(this.returnFocusTo)) this.scheduleFocusReturn();
|
|
54
58
|
}, CLOSE_ANIMATION_MS);
|
|
55
59
|
}
|
|
56
60
|
});
|
|
57
61
|
});
|
|
58
62
|
}
|
|
59
63
|
ngOnDestroy() {
|
|
64
|
+
this.destroyed = true;
|
|
60
65
|
this.clearCloseTimer();
|
|
61
66
|
this.overlayStack.remove(this.drawerId);
|
|
62
67
|
this.unlockScroll();
|
|
68
|
+
if (this._visible() && this.returnFocusTo()) this.scheduleFocusReturn();
|
|
69
|
+
}
|
|
70
|
+
scheduleFocusReturn() {
|
|
71
|
+
afterNextRender(() => {
|
|
72
|
+
const target = this.returnFocusTo();
|
|
73
|
+
if (this.open() && !this.destroyed || !target?.isConnected) return;
|
|
74
|
+
focusReturnTarget(target);
|
|
75
|
+
}, { injector: this.environmentInjector });
|
|
63
76
|
}
|
|
64
77
|
onOverlayClick(event) {
|
|
65
78
|
if (!this.overlayStack.claim(this.drawerId, event)) return;
|
|
@@ -167,6 +180,13 @@ var DrawerComponent = class DrawerComponent {
|
|
|
167
180
|
isSignal: true,
|
|
168
181
|
isRequired: false,
|
|
169
182
|
transformFunction: null
|
|
183
|
+
},
|
|
184
|
+
returnFocusTo: {
|
|
185
|
+
classPropertyName: "returnFocusTo",
|
|
186
|
+
publicName: "returnFocusTo",
|
|
187
|
+
isSignal: true,
|
|
188
|
+
isRequired: false,
|
|
189
|
+
transformFunction: null
|
|
170
190
|
}
|
|
171
191
|
},
|
|
172
192
|
outputs: { closed: "closed" },
|
|
@@ -296,6 +316,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
296
316
|
required: false
|
|
297
317
|
}]
|
|
298
318
|
}],
|
|
319
|
+
returnFocusTo: [{
|
|
320
|
+
type: i0.Input,
|
|
321
|
+
args: [{
|
|
322
|
+
isSignal: true,
|
|
323
|
+
alias: "returnFocusTo",
|
|
324
|
+
required: false
|
|
325
|
+
}]
|
|
326
|
+
}],
|
|
299
327
|
closed: [{
|
|
300
328
|
type: i0.Output,
|
|
301
329
|
args: ["closed"]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-drawer.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/drawer/drawer.component.ts","../../../../libs/angular-ui-kit/src/lib/drawer/drawer.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n signal,\n computed,\n effect,\n untracked,\n inject,\n HostListener,\n OnDestroy,\n} from '@angular/core';\nimport { A11yModule } from '@angular/cdk/a11y';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_X_MARK } from '@lifecorner/angular-ui-kit/icons';\nimport { OverlayStackService } from '@lifecorner/angular-ui-kit/internal/overlay';\n\nexport type DrawerPosition = 'left' | 'right';\nexport type DrawerSize = 'sm' | 'md' | 'lg' | 'xl';\n\n/** Duration of the slide-out transition (see drawer.component.scss). */\nconst CLOSE_ANIMATION_MS = 250;\n\n/**\n * Drawer component for slide-out overlay panels.\n *\n * Features:\n * - Slide-in from left or right\n * - Size presets (sm, md, lg, xl)\n * - Optional backdrop overlay with click-to-close\n * - Close on Escape key support\n * - Heading text display\n * - Content projection for arbitrary body content\n * - Accessible with ARIA dialog role, focus trap and focus restore\n *\n * @example\n * ```html\n * <lc-drawer [open]=\"showPanel\" position=\"right\" size=\"md\"\n * heading=\"Settings\" (closed)=\"showPanel = false\">\n * <p>Any content goes here</p>\n * </lc-drawer>\n * ```\n */\n@Component({\n selector: 'lc-drawer',\n standalone: true,\n imports: [A11yModule, IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_X_MARK])],\n templateUrl: './drawer.component.html',\n styleUrl: './drawer.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DrawerComponent implements OnDestroy {\n private static nextId = 0;\n\n /** Whether the drawer is open. */\n readonly open = input<boolean>(false);\n\n /** Slide-in direction. */\n readonly position = input<DrawerPosition>('right');\n\n /** Predefined width (sm=320, md=400, lg=520, xl=640). */\n readonly size = input<DrawerSize>('md');\n\n /** Optional heading shown in the drawer header. */\n readonly heading = input<string>('');\n\n /** Whether the overlay backdrop is shown. */\n readonly hasOverlay = input<boolean>(true);\n\n /** Whether clicking the overlay closes the drawer. */\n readonly closeOnOverlayClick = input<boolean>(true);\n\n /** Whether pressing Escape closes the drawer. */\n readonly closeOnEscape = input<boolean>(true);\n\n /** ARIA label for assistive technology. */\n readonly ariaLabel = input<string>('');\n\n /** Accessible name of the close button. */\n readonly closeAriaLabel = input('Close drawer');\n\n /** Emitted when the drawer should close. */\n readonly closed = output<void>();\n\n /** Internal visibility — stays true during the slide-out animation. */\n protected _visible = signal(false);\n\n /** Width token → CSS. */\n protected readonly widthValue = computed(() => {\n const map: Record<DrawerSize, string> = {\n sm: '320px',\n md: '400px',\n lg: '520px',\n xl: '640px',\n };\n return map[this.size()];\n });\n\n protected readonly panelClasses = computed(() => {\n const cls = ['lc-drawer__panel', `lc-drawer__panel--${this.position()}`];\n if (this.open()) cls.push('lc-drawer__panel--open');\n return cls.join(' ');\n });\n\n private readonly overlayStack = inject(OverlayStackService);\n /** Identifies this instance in the overlay stack. */\n private readonly drawerId = `lc-drawer-${++DrawerComponent.nextId}`;\n private closeTimer?: ReturnType<typeof setTimeout>;\n private originalOverflow?: string;\n\n constructor() {\n effect(() => {\n const isOpen = this.open();\n untracked(() => {\n if (isOpen) {\n // Re-opening within the close animation must cancel the pending hide,\n // otherwise the stale timer hides the drawer and unlocks scroll while\n // `open()` is still true.\n this.clearCloseTimer();\n this._visible.set(true);\n this.lockScroll();\n this.overlayStack.push(this.drawerId);\n } else if (this._visible()) {\n this.overlayStack.remove(this.drawerId);\n // Keep the panel mounted until the slide-out transition has finished\n this.closeTimer = setTimeout(() => {\n this.closeTimer = undefined;\n this._visible.set(false);\n this.unlockScroll();\n }, CLOSE_ANIMATION_MS);\n }\n });\n });\n }\n\n ngOnDestroy(): void {\n // Navigating away while open must not leave the page scroll-locked\n this.clearCloseTimer();\n this.overlayStack.remove(this.drawerId);\n this.unlockScroll();\n }\n\n /**\n * Handle backdrop click. Ignored while another overlay (menu, popover, …)\n * sits above this drawer — the click belongs to that layer.\n */\n protected onOverlayClick(event: MouseEvent): void {\n if (!this.overlayStack.claim(this.drawerId, event)) return;\n if (this.closeOnOverlayClick()) {\n this.close();\n }\n }\n\n /** Handle close action. */\n protected close(): void {\n this.closed.emit();\n }\n\n /**\n * Escape closes the drawer only while it is the top-most overlay; the event\n * is consumed either way so overlays underneath stay open.\n */\n @HostListener('document:keydown', ['$event'])\n protected onKeydown(event: KeyboardEvent): void {\n if (event.key !== 'Escape' || !this.open()) return;\n if (!this.overlayStack.claim(this.drawerId, event)) return;\n event.stopPropagation();\n if (this.closeOnEscape()) {\n this.close();\n }\n }\n\n private lockScroll(): void {\n if (typeof document === 'undefined' || this.originalOverflow !== undefined) return;\n this.originalOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n\n private unlockScroll(): void {\n if (typeof document === 'undefined' || this.originalOverflow === undefined) return;\n document.body.style.overflow = this.originalOverflow;\n this.originalOverflow = undefined;\n }\n\n private clearCloseTimer(): void {\n if (this.closeTimer !== undefined) {\n clearTimeout(this.closeTimer);\n this.closeTimer = undefined;\n }\n }\n}\n","@if (_visible()) {\n <div class=\"lc-drawer\">\n <!-- Overlay -->\n @if (hasOverlay()) {\n <div\n class=\"lc-drawer__overlay\"\n [class.lc-drawer__overlay--visible]=\"open()\"\n (click)=\"onOverlayClick($event)\"\n aria-hidden=\"true\"\n ></div>\n }\n\n <!-- Panel. Focus moves in on open and back to the opener when the panel\n is torn down (cdkTrapFocusAutoCapture); focus is only trapped while\n the backdrop makes the page behind inert. -->\n <aside\n [class]=\"panelClasses()\"\n [style.width]=\"widthValue()\"\n role=\"dialog\"\n [attr.aria-label]=\"ariaLabel() || heading() || 'Drawer'\"\n [attr.aria-modal]=\"hasOverlay() ? 'true' : null\"\n [cdkTrapFocus]=\"hasOverlay()\"\n [cdkTrapFocusAutoCapture]=\"true\"\n >\n <!-- Header -->\n @if (heading()) {\n <header class=\"lc-drawer__header\">\n <h2 class=\"lc-drawer__heading\">{{ heading() }}</h2>\n <button\n type=\"button\"\n class=\"lc-drawer__close\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </header>\n } @else {\n <button\n type=\"button\"\n class=\"lc-drawer__close lc-drawer__close--floating\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n }\n\n <!-- Projected content -->\n <div class=\"lc-drawer__body\">\n <ng-content />\n </div>\n </aside>\n </div>\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-drawer.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/drawer/drawer.component.ts","../../../../libs/angular-ui-kit/src/lib/drawer/drawer.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n signal,\n computed,\n effect,\n untracked,\n inject,\n HostListener,\n OnDestroy,\n afterNextRender,\n EnvironmentInjector,\n} from '@angular/core';\nimport { A11yModule } from '@angular/cdk/a11y';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_X_MARK } from '@lifecorner/angular-ui-kit/icons';\nimport { OverlayStackService, focusReturnTarget } from '@lifecorner/angular-ui-kit/internal/overlay';\n\nexport type DrawerPosition = 'left' | 'right';\nexport type DrawerSize = 'sm' | 'md' | 'lg' | 'xl';\n\n/** Duration of the slide-out transition (see drawer.component.scss). */\nconst CLOSE_ANIMATION_MS = 250;\n\n/**\n * Drawer component for slide-out overlay panels.\n *\n * Features:\n * - Slide-in from left or right\n * - Size presets (sm, md, lg, xl)\n * - Optional backdrop overlay with click-to-close\n * - Close on Escape key support\n * - Heading text display\n * - Content projection for arbitrary body content\n * - Accessible with ARIA dialog role, focus trap and focus restore;\n * `returnFocusTo` (3.35) names the element that gets the focus back after\n * the drawer closed (WebKit doesn't focus a tapped button, so the one\n * captured at open is `body`)\n *\n * @example\n * ```html\n * <lc-drawer [open]=\"showPanel\" position=\"right\" size=\"md\"\n * heading=\"Settings\" (closed)=\"showPanel = false\">\n * <p>Any content goes here</p>\n * </lc-drawer>\n * ```\n */\n@Component({\n selector: 'lc-drawer',\n standalone: true,\n imports: [A11yModule, IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_X_MARK])],\n templateUrl: './drawer.component.html',\n styleUrl: './drawer.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DrawerComponent implements OnDestroy {\n private static nextId = 0;\n\n /** Whether the drawer is open. */\n readonly open = input<boolean>(false);\n\n /** Slide-in direction. */\n readonly position = input<DrawerPosition>('right');\n\n /** Predefined width (sm=320, md=400, lg=520, xl=640). */\n readonly size = input<DrawerSize>('md');\n\n /** Optional heading shown in the drawer header. */\n readonly heading = input<string>('');\n\n /** Whether the overlay backdrop is shown. */\n readonly hasOverlay = input<boolean>(true);\n\n /** Whether clicking the overlay closes the drawer. */\n readonly closeOnOverlayClick = input<boolean>(true);\n\n /** Whether pressing Escape closes the drawer. */\n readonly closeOnEscape = input<boolean>(true);\n\n /** ARIA label for assistive technology. */\n readonly ariaLabel = input<string>('');\n\n /** Accessible name of the close button. */\n readonly closeAriaLabel = input('Close drawer');\n\n /**\n * Element that gets the focus back after the drawer closed (3.35), instead\n * of the one that had it at open. Set it to the trigger: WebKit (Safari,\n * every iOS browser) doesn't focus a tapped button, so the captured element\n * is `body`. Applied when the panel has left the DOM after its 250 ms\n * slide-out, once the focus trap has restored its captured element. Holds\n * on ×, Escape, backdrop, `open` false, and when the drawer is destroyed\n * while open. A custom-element host that can't take focus (`<lc-button>`)\n * hands it to its first focusable descendant. Ignored when the element is\n * no longer in the document or can't take focus; the captured element then\n * keeps it. `null`: as before.\n * @default null\n */\n readonly returnFocusTo = input<HTMLElement | null>(null);\n\n /** Emitted when the drawer should close. */\n readonly closed = output<void>();\n\n /** Internal visibility — stays true during the slide-out animation. */\n protected _visible = signal(false);\n\n /** Width token → CSS. */\n protected readonly widthValue = computed(() => {\n const map: Record<DrawerSize, string> = {\n sm: '320px',\n md: '400px',\n lg: '520px',\n xl: '640px',\n };\n return map[this.size()];\n });\n\n protected readonly panelClasses = computed(() => {\n const cls = ['lc-drawer__panel', `lc-drawer__panel--${this.position()}`];\n if (this.open()) cls.push('lc-drawer__panel--open');\n return cls.join(' ');\n });\n\n private readonly overlayStack = inject(OverlayStackService);\n /** Identifies this instance in the overlay stack. */\n private readonly drawerId = `lc-drawer-${++DrawerComponent.nextId}`;\n private closeTimer?: ReturnType<typeof setTimeout>;\n private originalOverflow?: string;\n /**\n * The environment injector: a node injector would cancel the focus-return\n * hook when the drawer is destroyed while open.\n */\n private readonly environmentInjector = inject(EnvironmentInjector);\n private destroyed = false;\n\n constructor() {\n effect(() => {\n const isOpen = this.open();\n untracked(() => {\n if (isOpen) {\n // Re-opening within the close animation must cancel the pending hide,\n // otherwise the stale timer hides the drawer and unlocks scroll while\n // `open()` is still true.\n this.clearCloseTimer();\n this._visible.set(true);\n this.lockScroll();\n this.overlayStack.push(this.drawerId);\n } else if (this._visible()) {\n this.overlayStack.remove(this.drawerId);\n // Keep the panel mounted until the slide-out transition has finished\n this.closeTimer = setTimeout(() => {\n this.closeTimer = undefined;\n this._visible.set(false);\n this.unlockScroll();\n if (untracked(this.returnFocusTo)) this.scheduleFocusReturn();\n }, CLOSE_ANIMATION_MS);\n }\n });\n });\n }\n\n ngOnDestroy(): void {\n this.destroyed = true;\n // Navigating away while open must not leave the page scroll-locked\n this.clearCloseTimer();\n this.overlayStack.remove(this.drawerId);\n this.unlockScroll();\n if (this._visible() && this.returnFocusTo()) this.scheduleFocusReturn();\n }\n\n /**\n * Focuses `returnFocusTo` after the next render (3.35): the template\n * refresh has removed the panel, and `cdkTrapFocus` has restored its\n * captured element on destroy, so this is the last focus write. Reads the\n * input again then, so a trigger re-rendered meanwhile is taken from a\n * fresh query. Does nothing when the drawer opened again meanwhile.\n */\n private scheduleFocusReturn(): void {\n afterNextRender(\n () => {\n const target = this.returnFocusTo();\n if ((this.open() && !this.destroyed) || !target?.isConnected) return;\n focusReturnTarget(target);\n },\n { injector: this.environmentInjector },\n );\n }\n\n /**\n * Handle backdrop click. Ignored while another overlay (menu, popover, …)\n * sits above this drawer — the click belongs to that layer.\n */\n protected onOverlayClick(event: MouseEvent): void {\n if (!this.overlayStack.claim(this.drawerId, event)) return;\n if (this.closeOnOverlayClick()) {\n this.close();\n }\n }\n\n /** Handle close action. */\n protected close(): void {\n this.closed.emit();\n }\n\n /**\n * Escape closes the drawer only while it is the top-most overlay; the event\n * is consumed either way so overlays underneath stay open.\n */\n @HostListener('document:keydown', ['$event'])\n protected onKeydown(event: KeyboardEvent): void {\n if (event.key !== 'Escape' || !this.open()) return;\n if (!this.overlayStack.claim(this.drawerId, event)) return;\n event.stopPropagation();\n if (this.closeOnEscape()) {\n this.close();\n }\n }\n\n private lockScroll(): void {\n if (typeof document === 'undefined' || this.originalOverflow !== undefined) return;\n this.originalOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n\n private unlockScroll(): void {\n if (typeof document === 'undefined' || this.originalOverflow === undefined) return;\n document.body.style.overflow = this.originalOverflow;\n this.originalOverflow = undefined;\n }\n\n private clearCloseTimer(): void {\n if (this.closeTimer !== undefined) {\n clearTimeout(this.closeTimer);\n this.closeTimer = undefined;\n }\n }\n}\n","@if (_visible()) {\n <div class=\"lc-drawer\">\n <!-- Overlay -->\n @if (hasOverlay()) {\n <div\n class=\"lc-drawer__overlay\"\n [class.lc-drawer__overlay--visible]=\"open()\"\n (click)=\"onOverlayClick($event)\"\n aria-hidden=\"true\"\n ></div>\n }\n\n <!-- Panel. Focus moves in on open and back to the opener when the panel\n is torn down (cdkTrapFocusAutoCapture); focus is only trapped while\n the backdrop makes the page behind inert. -->\n <aside\n [class]=\"panelClasses()\"\n [style.width]=\"widthValue()\"\n role=\"dialog\"\n [attr.aria-label]=\"ariaLabel() || heading() || 'Drawer'\"\n [attr.aria-modal]=\"hasOverlay() ? 'true' : null\"\n [cdkTrapFocus]=\"hasOverlay()\"\n [cdkTrapFocusAutoCapture]=\"true\"\n >\n <!-- Header -->\n @if (heading()) {\n <header class=\"lc-drawer__header\">\n <h2 class=\"lc-drawer__heading\">{{ heading() }}</h2>\n <button\n type=\"button\"\n class=\"lc-drawer__close\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </header>\n } @else {\n <button\n type=\"button\"\n class=\"lc-drawer__close lc-drawer__close--floating\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n }\n\n <!-- Projected content -->\n <div class=\"lc-drawer__body\">\n <ng-content />\n </div>\n </aside>\n </div>\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-gallery.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/gallery/gallery.component.ts","../../../../libs/angular-ui-kit/src/lib/gallery/gallery.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n HostListener,\n Injector,\n OnDestroy,\n afterNextRender,\n computed,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport { CdkTrapFocus } from '@angular/cdk/a11y';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_CHEVRON_LEFT, ICON_CHEVRON_RIGHT, ICON_PLUS, ICON_X_MARK } from '@lifecorner/angular-ui-kit/icons';\nimport { NgStyle } from '@angular/common';\nimport { isSafeImageUrl } from '@lifecorner/angular-ui-kit/internal/url';\nimport { nameWithText } from '@lifecorner/angular-ui-kit/internal/a11y';\n\nexport type GalleryLayout = 'grid' | 'masonry';\nexport type GallerySize = 'sm' | 'md' | 'lg';\n\nexport interface GalleryItem {\n /** Image source URL */\n src: string;\n /** Thumbnail URL (falls back to src if not provided) */\n thumbnail?: string;\n /** Alt text for accessibility */\n alt?: string;\n /** Optional caption displayed below the image */\n caption?: string;\n /** Optional category for filtering */\n category?: string;\n}\n\nconst SIZE_COLUMNS: Record<GallerySize, number> = {\n sm: 4,\n md: 3,\n lg: 2,\n};\n\n/**\n * Gallery component for displaying image collections with lightbox.\n *\n * Features:\n * - Grid and masonry layout modes\n * - Responsive column count via size presets (sm, md, lg thumbnails)\n * - Configurable custom column count\n * - Lightbox overlay with navigation (prev/next)\n * - Keyboard navigation (Arrow keys, Escape); focus is trapped inside the\n * lightbox and returned to the originating thumbnail on close\n * - Optional captions and category filtering\n * - Lazy loading with placeholder shimmer\n * - Zoom control in lightbox\n * - Download button in lightbox\n * - Dark/light theme support\n *\n * @example\n * ```html\n * <lc-gallery\n * [items]=\"images\"\n * layout=\"grid\"\n * size=\"md\"\n * [showCaptions]=\"true\" />\n * ```\n */\n@Component({\n selector: 'lc-gallery',\n standalone: true,\n imports: [IconComponent, NgStyle, CdkTrapFocus],\n viewProviders: [ɵprovideKitIcons([ICON_PLUS, ICON_X_MARK, ICON_CHEVRON_LEFT, ICON_CHEVRON_RIGHT])],\n templateUrl: './gallery.component.html',\n styleUrls: ['./gallery.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class]': '\"gallery\"',\n },\n})\nexport class GalleryComponent implements OnDestroy {\n /** Array of gallery items to display */\n readonly items = input.required<GalleryItem[]>();\n\n /** Layout mode */\n readonly layout = input<GalleryLayout>('grid');\n\n /** Thumbnail size preset — controls column count (sm=4col, md=3col, lg=2col) */\n readonly size = input<GallerySize>('md');\n\n /** Custom column count (overrides size preset) */\n readonly columns = input<number | null>(null);\n\n /**\n * Whether to show captions below thumbnails. A shown caption is part of the\n * item's accessible name: `alt` (or `imageFallbackLabel` and its number),\n * then the caption (WCAG 2.5.3 Label in Name).\n * @default false\n */\n readonly showCaptions = input(false);\n\n /** Whether clicking a thumbnail opens the lightbox */\n readonly enableLightbox = input(true);\n\n /** Gap between items in pixels */\n readonly gap = input(8);\n\n /** Aspect ratio for grid items (ignored in masonry mode) */\n readonly aspectRatio = input('1 / 1');\n\n /** Label of the \"all\" category filter (shown when the items span more than one category). */\n readonly filterAllLabel = input('All');\n\n /** Accessible name of the lightbox dialog. */\n readonly lightboxAriaLabel = input('Image lightbox');\n\n /** Accessible name of the lightbox zoom-out button. */\n readonly zoomOutAriaLabel = input('Zoom out');\n\n /** Accessible name of the lightbox zoom-in button. */\n readonly zoomInAriaLabel = input('Zoom in');\n\n /** Accessible name of the lightbox reset-zoom button. */\n readonly resetZoomAriaLabel = input('Reset zoom');\n\n /** Accessible name of the lightbox download button. */\n readonly downloadAriaLabel = input('Download');\n\n /** Accessible name of the lightbox close button. */\n readonly closeAriaLabel = input('Close');\n\n /** Accessible name of the previous-image button. */\n readonly previousAriaLabel = input('Previous image');\n\n /** Accessible name of the next-image button. */\n readonly nextAriaLabel = input('Next image');\n\n /**\n * Accessible name of a thumbnail without `alt`, followed by its number. A\n * shown caption follows (`Bild 3, Sonnenuntergang am See`).\n * @default 'Image'\n */\n readonly imageFallbackLabel = input('Image');\n\n /** Emitted when lightbox opens with item index */\n readonly lightboxOpen = output<number>();\n\n /** Emitted when lightbox closes */\n readonly lightboxClose = output<void>();\n\n // ── Internal state ───────────────────────────────────────────────────\n\n private readonly injector = inject(Injector);\n private readonly closeBtn = viewChild<ElementRef<HTMLButtonElement>>('closeBtn');\n /** Element that opened the lightbox — focus returns there on close. */\n private triggerElement: HTMLElement | null = null;\n\n protected lightboxIndex = signal<number | null>(null);\n protected lightboxZoom = signal(100);\n protected loadedImages = signal<Set<number>>(new Set());\n protected activeFilter = signal<string | null>(null);\n\n // ── Computed ─────────────────────────────────────────────────────────\n\n protected gridColumns = computed(() => {\n return this.columns() ?? SIZE_COLUMNS[this.size()];\n });\n\n protected gridStyle = computed(() => {\n const cols = this.gridColumns();\n const g = this.gap();\n if (this.layout() === 'masonry') {\n return {\n 'column-count': `${cols}`,\n 'column-gap': `${g}px`,\n };\n }\n return {\n 'display': 'grid',\n 'grid-template-columns': `repeat(${cols}, 1fr)`,\n 'gap': `${g}px`,\n };\n });\n\n protected itemStyle = computed(() => {\n if (this.layout() === 'masonry') return {};\n return { 'aspect-ratio': this.aspectRatio() };\n });\n\n protected categories = computed<string[]>(() => {\n const cats = new Set<string>();\n for (const item of this.items()) {\n if (item.category) cats.add(item.category);\n }\n return Array.from(cats).sort();\n });\n\n protected filteredItems = computed(() => {\n const filter = this.activeFilter();\n if (!filter) return this.items();\n return this.items().filter(item => item.category === filter);\n });\n\n protected showFilters = computed(() => this.categories().length > 1);\n\n protected lightboxItem = computed<GalleryItem | null>(() => {\n const idx = this.lightboxIndex();\n if (idx === null) return null;\n const items = this.filteredItems();\n return items[idx] ?? null;\n });\n\n protected lightboxCounter = computed(() => {\n const idx = this.lightboxIndex();\n if (idx === null) return '';\n return `${idx + 1} / ${this.filteredItems().length}`;\n });\n\n protected lightboxTransform = computed(() => {\n const z = this.lightboxZoom() / 100;\n return `scale(${z})`;\n });\n\n protected canGoPrev = computed(() => {\n const idx = this.lightboxIndex();\n return idx !== null && idx > 0;\n });\n\n protected canGoNext = computed(() => {\n const idx = this.lightboxIndex();\n return idx !== null && idx < this.filteredItems().length - 1;\n });\n\n // ── Lifecycle ────────────────────────────────────────────────────────\n\n ngOnDestroy(): void {\n // No focus restore here: the injector is going away with the view.\n this.closeLightbox(false);\n }\n\n // ── Actions ──────────────────────────────────────────────────────────\n\n /**\n * Opens the lightbox for `index`. Like a native button, Space activates on\n * *keyup* (keydown only prevents the page scroll): activating on keydown\n * would move focus to the Close button before the key is released, and the\n * Space keyup would then immediately click Close.\n */\n protected openLightbox(index: number, event?: Event): void {\n if (!this.enableLightbox()) return;\n if (event instanceof KeyboardEvent) event.preventDefault();\n this.triggerElement =\n (event?.currentTarget as HTMLElement | null) ??\n (document.activeElement instanceof HTMLElement ? document.activeElement : null);\n this.lightboxIndex.set(index);\n this.lightboxZoom.set(100);\n this.lightboxOpen.emit(index);\n document.body.style.overflow = 'hidden';\n // The dialog is `@if`-rendered — move focus in once it exists.\n afterNextRender(() => this.closeBtn()?.nativeElement.focus(), { injector: this.injector });\n }\n\n protected closeLightbox(restoreFocus = true): void {\n if (this.lightboxIndex() === null) return;\n this.lightboxIndex.set(null);\n this.lightboxZoom.set(100);\n this.lightboxClose.emit();\n document.body.style.overflow = '';\n const trigger = this.triggerElement;\n this.triggerElement = null;\n if (restoreFocus && trigger?.isConnected) {\n afterNextRender(() => trigger.focus(), { injector: this.injector });\n }\n }\n\n protected goToPrev(): void {\n const idx = this.lightboxIndex();\n if (idx !== null && idx > 0) {\n this.lightboxIndex.set(idx - 1);\n this.lightboxZoom.set(100);\n }\n }\n\n protected goToNext(): void {\n const idx = this.lightboxIndex();\n if (idx !== null && idx < this.filteredItems().length - 1) {\n this.lightboxIndex.set(idx + 1);\n this.lightboxZoom.set(100);\n }\n }\n\n protected zoomIn(): void {\n this.lightboxZoom.update(z => Math.min(z + 25, 500));\n }\n\n protected zoomOut(): void {\n this.lightboxZoom.update(z => Math.max(z - 25, 25));\n }\n\n protected resetZoom(): void {\n this.lightboxZoom.set(100);\n }\n\n protected downloadCurrent(): void {\n const item = this.lightboxItem();\n // A programmatic click on a caller-supplied URL: only image-safe schemes.\n if (!item || !isSafeImageUrl(item.src)) return;\n const a = document.createElement('a');\n a.href = item.src;\n a.download = item.alt || 'image';\n a.target = '_blank';\n a.rel = 'noopener noreferrer';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n }\n\n protected setFilter(category: string | null): void {\n this.activeFilter.set(category);\n }\n\n protected onImageLoad(index: number): void {\n this.loadedImages.update(set => {\n const next = new Set(set);\n next.add(index);\n return next;\n });\n }\n\n protected isLoaded(index: number): boolean {\n return this.loadedImages().has(index);\n }\n\n protected getThumbnail(item: GalleryItem): string {\n return item.thumbnail || item.src;\n }\n\n /**\n * Name of a thumbnail (AS-33, WCAG 2.5.3 Label in Name): `alt` (or\n * `imageFallbackLabel` and its number), then the shown caption unless one\n * contains the other.\n */\n protected itemAriaLabel(item: GalleryItem, index: number): string {\n const label = item.alt || `${this.imageFallbackLabel()} ${index + 1}`;\n return nameWithText(label, this.showCaptions() ? (item.caption ?? '') : '');\n }\n\n protected trackByIndex(index: number): number {\n return index;\n }\n\n /**\n * Keyboard handling while the lightbox is open. Bound on the lightbox itself\n * (focus is trapped inside it) and, as a fallback, on `document`. A handled\n * Escape stops propagating so an enclosing modal/drawer doesn't close too.\n */\n @HostListener('document:keydown', ['$event'])\n protected onKeydown(event: KeyboardEvent): void {\n if (this.lightboxIndex() === null) return;\n switch (event.key) {\n case 'Escape':\n event.stopPropagation();\n this.closeLightbox();\n break;\n case 'ArrowLeft':\n event.stopPropagation();\n this.goToPrev();\n break;\n case 'ArrowRight':\n event.stopPropagation();\n this.goToNext();\n break;\n }\n }\n}\n","<!-- Filters -->\n@if (showFilters()) {\n <div class=\"gallery__filters\">\n <button\n type=\"button\"\n class=\"gallery__filter-btn\"\n [class.gallery__filter-btn--active]=\"activeFilter() === null\"\n (click)=\"setFilter(null)\"\n >\n {{ filterAllLabel() }}\n </button>\n @for (cat of categories(); track cat) {\n <button\n type=\"button\"\n class=\"gallery__filter-btn\"\n [class.gallery__filter-btn--active]=\"activeFilter() === cat\"\n (click)=\"setFilter(cat)\"\n >\n {{ cat }}\n </button>\n }\n </div>\n}\n\n<!-- Grid / Masonry -->\n<div class=\"gallery__grid\" [class.gallery__grid--masonry]=\"layout() === 'masonry'\" [ngStyle]=\"gridStyle()\">\n @for (item of filteredItems(); track item.src; let i = $index) {\n <div\n class=\"gallery__item\"\n [class.gallery__item--loaded]=\"isLoaded(i)\"\n [class.gallery__item--clickable]=\"enableLightbox()\"\n [ngStyle]=\"itemStyle()\"\n (click)=\"openLightbox(i, $event)\"\n (keydown.enter)=\"openLightbox(i, $event)\"\n (keydown.space)=\"$event.preventDefault()\"\n (keyup.space)=\"openLightbox(i, $event)\"\n [attr.tabindex]=\"enableLightbox() ? 0 : null\"\n [attr.role]=\"enableLightbox() ? 'button' : null\"\n [attr.aria-label]=\"itemAriaLabel(item, i)\"\n >\n @if (!isLoaded(i)) {\n <div class=\"gallery__placeholder\"></div>\n }\n <img\n class=\"gallery__thumb\"\n [src]=\"getThumbnail(item)\"\n [alt]=\"item.alt || ''\"\n loading=\"lazy\"\n (load)=\"onImageLoad(i)\"\n />\n @if (showCaptions() && item.caption) {\n <div class=\"gallery__caption\">{{ item.caption }}</div>\n }\n </div>\n }\n</div>\n\n<!-- Lightbox overlay -->\n@if (lightboxItem(); as item) {\n <div\n class=\"gallery__lightbox\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"lightboxAriaLabel()\"\n tabindex=\"-1\"\n cdkTrapFocus\n (keydown)=\"onKeydown($event)\"\n >\n <!-- Backdrop -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- backdrop click is a mouse shortcut; Escape and the close button close the lightbox -->\n <div class=\"gallery__lightbox-backdrop\" (click)=\"closeLightbox()\"></div>\n\n <!-- Toolbar -->\n <div class=\"gallery__lightbox-toolbar\">\n <div class=\"gallery__lightbox-counter\">{{ lightboxCounter() }}</div>\n <div class=\"gallery__lightbox-actions\">\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"zoomOutAriaLabel()\" (click)=\"zoomOut()\">\n <lc-icon name=\"minus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <span class=\"gallery__lightbox-zoom\">{{ lightboxZoom() }}%</span>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"zoomInAriaLabel()\" (click)=\"zoomIn()\">\n <lc-icon name=\"plus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"resetZoomAriaLabel()\" (click)=\"resetZoom()\">\n <lc-icon name=\"arrows-pointing-out\" size=\"xs\" [decorative]=\"true\" />\n </button>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"downloadAriaLabel()\" (click)=\"downloadCurrent()\">\n <lc-icon name=\"arrow-down-tray\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <button #closeBtn type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"closeAriaLabel()\" (click)=\"closeLightbox()\">\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </div>\n </div>\n\n <!-- Navigation -->\n @if (canGoPrev()) {\n <button\n type=\"button\"\n class=\"gallery__lightbox-nav gallery__lightbox-nav--prev\"\n [attr.aria-label]=\"previousAriaLabel()\"\n (click)=\"goToPrev()\"\n >\n <lc-icon name=\"chevron-left\" size=\"lg\" [decorative]=\"true\" />\n </button>\n }\n @if (canGoNext()) {\n <button\n type=\"button\"\n class=\"gallery__lightbox-nav gallery__lightbox-nav--next\"\n [attr.aria-label]=\"nextAriaLabel()\"\n (click)=\"goToNext()\"\n >\n <lc-icon name=\"chevron-right\" size=\"lg\" [decorative]=\"true\" />\n </button>\n }\n\n <!-- Image -->\n <div class=\"gallery__lightbox-content\">\n <img\n class=\"gallery__lightbox-image\"\n [src]=\"item.src\"\n [alt]=\"item.alt || ''\"\n [style.transform]=\"lightboxTransform()\"\n />\n </div>\n\n <!-- Caption -->\n @if (item.caption) {\n <div class=\"gallery__lightbox-caption\">{{ item.caption }}</div>\n }\n </div>\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-gallery.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/gallery/gallery.component.ts","../../../../libs/angular-ui-kit/src/lib/gallery/gallery.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n HostListener,\n Injector,\n OnDestroy,\n afterNextRender,\n computed,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport { CdkTrapFocus } from '@angular/cdk/a11y';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_CHEVRON_LEFT, ICON_CHEVRON_RIGHT, ICON_PLUS, ICON_X_MARK } from '@lifecorner/angular-ui-kit/icons';\nimport { NgStyle } from '@angular/common';\nimport { isSafeImageUrl } from '@lifecorner/angular-ui-kit/internal/url';\nimport { nameWithText } from '@lifecorner/angular-ui-kit/internal/a11y';\n\nexport type GalleryLayout = 'grid' | 'masonry';\nexport type GallerySize = 'sm' | 'md' | 'lg';\n\nexport interface GalleryItem {\n /** Image source URL */\n src: string;\n /** Thumbnail URL (falls back to src if not provided) */\n thumbnail?: string;\n /** Alt text for accessibility */\n alt?: string;\n /** Optional caption displayed below the image */\n caption?: string;\n /** Optional category for filtering */\n category?: string;\n}\n\nconst SIZE_COLUMNS: Record<GallerySize, number> = {\n sm: 4,\n md: 3,\n lg: 2,\n};\n\n/**\n * Gallery component for displaying image collections with lightbox.\n *\n * Features:\n * - Grid and masonry layout modes\n * - Responsive column count via size presets (sm, md, lg thumbnails)\n * - Configurable custom column count\n * - Lightbox overlay with navigation (prev/next)\n * - Keyboard navigation (Arrow keys, Escape); focus is trapped inside the\n * lightbox and returned to the originating thumbnail on close: the clicked\n * or tapped thumbnail, also in WebKit, where a tap doesn't focus it, so the\n * gallery needs no `returnFocusTo` (3.35)\n * - Optional captions and category filtering\n * - Lazy loading with placeholder shimmer\n * - Zoom control in lightbox\n * - Download button in lightbox\n * - Dark/light theme support\n *\n * @example\n * ```html\n * <lc-gallery\n * [items]=\"images\"\n * layout=\"grid\"\n * size=\"md\"\n * [showCaptions]=\"true\" />\n * ```\n */\n@Component({\n selector: 'lc-gallery',\n standalone: true,\n imports: [IconComponent, NgStyle, CdkTrapFocus],\n viewProviders: [ɵprovideKitIcons([ICON_PLUS, ICON_X_MARK, ICON_CHEVRON_LEFT, ICON_CHEVRON_RIGHT])],\n templateUrl: './gallery.component.html',\n styleUrls: ['./gallery.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class]': '\"gallery\"',\n },\n})\nexport class GalleryComponent implements OnDestroy {\n /** Array of gallery items to display */\n readonly items = input.required<GalleryItem[]>();\n\n /** Layout mode */\n readonly layout = input<GalleryLayout>('grid');\n\n /** Thumbnail size preset — controls column count (sm=4col, md=3col, lg=2col) */\n readonly size = input<GallerySize>('md');\n\n /** Custom column count (overrides size preset) */\n readonly columns = input<number | null>(null);\n\n /**\n * Whether to show captions below thumbnails. A shown caption is part of the\n * item's accessible name: `alt` (or `imageFallbackLabel` and its number),\n * then the caption (WCAG 2.5.3 Label in Name).\n * @default false\n */\n readonly showCaptions = input(false);\n\n /** Whether clicking a thumbnail opens the lightbox */\n readonly enableLightbox = input(true);\n\n /** Gap between items in pixels */\n readonly gap = input(8);\n\n /** Aspect ratio for grid items (ignored in masonry mode) */\n readonly aspectRatio = input('1 / 1');\n\n /** Label of the \"all\" category filter (shown when the items span more than one category). */\n readonly filterAllLabel = input('All');\n\n /** Accessible name of the lightbox dialog. */\n readonly lightboxAriaLabel = input('Image lightbox');\n\n /** Accessible name of the lightbox zoom-out button. */\n readonly zoomOutAriaLabel = input('Zoom out');\n\n /** Accessible name of the lightbox zoom-in button. */\n readonly zoomInAriaLabel = input('Zoom in');\n\n /** Accessible name of the lightbox reset-zoom button. */\n readonly resetZoomAriaLabel = input('Reset zoom');\n\n /** Accessible name of the lightbox download button. */\n readonly downloadAriaLabel = input('Download');\n\n /** Accessible name of the lightbox close button. */\n readonly closeAriaLabel = input('Close');\n\n /** Accessible name of the previous-image button. */\n readonly previousAriaLabel = input('Previous image');\n\n /** Accessible name of the next-image button. */\n readonly nextAriaLabel = input('Next image');\n\n /**\n * Accessible name of a thumbnail without `alt`, followed by its number. A\n * shown caption follows (`Bild 3, Sonnenuntergang am See`).\n * @default 'Image'\n */\n readonly imageFallbackLabel = input('Image');\n\n /** Emitted when lightbox opens with item index */\n readonly lightboxOpen = output<number>();\n\n /** Emitted when lightbox closes */\n readonly lightboxClose = output<void>();\n\n // ── Internal state ───────────────────────────────────────────────────\n\n private readonly injector = inject(Injector);\n private readonly closeBtn = viewChild<ElementRef<HTMLButtonElement>>('closeBtn');\n /** Element that opened the lightbox — focus returns there on close. */\n private triggerElement: HTMLElement | null = null;\n\n protected lightboxIndex = signal<number | null>(null);\n protected lightboxZoom = signal(100);\n protected loadedImages = signal<Set<number>>(new Set());\n protected activeFilter = signal<string | null>(null);\n\n // ── Computed ─────────────────────────────────────────────────────────\n\n protected gridColumns = computed(() => {\n return this.columns() ?? SIZE_COLUMNS[this.size()];\n });\n\n protected gridStyle = computed(() => {\n const cols = this.gridColumns();\n const g = this.gap();\n if (this.layout() === 'masonry') {\n return {\n 'column-count': `${cols}`,\n 'column-gap': `${g}px`,\n };\n }\n return {\n 'display': 'grid',\n 'grid-template-columns': `repeat(${cols}, 1fr)`,\n 'gap': `${g}px`,\n };\n });\n\n protected itemStyle = computed(() => {\n if (this.layout() === 'masonry') return {};\n return { 'aspect-ratio': this.aspectRatio() };\n });\n\n protected categories = computed<string[]>(() => {\n const cats = new Set<string>();\n for (const item of this.items()) {\n if (item.category) cats.add(item.category);\n }\n return Array.from(cats).sort();\n });\n\n protected filteredItems = computed(() => {\n const filter = this.activeFilter();\n if (!filter) return this.items();\n return this.items().filter(item => item.category === filter);\n });\n\n protected showFilters = computed(() => this.categories().length > 1);\n\n protected lightboxItem = computed<GalleryItem | null>(() => {\n const idx = this.lightboxIndex();\n if (idx === null) return null;\n const items = this.filteredItems();\n return items[idx] ?? null;\n });\n\n protected lightboxCounter = computed(() => {\n const idx = this.lightboxIndex();\n if (idx === null) return '';\n return `${idx + 1} / ${this.filteredItems().length}`;\n });\n\n protected lightboxTransform = computed(() => {\n const z = this.lightboxZoom() / 100;\n return `scale(${z})`;\n });\n\n protected canGoPrev = computed(() => {\n const idx = this.lightboxIndex();\n return idx !== null && idx > 0;\n });\n\n protected canGoNext = computed(() => {\n const idx = this.lightboxIndex();\n return idx !== null && idx < this.filteredItems().length - 1;\n });\n\n // ── Lifecycle ────────────────────────────────────────────────────────\n\n ngOnDestroy(): void {\n // No focus restore here: the injector is going away with the view.\n this.closeLightbox(false);\n }\n\n // ── Actions ──────────────────────────────────────────────────────────\n\n /**\n * Opens the lightbox for `index`. Like a native button, Space activates on\n * *keyup* (keydown only prevents the page scroll): activating on keydown\n * would move focus to the Close button before the key is released, and the\n * Space keyup would then immediately click Close.\n */\n protected openLightbox(index: number, event?: Event): void {\n if (!this.enableLightbox()) return;\n if (event instanceof KeyboardEvent) event.preventDefault();\n this.triggerElement =\n (event?.currentTarget as HTMLElement | null) ??\n (document.activeElement instanceof HTMLElement ? document.activeElement : null);\n this.lightboxIndex.set(index);\n this.lightboxZoom.set(100);\n this.lightboxOpen.emit(index);\n document.body.style.overflow = 'hidden';\n // The dialog is `@if`-rendered — move focus in once it exists.\n afterNextRender(() => this.closeBtn()?.nativeElement.focus(), { injector: this.injector });\n }\n\n protected closeLightbox(restoreFocus = true): void {\n if (this.lightboxIndex() === null) return;\n this.lightboxIndex.set(null);\n this.lightboxZoom.set(100);\n this.lightboxClose.emit();\n document.body.style.overflow = '';\n const trigger = this.triggerElement;\n this.triggerElement = null;\n if (restoreFocus && trigger?.isConnected) {\n afterNextRender(() => trigger.focus(), { injector: this.injector });\n }\n }\n\n protected goToPrev(): void {\n const idx = this.lightboxIndex();\n if (idx !== null && idx > 0) {\n this.lightboxIndex.set(idx - 1);\n this.lightboxZoom.set(100);\n }\n }\n\n protected goToNext(): void {\n const idx = this.lightboxIndex();\n if (idx !== null && idx < this.filteredItems().length - 1) {\n this.lightboxIndex.set(idx + 1);\n this.lightboxZoom.set(100);\n }\n }\n\n protected zoomIn(): void {\n this.lightboxZoom.update(z => Math.min(z + 25, 500));\n }\n\n protected zoomOut(): void {\n this.lightboxZoom.update(z => Math.max(z - 25, 25));\n }\n\n protected resetZoom(): void {\n this.lightboxZoom.set(100);\n }\n\n protected downloadCurrent(): void {\n const item = this.lightboxItem();\n // A programmatic click on a caller-supplied URL: only image-safe schemes.\n if (!item || !isSafeImageUrl(item.src)) return;\n const a = document.createElement('a');\n a.href = item.src;\n a.download = item.alt || 'image';\n a.target = '_blank';\n a.rel = 'noopener noreferrer';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n }\n\n protected setFilter(category: string | null): void {\n this.activeFilter.set(category);\n }\n\n protected onImageLoad(index: number): void {\n this.loadedImages.update(set => {\n const next = new Set(set);\n next.add(index);\n return next;\n });\n }\n\n protected isLoaded(index: number): boolean {\n return this.loadedImages().has(index);\n }\n\n protected getThumbnail(item: GalleryItem): string {\n return item.thumbnail || item.src;\n }\n\n /**\n * Name of a thumbnail (AS-33, WCAG 2.5.3 Label in Name): `alt` (or\n * `imageFallbackLabel` and its number), then the shown caption unless one\n * contains the other.\n */\n protected itemAriaLabel(item: GalleryItem, index: number): string {\n const label = item.alt || `${this.imageFallbackLabel()} ${index + 1}`;\n return nameWithText(label, this.showCaptions() ? (item.caption ?? '') : '');\n }\n\n protected trackByIndex(index: number): number {\n return index;\n }\n\n /**\n * Keyboard handling while the lightbox is open. Bound on the lightbox itself\n * (focus is trapped inside it) and, as a fallback, on `document`. A handled\n * Escape stops propagating so an enclosing modal/drawer doesn't close too.\n */\n @HostListener('document:keydown', ['$event'])\n protected onKeydown(event: KeyboardEvent): void {\n if (this.lightboxIndex() === null) return;\n switch (event.key) {\n case 'Escape':\n event.stopPropagation();\n this.closeLightbox();\n break;\n case 'ArrowLeft':\n event.stopPropagation();\n this.goToPrev();\n break;\n case 'ArrowRight':\n event.stopPropagation();\n this.goToNext();\n break;\n }\n }\n}\n","<!-- Filters -->\n@if (showFilters()) {\n <div class=\"gallery__filters\">\n <button\n type=\"button\"\n class=\"gallery__filter-btn\"\n [class.gallery__filter-btn--active]=\"activeFilter() === null\"\n (click)=\"setFilter(null)\"\n >\n {{ filterAllLabel() }}\n </button>\n @for (cat of categories(); track cat) {\n <button\n type=\"button\"\n class=\"gallery__filter-btn\"\n [class.gallery__filter-btn--active]=\"activeFilter() === cat\"\n (click)=\"setFilter(cat)\"\n >\n {{ cat }}\n </button>\n }\n </div>\n}\n\n<!-- Grid / Masonry -->\n<div class=\"gallery__grid\" [class.gallery__grid--masonry]=\"layout() === 'masonry'\" [ngStyle]=\"gridStyle()\">\n @for (item of filteredItems(); track item.src; let i = $index) {\n <div\n class=\"gallery__item\"\n [class.gallery__item--loaded]=\"isLoaded(i)\"\n [class.gallery__item--clickable]=\"enableLightbox()\"\n [ngStyle]=\"itemStyle()\"\n (click)=\"openLightbox(i, $event)\"\n (keydown.enter)=\"openLightbox(i, $event)\"\n (keydown.space)=\"$event.preventDefault()\"\n (keyup.space)=\"openLightbox(i, $event)\"\n [attr.tabindex]=\"enableLightbox() ? 0 : null\"\n [attr.role]=\"enableLightbox() ? 'button' : null\"\n [attr.aria-label]=\"itemAriaLabel(item, i)\"\n >\n @if (!isLoaded(i)) {\n <div class=\"gallery__placeholder\"></div>\n }\n <img\n class=\"gallery__thumb\"\n [src]=\"getThumbnail(item)\"\n [alt]=\"item.alt || ''\"\n loading=\"lazy\"\n (load)=\"onImageLoad(i)\"\n />\n @if (showCaptions() && item.caption) {\n <div class=\"gallery__caption\">{{ item.caption }}</div>\n }\n </div>\n }\n</div>\n\n<!-- Lightbox overlay -->\n@if (lightboxItem(); as item) {\n <div\n class=\"gallery__lightbox\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"lightboxAriaLabel()\"\n tabindex=\"-1\"\n cdkTrapFocus\n (keydown)=\"onKeydown($event)\"\n >\n <!-- Backdrop -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- backdrop click is a mouse shortcut; Escape and the close button close the lightbox -->\n <div class=\"gallery__lightbox-backdrop\" (click)=\"closeLightbox()\"></div>\n\n <!-- Toolbar -->\n <div class=\"gallery__lightbox-toolbar\">\n <div class=\"gallery__lightbox-counter\">{{ lightboxCounter() }}</div>\n <div class=\"gallery__lightbox-actions\">\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"zoomOutAriaLabel()\" (click)=\"zoomOut()\">\n <lc-icon name=\"minus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <span class=\"gallery__lightbox-zoom\">{{ lightboxZoom() }}%</span>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"zoomInAriaLabel()\" (click)=\"zoomIn()\">\n <lc-icon name=\"plus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"resetZoomAriaLabel()\" (click)=\"resetZoom()\">\n <lc-icon name=\"arrows-pointing-out\" size=\"xs\" [decorative]=\"true\" />\n </button>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"downloadAriaLabel()\" (click)=\"downloadCurrent()\">\n <lc-icon name=\"arrow-down-tray\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <button #closeBtn type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"closeAriaLabel()\" (click)=\"closeLightbox()\">\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </div>\n </div>\n\n <!-- Navigation -->\n @if (canGoPrev()) {\n <button\n type=\"button\"\n class=\"gallery__lightbox-nav gallery__lightbox-nav--prev\"\n [attr.aria-label]=\"previousAriaLabel()\"\n (click)=\"goToPrev()\"\n >\n <lc-icon name=\"chevron-left\" size=\"lg\" [decorative]=\"true\" />\n </button>\n }\n @if (canGoNext()) {\n <button\n type=\"button\"\n class=\"gallery__lightbox-nav gallery__lightbox-nav--next\"\n [attr.aria-label]=\"nextAriaLabel()\"\n (click)=\"goToNext()\"\n >\n <lc-icon name=\"chevron-right\" size=\"lg\" [decorative]=\"true\" />\n </button>\n }\n\n <!-- Image -->\n <div class=\"gallery__lightbox-content\">\n <img\n class=\"gallery__lightbox-image\"\n [src]=\"item.src\"\n [alt]=\"item.alt || ''\"\n [style.transform]=\"lightboxTransform()\"\n />\n </div>\n\n <!-- Caption -->\n @if (item.caption) {\n <div class=\"gallery__lightbox-caption\">{{ item.caption }}</div>\n }\n </div>\n}\n"],"mappings":"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"}
|
|
@@ -74,6 +74,11 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
74
74
|
args: [{ providedIn: "root" }]
|
|
75
75
|
}]
|
|
76
76
|
});
|
|
77
|
-
|
|
77
|
+
const FOCUSABLE = "button:not([disabled]), [href], input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
78
|
+
function focusReturnTarget(target) {
|
|
79
|
+
target.focus();
|
|
80
|
+
if (target.ownerDocument.activeElement !== target && target.localName.includes("-")) target.querySelector(FOCUSABLE)?.focus();
|
|
81
|
+
}
|
|
82
|
+
export { OverlayInsetsService, OverlayStackService, focusReturnTarget };
|
|
78
83
|
|
|
79
84
|
//# sourceMappingURL=lifecorner-angular-ui-kit-internal-overlay.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-internal-overlay.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/internal/overlay/overlay-stack.service.ts","../../../../libs/angular-ui-kit/src/lib/internal/overlay/overlay-insets.service.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\n\n/**\n * Keeps the open overlays (modal, drawer, popover, menu, tooltip, …) in\n * opening order so that a dismiss gesture — Escape, click outside — reaches\n * only the top-most one instead of closing every open layer at once.\n *\n * Every overlay pushes its id when it opens and removes it when it closes or\n * is destroyed. Internal to the library; not part of the public API.\n */\n@Injectable({ providedIn: 'root' })\nexport class OverlayStackService {\n private readonly stack: string[] = [];\n\n /**\n * Events already consumed by an overlay. All overlays listen on `document`,\n * where listeners fire in registration order — not z-order — so by the time a\n * lower overlay's listener runs, the top one may already have closed and left\n * the stack, making the lower one \"top\" for the very same event.\n */\n private readonly claimed = new WeakSet<Event>();\n\n /** Register `id` as the new top-most overlay (re-pushing moves it to the top). */\n push(id: string): void {\n this.remove(id);\n this.stack.push(id);\n }\n\n remove(id: string): void {\n const index = this.stack.indexOf(id);\n if (index !== -1) this.stack.splice(index, 1);\n }\n\n isTop(id: string): boolean {\n return this.stack.length > 0 && this.stack[this.stack.length - 1] === id;\n }\n\n /**\n * Whether overlay `id` may act on this dismiss event. True only for the\n * top-most overlay and only once per event; the caller then closes and, for\n * keyboard events, stops propagation so nothing outside the stack sees it.\n */\n claim(id: string, event: Event): boolean {\n if (this.claimed.has(event) || !this.isTop(id)) return false;\n this.claimed.add(event);\n return true;\n }\n}\n","import { Injectable, signal } from '@angular/core';\n\n/**\n * The space a fixed overlay keeps free at the top and the bottom of the\n * viewport, as CSS lengths (3.28, AS-34). `lc-app-shell` writes them in the\n * browser: `top` is its header's bottom edge, `bottom` the shown bottom bar's\n * top edge, measured from the viewport's bottom (or the safe-area insets when\n * there is no header or no bar). `lc-toast-outlet` reads them, so its stacks\n * clear the header and the bar even when the app renders the outlet outside\n * the shell (no custom property can inherit there).\n *\n * `null` means \"no shell\": on the server, before the shell has measured,\n * without ResizeObserver and after the shell is destroyed. Internal to the\n * library; not part of the public API and without a semver guarantee.\n */\n@Injectable({ providedIn: 'root' })\nexport class OverlayInsetsService {\n /** CSS length kept free above top-edge overlays, e.g. `'56px'`. */\n readonly top = signal<string | null>(null);\n\n /** CSS length kept free below bottom-edge overlays, e.g. `'102px'`. */\n readonly bottom = signal<string | null>(null);\n}\n"],"mappings":";;AAWA,IAAa,sBAAb,MAAa,oBAAmB;CACb,QAAkB,CAAA;CAQlB,0BAAU,IAAI,QAAO;CAGtC,KAAK,IAAU;EACb,KAAK,OAAO,EAAE;EACd,KAAK,MAAM,KAAK,EAAE;CACpB;CAEA,OAAO,IAAU;EACf,MAAM,QAAQ,KAAK,MAAM,QAAQ,EAAE;EACnC,IAAI,UAAU,IAAI,KAAK,MAAM,OAAO,OAAO,CAAC;CAC9C;CAEA,MAAM,IAAU;EACd,OAAO,KAAK,MAAM,SAAS,KAAK,KAAK,MAAM,KAAK,MAAM,SAAS,OAAO;CACxE;CAOA,MAAM,IAAY,OAAY;EAC5B,IAAI,KAAK,QAAQ,IAAI,KAAK,KAAK,CAAC,KAAK,MAAM,EAAE,GAAG,OAAO;EACvD,KAAK,QAAQ,IAAI,KAAK;EACtB,OAAO;CACT;CAnCW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAmB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAnB,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAmB,YADN;CAAM,CAAA;;AACnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAmB,YAAA,CAAA;EAD/B,MAAA;EAAW,MAAA,CAAA,EAAE,YAAY,OAAM,CAAE;;;ACMlC,IAAa,uBAAb,MAAa,qBAAoB;CAEtB,MAAM,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,MAAA,CAAA,+BAAA,CAAA,CAAA;CAGjC,SAAS,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CALlC,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAoB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAApB,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAoB,YADP;CAAM,CAAA;;AACnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAoB,YAAA,CAAA;EADhC,MAAA;EAAW,MAAA,CAAA,EAAE,YAAY,OAAM,CAAE"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-internal-overlay.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/internal/overlay/overlay-stack.service.ts","../../../../libs/angular-ui-kit/src/lib/internal/overlay/overlay-insets.service.ts","../../../../libs/angular-ui-kit/src/lib/internal/overlay/focus-return.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\n\n/**\n * Keeps the open overlays (modal, drawer, popover, menu, tooltip, …) in\n * opening order so that a dismiss gesture — Escape, click outside — reaches\n * only the top-most one instead of closing every open layer at once.\n *\n * Every overlay pushes its id when it opens and removes it when it closes or\n * is destroyed. Internal to the library; not part of the public API.\n */\n@Injectable({ providedIn: 'root' })\nexport class OverlayStackService {\n private readonly stack: string[] = [];\n\n /**\n * Events already consumed by an overlay. All overlays listen on `document`,\n * where listeners fire in registration order — not z-order — so by the time a\n * lower overlay's listener runs, the top one may already have closed and left\n * the stack, making the lower one \"top\" for the very same event.\n */\n private readonly claimed = new WeakSet<Event>();\n\n /** Register `id` as the new top-most overlay (re-pushing moves it to the top). */\n push(id: string): void {\n this.remove(id);\n this.stack.push(id);\n }\n\n remove(id: string): void {\n const index = this.stack.indexOf(id);\n if (index !== -1) this.stack.splice(index, 1);\n }\n\n isTop(id: string): boolean {\n return this.stack.length > 0 && this.stack[this.stack.length - 1] === id;\n }\n\n /**\n * Whether overlay `id` may act on this dismiss event. True only for the\n * top-most overlay and only once per event; the caller then closes and, for\n * keyboard events, stops propagation so nothing outside the stack sees it.\n */\n claim(id: string, event: Event): boolean {\n if (this.claimed.has(event) || !this.isTop(id)) return false;\n this.claimed.add(event);\n return true;\n }\n}\n","import { Injectable, signal } from '@angular/core';\n\n/**\n * The space a fixed overlay keeps free at the top and the bottom of the\n * viewport, as CSS lengths (3.28, AS-34). `lc-app-shell` writes them in the\n * browser: `top` is its header's bottom edge, `bottom` the shown bottom bar's\n * top edge, measured from the viewport's bottom (or the safe-area insets when\n * there is no header or no bar). `lc-toast-outlet` reads them, so its stacks\n * clear the header and the bar even when the app renders the outlet outside\n * the shell (no custom property can inherit there).\n *\n * `null` means \"no shell\": on the server, before the shell has measured,\n * without ResizeObserver and after the shell is destroyed. Internal to the\n * library; not part of the public API and without a semver guarantee.\n */\n@Injectable({ providedIn: 'root' })\nexport class OverlayInsetsService {\n /** CSS length kept free above top-edge overlays, e.g. `'56px'`. */\n readonly top = signal<string | null>(null);\n\n /** CSS length kept free below bottom-edge overlays, e.g. `'102px'`. */\n readonly bottom = signal<string | null>(null);\n}\n","/**\n * What a custom-element `returnFocusTo` host (`<lc-button>`) hands the focus\n * to when it can't take it itself.\n */\nconst FOCUSABLE =\n 'button:not([disabled]), [href], input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Gives the focus to an overlay's `returnFocusTo` element (3.34 in\n * `lc-modal`, shared since 3.35 with `lc-drawer` and `lc-sidenav`). A\n * custom-element host that can't take focus (`<lc-button>`) hands it to its\n * first focusable descendant. An element that can't take focus at all leaves\n * the focus where it is.\n */\nexport function focusReturnTarget(target: HTMLElement): void {\n target.focus();\n if (target.ownerDocument.activeElement !== target && target.localName.includes('-')) {\n target.querySelector<HTMLElement>(FOCUSABLE)?.focus();\n }\n}\n"],"mappings":";;AAWA,IAAa,sBAAb,MAAa,oBAAmB;CACb,QAAkB,CAAA;CAQlB,0BAAU,IAAI,QAAO;CAGtC,KAAK,IAAU;EACb,KAAK,OAAO,EAAE;EACd,KAAK,MAAM,KAAK,EAAE;CACpB;CAEA,OAAO,IAAU;EACf,MAAM,QAAQ,KAAK,MAAM,QAAQ,EAAE;EACnC,IAAI,UAAU,IAAI,KAAK,MAAM,OAAO,OAAO,CAAC;CAC9C;CAEA,MAAM,IAAU;EACd,OAAO,KAAK,MAAM,SAAS,KAAK,KAAK,MAAM,KAAK,MAAM,SAAS,OAAO;CACxE;CAOA,MAAM,IAAY,OAAY;EAC5B,IAAI,KAAK,QAAQ,IAAI,KAAK,KAAK,CAAC,KAAK,MAAM,EAAE,GAAG,OAAO;EACvD,KAAK,QAAQ,IAAI,KAAK;EACtB,OAAO;CACT;CAnCW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAmB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAnB,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAmB,YADN;CAAM,CAAA;;AACnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAmB,YAAA,CAAA;EAD/B,MAAA;EAAW,MAAA,CAAA,EAAE,YAAY,OAAM,CAAE;;;ACMlC,IAAa,uBAAb,MAAa,qBAAoB;CAEtB,MAAM,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,MAAA,CAAA,+BAAA,CAAA,CAAA;CAGjC,SAAS,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CALlC,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAoB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAApB,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAoB,YADP;CAAM,CAAA;;AACnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAoB,YAAA,CAAA;EADhC,MAAA;EAAW,MAAA,CAAA,EAAE,YAAY,OAAM,CAAE;;;ACXlC,MAAM,YACJ;AASF,SAAgB,kBAAkB,QAAmB;CACnD,OAAO,MAAK;CACZ,IAAI,OAAO,cAAc,kBAAkB,UAAU,OAAO,UAAU,SAAS,GAAG,GAChF,OAAO,cAA2B,SAAS,CAAC,EAAE,MAAK;AAEvD"}
|
|
@@ -4,8 +4,7 @@ import * as i1 from "@angular/cdk/a11y";
|
|
|
4
4
|
import { A11yModule } from "@angular/cdk/a11y";
|
|
5
5
|
import { IconComponent, ɵprovideKitIcons } from "@lifecorner/angular-ui-kit/icon";
|
|
6
6
|
import { ICON_X_MARK } from "@lifecorner/angular-ui-kit/icons";
|
|
7
|
-
import { OverlayStackService } from "@lifecorner/angular-ui-kit/internal/overlay";
|
|
8
|
-
const FOCUSABLE = "button:not([disabled]), [href], input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
7
|
+
import { OverlayStackService, focusReturnTarget } from "@lifecorner/angular-ui-kit/internal/overlay";
|
|
9
8
|
var ModalComponent = class ModalComponent {
|
|
10
9
|
static nextId = 0;
|
|
11
10
|
open = input(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
|
|
@@ -89,8 +88,7 @@ var ModalComponent = class ModalComponent {
|
|
|
89
88
|
afterNextRender(() => {
|
|
90
89
|
const target = this.returnFocusTo();
|
|
91
90
|
if (this.shown || !target?.isConnected) return;
|
|
92
|
-
target
|
|
93
|
-
if (target.ownerDocument.activeElement !== target && target.localName.includes("-")) target.querySelector(FOCUSABLE)?.focus();
|
|
91
|
+
focusReturnTarget(target);
|
|
94
92
|
}, { injector: this.environmentInjector });
|
|
95
93
|
}
|
|
96
94
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-modal.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/modal/modal.component.ts","../../../../libs/angular-ui-kit/src/lib/modal/modal.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n ViewEncapsulation,\n input,\n output,\n computed,\n effect,\n linkedSignal,\n HostListener,\n OnDestroy,\n inject,\n afterNextRender,\n untracked,\n EnvironmentInjector,\n} from '@angular/core';\nimport { A11yModule } from '@angular/cdk/a11y';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_X_MARK } from '@lifecorner/angular-ui-kit/icons';\nimport { OverlayStackService } from '@lifecorner/angular-ui-kit/internal/overlay';\n\nexport type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';\n\n/**\n * Where the dialog sits.\n * - `center`: centred on every screen (3.2)\n * - `auto`: a bottom sheet below 640 px viewport width (full width, top\n * corners rounded, above the safe-area inset, in thumb reach), centred above\n * - `sheet`: a bottom sheet on every screen\n */\nexport type ModalPlacement = 'center' | 'auto' | 'sheet';\n\n/**\n * What a custom-element `returnFocusTo` host (`<lc-button>`) hands the focus\n * to when it can't take it itself.\n */\nconst FOCUSABLE =\n 'button:not([disabled]), [href], input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Modal dialog component for focused user interactions.\n *\n * Features:\n * - Size presets (sm, md, lg, xl, full)\n * - Placement: centred, or a bottom sheet on phones (`placement=\"auto\"`)\n * - Header and footer stay visible while the body scrolls; the close button\n * is a 44 px target\n * - Focus trap for keyboard accessibility; `returnFocusTo` (3.34) names the\n * element that gets the focus back after the dialog closed (WebKit doesn't\n * focus a tapped button, so the one captured at open is `body`)\n * - Backdrop click and Escape key to close\n * - Two-way open binding\n * - Header, body, and footer content slots\n * - Optional close button\n * - Accessible with ARIA dialog role\n *\n * @example\n * ```html\n * <lc-modal [(open)]=\"isOpen\" size=\"md\" [closeOnBackdropClick]=\"true\">\n * <div slot=\"header\"><h2>Title</h2></div>\n * <div slot=\"body\">Content</div>\n * <div slot=\"footer\"><lc-button (click)=\"close()\">Close</lc-button></div>\n * </lc-modal>\n * ```\n */\n@Component({\n selector: 'lc-modal',\n standalone: true,\n imports: [A11yModule, IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_X_MARK])],\n templateUrl: './modal.component.html',\n styleUrls: ['./modal.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None, // Required for dynamic size class styling\n})\nexport class ModalComponent implements OnDestroy {\n private static nextId = 0;\n\n /**\n * Whether the modal is open (input from parent)\n * @default false\n */\n open = input<boolean>(false);\n\n /**\n * Modal size\n * @default 'md'\n */\n size = input<ModalSize>('md');\n\n /**\n * Where the dialog sits: `center` (3.2), `auto` — a bottom sheet below\n * 640 px viewport width and centred above — or `sheet` (always a bottom\n * sheet). The sheet spans the width, rounds its top corners, keeps clear of\n * `env(safe-area-inset-bottom)` and is at most 90 % of the dynamic viewport\n * high; header and footer stay visible while the body scrolls. `full`\n * size ignores it.\n * @default 'center'\n */\n placement = input<ModalPlacement>('center');\n\n /**\n * Whether clicking backdrop closes modal\n * @default true\n */\n closeOnBackdropClick = input<boolean>(true);\n\n /**\n * Whether pressing Escape closes modal\n * @default true\n */\n closeOnEscape = input<boolean>(true);\n\n /**\n * Whether to show close button in header\n * @default true\n */\n showCloseButton = input<boolean>(true);\n\n /**\n * ARIA label for accessibility\n */\n ariaLabel = input<string>();\n\n /**\n * ARIA labelledby ID\n */\n ariaLabelledBy = input<string>();\n\n /**\n * ARIA describedby ID\n */\n ariaDescribedBy = input<string>();\n\n /** Accessible name of the close button (`showCloseButton`). */\n closeAriaLabel = input('Close modal');\n\n /**\n * Element that gets the focus back after the dialog closed (3.34), instead\n * of the one that had it at open. Set it to the trigger: WebKit (Safari,\n * every iOS browser) doesn't focus a tapped button, so the captured element\n * is `body`. Applied after the next render, once the dialog has left the DOM\n * and the focus trap has restored its captured element. This holds on every\n * close: ×, Escape, backdrop, `open` false, `closeModal()`, and destroying an\n * open modal. A custom-element host that can't take focus (`<lc-button>`)\n * hands it to its first focusable descendant. Ignored when the element is no\n * longer in the document or can't take focus; the captured element then\n * keeps it. `null`: as before.\n * @default null\n */\n returnFocusTo = input<HTMLElement | null>(null);\n\n /**\n * Emitted when modal opens\n */\n readonly modalOpened = output<void>();\n\n /**\n * Emitted when modal closes\n */\n readonly modalClosed = output<void>();\n\n /**\n * Two-way binding for open state\n */\n readonly openChange = output<boolean>();\n\n /**\n * Emitted when backdrop is clicked\n */\n readonly backdropClicked = output<MouseEvent>();\n\n /**\n * Internal open state (protected for AOT); follows the `open` input and is\n * also set by the programmatic open/close methods.\n */\n protected _open = linkedSignal(() => this.open());\n\n /**\n * Computed modal classes\n */\n protected modalClasses = computed(() => {\n return `lc-modal lc-modal--${this.size()} lc-modal--placement-${this.placement()}`;\n });\n\n private readonly overlayStack = inject(OverlayStackService);\n /** Identifies this instance in the overlay stack. */\n private readonly modalId = `lc-modal-${++ModalComponent.nextId}`;\n private originalOverflow?: string;\n /**\n * The environment injector: a node injector would cancel the focus-return\n * hook when the modal is destroyed while open (`ConfirmService`).\n */\n private readonly environmentInjector = inject(EnvironmentInjector);\n /** Whether `showModal()` ran without a `hideModal()` after it. */\n private shown = false;\n\n constructor() {\n // Watch for open state changes\n effect(() => {\n if (this._open()) {\n this.showModal();\n } else {\n this.hideModal();\n }\n });\n }\n\n ngOnDestroy(): void {\n this.hideModal();\n }\n\n /**\n * Open the modal programmatically\n */\n openModal(): void {\n this._open.set(true);\n this.openChange.emit(true);\n this.modalOpened.emit();\n }\n\n /**\n * Close the modal programmatically\n */\n closeModal(): void {\n this._open.set(false);\n this.openChange.emit(false);\n this.modalClosed.emit();\n }\n\n /**\n * Handle backdrop click. Ignored while another overlay (popover, menu,\n * confirm dialog, …) sits above this modal — the click belongs to that layer.\n */\n protected onBackdropClick(event: MouseEvent): void {\n if (!this.overlayStack.claim(this.modalId, event)) return;\n this.backdropClicked.emit(event);\n if (this.closeOnBackdropClick()) {\n this.closeModal();\n }\n }\n\n /**\n * Handle close button click\n */\n protected onCloseClick(event: Event): void {\n event.preventDefault();\n event.stopPropagation();\n this.closeModal();\n }\n\n /**\n * Escape closes the modal only while it is the top-most overlay; the event\n * is consumed either way so overlays underneath stay open.\n */\n @HostListener('document:keydown', ['$event'])\n protected onDocumentKeydown(event: KeyboardEvent): void {\n if (event.key !== 'Escape' || !this._open()) return;\n if (!this.overlayStack.claim(this.modalId, event)) return;\n event.stopPropagation();\n if (this.closeOnEscape()) {\n this.closeModal();\n }\n }\n\n /**\n * Show the modal. Focus is moved inside and restored on close by the\n * template's `cdkTrapFocus` / `cdkTrapFocusAutoCapture`, then given to\n * `returnFocusTo` when set (3.34).\n */\n private showModal(): void {\n this.shown = true;\n this.overlayStack.push(this.modalId);\n\n // Lock body scroll\n if (typeof document !== 'undefined' && this.originalOverflow === undefined) {\n this.originalOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n }\n\n /**\n * Hide the modal\n */\n private hideModal(): void {\n this.overlayStack.remove(this.modalId);\n\n // Restore body scroll\n if (typeof document !== 'undefined' && this.originalOverflow !== undefined) {\n document.body.style.overflow = this.originalOverflow;\n this.originalOverflow = undefined;\n }\n\n if (!this.shown) return;\n this.shown = false;\n // Untracked: this runs inside the open effect, and a tracked read would\n // re-run it (and re-push the modal above a nested overlay) when the input\n // changes.\n if (untracked(this.returnFocusTo)) this.scheduleFocusReturn();\n }\n\n /**\n * Focuses `returnFocusTo` after the next render: the template refresh has\n * removed the dialog, and `cdkTrapFocus` has restored its captured element\n * on destroy, so this is the last focus write. Reads the input again then,\n * so a trigger re-rendered meanwhile is taken from a fresh query.\n */\n private scheduleFocusReturn(): void {\n afterNextRender(\n () => {\n const target = this.returnFocusTo();\n if (this.shown || !target?.isConnected) return;\n target.focus();\n if (target.ownerDocument.activeElement !== target && target.localName.includes('-')) {\n target.querySelector<HTMLElement>(FOCUSABLE)?.focus();\n }\n },\n { injector: this.environmentInjector },\n );\n }\n}\n","@if (_open()) {\n <!-- Backdrop (the lc-modal does NOT use CDK Overlay; do not add the\n `cdk-overlay-backdrop` class here — it would be styled by global CDK\n rules and, vice versa, leak modal styling onto other overlays.) -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- backdrop click is a mouse shortcut; Escape and the close button close the modal -->\n <div\n class=\"lc-modal-backdrop\"\n (click)=\"onBackdropClick($event)\"\n ></div>\n\n <!-- Modal Content -->\n <div\n [class]=\"modalClasses()\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"ariaLabelledBy()\"\n [attr.aria-describedby]=\"ariaDescribedBy()\"\n cdkTrapFocus\n [cdkTrapFocusAutoCapture]=\"true\"\n >\n <div class=\"lc-modal__header\">\n <ng-content select=\"[slot='header']\" />\n \n @if (showCloseButton()) {\n <button\n type=\"button\"\n class=\"lc-modal__close\"\n [attr.aria-label]=\"closeAriaLabel()\"\n (click)=\"onCloseClick($event)\"\n >\n <lc-icon name=\"x-mark\" size=\"md\" [decorative]=\"true\" />\n </button>\n }\n </div>\n \n <div class=\"lc-modal__body\">\n <ng-content select=\"[slot='body']\" />\n <ng-content />\n </div>\n \n <div class=\"lc-modal__footer\">\n <ng-content select=\"[slot='footer']\" />\n </div>\n </div>\n}\n"],"mappings":";;;;;;;AAoCA,MAAM,YACJ;AAsCF,IAAa,iBAAb,MAAa,eAAc;CACjB,OAAO,SAAS;CAMxB,OAAO,MAAe,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAM5B,OAAO,MAAiB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAW7B,YAAY,MAAsB,UAAS,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAM3C,uBAAuB,MAAe,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,uBAAA,CAAA,+BAAA,CAAA,CAAA;CAM3C,gBAAgB,MAAe,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAMpC,kBAAkB,MAAe,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAKtC,YAAY,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAK3B,iBAAiB,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CAKhC,kBAAkB,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAGjC,iBAAiB,MAAM,eAAc,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CAerC,gBAAgB,MAA0B,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAKtC,cAAc,OAAM;CAKpB,cAAc,OAAM;CAKpB,aAAa,OAAM;CAKnB,kBAAkB,OAAM;CAMvB,QAAQ,mBAAmB,KAAK,KAAI,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAKvC,eAAe,eAAc;EACrC,OAAO,sBAAsB,KAAK,KAAI,EAAE,uBAAwB,KAAK,UAAS;CAChF,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAEe,eAAe,OAAO,mBAAmB;CAEzC,UAAU,YAAY,EAAE,eAAe;CAChD;CAKS,sBAAsB,OAAO,mBAAmB;CAEzD,QAAQ;CAEhB,cAAA;EAEE,aAAY;GACV,IAAI,KAAK,MAAK,GACZ,KAAK,UAAS;QAEd,KAAK,UAAS;EAElB,CAAC;CACH;CAEA,cAAW;EACT,KAAK,UAAS;CAChB;CAKA,YAAS;EACP,KAAK,MAAM,IAAI,IAAI;EACnB,KAAK,WAAW,KAAK,IAAI;EACzB,KAAK,YAAY,KAAI;CACvB;CAKA,aAAU;EACR,KAAK,MAAM,IAAI,KAAK;EACpB,KAAK,WAAW,KAAK,KAAK;EAC1B,KAAK,YAAY,KAAI;CACvB;CAMU,gBAAgB,OAAiB;EACzC,IAAI,CAAC,KAAK,aAAa,MAAM,KAAK,SAAS,KAAK,GAAG;EACnD,KAAK,gBAAgB,KAAK,KAAK;EAC/B,IAAI,KAAK,qBAAoB,GAC3B,KAAK,WAAU;CAEnB;CAKU,aAAa,OAAY;EACjC,MAAM,eAAc;EACpB,MAAM,gBAAe;EACrB,KAAK,WAAU;CACjB;CAOU,kBAAkB,OAAoB;EAC9C,IAAI,MAAM,QAAQ,YAAY,CAAC,KAAK,MAAK,GAAI;EAC7C,IAAI,CAAC,KAAK,aAAa,MAAM,KAAK,SAAS,KAAK,GAAG;EACnD,MAAM,gBAAe;EACrB,IAAI,KAAK,cAAa,GACpB,KAAK,WAAU;CAEnB;CAOQ,YAAS;EACf,KAAK,QAAQ;EACb,KAAK,aAAa,KAAK,KAAK,OAAO;EAGnC,IAAI,OAAO,aAAa,eAAe,KAAK,qBAAqB,QAAW;GAC1E,KAAK,mBAAmB,SAAS,KAAK,MAAM;GAC5C,SAAS,KAAK,MAAM,WAAW;EACjC;CACF;CAKQ,YAAS;EACf,KAAK,aAAa,OAAO,KAAK,OAAO;EAGrC,IAAI,OAAO,aAAa,eAAe,KAAK,qBAAqB,QAAW;GAC1E,SAAS,KAAK,MAAM,WAAW,KAAK;GACpC,KAAK,mBAAmB;EAC1B;EAEA,IAAI,CAAC,KAAK,OAAO;EACjB,KAAK,QAAQ;EAIb,IAAI,UAAU,KAAK,aAAa,GAAG,KAAK,oBAAmB;CAC7D;CAQQ,sBAAmB;EACzB,sBACO;GACH,MAAM,SAAS,KAAK,cAAa;GACjC,IAAI,KAAK,SAAS,CAAC,QAAQ,aAAa;GACxC,OAAO,MAAK;GACZ,IAAI,OAAO,cAAc,kBAAkB,UAAU,OAAO,UAAU,SAAS,GAAG,GAChF,OAAO,cAA2B,SAAS,CAAC,EAAE,MAAK;EAEvD,GACA,EAAE,UAAU,KAAK,oBAAmB,CAAE;CAE1C;CApPW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAc,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAd,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAc,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,sBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,eAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,iBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,gBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,iBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,gBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,eAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,SAAA;GAAA,aAAA;GAAA,aAAA;GAAA,YAAA;GAAA,iBAAA;EAAA;EAAA,MAAA,EAAA,WAAA,EAAA,oBAAA,4BAAA,EAAA;EAAA,UAAA;EAAA,UC3E3B;EA8CA,QAAA,CAAA,wxHAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MDsBY;GAAU;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA,CAAA,gBAAA,yBAAA;IAAA,UAAA,CAAA,cAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAa,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;EAAA;EAAA,eACpB,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;EAAC,iBAAA,GAAA,wBAAA;EAAA,eAAA,GAAA,kBAAA;CAAA,CAAA;;AAMrC,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAc,YAAA,CAAA;EAV1B,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAU,YACR;GAAI,SACP,CAAC,YAAY,aAAa;GAAC,eACrB,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;GAAC,iBAG/B,wBAAwB;GAAM,eAChC,kBAAkB;GAAI,UAAA;GAAA,QAAA,CAAA,wxHAAA;EAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsLpC,MAAA;GAAa,MAAA,CAAA,oBAAoB,CAAC,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-modal.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/modal/modal.component.ts","../../../../libs/angular-ui-kit/src/lib/modal/modal.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n ViewEncapsulation,\n input,\n output,\n computed,\n effect,\n linkedSignal,\n HostListener,\n OnDestroy,\n inject,\n afterNextRender,\n untracked,\n EnvironmentInjector,\n} from '@angular/core';\nimport { A11yModule } from '@angular/cdk/a11y';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_X_MARK } from '@lifecorner/angular-ui-kit/icons';\nimport { OverlayStackService, focusReturnTarget } from '@lifecorner/angular-ui-kit/internal/overlay';\n\nexport type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';\n\n/**\n * Where the dialog sits.\n * - `center`: centred on every screen (3.2)\n * - `auto`: a bottom sheet below 640 px viewport width (full width, top\n * corners rounded, above the safe-area inset, in thumb reach), centred above\n * - `sheet`: a bottom sheet on every screen\n */\nexport type ModalPlacement = 'center' | 'auto' | 'sheet';\n\n/**\n * Modal dialog component for focused user interactions.\n *\n * Features:\n * - Size presets (sm, md, lg, xl, full)\n * - Placement: centred, or a bottom sheet on phones (`placement=\"auto\"`)\n * - Header and footer stay visible while the body scrolls; the close button\n * is a 44 px target\n * - Focus trap for keyboard accessibility; `returnFocusTo` (3.34) names the\n * element that gets the focus back after the dialog closed (WebKit doesn't\n * focus a tapped button, so the one captured at open is `body`)\n * - Backdrop click and Escape key to close\n * - Two-way open binding\n * - Header, body, and footer content slots\n * - Optional close button\n * - Accessible with ARIA dialog role\n *\n * @example\n * ```html\n * <lc-modal [(open)]=\"isOpen\" size=\"md\" [closeOnBackdropClick]=\"true\">\n * <div slot=\"header\"><h2>Title</h2></div>\n * <div slot=\"body\">Content</div>\n * <div slot=\"footer\"><lc-button (click)=\"close()\">Close</lc-button></div>\n * </lc-modal>\n * ```\n */\n@Component({\n selector: 'lc-modal',\n standalone: true,\n imports: [A11yModule, IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_X_MARK])],\n templateUrl: './modal.component.html',\n styleUrls: ['./modal.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None, // Required for dynamic size class styling\n})\nexport class ModalComponent implements OnDestroy {\n private static nextId = 0;\n\n /**\n * Whether the modal is open (input from parent)\n * @default false\n */\n open = input<boolean>(false);\n\n /**\n * Modal size\n * @default 'md'\n */\n size = input<ModalSize>('md');\n\n /**\n * Where the dialog sits: `center` (3.2), `auto` — a bottom sheet below\n * 640 px viewport width and centred above — or `sheet` (always a bottom\n * sheet). The sheet spans the width, rounds its top corners, keeps clear of\n * `env(safe-area-inset-bottom)` and is at most 90 % of the dynamic viewport\n * high; header and footer stay visible while the body scrolls. `full`\n * size ignores it.\n * @default 'center'\n */\n placement = input<ModalPlacement>('center');\n\n /**\n * Whether clicking backdrop closes modal\n * @default true\n */\n closeOnBackdropClick = input<boolean>(true);\n\n /**\n * Whether pressing Escape closes modal\n * @default true\n */\n closeOnEscape = input<boolean>(true);\n\n /**\n * Whether to show close button in header\n * @default true\n */\n showCloseButton = input<boolean>(true);\n\n /**\n * ARIA label for accessibility\n */\n ariaLabel = input<string>();\n\n /**\n * ARIA labelledby ID\n */\n ariaLabelledBy = input<string>();\n\n /**\n * ARIA describedby ID\n */\n ariaDescribedBy = input<string>();\n\n /** Accessible name of the close button (`showCloseButton`). */\n closeAriaLabel = input('Close modal');\n\n /**\n * Element that gets the focus back after the dialog closed (3.34), instead\n * of the one that had it at open. Set it to the trigger: WebKit (Safari,\n * every iOS browser) doesn't focus a tapped button, so the captured element\n * is `body`. Applied after the next render, once the dialog has left the DOM\n * and the focus trap has restored its captured element. This holds on every\n * close: ×, Escape, backdrop, `open` false, `closeModal()`, and destroying an\n * open modal. A custom-element host that can't take focus (`<lc-button>`)\n * hands it to its first focusable descendant. Ignored when the element is no\n * longer in the document or can't take focus; the captured element then\n * keeps it. `null`: as before.\n * @default null\n */\n returnFocusTo = input<HTMLElement | null>(null);\n\n /**\n * Emitted when modal opens\n */\n readonly modalOpened = output<void>();\n\n /**\n * Emitted when modal closes\n */\n readonly modalClosed = output<void>();\n\n /**\n * Two-way binding for open state\n */\n readonly openChange = output<boolean>();\n\n /**\n * Emitted when backdrop is clicked\n */\n readonly backdropClicked = output<MouseEvent>();\n\n /**\n * Internal open state (protected for AOT); follows the `open` input and is\n * also set by the programmatic open/close methods.\n */\n protected _open = linkedSignal(() => this.open());\n\n /**\n * Computed modal classes\n */\n protected modalClasses = computed(() => {\n return `lc-modal lc-modal--${this.size()} lc-modal--placement-${this.placement()}`;\n });\n\n private readonly overlayStack = inject(OverlayStackService);\n /** Identifies this instance in the overlay stack. */\n private readonly modalId = `lc-modal-${++ModalComponent.nextId}`;\n private originalOverflow?: string;\n /**\n * The environment injector: a node injector would cancel the focus-return\n * hook when the modal is destroyed while open (`ConfirmService`).\n */\n private readonly environmentInjector = inject(EnvironmentInjector);\n /** Whether `showModal()` ran without a `hideModal()` after it. */\n private shown = false;\n\n constructor() {\n // Watch for open state changes\n effect(() => {\n if (this._open()) {\n this.showModal();\n } else {\n this.hideModal();\n }\n });\n }\n\n ngOnDestroy(): void {\n this.hideModal();\n }\n\n /**\n * Open the modal programmatically\n */\n openModal(): void {\n this._open.set(true);\n this.openChange.emit(true);\n this.modalOpened.emit();\n }\n\n /**\n * Close the modal programmatically\n */\n closeModal(): void {\n this._open.set(false);\n this.openChange.emit(false);\n this.modalClosed.emit();\n }\n\n /**\n * Handle backdrop click. Ignored while another overlay (popover, menu,\n * confirm dialog, …) sits above this modal — the click belongs to that layer.\n */\n protected onBackdropClick(event: MouseEvent): void {\n if (!this.overlayStack.claim(this.modalId, event)) return;\n this.backdropClicked.emit(event);\n if (this.closeOnBackdropClick()) {\n this.closeModal();\n }\n }\n\n /**\n * Handle close button click\n */\n protected onCloseClick(event: Event): void {\n event.preventDefault();\n event.stopPropagation();\n this.closeModal();\n }\n\n /**\n * Escape closes the modal only while it is the top-most overlay; the event\n * is consumed either way so overlays underneath stay open.\n */\n @HostListener('document:keydown', ['$event'])\n protected onDocumentKeydown(event: KeyboardEvent): void {\n if (event.key !== 'Escape' || !this._open()) return;\n if (!this.overlayStack.claim(this.modalId, event)) return;\n event.stopPropagation();\n if (this.closeOnEscape()) {\n this.closeModal();\n }\n }\n\n /**\n * Show the modal. Focus is moved inside and restored on close by the\n * template's `cdkTrapFocus` / `cdkTrapFocusAutoCapture`, then given to\n * `returnFocusTo` when set (3.34).\n */\n private showModal(): void {\n this.shown = true;\n this.overlayStack.push(this.modalId);\n\n // Lock body scroll\n if (typeof document !== 'undefined' && this.originalOverflow === undefined) {\n this.originalOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n }\n\n /**\n * Hide the modal\n */\n private hideModal(): void {\n this.overlayStack.remove(this.modalId);\n\n // Restore body scroll\n if (typeof document !== 'undefined' && this.originalOverflow !== undefined) {\n document.body.style.overflow = this.originalOverflow;\n this.originalOverflow = undefined;\n }\n\n if (!this.shown) return;\n this.shown = false;\n // Untracked: this runs inside the open effect, and a tracked read would\n // re-run it (and re-push the modal above a nested overlay) when the input\n // changes.\n if (untracked(this.returnFocusTo)) this.scheduleFocusReturn();\n }\n\n /**\n * Focuses `returnFocusTo` after the next render: the template refresh has\n * removed the dialog, and `cdkTrapFocus` has restored its captured element\n * on destroy, so this is the last focus write. Reads the input again then,\n * so a trigger re-rendered meanwhile is taken from a fresh query.\n */\n private scheduleFocusReturn(): void {\n afterNextRender(\n () => {\n const target = this.returnFocusTo();\n if (this.shown || !target?.isConnected) return;\n focusReturnTarget(target);\n },\n { injector: this.environmentInjector },\n );\n }\n}\n","@if (_open()) {\n <!-- Backdrop (the lc-modal does NOT use CDK Overlay; do not add the\n `cdk-overlay-backdrop` class here — it would be styled by global CDK\n rules and, vice versa, leak modal styling onto other overlays.) -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- backdrop click is a mouse shortcut; Escape and the close button close the modal -->\n <div\n class=\"lc-modal-backdrop\"\n (click)=\"onBackdropClick($event)\"\n ></div>\n\n <!-- Modal Content -->\n <div\n [class]=\"modalClasses()\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"ariaLabelledBy()\"\n [attr.aria-describedby]=\"ariaDescribedBy()\"\n cdkTrapFocus\n [cdkTrapFocusAutoCapture]=\"true\"\n >\n <div class=\"lc-modal__header\">\n <ng-content select=\"[slot='header']\" />\n \n @if (showCloseButton()) {\n <button\n type=\"button\"\n class=\"lc-modal__close\"\n [attr.aria-label]=\"closeAriaLabel()\"\n (click)=\"onCloseClick($event)\"\n >\n <lc-icon name=\"x-mark\" size=\"md\" [decorative]=\"true\" />\n </button>\n }\n </div>\n \n <div class=\"lc-modal__body\">\n <ng-content select=\"[slot='body']\" />\n <ng-content />\n </div>\n \n <div class=\"lc-modal__footer\">\n <ng-content select=\"[slot='footer']\" />\n </div>\n </div>\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from "@angular/core";
|
|
2
|
-
import { ChangeDetectionStrategy, Component, HostListener, computed, effect, inject, input, model, output, signal, untracked } from "@angular/core";
|
|
2
|
+
import { ChangeDetectionStrategy, Component, EnvironmentInjector, HostListener, afterNextRender, computed, effect, inject, input, model, output, signal, untracked } from "@angular/core";
|
|
3
3
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|
4
4
|
import { NgStyle, NgTemplateOutlet } from "@angular/common";
|
|
5
5
|
import { NavigationEnd, Router, RouterLink } from "@angular/router";
|
|
@@ -11,6 +11,7 @@ import { IconComponent, ɵprovideKitIcons } from "@lifecorner/angular-ui-kit/ico
|
|
|
11
11
|
import { ICON_CHEVRON_DOWN, ICON_CHEVRON_LEFT, ICON_CHEVRON_RIGHT } from "@lifecorner/angular-ui-kit/icons";
|
|
12
12
|
import { BadgeComponent } from "@lifecorner/angular-ui-kit/badge";
|
|
13
13
|
import { LogoComponent } from "@lifecorner/angular-ui-kit/logo";
|
|
14
|
+
import { focusReturnTarget } from "@lifecorner/angular-ui-kit/internal/overlay";
|
|
14
15
|
var SidenavComponent = class SidenavComponent {
|
|
15
16
|
router = inject(Router);
|
|
16
17
|
routerUrl = signal(this.router.url, ...ngDevMode ? [{ debugName: "routerUrl" }] : /* istanbul ignore next */ []);
|
|
@@ -42,6 +43,7 @@ var SidenavComponent = class SidenavComponent {
|
|
|
42
43
|
width = input("320px", ...ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []);
|
|
43
44
|
ariaLabel = input("Side navigation", ...ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []);
|
|
44
45
|
hasOverlay = input(true, ...ngDevMode ? [{ debugName: "hasOverlay" }] : /* istanbul ignore next */ []);
|
|
46
|
+
returnFocusTo = input(null, ...ngDevMode ? [{ debugName: "returnFocusTo" }] : /* istanbul ignore next */ []);
|
|
45
47
|
items = input([], ...ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []);
|
|
46
48
|
activeRoute = input("", ...ngDevMode ? [{ debugName: "activeRoute" }] : /* istanbul ignore next */ []);
|
|
47
49
|
theme = input("auto", ...ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []);
|
|
@@ -60,6 +62,9 @@ var SidenavComponent = class SidenavComponent {
|
|
|
60
62
|
closed = output();
|
|
61
63
|
itemClicked = output();
|
|
62
64
|
itemAction = output();
|
|
65
|
+
drawerOpen = computed(() => this.isOpen() && this.effectiveMode() === "drawer", ...ngDevMode ? [{ debugName: "drawerOpen" }] : /* istanbul ignore next */ []);
|
|
66
|
+
drawerShown = false;
|
|
67
|
+
environmentInjector = inject(EnvironmentInjector);
|
|
63
68
|
constructor() {
|
|
64
69
|
this.router.events.pipe(filter((event) => event instanceof NavigationEnd), takeUntilDestroyed()).subscribe(() => this.routerUrl.set(this.router.url));
|
|
65
70
|
effect(() => {
|
|
@@ -73,9 +78,29 @@ var SidenavComponent = class SidenavComponent {
|
|
|
73
78
|
if (parents.every((id) => expanded.has(id))) return;
|
|
74
79
|
this.expandedItems.set(/* @__PURE__ */ new Set([...expanded, ...parents]));
|
|
75
80
|
});
|
|
81
|
+
effect(() => {
|
|
82
|
+
const open = this.drawerOpen();
|
|
83
|
+
untracked(() => {
|
|
84
|
+
if (open) this.drawerShown = true;
|
|
85
|
+
else this.drawerClosed();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
76
88
|
}
|
|
77
89
|
ngOnDestroy() {
|
|
78
90
|
this.teardownMediaQuery();
|
|
91
|
+
this.drawerClosed();
|
|
92
|
+
}
|
|
93
|
+
drawerClosed() {
|
|
94
|
+
if (!this.drawerShown) return;
|
|
95
|
+
this.drawerShown = false;
|
|
96
|
+
if (this.returnFocusTo()) this.scheduleFocusReturn();
|
|
97
|
+
}
|
|
98
|
+
scheduleFocusReturn() {
|
|
99
|
+
afterNextRender(() => {
|
|
100
|
+
const target = this.returnFocusTo();
|
|
101
|
+
if (this.drawerShown || !target?.isConnected) return;
|
|
102
|
+
focusReturnTarget(target);
|
|
103
|
+
}, { injector: this.environmentInjector });
|
|
79
104
|
}
|
|
80
105
|
setupMediaQuery() {
|
|
81
106
|
if (typeof window === "undefined") return;
|
|
@@ -315,6 +340,13 @@ var SidenavComponent = class SidenavComponent {
|
|
|
315
340
|
isRequired: false,
|
|
316
341
|
transformFunction: null
|
|
317
342
|
},
|
|
343
|
+
returnFocusTo: {
|
|
344
|
+
classPropertyName: "returnFocusTo",
|
|
345
|
+
publicName: "returnFocusTo",
|
|
346
|
+
isSignal: true,
|
|
347
|
+
isRequired: false,
|
|
348
|
+
transformFunction: null
|
|
349
|
+
},
|
|
318
350
|
items: {
|
|
319
351
|
classPropertyName: "items",
|
|
320
352
|
publicName: "items",
|
|
@@ -709,6 +741,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
709
741
|
required: false
|
|
710
742
|
}]
|
|
711
743
|
}],
|
|
744
|
+
returnFocusTo: [{
|
|
745
|
+
type: i0.Input,
|
|
746
|
+
args: [{
|
|
747
|
+
isSignal: true,
|
|
748
|
+
alias: "returnFocusTo",
|
|
749
|
+
required: false
|
|
750
|
+
}]
|
|
751
|
+
}],
|
|
712
752
|
items: [{
|
|
713
753
|
type: i0.Input,
|
|
714
754
|
args: [{
|