Skip to content

Commit

Permalink
feat(wrapFocus): add wrapFocusWithoutSentinels
Browse files Browse the repository at this point in the history
  • Loading branch information
tay1orjones committed Mar 4, 2024
1 parent 3b71d1c commit 9736ddd
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 147 deletions.
92 changes: 55 additions & 37 deletions packages/react/src/components/ComposedModal/ComposedModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@ import mergeRefs from '../../tools/mergeRefs';
import cx from 'classnames';
import toggleClass from '../../tools/toggleClass';
import requiredIfGivenPropIsTruthy from '../../prop-types/requiredIfGivenPropIsTruthy';
import wrapFocus from '../../internal/wrapFocus';
import wrapFocus, { wrapFocusWithoutSentinels } from '../../internal/wrapFocus';
import { usePrefix } from '../../internal/usePrefix';
import { keys, match } from '../../internal/keyboard';
import { tabbable } from 'tabbable';
import { useWindowEvent } from '../../internal/useEvent';

export interface ModalBodyProps extends HTMLAttributes<HTMLDivElement> {
/** Specify the content to be placed in the ModalBody. */
Expand Down Expand Up @@ -260,31 +258,26 @@ const ComposedModal = React.forwardRef<HTMLDivElement, ComposedModalProps>(
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps

function handleKeyDown(evt) {
evt.stopPropagation();
if (match(evt, keys.Escape)) {
closeModal(evt);
function handleKeyDown(event) {
event.stopPropagation();
if (match(event, keys.Escape)) {
closeModal(event);
}

if (open && match(evt, keys.Tab) && innerModal.current) {
console.log(`onKeyDown`);
const { current: bodyNode } = innerModal;

wrapFocus({
bodyNode,
hasTrapNodes: false,
startTrapNode: null,
endTrapNode: null,
currentActiveNode: evt.target,
oldActiveNode: null,
selectorsFloatingMenus: selectorsFloatingMenus?.filter(
Boolean
) as string[],
event: evt,
if (
focusTrapWithoutSentinels &&
open &&
match(event, keys.Tab) &&
innerModal.current
) {
wrapFocusWithoutSentinels({
containerNode: innerModal.current,
currentActiveNode: event.target,
event: event,
});
}

onKeyDown?.(evt);
onKeyDown?.(event);
}
function handleMousedown(evt: MouseEvent) {
evt.stopPropagation();
Expand All @@ -294,6 +287,27 @@ const ComposedModal = React.forwardRef<HTMLDivElement, ComposedModalProps>(
}
}

function handleBlur({
target: oldActiveNode,
relatedTarget: currentActiveNode,
}) {
if (open && currentActiveNode && oldActiveNode && innerModal.current) {
const { current: bodyNode } = innerModal;
const { current: startSentinelNode } = startSentinel;
const { current: endSentinelNode } = endSentinel;
wrapFocus({
bodyNode,
startTrapNode: startSentinelNode,
endTrapNode: endSentinelNode,
currentActiveNode,
oldActiveNode,
selectorsFloatingMenus: selectorsFloatingMenus?.filter(
Boolean
) as string[],
});
}
}

function closeModal(evt) {
if (!onClose || onClose(evt) !== false) {
setIsOpen(false);
Expand Down Expand Up @@ -387,13 +401,14 @@ const ComposedModal = React.forwardRef<HTMLDivElement, ComposedModalProps>(
});
}

const focusTrapWithoutSentinels = true; // TODO: replace with a feature flag
return (
<div
{...rest}
role="presentation"
ref={ref}
aria-hidden={!open}
onKeyDown={onKeyDown}
onBlur={!focusTrapWithoutSentinels ? handleBlur : () => {}}
onMouseDown={handleMousedown}
onKeyDown={handleKeyDown}
className={modalClass}>
Expand All @@ -404,24 +419,27 @@ const ComposedModal = React.forwardRef<HTMLDivElement, ComposedModalProps>(
aria-label={ariaLabel ? ariaLabel : generatedAriaLabel}
aria-labelledby={ariaLabelledBy}>
{/* Non-translatable: Focus-wrap code makes this `<button>` not actually read by screen readers */}

{/* <button
type="button"
ref={startSentinel}
className={`${prefix}--visually-hidden`}>
Focus sentinel
</button> */}
{!focusTrapWithoutSentinels && (
<button
type="button"
ref={startSentinel}
className={`${prefix}--visually-hidden`}>
Focus sentinel
</button>
)}
<div ref={innerModal} className={`${prefix}--modal-container-body`}>
{normalizedSlug}
{childrenWithProps}
</div>
{/* Non-translatable: Focus-wrap code makes this `<button>` not actually read by screen readers */}
{/* <button
type="button"
ref={endSentinel}
className={`${prefix}--visually-hidden`}>
Focus sentinel
</button> */}
{!focusTrapWithoutSentinels && (
<button
type="button"
ref={endSentinel}
className={`${prefix}--visually-hidden`}>
Focus sentinel
</button>
)}
</div>
</div>
);
Expand Down
49 changes: 34 additions & 15 deletions packages/react/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ButtonSet from '../ButtonSet';
import InlineLoading from '../InlineLoading';
import requiredIfGivenPropIsTruthy from '../../prop-types/requiredIfGivenPropIsTruthy';
import wrapFocus, {
wrapFocusWithoutSentinels,
elementOrParentIsFloatingMenu,
} from '../../internal/wrapFocus';
import debounce from 'lodash.debounce';
Expand Down Expand Up @@ -284,13 +285,26 @@ const Modal = React.forwardRef(function Modal(
if (match(evt, keys.Escape)) {
onRequestClose(evt);
}

if (
match(evt, keys.Enter) &&
shouldSubmitOnEnter &&
!isCloseButton(evt.target as Element)
) {
onRequestSubmit(evt);
}

if (
focusTrapWithoutSentinels &&
match(evt, keys.Tab) &&
innerModal.current
) {
wrapFocusWithoutSentinels({
containerNode: innerModal.current,
currentActiveNode: evt.target,
event: evt as any,
});
}
}
}

Expand Down Expand Up @@ -563,32 +577,37 @@ const Modal = React.forwardRef(function Modal(
</div>
);

const focusTrapWithoutSentinels = true; // TODO: replace with a feature flag
return (
<div
{...rest}
onKeyDown={handleKeyDown}
onMouseDown={handleMousedown}
onBlur={handleBlur}
onBlur={!focusTrapWithoutSentinels ? handleBlur : () => {}}
className={modalClasses}
role="presentation"
ref={ref}>
{/* Non-translatable: Focus-wrap code makes this `<span>` not actually read by screen readers */}
<span
ref={startTrap}
tabIndex={0}
role="link"
className={`${prefix}--visually-hidden`}>
Focus sentinel
</span>
{!focusTrapWithoutSentinels && (
<span
ref={startTrap}
tabIndex={0}
role="link"
className={`${prefix}--visually-hidden`}>
Focus sentinel
</span>
)}
{modalBody}
{/* Non-translatable: Focus-wrap code makes this `<span>` not actually read by screen readers */}
<span
ref={endTrap}
tabIndex={0}
role="link"
className={`${prefix}--visually-hidden`}>
Focus sentinel
</span>
{!focusTrapWithoutSentinels && (
<span
ref={endTrap}
tabIndex={0}
role="link"
className={`${prefix}--visually-hidden`}>
Focus sentinel
</span>
)}
</div>
);
});
Expand Down
50 changes: 33 additions & 17 deletions packages/react/src/components/Notification/Notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ import {
useNoInteractiveChildren,
useInteractiveChildrenNeedDescription,
} from '../../internal/useNoInteractiveChildren';
import { keys, matches } from '../../internal/keyboard';
import { keys, matches, match } from '../../internal/keyboard';
import { usePrefix } from '../../internal/usePrefix';
import { useId } from '../../internal/useId';
import { noopFn } from '../../internal/noopFn';
import wrapFocus from '../../internal/wrapFocus';
import wrapFocus, { wrapFocusWithoutSentinels } from '../../internal/wrapFocus';

/**
* Conditionally call a callback when the escape key is pressed
Expand Down Expand Up @@ -980,6 +980,16 @@ export function ActionableNotification({
}
}

function handleKeyDown(event) {
if (isOpen && match(event, keys.Tab) && ref.current) {
wrapFocusWithoutSentinels({
containerNode: ref.current,
currentActiveNode: event.target,
event,
});
}
}

const handleClose = (evt: MouseEvent) => {
if (!onClose || onClose(evt) !== false) {
setIsOpen(false);
Expand All @@ -996,21 +1006,25 @@ export function ActionableNotification({
return null;
}

const focusTrapWithoutSentinels = true; // TODO: replace with a feature flag
return (
<div
{...rest}
ref={ref}
role={role}
className={containerClassName}
aria-labelledby={title ? id : subtitleId}
onBlur={handleBlur}>
<span
ref={startTrap}
tabIndex={0}
role="link"
className={`${prefix}--visually-hidden`}>
Focus sentinel
</span>
onBlur={!focusTrapWithoutSentinels ? handleBlur : () => {}}
onKeyDown={focusTrapWithoutSentinels ? handleKeyDown : () => {}}>
{!focusTrapWithoutSentinels && (
<span
ref={startTrap}
tabIndex={0}
role="link"
className={`${prefix}--visually-hidden`}>
Focus sentinel
</span>
)}

<div className={`${prefix}--actionable-notification__details`}>
<NotificationIcon
Expand Down Expand Up @@ -1059,13 +1073,15 @@ export function ActionableNotification({
/>
)}
</div>
<span
ref={endTrap}
tabIndex={0}
role="link"
className={`${prefix}--visually-hidden`}>
Focus sentinel
</span>
{!focusTrapWithoutSentinels && (
<span
ref={endTrap}
tabIndex={0}
role="link"
className={`${prefix}--visually-hidden`}>
Focus sentinel
</span>
)}
</div>
);
}
Expand Down
Loading

0 comments on commit 9736ddd

Please sign in to comment.