diff --git a/src/components/Dropdown/Menu/Item/Item.tsx b/src/components/Dropdown/Menu/Item/Item.tsx index b368d0af..7d44ec09 100644 --- a/src/components/Dropdown/Menu/Item/Item.tsx +++ b/src/components/Dropdown/Menu/Item/Item.tsx @@ -1,63 +1,62 @@ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; import styles from './Item.module.css'; -export interface ItemProps { - className?: string; - href?: string; - onClick?: () => void; -} - -const Item: React.FC = ({ - className = '', - href = null, - onClick = null, - ...passedProps -}) => { - if (href) { - // The anchor's content is provided by passedProps - /* eslint-disable jsx-a11y/anchor-has-content */ +export type ButtonElementProps = React.ButtonHTMLAttributes; + +type AnchorElementProps = React.AnchorHTMLAttributes; + +type DivElementProps = React.AllHTMLAttributes; + +export type ItemProps = + | ButtonElementProps + | AnchorElementProps + | DivElementProps; + +// Guard to check if href exists in props +const hasHref = (props: ItemProps): props is AnchorElementProps => + 'href' in props && props.href !== undefined; + +const Item: React.FC = ({ className = '', ...passedProps }) => { + /** + * If an `href` prop is passed, the rendered element automatically renders + * as a bare element with default styles + */ + if (hasHref(passedProps as AnchorElementProps)) { return ( + // eslint-disable-next-line jsx-a11y/anchor-has-content ); - /* eslint-enable jsx-a11y/anchor-has-content */ } - if (onClick) { + /** + * If an `onClick` prop is passed, the rendered element automatically renders + * as a bare ) : ( - React.Children.map(initChildren, (child) => - React.cloneElement(child, { + React.Children.map(initChildren, (c) => { + const child = c as ReactElement; + return React.cloneElement(child, { ...toggleProps, - className: classNames( - child && child.props && child.props.className, - toggleProps.className - ), + className: classNames(child?.props?.className, toggleProps.className), 'data-is-open': isOpen, - onClick: (event) => { + onClick: (event: MouseEvent) => { onToggle(event); - if (child && child.props && child.props.onClick) { + if (child?.props?.onClick) { child.props.onClick(event); } }, - }) - ) + }); + }) ); }, [initChildren, isOpen, onToggle, toggleProps]); @@ -93,6 +114,7 @@ const EasyDropdown = ({ @@ -111,9 +133,11 @@ const EasyDropdown = ({ {menuItemGroups[group].map( ({ children: itemChildren, label, ...itemProps }) => ( { + onClick={( + event: React.MouseEvent + ) => { // Clicking a menu item has the side effect of closing the menu if (!isControlled) { setUncontrolledIsOpen(false); @@ -137,36 +161,6 @@ const EasyDropdown = ({ ); }; -EasyDropdown.propTypes = { - /** Pass a custom node if you want to control the toggle fully. */ - children: PropTypes.node, - /** Totally optional, for additional styling */ - className: PropTypes.string, - /** If defaultIsOpen is provided, the component will run in "uncontrolled" mode */ - defaultIsOpen: PropTypes.bool, - disabled: PropTypes.bool, - /** Without `defaultIsOpen`, `isOpen` fully controls the dropdown state */ - isOpen: PropTypes.bool, - /** An array of items that comprise the menu */ - menuItems: PropTypes.arrayOf( - PropTypes.shape({ - /** Children will be rendered instead of the label, if provided */ - children: PropTypes.node, - /** If you provide group IDs, the menu items will be grouped with dividers between them. */ - group: PropTypes.string, - /** This will be the array key and the fallback contents */ - label: PropTypes.string.isRequired, - onClick: PropTypes.func, - }) - ), - menuProps: PropTypes.object, - /** Without `defaultIsOpen`, `onToggle` is the only way to set state. With it, it's a convenience callback. */ - onToggle: PropTypes.func, - toggleProps: PropTypes.shape({ - className: PropTypes.string, - }), -}; - EasyDropdown.defaultProps = { children: undefined, className: '', diff --git a/src/components/EasyDropdown/index.ts b/src/components/EasyDropdown/index.ts new file mode 100644 index 00000000..4b15bbdd --- /dev/null +++ b/src/components/EasyDropdown/index.ts @@ -0,0 +1 @@ +export { default } from './EasyDropdown'; diff --git a/src/components/EasyDropdown/story.js b/src/components/EasyDropdown/story.js deleted file mode 100644 index 59f6cf41..00000000 --- a/src/components/EasyDropdown/story.js +++ /dev/null @@ -1,162 +0,0 @@ -import React from 'react'; -import { storiesOf } from '@storybook/react'; -import { object, select } from '@storybook/addon-knobs'; -import { action } from '@storybook/addon-actions'; - -import Button from '../Button'; -import EasyDropdown from './'; -import Readme from './README.md'; - -const defaultIsOpenOptions = ['true', 'false', 'undefined']; - -storiesOf('Galaxies/EasyDropdown', module) - .addParameters({ - readme: { - sidebar: Readme, - }, - }) - .add( - 'Overview', - () => { - const customMenuItems = object('menuItems', [ - { label: 'Item 1', onClick: 'IRL your function will go here' }, - { - label: 'Item 2', - group: 'Group 1', - onClick: 'IRL your function will go here', - }, - { - label: 'Item 3', - group: 'Group 1', - onClick: 'IRL your function will go here', - }, - { label: 'Item 4', onClick: 'IRL your function will go here' }, - { label: 'Item 5', onClick: 'IRL your function will go here' }, - { - label: 'Item 5', - group: 'Group 2', - onClick: 'IRL your function will go here', - }, - { - label: 'Item 6', - group: 'Group 3', - onClick: 'IRL your function will go here', - }, - ]); - - const customDefaultIsOpen = select( - 'defaultIsOpen', - defaultIsOpenOptions, - 'false' - ); - - const defaultIsOpen = - customDefaultIsOpen === 'undefined' - ? undefined - : JSON.parse(customDefaultIsOpen); - - const menuItems = customMenuItems.map(a => ({ - ...a, - onClick: action(a.label), - })); - - return ( - - Toggle me! - - ); - }, - { - info: { - inline: true, - source: true, - }, - } - ) - .add( - 'Examples', - () => { - return ( -
- {} }, - { label: "I'm in a group", onClick: () => {}, group: 'Group 1' }, - { label: 'Me too', onClick: () => {}, group: 'Group 1' }, - { label: 'Also me', onClick: () => {}, group: 'Group 1' }, - ]} - defaultIsOpen={false} - > - Simple config - -
-
- {} }]} - defaultIsOpen={false} - > - Button with addon - - -
-
- {} }]} - defaultIsOpen={false} - style={{ width: '100%' }} - toggleProps={{ style: { width: '100%' } }} - > - Full-width dropdown - - -
-
- {} }]} - defaultIsOpen={false} - > - - -
-
- - I'm fancy - - ), - onClick: action('Custom menu item onClick'), - style: { padding: '0' }, - }, - ]} - defaultIsOpen={false} - > - Custom button in the dropdown! - -
- ); - }, - { - info: { - inline: true, - source: true, - }, - } - ); diff --git a/src/components/EasyDropdown/story.tsx b/src/components/EasyDropdown/story.tsx new file mode 100644 index 00000000..1f4867bd --- /dev/null +++ b/src/components/EasyDropdown/story.tsx @@ -0,0 +1,170 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { object, select } from '@storybook/addon-knobs'; +import { action } from '@storybook/addon-actions'; + +import Button from '../Button'; +import EasyDropdown from '.'; +import Readme from './README.md'; +import { Tailwind, Wrap } from '../../stories/storybook-helpers'; + +const defaultIsOpenOptions = ['true', 'false', 'undefined']; + +storiesOf('Galaxies/EasyDropdown', module) + .addParameters({ + readme: { + sidebar: Readme, + }, + }) + .add( + 'Overview', + () => { + const customMenuItems = object('menuItems', [ + { label: 'Item 1', onClick: 'IRL your function will go here' }, + { + label: 'Item 2', + group: 'Group 1', + onClick: 'IRL your function will go here', + }, + { + label: 'Item 3', + group: 'Group 1', + onClick: 'IRL your function will go here', + }, + { label: 'Item 4', onClick: 'IRL your function will go here' }, + { label: 'Item 5', onClick: 'IRL your function will go here' }, + { + label: 'Item 5', + group: 'Group 2', + onClick: 'IRL your function will go here', + }, + { + label: 'Item 6', + group: 'Group 3', + onClick: 'IRL your function will go here', + }, + ]); + + const customDefaultIsOpen = select( + 'defaultIsOpen', + defaultIsOpenOptions, + 'false' + ); + + const defaultIsOpen = + customDefaultIsOpen === 'undefined' + ? undefined + : JSON.parse(customDefaultIsOpen); + + const menuItems = customMenuItems.map((a) => ({ + ...a, + onClick: action(a.label), + })); + + return ( + + Toggle me! + + ); + }, + { + info: { + inline: true, + source: true, + }, + } + ) + .add( + 'Examples', + () => { + return ( +
+ + + {} }, + { + label: "I'm in a group", + group: 'Group 1', + }, + { label: 'Me too', onClick: () => {}, group: 'Group 1' }, + { label: 'Also me', onClick: () => {}, group: 'Group 1' }, + ]} + defaultIsOpen={false} + > + Simple config + + + + + {} }]} + defaultIsOpen={false} + > + Button with addon + + + + + + {} }]} + defaultIsOpen={false} + toggleProps={{ className: 'w-full' }} + > + Full-width dropdown + + + + + + {} }]} + defaultIsOpen={false} + > + + + + + + + I'm fancy + + ), + onClick: action('Custom menu item onClick'), + className: 'p-2', + }, + ]} + defaultIsOpen={false} + > + Custom button in the dropdown! + + +
+ ); + }, + { + info: { + inline: true, + source: true, + }, + } + ); diff --git a/src/components/EasyPill/EasyPill.tsx b/src/components/EasyPill/EasyPill.tsx index 088c1b69..eda91e2f 100644 --- a/src/components/EasyPill/EasyPill.tsx +++ b/src/components/EasyPill/EasyPill.tsx @@ -6,22 +6,16 @@ import Icon from '../Icon'; import Pill from '../Pill'; import { PillProps } from '../Pill/Pill'; import styles from './EasyPill.module.css'; - -type EasyPillActions = { - children?: React.ReactNode; - label: string; - onClick: Function; - group?: string; -}; +import type { MenuItem } from '../EasyDropdown/EasyDropdown'; interface EasyPillProps extends PillProps { - actions?: EasyPillActions[]; + actions?: MenuItem[]; children?: React.ReactNode; onDelete?: (...args) => void; } interface EasyPillDropdownProps { - actions: EasyPillActions[]; + actions: MenuItem[]; onDelete?: (...args) => void; }