Skip to content

Commit

Permalink
Merge pull request #12 from AkinAguda/development
Browse files Browse the repository at this point in the history
Development
  • Loading branch information
AkinAguda authored May 1, 2021
2 parents 9ca6f50 + 6ff1f96 commit f77086b
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 22 deletions.
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
Unopinionated dropdown component for react.

![build](https://github.com/AkinAguda/unop-react-dropdown/actions/workflows/main.yml/badge.svg)
![](https://img.shields.io/badge/coverage-94.19%25-green)

<!-- ![](https://img.shields.io/badge/coverage-94.19%25-green) -->

### Motivation

Expand Down Expand Up @@ -96,15 +97,17 @@ Full list of component props, and their options can be found [here](#api)

### Component props

| Prop | Type | Default | Description |
| ---------------- | ----------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| trigger | Jsx.Element | none | This is the only compulsory prop. This will be passed an onClick to handle the toggling when it is rendered. This should prefarably be a button |
| align | 'RIGHT' or 'LEFT' or 'CENTER' | 'LEFT' | When 'RIGHT', the dropdown will be rendered below the trigger, aligned to the right. When 'CENTER', the dropdown will be aligned to the center |
| onAppear | function | null | This will be called when the dropdown is visible |
| onDisappear | function | null | This will be called when the dropdown is invisible |
| onDisappearStart | function | null | This will be called when the timeout to diappear(become invisible) starts |
| delay | number | 0 | This is the delay in milliseconds before the dropdown goes invisible |
| hover | boolean | false | When true, the dropdown will become visible on hover |
| Prop | Type | Default | Description |
| ---------------------- | ----------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| trigger | Jsx.Element | none | This is the only compulsory prop. This will be passed an onClick to handle the toggling when it is rendered. This should prefarably be a button |
| align | 'RIGHT' or 'LEFT' or 'CENTER' | 'LEFT' | When 'RIGHT', the dropdown will be rendered below the trigger, aligned to the right. When 'CENTER', the dropdown will be aligned to the center |
| onAppear | function | null | This will be called when the dropdown is visible |
| onDisappear | function | null | This will be called when the dropdown is invisible |
| onDisappearStart | function | null | This will be called when the timeout to diappear(become invisible) starts |
| delay | number | 0 | This is the delay in milliseconds before the dropdown goes invisible |
| hover | boolean | false | When true, the dropdown will become visible on hover |
| closeOnClickOut | boolean | false | When true, this closes the dropdown when the user clicks on any element that is outside the dropdown |
| closeOnDropdownClicked | boolean | false | When true, this closes the dropdown when any area in the dropdown is clicked |

### License

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.1.8",
"version": "0.2.8",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down
2 changes: 2 additions & 0 deletions src/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const DropDown: React.FC<DropDownProps> = ({
dropdownMenuRef,
show,
style,
dropdownRef,
}) => (
<div
className="UnopdropDown_EMFQP"
Expand All @@ -19,6 +20,7 @@ const DropDown: React.FC<DropDownProps> = ({
onFocus={() => {}}
role="button"
tabIndex={0}
ref={dropdownRef}
>
{React.cloneElement(trigger, { onClick: handleClick })}

Expand Down
46 changes: 38 additions & 8 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React, { useState, useRef, useEffect } from 'react';
import DropDown from './Dropdown';
import { UnopDropdownProps, DropDowndirections } from './types';
import {
UnopDropdownProps,
DropDowndirections,
CustomMouseEvent,
} from './types';
import { Utility } from './functions.module';

const UnopDropdown: React.FC<UnopDropdownProps> = ({
Expand All @@ -12,6 +16,8 @@ const UnopDropdown: React.FC<UnopDropdownProps> = ({
onDisappearStart,
delay,
hover,
closeOnClickOut = false,
closeOnDropdownClicked = false,
}) => {
const [show, setShow] = useState(false);

Expand All @@ -21,6 +27,8 @@ const UnopDropdown: React.FC<UnopDropdownProps> = ({

const dropdownMenuRef = useRef<HTMLDivElement>(null);

const dropdownRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const element = dropdownMenuRef.current!;
element.style.visibility = 'hidden';
Expand All @@ -30,14 +38,37 @@ const UnopDropdown: React.FC<UnopDropdownProps> = ({
element.style.visibility = 'visible';
}, []);

const displayMenuItem = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
useEffect(() => {
if (closeOnClickOut || closeOnDropdownClicked) {
window.addEventListener('click', (e) => {
const path = e.composedPath();
if (
show &&
closeOnClickOut &&
!path.includes(dropdownMenuRef.current!)
) {
handleAction(e);
} else {
if (
show &&
closeOnDropdownClicked &&
path.includes(dropdownMenuRef.current!)
) {
handleAction(e);
}
}
});
}
}, [show]);

const displayMenuItem = (e: CustomMouseEvent) => {
if (timer) clearTimeout(timer.current!);
timer.current = null;
setShow(true);
if (onAppear) onAppear(e);
};

const makeDisappear = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const makeDisappear = (e: CustomMouseEvent) => {
const timerFunc = () =>
setTimeout(() => {
setShow(false);
Expand All @@ -47,7 +78,7 @@ const UnopDropdown: React.FC<UnopDropdownProps> = ({
if (onDisappearStart) onDisappearStart(e);
};

const handleAction = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const handleAction = (e: CustomMouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (show) {
Expand All @@ -57,15 +88,13 @@ const UnopDropdown: React.FC<UnopDropdownProps> = ({
}
};

const handleMouseOver = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const handleMouseOver = (e: CustomMouseEvent) => {
if (hover && !show) {
handleAction(e);
}
};

const handleMouseLeave = (
e: React.MouseEvent<HTMLDivElement, MouseEvent>
) => {
const handleMouseLeave = (e: CustomMouseEvent) => {
if (hover && show) {
handleAction(e);
}
Expand All @@ -80,6 +109,7 @@ const UnopDropdown: React.FC<UnopDropdownProps> = ({
trigger={trigger}
style={Utility.getStyleObject(align, dropdownWidth.current)}
dropdownMenuRef={dropdownMenuRef}
dropdownRef={dropdownRef}
>
{children}
</DropDown>
Expand Down
13 changes: 10 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ export interface CommonProps {
trigger: JSX.Element;
}

export type CustomMouseEvent =
| MouseEvent
| React.MouseEvent<HTMLDivElement, MouseEvent>;

export interface DropDownProps extends CommonProps {
handleClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
handleMouseOver: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
handleMouseLeave: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
handleClick: (e: CustomMouseEvent) => void;
handleMouseOver: (e: CustomMouseEvent) => void;
handleMouseLeave: (e: CustomMouseEvent) => void;
show: boolean;
style: React.CSSProperties;
dropdownMenuRef: React.RefObject<HTMLDivElement>;
dropdownRef: React.RefObject<HTMLDivElement>;
}

export enum DropDowndirections {
Expand All @@ -24,4 +29,6 @@ export interface UnopDropdownProps extends CommonProps {
onDisappear?: (e?: any) => void;
delay?: number;
hover?: boolean;
closeOnClickOut?: boolean;
closeOnDropdownClicked?: boolean;
}
14 changes: 14 additions & 0 deletions stories/Dropdown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ HoverTrigger.args = {
trigger: <button>hover</button>,
};

export const CloseOnClickOut = Template.bind({});

CloseOnClickOut.args = {
...Default.args,
closeOnClickOut: true,
};

export const CloseOnDropdownClicked = Template.bind({});

CloseOnDropdownClicked.args = {
...Default.args,
closeOnDropdownClicked: true,
};

export const LeftAlignedDropdown = Template.bind({});

LeftAlignedDropdown.args = {
Expand Down
77 changes: 77 additions & 0 deletions test/Dropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,83 @@ describe('UnopDropdown', () => {
});
});

describe('Click out an close on click work fine', () => {
it('closes properly when cliked out', () => {
const dropdown = mount(
<div>
<UnopDropdown
closeOnClickOut
trigger={<button id="trigger">Hover</button>}
>
<ul>
<li>Hello</li>
</ul>
</UnopDropdown>
<div id="randomElement"></div>
</div>
);
const dropdownMenu = dropdown.find('.drop-down-menu_EMFQP');
const trigger = dropdown.find('#trigger');
const outsideElement = dropdown.find('#randomElement');
trigger.simulate('click');
expect(dropdownMenu.getDOMNode().classList).toContain(
'reveal-drop-down-menu_EMFQP'
);
trigger.simulate('click');
setTimeout(() => {
expect(dropdownMenu.getDOMNode().classList).toContain(
'reveal-drop-down-menu_EMFQP'
);
}, 0);
outsideElement.simulate('click');
setTimeout(() => {
expect(dropdownMenu.getDOMNode().classList).not.toContain(
'reveal-drop-down-menu_EMFQP'
);
}, 0);
});
it('closes properly when item inside is clicked', () => {
const dropdown = mount(
<div>
<UnopDropdown
closeOnDropdownClicked
trigger={<button id="trigger">Hover</button>}
>
<ul>
<li id="item">Hello</li>
</ul>
</UnopDropdown>
<div id="randomElement"></div>
</div>
);
const dropdownMenu = dropdown.find('.drop-down-menu_EMFQP');
const trigger = dropdown.find('#trigger');
const item = dropdown.find('#item');
const outsideElement = dropdown.find('#randomElement');

trigger.simulate('click');
setTimeout(() => {
expect(dropdownMenu.getDOMNode().classList).toContain(
'reveal-drop-down-menu_EMFQP'
);
}, 0);

outsideElement.simulate('click');
setTimeout(() => {
expect(dropdownMenu.getDOMNode().classList).toContain(
'reveal-drop-down-menu_EMFQP'
);
}, 0);

item.simulate('click');
setTimeout(() => {
expect(dropdownMenu.getDOMNode().classList).not.toContain(
'reveal-drop-down-menu_EMFQP'
);
}, 0);
});
});

describe('Unopdropdown delays properly', () => {
const timeout = 300;

Expand Down

0 comments on commit f77086b

Please sign in to comment.