Skip to content

Commit

Permalink
feat(dropdown): ability to exclude elements from closing the dropdow
Browse files Browse the repository at this point in the history
  • Loading branch information
anho committed Nov 11, 2020
1 parent 42dfe1b commit 55b4565
Show file tree
Hide file tree
Showing 4 changed files with 28 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,8 @@ export const Autocomplete = <T extends {}>({
dropdownProps,
renderToggleElement,
}: AutocompleteProps<T>) => {
const listRef: React.MutableRefObject<HTMLDivElement | undefined> = useRef();
const inputRef: React.MutableRefObject<
HTMLInputElement | undefined
> = useRef();
const listRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);

const [{ isOpen, query, highlightedItemIndex }, dispatch] = useReducer(
reducer,
Expand Down Expand Up @@ -236,13 +234,12 @@ export const Autocomplete = <T extends {}>({

return (
<Dropdown
nonClosingRefs={[inputRef]}
className={dropdownClassNames}
isOpen={isOpen}
onClose={() => {
if (inputRef.current !== document.activeElement) {
willClearQueryOnClose && updateQuery('');
dispatch({ type: TOGGLED_LIST, payload: false });
}
willClearQueryOnClose && updateQuery('');
dispatch({ type: TOGGLED_LIST, payload: false });
}}
toggleElement={renderToggleElementFunction(toggleProps)}
{...dropdownProps}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { RefObject, useCallback, useEffect, useState } from 'react';
import cn from 'classnames';
import { usePopper } from 'react-popper';
import { Modifier, Placement, State as PopperState } from '@popperjs/core';
Expand Down Expand Up @@ -125,6 +125,13 @@ export interface DropdownProps {
* Defaults to `true`
*/
usePortal?: boolean;
/**
* By passing down references from upstream components as part of this array,
* you will be able to disable automatic closing of the dropdown if you click
* somewhere else in the document besides the dropdown itself. See the
* Autocomplete component for an actual example of this usage.
*/
nonClosingRefs?: RefObject<HTMLElement>[];
}

export function Dropdown({
Expand All @@ -141,6 +148,7 @@ export function Dropdown({
testId,
toggleElement,
usePortal,
nonClosingRefs,
...otherProps
}: DropdownProps) {
const [referenceElement, setReferenceElement] = useState<HTMLElement | null>(
Expand Down Expand Up @@ -238,6 +246,7 @@ export function Dropdown({
})}
{isOpen && (
<DropdownContainer
nonClosingRefs={nonClosingRefs}
className={dropdownContainerClassName}
getRef={getContainerRef}
isOpen={isOpen}
Expand Down Expand Up @@ -269,6 +278,7 @@ export function Dropdown({

{isOpen && (
<DropdownContainer
nonClosingRefs={nonClosingRefs}
className={dropdownContainerClassName}
getRef={getContainerRef}
isOpen={isOpen}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { forwardRef, useEffect, useRef } from 'react';
import React, { forwardRef, useEffect, useRef, RefObject } from 'react';
import cn from 'classnames';

import { useOnClickOutside } from '../../../utils/useOnClickOutside';
Expand All @@ -11,6 +11,7 @@ export interface DropdownContainerProps
children?: React.ReactNode;
className?: string;
getRef?: (ref: HTMLElement | null) => void;
nonClosingRefs?: RefObject<HTMLElement>[];
isOpen: boolean;
onClose?: Function;
openSubmenu?: (value: boolean) => void;
Expand All @@ -36,6 +37,7 @@ export const DropdownContainer = forwardRef<
submenu,
testId,
usePortal,
nonClosingRefs,
...props
},
refCallback,
Expand All @@ -47,8 +49,9 @@ export const DropdownContainer = forwardRef<
>;
const dropdown = useRef<HTMLDivElement | null>(null);
const classNames = cn(className, styles['DropdownContainer']);
const clickableRefs = [dropdown, ...(nonClosingRefs || [])];

useOnClickOutside(dropdown, (event) => {
useOnClickOutside(clickableRefs, (event) => {
if (isOpen && onClose) {
event.stopImmediatePropagation();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useEffect, useRef } from 'react';
import { RefObject, useEffect, useRef } from 'react';

/**
* Runs the given handler when a click event is fired outside the HTMLElement
* the given RefObject points to.
*
* @param ref - RefObject pointing to a HTMLElement to track clicks outside
* @param refs - RefObject[] pointing to a HTMLElement to track clicks outside
* @param handler - Event handler to run on click outside
*/
export function useOnClickOutside(
ref: React.RefObject<HTMLElement>,
refs: React.RefObject<HTMLElement>[],
handler: (event: MouseEvent) => void | null,
) {
const noHandler = !handler;
Expand All @@ -19,11 +19,12 @@ export function useOnClickOutside(
return;
}

const refContains = (ref:RefObject<HTMLElement>, node: Node) =>
ref.current && ref.current.contains(node)
const listener = (event: MouseEvent) => {
if (
!ref.current ||
!handlerRef.current ||
ref.current.contains(event.target as Node)
refs.some((ref) => refContains(ref, event.target as Node))
) {
return;
}
Expand All @@ -36,5 +37,5 @@ export function useOnClickOutside(
return () => {
document.removeEventListener('click', listener, {});
};
}, [noHandler, ref]);
}, [noHandler, refs]);
}

0 comments on commit 55b4565

Please sign in to comment.