Skip to content

Commit

Permalink
chore: updates page load focus management (#4740)
Browse files Browse the repository at this point in the history
* Updates to check for visibility of the form control

* Updates comments

* Updats comments

* Removes a  comment

* Fixes a typo
  • Loading branch information
thiessenp-cds authored Nov 28, 2024
1 parent 885b985 commit 0b9ff2c
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { ErrorSaving } from "@formBuilder/components/shared/ErrorSaving";
import { toast } from "@formBuilder/components/shared/Toast";
import { defaultForm } from "@lib/store/defaults";
import { showReviewPage } from "@lib/utils/form-builder/showReviewPage";
import { focusElement } from "@lib/client/clientHelpers";
import { tryFocusOnPageLoad } from "@lib/client/clientHelpers";
import { useIsFormClosed } from "@lib/hooks/useIsFormClosed";

export const Preview = ({
Expand Down Expand Up @@ -202,7 +202,7 @@ export const Preview = ({
{allowGrouping && isShowReviewPage && (
<BackButton
language={language}
onClick={() => focusElement("h2")}
onClick={() => tryFocusOnPageLoad("h2")}
/>
)}
<Button
Expand Down
4 changes: 2 additions & 2 deletions components/clientComponents/forms/NextButton/NextButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Language } from "@lib/types/form-builder-types";

import { getLocalizedProperty } from "@lib/utils";
import { showReviewPage } from "@lib/utils/form-builder/showReviewPage";
import { focusElement } from "@lib/client/clientHelpers";
import { tryFocusOnPageLoad } from "@lib/client/clientHelpers";
import { useFormDelay } from "@lib/hooks/useFormDelayContext";

export const NextButton = ({
Expand Down Expand Up @@ -93,7 +93,7 @@ export const NextButton = ({
if (await handleValidation()) {
updateFormDelay(formRecord.form, currentGroup);
handleNextAction();
focusElement("h2");
tryFocusOnPageLoad("h2");
}
}}
>
Expand Down
45 changes: 35 additions & 10 deletions lib/client/clientHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,43 @@ export const scrollToBottom = (containerEl: HTMLElement) => {
};

/**
* When a function does not have access to an element (ref), use the DOM to get an element and
* after the next tick (after the current event loop), focus the element. This moves the page to
* the element location and announces the text in the element to AT. Use a react ref over this to
* get and focus an element when possible.
* @param elementSelector selector to get the DOM element (first one found). Defaults to H1. Must
* have a tabIndex.
* @returns undefined
* Tries to focus the first form control or if that fails a fallback selector. This is part of focus
* management to set a page load entry point using a DOM focus for AT users. If a form control
* exists it is preferred over a fallback selector like a heading, this is to avoid confusion
* e.g. #4690
*
* Note: the setTimeout is a hack to help sequence the query selector after the DOM loads. Use a
* react ref over this method if possible.
*/
export const focusElement = (elementSelector = "H1") => {
const NEXT_TICK = 0;
export const tryFocusOnPageLoad = (fallbackSelector = "H1") => {
const NEXT_TICK = 4;
setTimeout(() => {
(document.querySelector(elementSelector) as HTMLElement)?.focus();
let focusEl = null;

// Note: checkVisibility() is still fairly new so we should check if available, if not, use the
// fallback instead. Because controls may or may not be nested in a parent element that is used
// for controlling visibility, determining an elements visibility using the computed style etc.
// is tricky and error prone. The WebAPI is probably the best reliable way.
// https://chromestatus.com/feature/5163102852087808
// https://developer.mozilla.org/en-US/docs/Web/API/Element/checkVisibility#browser_compatibility
if (window.document.documentElement.checkVisibility !== undefined) {
// Get the list of form controls in order, if any and take the first visible one
// Note: the CSS selector could be more efficient but e.g. not all form controls are nested
// in a form element.
const formControlEls = document.querySelectorAll(
"input, textarea, select"
) as NodeListOf<HTMLElement>;
const visibleElements = Array.from(formControlEls).filter((element) =>
element.checkVisibility()
);
focusEl = visibleElements.length > 0 ? visibleElements[0] : null;
}

if (!focusEl) {
focusEl = document.querySelector(fallbackSelector) as HTMLElement;
}

focusEl?.focus();
}, NEXT_TICK);
};

Expand Down

0 comments on commit 0b9ff2c

Please sign in to comment.