UNPKG

spartan-ui

Version:

React component library that focusses on getting the basics right leaving the design up to you

251 lines (226 loc) 11.3 kB
import React, { useState, useEffect, useRef } from 'react'; import FocusLock from 'react-focus-lock'; const Accordion = ({ as: Element = 'div', children, onClick, multi = false, defaultOpen = [], ...rest }) => { const [{ open }, setState] = useState({ open: defaultOpen, }); const handleClick = (id) => (e) => { if (open.includes(id)) { setState({ open: open.filter((openId) => openId !== id) }); } else { setState({ open: multi ? [...open, id] : [id] }); } if (onClick) { onClick(e, open); } e.preventDefault(); }; return (React.createElement(Element, Object.assign({}, rest), React.Children.map(children, child => React.isValidElement(child) ? React.cloneElement(child, { onClick: handleClick(child.props.id), open: open.includes(child.props.id), }) : child))); }; var detectKeyPress = (match, callback) => (e) => { if (match.filter((key) => (e.key && e.key === key.name) || (e.keyCode && e.keyCode === key.code)).length > 0) { callback(e); } }; const KEY = { ENTER: { name: 'Enter', code: 13, }, ESCAPE: { name: 'Escape', code: 27, }, SPACE: { name: 'Space', code: 32, }, }; const Accordion$1 = ({ as: Element = 'div', children, id, open = false, onClick, ...rest }) => { const handleClick = (e) => { onClick(e, !open); e.preventDefault(); }; const handleKeyDown = (e) => { handleClick(e); e.preventDefault(); }; const headingProps = { id: `${id}-heading`, onClick: handleClick, onKeyDown: detectKeyPress([KEY.ENTER, KEY.SPACE], handleKeyDown), role: 'button', tabIndex: 0, 'aria-controls': `${id}-body`, 'aria-expanded': open, }; const bodyProps = { hidden: !open, id: `${id}-body`, 'aria-hidden': !open, 'aria-labelledby': `${id}-heading`, }; return (React.createElement(Element, Object.assign({}, rest, { id: id }), children({ open, headingProps, bodyProps }))); }; const Alert = ({ as: Element = 'div', children, minor, ...rest }) => (React.createElement(Element, Object.assign({}, rest, { role: "alert", "aria-atomic": "true", "aria-live": minor ? 'polite' : undefined }), children)); const Breadcrumb = ({ as: Element = 'nav', children, 'aria-label': label = 'Breadcrumb', ...rest }) => (React.createElement(Element, Object.assign({}, rest, { role: "list", "aria-label": label }), children)); const BreadcrumbItem = ({ as: Element = 'span', children, active, ...rest }) => (React.createElement(Element, Object.assign({}, rest, { role: "listitem", "aria-current": active ? 'page' : undefined }), children)); const Button = ({ as: Element = 'button', children, active, type = 'button', disabled, ...rest }) => (React.createElement(Element, Object.assign({}, rest, { role: "button", type: type, disabled: disabled, "aria-disabled": disabled || undefined, "aria-pressed": active }), children)); const Dropdown = ({ as: Element = 'div', children, id, defaultOpen = false, ...rest }) => { const [{ open }, setState] = useState({ open: defaultOpen, }); const buttonProps = { id: `${id}-button`, onClick: () => { setState({ open: !open }); }, 'aria-controls': `${id}-menu`, 'aria-expanded': open, 'aria-haspopup': true, }; const menuProps = { hidden: !open, id: `${id}-menu`, role: 'list', 'aria-hidden': !open, 'aria-labelledby': `${id}-button`, }; const menuItemProps = { role: 'listitem', }; return (React.createElement(Element, Object.assign({}, rest, { id: id }), children({ open, buttonProps, menuProps, menuItemProps }))); }; const Field = ({ as = 'div', children, name, error = false, info = false, required = false, optional = false, optionList = false, ...rest }) => { const Element = as === 'div' && optionList ? 'fieldset' : as; const inputProps = { name, 'aria-describedby': [ error && `field-${name}-error`, info && `field-${name}-info`, ] .filter(Boolean) .join(' '), 'aria-invalid': error || undefined, required: required || (optional ? false : undefined), }; const labelProps = { as: optionList ? 'legend' : undefined, htmlFor: !optionList ? name : undefined, }; const ids = { error: `${name}-error`, info: `${name}-info`, }; return (React.createElement(Element, Object.assign({}, rest, { id: `field-${name}` }), children({ ids, inputProps, labelProps }))); }; const Input = ({ as: Element = 'input', name, id, type = 'text', defaultValue, disabled = false, placeholder, required, value, onBlur, onChange, ...rest }) => (React.createElement(Element, Object.assign({}, rest, { name: name, id: id, type: type, defaultValue: defaultValue, disabled: disabled, placeholder: placeholder, required: required, value: value, "aria-disabled": disabled || undefined, "aria-required": typeof required !== 'undefined' ? required : undefined, onBlur: onBlur, onChange: onChange }))); const InputOption = ({ children, type = 'checkbox', name, id, value, ...rest }) => React.createElement(Input, Object.assign({}, rest, { name: name, id: id || `${name}-${value}`, type: type })); const Select = ({ as: Element = 'select', name, id, defaultValue, disabled = false, multiple, options, placeholder, required, size, value, onBlur, onChange, ...rest }) => (React.createElement(Element, Object.assign({}, rest, { name: name, id: id, defaultValue: defaultValue, disabled: disabled, multiple: multiple, placeholder: placeholder, required: required, size: size, value: value, "aria-disabled": disabled || undefined, "aria-required": typeof required !== 'undefined' ? required : undefined, onBlur: onBlur, onChange: onChange }), options.map(option => (React.createElement("option", { key: option.value, value: option.value }, option.label))))); const TextArea = ({ as: Element = 'textarea', name, id, cols, defaultValue, disabled = false, placeholder, required, rows, spellCheck, value, wrap, onBlur, onChange, ...rest }) => (React.createElement(Element, Object.assign({}, rest, { name: name, id: id, cols: cols, defaultValue: defaultValue, disabled: disabled, placeholder: placeholder, required: required, rows: rows, spellCheck: spellCheck, value: value, wrap: wrap, "aria-disabled": disabled || undefined, "aria-required": typeof required !== 'undefined' ? required : undefined, onBlur: onBlur, onChange: onChange }))); const Label = ({ as: Element = 'label', children, htmlFor, ...rest }) => (React.createElement(Element, Object.assign({}, rest, { htmlFor: htmlFor }), children)); var useBodyScrollLock = (trigger) => { useEffect(() => { document.body.style.overflow = trigger ? 'hidden' : ''; return () => { document.body.style.overflow = ''; }; }, [trigger]); }; var useClickAway = (ref, handler, disabled = true) => { useEffect(() => { if (disabled) { return; } const listener = (e) => { if (!ref.current || ref.current.contains(e.target)) { return; } handler(e); }; document.addEventListener('mousedown', listener); return () => { document.removeEventListener('mousedown', listener); }; }, [ref, handler, disabled]); }; var useOnKeyPress = (key, handler, disabled = false) => { useEffect(() => { if (disabled) { return; } const handleKeyPress = detectKeyPress([key], handler); window.addEventListener('keydown', handleKeyPress); return () => { window.removeEventListener('keydown', handleKeyPress); }; }, [key, handler, disabled]); }; var useSetFocus = (ref, trigger) => { const originalFocusRef = useRef(); useEffect(() => { const { current } = ref; const { current: originalFocus } = originalFocusRef; if (!current) { return; } if (document.activeElement !== current) { originalFocusRef.current = document.activeElement; } if (trigger) { current.focus(); } else if (originalFocus) { originalFocus.focus(); } return () => { if (document.activeElement === current && originalFocus) { originalFocus.focus(); } }; }, [ref, trigger]); }; const Modal = ({ as: Element = 'div', children, id, open = false, alert = false, onClose, focusLockOptions, ...rest }) => { const modalRef = useRef(null); useBodyScrollLock(open); useClickAway(modalRef, onClose, alert); useOnKeyPress(KEY.ESCAPE, onClose, alert); const headingProps = { id: `${id}-heading`, }; return (React.createElement(React.Fragment, null, open && (React.createElement(FocusLock, Object.assign({ returnFocus: true }, focusLockOptions), React.createElement(Element, Object.assign({}, rest, { ref: modalRef, role: alert ? 'alertdialog' : 'dialog', id: id, hidden: !open, tabIndex: open ? 0 : -1, "aria-hidden": !open, "aria-labelledby": `${id}-heading` }), children({ headingProps })))))); }; const Progress = ({ as: Element = 'div', children, id, loading = false, value, min = 0, max = 100, label, ...rest }) => { const indicatorRef = useRef(null); useSetFocus(indicatorRef, loading); const hasMessage = !label; const isDeterminate = typeof value !== 'undefined'; const indicatorProps = { ref: indicatorRef, tabIndex: loading ? 0 : undefined, 'aria-busy': loading, 'aria-label': hasMessage ? undefined : label, 'aria-labelledby': hasMessage ? `${id}-message` : undefined, 'aria-valuemax': isDeterminate ? max : undefined, 'aria-valuemin': isDeterminate ? min : undefined, 'aria-valuenow': isDeterminate ? value : undefined, }; const messageProps = { id: `${id}-message`, role: 'staus', 'aria-atomic': true, 'aria-live': 'polite', }; return (React.createElement(Element, Object.assign({}, rest, { id: id }), loading && children({ indicatorProps, messageProps }))); }; const SRLabel = ({ as: Element = 'span', children, label, ...rest }) => (React.createElement(Element, Object.assign({}, rest, { "aria-label": label }), children)); const SRNumber = ({ as: Element = 'span', children, ...rest }) => (React.createElement(Element, Object.assign({}, rest, { "aria-label": children.replace(/[\s|\-|,]/g, '.').replace(/\d/g, ' $&') }), children)); export { Accordion, Accordion$1 as AccordionPanel, Alert, Breadcrumb, BreadcrumbItem, Button, Dropdown, Field, Input, InputOption, Label, Modal, Progress, SRLabel, SRNumber, Select, TextArea }; //# sourceMappingURL=index.dev.js.map