diff --git a/src/Alert/index.js b/src/Alert/index.js index cdaa0d3..98c709d 100644 --- a/src/Alert/index.js +++ b/src/Alert/index.js @@ -1,48 +1,28 @@ -import React, { useRef, useEffect} from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; +import useEventListener from '../hooks/useEventListener'; -const Alert = (props) => { - const hxRef = useRef(null); - - useEffect(() => { - hxRef.current.addEventListener('open', props.onOpen); - hxRef.current.addEventListener('close', props.onClose); - return () => { - hxRef.current.removeEventListener('open', props.onOpen); - hxRef.current.removeEventListener('close', props.onClose); - }; - }, []); - - return ( - <> - {/* - Wrappping element needed: Otherwise when alert removes itself from DOM on close it confusing React - about where highest level parent element went, and will throw an error. - */} - - {props.children} - - - ); +const Alert = ({ onOpen, onClose, className, children, onDismiss, onSubmit, ...rest }) => { + const hxRef = useEventListener({ onDismiss, onSubmit }); + return ( + <> + {/* Wrappping element needed: Otherwise when alert removes itself from DOM on close, it will cause error */} + + {children} + + + ); }; Alert.propTypes = { - className: PropTypes.string, - children: PropTypes.node.isRequired, - type: PropTypes.string, - status: PropTypes.string, - cta: PropTypes.string, - persist: PropTypes.bool, - onOpen: PropTypes.func, - onClose: PropTypes.func + className: PropTypes.string, + children: PropTypes.node.isRequired, + type: PropTypes.string, + status: PropTypes.string, + cta: PropTypes.string, + persist: PropTypes.bool, + onDismiss: PropTypes.func, + onSubmit: PropTypes.func, }; export default Alert; diff --git a/src/Alert/stories.js b/src/Alert/stories.js index 8d4c625..e852182 100644 --- a/src/Alert/stories.js +++ b/src/Alert/stories.js @@ -2,28 +2,32 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { boolean, select, text } from '@storybook/addon-knobs/react'; import Alert from './index'; +import { action } from '@storybook/addon-actions'; const TYPES = { - 'info': 'info', - 'error': 'error', - 'success': 'success', - 'warning': 'warning' + info: 'info', + error: 'error', + success: 'success', + warning: 'warning', }; -storiesOf('Alert', module) - .add('All Knobs', () => { - let content = text('content', 'Nope! Nope! Nope! Nope! Nope!'); - let cta = text('cta', 'burn it'); - let status = text('status', 'spider'); - let persist = boolean('persist', false); - let type = select('type', TYPES, ''); +storiesOf('Alert', module).add('All Knobs', () => { + let content = text('content', 'Nope! Nope! Nope! Nope! Nope!'); + let cta = text('cta', 'burn it'); + let status = text('status', 'spider'); + let persist = boolean('persist', false); + let type = select('type', TYPES, ''); - return ( - {content} - ); - }); + return ( + + {content} + + ); +}); diff --git a/src/HxDiv/index.js b/src/HxDiv/index.js new file mode 100644 index 0000000..28b584f --- /dev/null +++ b/src/HxDiv/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const HxDiv = ({ className, ...rest }) => { + return ; +}; + +HxDiv.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired, + scroll: PropTypes.oneOf(['vertical', 'horizontal', 'both', 'none']), +}; + +export default HxDiv; diff --git a/src/Icon/index.js b/src/Icon/index.js index 14370e0..9e2baac 100644 --- a/src/Icon/index.js +++ b/src/Icon/index.js @@ -4,17 +4,13 @@ import PropTypes from 'prop-types'; export const Icons = Object.keys(HelixUI.Utils.ICONS); -class Icon extends React.Component { - render() { - const { type, ...props } = this.props; - - return ; - } -} +const Icon = ({ type, className, ...rest }) => { + return ; +}; -// additional configs here Icon.propTypes = { type: PropTypes.string, + className: PropTypes.string, }; export default Icon; diff --git a/src/Modal/index.js b/src/Modal/index.js new file mode 100644 index 0000000..d8c6b32 --- /dev/null +++ b/src/Modal/index.js @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import useEventListener from '../hooks/useEventListener'; +import { SIZES } from '../constants'; +import { wcBool } from '../utils'; + +const Modal = ({ onOpen, onClose, className, open, size, children, ...rest }) => { + const hxRef = useEventListener({ onOpen, onClose }); + return ( + + {children} + + ); +}; + +Modal.propTypes = { + id: PropTypes.string, + size: PropTypes.oneOf(SIZES), + children: PropTypes.node.isRequired, + className: PropTypes.string, + open: PropTypes.bool, + onClose: PropTypes.func, + onOpen: PropTypes.func, +}; + +export default Modal; diff --git a/src/Modal/stories.js b/src/Modal/stories.js new file mode 100644 index 0000000..8e57910 --- /dev/null +++ b/src/Modal/stories.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { boolean, select, text } from '@storybook/addon-knobs/react'; +import Modal from './index'; +import HxDiv from '../HxDiv'; +import Button from '../Button'; + +const SIZES = { + small: 'small', + medium: 'medium', + large: 'large', +}; + +storiesOf('Modal', module).add('All Knobs', () => { + let header = text('header in H3', 'Modal Header'); + let footer = text('footer', ''); + let open = boolean('open', true); + let size = select('size', SIZES, 'medium'); + let scroll = boolean('scroll', false); + + const smallText = + 'This is the body of a demo modal. Interaction with content behind this modal cannot take place until this modal is closed.\n'; + const lorumIpsum = ( +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. Gravida rutrum quisque non tellus. Sagittis vitae et leo duis + ut diam quam nulla. Diam vel quam elementum pulvinar etiam non. Pulvinar sapien et ligula + ullamcorper malesuada proin libero nunc. Ultricies integer quis auctor elit sed vulputate mi + sit amet. Egestas dui id ornare arcu odio ut. In iaculis nunc sed augue. Pellentesque + adipiscing commodo elit at imperdiet dui accumsan sit amet. Erat velit scelerisque in dictum + non. Auctor augue mauris augue neque gravida in fermentum et. Posuere sollicitudin aliquam + ultrices sagittis orci a scelerisque purus. Ullamcorper dignissim cras tincidunt lobortis + feugiat vivamus at augue. Tincidunt vitae semper quis lectus nulla. Purus ut faucibus pulvinar + 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. +

+ ); + const longText = [1, 2, 3, 4, 5].map(() => lorumIpsum); + const defaultFooter = ( + <> + + + + ); + + return ( + + {header && ( +
+

{header}

+
+ )} + + {smallText} + {scroll ? longText : null} + + {
{footer || defaultFooter}
} +
+ ); +}); diff --git a/src/Select/stories.js b/src/Select/stories.js index 10caca2..890f58d 100644 --- a/src/Select/stories.js +++ b/src/Select/stories.js @@ -27,11 +27,7 @@ storiesOf('Select', module) const Demo = (props) => { return (
-
diff --git a/src/Tooltip/index.js b/src/Tooltip/index.js index b8181b5..85c4b70 100644 --- a/src/Tooltip/index.js +++ b/src/Tooltip/index.js @@ -1,7 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; - -import positions from './positions.js'; +import { POSITIONS } from '../constants'; const Tooltip = ({ children, id, position }) => { return ( @@ -14,7 +13,7 @@ const Tooltip = ({ children, id, position }) => { Tooltip.propTypes = { children: PropTypes.node.isRequired, id: PropTypes.string.isRequired, - position: PropTypes.oneOf(positions), + position: PropTypes.oneOf(POSITIONS), }; Tooltip.defaultProps = { diff --git a/src/Tooltip/positions.js b/src/constants.js similarity index 65% rename from src/Tooltip/positions.js rename to src/constants.js index 3ff927e..7aa835d 100644 --- a/src/Tooltip/positions.js +++ b/src/constants.js @@ -1,4 +1,10 @@ -const positions = [ +export const SIZES = { + small: 'hxSm', + medium: 'hxMd', + large: 'hxLg', +}; + +export const POSITIONS = [ 'top-left', 'top-center', 'top-right', @@ -12,5 +18,3 @@ const positions = [ 'left-middle', 'left-top', ]; - -export default positions; diff --git a/src/hooks/useEventListener.js b/src/hooks/useEventListener.js new file mode 100644 index 0000000..8a387f0 --- /dev/null +++ b/src/hooks/useEventListener.js @@ -0,0 +1,25 @@ +import { useEffect, useRef } from 'react'; + +const handlerNameToEvent = (handlerName) => handlerName.replace(/^on/, '').toLowerCase(); + +/** + * Adds event handlers like onOpen or onClose to event listeners: 'open' or 'close' + * @param {object} eventHandlers such as onClose, onOpen, ...etc + * @param {React.MutableRefObject} ref + * @return {React.MutableRefObject} + */ +function useEventListener(eventHandlers = {}, ref) { + const theRef = ref || useRef(null); + useEffect(() => { + Object.entries(eventHandlers).forEach(([handlerName, eventHandler], key) => { + theRef.current.addEventListener(handlerNameToEvent(handlerName), eventHandler); + }); + return () => { + Object.entries(eventHandlers).forEach(([handlerName, eventHandler], key) => { + theRef.current.removeEventListener(handlerNameToEvent(handlerName), eventHandler); + }); + }; + }, []); + return theRef; +} +export default useEventListener; diff --git a/src/index.mjs b/src/index.mjs index ea17a6c..9043e83 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -2,9 +2,15 @@ import Button from './Button'; import Alert from './Alert'; import Icon from './Icon'; +import Tooltip from './Tooltip'; +import Modal from './Modal'; +import Select from './Select'; export default { - Button, - Icon, - Alert + Button, + Alert, + Icon, + Modal, + Select, + Tooltip }; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..d04106a --- /dev/null +++ b/src/utils.js @@ -0,0 +1,7 @@ +/** + * Because Web components attributes convert booleans to strings + * this stops values like false from being converted to "false" and then evaluating as true. + * @param {boolean} bool + * @return {*} + */ +export const wcBool = (bool) => bool ? true : null;