forty-cdk 0.7.0 → 0.8.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/LICENSE +21 -21
- package/README.md +3 -3
- package/aspect-ratio/README.md +19 -0
- package/avatar/README.md +3 -3
- package/breadcrumbs/README.md +12 -0
- package/calendar/README.md +20 -17
- package/date-picker/README.md +22 -63
- package/dialog/README.md +1 -1
- package/drag-drop/README.md +2 -2
- package/fesm2022/forty-cdk-accordion.mjs +2 -15
- package/fesm2022/forty-cdk-accordion.mjs.map +1 -1
- package/fesm2022/forty-cdk-aspect-ratio.mjs +11 -15
- package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -1
- package/fesm2022/forty-cdk-avatar.mjs +8 -4
- package/fesm2022/forty-cdk-avatar.mjs.map +1 -1
- package/fesm2022/forty-cdk-breadcrumbs.mjs +32 -21
- package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -1
- package/fesm2022/forty-cdk-button.mjs +253 -20
- package/fesm2022/forty-cdk-button.mjs.map +1 -1
- package/fesm2022/forty-cdk-calendar.mjs +109 -76
- package/fesm2022/forty-cdk-calendar.mjs.map +1 -1
- package/fesm2022/forty-cdk-checkbox.mjs +2 -15
- package/fesm2022/forty-cdk-checkbox.mjs.map +1 -1
- package/fesm2022/forty-cdk-combobox.mjs +124 -97
- package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
- package/fesm2022/forty-cdk-core.mjs +1446 -1174
- package/fesm2022/forty-cdk-core.mjs.map +1 -1
- package/fesm2022/forty-cdk-date-field.mjs +3 -0
- package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
- package/fesm2022/forty-cdk-date-picker.mjs +35 -70
- package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
- package/fesm2022/forty-cdk-date-range-field.mjs +25 -106
- package/fesm2022/forty-cdk-date-range-field.mjs.map +1 -1
- package/fesm2022/forty-cdk-dialog.mjs +31 -17
- package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
- package/fesm2022/forty-cdk-disclosure.mjs +2 -15
- package/fesm2022/forty-cdk-disclosure.mjs.map +1 -1
- package/fesm2022/forty-cdk-drag-drop.mjs +81 -15
- package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
- package/fesm2022/forty-cdk-drawer.mjs +37 -23
- package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
- package/fesm2022/forty-cdk-field.mjs +2 -15
- package/fesm2022/forty-cdk-field.mjs.map +1 -1
- package/fesm2022/forty-cdk-fieldset.mjs +2 -15
- package/fesm2022/forty-cdk-fieldset.mjs.map +1 -1
- package/fesm2022/forty-cdk-file-upload.mjs +44 -46
- package/fesm2022/forty-cdk-file-upload.mjs.map +1 -1
- package/fesm2022/forty-cdk-input.mjs +2 -15
- package/fesm2022/forty-cdk-input.mjs.map +1 -1
- package/fesm2022/forty-cdk-internationalized-date.mjs +10 -8
- package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
- package/fesm2022/forty-cdk-listbox.mjs +87 -3
- package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
- package/fesm2022/forty-cdk-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-menubar.mjs.map +1 -1
- package/fesm2022/forty-cdk-meter.mjs +31 -29
- package/fesm2022/forty-cdk-meter.mjs.map +1 -1
- package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-number-input.mjs +1 -1
- package/fesm2022/forty-cdk-number-input.mjs.map +1 -1
- package/fesm2022/forty-cdk-otp-input.mjs +4 -17
- package/fesm2022/forty-cdk-otp-input.mjs.map +1 -1
- package/fesm2022/forty-cdk-pane-resizer.mjs +2 -15
- package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -1
- package/fesm2022/forty-cdk-popover.mjs +8 -1
- package/fesm2022/forty-cdk-popover.mjs.map +1 -1
- package/fesm2022/forty-cdk-progress.mjs +25 -14
- package/fesm2022/forty-cdk-progress.mjs.map +1 -1
- package/fesm2022/forty-cdk-radio-group.mjs +6 -5
- package/fesm2022/forty-cdk-radio-group.mjs.map +1 -1
- package/fesm2022/forty-cdk-search.mjs +136 -38
- package/fesm2022/forty-cdk-search.mjs.map +1 -1
- package/fesm2022/forty-cdk-select.mjs +308 -162
- package/fesm2022/forty-cdk-select.mjs.map +1 -1
- package/fesm2022/forty-cdk-separator.mjs +1 -15
- package/fesm2022/forty-cdk-separator.mjs.map +1 -1
- package/fesm2022/forty-cdk-slider.mjs +79 -90
- package/fesm2022/forty-cdk-slider.mjs.map +1 -1
- package/fesm2022/forty-cdk-stepper.mjs +13 -6
- package/fesm2022/forty-cdk-stepper.mjs.map +1 -1
- package/fesm2022/forty-cdk-switch.mjs +2 -15
- package/fesm2022/forty-cdk-switch.mjs.map +1 -1
- package/fesm2022/forty-cdk-table.mjs +244 -84
- package/fesm2022/forty-cdk-table.mjs.map +1 -1
- package/fesm2022/forty-cdk-time-field.mjs +3 -0
- package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
- package/fesm2022/forty-cdk-time-picker.mjs +61 -129
- package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
- package/fesm2022/forty-cdk-time-range-field.mjs +64 -111
- package/fesm2022/forty-cdk-time-range-field.mjs.map +1 -1
- package/fesm2022/forty-cdk-toggle.mjs +5 -6
- package/fesm2022/forty-cdk-toggle.mjs.map +1 -1
- package/fesm2022/forty-cdk-toolbar.mjs +10 -3
- package/fesm2022/forty-cdk-toolbar.mjs.map +1 -1
- package/fesm2022/forty-cdk-tree.mjs +36 -19
- package/fesm2022/forty-cdk-tree.mjs.map +1 -1
- package/fesm2022/forty-cdk-virtualization.mjs +34 -18
- package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
- package/file-upload/README.md +4 -4
- package/internationalized-date/README.md +23 -23
- package/listbox/README.md +1 -1
- package/meter/README.md +12 -10
- package/package.json +1 -1
- package/progress/README.md +3 -1
- package/radio-group/README.md +2 -2
- package/search/README.md +34 -15
- package/select/README.md +7 -1
- package/signal-forms/README.md +1 -1
- package/stepper/README.md +13 -0
- package/table/README.md +21 -28
- package/time-range-field/README.md +17 -12
- package/toggle/README.md +1 -1
- package/toolbar/README.md +4 -4
- package/tree/README.md +11 -9
- package/types/forty-cdk-accordion.d.ts +3 -20
- package/types/forty-cdk-aspect-ratio.d.ts +11 -20
- package/types/forty-cdk-avatar.d.ts +2 -2
- package/types/forty-cdk-breadcrumbs.d.ts +18 -8
- package/types/forty-cdk-button.d.ts +6 -22
- package/types/forty-cdk-calendar.d.ts +32 -9
- package/types/forty-cdk-checkbox.d.ts +2 -20
- package/types/forty-cdk-combobox.d.ts +66 -9
- package/types/forty-cdk-core.d.ts +655 -307
- package/types/forty-cdk-date-field.d.ts +1 -0
- package/types/forty-cdk-date-picker.d.ts +21 -31
- package/types/forty-cdk-date-range-field.d.ts +1 -0
- package/types/forty-cdk-dialog.d.ts +21 -7
- package/types/forty-cdk-disclosure.d.ts +3 -20
- package/types/forty-cdk-drag-drop.d.ts +42 -7
- package/types/forty-cdk-drawer.d.ts +21 -6
- package/types/forty-cdk-field.d.ts +1 -20
- package/types/forty-cdk-fieldset.d.ts +1 -20
- package/types/forty-cdk-file-upload.d.ts +31 -34
- package/types/forty-cdk-input.d.ts +1 -20
- package/types/forty-cdk-internationalized-date.d.ts +8 -6
- package/types/forty-cdk-listbox.d.ts +34 -2
- package/types/forty-cdk-menu.d.ts +6 -2
- package/types/forty-cdk-menubar.d.ts +6 -7
- package/types/forty-cdk-meter.d.ts +29 -27
- package/types/forty-cdk-navigation-menu.d.ts +7 -2
- package/types/forty-cdk-otp-input.d.ts +6 -23
- package/types/forty-cdk-pane-resizer.d.ts +1 -20
- package/types/forty-cdk-popover.d.ts +18 -2
- package/types/forty-cdk-progress.d.ts +31 -11
- package/types/forty-cdk-radio-group.d.ts +8 -7
- package/types/forty-cdk-search.d.ts +137 -34
- package/types/forty-cdk-select.d.ts +122 -125
- package/types/forty-cdk-separator.d.ts +1 -20
- package/types/forty-cdk-slider.d.ts +7 -17
- package/types/forty-cdk-stepper.d.ts +20 -4
- package/types/forty-cdk-switch.d.ts +1 -20
- package/types/forty-cdk-table.d.ts +58 -39
- package/types/forty-cdk-time-field.d.ts +1 -0
- package/types/forty-cdk-time-picker.d.ts +47 -77
- package/types/forty-cdk-time-range-field.d.ts +30 -5
- package/types/forty-cdk-toggle.d.ts +9 -4
- package/types/forty-cdk-toolbar.d.ts +1 -0
- package/types/forty-cdk-tree.d.ts +21 -1
|
@@ -156,13 +156,15 @@ class ForAvatarImage {
|
|
|
156
156
|
* if the consumer wants to log analytics, swap to a different src on
|
|
157
157
|
* error, etc.
|
|
158
158
|
*/
|
|
159
|
-
|
|
159
|
+
loadStatusChange = output();
|
|
160
160
|
constructor() {
|
|
161
161
|
if (!this.#parent) {
|
|
162
162
|
throw new Error('[forty-cdk/avatar] ForAvatarImage must be used inside a [forAvatar] element.');
|
|
163
163
|
}
|
|
164
164
|
const parent = this.#parent;
|
|
165
165
|
const host = this.#host;
|
|
166
|
+
let destroyed = false;
|
|
167
|
+
inject(DestroyRef).onDestroy(() => (destroyed = true));
|
|
166
168
|
afterNextRender(() => {
|
|
167
169
|
this.#syncFromAttr();
|
|
168
170
|
});
|
|
@@ -181,12 +183,14 @@ class ForAvatarImage {
|
|
|
181
183
|
let last = null;
|
|
182
184
|
let lastReportedToken = -1;
|
|
183
185
|
const emit = (status) => {
|
|
186
|
+
if (destroyed)
|
|
187
|
+
return;
|
|
184
188
|
if (last === status && lastReportedToken === this.#requestToken)
|
|
185
189
|
return;
|
|
186
190
|
last = status;
|
|
187
191
|
lastReportedToken = this.#requestToken;
|
|
188
192
|
parent.reportStatus(status);
|
|
189
|
-
this.
|
|
193
|
+
this.loadStatusChange.emit(status);
|
|
190
194
|
};
|
|
191
195
|
this.#emit = emit;
|
|
192
196
|
}
|
|
@@ -262,7 +266,7 @@ class ForAvatarImage {
|
|
|
262
266
|
/** The `src` the latest `#syncFromAttr` observed, used to match native events. */
|
|
263
267
|
#requestSrc = null;
|
|
264
268
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForAvatarImage, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
265
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForAvatarImage, isStandalone: true, selector: "img[forAvatarImage]", outputs: {
|
|
269
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForAvatarImage, isStandalone: true, selector: "img[forAvatarImage]", outputs: { loadStatusChange: "loadStatusChange" }, host: { listeners: { "load": "onLoad()", "error": "onError()" }, properties: { "attr.data-status": "status()" } }, exportAs: ["forAvatarImage"], ngImport: i0 });
|
|
266
270
|
}
|
|
267
271
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForAvatarImage, decorators: [{
|
|
268
272
|
type: Directive,
|
|
@@ -275,7 +279,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
|
|
|
275
279
|
'(error)': 'onError()',
|
|
276
280
|
},
|
|
277
281
|
}]
|
|
278
|
-
}], ctorParameters: () => [], propDecorators: {
|
|
282
|
+
}], ctorParameters: () => [], propDecorators: { loadStatusChange: [{ type: i0.Output, args: ["loadStatusChange"] }] } });
|
|
279
283
|
|
|
280
284
|
/**
|
|
281
285
|
* Marker for the avatar's fallback content (initials, generic icon, …).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forty-cdk-avatar.mjs","sources":["../../../projects/forty-cdk/avatar/src/avatar-context.ts","../../../projects/forty-cdk/avatar/src/avatar-defaults.ts","../../../projects/forty-cdk/avatar/src/avatar.ts","../../../projects/forty-cdk/avatar/src/avatar-image.ts","../../../projects/forty-cdk/avatar/src/avatar-fallback.ts","../../../projects/forty-cdk/avatar/src/forty-cdk-avatar.ts"],"sourcesContent":["import { inject, InjectionToken, type Signal } from '@angular/core';\n\n/**\n * Lifecycle stages of the avatar's image:\n * - `idle`: no `src` set yet.\n * - `loading`: `src` set, waiting for the network.\n * - `loaded`: image rendered successfully.\n * - `error`: image failed to load.\n */\nexport type ForAvatarStatus = 'idle' | 'loading' | 'loaded' | 'error';\n\nexport interface ForAvatarContext {\n /** Current status of the avatar's image. */\n readonly status: Signal<ForAvatarStatus>;\n /**\n * `true` when the consumer should render the fallback. Drives `@if` in\n * the consumer's template. Honors `fallbackDelayMs` for `idle` / `loading`\n * states; `error` flips it to `true` immediately.\n */\n readonly shouldShowFallback: Signal<boolean>;\n /**\n * Internal — reported by `[forAvatarImage]` so the root can transition\n * its state machine. Consumers should not call this.\n */\n reportStatus(status: ForAvatarStatus): void;\n}\n\nexport const FOR_AVATAR_CONTEXT = new InjectionToken<ForAvatarContext>('FOR_AVATAR_CONTEXT');\n\n/**\n * Injects the nearest {@link ForAvatarContext}, throwing a descriptive error\n * when used outside a `[forAvatar]` element.\n *\n * @param piece Name of the calling directive, used in the error message.\n */\nexport function injectAvatarContext(piece: string): ForAvatarContext {\n const ctx = inject(FOR_AVATAR_CONTEXT, { optional: true });\n if (!ctx) {\n throw new Error(`[forty-cdk/avatar] ${piece} must be used inside a [forAvatar] element.`);\n }\n return ctx;\n}\n","import { type Provider } from '@angular/core';\n\nimport { createDefaults } from 'forty-cdk/core';\n\n/**\n * Defaults inherited by descendant avatars in the surrounding injector\n * scope. Configure with `provideForAvatarDefaults` either at the\n * application root or in any component's `providers` array; partial\n * overrides merge with the parent scope.\n */\nexport interface ForAvatarDefaults {\n /**\n * Milliseconds to defer the fallback for fast loads, avoiding a brief\n * \"initials → image\" flicker. Set to `0` to show immediately during\n * `idle` / `loading`, or to e.g. `500` to skip rendering during quick\n * cached loads. `error` always shows the fallback immediately.\n */\n fallbackDelayMs: number;\n}\n\n/**\n * Library fallback for avatar defaults, read at the root injector when no\n * consumer has called `provideForAvatarDefaults`. Exported for the shared defaults\n * contract spec; not re-exported from the primitive's public entry.\n */\nexport const FOR_AVATAR_FALLBACK_DEFAULTS: ForAvatarDefaults = {\n fallbackDelayMs: 0,\n};\n\nconst { token, provideDefaults } = createDefaults<ForAvatarDefaults>(\n 'FOR_AVATAR_DEFAULTS',\n FOR_AVATAR_FALLBACK_DEFAULTS,\n);\n\n/** Token holding the resolved avatar defaults for the current scope. */\nexport const FOR_AVATAR_DEFAULTS = token;\n\n/**\n * Configures forty-cdk avatar defaults for this injector scope. Partial\n * overrides inherit unspecified keys from the parent scope (or library\n * defaults at the root).\n */\nexport function provideForAvatarDefaults(defaults: Partial<ForAvatarDefaults> = {}): Provider[] {\n return provideDefaults(defaults);\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n computed,\n DestroyRef,\n Directive,\n effect,\n inject,\n input,\n linkedSignal,\n numberAttribute,\n PLATFORM_ID,\n signal,\n} from '@angular/core';\n\nimport { FOR_AVATAR_CONTEXT, type ForAvatarContext, type ForAvatarStatus } from './avatar-context';\nimport { FOR_AVATAR_DEFAULTS } from './avatar-defaults';\n\n/**\n * Headless avatar root. Tracks the load lifecycle of an inner\n * `[forAvatarImage]` and exposes `status` + `shouldShowFallback` so the\n * consumer can swap the image for fallback content (initials, generic icon)\n * via `@if`.\n *\n * No ARIA role is imposed — avatars are typically presentational. If the\n * surrounding component conveys identity (a person's name in the header,\n * a chat bubble), the consumer should provide an `aria-label` on the\n * relevant ancestor or pair the avatar with visible text.\n *\n * @example\n * ```html\n * <span forAvatar #a=\"forAvatar\" fallbackDelayMs=\"500\">\n * <img forAvatarImage [src]=\"user.avatarUrl\" [alt]=\"user.name\" />\n * @if (a.shouldShowFallback()) {\n * <span forAvatarFallback>{{ user.initials }}</span>\n * }\n * </span>\n * ```\n */\n@Directive({\n selector: '[forAvatar]',\n exportAs: 'forAvatar',\n host: {\n '[attr.data-status]': 'status()',\n },\n providers: [{ provide: FOR_AVATAR_CONTEXT, useExisting: ForAvatar }],\n})\nexport class ForAvatar implements ForAvatarContext {\n readonly #defaults = inject(FOR_AVATAR_DEFAULTS);\n\n /**\n * Milliseconds to defer the fallback for fast loads, avoiding a brief\n * \"initials → image\" flicker. Set to `0` (default) to show immediately\n * during `idle` / `loading`, or to e.g. `500` to skip rendering during\n * quick cached loads. `error` always shows the fallback immediately.\n * The default is read from `provideForAvatarDefaults` for the\n * surrounding scope.\n */\n readonly fallbackDelayMs = input(this.#defaults.fallbackDelayMs, {\n transform: numberAttribute,\n });\n\n readonly #status = signal<ForAvatarStatus>('idle');\n readonly status = this.#status.asReadonly();\n\n // Whether the deferred-fallback timer has fired for the current status/delay.\n // `linkedSignal` resets it to `false` whenever the source changes (the\n // canonical replacement for resetting state inside an effect); the timer\n // callback in the constructor flips it to `true`.\n readonly #timerElapsed = linkedSignal<{ status: ForAvatarStatus; delay: number }, boolean>({\n source: () => ({ status: this.#status(), delay: this.fallbackDelayMs() }),\n computation: () => false,\n });\n #timer: ReturnType<typeof setTimeout> | null = null;\n readonly #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n\n readonly shouldShowFallback = computed(() => {\n const status = this.#status();\n if (status === 'loaded') return false;\n if (status === 'error') return true;\n // idle | loading: an immediate (delay <= 0) gate shows the fallback right\n // away; a deferred gate waits for the timer armed in the effect below.\n if (this.fallbackDelayMs() <= 0) return true;\n return this.#timerElapsed();\n });\n\n /** @internal */\n reportStatus(status: ForAvatarStatus): void {\n this.#status.set(status);\n }\n\n constructor() {\n // Imperative timer integration: the effect's only job is to (re)arm a\n // setTimeout for the deferred-fallback case (idle | loading with a positive\n // delay). The synchronous branches (loaded / error / delay <= 0) are pure\n // derivations and live in `shouldShowFallback`. The signal write happens\n // inside the timer callback, which runs outside the effect's reactive\n // scope, so this does NOT create a self-cycle in the reactive graph.\n effect(() => {\n const status = this.#status();\n const delay = this.fallbackDelayMs();\n this.#cancelTimer();\n if (this.#isBrowser && (status === 'idle' || status === 'loading') && delay > 0) {\n this.#timer = setTimeout(() => {\n this.#timer = null;\n this.#timerElapsed.set(true);\n }, delay);\n }\n });\n\n inject(DestroyRef).onDestroy(() => this.#cancelTimer());\n }\n\n #cancelTimer(): void {\n if (this.#timer !== null) {\n clearTimeout(this.#timer);\n this.#timer = null;\n }\n }\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n afterNextRender,\n DestroyRef,\n Directive,\n ElementRef,\n inject,\n output,\n PLATFORM_ID,\n} from '@angular/core';\n\nimport { FOR_AVATAR_CONTEXT, type ForAvatarStatus } from './avatar-context';\n\n/**\n * Wraps an `<img>` and reports its load lifecycle to the surrounding\n * `[forAvatar]`. The native `src` / `alt` bindings are owned by the\n * consumer — this directive only observes.\n *\n * Reflects `data-status` on the image element so the consumer can hide it\n * via CSS while loading or after an error (e.g. `img[data-status=\"error\"]\n * { display: none }`).\n */\n@Directive({\n selector: 'img[forAvatarImage]',\n exportAs: 'forAvatarImage',\n host: {\n '[attr.data-status]': 'status()',\n '(load)': 'onLoad()',\n '(error)': 'onError()',\n },\n})\nexport class ForAvatarImage {\n readonly #host = inject<ElementRef<HTMLImageElement>>(ElementRef).nativeElement;\n readonly #parent = inject(FOR_AVATAR_CONTEXT, { optional: true });\n\n /**\n * Emits whenever the image lifecycle transitions to a new state. Useful\n * if the consumer wants to log analytics, swap to a different src on\n * error, etc.\n */\n readonly loadStatusChanged = output<ForAvatarStatus>();\n\n constructor() {\n if (!this.#parent) {\n throw new Error(\n '[forty-cdk/avatar] ForAvatarImage must be used inside a [forAvatar] element.',\n );\n }\n const parent = this.#parent;\n const host = this.#host;\n\n afterNextRender(() => {\n this.#syncFromAttr();\n });\n\n if (isPlatformBrowser(inject(PLATFORM_ID)) && typeof MutationObserver !== 'undefined') {\n const observer = new MutationObserver(() => {\n this.#syncFromAttr();\n });\n observer.observe(host, { attributes: true, attributeFilter: ['src'] });\n inject(DestroyRef).onDestroy(() => observer.disconnect());\n }\n\n // Re-emit upstream whenever the parent's status changes due to our writes.\n // De-dupe on the (request, status) pair rather than status alone: a new\n // `src` opens a fresh request token, so an identical status (e.g. a second\n // image that also `loaded`) still re-reports for the new request, while a\n // repeated status within the same request is suppressed.\n let last: ForAvatarStatus | null = null;\n let lastReportedToken = -1;\n const emit = (status: ForAvatarStatus): void => {\n if (last === status && lastReportedToken === this.#requestToken) return;\n last = status;\n lastReportedToken = this.#requestToken;\n parent.reportStatus(status);\n this.loadStatusChanged.emit(status);\n };\n this.#emit = emit;\n }\n\n protected status(): ForAvatarStatus {\n return this.#parent!.status();\n }\n\n protected onLoad(): void {\n // Ignore a `load` that fires for a request the host has already moved past\n // (the native event can arrive after `src` was reassigned).\n if (!this.#isCurrentRequest()) return;\n this.#emit('loaded');\n }\n\n protected onError(): void {\n if (!this.#isCurrentRequest()) return;\n this.#emit('error');\n }\n\n /**\n * True when the `src` the host currently carries is the one the latest\n * `#syncFromAttr` observed, so a native load/error event belongs to the\n * in-flight request rather than a superseded one.\n */\n #isCurrentRequest(): boolean {\n return this.#host.getAttribute('src') === this.#requestSrc;\n }\n\n #syncFromAttr(): void {\n const src = this.#host.getAttribute('src');\n this.#requestToken++;\n this.#requestSrc = src;\n if (!src) {\n this.#emit('idle');\n return;\n }\n if (this.#host.complete) {\n // Cached image: load/error events may not fire — derive from naturalWidth.\n if (this.#host.naturalWidth > 0) {\n this.#emit('loaded');\n return;\n }\n // A complete image with zero intrinsic width is ambiguous: it may be a\n // broken image OR a valid SVG with no intrinsic dimensions. Don't\n // pessimistically flag `error` and hide a valid image — verify with\n // `decode()`, which resolves for a decodable (even zero-size) image and\n // rejects for a broken one. Stay `loading` until it settles.\n this.#emit('loading');\n this.#verifyCachedZeroSize(src);\n return;\n }\n this.#emit('loading');\n }\n\n #verifyCachedZeroSize(src: string): void {\n const decode = this.#host.decode?.bind(this.#host);\n if (!decode) {\n // No `decode()` (older engines / jsdom): keep the legacy heuristic.\n this.#emit('error');\n return;\n }\n decode().then(\n () => {\n if (this.#host.getAttribute('src') === src) {\n this.#emit('loaded');\n }\n },\n () => {\n if (this.#host.getAttribute('src') === src) {\n this.#emit('error');\n }\n },\n );\n }\n\n // Initialized in constructor.\n #emit!: (status: ForAvatarStatus) => void;\n\n /** Monotonic id bumped on every `src` change so stale events are ignored. */\n #requestToken = 0;\n /** The `src` the latest `#syncFromAttr` observed, used to match native events. */\n #requestSrc: string | null = null;\n}\n","import { Directive } from '@angular/core';\n\nimport { injectAvatarContext } from './avatar-context';\n\n/**\n * Marker for the avatar's fallback content (initials, generic icon, …).\n * Reflects `data-status` from the parent `[forAvatar]` so the consumer can\n * style for the `error` state separately from `idle` / `loading` if needed.\n *\n * The decision of *whether* to render the fallback lives on the root —\n * drive `@if` with `forAvatar.shouldShowFallback()`.\n */\n@Directive({\n selector: '[forAvatarFallback]',\n exportAs: 'forAvatarFallback',\n host: {\n '[attr.data-status]': 'parent.status()',\n },\n})\nexport class ForAvatarFallback {\n protected readonly parent = injectAvatarContext('ForAvatarFallback');\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"forty-cdk-avatar.mjs","sources":["../../../projects/forty-cdk/avatar/src/avatar-context.ts","../../../projects/forty-cdk/avatar/src/avatar-defaults.ts","../../../projects/forty-cdk/avatar/src/avatar.ts","../../../projects/forty-cdk/avatar/src/avatar-image.ts","../../../projects/forty-cdk/avatar/src/avatar-fallback.ts","../../../projects/forty-cdk/avatar/src/forty-cdk-avatar.ts"],"sourcesContent":["import { inject, InjectionToken, type Signal } from '@angular/core';\n\n/**\n * Lifecycle stages of the avatar's image:\n * - `idle`: no `src` set yet.\n * - `loading`: `src` set, waiting for the network.\n * - `loaded`: image rendered successfully.\n * - `error`: image failed to load.\n */\nexport type ForAvatarStatus = 'idle' | 'loading' | 'loaded' | 'error';\n\nexport interface ForAvatarContext {\n /** Current status of the avatar's image. */\n readonly status: Signal<ForAvatarStatus>;\n /**\n * `true` when the consumer should render the fallback. Drives `@if` in\n * the consumer's template. Honors `fallbackDelayMs` for `idle` / `loading`\n * states; `error` flips it to `true` immediately.\n */\n readonly shouldShowFallback: Signal<boolean>;\n /**\n * Internal — reported by `[forAvatarImage]` so the root can transition\n * its state machine. Consumers should not call this.\n */\n reportStatus(status: ForAvatarStatus): void;\n}\n\nexport const FOR_AVATAR_CONTEXT = new InjectionToken<ForAvatarContext>('FOR_AVATAR_CONTEXT');\n\n/**\n * Injects the nearest {@link ForAvatarContext}, throwing a descriptive error\n * when used outside a `[forAvatar]` element.\n *\n * @param piece Name of the calling directive, used in the error message.\n */\nexport function injectAvatarContext(piece: string): ForAvatarContext {\n const ctx = inject(FOR_AVATAR_CONTEXT, { optional: true });\n if (!ctx) {\n throw new Error(`[forty-cdk/avatar] ${piece} must be used inside a [forAvatar] element.`);\n }\n return ctx;\n}\n","import { type Provider } from '@angular/core';\n\nimport { createDefaults } from 'forty-cdk/core';\n\n/**\n * Defaults inherited by descendant avatars in the surrounding injector\n * scope. Configure with `provideForAvatarDefaults` either at the\n * application root or in any component's `providers` array; partial\n * overrides merge with the parent scope.\n */\nexport interface ForAvatarDefaults {\n /**\n * Milliseconds to defer the fallback for fast loads, avoiding a brief\n * \"initials → image\" flicker. Set to `0` to show immediately during\n * `idle` / `loading`, or to e.g. `500` to skip rendering during quick\n * cached loads. `error` always shows the fallback immediately.\n */\n fallbackDelayMs: number;\n}\n\n/**\n * Library fallback for avatar defaults, read at the root injector when no\n * consumer has called `provideForAvatarDefaults`. Exported for the shared defaults\n * contract spec; not re-exported from the primitive's public entry.\n */\nexport const FOR_AVATAR_FALLBACK_DEFAULTS: ForAvatarDefaults = {\n fallbackDelayMs: 0,\n};\n\nconst { token, provideDefaults } = createDefaults<ForAvatarDefaults>(\n 'FOR_AVATAR_DEFAULTS',\n FOR_AVATAR_FALLBACK_DEFAULTS,\n);\n\n/** Token holding the resolved avatar defaults for the current scope. */\nexport const FOR_AVATAR_DEFAULTS = token;\n\n/**\n * Configures forty-cdk avatar defaults for this injector scope. Partial\n * overrides inherit unspecified keys from the parent scope (or library\n * defaults at the root).\n */\nexport function provideForAvatarDefaults(defaults: Partial<ForAvatarDefaults> = {}): Provider[] {\n return provideDefaults(defaults);\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n computed,\n DestroyRef,\n Directive,\n effect,\n inject,\n input,\n linkedSignal,\n numberAttribute,\n PLATFORM_ID,\n signal,\n} from '@angular/core';\n\nimport { FOR_AVATAR_CONTEXT, type ForAvatarContext, type ForAvatarStatus } from './avatar-context';\nimport { FOR_AVATAR_DEFAULTS } from './avatar-defaults';\n\n/**\n * Headless avatar root. Tracks the load lifecycle of an inner\n * `[forAvatarImage]` and exposes `status` + `shouldShowFallback` so the\n * consumer can swap the image for fallback content (initials, generic icon)\n * via `@if`.\n *\n * No ARIA role is imposed — avatars are typically presentational. If the\n * surrounding component conveys identity (a person's name in the header,\n * a chat bubble), the consumer should provide an `aria-label` on the\n * relevant ancestor or pair the avatar with visible text.\n *\n * @example\n * ```html\n * <span forAvatar #a=\"forAvatar\" fallbackDelayMs=\"500\">\n * <img forAvatarImage [src]=\"user.avatarUrl\" [alt]=\"user.name\" />\n * @if (a.shouldShowFallback()) {\n * <span forAvatarFallback>{{ user.initials }}</span>\n * }\n * </span>\n * ```\n */\n@Directive({\n selector: '[forAvatar]',\n exportAs: 'forAvatar',\n host: {\n '[attr.data-status]': 'status()',\n },\n providers: [{ provide: FOR_AVATAR_CONTEXT, useExisting: ForAvatar }],\n})\nexport class ForAvatar implements ForAvatarContext {\n readonly #defaults = inject(FOR_AVATAR_DEFAULTS);\n\n /**\n * Milliseconds to defer the fallback for fast loads, avoiding a brief\n * \"initials → image\" flicker. Set to `0` (default) to show immediately\n * during `idle` / `loading`, or to e.g. `500` to skip rendering during\n * quick cached loads. `error` always shows the fallback immediately.\n * The default is read from `provideForAvatarDefaults` for the\n * surrounding scope.\n */\n readonly fallbackDelayMs = input(this.#defaults.fallbackDelayMs, {\n transform: numberAttribute,\n });\n\n readonly #status = signal<ForAvatarStatus>('idle');\n readonly status = this.#status.asReadonly();\n\n // Whether the deferred-fallback timer has fired for the current status/delay.\n // `linkedSignal` resets it to `false` whenever the source changes (the\n // canonical replacement for resetting state inside an effect); the timer\n // callback in the constructor flips it to `true`.\n readonly #timerElapsed = linkedSignal<{ status: ForAvatarStatus; delay: number }, boolean>({\n source: () => ({ status: this.#status(), delay: this.fallbackDelayMs() }),\n computation: () => false,\n });\n #timer: ReturnType<typeof setTimeout> | null = null;\n readonly #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n\n readonly shouldShowFallback = computed(() => {\n const status = this.#status();\n if (status === 'loaded') return false;\n if (status === 'error') return true;\n // idle | loading: an immediate (delay <= 0) gate shows the fallback right\n // away; a deferred gate waits for the timer armed in the effect below.\n if (this.fallbackDelayMs() <= 0) return true;\n return this.#timerElapsed();\n });\n\n /** @internal */\n reportStatus(status: ForAvatarStatus): void {\n this.#status.set(status);\n }\n\n constructor() {\n // Imperative timer integration: the effect's only job is to (re)arm a\n // setTimeout for the deferred-fallback case (idle | loading with a positive\n // delay). The synchronous branches (loaded / error / delay <= 0) are pure\n // derivations and live in `shouldShowFallback`. The signal write happens\n // inside the timer callback, which runs outside the effect's reactive\n // scope, so this does NOT create a self-cycle in the reactive graph.\n effect(() => {\n const status = this.#status();\n const delay = this.fallbackDelayMs();\n this.#cancelTimer();\n if (this.#isBrowser && (status === 'idle' || status === 'loading') && delay > 0) {\n this.#timer = setTimeout(() => {\n this.#timer = null;\n this.#timerElapsed.set(true);\n }, delay);\n }\n });\n\n inject(DestroyRef).onDestroy(() => this.#cancelTimer());\n }\n\n #cancelTimer(): void {\n if (this.#timer !== null) {\n clearTimeout(this.#timer);\n this.#timer = null;\n }\n }\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n afterNextRender,\n DestroyRef,\n Directive,\n ElementRef,\n inject,\n output,\n PLATFORM_ID,\n} from '@angular/core';\n\nimport { FOR_AVATAR_CONTEXT, type ForAvatarStatus } from './avatar-context';\n\n/**\n * Wraps an `<img>` and reports its load lifecycle to the surrounding\n * `[forAvatar]`. The native `src` / `alt` bindings are owned by the\n * consumer — this directive only observes.\n *\n * Reflects `data-status` on the image element so the consumer can hide it\n * via CSS while loading or after an error (e.g. `img[data-status=\"error\"]\n * { display: none }`).\n */\n@Directive({\n selector: 'img[forAvatarImage]',\n exportAs: 'forAvatarImage',\n host: {\n '[attr.data-status]': 'status()',\n '(load)': 'onLoad()',\n '(error)': 'onError()',\n },\n})\nexport class ForAvatarImage {\n readonly #host = inject<ElementRef<HTMLImageElement>>(ElementRef).nativeElement;\n readonly #parent = inject(FOR_AVATAR_CONTEXT, { optional: true });\n\n /**\n * Emits whenever the image lifecycle transitions to a new state. Useful\n * if the consumer wants to log analytics, swap to a different src on\n * error, etc.\n */\n readonly loadStatusChange = output<ForAvatarStatus>();\n\n constructor() {\n if (!this.#parent) {\n throw new Error(\n '[forty-cdk/avatar] ForAvatarImage must be used inside a [forAvatar] element.',\n );\n }\n const parent = this.#parent;\n const host = this.#host;\n\n let destroyed = false;\n inject(DestroyRef).onDestroy(() => (destroyed = true));\n\n afterNextRender(() => {\n this.#syncFromAttr();\n });\n\n if (isPlatformBrowser(inject(PLATFORM_ID)) && typeof MutationObserver !== 'undefined') {\n const observer = new MutationObserver(() => {\n this.#syncFromAttr();\n });\n observer.observe(host, { attributes: true, attributeFilter: ['src'] });\n inject(DestroyRef).onDestroy(() => observer.disconnect());\n }\n\n // Re-emit upstream whenever the parent's status changes due to our writes.\n // De-dupe on the (request, status) pair rather than status alone: a new\n // `src` opens a fresh request token, so an identical status (e.g. a second\n // image that also `loaded`) still re-reports for the new request, while a\n // repeated status within the same request is suppressed.\n let last: ForAvatarStatus | null = null;\n let lastReportedToken = -1;\n const emit = (status: ForAvatarStatus): void => {\n if (destroyed) return;\n if (last === status && lastReportedToken === this.#requestToken) return;\n last = status;\n lastReportedToken = this.#requestToken;\n parent.reportStatus(status);\n this.loadStatusChange.emit(status);\n };\n this.#emit = emit;\n }\n\n protected status(): ForAvatarStatus {\n return this.#parent!.status();\n }\n\n protected onLoad(): void {\n // Ignore a `load` that fires for a request the host has already moved past\n // (the native event can arrive after `src` was reassigned).\n if (!this.#isCurrentRequest()) return;\n this.#emit('loaded');\n }\n\n protected onError(): void {\n if (!this.#isCurrentRequest()) return;\n this.#emit('error');\n }\n\n /**\n * True when the `src` the host currently carries is the one the latest\n * `#syncFromAttr` observed, so a native load/error event belongs to the\n * in-flight request rather than a superseded one.\n */\n #isCurrentRequest(): boolean {\n return this.#host.getAttribute('src') === this.#requestSrc;\n }\n\n #syncFromAttr(): void {\n const src = this.#host.getAttribute('src');\n this.#requestToken++;\n this.#requestSrc = src;\n if (!src) {\n this.#emit('idle');\n return;\n }\n if (this.#host.complete) {\n // Cached image: load/error events may not fire — derive from naturalWidth.\n if (this.#host.naturalWidth > 0) {\n this.#emit('loaded');\n return;\n }\n // A complete image with zero intrinsic width is ambiguous: it may be a\n // broken image OR a valid SVG with no intrinsic dimensions. Don't\n // pessimistically flag `error` and hide a valid image — verify with\n // `decode()`, which resolves for a decodable (even zero-size) image and\n // rejects for a broken one. Stay `loading` until it settles.\n this.#emit('loading');\n this.#verifyCachedZeroSize(src);\n return;\n }\n this.#emit('loading');\n }\n\n #verifyCachedZeroSize(src: string): void {\n const decode = this.#host.decode?.bind(this.#host);\n if (!decode) {\n // No `decode()` (older engines / jsdom): keep the legacy heuristic.\n this.#emit('error');\n return;\n }\n decode().then(\n () => {\n if (this.#host.getAttribute('src') === src) {\n this.#emit('loaded');\n }\n },\n () => {\n if (this.#host.getAttribute('src') === src) {\n this.#emit('error');\n }\n },\n );\n }\n\n // Initialized in constructor.\n #emit!: (status: ForAvatarStatus) => void;\n\n /** Monotonic id bumped on every `src` change so stale events are ignored. */\n #requestToken = 0;\n /** The `src` the latest `#syncFromAttr` observed, used to match native events. */\n #requestSrc: string | null = null;\n}\n","import { Directive } from '@angular/core';\n\nimport { injectAvatarContext } from './avatar-context';\n\n/**\n * Marker for the avatar's fallback content (initials, generic icon, …).\n * Reflects `data-status` from the parent `[forAvatar]` so the consumer can\n * style for the `error` state separately from `idle` / `loading` if needed.\n *\n * The decision of *whether* to render the fallback lives on the root —\n * drive `@if` with `forAvatar.shouldShowFallback()`.\n */\n@Directive({\n selector: '[forAvatarFallback]',\n exportAs: 'forAvatarFallback',\n host: {\n '[attr.data-status]': 'parent.status()',\n },\n})\nexport class ForAvatarFallback {\n protected readonly parent = injectAvatarContext('ForAvatarFallback');\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, Directive, booleanAttribute } from '@angular/core';
|
|
2
|
+
import { inject, input, Directive, booleanAttribute } from '@angular/core';
|
|
3
3
|
import { createDefaults } from 'forty-cdk/core';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Library fallback for breadcrumbs defaults, read at the root injector when no
|
|
7
|
+
* consumer has called `provideForBreadcrumbsDefaults`. Exported for the shared
|
|
8
|
+
* defaults contract spec; not re-exported from the primitive's public entry.
|
|
9
|
+
*/
|
|
10
|
+
const FOR_BREADCRUMBS_FALLBACK_DEFAULTS = {
|
|
11
|
+
label: 'Breadcrumb',
|
|
12
|
+
};
|
|
13
|
+
const { token, provideDefaults } = createDefaults('FOR_BREADCRUMBS_DEFAULTS', FOR_BREADCRUMBS_FALLBACK_DEFAULTS);
|
|
14
|
+
/** Token holding the resolved breadcrumbs defaults for the current scope. */
|
|
15
|
+
const FOR_BREADCRUMBS_DEFAULTS = token;
|
|
16
|
+
/**
|
|
17
|
+
* Configures forty-cdk breadcrumbs defaults for this injector scope. Partial
|
|
18
|
+
* overrides inherit unspecified keys from the parent scope (or library
|
|
19
|
+
* defaults at the root).
|
|
20
|
+
*/
|
|
21
|
+
function provideForBreadcrumbsDefaults(defaults = {}) {
|
|
22
|
+
return provideDefaults(defaults);
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
/**
|
|
6
26
|
* Headless breadcrumb trail implementing the
|
|
7
27
|
* [WAI-ARIA Breadcrumb pattern](https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/):
|
|
@@ -32,17 +52,21 @@ import { createDefaults } from 'forty-cdk/core';
|
|
|
32
52
|
* ```
|
|
33
53
|
*/
|
|
34
54
|
class ForBreadcrumbs {
|
|
55
|
+
#defaults = inject(FOR_BREADCRUMBS_DEFAULTS);
|
|
35
56
|
/**
|
|
36
|
-
* Accessible label for the navigation landmark. Defaults to
|
|
37
|
-
*
|
|
38
|
-
*
|
|
57
|
+
* Accessible label for the navigation landmark. Defaults to the scope's
|
|
58
|
+
* `label` (`'Breadcrumb'` unless overridden via
|
|
59
|
+
* `provideForBreadcrumbsDefaults`), so a bare `<nav forBreadcrumbs>` is
|
|
60
|
+
* already a correctly labelled landmark — the conventional name for this
|
|
61
|
+
* pattern. Override it per-instance via `[ariaLabel]` (or point a native
|
|
39
62
|
* `aria-labelledby` at a visible heading) when the page hosts more than one
|
|
40
|
-
* breadcrumb trail and they need to be told apart
|
|
63
|
+
* breadcrumb trail and they need to be told apart; set it to `null` to drop
|
|
64
|
+
* the attribute and rely on `aria-labelledby`.
|
|
41
65
|
*/
|
|
42
|
-
ariaLabel = input(
|
|
66
|
+
ariaLabel = input(this.#defaults.label, /* @ts-ignore */
|
|
43
67
|
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
44
68
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForBreadcrumbs, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
45
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForBreadcrumbs, isStandalone: true, selector: "[forBreadcrumbs]", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "navigation" }, properties: { "attr.aria-label": "ariaLabel()
|
|
69
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForBreadcrumbs, isStandalone: true, selector: "[forBreadcrumbs]", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "navigation" }, properties: { "attr.aria-label": "ariaLabel()" } }, exportAs: ["forBreadcrumbs"], ngImport: i0 });
|
|
46
70
|
}
|
|
47
71
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForBreadcrumbs, decorators: [{
|
|
48
72
|
type: Directive,
|
|
@@ -51,7 +75,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
|
|
|
51
75
|
exportAs: 'forBreadcrumbs',
|
|
52
76
|
host: {
|
|
53
77
|
role: 'navigation',
|
|
54
|
-
'[attr.aria-label]':
|
|
78
|
+
'[attr.aria-label]': 'ariaLabel()',
|
|
55
79
|
},
|
|
56
80
|
}]
|
|
57
81
|
}], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
|
|
@@ -104,19 +128,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
|
|
|
104
128
|
}]
|
|
105
129
|
}] });
|
|
106
130
|
|
|
107
|
-
const FALLBACK = {};
|
|
108
|
-
const { token, provideDefaults } = createDefaults('FOR_BREADCRUMBS_DEFAULTS', FALLBACK);
|
|
109
|
-
/** Token holding the resolved breadcrumbs defaults for the current scope. */
|
|
110
|
-
const FOR_BREADCRUMBS_DEFAULTS = token;
|
|
111
|
-
/**
|
|
112
|
-
* Configures forty-cdk breadcrumbs defaults for this injector scope. Partial
|
|
113
|
-
* overrides inherit unspecified keys from the parent scope (or library
|
|
114
|
-
* defaults at the root).
|
|
115
|
-
*/
|
|
116
|
-
function provideForBreadcrumbsDefaults(defaults = {}) {
|
|
117
|
-
return provideDefaults(defaults);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
131
|
/**
|
|
121
132
|
* Generated bundle index. Do not edit.
|
|
122
133
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forty-cdk-breadcrumbs.mjs","sources":["../../../projects/forty-cdk/breadcrumbs/src/breadcrumbs.ts","../../../projects/forty-cdk/breadcrumbs/src/
|
|
1
|
+
{"version":3,"file":"forty-cdk-breadcrumbs.mjs","sources":["../../../projects/forty-cdk/breadcrumbs/src/breadcrumbs-defaults.ts","../../../projects/forty-cdk/breadcrumbs/src/breadcrumbs.ts","../../../projects/forty-cdk/breadcrumbs/src/breadcrumb-item.ts","../../../projects/forty-cdk/breadcrumbs/src/breadcrumb-separator.ts","../../../projects/forty-cdk/breadcrumbs/src/forty-cdk-breadcrumbs.ts"],"sourcesContent":["import { type Provider } from '@angular/core';\n\nimport { createDefaults } from 'forty-cdk/core';\n\n/**\n * Defaults inherited by descendant breadcrumb trails in the surrounding\n * injector scope. Configure with `provideForBreadcrumbsDefaults` either at the\n * application root or in any component's `providers` array; partial overrides\n * merge with the parent scope.\n */\nexport interface ForBreadcrumbsDefaults {\n /**\n * Accessible name for the breadcrumb `navigation` landmark, for trails that\n * don't set `[ariaLabel]` locally. Localize it here to translate every\n * breadcrumb landmark in the scope.\n */\n label: string;\n}\n\n/**\n * Library fallback for breadcrumbs defaults, read at the root injector when no\n * consumer has called `provideForBreadcrumbsDefaults`. Exported for the shared\n * defaults contract spec; not re-exported from the primitive's public entry.\n */\nexport const FOR_BREADCRUMBS_FALLBACK_DEFAULTS: ForBreadcrumbsDefaults = {\n label: 'Breadcrumb',\n};\n\nconst { token, provideDefaults } = createDefaults<ForBreadcrumbsDefaults>(\n 'FOR_BREADCRUMBS_DEFAULTS',\n FOR_BREADCRUMBS_FALLBACK_DEFAULTS,\n);\n\n/** Token holding the resolved breadcrumbs defaults for the current scope. */\nexport const FOR_BREADCRUMBS_DEFAULTS = token;\n\n/**\n * Configures forty-cdk breadcrumbs defaults for this injector scope. Partial\n * overrides inherit unspecified keys from the parent scope (or library\n * defaults at the root).\n */\nexport function provideForBreadcrumbsDefaults(\n defaults: Partial<ForBreadcrumbsDefaults> = {},\n): Provider[] {\n return provideDefaults(defaults);\n}\n","import { Directive, inject, input } from '@angular/core';\n\nimport { FOR_BREADCRUMBS_DEFAULTS } from './breadcrumbs-defaults';\n\n/**\n * Headless breadcrumb trail implementing the\n * [WAI-ARIA Breadcrumb pattern](https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/):\n * a `navigation` landmark wrapping a set of links that reveal the page's\n * position within the site hierarchy.\n *\n * The directive contributes only the landmark role + accessible name; the\n * consumer owns the markup structure (an `<ol>` of `<li>` links is the APG\n * recommendation) and applies their own styles. Each link decorated with\n * `[forBreadcrumbItem]` can mark itself the current page via `current`, and\n * visual dividers decorated with `[forBreadcrumbSeparator]` are hidden from\n * assistive technology.\n *\n * There is no keyboard interaction or focus management beyond what the native\n * links provide — breadcrumb links are ordinary tab stops.\n *\n * @example\n * ```html\n * <nav forBreadcrumbs>\n * <ol>\n * <li><a forBreadcrumbItem href=\"/\">Home</a></li>\n * <li forBreadcrumbSeparator aria-hidden=\"true\">/</li>\n * <li><a forBreadcrumbItem href=\"/library\">Library</a></li>\n * <li forBreadcrumbSeparator aria-hidden=\"true\">/</li>\n * <li><a forBreadcrumbItem href=\"/library/data\" current>Data</a></li>\n * </ol>\n * </nav>\n * ```\n */\n@Directive({\n selector: '[forBreadcrumbs]',\n exportAs: 'forBreadcrumbs',\n host: {\n role: 'navigation',\n '[attr.aria-label]': 'ariaLabel()',\n },\n})\nexport class ForBreadcrumbs {\n readonly #defaults = inject(FOR_BREADCRUMBS_DEFAULTS);\n\n /**\n * Accessible label for the navigation landmark. Defaults to the scope's\n * `label` (`'Breadcrumb'` unless overridden via\n * `provideForBreadcrumbsDefaults`), so a bare `<nav forBreadcrumbs>` is\n * already a correctly labelled landmark — the conventional name for this\n * pattern. Override it per-instance via `[ariaLabel]` (or point a native\n * `aria-labelledby` at a visible heading) when the page hosts more than one\n * breadcrumb trail and they need to be told apart; set it to `null` to drop\n * the attribute and rely on `aria-labelledby`.\n */\n readonly ariaLabel = input<string | null>(this.#defaults.label);\n}\n","import { booleanAttribute, Directive, input } from '@angular/core';\n\n/**\n * A single link within a breadcrumb trail. Apply on an `<a>` so navigation is\n * native. Set `current` on the link that points to the page the user is on —\n * it reflects [`aria-current=\"page\"`](https://www.w3.org/TR/wai-aria-1.2/#aria-current)\n * so assistive technology announces it as the current location.\n */\n@Directive({\n selector: '[forBreadcrumbItem]',\n exportAs: 'forBreadcrumbItem',\n host: {\n '[attr.aria-current]': 'current() ? \"page\" : null',\n },\n})\nexport class ForBreadcrumbItem {\n /**\n * Whether this item represents the page the user is currently on. The\n * current item reflects `aria-current=\"page\"`; all others emit no\n * `aria-current` attribute.\n */\n readonly current = input(false, { transform: booleanAttribute });\n}\n","import { Directive } from '@angular/core';\n\n/**\n * Decorative divider rendered between breadcrumb items (a `/`, `>`, chevron,\n * etc.). Reflects `aria-hidden=\"true\"` so assistive technology skips it — the\n * trail's structure is already conveyed by the list and the links, making the\n * visual separator redundant in the accessibility tree.\n */\n@Directive({\n selector: '[forBreadcrumbSeparator]',\n exportAs: 'forBreadcrumbSeparator',\n host: {\n 'aria-hidden': 'true',\n },\n})\nexport class ForBreadcrumbSeparator {}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,223 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject,
|
|
3
|
-
import {
|
|
2
|
+
import { inject, DOCUMENT, PLATFORM_ID, signal, DestroyRef, Injectable, ElementRef, computed, input, booleanAttribute, output, Directive } from '@angular/core';
|
|
3
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Application-scoped tracker for the last input modality the user employed —
|
|
7
|
+
* the in-house implementation of input-modality (pointer vs keyboard)
|
|
8
|
+
* detection. Created once per Angular bootstrap (one per SSR
|
|
9
|
+
* request), tied to the root injector lifetime.
|
|
10
|
+
*
|
|
11
|
+
* It installs exactly **one** capture-phase `keydown` and **one** capture-phase
|
|
12
|
+
* `pointerdown` listener on `document`, regardless of how many primitives read
|
|
13
|
+
* its state — every `injectFocusVisible()` consumer shares this single
|
|
14
|
+
* singleton, so the document is never listened to more than once. Capture phase
|
|
15
|
+
* is used so the modality is settled before any overlay content can stop
|
|
16
|
+
* propagation.
|
|
17
|
+
*
|
|
18
|
+
* Why a service rather than module-level state:
|
|
19
|
+
*
|
|
20
|
+
* - SSR isolation: module-level globals leak between simultaneous server
|
|
21
|
+
* requests in the same Node process. A `providedIn: 'root'` service is
|
|
22
|
+
* instantiated per application injector.
|
|
23
|
+
* - Bootstrap-safety: `TestBed.resetTestingModule()`, micro-frontend reloads,
|
|
24
|
+
* and anything else that destroys `ApplicationRef` must not leave stale
|
|
25
|
+
* `document` listeners behind. The listeners are registered in the
|
|
26
|
+
* constructor and removed via `DestroyRef`.
|
|
27
|
+
* - SSR safety: `document` is inaccessible on the server. The service is a
|
|
28
|
+
* no-op when `PLATFORM_ID` is not the browser; `keyboard` stays `false` and
|
|
29
|
+
* no listener is installed.
|
|
30
|
+
*
|
|
31
|
+
* Internal — not re-exported from `public-api.ts`.
|
|
32
|
+
*/
|
|
33
|
+
class InputModality {
|
|
34
|
+
#document = inject(DOCUMENT);
|
|
35
|
+
#isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
36
|
+
#keyboard = signal(false, /* @ts-ignore */
|
|
37
|
+
...(ngDevMode ? [{ debugName: "#keyboard" }] : /* istanbul ignore next */ []));
|
|
38
|
+
/**
|
|
39
|
+
* `true` when the most recent input modality was the keyboard, `false` after
|
|
40
|
+
* a pointer interaction (or before any interaction). Stays `false` on the
|
|
41
|
+
* server. A `keydown` carrying a `Meta` / `Control` / `Alt` modifier is
|
|
42
|
+
* treated as a shortcut rather than keyboard navigation and does **not** flip
|
|
43
|
+
* this to `true` (matching the platform `:focus-visible` heuristic); `Shift`
|
|
44
|
+
* alone still counts, since `Shift`+`Tab` is legitimate keyboard navigation.
|
|
45
|
+
*/
|
|
46
|
+
keyboard = this.#keyboard.asReadonly();
|
|
47
|
+
#onKeyDown = (event) => {
|
|
48
|
+
if (event.metaKey || event.ctrlKey || event.altKey) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.#keyboard.set(true);
|
|
52
|
+
};
|
|
53
|
+
#onPointerDown = () => {
|
|
54
|
+
this.#keyboard.set(false);
|
|
55
|
+
};
|
|
56
|
+
constructor() {
|
|
57
|
+
if (!this.#isBrowser) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.#document.addEventListener('keydown', this.#onKeyDown, true);
|
|
61
|
+
this.#document.addEventListener('pointerdown', this.#onPointerDown, true);
|
|
62
|
+
inject(DestroyRef).onDestroy(() => {
|
|
63
|
+
this.#document.removeEventListener('keydown', this.#onKeyDown, true);
|
|
64
|
+
this.#document.removeEventListener('pointerdown', this.#onPointerDown, true);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: InputModality, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
68
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: InputModality, providedIn: 'root' });
|
|
69
|
+
}
|
|
70
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: InputModality, decorators: [{
|
|
71
|
+
type: Injectable,
|
|
72
|
+
args: [{ providedIn: 'root' }]
|
|
73
|
+
}], ctorParameters: () => [] });
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns a `Signal<boolean>` that is `true` while the last global input
|
|
77
|
+
* modality was the keyboard, and `false` after a pointer interaction (or
|
|
78
|
+
* before any interaction). This is the modality half of `:focus-visible`
|
|
79
|
+
* styling: a consumer that wants a focus ring only on keyboard focus combines
|
|
80
|
+
* this with the element's own focused state (`:focus`, or a `focusin` /
|
|
81
|
+
* `focusout` listener) — e.g. `host: { '[attr.data-focus-visible]':
|
|
82
|
+
* "focused() && focusVisible() ? '' : null" }`.
|
|
83
|
+
*
|
|
84
|
+
* Backed by the application-scoped {@link InputModality} singleton, so every
|
|
85
|
+
* consumer shares a single capture-phase `keydown` / `pointerdown` listener on
|
|
86
|
+
* `document` no matter how many call this.
|
|
87
|
+
*
|
|
88
|
+
* SSR-safe: on the server the backing service installs no listener and the
|
|
89
|
+
* signal stays `false`. Must be called from an injection context.
|
|
90
|
+
*
|
|
91
|
+
* Internal — not re-exported from `public-api.ts`.
|
|
92
|
+
*/
|
|
93
|
+
function injectFocusVisible() {
|
|
94
|
+
return inject(InputModality).keyboard;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Returns a `Signal<boolean>` reflecting whether a pointing device is currently
|
|
99
|
+
* hovering the host element. State is set on `pointerenter` and cleared on
|
|
100
|
+
* `pointerleave`.
|
|
101
|
+
*
|
|
102
|
+
* Touch is suppressed: a `pointerenter` whose `pointerType` is `'touch'` does
|
|
103
|
+
* not set the hovered state, so the emulated mouse-enter a tap produces on a
|
|
104
|
+
* touchscreen never leaves the element stuck in a hovered state after the
|
|
105
|
+
* finger lifts. Only `'mouse'` / `'pen'` pointers report hover.
|
|
106
|
+
*
|
|
107
|
+
* Attaches `pointerenter` / `pointerleave` listeners to the host element. The
|
|
108
|
+
* `disabled` option short-circuits the result reactively (a pure `computed`,
|
|
109
|
+
* never an `effect`-written signal) and also stops hover from arming while
|
|
110
|
+
* disabled.
|
|
111
|
+
*
|
|
112
|
+
* SSR-safe: on the server no listener is attached and the signal stays
|
|
113
|
+
* `false`. Must be called from an injection context (injects `ElementRef`,
|
|
114
|
+
* `PLATFORM_ID`, `DestroyRef`).
|
|
115
|
+
*
|
|
116
|
+
* Internal — not re-exported from `public-api.ts`.
|
|
117
|
+
*/
|
|
118
|
+
function injectHovered(opts) {
|
|
119
|
+
const disabled = opts?.disabled;
|
|
120
|
+
const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
121
|
+
if (!isBrowser) {
|
|
122
|
+
return signal(false).asReadonly();
|
|
123
|
+
}
|
|
124
|
+
const el = inject(ElementRef).nativeElement;
|
|
125
|
+
const hovered = signal(false, /* @ts-ignore */
|
|
126
|
+
...(ngDevMode ? [{ debugName: "hovered" }] : /* istanbul ignore next */ []));
|
|
127
|
+
const onPointerEnter = (event) => {
|
|
128
|
+
if (event.pointerType === 'touch') {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (disabled?.()) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
hovered.set(true);
|
|
135
|
+
};
|
|
136
|
+
const onPointerLeave = () => {
|
|
137
|
+
hovered.set(false);
|
|
138
|
+
};
|
|
139
|
+
el.addEventListener('pointerenter', onPointerEnter);
|
|
140
|
+
el.addEventListener('pointerleave', onPointerLeave);
|
|
141
|
+
inject(DestroyRef).onDestroy(() => {
|
|
142
|
+
el.removeEventListener('pointerenter', onPointerEnter);
|
|
143
|
+
el.removeEventListener('pointerleave', onPointerLeave);
|
|
144
|
+
});
|
|
145
|
+
return computed(() => (disabled?.() ? false : hovered()));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Returns a `Signal<boolean>` reflecting whether the host element is currently
|
|
150
|
+
* being pressed — held down via the primary pointer button, or via the
|
|
151
|
+
* <kbd>Enter</kbd> / <kbd>Space</kbd> key while focused. The state clears on
|
|
152
|
+
* pointer release, the pointer leaving the element mid-press, key release, or
|
|
153
|
+
* `blur` (covers focus leaving the element while a key is held).
|
|
154
|
+
*
|
|
155
|
+
* Attaches `pointerdown` / `pointerup` / `pointerleave` / `keydown` / `keyup` /
|
|
156
|
+
* `blur` listeners to the host element. The `disabled` option short-circuits
|
|
157
|
+
* the result reactively (a pure `computed`, never an `effect`-written signal)
|
|
158
|
+
* and also stops a fresh press from arming while disabled.
|
|
159
|
+
*
|
|
160
|
+
* SSR-safe: on the server no listener is attached and the signal stays
|
|
161
|
+
* `false`. Must be called from an injection context (injects `ElementRef`,
|
|
162
|
+
* `PLATFORM_ID`, `DestroyRef`).
|
|
163
|
+
*
|
|
164
|
+
* Internal — not re-exported from `public-api.ts`.
|
|
165
|
+
*/
|
|
166
|
+
function injectPressed(opts) {
|
|
167
|
+
const disabled = opts?.disabled;
|
|
168
|
+
const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
169
|
+
if (!isBrowser) {
|
|
170
|
+
return signal(false).asReadonly();
|
|
171
|
+
}
|
|
172
|
+
const el = inject(ElementRef).nativeElement;
|
|
173
|
+
const pressed = signal(false, /* @ts-ignore */
|
|
174
|
+
...(ngDevMode ? [{ debugName: "pressed" }] : /* istanbul ignore next */ []));
|
|
175
|
+
const onPointerDown = (event) => {
|
|
176
|
+
if (event.pointerType === 'mouse' && event.button !== 0) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (disabled?.()) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
pressed.set(true);
|
|
183
|
+
};
|
|
184
|
+
const onPointerEnd = () => {
|
|
185
|
+
pressed.set(false);
|
|
186
|
+
};
|
|
187
|
+
const onKeyDown = (event) => {
|
|
188
|
+
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (disabled?.()) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
pressed.set(true);
|
|
195
|
+
};
|
|
196
|
+
const onKeyUp = (event) => {
|
|
197
|
+
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
pressed.set(false);
|
|
201
|
+
};
|
|
202
|
+
const onBlur = () => {
|
|
203
|
+
pressed.set(false);
|
|
204
|
+
};
|
|
205
|
+
el.addEventListener('pointerdown', onPointerDown);
|
|
206
|
+
el.addEventListener('pointerup', onPointerEnd);
|
|
207
|
+
el.addEventListener('pointerleave', onPointerEnd);
|
|
208
|
+
el.addEventListener('keydown', onKeyDown);
|
|
209
|
+
el.addEventListener('keyup', onKeyUp);
|
|
210
|
+
el.addEventListener('blur', onBlur);
|
|
211
|
+
inject(DestroyRef).onDestroy(() => {
|
|
212
|
+
el.removeEventListener('pointerdown', onPointerDown);
|
|
213
|
+
el.removeEventListener('pointerup', onPointerEnd);
|
|
214
|
+
el.removeEventListener('pointerleave', onPointerEnd);
|
|
215
|
+
el.removeEventListener('keydown', onKeyDown);
|
|
216
|
+
el.removeEventListener('keyup', onKeyUp);
|
|
217
|
+
el.removeEventListener('blur', onBlur);
|
|
218
|
+
});
|
|
219
|
+
return computed(() => (disabled?.() ? false : pressed()));
|
|
220
|
+
}
|
|
4
221
|
|
|
5
222
|
/**
|
|
6
223
|
* Headless implementation of the [WAI-ARIA Button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/).
|
|
@@ -8,8 +225,10 @@ import { injectFocusVisible, injectHovered, injectPressed, createDefaults } from
|
|
|
8
225
|
* Works on a native `<button>` host and on any arbitrary host element (e.g. `<div>`, `<span>`).
|
|
9
226
|
* On a native button the platform handles Enter/Space → click synthesis and no `role` or
|
|
10
227
|
* `tabindex` is emitted (the platform owns those). On a non-button host, `role="button"` and
|
|
11
|
-
* `tabindex="0"` are applied, and
|
|
12
|
-
*
|
|
228
|
+
* `tabindex="0"` are applied, and the directive synthesizes a click through its single `onClick`
|
|
229
|
+
* path: Enter activates on `keydown`, while Space activates on `keyup` (its `keydown` always
|
|
230
|
+
* calls `preventDefault()` to stop the page scrolling, even when disabled) — matching native
|
|
231
|
+
* button and APG behavior.
|
|
13
232
|
*
|
|
14
233
|
* Disabled stays focusable: per the APG a disabled button must remain reachable by assistive
|
|
15
234
|
* technology, so the native `disabled` attribute is never set. Instead `aria-disabled="true"` and
|
|
@@ -50,6 +269,7 @@ class ForButton {
|
|
|
50
269
|
activate = output();
|
|
51
270
|
#focused = signal(false, /* @ts-ignore */
|
|
52
271
|
...(ngDevMode ? [{ debugName: "#focused" }] : /* istanbul ignore next */ []));
|
|
272
|
+
#spaceHeld = false;
|
|
53
273
|
#keyboardModality = injectFocusVisible();
|
|
54
274
|
hovered = injectHovered({ disabled: this.disabled });
|
|
55
275
|
pressed = injectPressed({ disabled: this.disabled });
|
|
@@ -73,9 +293,33 @@ class ForButton {
|
|
|
73
293
|
if (this.#isNativeButton) {
|
|
74
294
|
return;
|
|
75
295
|
}
|
|
76
|
-
if (event.key
|
|
296
|
+
if (event.key === 'Enter') {
|
|
297
|
+
if (this.disabled()) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
event.preventDefault();
|
|
301
|
+
this.#host.click();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (event.key === ' ') {
|
|
305
|
+
event.preventDefault();
|
|
306
|
+
if (this.disabled()) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
this.#spaceHeld = true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
onKeyup(event) {
|
|
313
|
+
if (this.#isNativeButton) {
|
|
77
314
|
return;
|
|
78
315
|
}
|
|
316
|
+
if (event.key !== ' ') {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (!this.#spaceHeld) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
this.#spaceHeld = false;
|
|
79
323
|
if (this.disabled()) {
|
|
80
324
|
return;
|
|
81
325
|
}
|
|
@@ -87,9 +331,10 @@ class ForButton {
|
|
|
87
331
|
}
|
|
88
332
|
onFocusOut() {
|
|
89
333
|
this.#focused.set(false);
|
|
334
|
+
this.#spaceHeld = false;
|
|
90
335
|
}
|
|
91
336
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForButton, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
92
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForButton, isStandalone: true, selector: "[forButton]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activate: "activate" }, host: { listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)", "focusin": "onFocusIn()", "focusout": "onFocusOut()" }, properties: { "attr.type": "resolvedType()", "attr.role": "resolvedRole()", "attr.tabindex": "resolvedTabindex()", "attr.aria-disabled": "disabled() ? 'true' : null", "attr.data-disabled": "disabled() ? '' : null", "attr.data-pressed": "pressed() ? '' : null", "attr.data-hovered": "hovered() ? '' : null", "attr.data-focus-visible": "focusVisible() ? '' : null" } }, exportAs: ["forButton"], ngImport: i0 });
|
|
337
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForButton, isStandalone: true, selector: "[forButton]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activate: "activate" }, host: { listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)", "keyup": "onKeyup($event)", "focusin": "onFocusIn()", "focusout": "onFocusOut()" }, properties: { "attr.type": "resolvedType()", "attr.role": "resolvedRole()", "attr.tabindex": "resolvedTabindex()", "attr.aria-disabled": "disabled() ? 'true' : null", "attr.data-disabled": "disabled() ? '' : null", "attr.data-pressed": "pressed() ? '' : null", "attr.data-hovered": "hovered() ? '' : null", "attr.data-focus-visible": "focusVisible() ? '' : null" } }, exportAs: ["forButton"], ngImport: i0 });
|
|
93
338
|
}
|
|
94
339
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForButton, decorators: [{
|
|
95
340
|
type: Directive,
|
|
@@ -107,28 +352,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
|
|
|
107
352
|
'[attr.data-focus-visible]': "focusVisible() ? '' : null",
|
|
108
353
|
'(click)': 'onClick($event)',
|
|
109
354
|
'(keydown)': 'onKeydown($event)',
|
|
355
|
+
'(keyup)': 'onKeyup($event)',
|
|
110
356
|
'(focusin)': 'onFocusIn()',
|
|
111
357
|
'(focusout)': 'onFocusOut()',
|
|
112
358
|
},
|
|
113
359
|
}]
|
|
114
360
|
}], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], activate: [{ type: i0.Output, args: ["activate"] }] } });
|
|
115
361
|
|
|
116
|
-
const FALLBACK = {};
|
|
117
|
-
const { token, provideDefaults } = createDefaults('FOR_BUTTON_DEFAULTS', FALLBACK);
|
|
118
|
-
/** Token holding the resolved button defaults for the current scope. */
|
|
119
|
-
const FOR_BUTTON_DEFAULTS = token;
|
|
120
|
-
/**
|
|
121
|
-
* Configures forty-cdk button defaults for this injector scope. Partial
|
|
122
|
-
* overrides inherit unspecified keys from the parent scope (or library
|
|
123
|
-
* defaults at the root).
|
|
124
|
-
*/
|
|
125
|
-
function provideForButtonDefaults(defaults = {}) {
|
|
126
|
-
return provideDefaults(defaults);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
362
|
/**
|
|
130
363
|
* Generated bundle index. Do not edit.
|
|
131
364
|
*/
|
|
132
365
|
|
|
133
|
-
export {
|
|
366
|
+
export { ForButton };
|
|
134
367
|
//# sourceMappingURL=forty-cdk-button.mjs.map
|