Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.

Update ref.current access in hooks and fix useForwardedRef

+13 -12
src/useDialogFocus.ts
···
const hasPriority = usePriority(ref, disabled);
useLayoutEffect(() => {
-
if (!ref.current || disabled) return;
let selection = snapshotSelection(ownerRef && ownerRef.current);
let willReceiveFocus = false;
let focusMovesForward = true;
function onClick(event: MouseEvent) {
-
if (!ref.current || event.defaultPrevented) return;
const target = event.target as HTMLElement | null;
-
if (target && getFocusTargets(ref.current).indexOf(target) > -1) {
selection = null;
willReceiveFocus = true;
}
}
function onFocus(event: FocusEvent) {
-
if (!ref.current || event.defaultPrevented) return;
const active = document.activeElement as HTMLElement;
const owner =
···
!contains(ref.current, relatedTarget)
) {
// Get the next focus target of the container
-
const focusTarget = getNextFocusTarget(ref.current, !focusMovesForward);
if (focusTarget) {
focusMovesForward = true;
event.preventDefault();
···
}
function onKey(event: KeyboardEvent) {
-
if (!ref.current || event.defaultPrevented || event.isComposing) return;
// Mark whether focus is moving forward for the `onFocus` handler
if (event.code === 'Tab') {
···
const active = document.activeElement as HTMLElement;
const owner =
(ownerRef && ownerRef.current) || (selection && selection.element);
-
const focusTargets = getFocusTargets(ref.current);
if (
!focusTargets.length ||
···
return;
} else if (event.code === 'Tab') {
// Skip over the listbox via the parent if we press tab
-
const currentTarget = contains(owner, active) ? owner! : ref.current;
const focusTarget = getNextFocusTarget(currentTarget, event.shiftKey);
if (focusTarget) {
event.preventDefault();
···
event.code === 'Enter'
) {
// Move focus to first target when Enter is pressed
-
const newTarget = getFirstFocusTarget(ref.current);
if (newTarget) {
willReceiveFocus = true;
newTarget.focus();
···
}
}
-
ref.current.addEventListener('mousedown', onClick, true);
document.body.addEventListener('focusin', onFocus);
document.addEventListener('keydown', onKey);
return () => {
-
ref.current!.removeEventListener('mousedown', onClick);
document.body.removeEventListener('focusin', onFocus);
document.removeEventListener('keydown', onKey);
};
-
}, [ref.current!, disabled, hasPriority]);
}
···
const hasPriority = usePriority(ref, disabled);
useLayoutEffect(() => {
+
const { current: element } = ref;
+
if (!element || disabled) return;
let selection = snapshotSelection(ownerRef && ownerRef.current);
let willReceiveFocus = false;
let focusMovesForward = true;
function onClick(event: MouseEvent) {
+
if (!element || event.defaultPrevented) return;
const target = event.target as HTMLElement | null;
+
if (target && getFocusTargets(element).indexOf(target) > -1) {
selection = null;
willReceiveFocus = true;
}
}
function onFocus(event: FocusEvent) {
+
if (!element || event.defaultPrevented) return;
const active = document.activeElement as HTMLElement;
const owner =
···
!contains(ref.current, relatedTarget)
) {
// Get the next focus target of the container
+
const focusTarget = getNextFocusTarget(element, !focusMovesForward);
if (focusTarget) {
focusMovesForward = true;
event.preventDefault();
···
}
function onKey(event: KeyboardEvent) {
+
if (!element || event.defaultPrevented || event.isComposing) return;
// Mark whether focus is moving forward for the `onFocus` handler
if (event.code === 'Tab') {
···
const active = document.activeElement as HTMLElement;
const owner =
(ownerRef && ownerRef.current) || (selection && selection.element);
+
const focusTargets = getFocusTargets(element);
if (
!focusTargets.length ||
···
return;
} else if (event.code === 'Tab') {
// Skip over the listbox via the parent if we press tab
+
const currentTarget = contains(owner, active) ? owner! : element;
const focusTarget = getNextFocusTarget(currentTarget, event.shiftKey);
if (focusTarget) {
event.preventDefault();
···
event.code === 'Enter'
) {
// Move focus to first target when Enter is pressed
+
const newTarget = getFirstFocusTarget(element);
if (newTarget) {
willReceiveFocus = true;
newTarget.focus();
···
}
}
+
element.addEventListener('mousedown', onClick, true);
document.body.addEventListener('focusin', onFocus);
document.addEventListener('keydown', onKey);
return () => {
+
element.removeEventListener('mousedown', onClick);
document.body.removeEventListener('focusin', onFocus);
document.removeEventListener('keydown', onKey);
};
+
}, [ref.current, disabled, hasPriority]);
}
+7 -9
src/useDismissable.ts
···
}, [onDismiss]);
useLayoutEffect(() => {
-
if (!ref.current || disabled) return;
function onFocusOut(event: FocusEvent) {
if (event.defaultPrevented) return;
const { target, relatedTarget } = event;
-
if (
-
contains(ref.current, target) &&
-
!contains(ref.current, relatedTarget)
-
) {
onDismissRef.current();
}
}
···
// The current dialog can be dismissed by pressing escape if it either has focus
// or it has priority
const active = document.activeElement;
-
if (hasPriority || (active && contains(ref.current, active))) {
event.preventDefault();
onDismissRef.current();
}
···
function onClick(event: MouseEvent | TouchEvent) {
const { target } = event;
-
if (contains(ref.current, target) || event.defaultPrevented) {
return;
}
// The current dialog can be dismissed by pressing outside of it if it either has
// focus or it has priority
const active = document.activeElement;
-
if (hasPriority || (active && contains(ref.current, active))) {
event.preventDefault();
onDismissRef.current();
}
···
document.removeEventListener('touchstart', onClick);
document.removeEventListener('keydown', onKey);
};
-
}, [ref.current!, hasPriority, disabled, focusLoss]);
}
···
}, [onDismiss]);
useLayoutEffect(() => {
+
const { current: element } = ref;
+
if (!element || disabled) return;
function onFocusOut(event: FocusEvent) {
if (event.defaultPrevented) return;
const { target, relatedTarget } = event;
+
if (contains(element, target) && !contains(element, relatedTarget)) {
onDismissRef.current();
}
}
···
// The current dialog can be dismissed by pressing escape if it either has focus
// or it has priority
const active = document.activeElement;
+
if (hasPriority || (active && contains(element, active))) {
event.preventDefault();
onDismissRef.current();
}
···
function onClick(event: MouseEvent | TouchEvent) {
const { target } = event;
+
if (contains(element, target) || event.defaultPrevented) {
return;
}
// The current dialog can be dismissed by pressing outside of it if it either has
// focus or it has priority
const active = document.activeElement;
+
if (hasPriority || (active && contains(element, active))) {
event.preventDefault();
onDismissRef.current();
}
···
document.removeEventListener('touchstart', onClick);
document.removeEventListener('keydown', onKey);
};
+
}, [ref.current, hasPriority, disabled, focusLoss]);
}
+5 -7
src/useForwardedRef.ts
···
export function useForwardedRef<T extends HTMLElement>(
forwarded: ForwardedRef<T>
): Ref<T> {
-
const ref: RefWithState<T> = useRef<T>(null);
-
if (ref._forwarded !== forwarded) {
-
ref._forwarded = forwarded;
-
Object.defineProperty(ref, 'current', {
enumerable: true,
configurable: true,
get() {
···
this._forwarded.current = value;
}
},
-
});
}
-
-
return ref;
}
···
export function useForwardedRef<T extends HTMLElement>(
forwarded: ForwardedRef<T>
): Ref<T> {
+
const ref = useRef<RefWithState<T> | null>(null);
+
if (!ref.current || ref.current._forwarded !== forwarded) {
+
ref.current = Object.defineProperty({ _forwarded: forwarded }, 'current', {
enumerable: true,
configurable: true,
get() {
···
this._forwarded.current = value;
}
},
+
}) as RefWithState<T>;
}
+
return ref.current;
}
+12 -11
src/useMenuFocus.ts
···
const disabled = !!(options && options.disabled);
useLayoutEffect(() => {
-
if (!ref.current || disabled) return;
let selection: RestoreSelection | null = null;
function onFocus(event: FocusEvent) {
-
if (!ref.current || event.defaultPrevented) return;
const owner =
(ownerRef && ownerRef.current) || (selection && selection.element);
···
// When owner is explicitly passed we can make a snapshot early
selection = snapshotSelection(owner);
} else if (
-
contains(ref.current, target) &&
-
!contains(ref.current, relatedTarget) &&
(!ownerRef || contains(relatedTarget, ownerRef.current))
) {
// Check whether focus is about to move into the container and snapshot last focus
selection = snapshotSelection(owner);
} else if (
-
contains(ref.current, relatedTarget) &&
-
!contains(ref.current, target)
) {
// Reset focus if it's lost and has left the menu
selection = null;
···
}
function onKey(event: KeyboardEvent) {
-
if (!ref.current || event.defaultPrevented || event.isComposing) return;
const owner =
(ownerRef && ownerRef.current) || (selection && selection.element);
const active = document.activeElement as HTMLElement;
-
const focusTargets = getFocusTargets(ref.current);
if (
!focusTargets.length ||
-
(!contains(ref.current, active) && !contains(owner, active))
) {
// Do nothing if container doesn't contain focus or not targets are available
return;
···
event.code === 'Enter'
) {
// Move focus to first target when enter is pressed
-
const newTarget = getFirstFocusTarget(ref.current);
if (newTarget) newTarget.focus();
} else if (
owner &&
···
document.body.removeEventListener('focusin', onFocus);
document.removeEventListener('keydown', onKey);
};
-
}, [ref.current!, disabled]);
}
···
const disabled = !!(options && options.disabled);
useLayoutEffect(() => {
+
const { current: element } = ref;
+
if (!element || disabled) return;
let selection: RestoreSelection | null = null;
function onFocus(event: FocusEvent) {
+
if (!element || event.defaultPrevented) return;
const owner =
(ownerRef && ownerRef.current) || (selection && selection.element);
···
// When owner is explicitly passed we can make a snapshot early
selection = snapshotSelection(owner);
} else if (
+
contains(element, target) &&
+
!contains(element, relatedTarget) &&
(!ownerRef || contains(relatedTarget, ownerRef.current))
) {
// Check whether focus is about to move into the container and snapshot last focus
selection = snapshotSelection(owner);
} else if (
+
contains(element, relatedTarget) &&
+
!contains(element, target)
) {
// Reset focus if it's lost and has left the menu
selection = null;
···
}
function onKey(event: KeyboardEvent) {
+
if (!element || event.defaultPrevented || event.isComposing) return;
const owner =
(ownerRef && ownerRef.current) || (selection && selection.element);
const active = document.activeElement as HTMLElement;
+
const focusTargets = getFocusTargets(element);
if (
!focusTargets.length ||
+
(!contains(element, active) && !contains(owner, active))
) {
// Do nothing if container doesn't contain focus or not targets are available
return;
···
event.code === 'Enter'
) {
// Move focus to first target when enter is pressed
+
const newTarget = getFirstFocusTarget(element);
if (newTarget) newTarget.focus();
} else if (
owner &&
···
document.body.removeEventListener('focusin', onFocus);
document.removeEventListener('keydown', onKey);
};
+
}, [ref.current, disabled]);
}
+12 -16
src/useModalFocus.ts
···
const hasPriority = usePriority(ref, disabled);
useLayoutEffect(() => {
-
if (!ref.current || !hasPriority || disabled) return;
let selection: RestoreSelection | null = null;
-
if (
-
!document.activeElement ||
-
!contains(ref.current, document.activeElement)
-
) {
-
const newTarget = getAutofocusTarget(ref.current);
-
selection = snapshotSelection(ref.current);
newTarget.focus();
}
function onBlur(event: FocusEvent) {
-
const parent = ref.current;
-
if (!parent || event.defaultPrevented) return;
if (
-
contains(parent, event.target) &&
-
!contains(parent, event.relatedTarget)
) {
-
const target = getFirstFocusTarget(parent);
if (target) target.focus();
}
}
function onKeyDown(event: KeyboardEvent) {
-
const parent = ref.current;
-
if (!parent || event.defaultPrevented) return;
if (event.code === 'Tab') {
const activeElement = document.activeElement as HTMLElement;
-
const targets = getFocusTargets(parent);
const index = targets.indexOf(activeElement);
if (event.shiftKey && index === 0) {
event.preventDefault();
···
document.body.removeEventListener('focusout', onBlur);
document.removeEventListener('keydown', onKeyDown);
};
-
}, [ref.current!, hasPriority, disabled]);
}
···
const hasPriority = usePriority(ref, disabled);
useLayoutEffect(() => {
+
const { current: element } = ref;
+
if (!element || !hasPriority || disabled) return;
let selection: RestoreSelection | null = null;
+
if (!document.activeElement || !contains(element, document.activeElement)) {
+
const newTarget = getAutofocusTarget(element);
+
selection = snapshotSelection(element);
newTarget.focus();
}
function onBlur(event: FocusEvent) {
+
if (!element || event.defaultPrevented) return;
if (
+
contains(element, event.target) &&
+
!contains(element, event.relatedTarget)
) {
+
const target = getFirstFocusTarget(element);
if (target) target.focus();
}
}
function onKeyDown(event: KeyboardEvent) {
+
if (!element || event.defaultPrevented) return;
if (event.code === 'Tab') {
const activeElement = document.activeElement as HTMLElement;
+
const targets = getFocusTargets(element);
const index = targets.indexOf(activeElement);
if (event.shiftKey && index === 0) {
event.preventDefault();
···
document.body.removeEventListener('focusout', onBlur);
document.removeEventListener('keydown', onKeyDown);
};
+
}, [ref.current, hasPriority, disabled]);
}
+5 -6
src/usePriority.ts
···
});
useLayoutEffect(() => {
-
if (!ref.current || isDisabled) return;
-
-
const { current } = ref;
function onChange() {
setHasPriority(() => priorityStack[0] === ref.current);
}
-
priorityStack.push(current);
priorityStack.sort(sortByHierarchy);
listeners.add(onChange);
listeners.forEach(fn => fn());
return () => {
-
const index = priorityStack.indexOf(current);
priorityStack.splice(index, 1);
listeners.delete(onChange);
listeners.forEach(fn => fn());
};
-
}, [ref.current!, isDisabled]);
return hasPriority;
};
···
});
useLayoutEffect(() => {
+
const { current: element } = ref;
+
if (!element || isDisabled) return;
function onChange() {
setHasPriority(() => priorityStack[0] === ref.current);
}
+
priorityStack.push(element);
priorityStack.sort(sortByHierarchy);
listeners.add(onChange);
listeners.forEach(fn => fn());
return () => {
+
const index = priorityStack.indexOf(element);
priorityStack.splice(index, 1);
listeners.delete(onChange);
listeners.forEach(fn => fn());
};
+
}, [ref.current, isDisabled]);
return hasPriority;
};