Skip to content

Commit

Permalink
Merge pull request #123 from kodiak-packages/dropdown-component
Browse files Browse the repository at this point in the history
Create popover component
  • Loading branch information
bramvanhoutte authored Oct 5, 2020
2 parents a89c66a + 737d02b commit 2e44f60
Show file tree
Hide file tree
Showing 16 changed files with 549 additions and 46 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@
"typescript": "^3.9.7"
},
"dependencies": {
"@popperjs/core": "^2.4.4",
"classnames": "^2.2.6",
"react-feather": "^2.0.8",
"react-popper": "^2.2.3",
"react-portal": "^4.2.1"
},
"keywords": [
Expand Down
101 changes: 56 additions & 45 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { MouseEventHandler } from 'react';
import React, { FocusEventHandler, MouseEventHandler } from 'react';
import classNames from 'classnames';

import Spinner from '../utils/Spinner/Spinner';
Expand All @@ -10,6 +10,8 @@ interface Props {
children: React.ReactNode;
type?: 'primary' | 'secondary';
onClick?: MouseEventHandler<HTMLButtonElement>;
onFocus?: FocusEventHandler<HTMLButtonElement>;
onBlur?: FocusEventHandler<HTMLButtonElement>;
className?: string;
isDisabled?: boolean;
isLoading?: boolean;
Expand All @@ -20,51 +22,60 @@ interface Props {
size?: 'normal' | 'small';
}

const Button: React.FC<Props> = ({
children,
type = 'primary',
onClick,
className,
isDisabled,
isLoading,
htmlType = 'button',
prefixIcon,
suffixIcon,
name,
size = 'normal',
}: Props) => {
const buttonClassNames = classNames(
cssReset.ventura,
styles.button,
const Button = React.forwardRef<HTMLButtonElement, Props>(
(
{
[styles.typePrimary]: type === 'primary',
[styles.typeSecondary]: type === 'secondary',
[styles.smallButton]: size === 'small',
},
className,
);
children,
type = 'primary',
onClick,
onFocus,
onBlur,
className,
isDisabled,
isLoading,
htmlType = 'button',
prefixIcon,
suffixIcon,
name,
size = 'normal',
}: Props,
ref,
) => {
const buttonClassNames = classNames(
cssReset.ventura,
styles.button,
{
[styles.typePrimary]: type === 'primary',
[styles.typeSecondary]: type === 'secondary',
[styles.smallButton]: size === 'small',
},
className,
);

const labelClassNames = classNames(styles.label, {
[styles.labelWithPrefixIcon]: Boolean(prefixIcon) || isLoading,
[styles.labelWithSuffixIcon]: Boolean(suffixIcon),
[styles.smallLabel]: size === 'small',
});

return (
<button
disabled={isDisabled || isLoading}
className={buttonClassNames}
// eslint-disable-next-line react/button-has-type
type={htmlType}
onClick={isLoading || suffixIcon ? undefined : onClick}
name={name}
data-testid={name && `button-${name}`}
>
{isLoading ? <Spinner className={styles.spinner} /> : prefixIcon}
<span className={labelClassNames}>{children}</span>
{isLoading || suffixIcon}
</button>
);
};
const labelClassNames = classNames(styles.label, {
[styles.labelWithPrefixIcon]: Boolean(prefixIcon) || isLoading,
[styles.labelWithSuffixIcon]: Boolean(suffixIcon),
[styles.smallLabel]: size === 'small',
});

return (
<button
disabled={isDisabled || isLoading}
className={buttonClassNames}
// eslint-disable-next-line react/button-has-type
type={htmlType}
onClick={isLoading || isDisabled ? undefined : onClick}
onFocus={isLoading || isDisabled ? undefined : onFocus}
onBlur={onBlur}
name={name}
data-testid={name && `button-${name}`}
ref={ref}
>
{isLoading ? <Spinner className={styles.spinner} /> : prefixIcon}
<span className={labelClassNames}>{children}</span>
{isLoading || suffixIcon}
</button>
);
},
);
export default Button;
6 changes: 6 additions & 0 deletions src/components/Menu/Menu.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.menu {
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: var(--border-radius-small);
}
36 changes: 36 additions & 0 deletions src/components/Menu/Menu.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react';

import { GitHub } from '../../index';
import Menu from './Menu';

describe('Menu', () => {
test('default snapshot', () => {
const component = (
<Menu>
<Menu.Item>Option 1</Menu.Item>
<Menu.Item prefixIcon={<GitHub />}>Option 2</Menu.Item>
</Menu>
);
const { asFragment } = render(component);
expect(asFragment()).toMatchSnapshot();
});

test('onClick should be triggered when clicked', () => {
const onClickFn = jest.fn();
const component = (
<Menu>
<Menu.Item name="click" onClick={onClickFn}>
Option 1
</Menu.Item>
<Menu.Item prefixIcon={<GitHub />}>Option 2</Menu.Item>
</Menu>
);
const { getByTestId } = render(component);

const menuItem = getByTestId('menu-item-click');
fireEvent.click(menuItem);

expect(onClickFn).toHaveBeenCalledTimes(1);
});
});
24 changes: 24 additions & 0 deletions src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import classNames from 'classnames';

import MenuItem from './MenuItem/MenuItem';

import cssReset from '../../css-reset.module.css';
import styles from './Menu.module.css';

type Props = {
className?: string;
children?: React.ReactNode;
};

const Menu: React.FC<Props> & {
Item: typeof MenuItem;
} = ({ className, children }: Props) => {
const menuClassNames = classNames(cssReset.ventura, styles.menu, className);

return <div className={menuClassNames}>{children}</div>;
};

Menu.Item = MenuItem;

export default Menu;
28 changes: 28 additions & 0 deletions src/components/Menu/MenuItem/MenuItem.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.menuItem {
line-height: 14px;
font-size: 14px;
padding: 10px 14px;
border: none;
cursor: pointer;
display: inline-block;
background-color: var(--color-secondary);
text-align: left;
}

.menuItem:focus {
background-color: var(--color-secondary-hover);
outline: none;
}

.menuItem:hover {
background-color: var(--color-secondary-hover);
}

.label {
line-height: inherit;
}

.labelWithPrefixIcon {
margin-left: 8px;
}

41 changes: 41 additions & 0 deletions src/components/Menu/MenuItem/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { MouseEventHandler } from 'react';
import classNames from 'classnames';

import cssReset from '../../../css-reset.module.css';
import styles from './MenuItem.module.css';

type Props = {
className?: string;
children?: React.ReactNode;
prefixIcon?: React.ReactElement;
isDisabled?: boolean;
onClick?: MouseEventHandler<HTMLButtonElement>;
name?: string;
};

const MenuItem = React.forwardRef<HTMLButtonElement, Props>(
({ className, children, isDisabled = false, onClick, name, prefixIcon }: Props, ref) => {
const menuItemClassNames = classNames(cssReset.ventura, styles.menuItem, className);

const labelClassNames = classNames(styles.label, {
[styles.labelWithPrefixIcon]: Boolean(prefixIcon),
});

return (
<button
disabled={isDisabled}
className={menuItemClassNames}
type="button"
onClick={isDisabled ? undefined : onClick}
name={name}
data-testid={name && `menu-item-${name}`}
ref={ref}
>
{prefixIcon}
<span className={labelClassNames}>{children}</span>
</button>
);
},
);

export default MenuItem;
45 changes: 45 additions & 0 deletions src/components/Menu/__snapshots__/Menu.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Menu default snapshot 1`] = `
<DocumentFragment>
<div
class="ventura menu"
>
<button
class="ventura menuItem"
type="button"
>
<span
class="label"
>
Option 1
</span>
</button>
<button
class="ventura menuItem"
type="button"
>
<svg
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
/>
</svg>
<span
class="label labelWithPrefixIcon"
>
Option 2
</span>
</button>
</div>
</DocumentFragment>
`;
68 changes: 68 additions & 0 deletions src/components/Popover/Popover.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
name: Popover
menu: Components
route: /popover
---

import { Playground, Props } from 'docz';
import { Popover, Menu, Button, Trash } from '../../index.ts';

# Popover

A Popover is used to display extra information or actions in a compact way.

## Best practices

- Make sure the correct popover events are covered, depending on the event. (Click, hover, focus, etc...).

## Examples

### Basic usage

<Playground>
{() => {
const [isVisible, setIsVisible] = React.useState(false);
const popoverContent = (
<Menu>
<Menu.Item>Option 1</Menu.Item>
<Menu.Item prefixIcon={<Trash />}>Option 2</Menu.Item>
</Menu>);
const onClose = () => {
setIsVisible(false);
}
return (
<Popover isVisible={isVisible} onClose={setIsVisible} content={popoverContent}>
<Button onClick={() => setIsVisible(!isVisible)}>Open basic modal</Button>
</ Popover>
);
}}

</Playground>

### Popover on hover

<Playground>
{() => {
const [isVisible, setIsVisible] = React.useState(false);
const popoverContent = (
<span>
A handy tooltip!
</span>);
const closePopover = () => {
setIsVisible(false);
}
const openPopover = () => {
setIsVisible(true);
}
return (
<Popover placement={'top'} isVisible={isVisible} onClose={setIsVisible} content={popoverContent}>
<Trash onMouseEnter={openPopover} onMouseLeave={closePopover}/>
</ Popover>
);
}}

</Playground>

## API

<Props of={Popover} />
10 changes: 10 additions & 0 deletions src/components/Popover/Popover.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.contentContainer {
border-radius: var(--border-radius-small);
background-color: var(--color-secondary);
box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.1);
z-index: 100;
}

.triggerContainer {
display: inline-block;
}
Loading

0 comments on commit 2e44f60

Please sign in to comment.