Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,10 @@ export function FloatingFocusManager(props: FloatingFocusManagerProps): React.JS
const getNodeId = useStableCallback(() => dataRef.current.floatingContext?.nodeId);

const ignoreInitialFocus = initialFocus === false;
// If the reference is a combobox and is typeable (e.g. input/textarea),
// there are different focus semantics. The guards should not be rendered, but
// aria-hidden should be applied to all nodes still. Further, the visually
// hidden dismiss button should only appear at the end of the list, not the
// start.
// A typeable combobox reference (e.g. input/textarea) with `initialFocus={false}`
// has different focus semantics: focus is not trapped inside the floating element,
// so in the modal case the guards are not rendered, but `aria-hidden` is still
// applied to the outside nodes.
const isUntrappedTypeableCombobox = isTypeableCombobox(domReference) && ignoreInitialFocus;

const initialFocusRef = useValueAsRef(initialFocus);
Expand Down Expand Up @@ -447,7 +446,6 @@ export function FloatingFocusManager(props: FloatingFocusManagerProps): React.JS
insideElements.some(
(element) => element === relatedTarget || contains(element, relatedTarget),
) ||
(relatedTarget != null && triggers.hasElement(relatedTarget)) ||
triggers.hasMatchingElement((trigger) => contains(trigger, relatedTarget)) ||
isRelatedFocusGuard ||
(tree &&
Expand All @@ -470,7 +468,7 @@ export function FloatingFocusManager(props: FloatingFocusManagerProps): React.JS
handleTabIndex(floatingFocusElement);
}

// Restore focus to the previous tabbable element index to prevent
// Restore focus to the previously focused tabbable element to prevent
// focus from being lost outside the floating tree.
if (
restoreFocus &&
Expand All @@ -485,10 +483,10 @@ export function FloatingFocusManager(props: FloatingFocusManagerProps): React.JS
// If explicitly requested to restore focus to the popup container, do not search
// for the next/previous tabbable element.
if (restoreFocus === 'popup') {
// If the element is removed on pointerdown, focus tries to move it,
// but since it's removed at the same time, focus gets lost as it
// happens after the .focus() call above.
// In this case, focus needs to be moved asynchronously.
// If the focused element is removed on pointerdown, the browser
// tries to move focus to it right after the `.focus()` call above,
// but because it's removed in the same tick, focus is lost instead.
// Re-focusing asynchronously (next frame) wins that race.
restoreFocusFrame.request(() => {
floatingFocusElement.focus();
});
Expand Down Expand Up @@ -732,14 +730,13 @@ export function FloatingFocusManager(props: FloatingFocusManagerProps): React.JS

const doc = ownerDocument(floatingFocusElement);
const elementFocusedBeforeOpen = activeElement(doc);
// Only nullish interaction types represent programmatic opens. The empty
// string default is intentionally not treated as programmatic.
// Only an explicit `null` interaction type represents a programmatic open.
// `undefined` is normalized to `''` by the prop default, so it never reaches
// here as nullish and is intentionally not treated as programmatic.
const preferPreviousFocus = openInteractionTypeRef.current == null;

addPreviouslyFocusedElement(elementFocusedBeforeOpen);

// Dismissing via outside press should always ignore `returnFocus` to
// prevent unwanted scrolling.
function onOpenChangeLocal(details: FloatingUIOpenChangeDetails) {
if (!details.open) {
closeTypeRef.current = getEventType(details.nativeEvent, lastInteractionTypeRef.current);
Expand All @@ -761,6 +758,11 @@ export function FloatingFocusManager(props: FloatingFocusManagerProps): React.JS
) {
preventReturnFocusRef.current = false;
} else {
// On outside press, only return focus to the reference when the browser supports the
// `focus({ preventScroll })` option; without it, restoring focus scrolls the page.
// Chrome on Android and Samsung Internet still don't support `preventScroll`
// (https://issues.chromium.org/issues/41453122), so the runtime check keeps return
// focus disabled there to avoid the scroll jump.
let isPreventScrollSupported = false;
ownerDocument(floatingFocusElement)
.createElement('div')
Expand Down Expand Up @@ -836,7 +838,8 @@ export function FloatingFocusManager(props: FloatingFocusManagerProps): React.JS
const returnElement = getReturnElement();

queueMicrotask(() => {
// This is `returnElement`, if it's tabbable, or its first tabbable child.
// `returnElement` if it is tabbable, otherwise its first tabbable child,
// otherwise `returnElement` itself (which may not be tabbable at all).
const tabbableReturnElement = getFirstTabbableElement(returnElement);
const hasExplicitReturnFocus = typeof returnFocusValueOrFn !== 'boolean';

Expand Down Expand Up @@ -888,8 +891,8 @@ export function FloatingFocusManager(props: FloatingFocusManagerProps): React.JS
}
}, [open, floating]);

// Synchronize the `context` & `modal` value to the FloatingPortal context.
// It will decide whether or not it needs to render its own guards.
// Synchronize the focus manager state (modal, closeOnFocusOut, open, etc.) to the
// FloatingPortal context, which uses it to decide whether to render its own guards.
useIsoLayoutEffect(() => {
if (disabled || !portalContext) {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,10 @@ export function useFloatingPortalNode(
* @internal
*/
export const FloatingPortal = React.forwardRef(function FloatingPortal(
componentProps: FloatingPortal.Props<any> & { renderGuards?: boolean | undefined },
componentProps: FloatingPortal.Props<any>,
forwardedRef: React.ForwardedRef<HTMLDivElement>,
) {
const { render, className, style, children, container, renderGuards, ...elementProps } =
componentProps;
const { render, className, style, children, container, ...elementProps } = componentProps;

const { portalNode, portalSubtree } = useFloatingPortalNode({
container,
Expand All @@ -187,9 +186,7 @@ export const FloatingPortal = React.forwardRef(function FloatingPortal(
const open = focusManagerState?.open;

const shouldRenderGuards =
typeof renderGuards === 'boolean'
? renderGuards
: !!focusManagerState && !focusManagerState.modal && focusManagerState.open && !!portalNode;
!!focusManagerState && !focusManagerState.modal && focusManagerState.open && !!portalNode;

// https://codesandbox.io/s/tabbable-portal-f4tng?file=/src/TabbablePortal.tsx
React.useEffect(() => {
Expand Down
Loading