Skip to content

Commit

Permalink
Create InputGroup component (#430)
Browse files Browse the repository at this point in the history
  • Loading branch information
dacerondrej committed Mar 3, 2023
1 parent ff6205b commit 5eeea23
Show file tree
Hide file tree
Showing 14 changed files with 592 additions and 19 deletions.
16 changes: 9 additions & 7 deletions src/lib/components/Button/Button.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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. */
Expand All @@ -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}
>
Expand Down
133 changes: 133 additions & 0 deletions src/lib/components/InputGroup/InputGroup.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<label
className={classNames(
styles.root,
resolveContextOrProp(formLayoutContext && formLayoutContext.layout, layout) === 'horizontal'
? styles.isRootLayoutHorizontal
: styles.isRootLayoutVertical,
getRootValidationStateClassName(validationState, styles),
)}
>
<div
className={classNames(
styles.label,
!isLabelVisible && styles.isLabelHidden,
)}
>
{label}
</div>
<div className={styles.field}>
<div
{...transferProps(restProps)}
className={styles.inputGroup}
>
<InputGroupContext.Provider
value={{
layout,
size,
}}
>
{children}
</InputGroupContext.Provider>
</div>
{validationTexts && (
<div
className={styles.validationTexts}
>
{validationTexts.map((validationText) => (
<Text blockLevel key={validationText}>
{validationText}
</Text>
))}
</div>
)}
</div>
</label>
);
};

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;
71 changes: 71 additions & 0 deletions src/lib/components/InputGroup/InputGroup.scss
Original file line number Diff line number Diff line change
@@ -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);
}
3 changes: 3 additions & 0 deletions src/lib/components/InputGroup/InputGroupContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React from 'react';

export const InputGroupContext = React.createContext(null);
Loading

0 comments on commit 5eeea23

Please sign in to comment.