Skip to content
This repository has been archived by the owner on Dec 8, 2022. It is now read-only.

Fix wait not blocking focus nav #4

Merged
merged 29 commits into from
Dec 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
44c02c9
moved over changes from skyux2 PR
blackbaud-conorwright Sep 25, 2018
80f6e5c
fixed a11y issue with test fixture for wait
blackbaud-conorwright Sep 25, 2018
97d27be
Merge branch 'master' into fix-wait-not-blocking-focus-nav
blackbaud-conorwright Sep 26, 2018
2022723
Merge branch 'master' into fix-wait-not-blocking-focus-nav
blackbaud-conorwright Sep 27, 2018
8acdfa3
Merge remote-tracking branch 'origin/master' into fix-wait-not-blocki…
blackbaud-conorwright Oct 15, 2018
ef2ca41
fixed while condition and addressed pr comments
blackbaud-conorwright Oct 17, 2018
9754cdc
Merge branch 'master' into fix-wait-not-blocking-focus-nav
blackbaud-conorwright Oct 18, 2018
ac1d214
fixed tab nav temporarily focusing inside waited element
blackbaud-conorwright Oct 24, 2018
37057fa
fixed switching wait type while waiting.Added focus nav wrap
blackbaud-conorwright Oct 24, 2018
61437ef
updated unit tests for new tab propagation strat
blackbaud-conorwright Oct 24, 2018
1d0d0c3
fixed tslint issues
blackbaud-conorwright Oct 25, 2018
36e971f
trying out a condition
blackbaud-conorwright Oct 25, 2018
9eebda1
fixed missing coverage
blackbaud-conorwright Oct 25, 2018
219e256
addressed constructor misses
blackbaud-conorwright Oct 30, 2018
9b042e9
cleaned up the visual demo a little for wait
blackbaud-conorwright Oct 30, 2018
5cd9fbc
fixed the issue Alex found with shift-tab nav
blackbaud-conorwright Oct 30, 2018
96f85ac
reimplemented to only maintain 1 listener
blackbaud-conorwright Nov 1, 2018
dd142f4
fixed the focus nav not ignoring hidden elements
blackbaud-conorwright Nov 3, 2018
03902f0
Merge branch 'master' into fix-wait-not-blocking-focus-nav
blackbaud-conorwright Nov 13, 2018
853bd53
Merge branch 'master' into fix-wait-not-blocking-focus-nav
Blackbaud-SteveBrush Nov 19, 2018
b588b0f
implemented focusin strategy (#21)
blackbaud-conorwright Dec 6, 2018
4d63c89
fixed failing test
blackbaud-conorwright Dec 6, 2018
6ff1289
removed unused import
blackbaud-conorwright Dec 7, 2018
c34ba87
switched back to using id instead of waitcmp
blackbaud-conorwright Dec 11, 2018
14f23b1
renamed id parameter
blackbaud-conorwright Dec 13, 2018
e2c20b4
Made id private
blackbaud-conorwright Dec 13, 2018
284d524
moved to private area
blackbaud-conorwright Dec 13, 2018
836d73f
removed leftover code. Added additional coverage
blackbaud-conorwright Dec 13, 2018
6cd6698
Merge branch 'fix-wait-not-blocking-focus-nav' of github.com:blackbau…
blackbaud-conorwright Dec 13, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions e2e/wait.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ describe('Wait', () => {
SkyHostBrowser.get('visual/wait');
SkyHostBrowser.setWindowBreakpoint('lg');
element(by.css('.sky-test-full-page')).click();
element(by.css('.sky-test-wait')).click();
expect('.sky-wait-demo').toMatchBaselineScreenshot(done, {
screenshotName: 'sky-wait-demo-full-page'
});
Expand All @@ -55,9 +54,8 @@ describe('Wait', () => {
it('should display non-blocking wait on full page', (done) => {
SkyHostBrowser.get('visual/wait');
SkyHostBrowser.setWindowBreakpoint('lg');
element(by.css('.sky-test-full-page')).click();
element(by.css('.sky-test-non-blocking')).click();
element(by.css('.sky-test-wait')).click();
element(by.css('.sky-test-full-page')).click();
expect('.sky-wait-demo').toMatchBaselineScreenshot(done, {
screenshotName: 'sky-wait-demo-full-page-non-block'
});
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"devDependencies": {
"@blackbaud/skyux": "2.30.0",
"@blackbaud/skyux-builder": "1.29.0",
"@skyux-sdk/builder-plugin-skyux": "1.0.0-rc.5"
"@skyux-sdk/builder-plugin-skyux": "1.0.0-rc.5",
"@skyux-sdk/testing": "^3.1.0"
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
53 changes: 48 additions & 5 deletions src/app/public/modules/wait/fixtures/wait.component.fixture.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,52 @@
<div class="sky-wait-test-component">
<button *ngIf="showAnchor0"
class="sky-btn sky-btn-secondary"
id="anchor-0"
href="/"
[ngStyle]="{
'display': anchor0Display,
'visibility': anchor0Visibility
}"
>
Anchor tag 0
</button>
<button
class="sky-btn sky-btn-secondary"
id="anchor-1"
href="/"
>
Anchor tag 1
</button>
<div class="sky-wait-test-component">
<button
class='sky-btn sky-btn-primary'
id="inside-button"
>
I am button
</button>
<sky-wait
[ariaLabel]="ariaLabel"
[isFullPage]="isFullPage"
[isNonBlocking]="isNonBlocking"
[isWaiting]="isWaiting"
>
</sky-wait>
</div>
<div>
<button *ngIf="showAnchor2"
class="sky-btn sky-btn-secondary"
id="anchor-2"
href="/"
[ngStyle]="{
'display': anchor2Display,
'visibility': anchor2Visibility
}"
>
Anchor tag 2
</button>
<sky-wait
[ariaLabel]="ariaLabel"
[isFullPage]="isFullPage"
[isNonBlocking]="isNonBlocking"
[isWaiting]="isWaiting"
[isFullPage]="false"
[isNonBlocking]="false"
[isWaiting]="secondWaitIsWaiting"
>
</sky-wait>
</div>
30 changes: 21 additions & 9 deletions src/app/public/modules/wait/fixtures/wait.component.fixture.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
import {
Component,
Input
ViewChild
} from '@angular/core';

import {
SkyWaitComponent
} from '..';

@Component({
selector: 'sky-test-cmp',
templateUrl: './wait.component.fixture.html'
})
export class SkyWaitTestComponent {
@Input()
public isWaiting: boolean;
public ariaLabel: string;

@Input()
public isFullPage: boolean;
public isWaiting: boolean = false;
public isFullPage: boolean = false;
public isNonBlocking: boolean = false;

@Input()
public isNonBlocking: boolean;
public showAnchor0 = true;
public showAnchor2 = true;

@Input()
public ariaLabel: string;
public anchor0Visibility: string = '';
public anchor0Display: string = '';

public anchor2Visibility: string = '';
public anchor2Display: string = '';

public secondWaitIsWaiting: boolean = false;

@ViewChild(SkyWaitComponent)
public waitComponent: SkyWaitComponent;
}
198 changes: 192 additions & 6 deletions src/app/public/modules/wait/wait-adapter.service.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,219 @@
import {
ElementRef,
Injectable,
Renderer
Renderer,
OnDestroy
} from '@angular/core';

@Injectable()
export class SkyWaitAdapterService {
export class SkyWaitAdapterService implements OnDestroy {
private static isPageWaitActive: boolean = false;
private static busyElements: {[key: string]: {busyEl: HTMLElement, listener: any}} = {};

constructor(private renderer: Renderer) { }
private focussableElements: HTMLElement[];

public setWaitBounds(waitEl: ElementRef) {
constructor(
private renderer: Renderer
) { }

public ngOnDestroy(): void {
this.clearListeners();
}

public setWaitBounds(waitEl: ElementRef): void {
this.renderer.setElementStyle(waitEl.nativeElement.parentElement, 'position', 'relative');
}

public removeWaitBounds(waitEl: ElementRef) {
public removeWaitBounds(waitEl: ElementRef): void {
this.renderer.setElementStyle(waitEl.nativeElement.parentElement, 'position', undefined);
}

public setBusyState(
waitEl: ElementRef,
isFullPage: boolean,
isWaiting: boolean,
isNonBlocking = false
isNonBlocking = false,
waitComponentId?: string
): void {
let busyEl = isFullPage ? document.body : waitEl.nativeElement.parentElement;
let state = isWaiting ? 'true' : undefined;

if (!isNonBlocking) {
this.renderer.setElementAttribute(busyEl, 'aria-busy', state);

if (isWaiting) {
// Remove focus from page when full page blocking wait
if (isFullPage || busyEl.contains(document.activeElement)) {
this.clearDocumentFocus();
}

if (isFullPage) {
SkyWaitAdapterService.isPageWaitActive = true;
let endListenerFunc = this.renderer.listen(
document.body,
'keydown',
(event: KeyboardEvent) => {
if (event.key.toLowerCase() === 'tab') {
(event.target as any).blur();
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
this.clearDocumentFocus();
}
});
SkyWaitAdapterService.busyElements[waitComponentId] = {
listener: endListenerFunc,
busyEl: undefined
};
} else {
let endListenerFunc = this.renderer.listen(
busyEl,
'focusin',
(event: KeyboardEvent) => {
if (!isNonBlocking) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();

if (SkyWaitAdapterService.isPageWaitActive) {
this.clearDocumentFocus();
} else {
// Propagate tab navigation if attempted into waited element
let target: any = event.target;
target.blur();
this.focusNextElement(target, this.isShift(event), busyEl);
}
}
});
SkyWaitAdapterService.busyElements[waitComponentId] = {
listener: endListenerFunc,
busyEl: busyEl
};
}
} else {
if (isFullPage) {
SkyWaitAdapterService.isPageWaitActive = false;
}
if (waitComponentId in SkyWaitAdapterService.busyElements) {
SkyWaitAdapterService.busyElements[waitComponentId].listener();
delete SkyWaitAdapterService.busyElements[waitComponentId];
}
}
}
}

private focusNextElement(targetElement: HTMLElement, shiftKey: boolean, busyEl: Element): void {
let focussable = this.getFocussableElements();

// If shift tab, go in the other direction
let modifier = shiftKey ? -1 : 1;
blackbaud-conorwright marked this conversation as resolved.
Show resolved Hide resolved

// Find the next navigable element that isn't waiting
let startingIndex = focussable.indexOf(targetElement);
let curIndex = startingIndex + modifier;
while (focussable[curIndex] && this.isElementBusyOrHidden(focussable[curIndex])) {
curIndex += modifier;
}

if (focussable[curIndex] && !this.isElementBusyOrHidden(focussable[curIndex])) {
focussable[curIndex].focus();
} else {
// Try wrapping the navigation
curIndex = modifier > 0 ? 0 : focussable.length - 1;
while (
curIndex !== startingIndex &&
focussable[curIndex] &&
this.isElementBusyOrHidden(focussable[curIndex])
) {
/* istanbul ignore next */
/* sanity check */
curIndex += modifier;
}

/* istanbul ignore else */
/* sanity check */
if (focussable[curIndex] && !this.isElementBusyOrHidden(focussable[curIndex])) {
focussable[curIndex].focus();
} else {
// No valid target, wipe focus
this.clearDocumentFocus();
}
}

// clear focussableElements list
this.focussableElements = undefined;
}

private isShift(event: Event): boolean {
// Determine if shift+tab was used based on element order
let elements = this.getFocussableElements().filter(elem => !this.isElementHidden(elem));

let previousInd = elements.indexOf((event as any).relatedTarget);
let currentInd = elements.indexOf(event.target as HTMLElement);

return previousInd === currentInd + 1
|| (previousInd === 0 && currentInd === elements.length - 1)
|| (previousInd > currentInd)
|| !(event as any).relatedTarget;
}

private isElementHidden(element: any): boolean {
const style = window.getComputedStyle(element);
return style.display === 'none' || style.visibility === 'hidden';
}

private isElementBusyOrHidden(element: any): boolean {
if (this.isElementHidden(element)) {
return true;
}

for (let key of Object.keys(SkyWaitAdapterService.busyElements)) {
const parentElement = SkyWaitAdapterService.busyElements[key].busyEl;
if (parentElement && parentElement.contains(element)) {
return true;
}
}
return false;
}

private clearDocumentFocus(): void {
if (document.activeElement && (document.activeElement as any).blur) {
(document.activeElement as any).blur();
}
document.body.focus();
}

private getFocussableElements(): HTMLElement[] {
// Keep this cached so we can reduce querys
if (this.focussableElements) {
return this.focussableElements;
}

// Select all possible focussable elements
let focussableElements =
'a[href], ' +
Blackbaud-AlexKingman marked this conversation as resolved.
Show resolved Hide resolved
'area[href], ' +
'input:not([disabled]):not([tabindex=\'-1\']), ' +
'button:not([disabled]):not([tabindex=\'-1\']), ' +
'select:not([disabled]):not([tabindex=\'-1\']), ' +
'textarea:not([disabled]):not([tabindex=\'-1\']), ' +
'iframe, object, embed, ' +
'*[tabindex]:not([tabindex=\'-1\']), ' +
Blackbaud-AlexKingman marked this conversation as resolved.
Show resolved Hide resolved
'*[contenteditable=true]';

this.focussableElements = Array.prototype.filter.call(
document.body.querySelectorAll(focussableElements),
(element: any) => {
return element.offsetWidth > 0 || element.offsetHeight > 0 || element === document.activeElement;
});
return this.focussableElements;
}

private clearListeners(): void {
SkyWaitAdapterService.isPageWaitActive = false;
for (let key of Object.keys(SkyWaitAdapterService.busyElements)) {
SkyWaitAdapterService.busyElements[key].listener();
delete SkyWaitAdapterService.busyElements[key];
}
}
}
Loading