From 781da8cf69b9841ec787056fa86707942ce4ae57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Dacer?= Date: Tue, 10 Jan 2023 22:57:01 +0100 Subject: [PATCH] Create `InputGroup` component (#430) --- src/lib/components/Button/Button.jsx | 16 +- src/lib/components/InputGroup/InputGroup.jsx | 133 ++++++++++ src/lib/components/InputGroup/InputGroup.scss | 71 ++++++ .../InputGroup/InputGroupContext.js | 3 + src/lib/components/InputGroup/README.mdx | 228 ++++++++++++++++++ .../InputGroup/__tests__/InputGroup.test.jsx | 86 +++++++ src/lib/components/InputGroup/_theme.scss | 1 + src/lib/components/InputGroup/index.js | 2 + .../components/SelectField/SelectField.jsx | 18 +- .../components/SelectField/SelectField.scss | 14 ++ src/lib/components/TextField/TextField.jsx | 18 +- src/lib/components/TextField/TextField.scss | 14 ++ src/lib/index.js | 1 + src/lib/theme.scss | 6 + 14 files changed, 592 insertions(+), 19 deletions(-) create mode 100644 src/lib/components/InputGroup/InputGroup.jsx create mode 100644 src/lib/components/InputGroup/InputGroup.scss create mode 100644 src/lib/components/InputGroup/InputGroupContext.js create mode 100644 src/lib/components/InputGroup/README.mdx create mode 100644 src/lib/components/InputGroup/__tests__/InputGroup.test.jsx create mode 100644 src/lib/components/InputGroup/_theme.scss create mode 100644 src/lib/components/InputGroup/index.js diff --git a/src/lib/components/Button/Button.jsx b/src/lib/components/Button/Button.jsx index 1d39a9a7..7a3a0085 100644 --- a/src/lib/components/Button/Button.jsx +++ b/src/lib/components/Button/Button.jsx @@ -7,6 +7,7 @@ import { getRootSizeClassName } from '../_helpers/getRootSizeClassName'; import { resolveContextOrProp } from '../_helpers/resolveContextOrProp'; import { transferProps } from '../_helpers/transferProps'; import { ButtonGroupContext } from '../ButtonGroup'; +import { InputGroupContext } from '../InputGroup/InputGroupContext'; import getRootLabelVisibilityClassName from './helpers/getRootLabelVisibilityClassName'; import getRootPriorityClassName from './helpers/getRootPriorityClassName'; import styles from './Button.scss'; @@ -28,8 +29,9 @@ export const Button = React.forwardRef((props, ref) => { color, ...restProps } = props; - - const context = useContext(ButtonGroupContext); + const inputGroupContext = useContext(InputGroupContext); + const buttonGroupContext = useContext(ButtonGroupContext); + const primaryContext = buttonGroupContext ?? inputGroupContext; return ( /* No worries, `type` is always assigned correctly through props. */ @@ -39,20 +41,20 @@ export const Button = React.forwardRef((props, ref) => { className={classNames( styles.root, getRootPriorityClassName( - resolveContextOrProp(context && context.priority, priority), + resolveContextOrProp(buttonGroupContext && buttonGroupContext.priority, priority), styles, ), getRootColorClassName(color, styles), getRootSizeClassName( - resolveContextOrProp(context && context.size, size), + resolveContextOrProp(primaryContext && primaryContext.size, size), styles, ), getRootLabelVisibilityClassName(labelVisibility, styles), - resolveContextOrProp(context && context.block, block) && styles.isRootBlock, - context && styles.isRootGrouped, + resolveContextOrProp(buttonGroupContext && buttonGroupContext.block, block) && styles.isRootBlock, + primaryContext && styles.isRootGrouped, feedbackIcon && styles.hasRootFeedback, )} - disabled={resolveContextOrProp(context && context.disabled, disabled) || !!feedbackIcon} + disabled={resolveContextOrProp(buttonGroupContext && buttonGroupContext.disabled, disabled) || !!feedbackIcon} id={id} ref={ref} > diff --git a/src/lib/components/InputGroup/InputGroup.jsx b/src/lib/components/InputGroup/InputGroup.jsx new file mode 100644 index 00000000..68f7c000 --- /dev/null +++ b/src/lib/components/InputGroup/InputGroup.jsx @@ -0,0 +1,133 @@ +import PropTypes from 'prop-types'; +import React, { useContext } from 'react'; +import { Text } from '../Text'; +import { withGlobalProps } from '../../provider'; +import { classNames } from '../../utils/classNames'; +import { getRootValidationStateClassName } from '../_helpers/getRootValidationStateClassName'; +import { isChildrenEmpty } from '../_helpers/isChildrenEmpty'; +import { resolveContextOrProp } from '../_helpers/resolveContextOrProp'; +import { transferProps } from '../_helpers/transferProps'; +import { FormLayoutContext } from '../FormLayout'; +import { InputGroupContext } from './InputGroupContext'; +import styles from './InputGroup.scss'; + +export const InputGroup = ({ + children, + isLabelVisible, + label, + layout, + size, + validationTexts, + ...restProps +}) => { + const formLayoutContext = useContext(FormLayoutContext); + + if (isChildrenEmpty(children)) { + return null; + } + + const validationState = children.reduce( + (state, child) => { + if (state === 'invalid' || (state === 'warning' && child.props.validationState === 'valid')) { + return state; + } + return child.props.validationState ?? state; + }, + null, + ); + + return ( + + ); +}; + +InputGroup.defaultProps = { + children: null, + isLabelVisible: true, + layout: 'horizontal', + size: 'medium', + validationTexts: null, +}; + +InputGroup.propTypes = { + /** + * Supported elements to be grouped: + * * `Button` + * * `SelectField` + * * `TextField` + * + * If none are provided nothing is rendered. + */ + children: PropTypes.node, + /** + * If `false`, the label will be visually hidden (but remains accessible by assistive + * technologies). + */ + isLabelVisible: PropTypes.bool, + /** + * Input group label. + */ + label: PropTypes.string.isRequired, + /** + * Layout of the group. + * + * Ignored if the component is rendered within `FormLayout` component + * as the value is inherited in such case. + */ + layout: PropTypes.oneOf(['horizontal', 'vertical']), + /** + * Size of the `children` elements. + */ + size: PropTypes.oneOf(['small', 'medium', 'large']), + /** + * An array of validation messages to be displayed. + */ + validationTexts: PropTypes.node, +}; + +export const InputGroupWithGlobalProps = withGlobalProps(InputGroup, 'InputGroup'); + +export default InputGroupWithGlobalProps; diff --git a/src/lib/components/InputGroup/InputGroup.scss b/src/lib/components/InputGroup/InputGroup.scss new file mode 100644 index 00000000..8d8df674 --- /dev/null +++ b/src/lib/components/InputGroup/InputGroup.scss @@ -0,0 +1,71 @@ +@use "../../styles/tools/form-fields/box-field-elements"; +@use "../../styles/tools/form-fields/box-field-layout"; +@use "../../styles/tools/form-fields/box-field-sizes"; +@use "../../styles/tools/form-fields/foundation"; +@use "../../styles/tools/form-fields/variants"; +@use "../../styles/tools/accessibility"; +@use "theme"; + +.root { + @include box-field-elements.input-container(); +} + +.label { + @include foundation.label(); +} + +.inputGroup { + @include box-field-elements.input-container(); + + gap: theme.$gap; + max-width: none; +} + +.validationTexts { + @include foundation.help-text(); +} + +// States +.isRootStateInvalid { + @include variants.validation(invalid); +} + +.isRootStateValid { + @include variants.validation(valid); +} + +.isRootStateWarning { + @include variants.validation(warning); +} + +// Invisible label +.isLabelHidden { + @include accessibility.hide-text(); +} + +// Layouts +.isRootLayoutVertical, +.isRootLayoutHorizontal { + @include box-field-layout.vertical(); +} + +.isRootLayoutHorizontal { + @include box-field-layout.horizontal($has-min-tap-target: true); +} + +.isRootLayoutHorizontal .label { + width: max-content; +} + +// Sizes +.isRootSizeSmall { + @include box-field-sizes.size(small); +} + +.isRootSizeMedium { + @include box-field-sizes.size(medium); +} + +.isRootSizeLarge { + @include box-field-sizes.size(large); +} diff --git a/src/lib/components/InputGroup/InputGroupContext.js b/src/lib/components/InputGroup/InputGroupContext.js new file mode 100644 index 00000000..861e8a59 --- /dev/null +++ b/src/lib/components/InputGroup/InputGroupContext.js @@ -0,0 +1,3 @@ +import React from 'react'; + +export const InputGroupContext = React.createContext(null); diff --git a/src/lib/components/InputGroup/README.mdx b/src/lib/components/InputGroup/README.mdx new file mode 100644 index 00000000..e5f83aa0 --- /dev/null +++ b/src/lib/components/InputGroup/README.mdx @@ -0,0 +1,228 @@ +--- +name: InputGroup +menu: 'Layouts' +route: /components/input-group +--- + +# InputGroup + +InputGroup visually groups related Button, SelectField and TextField elements together. + +import { + Playground, + Props, +} from 'docz' +import Icon from '../../../docs/_components/Icon' +import { + Button, + InputGroup, + SelectField, + TextField, +} from '../..' + +## Basic Usage + +To implement the InputGroup component, you need to import it first: + +```js +import { InputGroup } from '@react-ui-org/react-ui'; +``` + +And use it: + + + {() => { + const [fruit, setFruit] = React.useState('apple'); + const options = [ + { + label: 'Apple', + value: 'apple', + }, + { + label: 'Pear', + value: 'pear', + }, + { + label: 'Cherry', + value: 'cherry', + }, + ]; + return ( + + setFruit(e.target.value)} + options={options} + value={fruit} + /> + +