LG-8682 Bypass Blocks - aka make skip to main content work#7769
LG-8682 Bypass Blocks - aka make skip to main content work#7769JackRyan1989 merged 3 commits intomainfrom
Conversation
…PAT concerns around skip to main content link.
|
Unaware this was being worked on, I was discussing this bug earlier with @eric-gade , and we came up with a similar conclusion that the key thing would be to prevent the default behavior of the skip link. I sorta feel this should be the default behavior across the board, even to the extent that I was surprised the base implementation of the skiplink doesn't already do this. I was suggesting we add this to the app-wide document
.querySelector('.usa-skipnav')
?.addEventListener('click', (event) => event.preventDefault());Do you have thoughts on that approach? The focusing and scrolling should already be happening as a result of the base implementation in the U.S. Web Design System: https://github.com/uswds/uswds/blob/v2.13.3/src/js/components/skipnav.js#L20 |
| * @param - null | ||
| * | ||
| * @return - null | ||
| */ | ||
|
|
There was a problem hiding this comment.
I'd ditch the null documentation?
| * @param - null | |
| * | |
| * @return - null | |
| */ | |
| */ |
| const { inPersonURL, arcgisSearchEnabled } = useContext(InPersonContext); | ||
| useDidUpdateEffect(onStepChange, [stepName]); | ||
| useEffect(() => { | ||
| hijackSkipNav(); |
There was a problem hiding this comment.
does this need any guard to make sure it's not called twice? or should the function itself check to see that there's not already a listener set?
There was a problem hiding this comment.
To make sure I understand, is this what your concerned about? Memory Issues
If it is, I think it would solve the issue to create a function like:
function setFocus(event) {
event.preventDefault();
mainContent?.focus();
mainContent?.scrollIntoView();
}
And then have the addEventListener work like skipNavLink?.addEventListener('click', setFocus);
Does that seem like a reasonable solution?
There was a problem hiding this comment.
@JackRyan1989 The return value to the useEffect callback should be another callback that removes the event listener. Additionally, you may want hijackSkipNav to track the times it's been called/cleaned up to avoid redundant attachment (and premature detachment) of event listeners.
There was a problem hiding this comment.
yeah either solution works but I definitely think the unmount solution would be the cleanest
Personally I don't have an opinion about implementing this site wide. The motivation for this particular change is that it's affecting keyboard accessibility, is the functionality of this link causing issues elsewhere in the application? And interestingly (and at least in Firefox) the autoscroll aspect of |
|
I think my main concern is if we have the link working different ways on different pages, it's hard to track, and likely to cause some confusion down the line. It feels like this ought to be the default behavior of the skip link, or at least if the USWDS implementation is already managing most of the programmatic focus shifting, the native behavior of updating the page location isn't necessary. I guess if we were also relying on the native behavior to move the viewport due to inconsistencies with the browser, we could pull that into the global event handler as well? |
Oh yeah gotcha. Yeah that seems like a good idea to me. |
| export default function hijackSkipNav(): void { | ||
| const skipNavLink = document.querySelector<HTMLFormElement>('.usa-skipnav'); | ||
| const mainContent = document.querySelector<HTMLFormElement>('#main-content'); | ||
|
|
||
| skipNavLink?.addEventListener('click', (e) => { | ||
| e.preventDefault(); | ||
| mainContent?.focus(); | ||
| mainContent?.scrollIntoView(); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Here's an example of the kind of logic I mentioned.
| export default function hijackSkipNav(): void { | |
| const skipNavLink = document.querySelector<HTMLFormElement>('.usa-skipnav'); | |
| const mainContent = document.querySelector<HTMLFormElement>('#main-content'); | |
| skipNavLink?.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| mainContent?.focus(); | |
| mainContent?.scrollIntoView(); | |
| }); | |
| } | |
| // Function has same identity between calls, so can't be duplicated | |
| function hijackListener(e: Event) { | |
| e.preventDefault(); | |
| // Get the main content on click so we don't need to worry about it being removed; | |
| // this is an infrequent operation, so a repeated DOM search is acceptable | |
| const mainContent = document.querySelector<HTMLFormElement>('#main-content'); | |
| mainContent?.focus(); | |
| mainContent?.scrollIntoView(); | |
| } | |
| // Keep track of the listener calls so we don't cleanup the listener until it's appropriate | |
| // Listeners are no longer relevant after there's no reference to the element, so WeakMap is appropriate | |
| const listenerCounts = new WeakMap<HTMLFormElement,number>(); | |
| export default function hijackSkipNav(): () => void { | |
| const skipNavLink = document.querySelector<HTMLFormElement>('.usa-skipnav'); | |
| skipNavLink?.addEventListener('click', hijackListener); | |
| const prevCnt = listenerCounts.get(skipNavLink) || 0; | |
| listenerCounts.set(skipNavLink, prevCnt + 1); | |
| // Return a function that can be passed as the useEffect return value for listener cleanup | |
| return () => { | |
| // Only cleanup if this is the last usage; otherwise decrement the listener counter | |
| const cnt = listenerCounts.get(skipNavLink); | |
| if (cnt === 1) { | |
| listenerCounts.delete(skipNavLink); | |
| skipNavLink?.removeEventListener('click', hijackListener); | |
| } else if (cnt > 1) { | |
| listenerCounts.set(skipNavLink, cnt - 1); | |
| } | |
| }; | |
| } |
There was a problem hiding this comment.
Could be overkill since this should only be happening once per page.
There was a problem hiding this comment.
There was a problem hiding this comment.
If @aduth said to make this the default behavior, then I'm good with that.
|
Hi @JackRyan1989 @NavaTim -- as he mentioned, @aduth and I were discussing this Friday morning, and it also relates to an accessibility ticket I've picked up (LG-8786). If you are already going to implement the application-wide behavior then I do not want to duplicate efforts -- I've haven't had a chance to work on this issue any more until this morning, but will hold off until I hear from you all. In the meantime, @aduth suggested the possibility of posting to the uswds repo about a feature request for the skip-to-content module and seeing whether or not they can implement something similar. I'll take charge of making that post. EDIT: I've posted to uswds here. |
Hey Eric, Sounds good. Seems like the most straightforward option is to make this change the default behavior. I'll go ahead and handle that with this PR and thanks for reaching out to USWDS. |
@aduth Thanks for the guidance here. Changes have been made. I confirmed that the scrollIntoView and focus method calls are not necessary on Chrome, but I'm still experiencing the issue where the main-content section doesn't scroll into view on Firefox. Happy to remove if you think it would be a better idea. |
app/javascript/packs/application.ts
Outdated
| const mainContent = document.getElementById('main-content'); | ||
| document.querySelector('.usa-skipnav')?.addEventListener('click', (event) => { | ||
| event.preventDefault(); | ||
| mainContent?.focus(); |
There was a problem hiding this comment.
Is the focus necessary, if this is already happening in the base USWDS implementation?
| mainContent?.focus(); |
🎫 Ticket
🛠 Summary of changes
One pages within the document upload/authorization flow, selecting the 'Skip to main content' link does not actually work. It interrupts the navigation as managed by the React flow state machine. This is a bit of a triage: we load some JS that prevents the default functionality of the link, and instead manually focuses the main content section and scrolls to it.
I did not see a spec file for the
document-capture.tsxfile, so I didn't create one? But am happy to either update an existing test spec or create a new one for this particular component.📜 Testing Plan
👀 No Screenshots for you!