Skip to content

Commit

Permalink
refactor: Reuse nodeBelongs in focus tracker utility (#1468)
Browse files Browse the repository at this point in the history
  • Loading branch information
just-boris authored Aug 28, 2023
1 parent fb4b027 commit b28e7f5
Show file tree
Hide file tree
Showing 5 changed files with 22 additions and 34 deletions.
2 changes: 1 addition & 1 deletion src/date-picker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const DatePicker = React.forwardRef(
const calendarDescriptionId = useUniqueId('calendar-description-');
const mergedRef = useMergeRefs(rootRef, __internalRootRef);

useFocusTracker({ rootRef, onBlur, onFocus, viewportId: expandToViewport ? dropdownId : '' });
useFocusTracker({ rootRef, onBlur, onFocus });

const onDropdownCloseHandler = useCallback(() => setIsDropDownOpen(false), [setIsDropDownOpen]);

Expand Down
2 changes: 1 addition & 1 deletion src/date-range-picker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ const DateRangePicker = React.forwardRef(
const rootRef = useRef<HTMLDivElement>(null);
const dropdownId = useUniqueId('date-range-picker-dropdown');

useFocusTracker({ rootRef, onBlur, onFocus, viewportId: expandToViewport ? dropdownId : '' });
useFocusTracker({ rootRef, onBlur, onFocus });

const [isDropDownOpen, setIsDropDownOpen] = useState<boolean>(false);

Expand Down
19 changes: 6 additions & 13 deletions src/internal/focus-tracker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { containsOrEqual } from './utils/dom';
import { nodeBelongs } from './utils/node-belongs';

interface FocusTrackerOptions {
onFocusEnter: () => void;
Expand All @@ -10,18 +10,16 @@ interface FocusTrackerOptions {
export default class FocusTracker {
private readonly onFocusLeave: () => void;
private readonly onFocusEnter: () => void;
private readonly viewportId: string;

private currentlyFocused = false;

constructor(private node: HTMLElement, { onFocusEnter, onFocusLeave }: FocusTrackerOptions, viewportId = '') {
constructor(private node: HTMLElement, { onFocusEnter, onFocusLeave }: FocusTrackerOptions) {
this.onFocusEnter = onFocusEnter;
this.onFocusLeave = onFocusLeave;
this.viewportId = viewportId;
}

initialize() {
this.currentlyFocused = containsOrEqual(this.node, document.activeElement as any);
this.currentlyFocused = nodeBelongs(this.node, document.activeElement);
document.addEventListener('focusin', this.focusInListener);
document.addEventListener('focusout', this.focusOutListener);
}
Expand All @@ -32,20 +30,15 @@ export default class FocusTracker {
}

private focusInListener = (event: FocusEvent) => {
const focusIsInside = containsOrEqual(this.node, event.target as Node);
const focusIsInside = nodeBelongs(this.node, event.target);
if (!this.currentlyFocused && focusIsInside) {
this.triggerFocus();
}
};

private focusOutListener = (event: FocusEvent) => {
const nextFocused = event.relatedTarget as Node;
let isNextFocusedInParent = !containsOrEqual(this.node, nextFocused);

if (this.viewportId) {
const viewport = document.getElementById(this.viewportId);
isNextFocusedInParent = isNextFocusedInParent && !containsOrEqual(viewport, nextFocused);
}
const nextFocused = event.relatedTarget;
const isNextFocusedInParent = !nodeBelongs(this.node, nextFocused);
if (this.currentlyFocused && (nextFocused === null || isNextFocusedInParent)) {
this.triggerBlur();
}
Expand Down
23 changes: 9 additions & 14 deletions src/internal/hooks/use-focus-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,27 @@ interface UseFocusTracker {
onBlur?: NonCancelableEventHandler<any>;
onFocus?: NonCancelableEventHandler<any>;
rootRef: MutableRefObject<HTMLElement | null>;
viewportId?: string;
}): void;
}

export const useFocusTracker: UseFocusTracker = ({ rootRef, onBlur, onFocus, viewportId }) => {
export const useFocusTracker: UseFocusTracker = ({ rootRef, onBlur, onFocus }) => {
const focusTracker = useRef<FocusTracker | null>(null);

useEffect(() => {
if (!rootRef.current) {
return;
}
focusTracker.current = new FocusTracker(
rootRef.current,
{
onFocusLeave: () => {
fireNonCancelableEvent(onBlur);
},
onFocusEnter: () => {
fireNonCancelableEvent(onFocus);
},
focusTracker.current = new FocusTracker(rootRef.current, {
onFocusLeave: () => {
fireNonCancelableEvent(onBlur);
},
viewportId
);
onFocusEnter: () => {
fireNonCancelableEvent(onFocus);
},
});
focusTracker.current.initialize();
return () => {
focusTracker.current?.destroy();
};
}, [rootRef, onBlur, onFocus, viewportId]);
}, [rootRef, onBlur, onFocus]);
};
10 changes: 5 additions & 5 deletions src/internal/utils/node-belongs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { containsOrEqual, findUpUntil } from './dom';
* @param container Container node
* @param target Node that is checked to be a descendant of the container
*/
export function nodeBelongs(container: Node | null, target: Node): boolean {
const portal = findUpUntil(
target as HTMLElement,
node => node instanceof HTMLElement && !!node.dataset.awsuiReferrerId
);
export function nodeBelongs(container: Node | null, target: Node | EventTarget | null): boolean {
if (!(target instanceof Node)) {
return false;
}
const portal = findUpUntil(target as HTMLElement, node => !!node.dataset.awsuiReferrerId);
const referrer = portal instanceof HTMLElement ? document.getElementById(portal.dataset.awsuiReferrerId ?? '') : null;
return referrer ? containsOrEqual(container, referrer) : containsOrEqual(container, target);
}

0 comments on commit b28e7f5

Please sign in to comment.