Skip to content

Commit

Permalink
feat(@clayui/drop-down): add api for controlling the active state of …
Browse files Browse the repository at this point in the history
…the menu
  • Loading branch information
bryceosterhaus committed Jan 27, 2021
1 parent 2f090ff commit 28e5191
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 112 deletions.
14 changes: 14 additions & 0 deletions packages/clay-drop-down/src/DropDown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import Section from './Section';
interface IProps extends React.HTMLAttributes<HTMLDivElement | HTMLLIElement> {
/**
* Flag to indicate if the DropDown menu is active or not.
*
* This API is generally used in conjunction with `closeOnClickOutside=true`
* since often we are controlling the active state by clicking another element
* within the document.
*/
active: boolean;

Expand All @@ -34,6 +38,10 @@ interface IProps extends React.HTMLAttributes<HTMLDivElement | HTMLLIElement> {
*/
containerElement?: React.JSXElementConstructor<any> | 'div' | 'li';

closeOnClickOutside?: React.ComponentProps<
typeof Menu
>['closeOnClickOutside'];

/**
* Flag to indicate if menu contains icon symbols on the right side.
*/
Expand All @@ -55,6 +63,10 @@ interface IProps extends React.HTMLAttributes<HTMLDivElement | HTMLLIElement> {

/**
* Callback for when the active state changes.
*
* This API is generally used in conjunction with `closeOnClickOutside=true`
* since often we are controlling the active state by clicking another element
* within the document.
*/
onActiveChange: (val: boolean) => void;

Expand Down Expand Up @@ -87,6 +99,7 @@ const ClayDropDown: React.FunctionComponent<IProps> & {
alignmentPosition,
children,
className,
closeOnClickOutside,
containerElement: ContainerElement = 'div',
hasLeftSymbols,
hasRightSymbols,
Expand Down Expand Up @@ -156,6 +169,7 @@ const ClayDropDown: React.FunctionComponent<IProps> & {
active={active}
alignElementRef={triggerElementRef}
alignmentPosition={alignmentPosition}
closeOnClickOutside={closeOnClickOutside}
hasLeftSymbols={hasLeftSymbols}
hasRightSymbols={hasRightSymbols}
height={menuHeight}
Expand Down
32 changes: 28 additions & 4 deletions packages/clay-drop-down/src/DropDownWithItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import {ClayCheckbox, ClayRadio} from '@clayui/form';
import {useInternalState} from '@clayui/shared';
import React from 'react';
import warning from 'warning';

Expand Down Expand Up @@ -46,6 +47,8 @@ interface IDropDownContentProps {
}

export interface IProps extends IDropDownContentProps {
active?: React.ComponentProps<typeof ClayDropDown>['active'];

/**
* Default position of menu element. Values come from `./Menu`.
*/
Expand All @@ -60,6 +63,10 @@ export interface IProps extends IDropDownContentProps {

className?: string;

closeOnClickOutside?: React.ComponentProps<
typeof ClayDropDown
>['closeOnClickOutside'];

/**
* HTML element tag that the container should render.
*/
Expand Down Expand Up @@ -99,6 +106,10 @@ export interface IProps extends IDropDownContentProps {
*/
offsetFn?: React.ComponentProps<typeof ClayDropDown>['offsetFn'];

onActiveChange?: React.ComponentProps<
typeof ClayDropDown
>['onActiveChange'];

/**
* Callback will always be called when the user is interacting with search.
*/
Expand Down Expand Up @@ -291,9 +302,11 @@ const findNested = <
});

export const ClayDropDownWithItems: React.FunctionComponent<IProps> = ({
active,
alignmentPosition,
caption,
className,
closeOnClickOutside,
containerElement,
footerContent,
helpText,
Expand All @@ -302,14 +315,20 @@ export const ClayDropDownWithItems: React.FunctionComponent<IProps> = ({
menuHeight,
menuWidth,
offsetFn,
onActiveChange,
onSearchValueChange = () => {},
searchable,
searchProps,
searchValue = '',
spritemap,
trigger,
}: IProps) => {
const [active, setActive] = React.useState(false);
const [internalActive, setInternalActive] = useInternalState({
initialValue: false,
onChange: onActiveChange,
value: active,
});

const hasRightSymbols = React.useMemo(
() => !!findNested(items, 'symbolRight'),
[items]
Expand All @@ -323,21 +342,26 @@ export const ClayDropDownWithItems: React.FunctionComponent<IProps> = ({

return (
<ClayDropDown
active={active}
active={internalActive}
alignmentPosition={alignmentPosition}
className={className}
closeOnClickOutside={closeOnClickOutside}
containerElement={containerElement}
hasLeftSymbols={hasLeftSymbols}
hasRightSymbols={hasRightSymbols}
menuElementAttrs={menuElementAttrs}
menuHeight={menuHeight}
menuWidth={menuWidth}
offsetFn={offsetFn}
onActiveChange={setActive}
onActiveChange={
setInternalActive as React.ComponentProps<
typeof ClayDropDown
>['onActiveChange']
}
trigger={trigger}
>
<ClayDropDownContext.Provider
value={{close: () => setActive(false)}}
value={{close: () => setInternalActive(false)}}
>
{helpText && <Help>{helpText}</Help>}

Expand Down
52 changes: 30 additions & 22 deletions packages/clay-drop-down/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ interface IProps extends React.HTMLAttributes<HTMLDivElement> {
*/
alignmentPosition?: number | TPointOptions;

/**
* Flag to indicate if clicking outside of the menu should automatically close it.
*/
closeOnClickOutside?: boolean;

/**
* Flag to indicate if menu is displaying a clay-icon on the left.
*/
Expand Down Expand Up @@ -165,6 +170,7 @@ const ClayDropDownMenu = React.forwardRef<HTMLDivElement, IProps>(
autoBestAlign = true,
children,
className,
closeOnClickOutside = true,
hasLeftSymbols,
hasRightSymbols,
height,
Expand All @@ -186,31 +192,33 @@ const ClayDropDownMenu = React.forwardRef<HTMLDivElement, IProps>(
const subPortalRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
const handleClick = (event: MouseEvent) => {
const nodeRefs = [alignElementRef, subPortalRef];
const nodes: Array<Node> = (Array.isArray(nodeRefs)
? nodeRefs
: [nodeRefs]
)
.filter((ref) => ref.current)
.map((ref) => ref.current!);

if (
event.target instanceof Node &&
!nodes.find((element) =>
element.contains(event.target as Node)
if (closeOnClickOutside) {
const handleClick = (event: MouseEvent) => {
const nodeRefs = [alignElementRef, subPortalRef];
const nodes: Array<Node> = (Array.isArray(nodeRefs)
? nodeRefs
: [nodeRefs]
)
) {
onSetActive(false);
}
};
.filter((ref) => ref.current)
.map((ref) => ref.current!);

if (
event.target instanceof Node &&
!nodes.find((element) =>
element.contains(event.target as Node)
)
) {
onSetActive(false);
}
};

window.addEventListener('mousedown', handleClick);
window.addEventListener('mousedown', handleClick);

return () => {
window.removeEventListener('mousedown', handleClick);
};
}, []);
return () => {
window.removeEventListener('mousedown', handleClick);
};
}
}, [closeOnClickOutside]);

useEffect(() => {
const handleEsc = (event: KeyboardEvent) => {
Expand Down
Loading

0 comments on commit 28e5191

Please sign in to comment.