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 |
|
18 | import {DOMAttributes} from '@react-types/shared';
|
19 | import {FocusEvent, useCallback, useRef} from 'react';
|
20 | import {useSyntheticBlurEvent} from './utils';
|
21 |
|
22 | export 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 |
|
33 | export 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 | */
|
41 | export 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 | }
|