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

refactor(modal): Update modal to use focus-trap module. #5676

Merged
merged 21 commits into from
Nov 16, 2022

Conversation

driskull
Copy link
Member

@driskull driskull commented Nov 1, 2022

Related Issue: #1169 #5270

Summary

refactor(modal): Update modal to use focus-trap module.

Precursor to #2133

@driskull
Copy link
Member Author

driskull commented Nov 1, 2022

@macandcheese @geospatialem @jcfranco could you play around with this and let me know if its working correctly?

@driskull driskull marked this pull request as ready for review November 3, 2022 16:55
@driskull driskull requested a review from a team as a code owner November 3, 2022 16:55
@macandcheese
Copy link
Contributor

Seems to work well! The "tab fencing" demo doesn't seem to work as described, but that may just be out of date sample content. Is it still possible to specify an element to focus first?

@driskull
Copy link
Member Author

driskull commented Nov 3, 2022

Is it still possible to specify an element to focus first?

Yes, that's possible with the module. Do we need to support that for modal? I didn't see anything like that.

https://github.com/focus-trap/focus-trap#createoptions initialFocus.

@macandcheese
Copy link
Contributor

Yes, that's possible with the module. Do we need to support that for modal? I didn't see anything like that.

https://github.com/focus-trap/focus-trap#createoptions initialFocus.

Just going off the existing content in the "tab fencing" example modal. That may just be out of date demo - we already have setFocus available

@driskull
Copy link
Member Author

driskull commented Nov 3, 2022

From what I understand, it's ideal to focus on the first focusable element when a component's focus/setFocus method is called.

In most cases, this will be the close button but if that is disabled it would go to the next focusable element which could be some modal content or if no focusable modal content then any footer focusable content.

If we're following the rules, we shouldn't prioritize modal "content" over whatever the first focusable element is.

So that demo wording should be updated.

Ideally, we remove any options on setFocus. A user shouldn't be allowed or have to worry about focusing a specific element within the component. The component should handle that by focusing the first focusable element. In some components this will be a ranging focus element like the last selected radio button.

@jcfranco
Copy link
Member

jcfranco commented Nov 4, 2022

@driskull We should also confirm if this impacts the a11y/design of things. IMO, for closable dialogs with interactive content, as a keyboard user I would expect to be able to interact with the content without an extra keypress to move focus. Note that I'm not vouching for setFocus options, but we'll need to find another approach if focusing the close button in all cases isn't ideal and we don't want to provide focus IDs and keep the same design. cc @ashetland @SkyeSeitz @geospatialem

FWIW, @benelan was looking into this topic for #5147.

If we're following the rules, we shouldn't prioritize modal "content" over whatever the first focusable element is.

Which rules are you referring to here?

@ashetland
Copy link

@driskull We should also confirm if this impacts the a11y/design of things. IMO, for closable dialogs with interactive content, as a keyboard user I would expect to be able to interact with the content without an extra keypress to move focus. Note that I'm not vouching for setFocus options, but we'll need to find another approach if focusing the close button in all cases isn't ideal and we don't want to provide focus IDs and keep the same design. cc @ashetland @SkyeSeitz @geospatialem

FWIW, @benelan was looking into this topic for #5147.

If we're following the rules, we shouldn't prioritize modal "content" over whatever the first focusable element is.

Which rules are you referring to here?

I agree with @jcfranco here regarding interactive content. I like to think of this in terms of primary action. When the dialog appears, what's my primary action? If there is interactive content, my primary action would be to interact with that content before I close the dialog.

@driskull
Copy link
Member Author

driskull commented Nov 4, 2022

Which rules are you referring to here?

W3C guidelines for modal, but they are all pretty similar regarding focus.

https://www.w3.org/WAI/ARIA/apg/example-index/dialog-modal/dialog

Note that I'm not vouching for setFocus options, but we'll need to find another approach if focusing the close button in all cases isn't ideal and we don't want to provide focus IDs and keep the same design

I agree its not ideal in all cases, but in those cases the user is slotting the content and the footer buttons so they can always focus on one of those once the modal is opened. I don't think we need setFocus options because of that. I don't think there are any modal shadowRoot elements that need to specifically be focused.

I agree with @jcfranco here regarding interactive content. I like to think of this in terms of primary action. When the dialog appears, what's my primary action? If there is interactive content, my primary action would be to interact with that content before I close the dialog.

For these cases, we wouldn't be able to use any setFocus options to determine a primary action anyway. The author of the modal would have to focus on the primary action once the modal is opened. Which they can do because they are slotting the primary actions and those actions are in light DOM.

@driskull
Copy link
Member Author

driskull commented Nov 4, 2022

Just to be clear, the only thing changing is we focus the close-button by default now because that's what the first focusable element in the component is.

@driskull
Copy link
Member Author

driskull commented Nov 4, 2022

If we want to provide an option in the future for which element to focus by default, we could do that with setFocus by passing in the element or some selector.

https://github.com/focus-trap/focus-trap#what-it-does

When a focus trap is activated, this is what should happen:

Some element within the focus trap receives focus. By default, this will be the first element in the focus trap's tab order (as determined by tabbable). Alternately, you can specify an element that should receive this initial focus.

@benelan
Copy link
Member

benelan commented Nov 7, 2022

It seems like focus-trap could be another option. Although I'd prefer to go with delegatesFocus if everything pans out, since it's spec.

@driskull
Copy link
Member Author

driskull commented Nov 7, 2022

It seems like focus-trap could be another option. Although I'd prefer to go with delegatesFocus if everything pans out, since it's spec.

delegatesFocus and focus-trap are two different topics.

  • delegatesFocus: when a non-focusable part of the shadow DOM is clicked, the first focusable part is given focus, and the shadow host is given any available :focus styling.
  • focusTrap: Trap focus within a DOM node.

@benelan
Copy link
Member

benelan commented Nov 7, 2022

Ah, thanks for clarifying. I thought focus-trap was being suggested as an alternative.

@driskull
Copy link
Member Author

driskull commented Nov 7, 2022

Ah, thanks for clarifying. I thought focus-trap was being suggested as an alternative.

Nope! but ideally we should be consistent with how delegatesFocus works (by focusing first focusable element). Which focus-trap does.

@geospatialem
Copy link
Member

This looks great, @driskull! 🌟 Concur with your thoughts on the :focus placement for the close button, in particular around a11y.

If folks think the ordering seems odd with the close button being the first focusable element we could consider moving the close button to the footer by default. It would be a new pattern for the component so we'd need some design buy in for it, but perhaps an option if folks would rather see focusable elements from the content in focus first.

@benelan
Copy link
Member

benelan commented Nov 7, 2022

If folks think the ordering seems odd with the close button being the first focusable element we could consider moving the close button to the footer by default.

I'm not design, so take this with a grain of salt, but I don't think we should do this. If we do, we would probably need to change the "X" to "Close". I don't think I've ever seen a close "X" button in the footer, they are also in the top right. Imo changing that would cause more confusion/annoyance for users than whatever the first focusable element is.

@macandcheese
Copy link
Contributor

macandcheese commented Nov 7, 2022

Many Modals won't have footers either, so the close button as currently situated makes the most sense IMO. I think the focus on the close button should be expected as we've done that for awhile.

@driskull
Copy link
Member Author

driskull commented Nov 7, 2022

Many Modals won't have footers either, so the close button as currently situated makes the most sense IMO. I think the focus on the close button should be expected as we've done that for awhile.

Correct. We've gotten feedback from a11y team that focusing close button on panel first is correct. This should align with that.

@github-actions github-actions bot added the refactor Issues tied to code that needs to be significantly reworked. label Nov 9, 2022
@driskull driskull added the pr ready for visual snapshots Adding this label will run visual snapshot testing. label Nov 14, 2022
@jcfranco
Copy link
Member

I think the focus on the close button should be expected as we've done that for awhile.
Correct. We've gotten feedback from a11y team that focusing close button on panel first is correct. This should align with that.

Fair enough. As long as this is design/a11y-approved and we are consistent.

Just curious, but is there an a11y example out in the wild that has a close button similar to where we place ours?

If we want to provide an option in the future for which element to focus by default, we could do that with setFocus by passing in the element or some selector.
https://github.com/focus-trap/focus-trap#what-it-does

I like this. We can revisit supporting this later based on user feedback.

Copy link
Member

@jcfranco jcfranco left a comment

Choose a reason for hiding this comment

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

Awesome stuff!

return focusElement(
focusId === "close-button" ? closeButton : getFocusableElements(this.el)[0] || closeButton
);
if (closeButtonEl && focusId === "close-button") {
Copy link
Member

Choose a reason for hiding this comment

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

Since close-button is the only supported ID, I think you could just check for closeButtonEl being present or not. Passing any other ID would technically be invalid, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Correct. I think its nicer to check and fallback incase the user entered an invalid ID>

/**
* Used to delay setFocus call in ms.
*/
delay?: number;
Copy link
Member

Choose a reason for hiding this comment

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

If this is for the open/close transition, I'd suggest using the skipAnimation helper instead of introducing a delay argument. If it's something specific to focus-trap behavior, can you add more info about this to the documentation?

Copy link
Member Author

Choose a reason for hiding this comment

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

@jcfranco the problem is the delayInitialFocus is set to true.

I tried delayInitialFocus: !Build.isTesting, but the problem is that its not returning true.

{"testing":false,"browser":true,"dev":false,"server":false} 
    Location: http://localhost:3333/build/p-371d7086.entry.js:15:2306

src/utils/focusTrapComponent.ts Show resolved Hide resolved
src/utils/focusTrapComponent.ts Show resolved Hide resolved
@driskull driskull added pr ready for visual snapshots Adding this label will run visual snapshot testing. and removed pr ready for visual snapshots Adding this label will run visual snapshot testing. labels Nov 15, 2022
@driskull driskull merged commit 059d6ee into master Nov 16, 2022
@driskull driskull deleted the dris0000/modal-focus-trap-npm branch November 16, 2022 17:34
benelan added a commit that referenced this pull request Nov 16, 2022
* master: (24 commits)
  1.0.0-next.632
  feat(pick-list, value-list): Add calciteListFilter event, filteredItems prop, filterText prop and filteredData prop. (#5681)
  fix(block, date-picker, list-item-group, panel, pick-list-group, popover, tip, tip-manager)!: Set default internal heading to a div. (#5728)
  refactor(modal): Update modal to use focus-trap module. (#5676)
  1.0.0-next.631
  fix(input-date-picker): restores mouse clicks on date-picker popup (#5760)
  build(deps): bump loader-utils from 1.4.1 to 1.4.2 (#5764)
  ci(product-label): fix version syntax and use os agnostic newline char (#5762)
  1.0.0-next.630
  ci(pr-bot): don't label dependabot PRs (#5759)
  build(deps): bump chromatic from 6.7.1 to 6.11.4 (#5756)
  fix(combobox): Wrap and break text on long items (#5672)
  build(deps): bump @storybook/addon-interactions from 6.5.9 to 6.5.13 (#5753)
  build(deps): bump type-fest from 3.1.0 to 3.2.0 (#5752)
  build(deps): bump @storybook/addon-a11y from 6.5.12 to 6.5.13 (#5754)
  build(deps): bump postcss from 8.4.18 to 8.4.19 (#5755)
  ci: add chromatic (#5733)
  build(docs): generate docs-json for afd usage (#5748)
  1.0.0-next.629
  fix(inline-editable): Add text-ellipsis when not editing (#5679)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr ready for visual snapshots Adding this label will run visual snapshot testing. refactor Issues tied to code that needs to be significantly reworked.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants