Open jcfranco opened 1 year ago
It turns out delegatesFocus
does not take into account tabindex
or pointer-events: none
when determining the first focusable element on click
and focus()
. This is an issue for us because components emulated as disabled
will still be delegated focus if they are the first element in the flat tree.
Using inert
on the mock "disabled" element solves this issue, but there isn't enough browser support at the moment.
delegatesFocus
also doesn't understand our custom checked
property (e.g. in radio-group
). Meaning it will always focus the first radio-group-item
in the flat tree.
However, focus is delegated to elements with the native checked
property (e.g. radio-button-group
), even if the radio-button
isn't the first element.
Calling the focus()
method on components that delegate focus only works correctly once the component is fully loaded. In order to get rid of our async setFocus
method, users would need to ensure the component is ready before using focus()
.
delegatesFocus
for the components that don't have the possibility of the first focusuble element being mock "disabled".Here is a codepen demonstration: https://codepen.io/benesri/pen/PoavMWM
Here is the WICG issue where they made that decision: https://github.com/WICG/webcomponents/issues/830
Specifically this snippet from the issue body:
Since we want to change the behavior of focus delegation to not be related to sequential focus navigation, we should probably remove the tabindex priority thing as well in this case. So we should always delegate focus to the first focusable area in DOM/composed-tree order
and this comment near the bottom:
Sounds like there was a consensus to move forward with what's being proposed thus far:
- Programmatic focus would focus the first element in the flat tree that's programmatically focusable.
- Mouse focus would focus the first element in the flat tree that's mouse focusable.
- Keyboard focus would focus the first element in tab index order that's keyboard focusable.
@geospatialem I moved this to Stalled because a lot of the components need Franco's feedback. But can you please verify the components that have been completed so far when you have time? You can use the setFocus
method to ensure the first focusable element is focused. Here is an example for filter
:
https://codepen.io/benesri/pen/XWYvoGa?editors=1010
dropdown
(focus
)dropdown-group
(setFocus
)action-bar
(focus
)action-group
(focus
)action-pad
(setFocus
)date-picker
(focus
)filter
(setFocus
)inline-editable
(setFocus
)input-date-picker
(setFocus
)input-time-picker
(setFocus
)time-picker
(setFocus
)split-button
(focus
)pagination
(focus
)radio-button-group
(setFocus
)slider
(setFocus
)@benelan Thanks for the thorough list to work through! I wasn't able to verify the setFocus
method for the components listed below.
Is there an additional step needed to ensure setFocus
is selecting the first focusable element?
dropdown
: https://codepen.io/geospatialem/pen/QWBLEPzaction-group
:https://codepen.io/geospatialem/pen/ZEjzOgzdate-picker
: https://codepen.io/geospatialem/pen/jOpNMORinline-editable
: https://codepen.io/geospatialem/pen/QWBLKwosplit-button
: https://codepen.io/geospatialem/pen/abjomOZ orpagination
: https://codepen.io/geospatialem/pen/qByWaOrTaking another look at inline-editable. The rest don't have setFocus()
methods so you can use a timeout and focus()
customElements
.whenDefined("calcite-date-picker")
.then(() => setTimeout(() => document.querySelector("calcite-date-picker").focus(), 1000));
This works for inline-editable
(async () => {
await customElements.whenDefined("calcite-inline-editable");
const el = await document
.querySelector("calcite-inline-editable")
.componentOnReady();
requestAnimationFrame(() => el.setFocus());
})();
The above code also works with the native focus()
method so you don't need a timeout.
Awesome, thanks for the insights! ✨ The above components are verified, will await for discussion on the skipped components upon Franco's return.
Overview
Currently, developers have to rely on
focusin
/focusout
(composable + bubbling) from component internals or using, internal, blur/focus events for a few components.† The goal for this effort is to provide a general solution for focus/blur events given that these events are commonly used and expected in many web development scenarios.Also for consideration,
setFocus
has introduced a pattern that can lead to inconsistent focus behavior. Existing usage should be revisited to see if we can consolidate in favor of consistency (one focus pattern to rule them all).† In general, this works but leads to boilerplate code as
focusin
/focusout
fires when you move between elements inside a component and would need to check where focus lies when handling the event.Tasks
[ ] determine if
delegatesFocus
is a viable solution[x] consult design regarding potential double focus outline
[x] check browsers focusing is consistent, if not weigh options
delegatesFocus
is a web standard, I think we will have better consistency than we would if we try to jerry-rig up our own solution. Once we get it set up in the code we can do more browser testing, but all the browsers have adopted the pattern and are reletaively consistent, based on my research.***[ ] check that focus/blur events will behave as expected for both light/shadow DOM content (if so, we use these and drop our internal focus/blur events)
[ ] confirm dev expectations light/shadow DOM content focus/blur (e.g., blurring slotted content not emitting blur on custom element) - https://codepen.io/jcfranco/pen/ZExMgwx?editors=1000
[x] check if https://www.npmjs.com/package/delegates-focus-polyfill will fit the bill since Safari 14 doesn't support it
[ ] consult design, a11y concerns from having multiple focus targets from
setFocus
calciteComponent.focus/blur()
for shadow-targeted focus/blur orslottedContent.focus/blur()
for light-targeted focus/blurAdditional resources
cc @benelan @geospatialem