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 {FocusEvent, HTMLAttributes, useRef} from 'react';
|
19 | import {useSyntheticBlurEvent} from './utils';
|
20 |
|
21 | interface FocusWithinProps {
|
22 | /** Whether the focus within events should be disabled. */
|
23 | isDisabled?: boolean,
|
24 | /** Handler that is called when the target element or a descendant receives focus. */
|
25 | onFocusWithin?: (e: FocusEvent) => void,
|
26 | /** Handler that is called when the target element and all descendants lose focus. */
|
27 | onBlurWithin?: (e: FocusEvent) => void,
|
28 | /** Handler that is called when the the focus within state changes. */
|
29 | onFocusWithinChange?: (isFocusWithin: boolean) => void
|
30 | }
|
31 |
|
32 | interface FocusWithinResult {
|
33 | /** Props to spread onto the target element. */
|
34 | focusWithinProps: HTMLAttributes<HTMLElement>
|
35 | }
|
36 |
|
37 | /**
|
38 | * Handles focus events for the target and its descendants.
|
39 | */
|
40 | export function useFocusWithin(props: FocusWithinProps): FocusWithinResult {
|
41 | let state = useRef({
|
42 | isFocusWithin: false
|
43 | }).current;
|
44 |
|
45 | let onBlur = props.isDisabled ? null : (e: FocusEvent) => {
|
46 | // We don't want to trigger onBlurWithin and then immediately onFocusWithin again
|
47 | // when moving focus inside the element. Only trigger if the currentTarget doesn't
|
48 | // include the relatedTarget (where focus is moving).
|
49 | if (state.isFocusWithin && !(e.currentTarget as Element).contains(e.relatedTarget as Element)) {
|
50 | state.isFocusWithin = false;
|
51 |
|
52 | if (props.onBlurWithin) {
|
53 | props.onBlurWithin(e);
|
54 | }
|
55 |
|
56 | if (props.onFocusWithinChange) {
|
57 | props.onFocusWithinChange(false);
|
58 | }
|
59 | }
|
60 | };
|
61 |
|
62 | let onSyntheticFocus = useSyntheticBlurEvent(onBlur);
|
63 | let onFocus = props.isDisabled ? null : (e: FocusEvent) => {
|
64 | if (!state.isFocusWithin) {
|
65 | if (props.onFocusWithin) {
|
66 | props.onFocusWithin(e);
|
67 | }
|
68 |
|
69 | if (props.onFocusWithinChange) {
|
70 | props.onFocusWithinChange(true);
|
71 | }
|
72 |
|
73 | state.isFocusWithin = true;
|
74 | onSyntheticFocus(e);
|
75 | }
|
76 | };
|
77 |
|
78 | return {
|
79 | focusWithinProps: {
|
80 | onFocus,
|
81 | onBlur
|
82 | }
|
83 | };
|
84 | }
|