@neuravision/ng-construct 0.1.0 → 0.3.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.
@@ -1,7 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, output, signal, computed, ChangeDetectionStrategy, Component, inject, forwardRef, model, booleanAttribute, viewChild, contentChildren, effect, ElementRef, ContentChild, TemplateRef, Directive, Injectable, viewChildren, Renderer2, InjectionToken, contentChild, DOCUMENT, numberAttribute, Pipe } from '@angular/core';
2
+ import { input, output, signal, computed, ChangeDetectionStrategy, Component, inject, forwardRef, model, booleanAttribute, viewChild, contentChildren, effect, contentChild, ElementRef, TemplateRef, Directive, Injectable, viewChildren, Renderer2, InjectionToken, DOCUMENT, numberAttribute, Pipe } from '@angular/core';
3
3
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
4
4
  import { NgTemplateOutlet } from '@angular/common';
5
+ import { RouterLink, RouterLinkActive } from '@angular/router';
5
6
 
6
7
  /**
7
8
  * Alert component for displaying contextual feedback messages.
@@ -1200,36 +1201,65 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
1200
1201
  `, styles: [":host{display:block}\n"] }]
1201
1202
  }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }, { type: i0.Output, args: ["indeterminateChange"] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }] } });
1202
1203
 
1204
+ // ── Radio Button ─────────────────────────────────────────────────────────────
1203
1205
  /**
1204
- * Radio button component with form control support
1206
+ * Individual radio button. Use inside `af-radio-group` for full
1207
+ * accessibility, or standalone with its own `ControlValueAccessor`.
1205
1208
  *
1206
1209
  * @example
1207
- * <af-radio name="plan" value="standard" [(ngModel)]="selectedPlan">
1208
- * Standard
1209
- * </af-radio>
1210
- * <af-radio name="plan" value="premium" [(ngModel)]="selectedPlan">
1211
- * Premium
1212
- * </af-radio>
1210
+ * <af-radio-group ariaLabel="Plan" name="plan" [(ngModel)]="plan">
1211
+ * <af-radio value="standard">Standard</af-radio>
1212
+ * <af-radio value="premium">Premium</af-radio>
1213
+ * </af-radio-group>
1213
1214
  */
1214
1215
  class AfRadioComponent {
1215
- /** Radio group name */
1216
+ group = inject(forwardRef(() => AfRadioGroupComponent), { optional: true });
1217
+ /** Radio group name (only used without `af-radio-group`). */
1216
1218
  name = input('', ...(ngDevMode ? [{ debugName: "name" }] : []));
1217
- /** Radio value */
1219
+ /** Radio value. */
1218
1220
  value = input(undefined, ...(ngDevMode ? [{ debugName: "value" }] : []));
1219
- /** Whether radio is disabled */
1221
+ /** Whether this radio is disabled. */
1220
1222
  disabled = model(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
1223
+ inputRef = viewChild.required('inputEl');
1221
1224
  modelValue = signal(undefined, ...(ngDevMode ? [{ debugName: "modelValue" }] : []));
1222
1225
  onChangeCallback = () => { };
1223
1226
  onTouched = () => { };
1224
- isChecked = computed(() => this.modelValue() === this.value(), ...(ngDevMode ? [{ debugName: "isChecked" }] : []));
1225
- onChangeEvent(event) {
1226
- const target = event.target;
1227
- if (target.checked) {
1227
+ resolvedName = computed(() => this.group?.name() || this.name(), ...(ngDevMode ? [{ debugName: "resolvedName" }] : []));
1228
+ isChecked = computed(() => {
1229
+ const selected = this.group ? this.group.selectedValue() : this.modelValue();
1230
+ return selected === this.value();
1231
+ }, ...(ngDevMode ? [{ debugName: "isChecked" }] : []));
1232
+ isDisabled = computed(() => {
1233
+ if (this.group?.disabled())
1234
+ return true;
1235
+ return this.disabled();
1236
+ }, ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
1237
+ resolvedTabindex = computed(() => {
1238
+ if (!this.group)
1239
+ return null;
1240
+ return this.group.tabindexFor(this);
1241
+ }, ...(ngDevMode ? [{ debugName: "resolvedTabindex" }] : []));
1242
+ /** Focuses the native input element. */
1243
+ focus() {
1244
+ this.inputRef().nativeElement.focus();
1245
+ }
1246
+ onChangeEvent() {
1247
+ if (this.group) {
1248
+ this.group.selectRadio(this);
1249
+ }
1250
+ else {
1228
1251
  this.modelValue.set(this.value());
1229
1252
  this.onChangeCallback(this.value());
1230
1253
  }
1231
1254
  }
1232
- /** ControlValueAccessor implementation */
1255
+ onFocus() {
1256
+ this.group?.onRadioFocus(this);
1257
+ }
1258
+ onKeydown(event) {
1259
+ if (this.group) {
1260
+ this.group.onRadioKeydown(event, this);
1261
+ }
1262
+ }
1233
1263
  writeValue(value) {
1234
1264
  this.modelValue.set(value);
1235
1265
  }
@@ -1243,26 +1273,29 @@ class AfRadioComponent {
1243
1273
  this.disabled.set(isDisabled);
1244
1274
  }
1245
1275
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfRadioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1246
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.2", type: AfRadioComponent, isStandalone: true, selector: "af-radio", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange" }, providers: [
1276
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.2", type: AfRadioComponent, isStandalone: true, selector: "af-radio", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange" }, providers: [
1247
1277
  {
1248
1278
  provide: NG_VALUE_ACCESSOR,
1249
1279
  useExisting: forwardRef(() => AfRadioComponent),
1250
- multi: true
1251
- }
1252
- ], ngImport: i0, template: `
1280
+ multi: true,
1281
+ },
1282
+ ], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
1253
1283
  <label class="ct-radio">
1254
1284
  <input
1285
+ #inputEl
1255
1286
  class="ct-radio__input"
1256
1287
  type="radio"
1257
- [name]="name()"
1288
+ [name]="resolvedName()"
1258
1289
  [value]="value()"
1259
1290
  [checked]="isChecked()"
1260
- [disabled]="disabled()"
1261
- (change)="onChangeEvent($event)"
1291
+ [disabled]="isDisabled()"
1292
+ [attr.tabindex]="resolvedTabindex()"
1293
+ (change)="onChangeEvent()"
1262
1294
  (blur)="onTouched()"
1263
- />
1295
+ (focus)="onFocus()"
1296
+ (keydown)="onKeydown($event)" />
1264
1297
  <span class="ct-radio__label">
1265
- <ng-content></ng-content>
1298
+ <ng-content />
1266
1299
  </span>
1267
1300
  </label>
1268
1301
  `, isInline: true, styles: [":host{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
@@ -1273,26 +1306,172 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
1273
1306
  {
1274
1307
  provide: NG_VALUE_ACCESSOR,
1275
1308
  useExisting: forwardRef(() => AfRadioComponent),
1276
- multi: true
1277
- }
1309
+ multi: true,
1310
+ },
1278
1311
  ], template: `
1279
1312
  <label class="ct-radio">
1280
1313
  <input
1314
+ #inputEl
1281
1315
  class="ct-radio__input"
1282
1316
  type="radio"
1283
- [name]="name()"
1317
+ [name]="resolvedName()"
1284
1318
  [value]="value()"
1285
1319
  [checked]="isChecked()"
1286
- [disabled]="disabled()"
1287
- (change)="onChangeEvent($event)"
1320
+ [disabled]="isDisabled()"
1321
+ [attr.tabindex]="resolvedTabindex()"
1322
+ (change)="onChangeEvent()"
1288
1323
  (blur)="onTouched()"
1289
- />
1324
+ (focus)="onFocus()"
1325
+ (keydown)="onKeydown($event)" />
1290
1326
  <span class="ct-radio__label">
1291
- <ng-content></ng-content>
1327
+ <ng-content />
1292
1328
  </span>
1293
1329
  </label>
1294
1330
  `, styles: [":host{display:block}\n"] }]
1295
- }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }] } });
1331
+ }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], inputRef: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }] } });
1332
+ // ── Radio Group ──────────────────────────────────────────────────────────────
1333
+ /**
1334
+ * Groups `af-radio` components with `role="radiogroup"`, ARIA labeling,
1335
+ * roving tabindex, and arrow-key navigation per WAI-ARIA Radio Group Pattern.
1336
+ *
1337
+ * Implements `ControlValueAccessor` so the group value can be bound via
1338
+ * `[(ngModel)]` or reactive forms.
1339
+ *
1340
+ * @example
1341
+ * <af-radio-group ariaLabel="Select plan" name="plan" [(ngModel)]="plan">
1342
+ * <af-radio value="standard">Standard</af-radio>
1343
+ * <af-radio value="premium">Premium</af-radio>
1344
+ * </af-radio-group>
1345
+ */
1346
+ class AfRadioGroupComponent {
1347
+ /** Shared `name` attribute for all child radios. */
1348
+ name = input.required(...(ngDevMode ? [{ debugName: "name" }] : []));
1349
+ /** Accessible label for the radio group. */
1350
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
1351
+ /** ID of an external element labeling this group. */
1352
+ ariaLabelledBy = input('', ...(ngDevMode ? [{ debugName: "ariaLabelledBy" }] : []));
1353
+ /** Disables all radios in the group. */
1354
+ disabled = model(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
1355
+ radios = contentChildren(AfRadioComponent, ...(ngDevMode ? [{ debugName: "radios" }] : []));
1356
+ selectedValue = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedValue" }] : []));
1357
+ focusedIndex = signal(0, ...(ngDevMode ? [{ debugName: "focusedIndex" }] : []));
1358
+ onChangeCallback = () => { };
1359
+ onTouchedCallback = () => { };
1360
+ syncEffect = effect(() => {
1361
+ const radios = this.radios();
1362
+ const value = this.selectedValue();
1363
+ const checkedIdx = radios.findIndex((r) => r.value() === value);
1364
+ this.focusedIndex.set(checkedIdx >= 0 ? checkedIdx : 0);
1365
+ }, ...(ngDevMode ? [{ debugName: "syncEffect" }] : []));
1366
+ /** Returns the tabindex a child radio should use for roving tabindex. */
1367
+ tabindexFor(radio) {
1368
+ const radios = this.enabledRadios();
1369
+ const idx = radios.indexOf(radio);
1370
+ if (idx === -1)
1371
+ return -1;
1372
+ return idx === this.focusedIndex() ? 0 : -1;
1373
+ }
1374
+ /** Selects a radio and propagates the value. */
1375
+ selectRadio(radio) {
1376
+ const value = radio.value();
1377
+ this.selectedValue.set(value);
1378
+ this.onChangeCallback(value);
1379
+ this.onTouchedCallback();
1380
+ }
1381
+ /** Called when a child radio receives focus. */
1382
+ onRadioFocus(radio) {
1383
+ const idx = this.enabledRadios().indexOf(radio);
1384
+ if (idx >= 0) {
1385
+ this.focusedIndex.set(idx);
1386
+ }
1387
+ }
1388
+ /** Handles keyboard navigation within the group. */
1389
+ onRadioKeydown(event, _current) {
1390
+ const enabled = this.enabledRadios();
1391
+ if (enabled.length === 0)
1392
+ return;
1393
+ let nextIndex = null;
1394
+ switch (event.key) {
1395
+ case 'ArrowDown':
1396
+ case 'ArrowRight':
1397
+ event.preventDefault();
1398
+ nextIndex = (this.focusedIndex() + 1) % enabled.length;
1399
+ break;
1400
+ case 'ArrowUp':
1401
+ case 'ArrowLeft':
1402
+ event.preventDefault();
1403
+ nextIndex = (this.focusedIndex() - 1 + enabled.length) % enabled.length;
1404
+ break;
1405
+ case 'Home':
1406
+ event.preventDefault();
1407
+ nextIndex = 0;
1408
+ break;
1409
+ case 'End':
1410
+ event.preventDefault();
1411
+ nextIndex = enabled.length - 1;
1412
+ break;
1413
+ case ' ':
1414
+ event.preventDefault();
1415
+ this.selectRadio(enabled[this.focusedIndex()]);
1416
+ return;
1417
+ }
1418
+ if (nextIndex !== null) {
1419
+ this.focusedIndex.set(nextIndex);
1420
+ const target = enabled[nextIndex];
1421
+ target.focus();
1422
+ this.selectRadio(target);
1423
+ }
1424
+ }
1425
+ writeValue(value) {
1426
+ this.selectedValue.set(value);
1427
+ }
1428
+ registerOnChange(fn) {
1429
+ this.onChangeCallback = fn;
1430
+ }
1431
+ registerOnTouched(fn) {
1432
+ this.onTouchedCallback = fn;
1433
+ }
1434
+ setDisabledState(isDisabled) {
1435
+ this.disabled.set(isDisabled);
1436
+ }
1437
+ enabledRadios() {
1438
+ return this.radios().filter((r) => !r.isDisabled());
1439
+ }
1440
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfRadioGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1441
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.2", type: AfRadioGroupComponent, isStandalone: true, selector: "af-radio-group", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledBy: { classPropertyName: "ariaLabelledBy", publicName: "ariaLabelledBy", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange" }, providers: [
1442
+ {
1443
+ provide: NG_VALUE_ACCESSOR,
1444
+ useExisting: forwardRef(() => AfRadioGroupComponent),
1445
+ multi: true,
1446
+ },
1447
+ ], queries: [{ propertyName: "radios", predicate: AfRadioComponent, isSignal: true }], ngImport: i0, template: `
1448
+ <div
1449
+ role="radiogroup"
1450
+ [attr.aria-label]="ariaLabel() || null"
1451
+ [attr.aria-labelledby]="ariaLabelledBy() || null"
1452
+ [attr.aria-disabled]="disabled() || null">
1453
+ <ng-content />
1454
+ </div>
1455
+ `, isInline: true, styles: [":host{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1456
+ }
1457
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfRadioGroupComponent, decorators: [{
1458
+ type: Component,
1459
+ args: [{ selector: 'af-radio-group', changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1460
+ {
1461
+ provide: NG_VALUE_ACCESSOR,
1462
+ useExisting: forwardRef(() => AfRadioGroupComponent),
1463
+ multi: true,
1464
+ },
1465
+ ], template: `
1466
+ <div
1467
+ role="radiogroup"
1468
+ [attr.aria-label]="ariaLabel() || null"
1469
+ [attr.aria-labelledby]="ariaLabelledBy() || null"
1470
+ [attr.aria-disabled]="disabled() || null">
1471
+ <ng-content />
1472
+ </div>
1473
+ `, styles: [":host{display:block}\n"] }]
1474
+ }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledBy", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], radios: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => AfRadioComponent), { isSignal: true }] }] } });
1296
1475
 
1297
1476
  /**
1298
1477
  * Switch/Toggle component with form control support
@@ -1303,7 +1482,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
1303
1482
  * </af-switch>
1304
1483
  */
1305
1484
  class AfSwitchComponent {
1306
- /** Whether switch is disabled */
1485
+ /** Accessible label for icon-only or unlabeled switches. */
1486
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
1487
+ /** Whether switch is disabled. */
1307
1488
  disabled = model(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
1308
1489
  /** Checked state - supports two-way binding via [(checked)] */
1309
1490
  checked = model(false, ...(ngDevMode ? [{ debugName: "checked" }] : []));
@@ -1328,7 +1509,7 @@ class AfSwitchComponent {
1328
1509
  this.disabled.set(isDisabled);
1329
1510
  }
1330
1511
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfSwitchComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1331
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.2", type: AfSwitchComponent, isStandalone: true, selector: "af-switch", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", checked: "checkedChange" }, providers: [
1512
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.2", type: AfSwitchComponent, isStandalone: true, selector: "af-switch", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", checked: "checkedChange" }, providers: [
1332
1513
  {
1333
1514
  provide: NG_VALUE_ACCESSOR,
1334
1515
  useExisting: forwardRef(() => AfSwitchComponent),
@@ -1342,9 +1523,9 @@ class AfSwitchComponent {
1342
1523
  role="switch"
1343
1524
  [checked]="checked()"
1344
1525
  [disabled]="disabled()"
1526
+ [attr.aria-label]="ariaLabel() || null"
1345
1527
  (change)="onChange($event)"
1346
- (blur)="onTouched()"
1347
- />
1528
+ (blur)="onTouched()" />
1348
1529
  <span class="ct-switch__label">
1349
1530
  <ng-content></ng-content>
1350
1531
  </span>
@@ -1367,57 +1548,58 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
1367
1548
  role="switch"
1368
1549
  [checked]="checked()"
1369
1550
  [disabled]="disabled()"
1551
+ [attr.aria-label]="ariaLabel() || null"
1370
1552
  (change)="onChange($event)"
1371
- (blur)="onTouched()"
1372
- />
1553
+ (blur)="onTouched()" />
1373
1554
  <span class="ct-switch__label">
1374
1555
  <ng-content></ng-content>
1375
1556
  </span>
1376
1557
  </label>
1377
1558
  `, styles: [":host{display:block}\n"] }]
1378
- }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }] } });
1559
+ }], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }] } });
1379
1560
 
1380
1561
  /**
1381
- * Card component for containing content
1562
+ * Card component for containing content.
1563
+ *
1564
+ * When `interactive` is set the card becomes keyboard-accessible with
1565
+ * `role="button"`, roving `tabindex`, and Enter/Space activation.
1382
1566
  *
1383
1567
  * @example
1384
1568
  * <af-card elevation="md" padding="lg">
1385
- * <div header>
1386
- * <h3>Title</h3>
1387
- * </div>
1388
- * <div body>
1389
- * <p>Card content</p>
1390
- * </div>
1569
+ * <div header><h3>Title</h3></div>
1570
+ * <div body><p>Card content</p></div>
1571
+ * </af-card>
1572
+ *
1573
+ * <af-card interactive ariaLabel="Open project" (cardClick)="open()">
1574
+ * <p body>Click me</p>
1391
1575
  * </af-card>
1392
1576
  */
1393
1577
  class AfCardComponent {
1394
- /** Whether card is interactive (clickable/hoverable) */
1578
+ /** Makes the card interactive (clickable, keyboard-accessible). */
1395
1579
  interactive = input(false, ...(ngDevMode ? [{ debugName: "interactive" }] : []));
1396
- /** Shadow elevation level */
1580
+ /** Shadow elevation level. */
1397
1581
  elevation = input(null, ...(ngDevMode ? [{ debugName: "elevation" }] : []));
1398
- /** Content padding level */
1582
+ /** Content padding level. */
1399
1583
  padding = input(null, ...(ngDevMode ? [{ debugName: "padding" }] : []));
1400
- /** Click event emitter */
1584
+ /** Accessible label for interactive cards. */
1585
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
1586
+ /** Emitted when an interactive card is activated (click, Enter, or Space). */
1401
1587
  cardClick = output();
1402
- hasHeader = signal(false, ...(ngDevMode ? [{ debugName: "hasHeader" }] : []));
1403
- hasFooter = signal(false, ...(ngDevMode ? [{ debugName: "hasFooter" }] : []));
1404
- set headerContent(value) {
1405
- this.hasHeader.set(!!value);
1406
- }
1407
- set footerContent(value) {
1408
- this.hasFooter.set(!!value);
1409
- }
1588
+ headerRef = contentChild('[header]', { ...(ngDevMode ? { debugName: "headerRef" } : {}), read: ElementRef });
1589
+ footerRef = contentChild('[footer]', { ...(ngDevMode ? { debugName: "footerRef" } : {}), read: ElementRef });
1590
+ hasHeader = computed(() => !!this.headerRef(), ...(ngDevMode ? [{ debugName: "hasHeader" }] : []));
1591
+ hasFooter = computed(() => !!this.footerRef(), ...(ngDevMode ? [{ debugName: "hasFooter" }] : []));
1410
1592
  static ELEVATION_MAP = {
1411
1593
  none: 'none',
1412
1594
  sm: '0 1px 3px rgba(0, 0, 0, 0.08)',
1413
1595
  md: '0 4px 12px rgba(0, 0, 0, 0.08)',
1414
- lg: '0 8px 24px rgba(0, 0, 0, 0.12)'
1596
+ lg: '0 8px 24px rgba(0, 0, 0, 0.12)',
1415
1597
  };
1416
1598
  static PADDING_MAP = {
1417
1599
  none: '0',
1418
1600
  sm: 'var(--space-3, 0.75rem)',
1419
1601
  md: 'var(--space-5, 1.25rem)',
1420
- lg: 'var(--space-7, 2rem)'
1602
+ lg: 'var(--space-7, 2rem)',
1421
1603
  };
1422
1604
  cardClasses = computed(() => {
1423
1605
  const classes = ['ct-card'];
@@ -1441,24 +1623,36 @@ class AfCardComponent {
1441
1623
  this.cardClick.emit();
1442
1624
  }
1443
1625
  }
1626
+ onCardKeydown(event) {
1627
+ if (!this.interactive())
1628
+ return;
1629
+ if (event.key === 'Enter' || event.key === ' ') {
1630
+ event.preventDefault();
1631
+ this.cardClick.emit();
1632
+ }
1633
+ }
1444
1634
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1445
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfCardComponent, isStandalone: true, selector: "af-card", inputs: { interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, elevation: { classPropertyName: "elevation", publicName: "elevation", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cardClick: "cardClick" }, queries: [{ propertyName: "headerContent", first: true, predicate: ["[header]"], descendants: true, read: ElementRef }, { propertyName: "footerContent", first: true, predicate: ["[footer]"], descendants: true, read: ElementRef }], ngImport: i0, template: `
1635
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfCardComponent, isStandalone: true, selector: "af-card", inputs: { interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, elevation: { classPropertyName: "elevation", publicName: "elevation", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cardClick: "cardClick" }, queries: [{ propertyName: "headerRef", first: true, predicate: ["[header]"], descendants: true, read: ElementRef, isSignal: true }, { propertyName: "footerRef", first: true, predicate: ["[footer]"], descendants: true, read: ElementRef, isSignal: true }], ngImport: i0, template: `
1446
1636
  <section
1447
1637
  [class]="cardClasses()"
1448
1638
  [style]="cardStyles()"
1449
- (click)="onCardClick()">
1639
+ [attr.role]="interactive() ? 'button' : null"
1640
+ [attr.tabindex]="interactive() ? 0 : null"
1641
+ [attr.aria-label]="ariaLabel() || null"
1642
+ (click)="onCardClick()"
1643
+ (keydown)="onCardKeydown($event)">
1450
1644
  @if (hasHeader()) {
1451
1645
  <div class="ct-card__header">
1452
- <ng-content select="[header]"></ng-content>
1646
+ <ng-content select="[header]" />
1453
1647
  </div>
1454
1648
  }
1455
1649
  <div class="ct-card__body">
1456
- <ng-content select="[body]"></ng-content>
1457
- <ng-content></ng-content>
1650
+ <ng-content select="[body]" />
1651
+ <ng-content />
1458
1652
  </div>
1459
1653
  @if (hasFooter()) {
1460
1654
  <div class="ct-card__footer">
1461
- <ng-content select="[footer]"></ng-content>
1655
+ <ng-content select="[footer]" />
1462
1656
  </div>
1463
1657
  }
1464
1658
  </section>
@@ -1470,30 +1664,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
1470
1664
  <section
1471
1665
  [class]="cardClasses()"
1472
1666
  [style]="cardStyles()"
1473
- (click)="onCardClick()">
1667
+ [attr.role]="interactive() ? 'button' : null"
1668
+ [attr.tabindex]="interactive() ? 0 : null"
1669
+ [attr.aria-label]="ariaLabel() || null"
1670
+ (click)="onCardClick()"
1671
+ (keydown)="onCardKeydown($event)">
1474
1672
  @if (hasHeader()) {
1475
1673
  <div class="ct-card__header">
1476
- <ng-content select="[header]"></ng-content>
1674
+ <ng-content select="[header]" />
1477
1675
  </div>
1478
1676
  }
1479
1677
  <div class="ct-card__body">
1480
- <ng-content select="[body]"></ng-content>
1481
- <ng-content></ng-content>
1678
+ <ng-content select="[body]" />
1679
+ <ng-content />
1482
1680
  </div>
1483
1681
  @if (hasFooter()) {
1484
1682
  <div class="ct-card__footer">
1485
- <ng-content select="[footer]"></ng-content>
1683
+ <ng-content select="[footer]" />
1486
1684
  </div>
1487
1685
  }
1488
1686
  </section>
1489
1687
  `, styles: [":host{display:block}\n"] }]
1490
- }], propDecorators: { interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], elevation: [{ type: i0.Input, args: [{ isSignal: true, alias: "elevation", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }], cardClick: [{ type: i0.Output, args: ["cardClick"] }], headerContent: [{
1491
- type: ContentChild,
1492
- args: ['[header]', { read: ElementRef }]
1493
- }], footerContent: [{
1494
- type: ContentChild,
1495
- args: ['[footer]', { read: ElementRef }]
1496
- }] } });
1688
+ }], propDecorators: { interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], elevation: [{ type: i0.Input, args: [{ isSignal: true, alias: "elevation", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], cardClick: [{ type: i0.Output, args: ["cardClick"] }], headerRef: [{ type: i0.ContentChild, args: ['[header]', { ...{ read: ElementRef }, isSignal: true }] }], footerRef: [{ type: i0.ContentChild, args: ['[footer]', { ...{ read: ElementRef }, isSignal: true }] }] } });
1497
1689
 
1498
1690
  /**
1499
1691
  * Directive for defining custom cell templates in AfDataTableComponent.
@@ -1721,6 +1913,85 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
1721
1913
  args: [{ selector: 'af-data-table', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], template: "<div class=\"ct-data-table ct-data-table--simple\">\n <div class=\"ct-data-table__table\">\n <table [class]=\"tableClasses()\">\n <thead>\n <tr>\n @if (isSelectable()) {\n <th scope=\"col\" class=\"ct-table__cell--checkbox\">\n <label class=\"ct-check\">\n <input\n class=\"ct-check__input\"\n type=\"checkbox\"\n [checked]=\"allSelected\"\n [indeterminate]=\"someSelected\"\n (change)=\"toggleAll($any($event.target).checked)\"\n aria-label=\"Select all rows\" />\n </label>\n </th>\n }\n @for (col of columns(); track col.key) {\n <th scope=\"col\" [attr.aria-sort]=\"getAriaSort(col)\">\n @if (col.sortable) {\n <button class=\"ct-table__sort\" type=\"button\" (click)=\"toggleSort(col)\">\n {{ col.header }}\n <span class=\"ct-table__sort-indicator\" aria-hidden=\"true\"></span>\n </button>\n } @else {\n {{ col.header }}\n }\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of sortedData(); track row) {\n <tr (click)=\"onRowClick(row)\">\n @if (isSelectable()) {\n <td class=\"ct-table__cell--checkbox\">\n <label class=\"ct-check\">\n <input\n class=\"ct-check__input\"\n type=\"checkbox\"\n [checked]=\"isSelected(row)\"\n (change)=\"toggleSelection(row, $event)\"\n aria-label=\"Select row\" />\n </label>\n </td>\n }\n @for (col of columns(); track col.key) {\n <td [class]=\"col.cellClass || ''\">\n @if (getCellTemplate(col.key); as tmpl) {\n <ng-container *ngTemplateOutlet=\"tmpl; context: { $implicit: row, column: col }\"></ng-container>\n } @else {\n {{ row[col.key] }}\n }\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n</div>\n", styles: [":host{display:block}.ct-data-table__table tr{cursor:pointer}.ct-data-table__table tr:hover{background-color:var(--color-neutral-50)}\n"] }]
1722
1914
  }], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], sort: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }], rowId: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowId", required: false }] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], cellDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => AfCellDefDirective), { isSignal: true }] }] } });
1723
1915
 
1916
+ const FOCUSABLE_SELECTORS = [
1917
+ 'a[href]',
1918
+ 'area[href]',
1919
+ 'button:not([disabled])',
1920
+ 'input:not([disabled])',
1921
+ 'select:not([disabled])',
1922
+ 'textarea:not([disabled])',
1923
+ '[tabindex]:not([tabindex="-1"])',
1924
+ ].join(',');
1925
+ /**
1926
+ * Manages focus trapping within a container element.
1927
+ *
1928
+ * Handles Tab/Shift+Tab cycling, save/restore of the previously focused
1929
+ * element, and initial focus placement. Create one instance per overlay
1930
+ * (modal, drawer, popover, etc.).
1931
+ */
1932
+ class FocusTrap {
1933
+ previousActiveElement = null;
1934
+ /** Saves the currently focused element for later restoration. */
1935
+ saveFocus() {
1936
+ this.previousActiveElement = document.activeElement;
1937
+ }
1938
+ /** Restores focus to the element saved via `saveFocus()`. */
1939
+ restoreFocus() {
1940
+ if (this.previousActiveElement) {
1941
+ this.previousActiveElement.focus();
1942
+ this.previousActiveElement = null;
1943
+ }
1944
+ }
1945
+ /** Overrides the saved focus target (e.g. to return focus to a specific trigger). */
1946
+ setReturnFocus(element) {
1947
+ this.previousActiveElement = element;
1948
+ }
1949
+ /**
1950
+ * Focuses the first focusable element inside the container,
1951
+ * or the fallback element if no focusable children exist.
1952
+ */
1953
+ focusFirst(container, fallback) {
1954
+ const elements = queryFocusableElements(container);
1955
+ const first = elements[0];
1956
+ if (first) {
1957
+ first.focus();
1958
+ }
1959
+ else {
1960
+ fallback?.focus();
1961
+ }
1962
+ }
1963
+ /**
1964
+ * Handles a Tab keydown event to trap focus within the container.
1965
+ * Call this from a `(keydown)` handler when the key is `Tab`.
1966
+ */
1967
+ handleTab(event, container, fallback) {
1968
+ const elements = queryFocusableElements(container);
1969
+ if (elements.length === 0) {
1970
+ event.preventDefault();
1971
+ fallback?.focus();
1972
+ return;
1973
+ }
1974
+ const first = elements[0];
1975
+ const last = elements[elements.length - 1];
1976
+ const active = document.activeElement;
1977
+ if (event.shiftKey && active === first) {
1978
+ event.preventDefault();
1979
+ last.focus();
1980
+ }
1981
+ else if (!event.shiftKey && active === last) {
1982
+ event.preventDefault();
1983
+ first.focus();
1984
+ }
1985
+ }
1986
+ }
1987
+ /** Queries all visible, enabled, focusable elements within a container. */
1988
+ function queryFocusableElements(container) {
1989
+ if (!container)
1990
+ return [];
1991
+ return Array.from(container.querySelectorAll(FOCUSABLE_SELECTORS)).filter((el) => !el.hasAttribute('disabled') &&
1992
+ el.getAttribute('aria-hidden') !== 'true');
1993
+ }
1994
+
1724
1995
  /**
1725
1996
  * Modal/Dialog component with accessibility features
1726
1997
  *
@@ -1740,6 +2011,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
1740
2011
  */
1741
2012
  class AfModalComponent {
1742
2013
  static nextId = 0;
2014
+ focusTrap = new FocusTrap();
1743
2015
  /** Whether modal is open */
1744
2016
  open = input(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
1745
2017
  /** Modal title */
@@ -1752,12 +2024,8 @@ class AfModalComponent {
1752
2024
  closed = output();
1753
2025
  /** Unique title ID for aria-labelledby */
1754
2026
  titleId = `af-modal-title-${AfModalComponent.nextId++}`;
1755
- hasFooter = signal(false, ...(ngDevMode ? [{ debugName: "hasFooter" }] : []));
1756
- set footerContent(value) {
1757
- this.hasFooter.set(!!value);
1758
- }
1759
- previousActiveElement = null;
1760
- focusableElements = [];
2027
+ footerRef = contentChild('[footer]', { ...(ngDevMode ? { debugName: "footerRef" } : {}), read: ElementRef });
2028
+ hasFooter = computed(() => !!this.footerRef(), ...(ngDevMode ? [{ debugName: "hasFooter" }] : []));
1761
2029
  viewInitialized = signal(false, ...(ngDevMode ? [{ debugName: "viewInitialized" }] : []));
1762
2030
  dialogRef = viewChild('dialog', ...(ngDevMode ? [{ debugName: "dialogRef" }] : []));
1763
2031
  openEffect = effect(() => {
@@ -1767,14 +2035,14 @@ class AfModalComponent {
1767
2035
  this.onOpen();
1768
2036
  }
1769
2037
  else if (!isOpen) {
1770
- this.restoreFocus();
2038
+ this.focusTrap.restoreFocus();
1771
2039
  }
1772
2040
  }, ...(ngDevMode ? [{ debugName: "openEffect" }] : []));
1773
2041
  ngAfterViewInit() {
1774
2042
  this.viewInitialized.set(true);
1775
2043
  }
1776
2044
  ngOnDestroy() {
1777
- this.restoreFocus();
2045
+ this.focusTrap.restoreFocus();
1778
2046
  }
1779
2047
  onEscapeKey() {
1780
2048
  if (this.open()) {
@@ -1784,23 +2052,7 @@ class AfModalComponent {
1784
2052
  onKeydown(event) {
1785
2053
  if (!this.open() || event.key !== 'Tab')
1786
2054
  return;
1787
- this.refreshFocusableElements();
1788
- if (this.focusableElements.length === 0) {
1789
- event.preventDefault();
1790
- this.dialogRef()?.nativeElement.focus();
1791
- return;
1792
- }
1793
- const first = this.focusableElements[0];
1794
- const last = this.focusableElements[this.focusableElements.length - 1];
1795
- const active = document.activeElement;
1796
- if (event.shiftKey && active === first) {
1797
- event.preventDefault();
1798
- last.focus();
1799
- }
1800
- else if (!event.shiftKey && active === last) {
1801
- event.preventDefault();
1802
- first.focus();
1803
- }
2055
+ this.focusTrap.handleTab(event, this.dialogRef()?.nativeElement, this.dialogRef()?.nativeElement);
1804
2056
  }
1805
2057
  onBackdropClick(event) {
1806
2058
  if (this.closeOnBackdropClick() && event.target === event.currentTarget) {
@@ -1811,46 +2063,15 @@ class AfModalComponent {
1811
2063
  this.closed.emit();
1812
2064
  }
1813
2065
  onOpen() {
1814
- this.previousActiveElement = document.activeElement;
1815
- this.refreshFocusableElements();
2066
+ this.focusTrap.saveFocus();
1816
2067
  queueMicrotask(() => {
1817
2068
  if (!this.open())
1818
2069
  return;
1819
- const first = this.focusableElements[0];
1820
- if (first) {
1821
- first.focus();
1822
- }
1823
- else {
1824
- this.dialogRef()?.nativeElement.focus();
1825
- }
2070
+ this.focusTrap.focusFirst(this.dialogRef()?.nativeElement, this.dialogRef()?.nativeElement);
1826
2071
  });
1827
2072
  }
1828
- restoreFocus() {
1829
- if (this.previousActiveElement) {
1830
- this.previousActiveElement.focus();
1831
- this.previousActiveElement = null;
1832
- }
1833
- }
1834
- refreshFocusableElements() {
1835
- const dialog = this.dialogRef()?.nativeElement;
1836
- if (!dialog) {
1837
- this.focusableElements = [];
1838
- return;
1839
- }
1840
- const selectors = [
1841
- 'a[href]',
1842
- 'area[href]',
1843
- 'button:not([disabled])',
1844
- 'input:not([disabled])',
1845
- 'select:not([disabled])',
1846
- 'textarea:not([disabled])',
1847
- '[tabindex]:not([tabindex="-1"])'
1848
- ];
1849
- this.focusableElements = Array.from(dialog.querySelectorAll(selectors.join(',')))
1850
- .filter(el => !el.hasAttribute('disabled') && el.getAttribute('aria-hidden') !== 'true');
1851
- }
1852
2073
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1853
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfModalComponent, isStandalone: true, selector: "af-modal", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, showCloseButton: { classPropertyName: "showCloseButton", publicName: "showCloseButton", isSignal: true, isRequired: false, transformFunction: null }, closeOnBackdropClick: { classPropertyName: "closeOnBackdropClick", publicName: "closeOnBackdropClick", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed" }, host: { listeners: { "document:keydown.escape": "onEscapeKey()", "document:keydown": "onKeydown($event)" } }, queries: [{ propertyName: "footerContent", first: true, predicate: ["[footer]"], descendants: true, read: ElementRef }], viewQueries: [{ propertyName: "dialogRef", first: true, predicate: ["dialog"], descendants: true, isSignal: true }], ngImport: i0, template: `
2074
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfModalComponent, isStandalone: true, selector: "af-modal", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, showCloseButton: { classPropertyName: "showCloseButton", publicName: "showCloseButton", isSignal: true, isRequired: false, transformFunction: null }, closeOnBackdropClick: { classPropertyName: "closeOnBackdropClick", publicName: "closeOnBackdropClick", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed" }, host: { listeners: { "document:keydown.escape": "onEscapeKey()", "document:keydown": "onKeydown($event)" } }, queries: [{ propertyName: "footerRef", first: true, predicate: ["[footer]"], descendants: true, read: ElementRef, isSignal: true }], viewQueries: [{ propertyName: "dialogRef", first: true, predicate: ["dialog"], descendants: true, isSignal: true }], ngImport: i0, template: `
1854
2075
  @if (open()) {
1855
2076
  <div
1856
2077
  class="ct-modal"
@@ -1932,10 +2153,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
1932
2153
  </div>
1933
2154
  }
1934
2155
  `, styles: [":host{display:contents}\n"] }]
1935
- }], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], showCloseButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCloseButton", required: false }] }], closeOnBackdropClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnBackdropClick", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], footerContent: [{
1936
- type: ContentChild,
1937
- args: ['[footer]', { read: ElementRef }]
1938
- }], dialogRef: [{ type: i0.ViewChild, args: ['dialog', { isSignal: true }] }] } });
2156
+ }], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], showCloseButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCloseButton", required: false }] }], closeOnBackdropClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnBackdropClick", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], footerRef: [{ type: i0.ContentChild, args: ['[footer]', { ...{ read: ElementRef }, isSignal: true }] }], dialogRef: [{ type: i0.ViewChild, args: ['dialog', { isSignal: true }] }] } });
1939
2157
 
1940
2158
  /**
1941
2159
  * Toast notification service
@@ -2297,7 +2515,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
2297
2515
  }], propDecorators: { activeTab: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeTab", required: false }] }, { type: i0.Output, args: ["activeTabChange"] }], panels: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => AfTabPanelComponent), { isSignal: true }] }], tabButtons: [{ type: i0.ViewChildren, args: ['tabButton', { isSignal: true }] }] } });
2298
2516
 
2299
2517
  /**
2300
- * Dropdown menu component
2518
+ * Dropdown menu component implementing the WAI-ARIA Menu Pattern.
2519
+ *
2520
+ * Provides full keyboard navigation (Arrow keys, Home/End, type-ahead),
2521
+ * proper ARIA roles (`menu` / `menuitem`), and roving tabindex focus management.
2301
2522
  *
2302
2523
  * @example
2303
2524
  * <af-dropdown
@@ -2308,19 +2529,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
2308
2529
  */
2309
2530
  class AfDropdownComponent {
2310
2531
  static nextId = 0;
2311
- /** Dropdown button label */
2532
+ /** Dropdown button label. */
2312
2533
  label = input('Actions', ...(ngDevMode ? [{ debugName: "label" }] : []));
2313
- /** Menu items */
2534
+ /** Menu items. */
2314
2535
  items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
2315
- /** Item selected event */
2536
+ /** Emits the selected item's value. */
2316
2537
  itemSelected = output();
2317
2538
  triggerRef = viewChild('trigger', ...(ngDevMode ? [{ debugName: "triggerRef" }] : []));
2539
+ menuRef = viewChild('menu', ...(ngDevMode ? [{ debugName: "menuRef" }] : []));
2318
2540
  itemButtons = viewChildren('itemButton', ...(ngDevMode ? [{ debugName: "itemButtons" }] : []));
2319
2541
  isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
2320
- menuId = `af-dropdown-menu-${AfDropdownComponent.nextId++}`;
2542
+ focusedItemIndex = signal(0, ...(ngDevMode ? [{ debugName: "focusedItemIndex" }] : []));
2543
+ instanceId = AfDropdownComponent.nextId++;
2544
+ menuId = `af-dropdown-menu-${this.instanceId}`;
2545
+ triggerId = `af-dropdown-trigger-${this.instanceId}`;
2546
+ typeAheadBuffer = '';
2547
+ typeAheadTimer = null;
2321
2548
  toggle() {
2322
2549
  if (this.isOpen()) {
2323
- this.close();
2550
+ this.close(true);
2324
2551
  }
2325
2552
  else {
2326
2553
  this.open();
@@ -2332,46 +2559,180 @@ class AfDropdownComponent {
2332
2559
  this.close(true);
2333
2560
  }
2334
2561
  }
2335
- open() {
2336
- this.isOpen.set(true);
2337
- queueMicrotask(() => this.focusFirstItem());
2338
- }
2339
- close(returnFocus = false) {
2340
- this.isOpen.set(false);
2341
- if (returnFocus) {
2342
- this.triggerRef()?.nativeElement.focus();
2562
+ /** Handles keyboard events on the trigger button. */
2563
+ onTriggerKeydown(event) {
2564
+ switch (event.key) {
2565
+ case 'ArrowDown':
2566
+ case 'Enter':
2567
+ case ' ':
2568
+ event.preventDefault();
2569
+ if (!this.isOpen()) {
2570
+ this.open();
2571
+ }
2572
+ break;
2573
+ case 'ArrowUp':
2574
+ event.preventDefault();
2575
+ if (!this.isOpen()) {
2576
+ this.open(true);
2577
+ }
2578
+ break;
2343
2579
  }
2344
2580
  }
2345
- focusFirstItem() {
2346
- const first = this.itemButtons().find(ref => !ref.nativeElement.disabled);
2347
- first?.nativeElement.focus();
2581
+ /** Handles keyboard events within the open menu. */
2582
+ onMenuKeydown(event) {
2583
+ const actionableItems = this.getActionableItems();
2584
+ if (actionableItems.length === 0)
2585
+ return;
2586
+ switch (event.key) {
2587
+ case 'ArrowDown': {
2588
+ event.preventDefault();
2589
+ const next = this.nextEnabledIndex(this.focusedItemIndex(), 1);
2590
+ this.focusItem(next);
2591
+ break;
2592
+ }
2593
+ case 'ArrowUp': {
2594
+ event.preventDefault();
2595
+ const prev = this.nextEnabledIndex(this.focusedItemIndex(), -1);
2596
+ this.focusItem(prev);
2597
+ break;
2598
+ }
2599
+ case 'Home': {
2600
+ event.preventDefault();
2601
+ const first = this.nextEnabledIndex(-1, 1);
2602
+ this.focusItem(first);
2603
+ break;
2604
+ }
2605
+ case 'End': {
2606
+ event.preventDefault();
2607
+ const last = this.nextEnabledIndex(actionableItems.length, -1);
2608
+ this.focusItem(last);
2609
+ break;
2610
+ }
2611
+ case 'Escape':
2612
+ event.preventDefault();
2613
+ this.close(true);
2614
+ break;
2615
+ case 'Tab':
2616
+ this.close(false);
2617
+ break;
2618
+ case 'Enter':
2619
+ case ' ': {
2620
+ event.preventDefault();
2621
+ const item = actionableItems[this.focusedItemIndex()];
2622
+ if (item && !item.disabled) {
2623
+ this.selectItem(item);
2624
+ }
2625
+ break;
2626
+ }
2627
+ default:
2628
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
2629
+ this.handleTypeAhead(event.key);
2630
+ }
2631
+ }
2348
2632
  }
2349
2633
  onDocumentClick(event) {
2350
2634
  const target = event.target;
2351
2635
  if (!target.closest('.ct-dropdown')) {
2352
- this.close();
2636
+ this.close(false);
2353
2637
  }
2354
2638
  }
2355
- onEscape() {
2356
- if (this.isOpen()) {
2357
- this.close(true);
2639
+ /**
2640
+ * Returns the index of a non-separator item within the list of
2641
+ * actionable (non-separator) items.
2642
+ */
2643
+ getActionableIndex(item) {
2644
+ return this.getActionableItems().indexOf(item);
2645
+ }
2646
+ open(focusLast = false) {
2647
+ this.isOpen.set(true);
2648
+ const actionableItems = this.getActionableItems();
2649
+ const startIndex = focusLast
2650
+ ? this.nextEnabledIndex(actionableItems.length, -1)
2651
+ : this.nextEnabledIndex(-1, 1);
2652
+ this.focusedItemIndex.set(startIndex);
2653
+ queueMicrotask(() => this.focusCurrent());
2654
+ }
2655
+ close(returnFocus) {
2656
+ if (!this.isOpen())
2657
+ return;
2658
+ this.isOpen.set(false);
2659
+ this.typeAheadBuffer = '';
2660
+ if (returnFocus) {
2661
+ this.triggerRef()?.nativeElement.focus();
2662
+ }
2663
+ }
2664
+ focusItem(index) {
2665
+ this.focusedItemIndex.set(index);
2666
+ this.focusCurrent();
2667
+ }
2668
+ focusCurrent() {
2669
+ const buttons = this.itemButtons();
2670
+ const idx = this.focusedItemIndex();
2671
+ buttons[idx]?.nativeElement.focus();
2672
+ }
2673
+ nextEnabledIndex(from, direction) {
2674
+ const actionableItems = this.getActionableItems();
2675
+ const len = actionableItems.length;
2676
+ if (len === 0)
2677
+ return 0;
2678
+ let idx = from + direction;
2679
+ for (let i = 0; i < len; i++) {
2680
+ if (idx < 0)
2681
+ idx = len - 1;
2682
+ if (idx >= len)
2683
+ idx = 0;
2684
+ if (!actionableItems[idx].disabled)
2685
+ return idx;
2686
+ idx += direction;
2687
+ }
2688
+ return from;
2689
+ }
2690
+ getActionableItems() {
2691
+ return this.items().filter((item) => !item.separator);
2692
+ }
2693
+ handleTypeAhead(char) {
2694
+ if (this.typeAheadTimer) {
2695
+ clearTimeout(this.typeAheadTimer);
2696
+ }
2697
+ this.typeAheadBuffer += char.toLowerCase();
2698
+ this.typeAheadTimer = setTimeout(() => {
2699
+ this.typeAheadBuffer = '';
2700
+ this.typeAheadTimer = null;
2701
+ }, 500);
2702
+ const actionableItems = this.getActionableItems();
2703
+ const startIndex = this.focusedItemIndex() + 1;
2704
+ for (let i = 0; i < actionableItems.length; i++) {
2705
+ const idx = (startIndex + i) % actionableItems.length;
2706
+ const item = actionableItems[idx];
2707
+ if (!item.disabled && item.label.toLowerCase().startsWith(this.typeAheadBuffer)) {
2708
+ this.focusItem(idx);
2709
+ return;
2710
+ }
2358
2711
  }
2359
2712
  }
2360
2713
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfDropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2361
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfDropdownComponent, isStandalone: true, selector: "af-dropdown", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemSelected: "itemSelected" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "itemButtons", predicate: ["itemButton"], descendants: true, isSignal: true }], ngImport: i0, template: `
2714
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfDropdownComponent, isStandalone: true, selector: "af-dropdown", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemSelected: "itemSelected" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "menuRef", first: true, predicate: ["menu"], descendants: true, isSignal: true }, { propertyName: "itemButtons", predicate: ["itemButton"], descendants: true, isSignal: true }], ngImport: i0, template: `
2362
2715
  <div class="ct-dropdown" [attr.data-state]="isOpen() ? 'open' : 'closed'">
2363
2716
  <button
2364
2717
  #trigger
2365
2718
  class="ct-button ct-button--secondary ct-dropdown__trigger"
2366
2719
  [attr.aria-expanded]="isOpen()"
2367
2720
  [attr.aria-controls]="menuId"
2368
- [attr.aria-haspopup]="true"
2721
+ aria-haspopup="menu"
2369
2722
  type="button"
2370
- (click)="toggle()">
2723
+ (click)="toggle()"
2724
+ (keydown)="onTriggerKeydown($event)">
2371
2725
  {{ label() }}
2372
2726
  </button>
2373
2727
  @if (isOpen()) {
2374
- <div class="ct-dropdown__menu" [id]="menuId">
2728
+ <div
2729
+ #menu
2730
+ class="ct-dropdown__menu"
2731
+ [id]="menuId"
2732
+ role="menu"
2733
+ aria-orientation="vertical"
2734
+ [attr.aria-labelledby]="triggerId"
2735
+ (keydown)="onMenuKeydown($event)">
2375
2736
  @for (item of items(); track $index) {
2376
2737
  @if (item.separator) {
2377
2738
  <div class="ct-dropdown__separator" role="separator"></div>
@@ -2379,8 +2740,9 @@ class AfDropdownComponent {
2379
2740
  <button
2380
2741
  #itemButton
2381
2742
  class="ct-dropdown__item"
2382
- [disabled]="item.disabled"
2383
- [attr.aria-disabled]="item.disabled ? true : null"
2743
+ role="menuitem"
2744
+ [attr.tabindex]="focusedItemIndex() === getActionableIndex(item) ? 0 : -1"
2745
+ [attr.aria-disabled]="item.disabled ? 'true' : null"
2384
2746
  type="button"
2385
2747
  (click)="selectItem(item)">
2386
2748
  {{ item.label }}
@@ -2396,7 +2758,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
2396
2758
  type: Component,
2397
2759
  args: [{ selector: 'af-dropdown', changeDetection: ChangeDetectionStrategy.OnPush, host: {
2398
2760
  '(document:click)': 'onDocumentClick($event)',
2399
- '(document:keydown.escape)': 'onEscape()',
2400
2761
  }, template: `
2401
2762
  <div class="ct-dropdown" [attr.data-state]="isOpen() ? 'open' : 'closed'">
2402
2763
  <button
@@ -2404,13 +2765,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
2404
2765
  class="ct-button ct-button--secondary ct-dropdown__trigger"
2405
2766
  [attr.aria-expanded]="isOpen()"
2406
2767
  [attr.aria-controls]="menuId"
2407
- [attr.aria-haspopup]="true"
2768
+ aria-haspopup="menu"
2408
2769
  type="button"
2409
- (click)="toggle()">
2770
+ (click)="toggle()"
2771
+ (keydown)="onTriggerKeydown($event)">
2410
2772
  {{ label() }}
2411
2773
  </button>
2412
2774
  @if (isOpen()) {
2413
- <div class="ct-dropdown__menu" [id]="menuId">
2775
+ <div
2776
+ #menu
2777
+ class="ct-dropdown__menu"
2778
+ [id]="menuId"
2779
+ role="menu"
2780
+ aria-orientation="vertical"
2781
+ [attr.aria-labelledby]="triggerId"
2782
+ (keydown)="onMenuKeydown($event)">
2414
2783
  @for (item of items(); track $index) {
2415
2784
  @if (item.separator) {
2416
2785
  <div class="ct-dropdown__separator" role="separator"></div>
@@ -2418,8 +2787,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
2418
2787
  <button
2419
2788
  #itemButton
2420
2789
  class="ct-dropdown__item"
2421
- [disabled]="item.disabled"
2422
- [attr.aria-disabled]="item.disabled ? true : null"
2790
+ role="menuitem"
2791
+ [attr.tabindex]="focusedItemIndex() === getActionableIndex(item) ? 0 : -1"
2792
+ [attr.aria-disabled]="item.disabled ? 'true' : null"
2423
2793
  type="button"
2424
2794
  (click)="selectItem(item)">
2425
2795
  {{ item.label }}
@@ -2430,7 +2800,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
2430
2800
  }
2431
2801
  </div>
2432
2802
  `, styles: [":host{display:inline-block}\n"] }]
2433
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], itemSelected: [{ type: i0.Output, args: ["itemSelected"] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], itemButtons: [{ type: i0.ViewChildren, args: ['itemButton', { isSignal: true }] }] } });
2803
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], itemSelected: [{ type: i0.Output, args: ["itemSelected"] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], menuRef: [{ type: i0.ViewChild, args: ['menu', { isSignal: true }] }], itemButtons: [{ type: i0.ViewChildren, args: ['itemButton', { isSignal: true }] }] } });
2434
2804
 
2435
2805
  /**
2436
2806
  * Pagination component
@@ -3490,7 +3860,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
3490
3860
  * <af-badge variant="danger">Blocked</af-badge>
3491
3861
  */
3492
3862
  class AfBadgeComponent {
3493
- /** Color variant */
3863
+ /** Accessible label, useful when the badge has no visible text. */
3864
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
3865
+ /** Color variant. */
3494
3866
  variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : []));
3495
3867
  /** Icon character to display */
3496
3868
  icon = input('', ...(ngDevMode ? [{ debugName: "icon" }] : []));
@@ -3507,32 +3879,32 @@ class AfBadgeComponent {
3507
3879
  return classes.join(' ');
3508
3880
  }, ...(ngDevMode ? [{ debugName: "badgeClasses" }] : []));
3509
3881
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3510
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfBadgeComponent, isStandalone: true, selector: "af-badge", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
3511
- <span [class]="badgeClasses()">
3882
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfBadgeComponent, isStandalone: true, selector: "af-badge", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
3883
+ <span [class]="badgeClasses()" [attr.aria-label]="ariaLabel() || null">
3512
3884
  @if (icon()) {
3513
3885
  <span class="ct-badge__icon" aria-hidden="true">{{ icon() }}</span>
3514
3886
  }
3515
3887
  @if (dot()) {
3516
3888
  <span class="ct-badge__dot" aria-hidden="true"></span>
3517
3889
  }
3518
- <ng-content></ng-content>
3890
+ <ng-content />
3519
3891
  </span>
3520
3892
  `, isInline: true, styles: [":host{display:inline-block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3521
3893
  }
3522
3894
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfBadgeComponent, decorators: [{
3523
3895
  type: Component,
3524
3896
  args: [{ selector: 'af-badge', changeDetection: ChangeDetectionStrategy.OnPush, template: `
3525
- <span [class]="badgeClasses()">
3897
+ <span [class]="badgeClasses()" [attr.aria-label]="ariaLabel() || null">
3526
3898
  @if (icon()) {
3527
3899
  <span class="ct-badge__icon" aria-hidden="true">{{ icon() }}</span>
3528
3900
  }
3529
3901
  @if (dot()) {
3530
3902
  <span class="ct-badge__dot" aria-hidden="true"></span>
3531
3903
  }
3532
- <ng-content></ng-content>
3904
+ <ng-content />
3533
3905
  </span>
3534
3906
  `, styles: [":host{display:inline-block}\n"] }]
3535
- }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }] } });
3907
+ }], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }] } });
3536
3908
 
3537
3909
  /**
3538
3910
  * Progress bar for showing completion state
@@ -4130,6 +4502,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
4130
4502
  */
4131
4503
  class AfDrawerComponent {
4132
4504
  static nextId = 0;
4505
+ focusTrap = new FocusTrap();
4133
4506
  /** Two-way bindable open state */
4134
4507
  open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
4135
4508
  /** Slide-in position */
@@ -4149,8 +4522,6 @@ class AfDrawerComponent {
4149
4522
  /** Unique ID for aria-labelledby fallback */
4150
4523
  titleId = `af-drawer-title-${AfDrawerComponent.nextId++}`;
4151
4524
  panelRef = viewChild('panel', ...(ngDevMode ? [{ debugName: "panelRef" }] : []));
4152
- previousActiveElement = null;
4153
- focusableElements = [];
4154
4525
  containerClasses = computed(() => {
4155
4526
  const classes = ['ct-drawer'];
4156
4527
  const pos = this.position();
@@ -4174,7 +4545,7 @@ class AfDrawerComponent {
4174
4545
  }, ...(ngDevMode ? [{ debugName: "openEffect" }] : []));
4175
4546
  ngOnDestroy() {
4176
4547
  this.unlockBodyScroll();
4177
- this.restoreFocus();
4548
+ this.focusTrap.restoreFocus();
4178
4549
  }
4179
4550
  /** Closes the drawer and emits the closed event */
4180
4551
  close() {
@@ -4194,53 +4565,23 @@ class AfDrawerComponent {
4194
4565
  return;
4195
4566
  }
4196
4567
  if (event.key === 'Tab') {
4197
- this.trapFocus(event);
4568
+ const panel = this.panelRef()?.nativeElement;
4569
+ this.focusTrap.handleTab(event, panel, panel);
4198
4570
  }
4199
4571
  }
4200
4572
  onOpen() {
4201
- this.previousActiveElement = document.activeElement;
4573
+ this.focusTrap.saveFocus();
4202
4574
  this.lockBodyScroll();
4203
4575
  queueMicrotask(() => {
4204
4576
  if (!this.open())
4205
4577
  return;
4206
- this.refreshFocusableElements();
4207
- const first = this.focusableElements[0];
4208
- if (first) {
4209
- first.focus();
4210
- }
4211
- else {
4212
- this.panelRef()?.nativeElement.focus();
4213
- }
4578
+ const panel = this.panelRef()?.nativeElement;
4579
+ this.focusTrap.focusFirst(panel, panel);
4214
4580
  });
4215
4581
  }
4216
4582
  onClose() {
4217
4583
  this.unlockBodyScroll();
4218
- this.restoreFocus();
4219
- }
4220
- trapFocus(event) {
4221
- this.refreshFocusableElements();
4222
- if (this.focusableElements.length === 0) {
4223
- event.preventDefault();
4224
- this.panelRef()?.nativeElement.focus();
4225
- return;
4226
- }
4227
- const first = this.focusableElements[0];
4228
- const last = this.focusableElements[this.focusableElements.length - 1];
4229
- const active = document.activeElement;
4230
- if (event.shiftKey && active === first) {
4231
- event.preventDefault();
4232
- last.focus();
4233
- }
4234
- else if (!event.shiftKey && active === last) {
4235
- event.preventDefault();
4236
- first.focus();
4237
- }
4238
- }
4239
- restoreFocus() {
4240
- if (this.previousActiveElement) {
4241
- this.previousActiveElement.focus();
4242
- this.previousActiveElement = null;
4243
- }
4584
+ this.focusTrap.restoreFocus();
4244
4585
  }
4245
4586
  lockBodyScroll() {
4246
4587
  document.body.style.overflow = 'hidden';
@@ -4248,24 +4589,6 @@ class AfDrawerComponent {
4248
4589
  unlockBodyScroll() {
4249
4590
  document.body.style.overflow = '';
4250
4591
  }
4251
- refreshFocusableElements() {
4252
- const panel = this.panelRef()?.nativeElement;
4253
- if (!panel) {
4254
- this.focusableElements = [];
4255
- return;
4256
- }
4257
- const selectors = [
4258
- 'a[href]',
4259
- 'area[href]',
4260
- 'button:not([disabled])',
4261
- 'input:not([disabled])',
4262
- 'select:not([disabled])',
4263
- 'textarea:not([disabled])',
4264
- '[tabindex]:not([tabindex="-1"])',
4265
- ];
4266
- this.focusableElements = Array.from(panel.querySelectorAll(selectors.join(','))).filter((el) => !el.hasAttribute('disabled') &&
4267
- el.getAttribute('aria-hidden') !== 'true');
4268
- }
4269
4592
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfDrawerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4270
4593
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfDrawerComponent, isStandalone: true, selector: "af-drawer", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, showCloseButton: { classPropertyName: "showCloseButton", publicName: "showCloseButton", isSignal: true, isRequired: false, transformFunction: null }, closeOnBackdropClick: { classPropertyName: "closeOnBackdropClick", publicName: "closeOnBackdropClick", isSignal: true, isRequired: false, transformFunction: null }, closeButtonAriaLabel: { classPropertyName: "closeButtonAriaLabel", publicName: "closeButtonAriaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", closed: "closed" }, host: { listeners: { "document:keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: `
4271
4594
  <div
@@ -5234,7 +5557,7 @@ class AfFileUploadComponent {
5234
5557
  {{ liveAnnouncement() }}
5235
5558
  </span>
5236
5559
  </div>
5237
- `, isInline: true, styles: [":host{display:block}.af-file-upload__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "component", type: AfButtonComponent, selector: "af-button", inputs: ["variant", "size", "type", "disabled", "iconOnly", "ariaLabel", "title"], outputs: ["clicked"] }, { kind: "component", type: AfBadgeComponent, selector: "af-badge", inputs: ["variant", "icon", "dot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5560
+ `, isInline: true, styles: [":host{display:block}.af-file-upload__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "component", type: AfButtonComponent, selector: "af-button", inputs: ["variant", "size", "type", "disabled", "iconOnly", "ariaLabel", "title"], outputs: ["clicked"] }, { kind: "component", type: AfBadgeComponent, selector: "af-badge", inputs: ["ariaLabel", "variant", "icon", "dot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5238
5561
  }
5239
5562
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfFileUploadComponent, decorators: [{
5240
5563
  type: Component,
@@ -5872,17 +6195,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
5872
6195
 
5873
6196
  let nextId = 0;
5874
6197
  /**
5875
- * Individual navigation item used within af-navbar.
6198
+ * Individual navigation item used within af-navbar or af-toolbar.
6199
+ *
6200
+ * Supports Angular Router via `routerLink`, standard links via `href`,
6201
+ * and button mode when neither is provided. Content projection allows
6202
+ * icons and custom markup inside the link.
5876
6203
  *
5877
6204
  * @example
5878
- * <af-nav-item label="Dashboard" href="/dashboard" [active]="true" />
6205
+ * <af-nav-item label="Dashboard" routerLink="/dashboard">
6206
+ * <af-icon name="dashboard" /> Dashboard
6207
+ * </af-nav-item>
5879
6208
  */
5880
6209
  class AfNavItemComponent {
5881
- /** Text label for the navigation item. */
6210
+ /** Text label shown as fallback when no content is projected. Also used by the mobile menu. */
5882
6211
  label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
5883
6212
  /** URL for the navigation link. Renders as `<a>` when provided, `<button>` otherwise. */
5884
6213
  href = input('', ...(ngDevMode ? [{ debugName: "href" }] : []));
5885
- /** Marks this item as the currently active page. */
6214
+ /** Angular Router link. Renders as `<a>` with routerLink and auto-active detection. */
6215
+ routerLink = input(null, ...(ngDevMode ? [{ debugName: "routerLink" }] : []));
6216
+ /** Marks this item as the currently active page (used for href/button mode, routerLink auto-detects). */
5886
6217
  active = input(false, { ...(ngDevMode ? { debugName: "active" } : {}), transform: booleanAttribute });
5887
6218
  /** Disables interaction with this item. */
5888
6219
  disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : {}), transform: booleanAttribute });
@@ -5903,76 +6234,106 @@ class AfNavItemComponent {
5903
6234
  this.clicked.emit(event);
5904
6235
  }
5905
6236
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfNavItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5906
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfNavItemComponent, isStandalone: true, selector: "af-nav-item", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, viewQueries: [{ propertyName: "linkRef", first: true, predicate: ["linkEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
6237
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfNavItemComponent, isStandalone: true, selector: "af-nav-item", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, viewQueries: [{ propertyName: "linkRef", first: true, predicate: ["linkEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
5907
6238
  <li class="ct-navbar__item" role="none">
5908
- @if (href()) {
6239
+ @if (routerLink()) {
6240
+ <a
6241
+ #linkEl
6242
+ class="ct-navbar__link"
6243
+ [routerLink]="routerLink()!"
6244
+ routerLinkActive="ct-navbar__link--active"
6245
+ role="menuitem"
6246
+ [attr.aria-disabled]="disabled() || null"
6247
+ [attr.tabindex]="rovingTabindex()"
6248
+ (click)="onClick($event)">
6249
+ <ng-content>{{ label() }}</ng-content>
6250
+ </a>
6251
+ } @else if (href()) {
5909
6252
  <a
5910
6253
  #linkEl
5911
6254
  class="ct-navbar__link"
6255
+ [class.ct-navbar__link--active]="active()"
5912
6256
  [href]="href()"
5913
6257
  role="menuitem"
5914
6258
  [attr.aria-current]="active() ? 'page' : null"
5915
6259
  [attr.aria-disabled]="disabled() || null"
5916
6260
  [attr.tabindex]="rovingTabindex()"
5917
6261
  (click)="onClick($event)">
5918
- {{ label() }}
6262
+ <ng-content>{{ label() }}</ng-content>
5919
6263
  </a>
5920
6264
  } @else {
5921
6265
  <button
5922
6266
  #linkEl
5923
6267
  class="ct-navbar__link"
6268
+ [class.ct-navbar__link--active]="active()"
5924
6269
  type="button"
5925
6270
  role="menuitem"
5926
6271
  [attr.aria-current]="active() ? 'page' : null"
5927
6272
  [attr.aria-disabled]="disabled() || null"
5928
6273
  [attr.tabindex]="rovingTabindex()"
5929
6274
  (click)="onClick($event)">
5930
- {{ label() }}
6275
+ <ng-content>{{ label() }}</ng-content>
5931
6276
  </button>
5932
6277
  }
5933
6278
  </li>
5934
- `, isInline: true, styles: [":host{display:contents}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6279
+ `, isInline: true, styles: [":host{display:contents}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5935
6280
  }
5936
6281
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfNavItemComponent, decorators: [{
5937
6282
  type: Component,
5938
- args: [{ selector: 'af-nav-item', changeDetection: ChangeDetectionStrategy.OnPush, template: `
6283
+ args: [{ selector: 'af-nav-item', changeDetection: ChangeDetectionStrategy.OnPush, imports: [RouterLink, RouterLinkActive], template: `
5939
6284
  <li class="ct-navbar__item" role="none">
5940
- @if (href()) {
6285
+ @if (routerLink()) {
6286
+ <a
6287
+ #linkEl
6288
+ class="ct-navbar__link"
6289
+ [routerLink]="routerLink()!"
6290
+ routerLinkActive="ct-navbar__link--active"
6291
+ role="menuitem"
6292
+ [attr.aria-disabled]="disabled() || null"
6293
+ [attr.tabindex]="rovingTabindex()"
6294
+ (click)="onClick($event)">
6295
+ <ng-content>{{ label() }}</ng-content>
6296
+ </a>
6297
+ } @else if (href()) {
5941
6298
  <a
5942
6299
  #linkEl
5943
6300
  class="ct-navbar__link"
6301
+ [class.ct-navbar__link--active]="active()"
5944
6302
  [href]="href()"
5945
6303
  role="menuitem"
5946
6304
  [attr.aria-current]="active() ? 'page' : null"
5947
6305
  [attr.aria-disabled]="disabled() || null"
5948
6306
  [attr.tabindex]="rovingTabindex()"
5949
6307
  (click)="onClick($event)">
5950
- {{ label() }}
6308
+ <ng-content>{{ label() }}</ng-content>
5951
6309
  </a>
5952
6310
  } @else {
5953
6311
  <button
5954
6312
  #linkEl
5955
6313
  class="ct-navbar__link"
6314
+ [class.ct-navbar__link--active]="active()"
5956
6315
  type="button"
5957
6316
  role="menuitem"
5958
6317
  [attr.aria-current]="active() ? 'page' : null"
5959
6318
  [attr.aria-disabled]="disabled() || null"
5960
6319
  [attr.tabindex]="rovingTabindex()"
5961
6320
  (click)="onClick($event)">
5962
- {{ label() }}
6321
+ <ng-content>{{ label() }}</ng-content>
5963
6322
  </button>
5964
6323
  }
5965
6324
  </li>
5966
6325
  `, styles: [":host{display:contents}\n"] }]
5967
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clicked: [{ type: i0.Output, args: ["clicked"] }], linkRef: [{ type: i0.ViewChild, args: ['linkEl', { isSignal: true }] }] } });
6326
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clicked: [{ type: i0.Output, args: ["clicked"] }], linkRef: [{ type: i0.ViewChild, args: ['linkEl', { isSignal: true }] }] } });
5968
6327
  /**
5969
6328
  * Responsive navbar with mobile menu, keyboard navigation, and ARIA landmarks.
5970
6329
  *
5971
6330
  * @example
5972
6331
  * <af-navbar ariaLabel="Main navigation">
5973
6332
  * <a brand class="ct-navbar__brand" href="/">My App</a>
5974
- * <af-nav-item label="Dashboard" href="/dashboard" [active]="true" />
5975
- * <af-nav-item label="Settings" href="/settings" />
6333
+ * <af-nav-item label="Dashboard" routerLink="/dashboard">
6334
+ * <af-icon name="dashboard" /> Dashboard
6335
+ * </af-nav-item>
6336
+ * <af-nav-item label="Settings" routerLink="/settings" />
5976
6337
  * <button actions class="ct-button">Profile</button>
5977
6338
  * </af-navbar>
5978
6339
  */
@@ -6195,7 +6556,19 @@ class AfNavbarComponent {
6195
6556
  role="menu"
6196
6557
  aria-label="Mobile navigation">
6197
6558
  @for (item of items(); track item) {
6198
- @if (item.href()) {
6559
+ @if (item.routerLink(); as rl) {
6560
+ <a
6561
+ #mobileLink
6562
+ class="ct-navbar__link"
6563
+ [routerLink]="rl"
6564
+ routerLinkActive="ct-navbar__link--active"
6565
+ role="menuitem"
6566
+ [attr.aria-disabled]="item.disabled() || null"
6567
+ [attr.tabindex]="mobileMenuOpen() ? 0 : -1"
6568
+ (click)="onMobileItemClick($event, item)">
6569
+ {{ item.label() }}
6570
+ </a>
6571
+ } @else if (item.href()) {
6199
6572
  <a
6200
6573
  #mobileLink
6201
6574
  class="ct-navbar__link"
@@ -6223,11 +6596,11 @@ class AfNavbarComponent {
6223
6596
  }
6224
6597
  </div>
6225
6598
  </header>
6226
- `, isInline: true, styles: [":host{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6599
+ `, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6227
6600
  }
6228
6601
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfNavbarComponent, decorators: [{
6229
6602
  type: Component,
6230
- args: [{ selector: 'af-navbar', changeDetection: ChangeDetectionStrategy.OnPush, host: {
6603
+ args: [{ selector: 'af-navbar', changeDetection: ChangeDetectionStrategy.OnPush, imports: [RouterLink, RouterLinkActive], host: {
6231
6604
  '(keydown)': 'handleKeydown($event)',
6232
6605
  '(document:click)': 'onDocumentClick($event)',
6233
6606
  }, template: `
@@ -6268,7 +6641,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
6268
6641
  role="menu"
6269
6642
  aria-label="Mobile navigation">
6270
6643
  @for (item of items(); track item) {
6271
- @if (item.href()) {
6644
+ @if (item.routerLink(); as rl) {
6645
+ <a
6646
+ #mobileLink
6647
+ class="ct-navbar__link"
6648
+ [routerLink]="rl"
6649
+ routerLinkActive="ct-navbar__link--active"
6650
+ role="menuitem"
6651
+ [attr.aria-disabled]="item.disabled() || null"
6652
+ [attr.tabindex]="mobileMenuOpen() ? 0 : -1"
6653
+ (click)="onMobileItemClick($event, item)">
6654
+ {{ item.label() }}
6655
+ </a>
6656
+ } @else if (item.href()) {
6272
6657
  <a
6273
6658
  #mobileLink
6274
6659
  class="ct-navbar__link"
@@ -6347,6 +6732,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
6347
6732
  */
6348
6733
  class AfPopoverComponent {
6349
6734
  static nextId = 0;
6735
+ focusTrap = new FocusTrap();
6350
6736
  /** Two-way bindable open state. */
6351
6737
  open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
6352
6738
  /** Preferred position relative to the trigger. Flips automatically when space is insufficient. */
@@ -6370,8 +6756,6 @@ class AfPopoverComponent {
6370
6756
  wrapperRef = viewChild('wrapper', ...(ngDevMode ? [{ debugName: "wrapperRef" }] : []));
6371
6757
  contentRef = viewChild('popoverContent', ...(ngDevMode ? [{ debugName: "contentRef" }] : []));
6372
6758
  triggerDirective = contentChild(AfPopoverTriggerDirective, ...(ngDevMode ? [{ debugName: "triggerDirective" }] : []));
6373
- previousActiveElement = null;
6374
- focusableElements = [];
6375
6759
  flippedSide = signal(null, ...(ngDevMode ? [{ debugName: "flippedSide" }] : []));
6376
6760
  /** Effective side after auto-flip evaluation. */
6377
6761
  activeSide = computed(() => this.flippedSide() ?? this.position(), ...(ngDevMode ? [{ debugName: "activeSide" }] : []));
@@ -6392,7 +6776,7 @@ class AfPopoverComponent {
6392
6776
  }
6393
6777
  }, ...(ngDevMode ? [{ debugName: "openEffect" }] : []));
6394
6778
  ngOnDestroy() {
6395
- this.restoreFocus();
6779
+ this.focusTrap.restoreFocus();
6396
6780
  }
6397
6781
  /** Toggle the popover open state. */
6398
6782
  toggle() {
@@ -6421,59 +6805,29 @@ class AfPopoverComponent {
6421
6805
  return;
6422
6806
  const trigger = this.triggerDirective()?.elementRef.nativeElement;
6423
6807
  if (trigger) {
6424
- this.previousActiveElement = trigger;
6808
+ this.focusTrap.setReturnFocus(trigger);
6425
6809
  }
6426
6810
  this.close();
6427
6811
  }
6428
6812
  onKeydown(event) {
6429
6813
  if (!this.open() || event.key !== 'Tab')
6430
6814
  return;
6431
- this.trapFocus(event);
6815
+ const content = this.contentRef()?.nativeElement;
6816
+ this.focusTrap.handleTab(event, content, content);
6432
6817
  }
6433
6818
  onOpen() {
6434
- this.previousActiveElement = document.activeElement;
6819
+ this.focusTrap.saveFocus();
6435
6820
  this.flippedSide.set(this.computeFlippedSide());
6436
6821
  queueMicrotask(() => {
6437
6822
  if (!this.open())
6438
6823
  return;
6439
- this.refreshFocusableElements();
6440
- const first = this.focusableElements[0];
6441
- if (first) {
6442
- first.focus();
6443
- }
6444
- else {
6445
- this.contentRef()?.nativeElement.focus();
6446
- }
6824
+ const content = this.contentRef()?.nativeElement;
6825
+ this.focusTrap.focusFirst(content, content);
6447
6826
  });
6448
6827
  }
6449
6828
  onClose() {
6450
6829
  this.flippedSide.set(null);
6451
- this.restoreFocus();
6452
- }
6453
- restoreFocus() {
6454
- if (this.previousActiveElement) {
6455
- this.previousActiveElement.focus();
6456
- this.previousActiveElement = null;
6457
- }
6458
- }
6459
- trapFocus(event) {
6460
- this.refreshFocusableElements();
6461
- if (this.focusableElements.length === 0) {
6462
- event.preventDefault();
6463
- this.contentRef()?.nativeElement.focus();
6464
- return;
6465
- }
6466
- const first = this.focusableElements[0];
6467
- const last = this.focusableElements[this.focusableElements.length - 1];
6468
- const active = document.activeElement;
6469
- if (event.shiftKey && active === first) {
6470
- event.preventDefault();
6471
- last.focus();
6472
- }
6473
- else if (!event.shiftKey && active === last) {
6474
- event.preventDefault();
6475
- first.focus();
6476
- }
6830
+ this.focusTrap.restoreFocus();
6477
6831
  }
6478
6832
  computeFlippedSide() {
6479
6833
  const trigger = this.triggerDirective()?.elementRef.nativeElement;
@@ -6508,24 +6862,6 @@ class AfPopoverComponent {
6508
6862
  return opposite;
6509
6863
  return preferred;
6510
6864
  }
6511
- refreshFocusableElements() {
6512
- const content = this.contentRef()?.nativeElement;
6513
- if (!content) {
6514
- this.focusableElements = [];
6515
- return;
6516
- }
6517
- const selectors = [
6518
- 'a[href]',
6519
- 'area[href]',
6520
- 'button:not([disabled])',
6521
- 'input:not([disabled])',
6522
- 'select:not([disabled])',
6523
- 'textarea:not([disabled])',
6524
- '[tabindex]:not([tabindex="-1"])',
6525
- ];
6526
- this.focusableElements = Array.from(content.querySelectorAll(selectors.join(','))).filter((el) => !el.hasAttribute('disabled') &&
6527
- el.getAttribute('aria-hidden') !== 'true');
6528
- }
6529
6865
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfPopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6530
6866
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfPopoverComponent, isStandalone: true, selector: "af-popover", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, showArrow: { classPropertyName: "showArrow", publicName: "showArrow", isSignal: true, isRequired: false, transformFunction: null }, closeOnClickOutside: { classPropertyName: "closeOnClickOutside", publicName: "closeOnClickOutside", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", closed: "closed" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscapeKey()", "document:keydown": "onKeydown($event)" } }, providers: [
6531
6867
  { provide: AF_POPOVER, useExisting: forwardRef(() => AfPopoverComponent) },
@@ -7385,5 +7721,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
7385
7721
  * Generated bundle index. Do not edit.
7386
7722
  */
7387
7723
 
7388
- export { AfAccordionComponent, AfAccordionItemComponent, AfAlertComponent, AfAvatarComponent, AfBadgeComponent, AfBannerComponent, AfBreadcrumbsComponent, AfButtonComponent, AfCardComponent, AfCellDefDirective, AfCheckboxComponent, AfChipInputComponent, AfComboboxComponent, AfDataTableComponent, AfDatepickerComponent, AfDividerComponent, AfDrawerComponent, AfDropdownComponent, AfEmptyStateComponent, AfFieldComponent, AfFileUploadComponent, AfFormatLabelPipe, AfIconComponent, AfInputComponent, AfModalComponent, AfNavItemComponent, AfNavbarComponent, AfPaginationComponent, AfPopoverComponent, AfPopoverTriggerDirective, AfProgressBarComponent, AfRadioComponent, AfSelectComponent, AfSelectMenuComponent, AfSidebarComponent, AfSkeletonComponent, AfSkipLinkComponent, AfSliderComponent, AfSpinnerComponent, AfSwitchComponent, AfTabPanelComponent, AfTableBodyComponent, AfTableCellComponent, AfTableComponent, AfTableHeaderCellComponent, AfTableHeaderComponent, AfTableRowComponent, AfTabsComponent, AfTextareaComponent, AfToastContainerComponent, AfToastService, AfToggleGroupComponent, AfToolbarComponent, AfTooltipDirective };
7724
+ export { AfAccordionComponent, AfAccordionItemComponent, AfAlertComponent, AfAvatarComponent, AfBadgeComponent, AfBannerComponent, AfBreadcrumbsComponent, AfButtonComponent, AfCardComponent, AfCellDefDirective, AfCheckboxComponent, AfChipInputComponent, AfComboboxComponent, AfDataTableComponent, AfDatepickerComponent, AfDividerComponent, AfDrawerComponent, AfDropdownComponent, AfEmptyStateComponent, AfFieldComponent, AfFileUploadComponent, AfFormatLabelPipe, AfIconComponent, AfInputComponent, AfModalComponent, AfNavItemComponent, AfNavbarComponent, AfPaginationComponent, AfPopoverComponent, AfPopoverTriggerDirective, AfProgressBarComponent, AfRadioComponent, AfRadioGroupComponent, AfSelectComponent, AfSelectMenuComponent, AfSidebarComponent, AfSkeletonComponent, AfSkipLinkComponent, AfSliderComponent, AfSpinnerComponent, AfSwitchComponent, AfTabPanelComponent, AfTableBodyComponent, AfTableCellComponent, AfTableComponent, AfTableHeaderCellComponent, AfTableHeaderComponent, AfTableRowComponent, AfTabsComponent, AfTextareaComponent, AfToastContainerComponent, AfToastService, AfToggleGroupComponent, AfToolbarComponent, AfTooltipDirective };
7389
7725
  //# sourceMappingURL=neuravision-ng-construct.mjs.map