This commit is contained in:
66
node_modules/@react-aria/interactions/src/PressResponder.tsx
generated
vendored
Normal file
66
node_modules/@react-aria/interactions/src/PressResponder.tsx
generated
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {FocusableElement} from '@react-types/shared';
|
||||
import {mergeProps, useObjectRef, useSyncRef} from '@react-aria/utils';
|
||||
import {PressProps} from './usePress';
|
||||
import {PressResponderContext} from './context';
|
||||
import React, {ForwardedRef, JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react';
|
||||
|
||||
interface PressResponderProps extends PressProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const PressResponder = React.forwardRef(({children, ...props}: PressResponderProps, ref: ForwardedRef<FocusableElement>) => {
|
||||
let isRegistered = useRef(false);
|
||||
let prevContext = useContext(PressResponderContext);
|
||||
ref = useObjectRef(ref || prevContext?.ref);
|
||||
let context = mergeProps(prevContext || {}, {
|
||||
...props,
|
||||
ref,
|
||||
register() {
|
||||
isRegistered.current = true;
|
||||
if (prevContext) {
|
||||
prevContext.register();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useSyncRef(prevContext, ref);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRegistered.current) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.warn(
|
||||
'A PressResponder was rendered without a pressable child. ' +
|
||||
'Either call the usePress hook, or wrap your DOM node with <Pressable> component.'
|
||||
);
|
||||
}
|
||||
isRegistered.current = true; // only warn once in strict mode.
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PressResponderContext.Provider value={context}>
|
||||
{children}
|
||||
</PressResponderContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
export function ClearPressResponder({children}: {children: ReactNode}): JSX.Element {
|
||||
let context = useMemo(() => ({register: () => {}}), []);
|
||||
return (
|
||||
<PressResponderContext.Provider value={context}>
|
||||
{children}
|
||||
</PressResponderContext.Provider>
|
||||
);
|
||||
}
|
||||
95
node_modules/@react-aria/interactions/src/Pressable.tsx
generated
vendored
Normal file
95
node_modules/@react-aria/interactions/src/Pressable.tsx
generated
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {DOMAttributes, FocusableElement} from '@react-types/shared';
|
||||
import {getOwnerWindow, isFocusable, mergeProps, mergeRefs, useObjectRef} from '@react-aria/utils';
|
||||
import {PressProps, usePress} from './usePress';
|
||||
import React, {ForwardedRef, ReactElement, useEffect} from 'react';
|
||||
import {useFocusable} from './useFocusable';
|
||||
|
||||
interface PressableProps extends PressProps {
|
||||
children: ReactElement<DOMAttributes, string>
|
||||
}
|
||||
|
||||
export const Pressable = React.forwardRef(({children, ...props}: PressableProps, ref: ForwardedRef<FocusableElement>) => {
|
||||
ref = useObjectRef(ref);
|
||||
let {pressProps} = usePress({...props, ref});
|
||||
let {focusableProps} = useFocusable(props, ref);
|
||||
let child = React.Children.only(children);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return;
|
||||
}
|
||||
|
||||
let el = ref.current;
|
||||
if (!el || !(el instanceof getOwnerWindow(el).Element)) {
|
||||
console.error('<Pressable> child must forward its ref to a DOM element.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFocusable(el)) {
|
||||
console.warn('<Pressable> child must be focusable. Please ensure the tabIndex prop is passed through.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
el.localName !== 'button' &&
|
||||
el.localName !== 'input' &&
|
||||
el.localName !== 'select' &&
|
||||
el.localName !== 'textarea' &&
|
||||
el.localName !== 'a' &&
|
||||
el.localName !== 'area' &&
|
||||
el.localName !== 'summary'
|
||||
) {
|
||||
let role = el.getAttribute('role');
|
||||
if (!role) {
|
||||
console.warn('<Pressable> child must have an interactive ARIA role.');
|
||||
} else if (
|
||||
// https://w3c.github.io/aria/#widget_roles
|
||||
role !== 'application' &&
|
||||
role !== 'button' &&
|
||||
role !== 'checkbox' &&
|
||||
role !== 'combobox' &&
|
||||
role !== 'gridcell' &&
|
||||
role !== 'link' &&
|
||||
role !== 'menuitem' &&
|
||||
role !== 'menuitemcheckbox' &&
|
||||
role !== 'menuitemradio' &&
|
||||
role !== 'option' &&
|
||||
role !== 'radio' &&
|
||||
role !== 'searchbox' &&
|
||||
role !== 'separator' &&
|
||||
role !== 'slider' &&
|
||||
role !== 'spinbutton' &&
|
||||
role !== 'switch' &&
|
||||
role !== 'tab' &&
|
||||
role !== 'textbox' &&
|
||||
role !== 'treeitem'
|
||||
) {
|
||||
console.warn(`<Pressable> child must have an interactive ARIA role. Got "${role}".`);
|
||||
}
|
||||
}
|
||||
}, [ref]);
|
||||
|
||||
// @ts-ignore
|
||||
let childRef = parseInt(React.version, 10) < 19 ? child.ref : child.props.ref;
|
||||
|
||||
return React.cloneElement(
|
||||
child,
|
||||
{
|
||||
...mergeProps(pressProps, focusableProps, child.props),
|
||||
// @ts-ignore
|
||||
ref: mergeRefs(childRef, ref)
|
||||
}
|
||||
);
|
||||
});
|
||||
23
node_modules/@react-aria/interactions/src/context.ts
generated
vendored
Normal file
23
node_modules/@react-aria/interactions/src/context.ts
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {FocusableElement} from '@react-types/shared';
|
||||
import {PressProps} from './usePress';
|
||||
import React, {MutableRefObject} from 'react';
|
||||
|
||||
interface IPressResponderContext extends PressProps {
|
||||
register(): void,
|
||||
ref?: MutableRefObject<FocusableElement>
|
||||
}
|
||||
|
||||
export const PressResponderContext = React.createContext<IPressResponderContext>({register: () => {}});
|
||||
PressResponderContext.displayName = 'PressResponderContext';
|
||||
55
node_modules/@react-aria/interactions/src/createEventHandler.ts
generated
vendored
Normal file
55
node_modules/@react-aria/interactions/src/createEventHandler.ts
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {BaseEvent} from '@react-types/shared';
|
||||
import {SyntheticEvent} from 'react';
|
||||
|
||||
/**
|
||||
* This function wraps a React event handler to make stopPropagation the default, and support continuePropagation instead.
|
||||
*/
|
||||
export function createEventHandler<T extends SyntheticEvent>(handler?: (e: BaseEvent<T>) => void): ((e: T) => void) | undefined {
|
||||
if (!handler) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let shouldStopPropagation = true;
|
||||
return (e: T) => {
|
||||
let event: BaseEvent<T> = {
|
||||
...e,
|
||||
preventDefault() {
|
||||
e.preventDefault();
|
||||
},
|
||||
isDefaultPrevented() {
|
||||
return e.isDefaultPrevented();
|
||||
},
|
||||
stopPropagation() {
|
||||
if (shouldStopPropagation && process.env.NODE_ENV !== 'production') {
|
||||
console.error('stopPropagation is now the default behavior for events in React Spectrum. You can use continuePropagation() to revert this behavior.');
|
||||
} else {
|
||||
shouldStopPropagation = true;
|
||||
}
|
||||
},
|
||||
continuePropagation() {
|
||||
shouldStopPropagation = false;
|
||||
},
|
||||
isPropagationStopped() {
|
||||
return shouldStopPropagation;
|
||||
}
|
||||
};
|
||||
|
||||
handler(event);
|
||||
|
||||
if (shouldStopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
}
|
||||
45
node_modules/@react-aria/interactions/src/focusSafely.ts
generated
vendored
Normal file
45
node_modules/@react-aria/interactions/src/focusSafely.ts
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {FocusableElement} from '@react-types/shared';
|
||||
import {
|
||||
focusWithoutScrolling,
|
||||
getActiveElement,
|
||||
getOwnerDocument,
|
||||
runAfterTransition
|
||||
} from '@react-aria/utils';
|
||||
import {getInteractionModality} from './useFocusVisible';
|
||||
|
||||
/**
|
||||
* A utility function that focuses an element while avoiding undesired side effects such
|
||||
* as page scrolling and screen reader issues with CSS transitions.
|
||||
*/
|
||||
export function focusSafely(element: FocusableElement): void {
|
||||
// If the user is interacting with a virtual cursor, e.g. screen reader, then
|
||||
// wait until after any animated transitions that are currently occurring on
|
||||
// the page before shifting focus. This avoids issues with VoiceOver on iOS
|
||||
// causing the page to scroll when moving focus if the element is transitioning
|
||||
// from off the screen.
|
||||
const ownerDocument = getOwnerDocument(element);
|
||||
const activeElement = getActiveElement(ownerDocument);
|
||||
if (getInteractionModality() === 'virtual') {
|
||||
let lastFocusedElement = activeElement;
|
||||
runAfterTransition(() => {
|
||||
// If focus did not move and the element is still in the document, focus it.
|
||||
if (getActiveElement(ownerDocument) === lastFocusedElement && element.isConnected) {
|
||||
focusWithoutScrolling(element);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
focusWithoutScrolling(element);
|
||||
}
|
||||
}
|
||||
47
node_modules/@react-aria/interactions/src/index.ts
generated
vendored
Normal file
47
node_modules/@react-aria/interactions/src/index.ts
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
export {Pressable} from './Pressable';
|
||||
export {PressResponder, ClearPressResponder} from './PressResponder';
|
||||
export {useFocus} from './useFocus';
|
||||
export {
|
||||
isFocusVisible,
|
||||
getInteractionModality,
|
||||
setInteractionModality,
|
||||
addWindowFocusTracking,
|
||||
useInteractionModality,
|
||||
useFocusVisible,
|
||||
useFocusVisibleListener
|
||||
} from './useFocusVisible';
|
||||
export {useFocusWithin} from './useFocusWithin';
|
||||
export {useHover} from './useHover';
|
||||
export {useInteractOutside} from './useInteractOutside';
|
||||
export {useKeyboard} from './useKeyboard';
|
||||
export {useMove} from './useMove';
|
||||
export {usePress} from './usePress';
|
||||
export {useScrollWheel} from './useScrollWheel';
|
||||
export {useLongPress} from './useLongPress';
|
||||
export {useFocusable, FocusableProvider, Focusable, FocusableContext} from './useFocusable';
|
||||
export {focusSafely} from './focusSafely';
|
||||
|
||||
export type {FocusProps, FocusResult} from './useFocus';
|
||||
export type {FocusVisibleHandler, FocusVisibleProps, FocusVisibleResult, Modality} from './useFocusVisible';
|
||||
export type {FocusWithinProps, FocusWithinResult} from './useFocusWithin';
|
||||
export type {HoverProps, HoverResult} from './useHover';
|
||||
export type {InteractOutsideProps} from './useInteractOutside';
|
||||
export type {KeyboardProps, KeyboardResult} from './useKeyboard';
|
||||
export type {PressProps, PressHookProps, PressResult} from './usePress';
|
||||
export type {PressEvent, PressEvents, MoveStartEvent, MoveMoveEvent, MoveEndEvent, MoveEvents, HoverEvent, HoverEvents, FocusEvents, KeyboardEvents} from '@react-types/shared';
|
||||
export type {MoveResult} from './useMove';
|
||||
export type {LongPressProps, LongPressResult} from './useLongPress';
|
||||
export type {ScrollWheelProps} from './useScrollWheel';
|
||||
export type {FocusableAria, FocusableOptions, FocusableProviderProps} from './useFocusable';
|
||||
101
node_modules/@react-aria/interactions/src/textSelection.ts
generated
vendored
Normal file
101
node_modules/@react-aria/interactions/src/textSelection.ts
generated
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {getOwnerDocument, isIOS, runAfterTransition} from '@react-aria/utils';
|
||||
|
||||
// Safari on iOS starts selecting text on long press. The only way to avoid this, it seems,
|
||||
// is to add user-select: none to the entire page. Adding it to the pressable element prevents
|
||||
// that element from being selected, but nearby elements may still receive selection. We add
|
||||
// user-select: none on touch start, and remove it again on touch end to prevent this.
|
||||
// This must be implemented using global state to avoid race conditions between multiple elements.
|
||||
|
||||
// There are three possible states due to the delay before removing user-select: none after
|
||||
// pointer up. The 'default' state always transitions to the 'disabled' state, which transitions
|
||||
// to 'restoring'. The 'restoring' state can either transition back to 'disabled' or 'default'.
|
||||
|
||||
// For non-iOS devices, we apply user-select: none to the pressed element instead to avoid possible
|
||||
// performance issues that arise from applying and removing user-select: none to the entire page
|
||||
// (see https://github.com/adobe/react-spectrum/issues/1609).
|
||||
type State = 'default' | 'disabled' | 'restoring';
|
||||
|
||||
// Note that state only matters here for iOS. Non-iOS gets user-select: none applied to the target element
|
||||
// rather than at the document level so we just need to apply/remove user-select: none for each pressed element individually
|
||||
let state: State = 'default';
|
||||
let savedUserSelect = '';
|
||||
let modifiedElementMap = new WeakMap<Element, string>();
|
||||
|
||||
export function disableTextSelection(target?: Element): void {
|
||||
if (isIOS()) {
|
||||
if (state === 'default') {
|
||||
|
||||
const documentObject = getOwnerDocument(target);
|
||||
savedUserSelect = documentObject.documentElement.style.webkitUserSelect;
|
||||
documentObject.documentElement.style.webkitUserSelect = 'none';
|
||||
}
|
||||
|
||||
state = 'disabled';
|
||||
} else if (target instanceof HTMLElement || target instanceof SVGElement) {
|
||||
// If not iOS, store the target's original user-select and change to user-select: none
|
||||
// Ignore state since it doesn't apply for non iOS
|
||||
let property = 'userSelect' in target.style ? 'userSelect' : 'webkitUserSelect';
|
||||
modifiedElementMap.set(target, target.style[property]);
|
||||
target.style[property] = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
export function restoreTextSelection(target?: Element): void {
|
||||
if (isIOS()) {
|
||||
// If the state is already default, there's nothing to do.
|
||||
// If it is restoring, then there's no need to queue a second restore.
|
||||
if (state !== 'disabled') {
|
||||
return;
|
||||
}
|
||||
|
||||
state = 'restoring';
|
||||
|
||||
// There appears to be a delay on iOS where selection still might occur
|
||||
// after pointer up, so wait a bit before removing user-select.
|
||||
setTimeout(() => {
|
||||
// Wait for any CSS transitions to complete so we don't recompute style
|
||||
// for the whole page in the middle of the animation and cause jank.
|
||||
runAfterTransition(() => {
|
||||
// Avoid race conditions
|
||||
if (state === 'restoring') {
|
||||
|
||||
const documentObject = getOwnerDocument(target);
|
||||
if (documentObject.documentElement.style.webkitUserSelect === 'none') {
|
||||
documentObject.documentElement.style.webkitUserSelect = savedUserSelect || '';
|
||||
}
|
||||
|
||||
savedUserSelect = '';
|
||||
state = 'default';
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
} else if (target instanceof HTMLElement || target instanceof SVGElement) {
|
||||
// If not iOS, restore the target's original user-select if any
|
||||
// Ignore state since it doesn't apply for non iOS
|
||||
if (target && modifiedElementMap.has(target)) {
|
||||
let targetOldUserSelect = modifiedElementMap.get(target) as string;
|
||||
let property = 'userSelect' in target.style ? 'userSelect' : 'webkitUserSelect';
|
||||
|
||||
if (target.style[property] === 'none') {
|
||||
target.style[property] = targetOldUserSelect;
|
||||
}
|
||||
|
||||
if (target.getAttribute('style') === '') {
|
||||
target.removeAttribute('style');
|
||||
}
|
||||
modifiedElementMap.delete(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
87
node_modules/@react-aria/interactions/src/useFocus.ts
generated
vendored
Normal file
87
node_modules/@react-aria/interactions/src/useFocus.ts
generated
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
// Portions of the code in this file are based on code from react.
|
||||
// Original licensing for the following can be found in the
|
||||
// NOTICE file in the root directory of this source tree.
|
||||
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
|
||||
|
||||
import {DOMAttributes, FocusableElement, FocusEvents} from '@react-types/shared';
|
||||
import {FocusEvent, useCallback} from 'react';
|
||||
import {getActiveElement, getEventTarget, getOwnerDocument} from '@react-aria/utils';
|
||||
import {useSyntheticBlurEvent} from './utils';
|
||||
|
||||
export interface FocusProps<Target = FocusableElement> extends FocusEvents<Target> {
|
||||
/** Whether the focus events should be disabled. */
|
||||
isDisabled?: boolean
|
||||
}
|
||||
|
||||
export interface FocusResult<Target = FocusableElement> {
|
||||
/** Props to spread onto the target element. */
|
||||
focusProps: DOMAttributes<Target>
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles focus events for the immediate target.
|
||||
* Focus events on child elements will be ignored.
|
||||
*/
|
||||
export function useFocus<Target extends FocusableElement = FocusableElement>(props: FocusProps<Target>): FocusResult<Target> {
|
||||
let {
|
||||
isDisabled,
|
||||
onFocus: onFocusProp,
|
||||
onBlur: onBlurProp,
|
||||
onFocusChange
|
||||
} = props;
|
||||
|
||||
const onBlur: FocusProps<Target>['onBlur'] = useCallback((e: FocusEvent<Target>) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
if (onBlurProp) {
|
||||
onBlurProp(e);
|
||||
}
|
||||
|
||||
if (onFocusChange) {
|
||||
onFocusChange(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}, [onBlurProp, onFocusChange]);
|
||||
|
||||
|
||||
const onSyntheticFocus = useSyntheticBlurEvent<Target>(onBlur);
|
||||
|
||||
const onFocus: FocusProps<Target>['onFocus'] = useCallback((e: FocusEvent<Target>) => {
|
||||
// Double check that document.activeElement actually matches e.target in case a previously chained
|
||||
// focus handler already moved focus somewhere else.
|
||||
|
||||
const ownerDocument = getOwnerDocument(e.target);
|
||||
const activeElement = ownerDocument ? getActiveElement(ownerDocument) : getActiveElement();
|
||||
if (e.target === e.currentTarget && activeElement === getEventTarget(e.nativeEvent)) {
|
||||
if (onFocusProp) {
|
||||
onFocusProp(e);
|
||||
}
|
||||
|
||||
if (onFocusChange) {
|
||||
onFocusChange(true);
|
||||
}
|
||||
|
||||
onSyntheticFocus(e);
|
||||
}
|
||||
}, [onFocusChange, onFocusProp, onSyntheticFocus]);
|
||||
|
||||
return {
|
||||
focusProps: {
|
||||
onFocus: (!isDisabled && (onFocusProp || onFocusChange || onBlurProp)) ? onFocus : undefined,
|
||||
onBlur: (!isDisabled && (onBlurProp || onFocusChange)) ? onBlur : undefined
|
||||
}
|
||||
};
|
||||
}
|
||||
341
node_modules/@react-aria/interactions/src/useFocusVisible.ts
generated
vendored
Normal file
341
node_modules/@react-aria/interactions/src/useFocusVisible.ts
generated
vendored
Normal file
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
// Portions of the code in this file are based on code from react.
|
||||
// Original licensing for the following can be found in the
|
||||
// NOTICE file in the root directory of this source tree.
|
||||
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
|
||||
|
||||
import {getOwnerDocument, getOwnerWindow, isMac, isVirtualClick} from '@react-aria/utils';
|
||||
import {ignoreFocusEvent} from './utils';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useIsSSR} from '@react-aria/ssr';
|
||||
|
||||
export type Modality = 'keyboard' | 'pointer' | 'virtual';
|
||||
type HandlerEvent = PointerEvent | MouseEvent | KeyboardEvent | FocusEvent | null;
|
||||
type Handler = (modality: Modality, e: HandlerEvent) => void;
|
||||
export type FocusVisibleHandler = (isFocusVisible: boolean) => void;
|
||||
export interface FocusVisibleProps {
|
||||
/** Whether the element is a text input. */
|
||||
isTextInput?: boolean,
|
||||
/** Whether the element will be auto focused. */
|
||||
autoFocus?: boolean
|
||||
}
|
||||
|
||||
export interface FocusVisibleResult {
|
||||
/** Whether keyboard focus is visible globally. */
|
||||
isFocusVisible: boolean
|
||||
}
|
||||
|
||||
let currentModality: null | Modality = null;
|
||||
let changeHandlers = new Set<Handler>();
|
||||
interface GlobalListenerData {
|
||||
focus: () => void
|
||||
}
|
||||
export let hasSetupGlobalListeners = new Map<Window, GlobalListenerData>(); // We use a map here to support setting event listeners across multiple document objects.
|
||||
let hasEventBeforeFocus = false;
|
||||
let hasBlurredWindowRecently = false;
|
||||
|
||||
// Only Tab or Esc keys will make focus visible on text input elements
|
||||
const FOCUS_VISIBLE_INPUT_KEYS = {
|
||||
Tab: true,
|
||||
Escape: true
|
||||
};
|
||||
|
||||
function triggerChangeHandlers(modality: Modality, e: HandlerEvent) {
|
||||
for (let handler of changeHandlers) {
|
||||
handler(modality, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine if a KeyboardEvent is unmodified and could make keyboard focus styles visible.
|
||||
*/
|
||||
function isValidKey(e: KeyboardEvent) {
|
||||
// Control and Shift keys trigger when navigating back to the tab with keyboard.
|
||||
return !(e.metaKey || (!isMac() && e.altKey) || e.ctrlKey || e.key === 'Control' || e.key === 'Shift' || e.key === 'Meta');
|
||||
}
|
||||
|
||||
|
||||
function handleKeyboardEvent(e: KeyboardEvent) {
|
||||
hasEventBeforeFocus = true;
|
||||
if (isValidKey(e)) {
|
||||
currentModality = 'keyboard';
|
||||
triggerChangeHandlers('keyboard', e);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerEvent(e: PointerEvent | MouseEvent) {
|
||||
currentModality = 'pointer';
|
||||
if (e.type === 'mousedown' || e.type === 'pointerdown') {
|
||||
hasEventBeforeFocus = true;
|
||||
triggerChangeHandlers('pointer', e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickEvent(e: MouseEvent) {
|
||||
if (isVirtualClick(e)) {
|
||||
hasEventBeforeFocus = true;
|
||||
currentModality = 'virtual';
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocusEvent(e: FocusEvent) {
|
||||
// Firefox fires two extra focus events when the user first clicks into an iframe:
|
||||
// first on the window, then on the document. We ignore these events so they don't
|
||||
// cause keyboard focus rings to appear.
|
||||
if (e.target === window || e.target === document || ignoreFocusEvent || !e.isTrusted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If a focus event occurs without a preceding keyboard or pointer event, switch to virtual modality.
|
||||
// This occurs, for example, when navigating a form with the next/previous buttons on iOS.
|
||||
if (!hasEventBeforeFocus && !hasBlurredWindowRecently) {
|
||||
currentModality = 'virtual';
|
||||
triggerChangeHandlers('virtual', e);
|
||||
}
|
||||
|
||||
hasEventBeforeFocus = false;
|
||||
hasBlurredWindowRecently = false;
|
||||
}
|
||||
|
||||
function handleWindowBlur() {
|
||||
if (ignoreFocusEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When the window is blurred, reset state. This is necessary when tabbing out of the window,
|
||||
// for example, since a subsequent focus event won't be fired.
|
||||
hasEventBeforeFocus = false;
|
||||
hasBlurredWindowRecently = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup global event listeners to control when keyboard focus style should be visible.
|
||||
*/
|
||||
function setupGlobalFocusEvents(element?: HTMLElement | null) {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined' || hasSetupGlobalListeners.get(getOwnerWindow(element))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const windowObject = getOwnerWindow(element);
|
||||
const documentObject = getOwnerDocument(element);
|
||||
|
||||
// Programmatic focus() calls shouldn't affect the current input modality.
|
||||
// However, we need to detect other cases when a focus event occurs without
|
||||
// a preceding user event (e.g. screen reader focus). Overriding the focus
|
||||
// method on HTMLElement.prototype is a bit hacky, but works.
|
||||
let focus = windowObject.HTMLElement.prototype.focus;
|
||||
windowObject.HTMLElement.prototype.focus = function () {
|
||||
hasEventBeforeFocus = true;
|
||||
focus.apply(this, arguments as unknown as [options?: FocusOptions | undefined]);
|
||||
};
|
||||
|
||||
documentObject.addEventListener('keydown', handleKeyboardEvent, true);
|
||||
documentObject.addEventListener('keyup', handleKeyboardEvent, true);
|
||||
documentObject.addEventListener('click', handleClickEvent, true);
|
||||
|
||||
// Register focus events on the window so they are sure to happen
|
||||
// before React's event listeners (registered on the document).
|
||||
windowObject.addEventListener('focus', handleFocusEvent, true);
|
||||
windowObject.addEventListener('blur', handleWindowBlur, false);
|
||||
|
||||
if (typeof PointerEvent !== 'undefined') {
|
||||
documentObject.addEventListener('pointerdown', handlePointerEvent, true);
|
||||
documentObject.addEventListener('pointermove', handlePointerEvent, true);
|
||||
documentObject.addEventListener('pointerup', handlePointerEvent, true);
|
||||
} else if (process.env.NODE_ENV === 'test') {
|
||||
documentObject.addEventListener('mousedown', handlePointerEvent, true);
|
||||
documentObject.addEventListener('mousemove', handlePointerEvent, true);
|
||||
documentObject.addEventListener('mouseup', handlePointerEvent, true);
|
||||
}
|
||||
|
||||
// Add unmount handler
|
||||
windowObject.addEventListener('beforeunload', () => {
|
||||
tearDownWindowFocusTracking(element);
|
||||
}, {once: true});
|
||||
|
||||
hasSetupGlobalListeners.set(windowObject, {focus});
|
||||
}
|
||||
|
||||
const tearDownWindowFocusTracking = (element, loadListener?: () => void) => {
|
||||
const windowObject = getOwnerWindow(element);
|
||||
const documentObject = getOwnerDocument(element);
|
||||
if (loadListener) {
|
||||
documentObject.removeEventListener('DOMContentLoaded', loadListener);
|
||||
}
|
||||
if (!hasSetupGlobalListeners.has(windowObject)) {
|
||||
return;
|
||||
}
|
||||
windowObject.HTMLElement.prototype.focus = hasSetupGlobalListeners.get(windowObject)!.focus;
|
||||
|
||||
documentObject.removeEventListener('keydown', handleKeyboardEvent, true);
|
||||
documentObject.removeEventListener('keyup', handleKeyboardEvent, true);
|
||||
documentObject.removeEventListener('click', handleClickEvent, true);
|
||||
|
||||
windowObject.removeEventListener('focus', handleFocusEvent, true);
|
||||
windowObject.removeEventListener('blur', handleWindowBlur, false);
|
||||
|
||||
if (typeof PointerEvent !== 'undefined') {
|
||||
documentObject.removeEventListener('pointerdown', handlePointerEvent, true);
|
||||
documentObject.removeEventListener('pointermove', handlePointerEvent, true);
|
||||
documentObject.removeEventListener('pointerup', handlePointerEvent, true);
|
||||
} else if (process.env.NODE_ENV === 'test') {
|
||||
documentObject.removeEventListener('mousedown', handlePointerEvent, true);
|
||||
documentObject.removeEventListener('mousemove', handlePointerEvent, true);
|
||||
documentObject.removeEventListener('mouseup', handlePointerEvent, true);
|
||||
}
|
||||
|
||||
hasSetupGlobalListeners.delete(windowObject);
|
||||
};
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL
|
||||
* Adds a window (i.e. iframe) to the list of windows that are being tracked for focus visible.
|
||||
*
|
||||
* Sometimes apps render portions of their tree into an iframe. In this case, we cannot accurately track if the focus
|
||||
* is visible because we cannot see interactions inside the iframe. If you have this in your application's architecture,
|
||||
* then this function will attach event listeners inside the iframe. You should call `addWindowFocusTracking` with an
|
||||
* element from inside the window you wish to add. We'll retrieve the relevant elements based on that.
|
||||
* Note, you do not need to call this for the default window, as we call it for you.
|
||||
*
|
||||
* When you are ready to stop listening, but you do not wish to unmount the iframe, you may call the cleanup function
|
||||
* returned by `addWindowFocusTracking`. Otherwise, when you unmount the iframe, all listeners and state will be cleaned
|
||||
* up automatically for you.
|
||||
*
|
||||
* @param element @default document.body - The element provided will be used to get the window to add.
|
||||
* @returns A function to remove the event listeners and cleanup the state.
|
||||
*/
|
||||
export function addWindowFocusTracking(element?: HTMLElement | null): () => void {
|
||||
const documentObject = getOwnerDocument(element);
|
||||
let loadListener;
|
||||
if (documentObject.readyState !== 'loading') {
|
||||
setupGlobalFocusEvents(element);
|
||||
} else {
|
||||
loadListener = () => {
|
||||
setupGlobalFocusEvents(element);
|
||||
};
|
||||
documentObject.addEventListener('DOMContentLoaded', loadListener);
|
||||
}
|
||||
|
||||
return () => tearDownWindowFocusTracking(element, loadListener);
|
||||
}
|
||||
|
||||
// Server-side rendering does not have the document object defined
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
if (typeof document !== 'undefined') {
|
||||
addWindowFocusTracking();
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, keyboard focus is visible.
|
||||
*/
|
||||
export function isFocusVisible(): boolean {
|
||||
return currentModality !== 'pointer';
|
||||
}
|
||||
|
||||
export function getInteractionModality(): Modality | null {
|
||||
return currentModality;
|
||||
}
|
||||
|
||||
export function setInteractionModality(modality: Modality): void {
|
||||
currentModality = modality;
|
||||
triggerChangeHandlers(modality, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps state of the current modality.
|
||||
*/
|
||||
export function useInteractionModality(): Modality | null {
|
||||
setupGlobalFocusEvents();
|
||||
|
||||
let [modality, setModality] = useState(currentModality);
|
||||
useEffect(() => {
|
||||
let handler = () => {
|
||||
setModality(currentModality);
|
||||
};
|
||||
|
||||
changeHandlers.add(handler);
|
||||
return () => {
|
||||
changeHandlers.delete(handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useIsSSR() ? null : modality;
|
||||
}
|
||||
|
||||
const nonTextInputTypes = new Set([
|
||||
'checkbox',
|
||||
'radio',
|
||||
'range',
|
||||
'color',
|
||||
'file',
|
||||
'image',
|
||||
'button',
|
||||
'submit',
|
||||
'reset'
|
||||
]);
|
||||
|
||||
/**
|
||||
* If this is attached to text input component, return if the event is a focus event (Tab/Escape keys pressed) so that
|
||||
* focus visible style can be properly set.
|
||||
*/
|
||||
function isKeyboardFocusEvent(isTextInput: boolean, modality: Modality, e: HandlerEvent) {
|
||||
let document = getOwnerDocument(e?.target as Element);
|
||||
const IHTMLInputElement = typeof window !== 'undefined' ? getOwnerWindow(e?.target as Element).HTMLInputElement : HTMLInputElement;
|
||||
const IHTMLTextAreaElement = typeof window !== 'undefined' ? getOwnerWindow(e?.target as Element).HTMLTextAreaElement : HTMLTextAreaElement;
|
||||
const IHTMLElement = typeof window !== 'undefined' ? getOwnerWindow(e?.target as Element).HTMLElement : HTMLElement;
|
||||
const IKeyboardEvent = typeof window !== 'undefined' ? getOwnerWindow(e?.target as Element).KeyboardEvent : KeyboardEvent;
|
||||
|
||||
// For keyboard events that occur on a non-input element that will move focus into input element (aka ArrowLeft going from Datepicker button to the main input group)
|
||||
// we need to rely on the user passing isTextInput into here. This way we can skip toggling focus visiblity for said input element
|
||||
isTextInput = isTextInput ||
|
||||
(document.activeElement instanceof IHTMLInputElement && !nonTextInputTypes.has(document.activeElement.type)) ||
|
||||
document.activeElement instanceof IHTMLTextAreaElement ||
|
||||
(document.activeElement instanceof IHTMLElement && document.activeElement.isContentEditable);
|
||||
return !(isTextInput && modality === 'keyboard' && e instanceof IKeyboardEvent && !FOCUS_VISIBLE_INPUT_KEYS[e.key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages focus visible state for the page, and subscribes individual components for updates.
|
||||
*/
|
||||
export function useFocusVisible(props: FocusVisibleProps = {}): FocusVisibleResult {
|
||||
let {isTextInput, autoFocus} = props;
|
||||
let [isFocusVisibleState, setFocusVisible] = useState(autoFocus || isFocusVisible());
|
||||
useFocusVisibleListener((isFocusVisible) => {
|
||||
setFocusVisible(isFocusVisible);
|
||||
}, [isTextInput], {isTextInput});
|
||||
|
||||
return {isFocusVisible: isFocusVisibleState};
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for trigger change and reports if focus is visible (i.e., modality is not pointer).
|
||||
*/
|
||||
export function useFocusVisibleListener(fn: FocusVisibleHandler, deps: ReadonlyArray<any>, opts?: {isTextInput?: boolean}): void {
|
||||
setupGlobalFocusEvents();
|
||||
|
||||
useEffect(() => {
|
||||
let handler = (modality: Modality, e: HandlerEvent) => {
|
||||
// We want to early return for any keyboard events that occur inside text inputs EXCEPT for Tab and Escape
|
||||
if (!isKeyboardFocusEvent(!!(opts?.isTextInput), modality, e)) {
|
||||
return;
|
||||
}
|
||||
fn(isFocusVisible());
|
||||
};
|
||||
changeHandlers.add(handler);
|
||||
return () => {
|
||||
changeHandlers.delete(handler);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
}
|
||||
132
node_modules/@react-aria/interactions/src/useFocusWithin.ts
generated
vendored
Normal file
132
node_modules/@react-aria/interactions/src/useFocusWithin.ts
generated
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
// Portions of the code in this file are based on code from react.
|
||||
// Original licensing for the following can be found in the
|
||||
// NOTICE file in the root directory of this source tree.
|
||||
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
|
||||
|
||||
import {createSyntheticEvent, setEventTarget, useSyntheticBlurEvent} from './utils';
|
||||
import {DOMAttributes} from '@react-types/shared';
|
||||
import {FocusEvent, useCallback, useRef} from 'react';
|
||||
import {getActiveElement, getEventTarget, getOwnerDocument, nodeContains, useGlobalListeners} from '@react-aria/utils';
|
||||
|
||||
export interface FocusWithinProps {
|
||||
/** Whether the focus within events should be disabled. */
|
||||
isDisabled?: boolean,
|
||||
/** Handler that is called when the target element or a descendant receives focus. */
|
||||
onFocusWithin?: (e: FocusEvent) => void,
|
||||
/** Handler that is called when the target element and all descendants lose focus. */
|
||||
onBlurWithin?: (e: FocusEvent) => void,
|
||||
/** Handler that is called when the the focus within state changes. */
|
||||
onFocusWithinChange?: (isFocusWithin: boolean) => void
|
||||
}
|
||||
|
||||
export interface FocusWithinResult {
|
||||
/** Props to spread onto the target element. */
|
||||
focusWithinProps: DOMAttributes
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles focus events for the target and its descendants.
|
||||
*/
|
||||
export function useFocusWithin(props: FocusWithinProps): FocusWithinResult {
|
||||
let {
|
||||
isDisabled,
|
||||
onBlurWithin,
|
||||
onFocusWithin,
|
||||
onFocusWithinChange
|
||||
} = props;
|
||||
let state = useRef({
|
||||
isFocusWithin: false
|
||||
});
|
||||
|
||||
let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();
|
||||
|
||||
let onBlur = useCallback((e: FocusEvent) => {
|
||||
// Ignore events bubbling through portals.
|
||||
if (!e.currentTarget.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't want to trigger onBlurWithin and then immediately onFocusWithin again
|
||||
// when moving focus inside the element. Only trigger if the currentTarget doesn't
|
||||
// include the relatedTarget (where focus is moving).
|
||||
if (state.current.isFocusWithin && !(e.currentTarget as Element).contains(e.relatedTarget as Element)) {
|
||||
state.current.isFocusWithin = false;
|
||||
removeAllGlobalListeners();
|
||||
|
||||
if (onBlurWithin) {
|
||||
onBlurWithin(e);
|
||||
}
|
||||
|
||||
if (onFocusWithinChange) {
|
||||
onFocusWithinChange(false);
|
||||
}
|
||||
}
|
||||
}, [onBlurWithin, onFocusWithinChange, state, removeAllGlobalListeners]);
|
||||
|
||||
let onSyntheticFocus = useSyntheticBlurEvent(onBlur);
|
||||
let onFocus = useCallback((e: FocusEvent) => {
|
||||
// Ignore events bubbling through portals.
|
||||
if (!e.currentTarget.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Double check that document.activeElement actually matches e.target in case a previously chained
|
||||
// focus handler already moved focus somewhere else.
|
||||
const ownerDocument = getOwnerDocument(e.target);
|
||||
const activeElement = getActiveElement(ownerDocument);
|
||||
if (!state.current.isFocusWithin && activeElement === getEventTarget(e.nativeEvent)) {
|
||||
if (onFocusWithin) {
|
||||
onFocusWithin(e);
|
||||
}
|
||||
|
||||
if (onFocusWithinChange) {
|
||||
onFocusWithinChange(true);
|
||||
}
|
||||
|
||||
state.current.isFocusWithin = true;
|
||||
onSyntheticFocus(e);
|
||||
|
||||
// Browsers don't fire blur events when elements are removed from the DOM.
|
||||
// However, if a focus event occurs outside the element we're tracking, we
|
||||
// can manually fire onBlur.
|
||||
let currentTarget = e.currentTarget;
|
||||
addGlobalListener(ownerDocument, 'focus', e => {
|
||||
if (state.current.isFocusWithin && !nodeContains(currentTarget, e.target as Element)) {
|
||||
let nativeEvent = new ownerDocument.defaultView!.FocusEvent('blur', {relatedTarget: e.target});
|
||||
setEventTarget(nativeEvent, currentTarget);
|
||||
let event = createSyntheticEvent<FocusEvent>(nativeEvent);
|
||||
onBlur(event);
|
||||
}
|
||||
}, {capture: true});
|
||||
}
|
||||
}, [onFocusWithin, onFocusWithinChange, onSyntheticFocus, addGlobalListener, onBlur]);
|
||||
|
||||
if (isDisabled) {
|
||||
return {
|
||||
focusWithinProps: {
|
||||
// These cannot be null, that would conflict in mergeProps
|
||||
onFocus: undefined,
|
||||
onBlur: undefined
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
focusWithinProps: {
|
||||
onFocus,
|
||||
onBlur
|
||||
}
|
||||
};
|
||||
}
|
||||
187
node_modules/@react-aria/interactions/src/useFocusable.tsx
generated
vendored
Normal file
187
node_modules/@react-aria/interactions/src/useFocusable.tsx
generated
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {DOMAttributes, FocusableDOMProps, FocusableElement, FocusableProps, RefObject} from '@react-types/shared';
|
||||
import {focusSafely} from './';
|
||||
import {getOwnerWindow, isFocusable, mergeProps, mergeRefs, useObjectRef, useSyncRef} from '@react-aria/utils';
|
||||
import React, {ForwardedRef, forwardRef, MutableRefObject, ReactElement, ReactNode, useContext, useEffect, useRef} from 'react';
|
||||
import {useFocus} from './useFocus';
|
||||
import {useKeyboard} from './useKeyboard';
|
||||
|
||||
export interface FocusableOptions<T = FocusableElement> extends FocusableProps<T>, FocusableDOMProps {
|
||||
/** Whether focus should be disabled. */
|
||||
isDisabled?: boolean
|
||||
}
|
||||
|
||||
export interface FocusableProviderProps extends DOMAttributes {
|
||||
/** The child element to provide DOM props to. */
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
interface FocusableContextValue extends FocusableProviderProps {
|
||||
ref?: MutableRefObject<FocusableElement | null>
|
||||
}
|
||||
|
||||
// Exported for @react-aria/collections, which forwards this context.
|
||||
/** @private */
|
||||
export let FocusableContext = React.createContext<FocusableContextValue | null>(null);
|
||||
|
||||
function useFocusableContext(ref: RefObject<FocusableElement | null>): FocusableContextValue {
|
||||
let context = useContext(FocusableContext) || {};
|
||||
useSyncRef(context, ref);
|
||||
|
||||
// eslint-disable-next-line
|
||||
let {ref: _, ...otherProps} = context;
|
||||
return otherProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides DOM props to the nearest focusable child.
|
||||
*/
|
||||
export const FocusableProvider = React.forwardRef(function FocusableProvider(props: FocusableProviderProps, ref: ForwardedRef<FocusableElement>) {
|
||||
let {children, ...otherProps} = props;
|
||||
let objRef = useObjectRef(ref);
|
||||
let context = {
|
||||
...otherProps,
|
||||
ref: objRef
|
||||
};
|
||||
|
||||
return (
|
||||
<FocusableContext.Provider value={context}>
|
||||
{children}
|
||||
</FocusableContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
export interface FocusableAria {
|
||||
/** Props for the focusable element. */
|
||||
focusableProps: DOMAttributes
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to make an element focusable and capable of auto focus.
|
||||
*/
|
||||
export function useFocusable<T extends FocusableElement = FocusableElement>(props: FocusableOptions<T>, domRef: RefObject<FocusableElement | null>): FocusableAria {
|
||||
let {focusProps} = useFocus(props);
|
||||
let {keyboardProps} = useKeyboard(props);
|
||||
let interactions = mergeProps(focusProps, keyboardProps);
|
||||
let domProps = useFocusableContext(domRef);
|
||||
let interactionProps = props.isDisabled ? {} : domProps;
|
||||
let autoFocusRef = useRef(props.autoFocus);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocusRef.current && domRef.current) {
|
||||
focusSafely(domRef.current);
|
||||
}
|
||||
autoFocusRef.current = false;
|
||||
}, [domRef]);
|
||||
|
||||
// Always set a tabIndex so that Safari allows focusing native buttons and inputs.
|
||||
let tabIndex: number | undefined = props.excludeFromTabOrder ? -1 : 0;
|
||||
if (props.isDisabled) {
|
||||
tabIndex = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
focusableProps: mergeProps(
|
||||
{
|
||||
...interactions,
|
||||
tabIndex
|
||||
},
|
||||
interactionProps
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
export interface FocusableComponentProps extends FocusableOptions {
|
||||
children: ReactElement<DOMAttributes, string>
|
||||
}
|
||||
|
||||
export const Focusable = forwardRef(({children, ...props}: FocusableComponentProps, ref: ForwardedRef<FocusableElement>) => {
|
||||
ref = useObjectRef(ref);
|
||||
let {focusableProps} = useFocusable(props, ref);
|
||||
let child = React.Children.only(children);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return;
|
||||
}
|
||||
|
||||
let el = ref.current;
|
||||
if (!el || !(el instanceof getOwnerWindow(el).Element)) {
|
||||
console.error('<Focusable> child must forward its ref to a DOM element.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!props.isDisabled && !isFocusable(el)) {
|
||||
console.warn('<Focusable> child must be focusable. Please ensure the tabIndex prop is passed through.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
el.localName !== 'button' &&
|
||||
el.localName !== 'input' &&
|
||||
el.localName !== 'select' &&
|
||||
el.localName !== 'textarea' &&
|
||||
el.localName !== 'a' &&
|
||||
el.localName !== 'area' &&
|
||||
el.localName !== 'summary' &&
|
||||
el.localName !== 'img' &&
|
||||
el.localName !== 'svg'
|
||||
) {
|
||||
let role = el.getAttribute('role');
|
||||
if (!role) {
|
||||
console.warn('<Focusable> child must have an interactive ARIA role.');
|
||||
} else if (
|
||||
// https://w3c.github.io/aria/#widget_roles
|
||||
role !== 'application' &&
|
||||
role !== 'button' &&
|
||||
role !== 'checkbox' &&
|
||||
role !== 'combobox' &&
|
||||
role !== 'gridcell' &&
|
||||
role !== 'link' &&
|
||||
role !== 'menuitem' &&
|
||||
role !== 'menuitemcheckbox' &&
|
||||
role !== 'menuitemradio' &&
|
||||
role !== 'option' &&
|
||||
role !== 'radio' &&
|
||||
role !== 'searchbox' &&
|
||||
role !== 'separator' &&
|
||||
role !== 'slider' &&
|
||||
role !== 'spinbutton' &&
|
||||
role !== 'switch' &&
|
||||
role !== 'tab' &&
|
||||
role !== 'tabpanel' &&
|
||||
role !== 'textbox' &&
|
||||
role !== 'treeitem' &&
|
||||
// aria-describedby is also announced on these roles
|
||||
role !== 'img' &&
|
||||
role !== 'meter' &&
|
||||
role !== 'progressbar'
|
||||
) {
|
||||
console.warn(`<Focusable> child must have an interactive ARIA role. Got "${role}".`);
|
||||
}
|
||||
}
|
||||
}, [ref, props.isDisabled]);
|
||||
|
||||
// @ts-ignore
|
||||
let childRef = parseInt(React.version, 10) < 19 ? child.ref : child.props.ref;
|
||||
|
||||
return React.cloneElement(
|
||||
child,
|
||||
{
|
||||
...mergeProps(focusableProps, child.props),
|
||||
// @ts-ignore
|
||||
ref: mergeRefs(childRef, ref)
|
||||
}
|
||||
);
|
||||
});
|
||||
220
node_modules/@react-aria/interactions/src/useHover.ts
generated
vendored
Normal file
220
node_modules/@react-aria/interactions/src/useHover.ts
generated
vendored
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
// Portions of the code in this file are based on code from react.
|
||||
// Original licensing for the following can be found in the
|
||||
// NOTICE file in the root directory of this source tree.
|
||||
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
|
||||
|
||||
import {DOMAttributes, HoverEvents} from '@react-types/shared';
|
||||
import {getOwnerDocument, nodeContains, useGlobalListeners} from '@react-aria/utils';
|
||||
import {useEffect, useMemo, useRef, useState} from 'react';
|
||||
|
||||
export interface HoverProps extends HoverEvents {
|
||||
/** Whether the hover events should be disabled. */
|
||||
isDisabled?: boolean
|
||||
}
|
||||
|
||||
export interface HoverResult {
|
||||
/** Props to spread on the target element. */
|
||||
hoverProps: DOMAttributes,
|
||||
isHovered: boolean
|
||||
}
|
||||
|
||||
// iOS fires onPointerEnter twice: once with pointerType="touch" and again with pointerType="mouse".
|
||||
// We want to ignore these emulated events so they do not trigger hover behavior.
|
||||
// See https://bugs.webkit.org/show_bug.cgi?id=214609.
|
||||
let globalIgnoreEmulatedMouseEvents = false;
|
||||
let hoverCount = 0;
|
||||
|
||||
function setGlobalIgnoreEmulatedMouseEvents() {
|
||||
globalIgnoreEmulatedMouseEvents = true;
|
||||
|
||||
// Clear globalIgnoreEmulatedMouseEvents after a short timeout. iOS fires onPointerEnter
|
||||
// with pointerType="mouse" immediately after onPointerUp and before onFocus. On other
|
||||
// devices that don't have this quirk, we don't want to ignore a mouse hover sometime in
|
||||
// the distant future because a user previously touched the element.
|
||||
setTimeout(() => {
|
||||
globalIgnoreEmulatedMouseEvents = false;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function handleGlobalPointerEvent(e) {
|
||||
if (e.pointerType === 'touch') {
|
||||
setGlobalIgnoreEmulatedMouseEvents();
|
||||
}
|
||||
}
|
||||
|
||||
function setupGlobalTouchEvents() {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof PointerEvent !== 'undefined') {
|
||||
document.addEventListener('pointerup', handleGlobalPointerEvent);
|
||||
} else if (process.env.NODE_ENV === 'test') {
|
||||
document.addEventListener('touchend', setGlobalIgnoreEmulatedMouseEvents);
|
||||
}
|
||||
|
||||
hoverCount++;
|
||||
return () => {
|
||||
hoverCount--;
|
||||
if (hoverCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof PointerEvent !== 'undefined') {
|
||||
document.removeEventListener('pointerup', handleGlobalPointerEvent);
|
||||
} else if (process.env.NODE_ENV === 'test') {
|
||||
document.removeEventListener('touchend', setGlobalIgnoreEmulatedMouseEvents);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles pointer hover interactions for an element. Normalizes behavior
|
||||
* across browsers and platforms, and ignores emulated mouse events on touch devices.
|
||||
*/
|
||||
export function useHover(props: HoverProps): HoverResult {
|
||||
let {
|
||||
onHoverStart,
|
||||
onHoverChange,
|
||||
onHoverEnd,
|
||||
isDisabled
|
||||
} = props;
|
||||
|
||||
let [isHovered, setHovered] = useState(false);
|
||||
let state = useRef({
|
||||
isHovered: false,
|
||||
ignoreEmulatedMouseEvents: false,
|
||||
pointerType: '',
|
||||
target: null
|
||||
}).current;
|
||||
|
||||
useEffect(setupGlobalTouchEvents, []);
|
||||
let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();
|
||||
|
||||
let {hoverProps, triggerHoverEnd} = useMemo(() => {
|
||||
let triggerHoverStart = (event, pointerType) => {
|
||||
state.pointerType = pointerType;
|
||||
if (isDisabled || pointerType === 'touch' || state.isHovered || !event.currentTarget.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.isHovered = true;
|
||||
let target = event.currentTarget;
|
||||
state.target = target;
|
||||
|
||||
// When an element that is hovered over is removed, no pointerleave event is fired by the browser,
|
||||
// even though the originally hovered target may have shrunk in size so it is no longer hovered.
|
||||
// However, a pointerover event will be fired on the new target the mouse is over.
|
||||
// In Chrome this happens immediately. In Safari and Firefox, it happens upon moving the mouse one pixel.
|
||||
addGlobalListener(getOwnerDocument(event.target), 'pointerover', e => {
|
||||
if (state.isHovered && state.target && !nodeContains(state.target, e.target as Element)) {
|
||||
triggerHoverEnd(e, e.pointerType);
|
||||
}
|
||||
}, {capture: true});
|
||||
|
||||
if (onHoverStart) {
|
||||
onHoverStart({
|
||||
type: 'hoverstart',
|
||||
target,
|
||||
pointerType
|
||||
});
|
||||
}
|
||||
|
||||
if (onHoverChange) {
|
||||
onHoverChange(true);
|
||||
}
|
||||
|
||||
setHovered(true);
|
||||
};
|
||||
|
||||
let triggerHoverEnd = (event, pointerType) => {
|
||||
let target = state.target;
|
||||
state.pointerType = '';
|
||||
state.target = null;
|
||||
|
||||
if (pointerType === 'touch' || !state.isHovered || !target) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.isHovered = false;
|
||||
removeAllGlobalListeners();
|
||||
|
||||
if (onHoverEnd) {
|
||||
onHoverEnd({
|
||||
type: 'hoverend',
|
||||
target,
|
||||
pointerType
|
||||
});
|
||||
}
|
||||
|
||||
if (onHoverChange) {
|
||||
onHoverChange(false);
|
||||
}
|
||||
|
||||
setHovered(false);
|
||||
};
|
||||
|
||||
let hoverProps: DOMAttributes = {};
|
||||
|
||||
if (typeof PointerEvent !== 'undefined') {
|
||||
hoverProps.onPointerEnter = (e) => {
|
||||
if (globalIgnoreEmulatedMouseEvents && e.pointerType === 'mouse') {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerHoverStart(e, e.pointerType);
|
||||
};
|
||||
|
||||
hoverProps.onPointerLeave = (e) => {
|
||||
if (!isDisabled && e.currentTarget.contains(e.target as Element)) {
|
||||
triggerHoverEnd(e, e.pointerType);
|
||||
}
|
||||
};
|
||||
} else if (process.env.NODE_ENV === 'test') {
|
||||
hoverProps.onTouchStart = () => {
|
||||
state.ignoreEmulatedMouseEvents = true;
|
||||
};
|
||||
|
||||
hoverProps.onMouseEnter = (e) => {
|
||||
if (!state.ignoreEmulatedMouseEvents && !globalIgnoreEmulatedMouseEvents) {
|
||||
triggerHoverStart(e, 'mouse');
|
||||
}
|
||||
|
||||
state.ignoreEmulatedMouseEvents = false;
|
||||
};
|
||||
|
||||
hoverProps.onMouseLeave = (e) => {
|
||||
if (!isDisabled && e.currentTarget.contains(e.target as Element)) {
|
||||
triggerHoverEnd(e, 'mouse');
|
||||
}
|
||||
};
|
||||
}
|
||||
return {hoverProps, triggerHoverEnd};
|
||||
}, [onHoverStart, onHoverChange, onHoverEnd, isDisabled, state, addGlobalListener, removeAllGlobalListeners]);
|
||||
|
||||
useEffect(() => {
|
||||
// Call the triggerHoverEnd as soon as isDisabled changes to true
|
||||
// Safe to call triggerHoverEnd, it will early return if we aren't currently hovering
|
||||
if (isDisabled) {
|
||||
triggerHoverEnd({currentTarget: state.target}, state.pointerType);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isDisabled]);
|
||||
|
||||
return {
|
||||
hoverProps,
|
||||
isHovered
|
||||
};
|
||||
}
|
||||
140
node_modules/@react-aria/interactions/src/useInteractOutside.ts
generated
vendored
Normal file
140
node_modules/@react-aria/interactions/src/useInteractOutside.ts
generated
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
// Portions of the code in this file are based on code from react.
|
||||
// Original licensing for the following can be found in the
|
||||
// NOTICE file in the root directory of this source tree.
|
||||
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
|
||||
|
||||
import {getOwnerDocument, useEffectEvent} from '@react-aria/utils';
|
||||
import {RefObject} from '@react-types/shared';
|
||||
import {useEffect, useRef} from 'react';
|
||||
|
||||
export interface InteractOutsideProps {
|
||||
ref: RefObject<Element | null>,
|
||||
onInteractOutside?: (e: PointerEvent) => void,
|
||||
onInteractOutsideStart?: (e: PointerEvent) => void,
|
||||
/** Whether the interact outside events should be disabled. */
|
||||
isDisabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Example, used in components like Dialogs and Popovers so they can close
|
||||
* when a user clicks outside them.
|
||||
*/
|
||||
export function useInteractOutside(props: InteractOutsideProps): void {
|
||||
let {ref, onInteractOutside, isDisabled, onInteractOutsideStart} = props;
|
||||
let stateRef = useRef({
|
||||
isPointerDown: false,
|
||||
ignoreEmulatedMouseEvents: false
|
||||
});
|
||||
|
||||
let onPointerDown = useEffectEvent((e) => {
|
||||
if (onInteractOutside && isValidEvent(e, ref)) {
|
||||
if (onInteractOutsideStart) {
|
||||
onInteractOutsideStart(e);
|
||||
}
|
||||
stateRef.current.isPointerDown = true;
|
||||
}
|
||||
});
|
||||
|
||||
let triggerInteractOutside = useEffectEvent((e: PointerEvent) => {
|
||||
if (onInteractOutside) {
|
||||
onInteractOutside(e);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let state = stateRef.current;
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = ref.current;
|
||||
const documentObject = getOwnerDocument(element);
|
||||
|
||||
// Use pointer events if available. Otherwise, fall back to mouse and touch events.
|
||||
if (typeof PointerEvent !== 'undefined') {
|
||||
let onPointerUp = (e) => {
|
||||
if (state.isPointerDown && isValidEvent(e, ref)) {
|
||||
triggerInteractOutside(e);
|
||||
}
|
||||
state.isPointerDown = false;
|
||||
};
|
||||
|
||||
// changing these to capture phase fixed combobox
|
||||
documentObject.addEventListener('pointerdown', onPointerDown, true);
|
||||
documentObject.addEventListener('pointerup', onPointerUp, true);
|
||||
|
||||
return () => {
|
||||
documentObject.removeEventListener('pointerdown', onPointerDown, true);
|
||||
documentObject.removeEventListener('pointerup', onPointerUp, true);
|
||||
};
|
||||
} else if (process.env.NODE_ENV === 'test') {
|
||||
let onMouseUp = (e) => {
|
||||
if (state.ignoreEmulatedMouseEvents) {
|
||||
state.ignoreEmulatedMouseEvents = false;
|
||||
} else if (state.isPointerDown && isValidEvent(e, ref)) {
|
||||
triggerInteractOutside(e);
|
||||
}
|
||||
state.isPointerDown = false;
|
||||
};
|
||||
|
||||
let onTouchEnd = (e) => {
|
||||
state.ignoreEmulatedMouseEvents = true;
|
||||
if (state.isPointerDown && isValidEvent(e, ref)) {
|
||||
triggerInteractOutside(e);
|
||||
}
|
||||
state.isPointerDown = false;
|
||||
};
|
||||
|
||||
documentObject.addEventListener('mousedown', onPointerDown, true);
|
||||
documentObject.addEventListener('mouseup', onMouseUp, true);
|
||||
documentObject.addEventListener('touchstart', onPointerDown, true);
|
||||
documentObject.addEventListener('touchend', onTouchEnd, true);
|
||||
|
||||
return () => {
|
||||
documentObject.removeEventListener('mousedown', onPointerDown, true);
|
||||
documentObject.removeEventListener('mouseup', onMouseUp, true);
|
||||
documentObject.removeEventListener('touchstart', onPointerDown, true);
|
||||
documentObject.removeEventListener('touchend', onTouchEnd, true);
|
||||
};
|
||||
}
|
||||
}, [ref, isDisabled, onPointerDown, triggerInteractOutside]);
|
||||
}
|
||||
|
||||
function isValidEvent(event, ref) {
|
||||
if (event.button > 0) {
|
||||
return false;
|
||||
}
|
||||
if (event.target) {
|
||||
// if the event target is no longer in the document, ignore
|
||||
const ownerDocument = event.target.ownerDocument;
|
||||
if (!ownerDocument || !ownerDocument.documentElement.contains(event.target)) {
|
||||
return false;
|
||||
}
|
||||
// If the target is within a top layer element (e.g. toasts), ignore.
|
||||
if (event.target.closest('[data-react-aria-top-layer]')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ref.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// When the event source is inside a Shadow DOM, event.target is just the shadow root.
|
||||
// Using event.composedPath instead means we can get the actual element inside the shadow root.
|
||||
// This only works if the shadow root is open, there is no way to detect if it is closed.
|
||||
// If the event composed path contains the ref, interaction is inside.
|
||||
return !event.composedPath().includes(ref.current);
|
||||
}
|
||||
36
node_modules/@react-aria/interactions/src/useKeyboard.ts
generated
vendored
Normal file
36
node_modules/@react-aria/interactions/src/useKeyboard.ts
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {createEventHandler} from './createEventHandler';
|
||||
import {DOMAttributes, KeyboardEvents} from '@react-types/shared';
|
||||
|
||||
export interface KeyboardProps extends KeyboardEvents {
|
||||
/** Whether the keyboard events should be disabled. */
|
||||
isDisabled?: boolean
|
||||
}
|
||||
|
||||
export interface KeyboardResult {
|
||||
/** Props to spread onto the target element. */
|
||||
keyboardProps: DOMAttributes
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles keyboard interactions for a focusable element.
|
||||
*/
|
||||
export function useKeyboard(props: KeyboardProps): KeyboardResult {
|
||||
return {
|
||||
keyboardProps: props.isDisabled ? {} : {
|
||||
onKeyDown: createEventHandler(props.onKeyDown),
|
||||
onKeyUp: createEventHandler(props.onKeyUp)
|
||||
}
|
||||
};
|
||||
}
|
||||
135
node_modules/@react-aria/interactions/src/useLongPress.ts
generated
vendored
Normal file
135
node_modules/@react-aria/interactions/src/useLongPress.ts
generated
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {DOMAttributes, FocusableElement, LongPressEvent} from '@react-types/shared';
|
||||
import {focusWithoutScrolling, getOwnerDocument, mergeProps, useDescription, useGlobalListeners} from '@react-aria/utils';
|
||||
import {usePress} from './usePress';
|
||||
import {useRef} from 'react';
|
||||
|
||||
export interface LongPressProps {
|
||||
/** Whether long press events should be disabled. */
|
||||
isDisabled?: boolean,
|
||||
/** Handler that is called when a long press interaction starts. */
|
||||
onLongPressStart?: (e: LongPressEvent) => void,
|
||||
/**
|
||||
* Handler that is called when a long press interaction ends, either
|
||||
* over the target or when the pointer leaves the target.
|
||||
*/
|
||||
onLongPressEnd?: (e: LongPressEvent) => void,
|
||||
/**
|
||||
* Handler that is called when the threshold time is met while
|
||||
* the press is over the target.
|
||||
*/
|
||||
onLongPress?: (e: LongPressEvent) => void,
|
||||
/**
|
||||
* The amount of time in milliseconds to wait before triggering a long press.
|
||||
* @default 500ms
|
||||
*/
|
||||
threshold?: number,
|
||||
/**
|
||||
* A description for assistive techology users indicating that a long press
|
||||
* action is available, e.g. "Long press to open menu".
|
||||
*/
|
||||
accessibilityDescription?: string
|
||||
}
|
||||
|
||||
export interface LongPressResult {
|
||||
/** Props to spread on the target element. */
|
||||
longPressProps: DOMAttributes
|
||||
}
|
||||
|
||||
const DEFAULT_THRESHOLD = 500;
|
||||
|
||||
/**
|
||||
* Handles long press interactions across mouse and touch devices. Supports a customizable time threshold,
|
||||
* accessibility description, and normalizes behavior across browsers and devices.
|
||||
*/
|
||||
export function useLongPress(props: LongPressProps): LongPressResult {
|
||||
let {
|
||||
isDisabled,
|
||||
onLongPressStart,
|
||||
onLongPressEnd,
|
||||
onLongPress,
|
||||
threshold = DEFAULT_THRESHOLD,
|
||||
accessibilityDescription
|
||||
} = props;
|
||||
|
||||
const timeRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
let {addGlobalListener, removeGlobalListener} = useGlobalListeners();
|
||||
|
||||
let {pressProps} = usePress({
|
||||
isDisabled,
|
||||
onPressStart(e) {
|
||||
e.continuePropagation();
|
||||
if (e.pointerType === 'mouse' || e.pointerType === 'touch') {
|
||||
if (onLongPressStart) {
|
||||
onLongPressStart({
|
||||
...e,
|
||||
type: 'longpressstart'
|
||||
});
|
||||
}
|
||||
|
||||
timeRef.current = setTimeout(() => {
|
||||
// Prevent other usePress handlers from also handling this event.
|
||||
e.target.dispatchEvent(new PointerEvent('pointercancel', {bubbles: true}));
|
||||
|
||||
// Ensure target is focused. On touch devices, browsers typically focus on pointer up.
|
||||
if (getOwnerDocument(e.target).activeElement !== e.target) {
|
||||
focusWithoutScrolling(e.target as FocusableElement);
|
||||
}
|
||||
|
||||
if (onLongPress) {
|
||||
onLongPress({
|
||||
...e,
|
||||
type: 'longpress'
|
||||
});
|
||||
}
|
||||
timeRef.current = undefined;
|
||||
}, threshold);
|
||||
|
||||
// Prevent context menu, which may be opened on long press on touch devices
|
||||
if (e.pointerType === 'touch') {
|
||||
let onContextMenu = e => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
addGlobalListener(e.target, 'contextmenu', onContextMenu, {once: true});
|
||||
addGlobalListener(window, 'pointerup', () => {
|
||||
// If no contextmenu event is fired quickly after pointerup, remove the handler
|
||||
// so future context menu events outside a long press are not prevented.
|
||||
setTimeout(() => {
|
||||
removeGlobalListener(e.target, 'contextmenu', onContextMenu);
|
||||
}, 30);
|
||||
}, {once: true});
|
||||
}
|
||||
}
|
||||
},
|
||||
onPressEnd(e) {
|
||||
if (timeRef.current) {
|
||||
clearTimeout(timeRef.current);
|
||||
}
|
||||
|
||||
if (onLongPressEnd && (e.pointerType === 'mouse' || e.pointerType === 'touch')) {
|
||||
onLongPressEnd({
|
||||
...e,
|
||||
type: 'longpressend'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let descriptionProps = useDescription(onLongPress && !isDisabled ? accessibilityDescription : undefined);
|
||||
|
||||
return {
|
||||
longPressProps: mergeProps(pressProps, descriptionProps)
|
||||
};
|
||||
}
|
||||
231
node_modules/@react-aria/interactions/src/useMove.ts
generated
vendored
Normal file
231
node_modules/@react-aria/interactions/src/useMove.ts
generated
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {disableTextSelection, restoreTextSelection} from './textSelection';
|
||||
import {DOMAttributes, MoveEvents, PointerType} from '@react-types/shared';
|
||||
import React, {useMemo, useRef} from 'react';
|
||||
import {useEffectEvent, useGlobalListeners} from '@react-aria/utils';
|
||||
|
||||
export interface MoveResult {
|
||||
/** Props to spread on the target element. */
|
||||
moveProps: DOMAttributes
|
||||
}
|
||||
|
||||
interface EventBase {
|
||||
shiftKey: boolean,
|
||||
ctrlKey: boolean,
|
||||
metaKey: boolean,
|
||||
altKey: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles move interactions across mouse, touch, and keyboard, including dragging with
|
||||
* the mouse or touch, and using the arrow keys. Normalizes behavior across browsers and
|
||||
* platforms, and ignores emulated mouse events on touch devices.
|
||||
*/
|
||||
export function useMove(props: MoveEvents): MoveResult {
|
||||
let {onMoveStart, onMove, onMoveEnd} = props;
|
||||
|
||||
let state = useRef<{
|
||||
didMove: boolean,
|
||||
lastPosition: {pageX: number, pageY: number} | null,
|
||||
id: number | null
|
||||
}>({didMove: false, lastPosition: null, id: null});
|
||||
|
||||
let {addGlobalListener, removeGlobalListener} = useGlobalListeners();
|
||||
|
||||
let move = useEffectEvent((originalEvent: EventBase, pointerType: PointerType, deltaX: number, deltaY: number) => {
|
||||
if (deltaX === 0 && deltaY === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.current.didMove) {
|
||||
state.current.didMove = true;
|
||||
onMoveStart?.({
|
||||
type: 'movestart',
|
||||
pointerType,
|
||||
shiftKey: originalEvent.shiftKey,
|
||||
metaKey: originalEvent.metaKey,
|
||||
ctrlKey: originalEvent.ctrlKey,
|
||||
altKey: originalEvent.altKey
|
||||
});
|
||||
}
|
||||
onMove?.({
|
||||
type: 'move',
|
||||
pointerType,
|
||||
deltaX: deltaX,
|
||||
deltaY: deltaY,
|
||||
shiftKey: originalEvent.shiftKey,
|
||||
metaKey: originalEvent.metaKey,
|
||||
ctrlKey: originalEvent.ctrlKey,
|
||||
altKey: originalEvent.altKey
|
||||
});
|
||||
});
|
||||
|
||||
let end = useEffectEvent((originalEvent: EventBase, pointerType: PointerType) => {
|
||||
restoreTextSelection();
|
||||
if (state.current.didMove) {
|
||||
onMoveEnd?.({
|
||||
type: 'moveend',
|
||||
pointerType,
|
||||
shiftKey: originalEvent.shiftKey,
|
||||
metaKey: originalEvent.metaKey,
|
||||
ctrlKey: originalEvent.ctrlKey,
|
||||
altKey: originalEvent.altKey
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let moveProps = useMemo(() => {
|
||||
let moveProps: DOMAttributes = {};
|
||||
|
||||
let start = () => {
|
||||
disableTextSelection();
|
||||
state.current.didMove = false;
|
||||
};
|
||||
|
||||
if (typeof PointerEvent === 'undefined' && process.env.NODE_ENV === 'test') {
|
||||
let onMouseMove = (e: MouseEvent) => {
|
||||
if (e.button === 0) {
|
||||
move(e, 'mouse', e.pageX - (state.current.lastPosition?.pageX ?? 0), e.pageY - (state.current.lastPosition?.pageY ?? 0));
|
||||
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
|
||||
}
|
||||
};
|
||||
let onMouseUp = (e: MouseEvent) => {
|
||||
if (e.button === 0) {
|
||||
end(e, 'mouse');
|
||||
removeGlobalListener(window, 'mousemove', onMouseMove, false);
|
||||
removeGlobalListener(window, 'mouseup', onMouseUp, false);
|
||||
}
|
||||
};
|
||||
moveProps.onMouseDown = (e: React.MouseEvent) => {
|
||||
if (e.button === 0) {
|
||||
start();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
|
||||
addGlobalListener(window, 'mousemove', onMouseMove, false);
|
||||
addGlobalListener(window, 'mouseup', onMouseUp, false);
|
||||
}
|
||||
};
|
||||
|
||||
let onTouchMove = (e: TouchEvent) => {
|
||||
let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id);
|
||||
if (touch >= 0) {
|
||||
let {pageX, pageY} = e.changedTouches[touch];
|
||||
move(e, 'touch', pageX - (state.current.lastPosition?.pageX ?? 0), pageY - (state.current.lastPosition?.pageY ?? 0));
|
||||
state.current.lastPosition = {pageX, pageY};
|
||||
}
|
||||
};
|
||||
let onTouchEnd = (e: TouchEvent) => {
|
||||
let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id);
|
||||
if (touch >= 0) {
|
||||
end(e, 'touch');
|
||||
state.current.id = null;
|
||||
removeGlobalListener(window, 'touchmove', onTouchMove);
|
||||
removeGlobalListener(window, 'touchend', onTouchEnd);
|
||||
removeGlobalListener(window, 'touchcancel', onTouchEnd);
|
||||
}
|
||||
};
|
||||
moveProps.onTouchStart = (e: React.TouchEvent) => {
|
||||
if (e.changedTouches.length === 0 || state.current.id != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let {pageX, pageY, identifier} = e.changedTouches[0];
|
||||
start();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
state.current.lastPosition = {pageX, pageY};
|
||||
state.current.id = identifier;
|
||||
addGlobalListener(window, 'touchmove', onTouchMove, false);
|
||||
addGlobalListener(window, 'touchend', onTouchEnd, false);
|
||||
addGlobalListener(window, 'touchcancel', onTouchEnd, false);
|
||||
};
|
||||
} else {
|
||||
let onPointerMove = (e: PointerEvent) => {
|
||||
if (e.pointerId === state.current.id) {
|
||||
let pointerType = (e.pointerType || 'mouse') as PointerType;
|
||||
|
||||
// Problems with PointerEvent#movementX/movementY:
|
||||
// 1. it is always 0 on macOS Safari.
|
||||
// 2. On Chrome Android, it's scaled by devicePixelRatio, but not on Chrome macOS
|
||||
move(e, pointerType, e.pageX - (state.current.lastPosition?.pageX ?? 0), e.pageY - (state.current.lastPosition?.pageY ?? 0));
|
||||
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
|
||||
}
|
||||
};
|
||||
|
||||
let onPointerUp = (e: PointerEvent) => {
|
||||
if (e.pointerId === state.current.id) {
|
||||
let pointerType = (e.pointerType || 'mouse') as PointerType;
|
||||
end(e, pointerType);
|
||||
state.current.id = null;
|
||||
removeGlobalListener(window, 'pointermove', onPointerMove, false);
|
||||
removeGlobalListener(window, 'pointerup', onPointerUp, false);
|
||||
removeGlobalListener(window, 'pointercancel', onPointerUp, false);
|
||||
}
|
||||
};
|
||||
|
||||
moveProps.onPointerDown = (e: React.PointerEvent) => {
|
||||
if (e.button === 0 && state.current.id == null) {
|
||||
start();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY};
|
||||
state.current.id = e.pointerId;
|
||||
addGlobalListener(window, 'pointermove', onPointerMove, false);
|
||||
addGlobalListener(window, 'pointerup', onPointerUp, false);
|
||||
addGlobalListener(window, 'pointercancel', onPointerUp, false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let triggerKeyboardMove = (e: EventBase, deltaX: number, deltaY: number) => {
|
||||
start();
|
||||
move(e, 'keyboard', deltaX, deltaY);
|
||||
end(e, 'keyboard');
|
||||
};
|
||||
|
||||
moveProps.onKeyDown = (e) => {
|
||||
switch (e.key) {
|
||||
case 'Left':
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
triggerKeyboardMove(e, -1, 0);
|
||||
break;
|
||||
case 'Right':
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
triggerKeyboardMove(e, 1, 0);
|
||||
break;
|
||||
case 'Up':
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
triggerKeyboardMove(e, 0, -1);
|
||||
break;
|
||||
case 'Down':
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
triggerKeyboardMove(e, 0, 1);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return moveProps;
|
||||
}, [state, addGlobalListener, removeGlobalListener, move, end]);
|
||||
|
||||
return {moveProps};
|
||||
}
|
||||
1035
node_modules/@react-aria/interactions/src/usePress.ts
generated
vendored
Normal file
1035
node_modules/@react-aria/interactions/src/usePress.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
41
node_modules/@react-aria/interactions/src/useScrollWheel.ts
generated
vendored
Normal file
41
node_modules/@react-aria/interactions/src/useScrollWheel.ts
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2021 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {RefObject, ScrollEvents} from '@react-types/shared';
|
||||
import {useCallback} from 'react';
|
||||
import {useEvent} from '@react-aria/utils';
|
||||
|
||||
export interface ScrollWheelProps extends ScrollEvents {
|
||||
/** Whether the scroll listener should be disabled. */
|
||||
isDisabled?: boolean
|
||||
}
|
||||
|
||||
// scroll wheel needs to be added not passively so it's cancelable, small helper hook to remember that
|
||||
export function useScrollWheel(props: ScrollWheelProps, ref: RefObject<HTMLElement | null>): void {
|
||||
let {onScroll, isDisabled} = props;
|
||||
let onScrollHandler = useCallback((e) => {
|
||||
// If the ctrlKey is pressed, this is a zoom event, do nothing.
|
||||
if (e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// stop scrolling the page
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (onScroll) {
|
||||
onScroll({deltaX: e.deltaX, deltaY: e.deltaY});
|
||||
}
|
||||
}, [onScroll]);
|
||||
|
||||
useEvent(ref, 'wheel', isDisabled ? undefined : onScrollHandler);
|
||||
}
|
||||
178
node_modules/@react-aria/interactions/src/utils.ts
generated
vendored
Normal file
178
node_modules/@react-aria/interactions/src/utils.ts
generated
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {FocusableElement} from '@react-types/shared';
|
||||
import {focusWithoutScrolling, getOwnerWindow, isFocusable, useEffectEvent, useLayoutEffect} from '@react-aria/utils';
|
||||
import {FocusEvent as ReactFocusEvent, SyntheticEvent, useCallback, useRef} from 'react';
|
||||
|
||||
// Turn a native event into a React synthetic event.
|
||||
export function createSyntheticEvent<E extends SyntheticEvent>(nativeEvent: Event): E {
|
||||
let event = nativeEvent as any as E;
|
||||
event.nativeEvent = nativeEvent;
|
||||
event.isDefaultPrevented = () => event.defaultPrevented;
|
||||
// cancelBubble is technically deprecated in the spec, but still supported in all browsers.
|
||||
event.isPropagationStopped = () => (event as any).cancelBubble;
|
||||
event.persist = () => {};
|
||||
return event;
|
||||
}
|
||||
|
||||
export function setEventTarget(event: Event, target: Element): void {
|
||||
Object.defineProperty(event, 'target', {value: target});
|
||||
Object.defineProperty(event, 'currentTarget', {value: target});
|
||||
}
|
||||
|
||||
export function useSyntheticBlurEvent<Target extends Element = Element>(onBlur: (e: ReactFocusEvent<Target>) => void): (e: ReactFocusEvent<Target>) => void {
|
||||
let stateRef = useRef({
|
||||
isFocused: false,
|
||||
observer: null as MutationObserver | null
|
||||
});
|
||||
|
||||
// Clean up MutationObserver on unmount. See below.
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const state = stateRef.current;
|
||||
return () => {
|
||||
if (state.observer) {
|
||||
state.observer.disconnect();
|
||||
state.observer = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
let dispatchBlur = useEffectEvent((e: ReactFocusEvent<Target>) => {
|
||||
onBlur?.(e);
|
||||
});
|
||||
|
||||
// This function is called during a React onFocus event.
|
||||
return useCallback((e: ReactFocusEvent<Target>) => {
|
||||
// React does not fire onBlur when an element is disabled. https://github.com/facebook/react/issues/9142
|
||||
// Most browsers fire a native focusout event in this case, except for Firefox. In that case, we use a
|
||||
// MutationObserver to watch for the disabled attribute, and dispatch these events ourselves.
|
||||
// For browsers that do, focusout fires before the MutationObserver, so onBlur should not fire twice.
|
||||
if (
|
||||
e.target instanceof HTMLButtonElement ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
stateRef.current.isFocused = true;
|
||||
|
||||
let target = e.target;
|
||||
let onBlurHandler: EventListenerOrEventListenerObject | null = (e) => {
|
||||
stateRef.current.isFocused = false;
|
||||
|
||||
if (target.disabled) {
|
||||
// For backward compatibility, dispatch a (fake) React synthetic event.
|
||||
let event = createSyntheticEvent<ReactFocusEvent<Target>>(e);
|
||||
dispatchBlur(event);
|
||||
}
|
||||
|
||||
// We no longer need the MutationObserver once the target is blurred.
|
||||
if (stateRef.current.observer) {
|
||||
stateRef.current.observer.disconnect();
|
||||
stateRef.current.observer = null;
|
||||
}
|
||||
};
|
||||
|
||||
target.addEventListener('focusout', onBlurHandler, {once: true});
|
||||
|
||||
stateRef.current.observer = new MutationObserver(() => {
|
||||
if (stateRef.current.isFocused && target.disabled) {
|
||||
stateRef.current.observer?.disconnect();
|
||||
let relatedTargetEl = target === document.activeElement ? null : document.activeElement;
|
||||
target.dispatchEvent(new FocusEvent('blur', {relatedTarget: relatedTargetEl}));
|
||||
target.dispatchEvent(new FocusEvent('focusout', {bubbles: true, relatedTarget: relatedTargetEl}));
|
||||
}
|
||||
});
|
||||
|
||||
stateRef.current.observer.observe(target, {attributes: true, attributeFilter: ['disabled']});
|
||||
}
|
||||
}, [dispatchBlur]);
|
||||
}
|
||||
|
||||
export let ignoreFocusEvent = false;
|
||||
|
||||
/**
|
||||
* This function prevents the next focus event fired on `target`, without using `event.preventDefault()`.
|
||||
* It works by waiting for the series of focus events to occur, and reverts focus back to where it was before.
|
||||
* It also makes these events mostly non-observable by using a capturing listener on the window and stopping propagation.
|
||||
*/
|
||||
export function preventFocus(target: FocusableElement | null): (() => void) | undefined {
|
||||
// The browser will focus the nearest focusable ancestor of our target.
|
||||
while (target && !isFocusable(target)) {
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
let window = getOwnerWindow(target);
|
||||
let activeElement = window.document.activeElement as FocusableElement | null;
|
||||
if (!activeElement || activeElement === target) {
|
||||
return;
|
||||
}
|
||||
|
||||
ignoreFocusEvent = true;
|
||||
let isRefocusing = false;
|
||||
let onBlur = (e: FocusEvent) => {
|
||||
if (e.target === activeElement || isRefocusing) {
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
|
||||
let onFocusOut = (e: FocusEvent) => {
|
||||
if (e.target === activeElement || isRefocusing) {
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
// If there was no focusable ancestor, we don't expect a focus event.
|
||||
// Re-focus the original active element here.
|
||||
if (!target && !isRefocusing) {
|
||||
isRefocusing = true;
|
||||
focusWithoutScrolling(activeElement);
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let onFocus = (e: FocusEvent) => {
|
||||
if (e.target === target || isRefocusing) {
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
|
||||
let onFocusIn = (e: FocusEvent) => {
|
||||
if (e.target === target || isRefocusing) {
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
if (!isRefocusing) {
|
||||
isRefocusing = true;
|
||||
focusWithoutScrolling(activeElement);
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('blur', onBlur, true);
|
||||
window.addEventListener('focusout', onFocusOut, true);
|
||||
window.addEventListener('focusin', onFocusIn, true);
|
||||
window.addEventListener('focus', onFocus, true);
|
||||
|
||||
let cleanup = () => {
|
||||
cancelAnimationFrame(raf);
|
||||
window.removeEventListener('blur', onBlur, true);
|
||||
window.removeEventListener('focusout', onFocusOut, true);
|
||||
window.removeEventListener('focusin', onFocusIn, true);
|
||||
window.removeEventListener('focus', onFocus, true);
|
||||
ignoreFocusEvent = false;
|
||||
isRefocusing = false;
|
||||
};
|
||||
|
||||
let raf = requestAnimationFrame(cleanup);
|
||||
return cleanup;
|
||||
}
|
||||
Reference in New Issue
Block a user