-
Notifications
You must be signed in to change notification settings - Fork 60
Prevent Nav from Trapping while tabbing #427
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
Conversation
Initial work on setting tabindex to -1 deeply
Refactor setting tabindex
Added test for tabindex
@swift-ci test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a quick test and it looks to work good.
The code is OK, just left a few small comments.
src/components/NavBase.vue
Outdated
this.$emit('open'); | ||
// lock focus | ||
await this.$nextTick(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know why we added this, but do we need it for this fix?
tests/unit/components/Asset.spec.js
Outdated
@@ -185,7 +185,7 @@ describe('Asset', () => { | |||
expect(asset.exists()).toBe(true); | |||
expect(asset.props()).toEqual({ | |||
alt: image.alt, | |||
loading: "lazy", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets revert these as they are not related to the PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea I figured. These 2 are generated when I ran test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we open a separate PR to fix these?
function setOriginalValue(element, prop) { | ||
// make sure that prop isn't cached already | ||
let originalValue = element.getAttribute(PREFIX + prop); | ||
if (originalValue === undefined || originalValue === null || originalValue === '') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can probably just check for hasAttribute
instead?
* Licensed under Apache License v2.0 with Runtime Library Exception | ||
* | ||
* See https://swift.org/LICENSE.txt for license information | ||
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors | ||
*/ | ||
import TabManager from 'docc-render/utils/TabManager'; | ||
|
||
const PREFIX = 'data-original-'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe something like this, so its more clear what the prefix is?
const PREFIX = 'data-original-'; | |
const OG_PREFIX = 'data-original-'; |
const ARIA = 'aria-hidden'; | ||
const TABINDEX = 'tabindex'; | ||
|
||
function setOriginalValue(element, prop) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe call it something like preserveAttribute
or cacheAttribute
?
setOriginalValue
is very vague.
// remove the prefixed attribute | ||
element.removeAttribute(PREFIX + prop); | ||
|
||
if (typeof originalValue === 'string') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder why the check if its a string
🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tested this. If we call element.getAttribute
on an attribute that doesn't exist, the return value is null
and the type is therefore an object.
In our case, the cacheOriginalAttribute
ensures that even where there's no original value, we still set that attribute, just with an empty string. So in our case, the type of originalValue
here should always be a string. However, if we had a bug where we somehow didn't set the attribute correctly, we would want to check the type here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation, @hqhhuang , maybe then it's easier to do something like this?
function retrieveOriginalAttribute(element, prop) {
// exit function if attribute doesn't exist
if (!element.hasAttribute(OG_PREFIX + prop)) return;
// get the cached property
const originalValue = element.getAttribute(OG_PREFIX + prop);
// remove the prefixed attribute
element.removeAttribute(OG_PREFIX + prop);
if (originalValue.length) {
element.setAttribute(prop, originalValue);
} else {
// otherwise remove the attribute entirely.
element.removeAttribute(prop);
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cant you just check for if(originalValue)
? Its always a string
or null
, so a simple truthy
check should work.
We could also probably store OG_PREFIX + prop
as a var in the function, so we dont concat it 3 times.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good idea!
if (!element.hasAttribute(OG_PREFIX + prop)) return;
retrieveOriginalValue(element, TABINDEX); | ||
|
||
// make sure element's tabbable children are restored as well | ||
const tabbables = element.querySelectorAll(`[${PREFIX + TABINDEX}]`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TabManager.getTabbableElements
would not work here now, would it? 🤔 Because they are no longer tabbable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup. If we use getTabbableElements
here, the only ones that would get restored would be the ones that are focusable by nature.
Change function/variable name and revert unrelated changes
Remove `FocusTrap` from `NavBase`
Remove unnecessary async
Revert change to `Asset.spec.js`
@swift-ci test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, left a small final question.
function cacheOriginalAttribute(element, prop) { | ||
// make sure that prop isn't cached already | ||
let originalValue = element.getAttribute(OG_PREFIX + prop); | ||
if (!element.hasAttribute(OG_PREFIX + prop) || originalValue === '') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldnt we just check if there is a cached attribute and only cache if there is not?
if (!element.hasAttribute(OG_PREFIX + prop) || originalValue === '') { | |
if (!element.hasAttribute(OG_PREFIX + prop)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason why this function doesn't look like this?
function cacheOriginalAttribute(element, prop) {
// make sure that prop isn't cached already
if (element.hasAttribute(OG_PREFIX + prop)) return;
const originalValue = element.getAttribute(prop) || '';
element.setAttribute(OG_PREFIX + prop, originalValue);
}
I don't understand why adding the originalValue as a let var before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so we wrote the same comment @dobromir-hristov 😆 #427 (comment)
actually you wrote it first!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because if the element has a cached attribute, but its value is an empty string; we don't want to return here on this line:
if (element.hasAttribute(OG_PREFIX + prop)) return;
We would still want to set the attribute. Did I miss something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
then what about if (!!element.getAttribute(OG_PREFIX + prop)) return;
?
@@ -1,12 +1,44 @@ | |||
/** | |||
* This source file is part of the Swift.org open source project | |||
* | |||
* Copyright (c) 2021 Apple Inc. and the Swift project authors | |||
* Copyright (c) 2022 Apple Inc. and the Swift project authors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we should do 2021-2022
instead of 2022
if we edit a file. :)
function cacheOriginalAttribute(element, prop) { | ||
// make sure that prop isn't cached already | ||
let originalValue = element.getAttribute(OG_PREFIX + prop); | ||
if (!element.hasAttribute(OG_PREFIX + prop) || originalValue === '') { | ||
originalValue = element.getAttribute(prop) || ''; | ||
element.setAttribute(OG_PREFIX + prop, originalValue); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or maybe even:
function cacheOriginalAttribute(element, prop) { | |
// make sure that prop isn't cached already | |
let originalValue = element.getAttribute(OG_PREFIX + prop); | |
if (!element.hasAttribute(OG_PREFIX + prop) || originalValue === '') { | |
originalValue = element.getAttribute(prop) || ''; | |
element.setAttribute(OG_PREFIX + prop, originalValue); | |
} | |
} | |
function cacheOriginalAttribute(element, prop) { | |
// make sure that prop isn't cached already | |
if(element.hasAttribute(OG_PREFIX + prop)) return | |
// cache the attribute | |
const value = element.getAttribute(prop) || ''; | |
element.setAttribute(OG_PREFIX + prop, value); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replied above under Marina's comment: https://github.com/apple/swift-docc-render/pull/427/files#r965056810
// remove the prefixed attribute | ||
element.removeAttribute(PREFIX + prop); | ||
|
||
if (typeof originalValue === 'string') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cant you just check for if(originalValue)
? Its always a string
or null
, so a simple truthy
check should work.
We could also probably store OG_PREFIX + prop
as a var in the function, so we dont concat it 3 times.
// hide sibling elements from VO | ||
changeElementVOVisibility.hide(this.$refs.wrapper); | ||
// focus on the toggle to prevent tabbing to links in the body | ||
this.$refs.axToggle.focus(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why focusing on the axToggle here? should it be implicated focused if you clicked on it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You click on another element, the axToggle is only when you focus navigate around :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is needed to retain focus on the toggle when user expands Nav with mouse, then starts tabbing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we introduced a regression here....
Fix header and simplify methods
Fix header and revert whiteline change
simplify cacheAttribute function
@swift-ci test |
Bug/issue #, if applicable: rdar://94044029 ([AX] Localnav should not FocusTrap for documentation pages)
Summary
For accessibility considerations, when we tab to the end of the nav menu, we should tab to the browser menu bar next (instead of tabbing to the beginning of the nav menu again).
Dependencies
NA
Testing
Steps:
Checklist
Make sure you check off the following items. If they cannot be completed, provide a reason.
npm test
, and it succeeded