UNPKG

3.55 kBPlain TextView Raw
1/*
2 * Copyright 2020 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12
13// Portions of the code in this file are based on code from react.
14// Original licensing for the following can be found in the
15// NOTICE file in the root directory of this source tree.
16// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
17
18import {DOMAttributes} from '@react-types/shared';
19import {FocusEvent, useCallback, useRef} from 'react';
20import {useSyntheticBlurEvent} from './utils';
21
22export interface FocusWithinProps {
23 /** Whether the focus within events should be disabled. */
24 isDisabled?: boolean,
25 /** Handler that is called when the target element or a descendant receives focus. */
26 onFocusWithin?: (e: FocusEvent) => void,
27 /** Handler that is called when the target element and all descendants lose focus. */
28 onBlurWithin?: (e: FocusEvent) => void,
29 /** Handler that is called when the the focus within state changes. */
30 onFocusWithinChange?: (isFocusWithin: boolean) => void
31}
32
33export interface FocusWithinResult {
34 /** Props to spread onto the target element. */
35 focusWithinProps: DOMAttributes
36}
37
38/**
39 * Handles focus events for the target and its descendants.
40 */
41export function useFocusWithin(props: FocusWithinProps): FocusWithinResult {
42 let {
43 isDisabled,
44 onBlurWithin,
45 onFocusWithin,
46 onFocusWithinChange
47 } = props;
48 let state = useRef({
49 isFocusWithin: false
50 });
51
52 let onBlur = useCallback((e: FocusEvent) => {
53 // We don't want to trigger onBlurWithin and then immediately onFocusWithin again
54 // when moving focus inside the element. Only trigger if the currentTarget doesn't
55 // include the relatedTarget (where focus is moving).
56 if (state.current.isFocusWithin && !(e.currentTarget as Element).contains(e.relatedTarget as Element)) {
57 state.current.isFocusWithin = false;
58
59 if (onBlurWithin) {
60 onBlurWithin(e);
61 }
62
63 if (onFocusWithinChange) {
64 onFocusWithinChange(false);
65 }
66 }
67 }, [onBlurWithin, onFocusWithinChange, state]);
68
69 let onSyntheticFocus = useSyntheticBlurEvent(onBlur);
70 let onFocus = useCallback((e: FocusEvent) => {
71 // Double check that document.activeElement actually matches e.target in case a previously chained
72 // focus handler already moved focus somewhere else.
73 if (!state.current.isFocusWithin && document.activeElement === e.target) {
74 if (onFocusWithin) {
75 onFocusWithin(e);
76 }
77
78 if (onFocusWithinChange) {
79 onFocusWithinChange(true);
80 }
81
82 state.current.isFocusWithin = true;
83 onSyntheticFocus(e);
84 }
85 }, [onFocusWithin, onFocusWithinChange, onSyntheticFocus]);
86
87 if (isDisabled) {
88 return {
89 focusWithinProps: {
90 // These should not have been null, that would conflict in mergeProps
91 onFocus: undefined,
92 onBlur: undefined
93 }
94 };
95 }
96
97 return {
98 focusWithinProps: {
99 onFocus,
100 onBlur
101 }
102 };
103}