diff --git a/package.json b/package.json index 600c991..0a07e76 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ "react-dom": "^16.6.3" }, "dependencies": { - "classnames": "^2.2.6" + "classnames": "^2.2.6", + "use-onclickoutside": "^0.3.1" }, "husky": { "hooks": { diff --git a/src/Choice Tile/index.js b/src/ChoiceTile/index.js similarity index 96% rename from src/Choice Tile/index.js rename to src/ChoiceTile/index.js index 269d45b..88e2ef2 100644 --- a/src/Choice Tile/index.js +++ b/src/ChoiceTile/index.js @@ -52,7 +52,7 @@ ChoiceTile.propTypes = { invalid: PropTypes.bool, name: PropTypes.string.isRequired, onChange: PropTypes.func, - size: PropTypes.oneOf(SIZES), + size: PropTypes.oneOf(Object.keys(SIZES)), subdued: PropTypes.bool, title: PropTypes.string.isRequired, }; diff --git a/src/Choice Tile/stories.js b/src/ChoiceTile/stories.js similarity index 100% rename from src/Choice Tile/stories.js rename to src/ChoiceTile/stories.js diff --git a/src/Drawer/index.js b/src/Drawer/index.js index f8eef93..2b08614 100644 --- a/src/Drawer/index.js +++ b/src/Drawer/index.js @@ -20,7 +20,7 @@ Drawer.propTypes = { onClose: PropTypes.func, onOpen: PropTypes.func, open: PropTypes.bool, - size: PropTypes.oneOf(SIZES), + size: PropTypes.oneOf(Object.keys(SIZES)), }; Drawer.defaultProps = { diff --git a/src/Modal/index.js b/src/Modal/index.js index 9d2e8c5..f6a5dee 100644 --- a/src/Modal/index.js +++ b/src/Modal/index.js @@ -16,7 +16,7 @@ const Modal = ({ onOpen, onClose, className, open, size, children, ...rest }) => Modal.propTypes = { id: PropTypes.string, - size: PropTypes.oneOf(SIZES), + size: PropTypes.oneOf(Object.keys(SIZES)), children: PropTypes.node.isRequired, className: PropTypes.string, open: PropTypes.bool, diff --git a/src/Search/SearchAssist.js b/src/Search/SearchAssist.js new file mode 100644 index 0000000..09e7a15 --- /dev/null +++ b/src/Search/SearchAssist.js @@ -0,0 +1,61 @@ +import PropTypes from 'prop-types'; +import React, { useState, useRef } from 'react'; +import Search from './index'; +import SearchAssistance from './SearchAssistance'; +import { wcBool } from '../utils'; +import { POSITIONS } from '../constants'; +import useClickOutside from 'use-onclickoutside'; + +/** + * @see https://helixdesignsystem.github.io/helix-ui/components/search/ + */ +const SearchAssist = ({ children, onFocus, onBlur, position, ...rest }) => { + const [open, setOpen] = useState(false); + + const searchRef = useRef(); + useClickOutside(searchRef, () => setOpen(false)); + + const hasChildren = React.Children.toArray(children).filter((c) => c).length > 0; + + return ( +
+ { + setOpen(true); + onFocus && onFocus(e); + }} + wrapperId={`${rest.id}-hx-search-control`} + /> + {hasChildren && ( + setOpen(false)} + > + {children} + + )} +
+ ); +}; + +SearchAssist.propTypes = { + children: PropTypes.node.isRequired, + position: PropTypes.oneOf(POSITIONS), + className: PropTypes.string, + clearLabel: PropTypes.string, + label: PropTypes.string, + id: PropTypes.string.isRequired, + wrapperId: PropTypes.string, + disabled: PropTypes.bool, + onChange: PropTypes.func, + onFocus: PropTypes.func, + onBlur: PropTypes.func, + onClear: PropTypes.func, + optional: PropTypes.bool, + required: PropTypes.bool, +}; + +export default SearchAssist; diff --git a/src/Search/SearchAssistance.js b/src/Search/SearchAssistance.js new file mode 100644 index 0000000..8175083 --- /dev/null +++ b/src/Search/SearchAssistance.js @@ -0,0 +1,23 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { POSITIONS } from '../constants'; + +/** + * @see https://helixdesignsystem.github.io/helix-ui/elements/hx-search-assistance/ + */ +const SearchAssistance = ({ children, className, relativeTo, ...rest }) => { + return ( + + {children} + + ); +}; + +SearchAssistance.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string, + relativeTo: PropTypes.string.isRequired, + position: PropTypes.oneOf(POSITIONS), +}; + +export default SearchAssistance; diff --git a/src/Search/index.js b/src/Search/index.js new file mode 100644 index 0000000..42f8e59 --- /dev/null +++ b/src/Search/index.js @@ -0,0 +1,64 @@ +import classnames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import Icon from '../Icon'; +import { wcBool } from '../utils'; + +/** + * @see https://helixdesignsystem.github.io/helix-ui/components/search/ + */ +const Search = ({ + children, + disabled, + id, + label, + className, + clearLabel, + onClear, + optional, + required, + wrapperId, + ...rest +}) => { + return ( + + + + + {label && ( + + )} + + ); +}; + +Search.propTypes = { + className: PropTypes.string, + clearLabel: PropTypes.string, + label: PropTypes.string, + id: PropTypes.string.isRequired, + wrapperId: PropTypes.string, + disabled: PropTypes.bool, + onChange: PropTypes.func, + onFocus: PropTypes.func, + onBlur: PropTypes.func, + onClear: PropTypes.func, + optional: PropTypes.bool, + required: PropTypes.bool, +}; + +Search.defaultProps = { + clearLabel: 'Clear search', +}; + +export default Search; diff --git a/src/Search/stories.js b/src/Search/stories.js new file mode 100644 index 0000000..5859769 --- /dev/null +++ b/src/Search/stories.js @@ -0,0 +1,72 @@ +import { action } from '@storybook/addon-actions'; +import { boolean, text, select } from '@storybook/addon-knobs'; +import { storiesOf } from '@storybook/react'; +import React, { useState } from 'react'; +import Search from '../Search'; +import SearchAssist from './SearchAssist'; +import { InputContainer } from '../storyUtils'; +import { POSITIONS } from '../constants'; + +storiesOf('Search', module) + .add('All Knobs', () => { + let disabled = boolean('disabled', false); + let label = text('label', ''); + let optional = boolean('optional', false); + let required = boolean('required', false); + let position = select('positions', POSITIONS, 'bottom-center'); + + return ( + + + + ); + }) + .add('SearchAssist', () => { + let disabled = boolean('disabled', false); + let label = text('label', 'Search'); + let optional = boolean('optional', false); + let required = boolean('required', false); + let position = select('positions', POSITIONS, 'bottom-center'); + const [value, setValue] = useState(''); + + return ( + + setValue(e.target.value)} + value={value} + onClear={(e) => { + action('onClear'); + setValue(''); + }} + onFocus={(e) => action('onFocus')} + onBlur={(e) => action('onBlur')} + autocomplete="off" + {...(disabled && { disabled })} + {...(label && { label })} + {...(optional && { optional })} + {...(required && { required })} + {...(position && { position })} + > +
Search for "{value}"
+
+
Category Header
+ {POSITIONS.filter((p) => p.search(value) !== -1).map((item) => ( + + ))} +
+
+
+ ); + }); diff --git a/src/index.mjs b/src/index.mjs index 779609e..de07366 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -1,20 +1,26 @@ /* Export helix-react definition */ -import Button from './Button'; import Alert from './Alert'; +import Button from './Button'; +import ChoiceTile from './ChoiceTile'; import Drawer from './Drawer'; import Icon from './Icon'; import Modal from './Modal'; import Popover from './Popover'; import Tooltip from './Tooltip'; import Select from './Select'; +import Search from './Search'; +import SearchAssist from './Search/SearchAssist'; export default { - Button, Alert, + Button, + ChoiceTile, Drawer, Icon, Modal, Popover, Select, Tooltip, + Search, + SearchAssist }; diff --git a/src/storyUtils.js b/src/storyUtils.js index bf6075a..85b6dfc 100644 --- a/src/storyUtils.js +++ b/src/storyUtils.js @@ -1,3 +1,5 @@ +import React from 'react'; + export const getShortText = () => `lorem ipsum dolor sir amet`; export const getLongText = () => ` @@ -13,3 +15,5 @@ export const getLongText = () => ` elementum integer enim neque volutpat. Etiam sit amet nisl purus in mollis nunc. Diam sit amet nisl suscipit. Nulla pharetra diam sit amet nisl. Arcu odio ut sem nulla. `; + +export const InputContainer = ({ children }) =>
{children}
; diff --git a/yarn.lock b/yarn.lock index 0122082..d2e539c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1896,6 +1896,11 @@ aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" +are-passive-events-supported@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/are-passive-events-supported/-/are-passive-events-supported-1.1.1.tgz#3db180a1753a2186a2de50a32cded3ac0979f5dc" + integrity sha512-5wnvlvB/dTbfrCvJ027Y4L4gW/6Mwoy1uFSavney0YO++GU+0e/flnjiBBwH+1kh7xNCgCOGvmJC3s32joYbww== + are-we-there-yet@~1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" @@ -7461,6 +7466,19 @@ use-callback-ref@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.1.tgz#898759ccb9e14be6c7a860abafa3ffbd826c89bb" +use-latest@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.0.0.tgz#c86d2e4893b15f27def69da574a47136d107facb" + integrity sha512-CxmFi75KTXeTIBlZq3LhJ4Hz98pCaRKZHCpnbiaEHIr5QnuHvH8lKYoluPBt/ik7j/hFVPB8K3WqF6mQvLyQTg== + +use-onclickoutside@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/use-onclickoutside/-/use-onclickoutside-0.3.1.tgz#fdd723a6a499046b6bc761e4a03af432eee5917b" + integrity sha512-aahvbW5+G0XJfzj31FJeLsvc6qdKbzeTsQ8EtkHHq5qTg6bm/qkJeKLcgrpnYeHDDbd7uyhImLGdkbM9BRzOHQ== + dependencies: + are-passive-events-supported "^1.1.0" + use-latest "^1.0.0" + use-sidecar@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.2.tgz#e72f582a75842f7de4ef8becd6235a4720ad8af6"