Skip to content

Commit 28e5191

Browse files
feat(@clayui/drop-down): add api for controlling the active state of the menu
1 parent 2f090ff commit 28e5191

File tree

5 files changed

+203
-112
lines changed

5 files changed

+203
-112
lines changed

packages/clay-drop-down/src/DropDown.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import Section from './Section';
2121
interface IProps extends React.HTMLAttributes<HTMLDivElement | HTMLLIElement> {
2222
/**
2323
* Flag to indicate if the DropDown menu is active or not.
24+
*
25+
* This API is generally used in conjunction with `closeOnClickOutside=true`
26+
* since often we are controlling the active state by clicking another element
27+
* within the document.
2428
*/
2529
active: boolean;
2630

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

41+
closeOnClickOutside?: React.ComponentProps<
42+
typeof Menu
43+
>['closeOnClickOutside'];
44+
3745
/**
3846
* Flag to indicate if menu contains icon symbols on the right side.
3947
*/
@@ -55,6 +63,10 @@ interface IProps extends React.HTMLAttributes<HTMLDivElement | HTMLLIElement> {
5563

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

@@ -87,6 +99,7 @@ const ClayDropDown: React.FunctionComponent<IProps> & {
8799
alignmentPosition,
88100
children,
89101
className,
102+
closeOnClickOutside,
90103
containerElement: ContainerElement = 'div',
91104
hasLeftSymbols,
92105
hasRightSymbols,
@@ -156,6 +169,7 @@ const ClayDropDown: React.FunctionComponent<IProps> & {
156169
active={active}
157170
alignElementRef={triggerElementRef}
158171
alignmentPosition={alignmentPosition}
172+
closeOnClickOutside={closeOnClickOutside}
159173
hasLeftSymbols={hasLeftSymbols}
160174
hasRightSymbols={hasRightSymbols}
161175
height={menuHeight}

packages/clay-drop-down/src/DropDownWithItems.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import {ClayCheckbox, ClayRadio} from '@clayui/form';
7+
import {useInternalState} from '@clayui/shared';
78
import React from 'react';
89
import warning from 'warning';
910

@@ -46,6 +47,8 @@ interface IDropDownContentProps {
4647
}
4748

4849
export interface IProps extends IDropDownContentProps {
50+
active?: React.ComponentProps<typeof ClayDropDown>['active'];
51+
4952
/**
5053
* Default position of menu element. Values come from `./Menu`.
5154
*/
@@ -60,6 +63,10 @@ export interface IProps extends IDropDownContentProps {
6063

6164
className?: string;
6265

66+
closeOnClickOutside?: React.ComponentProps<
67+
typeof ClayDropDown
68+
>['closeOnClickOutside'];
69+
6370
/**
6471
* HTML element tag that the container should render.
6572
*/
@@ -99,6 +106,10 @@ export interface IProps extends IDropDownContentProps {
99106
*/
100107
offsetFn?: React.ComponentProps<typeof ClayDropDown>['offsetFn'];
101108

109+
onActiveChange?: React.ComponentProps<
110+
typeof ClayDropDown
111+
>['onActiveChange'];
112+
102113
/**
103114
* Callback will always be called when the user is interacting with search.
104115
*/
@@ -291,9 +302,11 @@ const findNested = <
291302
});
292303

293304
export const ClayDropDownWithItems: React.FunctionComponent<IProps> = ({
305+
active,
294306
alignmentPosition,
295307
caption,
296308
className,
309+
closeOnClickOutside,
297310
containerElement,
298311
footerContent,
299312
helpText,
@@ -302,14 +315,20 @@ export const ClayDropDownWithItems: React.FunctionComponent<IProps> = ({
302315
menuHeight,
303316
menuWidth,
304317
offsetFn,
318+
onActiveChange,
305319
onSearchValueChange = () => {},
306320
searchable,
307321
searchProps,
308322
searchValue = '',
309323
spritemap,
310324
trigger,
311325
}: IProps) => {
312-
const [active, setActive] = React.useState(false);
326+
const [internalActive, setInternalActive] = useInternalState({
327+
initialValue: false,
328+
onChange: onActiveChange,
329+
value: active,
330+
});
331+
313332
const hasRightSymbols = React.useMemo(
314333
() => !!findNested(items, 'symbolRight'),
315334
[items]
@@ -323,21 +342,26 @@ export const ClayDropDownWithItems: React.FunctionComponent<IProps> = ({
323342

324343
return (
325344
<ClayDropDown
326-
active={active}
345+
active={internalActive}
327346
alignmentPosition={alignmentPosition}
328347
className={className}
348+
closeOnClickOutside={closeOnClickOutside}
329349
containerElement={containerElement}
330350
hasLeftSymbols={hasLeftSymbols}
331351
hasRightSymbols={hasRightSymbols}
332352
menuElementAttrs={menuElementAttrs}
333353
menuHeight={menuHeight}
334354
menuWidth={menuWidth}
335355
offsetFn={offsetFn}
336-
onActiveChange={setActive}
356+
onActiveChange={
357+
setInternalActive as React.ComponentProps<
358+
typeof ClayDropDown
359+
>['onActiveChange']
360+
}
337361
trigger={trigger}
338362
>
339363
<ClayDropDownContext.Provider
340-
value={{close: () => setActive(false)}}
364+
value={{close: () => setInternalActive(false)}}
341365
>
342366
{helpText && <Help>{helpText}</Help>}
343367

packages/clay-drop-down/src/Menu.tsx

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ interface IProps extends React.HTMLAttributes<HTMLDivElement> {
120120
*/
121121
alignmentPosition?: number | TPointOptions;
122122

123+
/**
124+
* Flag to indicate if clicking outside of the menu should automatically close it.
125+
*/
126+
closeOnClickOutside?: boolean;
127+
123128
/**
124129
* Flag to indicate if menu is displaying a clay-icon on the left.
125130
*/
@@ -165,6 +170,7 @@ const ClayDropDownMenu = React.forwardRef<HTMLDivElement, IProps>(
165170
autoBestAlign = true,
166171
children,
167172
className,
173+
closeOnClickOutside = true,
168174
hasLeftSymbols,
169175
hasRightSymbols,
170176
height,
@@ -186,31 +192,33 @@ const ClayDropDownMenu = React.forwardRef<HTMLDivElement, IProps>(
186192
const subPortalRef = useRef<HTMLDivElement | null>(null);
187193

188194
useEffect(() => {
189-
const handleClick = (event: MouseEvent) => {
190-
const nodeRefs = [alignElementRef, subPortalRef];
191-
const nodes: Array<Node> = (Array.isArray(nodeRefs)
192-
? nodeRefs
193-
: [nodeRefs]
194-
)
195-
.filter((ref) => ref.current)
196-
.map((ref) => ref.current!);
197-
198-
if (
199-
event.target instanceof Node &&
200-
!nodes.find((element) =>
201-
element.contains(event.target as Node)
195+
if (closeOnClickOutside) {
196+
const handleClick = (event: MouseEvent) => {
197+
const nodeRefs = [alignElementRef, subPortalRef];
198+
const nodes: Array<Node> = (Array.isArray(nodeRefs)
199+
? nodeRefs
200+
: [nodeRefs]
202201
)
203-
) {
204-
onSetActive(false);
205-
}
206-
};
202+
.filter((ref) => ref.current)
203+
.map((ref) => ref.current!);
204+
205+
if (
206+
event.target instanceof Node &&
207+
!nodes.find((element) =>
208+
element.contains(event.target as Node)
209+
)
210+
) {
211+
onSetActive(false);
212+
}
213+
};
207214

208-
window.addEventListener('mousedown', handleClick);
215+
window.addEventListener('mousedown', handleClick);
209216

210-
return () => {
211-
window.removeEventListener('mousedown', handleClick);
212-
};
213-
}, []);
217+
return () => {
218+
window.removeEventListener('mousedown', handleClick);
219+
};
220+
}
221+
}, [closeOnClickOutside]);
214222

215223
useEffect(() => {
216224
const handleEsc = (event: KeyboardEvent) => {

0 commit comments

Comments
 (0)