Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conditions 600 Create New Conditions Multi Page Prototype Form #33054

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions src/applications/user-testing/new-conditions/app-entry.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import '@department-of-veterans-affairs/platform-polyfills';
import './sass/new-conditions.scss';

import { startAppFromIndex } from '@department-of-veterans-affairs/platform-startup/exports';

import routes from './routes';
import reducer from './reducers';
import manifest from './manifest.json';

startAppFromIndex({
entryName: manifest.entryName,
url: manifest.rootUrl,
reducer,
routes,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { VaTextInput } from '@department-of-veterans-affairs/component-library/dist/react-bindings';
import debounce from 'lodash/debounce';
import { fullStringSimilaritySearch } from 'platform/forms-system/src/js/utilities/addDisabilitiesStringSearch';

const INSTRUCTIONS =
'When autocomplete results are available use up and down arrows to review and enter to select. Touch device users, explore by touch or with swipe gestures.';

const createFreeTextItem = val => `Enter your condition as "${val}"`;

const Autocomplete = ({
availableResults,
debounceDelay,
formData,
id,
label,
onChange,
}) => {
const [value, setValue] = useState(formData);
const [results, setResults] = useState([]);
const [activeIndex, setActiveIndex] = useState(null);
const [ariaLiveText, setAriaLiveText] = useState('');

const containerRef = useRef(null);
const inputRef = useRef(null);
const resultsRef = useRef([]);

// Delays screen reader result count reading to avoid interruption by input content reading
const debouncedSetAriaLiveText = useRef(
debounce((resultCount, freeTextResult) => {
const makePlural = resultCount > 1 ? 's' : '';

setAriaLiveText(
`${resultCount} result${makePlural}. ${freeTextResult}, (1 of ${resultCount})`,
);
}, 700),
).current;

const debouncedSearch = useRef(
debounce(async inputValue => {
const freeTextResult = createFreeTextItem(inputValue);
const searchResults = fullStringSimilaritySearch(
inputValue,
availableResults,
);
const updatedResults = [freeTextResult, ...searchResults];
setResults(updatedResults);
setActiveIndex(0);

debouncedSetAriaLiveText(updatedResults.length, freeTextResult);
}, debounceDelay),
).current;

const closeList = useCallback(
() => {
debouncedSearch.cancel();
debouncedSetAriaLiveText.cancel();
setResults([]);
setActiveIndex(null);
},
[debouncedSearch, debouncedSetAriaLiveText],
);

const handleInputChange = inputValue => {
setValue(inputValue);
onChange(inputValue);

if (!inputValue) {
closeList();
setAriaLiveText('Input is empty. Please enter a condition.');
return;
}

debouncedSearch(inputValue);
};

const activateScrollToAndFocus = index => {
setActiveIndex(index);

const activeResult = resultsRef.current[index];
activeResult?.scrollIntoView({
block: 'nearest',
});
activeResult?.focus();
};

const focusOnInput = () =>
inputRef.current.shadowRoot.querySelector('input').focus();

const navigateList = (e, adjustment) => {
e.preventDefault();
const newIndex = activeIndex + adjustment;
if (newIndex > results.length - 1) {
return;
}

if (newIndex < 1) {
activateScrollToAndFocus(0);

focusOnInput();
} else {
activateScrollToAndFocus(newIndex);
}
};

const selectResult = result => {
const newValue = result === createFreeTextItem(value) ? value : result;
setValue(newValue);
onChange(newValue);
setAriaLiveText(`${newValue} is selected`);
closeList();

focusOnInput();
};

const handleKeyDown = e => {
if (results.length > 0) {
if (e.key === 'ArrowDown') {
navigateList(e, 1);
} else if (e.key === 'ArrowUp') {
navigateList(e, -1);
} else if (e.key === 'Enter') {
selectResult(results[activeIndex]);
} else if (e.key === 'Escape') {
closeList();
focusOnInput();
} else if (e.key === 'Tab') {
closeList();
} else {
focusOnInput();
}
}
};

useEffect(
() => {
const handleClickOutside = event => {
if (
containerRef.current &&
!containerRef.current.contains(event.target)
) {
closeList();
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
},
[closeList],
);

const handleFocus = () => {
if (value && results.length === 0) {
debouncedSearch(value);
}
};

return (
<div className="cc-autocomplete" ref={containerRef}>
<VaTextInput
data-testid="autocomplete-input"
id={id}
label={label}
message-aria-describedby={!value ? INSTRUCTIONS : null}
ref={inputRef}
required
value={value}
onFocus={handleFocus}
onInput={e => handleInputChange(e.target.value)}
onKeyDown={handleKeyDown}
/>
{results.length > 0 && (
<ul
aria-activedescendant={`option-${activeIndex}`}
className="cc-autocomplete__list"
data-testid="autocomplete-list"
role="listbox"
tabIndex={-1}
>
{results.map((result, index) => (
<li
aria-selected={activeIndex === index}
className={`cc-autocomplete__option ${
activeIndex === index ? 'cc-autocomplete__option--active' : ''
}`}
id={`option-${index}`}
key={result}
ref={el => {
resultsRef.current[index] = el;
}}
role="option"
tabIndex={-1}
onClick={() => selectResult(result)}
onKeyDown={handleKeyDown} // Keydown is handled on the input; this is never fired and prevents eslint error
onMouseEnter={() => activateScrollToAndFocus(index)}
>
{result}
</li>
))}
</ul>
)}
<p aria-live="polite" className="vads-u-visibility--screen-reader">
{ariaLiveText}
</p>
</div>
);
};

Autocomplete.propTypes = {
availableResults: PropTypes.array,
debounceDelay: PropTypes.number,
formData: PropTypes.string,
id: PropTypes.string,
label: PropTypes.string,
onChange: PropTypes.func,
};

export default Autocomplete;
78 changes: 78 additions & 0 deletions src/applications/user-testing/new-conditions/config/form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { VA_FORM_IDS } from 'platform/forms/constants';
import CallVBACenter from 'platform/static-data/CallVBACenter';
import React from 'react';
import { SUBTITLE, TITLE } from '../constants';
import ConfirmationPage from '../containers/ConfirmationPage';
import IntroductionPage from '../containers/IntroductionPage';
import manifest from '../manifest.json';

import chooseDemo from '../pages/chooseDemo';
import conditionByConditionPages from '../pages/conditionByConditionPages';
import conditionsFirstPages from '../pages/conditionsFirstPages';

const FormFooter = () => (
<div className="row vads-u-margin-bottom--2">
<div className="usa-width-two-thirds medium-8 columns">
<va-need-help>
<div slot="content">
<div>
<p className="help-talk">
For help filling out this form, or if the form isn’t working
right, please <CallVBACenter />
</p>
</div>
</div>
</va-need-help>
</div>
</div>
);

/** @type {FormConfig} */
const formConfig = {
rootUrl: manifest.rootUrl,
urlPrefix: '/',
submitUrl: '/v0/api',
submit: () =>
Promise.resolve({ attributes: { confirmationNumber: '123123123' } }),
trackingPrefix: 'new-conditions',
introduction: IntroductionPage,
confirmation: ConfirmationPage,
footerContent: FormFooter,
// dev: {
// showNavLinks: true,
// collapsibleNavLinks: true,
// },
formId: VA_FORM_IDS.FORM_21_526EZ,
saveInProgress: {
messages: {
inProgress:
'Your disability compensation application (21-526EZ) is in progress.',
expired:
'Your saved disability compensation application (21-526EZ) has expired. If you want to apply for disability compensation, please start a new application.',
saved: 'Your disability compensation application has been saved.',
},
},
version: 0,
prefillEnabled: true,
savedFormMessages: {
notFound: 'Please start over to apply for disability compensation.',
noAuth:
'Please sign in again to continue your application for disability compensation.',
},
title: TITLE,
subTitle: SUBTITLE,
v3SegmentedProgressBar: true,
defaultDefinitions: {},
chapters: {
newConditionsChapter: {
title: 'Conditions',
pages: {
chooseDemo,
...conditionByConditionPages,
...conditionsFirstPages,
},
},
},
};

export default formConfig;
4 changes: 4 additions & 0 deletions src/applications/user-testing/new-conditions/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const TITLE = 'Demo: File for disability compensation - New conditions';
export const SUBTITLE = 'Demo of VA Form 21-526EZ';
export const CONDITION_BY_CONDITION = 'ape';
export const CONDITIONS_FIRST = 'elk';
12 changes: 12 additions & 0 deletions src/applications/user-testing/new-conditions/containers/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';

import RoutedSavableApp from 'platform/forms/save-in-progress/RoutedSavableApp';
import formConfig from '../config/form';

export default function App({ location, children }) {

Check warning on line 6 in src/applications/user-testing/new-conditions/containers/App.jsx

View workflow job for this annotation

GitHub Actions / Linting (Files Changed)

src/applications/user-testing/new-conditions/containers/App.jsx:6:31:'location' is missing in props validation

Check warning on line 6 in src/applications/user-testing/new-conditions/containers/App.jsx

View workflow job for this annotation

GitHub Actions / Linting (Files Changed)

src/applications/user-testing/new-conditions/containers/App.jsx:6:41:'children' is missing in props validation

Check warning on line 6 in src/applications/user-testing/new-conditions/containers/App.jsx

View workflow job for this annotation

GitHub Actions / Linting (Files Changed)

src/applications/user-testing/new-conditions/containers/App.jsx:6:31:'location' is missing in props validation

Check warning on line 6 in src/applications/user-testing/new-conditions/containers/App.jsx

View workflow job for this annotation

GitHub Actions / Linting (Files Changed)

src/applications/user-testing/new-conditions/containers/App.jsx:6:41:'children' is missing in props validation
return (
<RoutedSavableApp formConfig={formConfig} currentLocation={location}>
{children}
</RoutedSavableApp>
);
}
Loading
Loading