@gravity-ui/uikit
Version:
Gravity UI base styling and components
116 lines (115 loc) • 3.94 kB
JavaScript
import * as React from 'react';
import { useLayoutEffect } from "../useLayoutEffect/index.js";
import { SyntheticFocusEvent } from "./SyntheticFocusEvent.js";
import { useSyntheticBlurEvent } from "./useSyntheticBlurEvent.js";
/**
* Callback on focus outside event.
* @callback onFocusEventCallback
* @param {FocusEvent} event
*/
/**
* Callback on focus change event.
* @callback onFocusChangeCallback
* @param {boolean} isFocusWithin
*/
/**
* Handles focus events for the target and its descendants.
* @param {object} props
* @param {boolean} [props.isDisabled] - whether the focus within events should be disabled.
* @param {onFocusEventCallback} props.onFocusWithin - handler that is called when the target element or a descendant receives focus.
* @param {onFocusEventCallback} props.onBlurWithin - handler that is called when the target element and all descendants lose focus.
* @param {onFocusChangeCallback} props.onFocusChange - handler that is called when the the focus within state changes.
* @returns container props
* @example
*
* function Select() {
* const [open, setOpen] = React.useState(false);
*
* const handleFocusOutside = React.useCallback(() => {setOpen(false);}, []);
*
* const {focusWithinProps} = useFocusWithin({onBlurWithin: handleFocusOutside});
*
* return (
* <span {...focusWithinProps}>
* <Button onClick={() => {setOpen(true)}}>Select</Button>
* <Popup open={open}>
* ...
* </Popup>
* </span>
* );
* }
*/
export function useFocusWithin(props) {
const { onFocusWithin, onBlurWithin, onFocusWithinChange, isDisabled } = props;
const isFocusWithinRef = React.useRef(false);
const onFocus = React.useCallback((event) => {
if (!isFocusWithinRef.current && document.activeElement === event.target) {
isFocusWithinRef.current = true;
if (onFocusWithin) {
onFocusWithin(event);
}
if (onFocusWithinChange) {
onFocusWithinChange(true);
}
}
}, [onFocusWithin, onFocusWithinChange]);
const onBlur = React.useCallback((event) => {
if (!isFocusWithinRef.current) {
return;
}
isFocusWithinRef.current = false;
if (onBlurWithin) {
onBlurWithin(event);
}
if (onFocusWithinChange) {
onFocusWithinChange(false);
}
}, [onBlurWithin, onFocusWithinChange]);
const { onBlur: onBlurHandler, onFocus: onFocusHandler } = useFocusEvents({
onFocus,
onBlur,
});
if (isDisabled) {
return {
focusWithinProps: {
onFocus: undefined,
onBlur: undefined,
},
};
}
return {
focusWithinProps: {
onFocus: onFocusHandler,
onBlur: onBlurHandler,
},
};
}
function useFocusEvents({ onFocus, onBlur, }) {
const capturedRef = React.useRef(false);
const onBlurRef = React.useRef(onBlur);
useLayoutEffect(() => {
onBlurRef.current = onBlur;
});
const onBlurHandler = React.useCallback((event) => {
if (event.currentTarget.contains(event.relatedTarget)) {
return;
}
capturedRef.current = false;
const newEvent = new SyntheticFocusEvent('blur', event.nativeEvent, {
currentTarget: event.currentTarget,
});
setTimeout(() => {
if (!capturedRef.current) {
onBlurRef.current(newEvent);
}
}, 0);
}, []);
const onSyntheticFocus = useSyntheticBlurEvent(onBlur);
const onFocusHandler = React.useCallback((event) => {
capturedRef.current = true;
onSyntheticFocus(event);
onFocus(event);
}, [onSyntheticFocus, onFocus]);
return { onBlur: onBlurHandler, onFocus: onFocusHandler };
}
//# sourceMappingURL=useFocusWithin.js.map