-
-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(design): create focus stack service (#2570)
Co-authored-by: Damien Retzinger <[email protected]>
- Loading branch information
1 parent
5814c62
commit 68aeffc
Showing
3 changed files
with
180 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export { daffFocusableElementsSelector } from './focusable-elements'; | ||
export { DaffFocusStackService } from './stack.service'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |