-
Notifications
You must be signed in to change notification settings - Fork 126
/
FormNav.jsx
218 lines (197 loc) · 7.07 KB
/
FormNav.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import uniq from 'lodash/uniq';
import {
getChaptersLengthDisplay,
createFormPageList,
createPageList,
getActiveExpandedPages,
getCurrentChapterDisplay,
} from '../helpers';
import {
focusByOrder,
customScrollAndFocus,
defaultFocusSelector,
waitForRenderThenFocus,
} from '../../../../utilities/ui';
import { querySelectorWithShadowRoot } from '../../../../utilities/ui/webComponents';
import { REVIEW_APP_DEFAULT_MESSAGE } from '../constants';
export default function FormNav(props) {
const {
formConfig = {},
currentPath,
formData,
isLoggedIn,
inProgressFormId,
} = props;
const [index, setIndex] = useState(0);
// This is converting the config into a list of pages with chapter keys,
// finding the current page, then getting the chapter name using the key
const formPages = createFormPageList(formConfig);
const pageList = createPageList(formConfig, formPages);
const eligiblePageList = getActiveExpandedPages(pageList, formData);
const uniqueChapters = uniq(
eligiblePageList.map(p => p.chapterKey).filter(key => !!key),
);
let page = eligiblePageList.filter(p => p.path === currentPath)[0];
// If the page isn’t active, it won’t be in the eligiblePageList
// This is a fallback to still find the chapter name if you open the page directly
// (the chapter index will probably be wrong, but this isn’t a scenario that happens in normal use)
if (!page) {
page =
formPages.find(p => `${formConfig.urlPrefix}${p.path}` === currentPath) ||
{};
}
let current;
let chapterName;
let inProgressMessage = null;
if (page.chapterKey) {
const onReviewPage = page.chapterKey === 'review';
current = uniqueChapters.indexOf(page.chapterKey) + 1;
// The review page is always part of our forms, but isn’t listed in chapter list
chapterName = onReviewPage
? formConfig?.customText?.reviewPageTitle || REVIEW_APP_DEFAULT_MESSAGE
: formConfig.chapters[page.chapterKey].title;
if (typeof chapterName === 'function' && !onReviewPage) {
// for FormNav, we only call chapter-config title-function if
// not on review-page.
chapterName = chapterName({ formData, formConfig, onReviewPage });
}
}
if (isLoggedIn) {
inProgressMessage = (
<span className="vads-u-display--block vads-u-font-family--sans vads-u-font-weight--normal vads-u-font-size--base">
We’ll save your application on every change.{' '}
{inProgressFormId &&
`Your application ID number is ${inProgressFormId}.`}
</span>
);
}
const showHeader = Math.abs(current - index) === 1;
// Some chapters may have progress-bar & step-header hidden via hideFormNavProgress.
const hideFormNavProgress =
formConfig?.chapters[page.chapterKey]?.hideFormNavProgress;
// Ensure other chapters [that do show progress-bar & step-header] have
// the correct number & total [with progress-hidden chapters discounted].
// formConfig, current, & chapters.length should NOT be manipulated,
// as they are likely used elsewhere in functional logic.
const chaptersLengthDisplay = getChaptersLengthDisplay({
uniqueChapters,
formConfig,
});
// Returns NaN if the current chapter isn't found
const currentChapterDisplay = getCurrentChapterDisplay(formConfig, current);
const stepText = Number.isNaN(currentChapterDisplay)
? ''
: `Step ${currentChapterDisplay} of ${chaptersLengthDisplay}: ${chapterName ||
''}`;
const handleFocus = async () => {
let root = document.querySelector('#react-root');
if (formConfig.v3SegmentedProgressBar) {
// Need to provide shadowRoot for focusing on shadow-DOM elements
const shadowHost = await querySelectorWithShadowRoot(
'va-segmented-progress-bar',
);
root = shadowHost.shadowRoot;
}
return () => {
// Check main toggle to enable custom focus; the unmounting of the page
// before the review & submit page may cause the customScrollAndFocus
// function to be called inadvertently
if (
!(
page.chapterKey === 'review' ||
window.location.pathname.endsWith('review-and-submit')
)
) {
if (formConfig.useCustomScrollAndFocus) {
customScrollAndFocus(page.scrollAndFocusTarget, index);
} else {
waitForRenderThenFocus(defaultFocusSelector, root, 400);
}
} else {
// h2 fallback for review page
focusByOrder([defaultFocusSelector, 'h2'], root);
}
};
};
// Handle focus on mount
useEffect(() => {
handleFocus();
}, []);
// The goal with this is to quickly "remove" the header from the DOM, and
// immediately re-render the component with the header included.
// `current` changes when the form chapter changes, and when this happens
// we want to force react to remove the <h2> and re-render it. This should
// ensure that VoiceOver on iOS will pick up on the new <h2>
// https://github.com/department-of-veterans-affairs/va.gov-team/issues/12323
useEffect(
() => {
if (current > index + 1) {
setIndex(index + 1);
} else if (current === index) {
setIndex(index - 1);
}
handleFocus();
},
[current, index],
);
const v3SegmentedProgressBar = formConfig?.v3SegmentedProgressBar;
// show progress-bar and stepText only if hideFormNavProgress is falsy.
return (
<div>
{!hideFormNavProgress && (
<va-segmented-progress-bar
total={chaptersLengthDisplay}
current={currentChapterDisplay}
uswds={v3SegmentedProgressBar}
heading-text={chapterName ?? ''} // functionality only available for v3
name="v3SegmentedProgressBar"
{...(v3SegmentedProgressBar ? { 'header-level': '2' } : {})}
/>
)}
{!v3SegmentedProgressBar &&
!hideFormNavProgress && (
<div className="schemaform-chapter-progress">
<div className="nav-header nav-header-schemaform">
{showHeader ? (
<h2
id="nav-form-header"
data-testid="navFormHeader"
className="vads-u-font-size--h4"
>
{stepText}
{inProgressMessage}
</h2>
) : (
<div data-testid="navFormDiv" className="vads-u-font-size--h4">
{stepText}
{inProgressMessage}
</div>
)}
</div>
</div>
)}
</div>
);
}
FormNav.defaultProps = {
currentPath: '',
formData: {},
isLoggedIn: false,
inProgressFormId: null,
};
FormNav.propTypes = {
formConfig: PropTypes.shape({
chapters: PropTypes.shape({}),
customText: PropTypes.shape({
reviewPageTitle: PropTypes.string,
}),
urlPrefix: PropTypes.string,
useCustomScrollAndFocus: PropTypes.bool,
}).isRequired,
currentPath: PropTypes.string,
formData: PropTypes.shape({}),
inProgressFormId: PropTypes.number,
isLoggedIn: PropTypes.bool,
};