Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 20 additions & 40 deletions src/Alert/index.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,28 @@
import React, { useRef, useEffect} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import useEventListener from '../hooks/useEventListener';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome idea 👍


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.
*/}
<hx-alert
type={props.type}
status={props.status}
persist={props.persist}
cta={props.cta}
className={props.className}
id={props.id}
ref={hxRef}
>
{props.children}
</hx-alert>
</>
);
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 */}
<hx-alert class={className} ref={hxRef} {...rest}>
{children}
</hx-alert>
</>
);
};

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;
44 changes: 24 additions & 20 deletions src/Alert/stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Alert
{ ...( cta && { cta }) }
{ ...( status && { status }) }
{ ...( persist && { persist }) }
{ ...( type && { type }) }
>{content}</Alert>
);
});
return (
<Alert
{...(cta && { cta })}
{...(status && { status })}
{...(persist && { persist })}
{...(type && { type })}
onDismiss={action('onDismiss')}
onSubmit={action('onSubmit')}
>
{content}
</Alert>
);
});
14 changes: 14 additions & 0 deletions src/HxDiv/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';

const HxDiv = ({ className, ...rest }) => {
return <hx-div class={className} {...rest} />;
};

HxDiv.propTypes = {
className: PropTypes.string,
children: PropTypes.node.isRequired,
scroll: PropTypes.oneOf(['vertical', 'horizontal', 'both', 'none']),
};

export default HxDiv;
12 changes: 4 additions & 8 deletions src/Icon/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <hx-icon type={type} {...props}></hx-icon>;
}
}
const Icon = ({ type, className, ...rest }) => {
return <hx-icon type={type} {...rest} class={className}></hx-icon>;
};

// additional configs here
Icon.propTypes = {
type: PropTypes.string,
className: PropTypes.string,
};

export default Icon;
32 changes: 32 additions & 0 deletions src/Modal/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<hx-modal
class={classNames(className, SIZES[size])}
ref={hxRef}
open={wcBool(open)}
{...rest}
>
{children}
</hx-modal>
);
};

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;
67 changes: 67 additions & 0 deletions src/Modal/stories.js
Original file line number Diff line number Diff line change
@@ -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 = (
<p>
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.
</p>
);
const longText = [1, 2, 3, 4, 5].map(() => lorumIpsum);
const defaultFooter = (
<>
<Button variant="primary">Confirm</Button>
<Button variant="tertiary">Cancel</Button>
</>
);

return (
<Modal
{...(open && { open })}
{...(size && { size })}
open={open}
onOpen={action('onOpen')}
onClose={action('onClose')}
>
{header && (
<header>
<h3>{header}</h3>
</header>
)}
<HxDiv scroll={scroll && 'vertical'}>
{smallText}
{scroll ? longText : null}
</HxDiv>
{<footer>{footer || defaultFooter}</footer>}
</Modal>
);
});
6 changes: 1 addition & 5 deletions src/Select/stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@ storiesOf('Select', module)
const Demo = (props) => {
return (
<div style={{ padding: 25, width: 500 }}>
<Select
label="Select"
onChange={action(`selected`)}
{...props}
>
<Select label="Select" onChange={action(`selected`)} {...props}>
<Options />
</Select>
</div>
Expand Down
5 changes: 2 additions & 3 deletions src/Tooltip/index.js
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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 = {
Expand Down
10 changes: 7 additions & 3 deletions src/Tooltip/positions.js → src/constants.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
const positions = [
export const SIZES = {
small: 'hxSm',
medium: 'hxMd',
large: 'hxLg',
};

export const POSITIONS = [
'top-left',
'top-center',
'top-right',
Expand All @@ -12,5 +18,3 @@ const positions = [
'left-middle',
'left-top',
];

export default positions;
25 changes: 25 additions & 0 deletions src/hooks/useEventListener.js
Original file line number Diff line number Diff line change
@@ -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;
12 changes: 9 additions & 3 deletions src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
7 changes: 7 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -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;