Skip to content

Commit

Permalink
feat(design): create focus stack service (#2570)
Browse files Browse the repository at this point in the history
Co-authored-by: Damien Retzinger <[email protected]>
  • Loading branch information
xelaint and damienwebdev committed Sep 26, 2023
1 parent 5814c62 commit 68aeffc
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 0 deletions.
1 change: 1 addition & 0 deletions libs/design/src/core/focus/public_api.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { daffFocusableElementsSelector } from './focusable-elements';
export { DaffFocusStackService } from './stack.service';
108 changes: 108 additions & 0 deletions libs/design/src/core/focus/stack.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Component } from '@angular/core';
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { DaffFocusStackService } from './stack.service';

@Component({
template: `
<button id="one">one</button>
<button id="two">two</button>
<button id="three">three</button>
<button id="four">four</button>
`,
})
export class FakeComponent {}

describe('DaffFocusStackService', () => {
let stack: DaffFocusStackService;
let wrapper: ComponentFixture<FakeComponent>;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
DaffFocusStackService,
],
declarations: [
FakeComponent,
],
});

wrapper = TestBed.createComponent(FakeComponent);
stack = TestBed.inject(DaffFocusStackService);
});


it('should be created', () => {
expect(stack).toBeTruthy();
expect(stack.length()).toEqual(0);
});

describe('focus', () => {
it('should not error in the event there is nothing in the stack', () => {
expect(() => stack.focus()).not.toThrowError();
});

it('it should focus on the item at the top of the stack ', () => {
const one = wrapper.debugElement.query(By.css('#one')).nativeElement;
const two = wrapper.debugElement.query(By.css('#two')).nativeElement;
expect(two).toBeInstanceOf(HTMLElement);

stack.push(one);
stack.push(two);
stack.focus();

expect(document.activeElement).toEqual(two);
});
});

describe('pop and push', () => {
it('it should add an item at the top of the stack', () => {
const one = wrapper.debugElement.query(By.css('#one')).nativeElement;
const two = wrapper.debugElement.query(By.css('#two')).nativeElement;
expect(two).toBeInstanceOf(HTMLElement);

stack.push(one);
stack.push(two);

expect(stack.length()).toEqual(2);
expect(stack.pop()).toEqual(two);
});
});

describe('pop', () => {
it('should pop an element off the stack and return it', () => {
const one = wrapper.debugElement.query(By.css('#one')).nativeElement;

stack.push(one);
expect(stack.length()).toEqual(1);
expect(stack.pop()).toEqual(one);
expect(stack.length()).toEqual(0);
});

it('should return the activeElement (in chrome, the body) if you pop an empty stack', () => {
expect(stack.pop()).toEqual(<HTMLElement>document.activeElement);
});

it('should focus the popped element if called with no arguments', () => {
const one = wrapper.debugElement.query(By.css('#one')).nativeElement;

stack.push(one);
expect(one).not.toEqual(document.activeElement);
stack.pop();
expect(one).toEqual(document.activeElement);
});

it('should not focus the popped element if pop is called with false', () => {
const one = wrapper.debugElement.query(By.css('#one')).nativeElement;

stack.push(one);
expect(one).not.toEqual(document.activeElement);
stack.pop(false);
expect(one).not.toEqual(document.activeElement);
});
});
});
71 changes: 71 additions & 0 deletions libs/design/src/core/focus/stack.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { DOCUMENT } from '@angular/common';
import {
Inject,
Injectable,
} from '@angular/core';

@Injectable({ providedIn: 'root' })
export class DaffFocusStackService {
private _stack: HTMLElement[] = [];

constructor(@Inject(DOCUMENT) private document: any) {

}

/**
* Return the current length of the stack.
*/
length(): number {
return this._stack.length;
}

/**
* Adds a HTML element to a focus stack and returns the new length of the stack.
*
* Generally, you will probably want to call this before you transition focus
* onto a new element.
*
* ```ts
* this._focusStack.push(this._doc.activeElement);
* ```
*/
push(el: HTMLElement): number {
this._stack.push(el);
return this._stack.length;
}

/**
* Focuses on the HTML element at the top of a stack.
*
* ```ts
* this._focusStack.push(this._doc.activeElement);
* ```
*/
focus() {
if(this._stack.length >= 1) {
this._stack.slice(-1)[0].focus();
} else {
(<HTMLElement>this.document.activeElement).blur();
}
}

/**
* Removes the HMTL element at the top of a stack and focuses on it.
*/
pop(focus: boolean = true): HTMLElement {
let el = this._stack.pop();
while(el === undefined && this._stack.length > 0) {
el = this._stack.pop();
}

if(el) {
if(focus) {
el.focus();
}
return el;
}

(<HTMLElement>this.document.activeElement).blur();
return this.document.activeElement;
}
}

0 comments on commit 68aeffc

Please sign in to comment.