Skip to content

Commit

Permalink
va-omb-info, va-crisis-line-modal, va-minimal-header: Fix for focus t…
Browse files Browse the repository at this point in the history
…rapping (#951)

* Fix for focus trapping for OMB-info and CLM
Detects if focus is 'focused' on the modal-triggering buttons within the components
Redirects focus back to the Modal if it is visible

* Version bump

* Improved focus trap modal detection
Still contains several console.logs due to test debugging

* Fix for not finding this.el correctly

* Removing console.logs

* Remove unneeded waitForChanges

* Focus Trapping Improvements
Adding Focus Trapping to minimal header
Adding shift+tab focus trap handlers for all needed components

* Updating tests
  • Loading branch information
Andrew565 authored Nov 29, 2023
1 parent d74bf96 commit bf563e0
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Host, State, h } from '@stencil/core';
import { Component, Host, State, h, Element, Listen } from '@stencil/core';
import arrowRightSvg from '../../assets/arrow-right-white.svg';
import { CONTACTS } from '../../contacts';

Expand All @@ -14,8 +14,15 @@ import { CONTACTS } from '../../contacts';
shadow: true,
})
export class VACrisisLineModal {
@Element() el: HTMLElement;

@State() isOpen: boolean = false;

/**
* Local state to track if the shift key is pressed
*/
@State() shifted: boolean = false;

setVisible() {
this.isOpen = true;
}
Expand All @@ -24,12 +31,40 @@ export class VACrisisLineModal {
this.isOpen = false;
}

// This keydown event listener tracks if the shift key is held down while changing focus
@Listen('keydown', { target: 'window' })
trackShiftKey(e: KeyboardEvent) {
this.shifted = e.shiftKey;
}

// Redirects focus back to the modal, if the modal is open/visible
private trapFocus() {
const modal = this.el?.shadowRoot.querySelector('va-modal');
const modalVisible = modal?.getAttribute('visible');

if (modalVisible !== null && modalVisible !== 'false') {
let focusedChild;
const query = this.shifted
? '.last-focusable-child'
: '[role="document"]';
if (this.shifted) {
focusedChild = modal
?.querySelector(query) as HTMLElement;
} else {
focusedChild = modal?.shadowRoot.querySelector(query) as HTMLElement;
}

focusedChild?.focus();
}
}

render() {
return (
<Host>
<div class="va-crisis-line-container">
<button
onClick={() => this.setVisible()}
onFocusin={() => this.trapFocus()}
data-show="#modal-crisisline"
class="va-crisis-line va-overlay-trigger"
part="button"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Component, Host, h, Prop} from '@stencil/core';
import {
Component,
Host,
h,
Prop,
State,
Element,
Listen,
} from '@stencil/core';
import vaSeal from '../../assets/va-seal.svg';

/**
Expand All @@ -12,19 +20,49 @@ import vaSeal from '../../assets/va-seal.svg';
styleUrl: 'va-minimal-header.scss',
shadow: true,
})

export class VaMinimalHeader {
@Element() el: HTMLElement;

/**
* Local state to track if the shift key is pressed
*/
@State() shifted: boolean = false;

@Prop() header?: string;
@Prop() subheader?: string;

// This keydown event listener tracks if the shift key is held down while changing focus
@Listen('keydown', { target: 'window' })
trackShiftKey(e: KeyboardEvent) {
this.shifted = e.shiftKey;
}

// Redirects focus back to the modal, if the modal is open/visible
private trapFocus() {
const modal = this.el?.shadowRoot.querySelector('va-crisis-line-modal').shadowRoot.querySelector('va-modal');
const modalVisible = modal?.getAttribute('visible');

if (modalVisible !== null && modalVisible !== 'false') {
let focusedChild;
const query = this.shifted ? '.last-focusable-child' : '.va-modal-close';
if (this.shifted) {
focusedChild = modal?.querySelector(query) as HTMLElement;
} else {
focusedChild = modal?.shadowRoot.querySelector(query) as HTMLElement;
}

focusedChild?.focus();
}
}

render() {
const {header, subheader} = this;
const { header, subheader } = this;

return (
<Host role="banner">
<va-official-gov-banner />
<va-crisis-line-modal />
<div class="va-header">
<div onFocusin={() => this.trapFocus()} class="va-header">
<a href="/" title="Go to VA.gov" class="va-logo-link">
<img class="va-logo" src={vaSeal} alt="VA logo and Seal, U.S. Department of Veterans Affairs" />
</a>
Expand All @@ -36,5 +74,4 @@ export class VaMinimalHeader {
</Host>
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('va-modal', () => {
<va-modal class="hydrated" modal-title="Example Title" visible="">
<mock:shadow-root>
<div aria-describedby="modal-content" aria-label="Example Title modal" aria-modal="true" class="va-modal-inner" role="dialog">
<button aria-label="Close Example Title modal" class="va-modal-close" type="button">
<button aria-label="Close Example Title modal" class="last-focusable-child va-modal-close" type="button">
<i aria-hidden="true"></i>
</button>
<div class="va-modal-body">
Expand Down Expand Up @@ -289,7 +289,7 @@ describe('va-modal', () => {
<mock:shadow-root>
<div aria-label="Example Title modal" aria-modal="true" class="usa-modal" role="dialog">
<div class="usa-modal__content">
<button aria-label="Close Example Title modal" class="va-modal-close" type="button">
<button aria-label="Close Example Title modal" class="last-focusable-child va-modal-close" type="button">
<i aria-hidden="true"></i>
</button>
<div class="usa-modal__main">
Expand Down
10 changes: 7 additions & 3 deletions packages/web-components/src/components/va-modal/va-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export class VaModal {
/**
* Save focusable children within the modal. Populated on setup
*/
@State() focusableChildren: HTMLElement[] = null;
focusableChildren: HTMLElement[] = null;

// This click event listener is used to close the modal when clickToClose
// is true and the user clicks the overlay outside of the modal contents.
Expand Down Expand Up @@ -344,11 +344,15 @@ export class VaModal {
// find all focusable children within the modal, but maintain tab order
this.focusableChildren = this.getFocusableChildren();

// find last focusable item so that focus can be redirected there when needed
const lastFocusChild = this.focusableChildren[this.focusableChildren.length - 1];
lastFocusChild.classList.add("last-focusable-child");

// If an initialFocusSelector is provided, the element will be focused on modal open
// if it exists. You are able to focus elements in both light and shadow DOM.
const initialFocus = (this.el.querySelector(this.initialFocusSelector) ||
this.el.shadowRoot.querySelector(this.initialFocusSelector) ||
this.closeButton) as HTMLElement;
this.el.shadowRoot.querySelector(this.initialFocusSelector) ||
this.closeButton) as HTMLElement;
initialFocus.focus();

// Prevents scrolling outside modal
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Host, h, Prop, State, Element } from '@stencil/core';
import { Component, Host, h, Prop, State, Element, Listen } from '@stencil/core';

/**
* @componentName OMB info
Expand All @@ -20,6 +20,11 @@ export class VaOmbInfo {
*/
@State() visible: boolean;

/**
* Local state to track if the shift key is pressed
*/
@State() shifted: boolean = false;

/* eslint-disable i18next/no-literal-string */
/**
* The name of the benefit displayed in the Respondent Burden section of the Privacy Act Statement.
Expand Down Expand Up @@ -50,6 +55,34 @@ export class VaOmbInfo {
this.modalContents = null;
};

// This keydown event listener tracks if the shift key is held down while changing focus
@Listen('keydown', { target: 'window' })
trackShiftKey (e: KeyboardEvent) {
this.shifted = e.shiftKey;
}

// Redirects focus back to the modal, if the modal is open/visible
private trapFocus() {
const modal = this.el?.shadowRoot.querySelector('va-modal');
const modalVisible = modal?.getAttribute('visible');

if (modalVisible !== null && modalVisible !== 'false') {
let focusedChild;
const query = this.shifted
? '.last-focusable-child'
: '[role="document"]';
if (this.shifted) {
focusedChild = modal
?.querySelector('va-telephone')
.shadowRoot.querySelector(query) as HTMLElement;
} else {
focusedChild = modal?.shadowRoot.querySelector(query) as HTMLElement;
}

focusedChild?.focus();
}
}

componentWillLoad() {
/* eslint-disable i18next/no-literal-string */
this.modalContents = (
Expand Down Expand Up @@ -136,6 +169,7 @@ export class VaOmbInfo {
<div>
<va-button
onClick={toggleModalVisible}
onFocusin={() => this.trapFocus()}
secondary
text="View Privacy Act Statement"
/>
Expand Down
1 change: 1 addition & 0 deletions packages/web-components/src/utils/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* https://github.com/KittyGiraudel/focusable-selectors/blob/main/index.js
*/
export const focusableQueryString = [
'a[href]:not([tabindex^="-"])',
'.hydrated:not([tabindex^="-"])',
'[tabindex]:not([tabindex^="-"])',
'input:not([type=hidden]):not([tabindex^="-"])',
Expand Down

0 comments on commit bf563e0

Please sign in to comment.