src/applications/appeals/shared/utils/focus.js
import {
defaultFocusSelector,
focusElement,
focusByOrder,
scrollTo,
scrollToFirstError,
waitForRenderThenFocus,
scrollAndFocus,
} from '~/platform/utilities/ui';
import { focusReview } from '~/platform/forms-system/src/js/utilities/ui/focus-review';
import { $, $$ } from '~/platform/forms-system/src/js/utilities/ui';
import { LAST_ISSUE } from '../constants';
export const focusFirstError = (_index, root) => {
const error = $('[error], .usa-input-error', root);
if (error) {
scrollToFirstError({ focusOnAlertRole: true });
return true;
}
return false;
};
export const focusEvidence = (index, root) => {
setTimeout(() => {
if (!focusFirstError(index, root)) {
scrollTo('topContentElement');
focusElement('#main h3', null, root);
}
});
};
export const focusH3AfterAlert = (
index,
{ name, onReviewPage, root = document } = {},
) => {
if (name && onReviewPage) {
focusReview(
name, // name of scroll element
true, // review accordion in edit mode
true, // reviewEditFocusOnHeaders setting from form/config.js
);
} else if (!focusFirstError(index, root)) {
scrollTo('topContentElement');
focusElement('h3#header', null, root);
}
};
export const focusIssue = (_index, root, value) => {
setTimeout(() => {
const item = value || window.sessionStorage.getItem(LAST_ISSUE);
window.sessionStorage.removeItem(LAST_ISSUE);
const [id, type] = (item || '').toString().split(',');
if (id < 0) {
// focus on add new issue after removing or cancelling adding a new issue
scrollTo('add-new-issue');
focusElement('.add-new-issue', null, root);
} else if (id) {
const card = $(`#issue-${id}`, root);
scrollTo(`issue-${id}`);
if (type === 'remove-cancel') {
const remove = $('.remove-issue', card)?.shadowRoot;
waitForRenderThenFocus('button', remove);
} else if (type === 'updated') {
waitForRenderThenFocus('input', card);
} else {
focusElement('.edit-issue-link', null, card);
}
} else {
scrollTo('topContentElement');
focusElement('h3');
}
});
};
// Focus on upload file card instead of delete button
export const focusFileCard = (name, root) => {
const target = $$('.schemaform-file-list li', root).find(entry =>
$('strong', entry)
.textContent?.trim()
.includes(name),
);
if (target) {
scrollTo(target.id);
setTimeout(() => {
const select = $('va-select', target); // SC only
if (select) {
// Set focusElement root parameter to a string because internally,
// focusElement will wait for shadow DOM to render before attempting to
// find the 'select' target
focusElement('select', {}, `#${target.id} va-select`);
} else {
focusElement(target);
}
});
}
};
// Focus on add another button after deleting & after removing all files
export const focusAddAnotherButton = root => {
// Add a timeout to allow for the upload button to reappear in the DOM
// before trying to focus on it
setTimeout(() => {
scrollTo($('#upload-wrap', root));
// focus on upload button, not the label
focusElement(
// including `#upload-button` because RTL can't access the shadowRoot
'button, #upload-button',
{},
$(`#upload-button`, root)?.shadowRoot,
);
}, 100);
};
// Focus on the 'Cancel' button when a file is being uploaded
export const focusCancelButton = root => {
setTimeout(() => {
const cancel = $('.schemaform-file-uploading .cancel-upload', root);
if (cancel) {
focusElement(
'button', // in shadow DOM
{},
cancel?.shadowRoot,
);
}
}, 100);
};
export const focusRadioH3 = () => {
scrollTo('topContentElement');
const radio = $('va-radio, va-checkbox-group, va-textarea');
if (radio) {
const target = radio.getAttribute('error') ? '[role="alert"]' : 'h3';
// va-radio content doesn't immediately render
waitForRenderThenFocus(target, radio.shadowRoot);
} else {
focusByOrder(['#main h3', defaultFocusSelector]);
}
};
// Testing focus on role="alert" inside web components
export const focusH3OrRadioError = (_index, root) => {
scrollTo('topContentElement');
const radio = $('va-radio, va-checkbox-group', root);
const hasError = radio.getAttribute('error');
const target = hasError ? '[role="alert"]' : 'h3';
waitForRenderThenFocus(target, hasError ? radio.shadowRoot : root);
};
// Temporary focus function for HLR homlessness question (page header is
// dynamic); once 100% released, change homeless form config to use
// `scrollAndFocusTarget: focusH3`
export const focusToggledHeader = (_index, root) => {
scrollTo('topContentElement');
const radio = $('va-radio', root);
if ((sessionStorage.getItem('hlrUpdated') || 'false') === 'false' && radio) {
waitForRenderThenFocus('h3', radio.shadowRoot);
} else {
waitForRenderThenFocus('#main h3');
}
};
export const focusH3 = (index, root) => {
scrollTo('topContentElement');
if (!focusFirstError(index, root)) {
focusElement('#main h3');
}
};
export const focusAlertH3 = (_index, root = document) => {
scrollTo('topContentElement');
const alert = $('va-alert[visible="true"]', root);
// va-alert header is not in the shadow DOM, but still the content doesn't
// immediately render
focusElement(`#main ${alert ? 'va-alert ' : ''}h3`);
};
// Used for onContinue callback on the contestable issues page
export const focusOnAlert = () => {
const alert = $('va-alert[status="error"] h3');
if (alert) {
scrollAndFocus(alert);
}
};