diff --git a/libs/design/src/core/roving-tab-index/incremental-hydration.integration.spec.ts b/libs/design/src/core/roving-tab-index/incremental-hydration.integration.spec.ts
new file mode 100644
index 0000000000..6194394f8b
--- /dev/null
+++ b/libs/design/src/core/roving-tab-index/incremental-hydration.integration.spec.ts
@@ -0,0 +1,64 @@
+import {
+ Component,
+ signal,
+} from '@angular/core';
+import {
+ ComponentFixture,
+ DeferBlockBehavior,
+ DeferBlockFixture,
+ DeferBlockState,
+ TestBed,
+} from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+
+import { DaffRovingTabIndexBoundaryDirective } from './roving-tab-index-boundary.directive';
+import { DaffRovingTabIndexDirective } from './roving-tab-index.directive';
+
+@Component({
+ template: `
+
+ @defer (hydrate on interaction) {
+
+ }
+
+ `,
+ imports: [
+ DaffRovingTabIndexBoundaryDirective,
+ DaffRovingTabIndexDirective,
+ ],
+})
+class WrapperComponent {
+ groupValue = signal('group');
+}
+
+describe('@daffodil/design/core | Roving Tab Index | Incremental Hydration', () => {
+ let wrapper: WrapperComponent;
+ let fixture: ComponentFixture;
+ let deferBlockFixture: DeferBlockFixture;
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ imports: [
+ WrapperComponent,
+ ],
+ deferBlockBehavior: DeferBlockBehavior.Manual,
+ });
+
+ fixture = TestBed.createComponent(WrapperComponent);
+ wrapper = fixture.componentInstance;
+ fixture.detectChanges();
+ deferBlockFixture = (await fixture.getDeferBlocks())[0];
+ });
+
+ // not functional as a mocked test, needs e2e to properly test
+ xit('should update the child to match the parent after deferred hydration', async () => {
+ await deferBlockFixture.render(DeferBlockState.Complete);
+
+ wrapper.groupValue.set('updated-group');
+ await fixture.whenStable();
+
+ const child = fixture.debugElement.query(By.css('.child'));
+
+ expect(child.attributes['data-rti']).toEqual('updated-group');
+ });
+});
diff --git a/libs/design/src/core/roving-tab-index/roving-tab-index-boundary.directive.ts b/libs/design/src/core/roving-tab-index/roving-tab-index-boundary.directive.ts
index 76238a6dbc..8b56618ffe 100644
--- a/libs/design/src/core/roving-tab-index/roving-tab-index-boundary.directive.ts
+++ b/libs/design/src/core/roving-tab-index/roving-tab-index-boundary.directive.ts
@@ -1,9 +1,8 @@
-import { CdkTrapFocus } from '@angular/cdk/a11y';
import {
computed,
Directive,
- effect,
forwardRef,
+ inject,
input,
} from '@angular/core';
@@ -25,7 +24,6 @@ import { DaffRovingTabIndexDirective } from './roving-tab-index.directive';
'(keydown.space)': 'enterGroup($event)',
},
hostDirectives: [
- CdkTrapFocus,
DaffRovingTabIndexDirective,
],
providers: [
@@ -36,22 +34,19 @@ import { DaffRovingTabIndexDirective } from './roving-tab-index.directive';
],
})
export class DaffRovingTabIndexBoundaryDirective implements DaffRovingTabIndexBoundary {
- /**
- * Don't touch this directly. Use `_uniqueId`.
- */
- private static _uniqueIdCounter = 0;
-
/**
* Don't touch this directly. Use `_uniqueId`.
*/
private _cachedUniqueId: string | undefined;
private get _uniqueId(): string {
if (!this._cachedUniqueId) {
- this._cachedUniqueId = `ε-rtiBoundary-${DaffRovingTabIndexBoundaryDirective._uniqueIdCounter++}`;
+ this._cachedUniqueId = `ε-rtiBoundary-${crypto.randomUUID()}`;
}
return this._cachedUniqueId;
}
+ private readonly groupService = inject(DaffRovingTabIndexService);
+
/**
* The name of the group for which that this element will act as boundary.
* Optional, will be autogenerated to a unique name if omitted.
@@ -62,15 +57,6 @@ export class DaffRovingTabIndexBoundaryDirective implements DaffRovingTabIndexBo
*/
readonly effectiveBoundary = computed(() => this.rtiBoundary() || this._uniqueId);
- constructor(
- private groupService: DaffRovingTabIndexService,
- private focusTrap: CdkTrapFocus,
- ) {
- effect(() => {
- this.focusTrap.enabled = this.effectiveBoundary() === this.groupService.group();
- });
- }
-
/**
* @docs-private
*/
diff --git a/libs/design/src/core/roving-tab-index/roving-tab-index-group.service.ts b/libs/design/src/core/roving-tab-index/roving-tab-index-group.service.ts
index 0226930683..b59c49fa22 100644
--- a/libs/design/src/core/roving-tab-index/roving-tab-index-group.service.ts
+++ b/libs/design/src/core/roving-tab-index/roving-tab-index-group.service.ts
@@ -63,26 +63,4 @@ export class DaffRovingTabIndexService {
: ary[index === ary.length - 1 ? 0 : index + 1]).focus();
}
}
-
- onKeydown(evt: Event) {
- if ('key' in evt) {
- switch ((evt).key) {
- case 'ArrowUp':
- case 'ArrowDown':
- if (this._group()) {
- evt.preventDefault();
- const ary = Array.from(this.document.querySelectorAll(`[data-rti="${this._group()}"]`));
- const index = ary.findIndex((el) => el === this.document.activeElement);
- (this.document.activeElement).blur();
- ((evt).key === 'ArrowUp'
- ? ary[index === 0 ? ary.length - 1 : index - 1]
- : ary[index === ary.length - 1 ? 0 : index + 1]).focus();
- }
- break;
-
- default:
- break;
- }
- }
- }
}
diff --git a/libs/design/src/core/roving-tab-index/roving-tab-index.directive.spec.ts b/libs/design/src/core/roving-tab-index/roving-tab-index.directive.spec.ts
index 87e910b4b5..9312e4357d 100644
--- a/libs/design/src/core/roving-tab-index/roving-tab-index.directive.spec.ts
+++ b/libs/design/src/core/roving-tab-index/roving-tab-index.directive.spec.ts
@@ -78,8 +78,9 @@ describe('@daffodil/design | DaffRovingTabIndexDirective', () => {
expect(directive.group()).toEqual('');
});
- describe('when the escape key is pressed', () => {
+ describe('when the escape key is pressed when inside a group', () => {
beforeEach(() => {
+ groupSpy.set('test');
(de.nativeElement).dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
fixture.detectChanges();
});
@@ -89,8 +90,9 @@ describe('@daffodil/design | DaffRovingTabIndexDirective', () => {
});
});
- describe('when the up arrow is pressed', () => {
+ describe('when the up arrow is pressed when inside a group', () => {
beforeEach(() => {
+ groupSpy.set('test');
(de.nativeElement).dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }));
fixture.detectChanges();
});
@@ -100,8 +102,9 @@ describe('@daffodil/design | DaffRovingTabIndexDirective', () => {
});
});
- describe('when the down arrow is pressed', () => {
+ describe('when the down arrow is pressed when inside a group', () => {
beforeEach(() => {
+ groupSpy.set('test');
(de.nativeElement).dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
fixture.detectChanges();
});
@@ -110,6 +113,74 @@ describe('@daffodil/design | DaffRovingTabIndexDirective', () => {
expect(serviceSpy.next).toHaveBeenCalledWith();
});
});
+
+ describe('when the up arrow is pressed when not inside a group', () => {
+ beforeEach(() => {
+ (de.nativeElement).dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }));
+ fixture.detectChanges();
+ });
+
+ it('should not navigate to the previous target', () => {
+ expect(serviceSpy.previous).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when the down arrow is pressed when not inside a group', () => {
+ beforeEach(() => {
+ (de.nativeElement).dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
+ fixture.detectChanges();
+ });
+
+ it('should not navigate to the next target', () => {
+ expect(serviceSpy.next).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when the tab key is pressed when inside a group', () => {
+ beforeEach(() => {
+ groupSpy.set('test');
+ (de.nativeElement).dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
+ fixture.detectChanges();
+ });
+
+ it('should navigate to the next target', () => {
+ expect(serviceSpy.next).toHaveBeenCalledWith();
+ });
+ });
+
+ describe('when the tab plus shift key is pressed when inside a group', () => {
+ beforeEach(() => {
+ groupSpy.set('test');
+ (de.nativeElement).dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }));
+ fixture.detectChanges();
+ });
+
+ it('should navigate to the previous target', () => {
+ expect(serviceSpy.previous).toHaveBeenCalledWith();
+ });
+ });
+
+ describe('when the tab key is pressed when not inside a group', () => {
+ beforeEach(() => {
+ (de.nativeElement).dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
+ fixture.detectChanges();
+ });
+
+ it('should not navigate to the previous target', () => {
+ expect(serviceSpy.previous).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when the tab plus shift key is pressed when not inside a group', () => {
+ beforeEach(() => {
+ (de.nativeElement).dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }));
+ fixture.detectChanges();
+ });
+
+ it('should not navigate to the next target', () => {
+ expect(serviceSpy.next).not.toHaveBeenCalled();
+ });
+ });
});
@Component({
diff --git a/libs/design/src/core/roving-tab-index/roving-tab-index.directive.ts b/libs/design/src/core/roving-tab-index/roving-tab-index.directive.ts
index b5feb39286..e80da9539f 100644
--- a/libs/design/src/core/roving-tab-index/roving-tab-index.directive.ts
+++ b/libs/design/src/core/roving-tab-index/roving-tab-index.directive.ts
@@ -1,9 +1,11 @@
import {
+ afterNextRender,
computed,
Directive,
Inject,
input,
Optional,
+ signal,
SkipSelf,
} from '@angular/core';
@@ -26,9 +28,13 @@ import { DaffRovingTabIndexService } from './roving-tab-index-group.service';
'(keydown.escape)': 'leaveGroup($event)',
'(keydown.arrowup)': 'previous($event)',
'(keydown.arrowdown)': 'next($event)',
+ '(keydown.tab)': 'next($event)',
+ '(keydown.shift.tab)': 'previous($event)',
},
})
export class DaffRovingTabIndexDirective {
+ private readonly _hydrationWorkaround = signal(false);
+
/**
* Allows the RTI group to be overriden.
* By default it will be the nearest ancestor or the default root group if no boundary ancestor exists.
@@ -39,7 +45,11 @@ export class DaffRovingTabIndexDirective {
* The group in which this RTI target resides.
* See {@link DaffRovingTabIndexBoundaryDirective} to make an element act as the boundary of an RTI group.
*/
- readonly group = computed(() => this.rti() || this.parent?.effectiveBoundary() || '');
+ readonly group = computed(() =>
+ this._hydrationWorkaround()
+ ? this.rti() || this.parent?.effectiveBoundary() || ''
+ : this.rti() || this.parent?.effectiveBoundary() || '',
+ );
/**
* @docs-private
*/
@@ -52,29 +62,42 @@ export class DaffRovingTabIndexDirective {
constructor(
private service: DaffRovingTabIndexService,
@Optional() @SkipSelf() @Inject(DAFF_ROVING_TAB_INDEX_BOUNDARY) private parent: DaffRovingTabIndexBoundary,
- ) {}
+ ) {
+ afterNextRender({
+ read: () => this._hydrationWorkaround.set(true),
+ });
+ }
/**
* @docs-private
*/
- leaveGroup(evt: Event) {
- evt.stopPropagation();
- this.service.leave();
+ protected leaveGroup(evt: Event) {
+ if (this.service.group()) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ this.service.leave();
+ }
}
/**
* @docs-private
*/
- next(evt: Event) {
- evt.stopPropagation();
- this.service.next();
+ protected next(evt: Event) {
+ if (this.service.group()) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ this.service.next();
+ }
}
/**
* @docs-private
*/
- previous(evt: Event) {
- evt.stopPropagation();
- this.service.previous();
+ protected previous(evt: Event) {
+ if (this.service.group()) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ this.service.previous();
+ }
}
}