Skip to content

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

Merged
merged 11 commits into from
Sep 9, 2022

Conversation

hqhhuang
Copy link
Contributor

@hqhhuang hqhhuang commented Sep 6, 2022

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:

  1. Medium or small viewport, tab open the nav menu bar
  2. tab through the items in the menu, at the end, focus should be brought to the browser address bar. It should not take you to the first link within our nav menu bar.

Checklist

Make sure you check off the following items. If they cannot be completed, provide a reason.

  • Added tests
  • Ran npm test, and it succeeded
  • Updated documentation if necessary - NA

Initial work on setting tabindex to -1 deeply
Refactor setting tabindex
Added test for tabindex
@hqhhuang
Copy link
Contributor Author

hqhhuang commented Sep 6, 2022

@swift-ci test

Copy link
Contributor

@dobromir-hristov dobromir-hristov left a 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.

this.$emit('open');
// lock focus
await this.$nextTick();
Copy link
Contributor

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?

@@ -185,7 +185,7 @@ describe('Asset', () => {
expect(asset.exists()).toBe(true);
expect(asset.props()).toEqual({
alt: image.alt,
loading: "lazy",
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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 === '') {
Copy link
Contributor

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-';
Copy link
Contributor

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?

Suggested change
const PREFIX = 'data-original-';
const OG_PREFIX = 'data-original-';

const ARIA = 'aria-hidden';
const TABINDEX = 'tabindex';

function setOriginalValue(element, prop) {
Copy link
Contributor

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') {
Copy link
Contributor

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 🤔

Copy link
Contributor Author

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.

Copy link
Member

@marinaaisa marinaaisa Sep 7, 2022

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);
  }
}

Copy link
Contributor

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.

Copy link
Contributor Author

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}]`);
Copy link
Contributor

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.

Copy link
Contributor Author

@hqhhuang hqhhuang Sep 6, 2022

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`
@hqhhuang
Copy link
Contributor Author

hqhhuang commented Sep 6, 2022

@swift-ci test

Copy link
Contributor

@dobromir-hristov dobromir-hristov left a 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 === '') {
Copy link
Contributor

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?

Suggested change
if (!element.hasAttribute(OG_PREFIX + prop) || originalValue === '') {
if (!element.hasAttribute(OG_PREFIX + prop)) {

Copy link
Member

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.

Copy link
Member

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!

Copy link
Contributor Author

@hqhhuang hqhhuang Sep 7, 2022

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?

Copy link
Member

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
Copy link
Member

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. :)

Comment on lines 16 to 23
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);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or maybe even:

Suggested change
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);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// remove the prefixed attribute
element.removeAttribute(PREFIX + prop);

if (typeof originalValue === 'string') {
Copy link
Contributor

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();
Copy link
Member

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?

Copy link
Contributor

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 :/

Copy link
Contributor Author

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.

Copy link
Contributor

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
@hqhhuang
Copy link
Contributor Author

hqhhuang commented Sep 7, 2022

@swift-ci test

@hqhhuang hqhhuang merged commit 787c2fb into swiftlang:main Sep 9, 2022
@hqhhuang hqhhuang deleted the nav-focus-trap branch September 9, 2022 07:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants