Skip to content

Commit

Permalink
feat(Select): reintroduce displayValue props to customize Header
Browse files Browse the repository at this point in the history
  • Loading branch information
Rémi Jarasson committed Dec 31, 2019
1 parent 1ed37bd commit 342f31e
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 43 deletions.
41 changes: 13 additions & 28 deletions src/Select/Header.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React, { Children } from 'react';
import PropTypes from 'prop-types';

import { HeaderButton, HeaderContent } from './elements';
Expand All @@ -9,17 +9,17 @@ import { HeaderButton, HeaderContent } from './elements';
* This component is in charge of displaying
* a search bar
*
* @param {array} values // The currently selected values
* @param {function} onClick // Callback function called when button is clicked
* @param {bool} isOpen // When popover is open
* @param {string} placeholder // Placeholder of button when no value is selected
* @param {bool} disabled - If button is disabled
* @param {function} onClick - Callback function called when button is clicked
* @param {bool} isOpen - When popover is open
* @param {string} placeholder - Placeholder of button when no value is selected
* @param {node} children - The content of Header
*
* @return {jsx}
*/
const Header = ({ disabled, options, displayValue, values, onClick, isOpen, placeholder }) => {
const selectedOptions = values
.map(value => options.find(option => option.value === value))
.filter(Boolean);
const Header = ({ disabled, onClick, isOpen, placeholder, children }) => {
// Remove empty children to ensure placeholder is display if empty
const filteredChildren = Children.toArray(children).filter(Boolean);

return (
<HeaderButton
Expand All @@ -29,42 +29,27 @@ const Header = ({ disabled, options, displayValue, values, onClick, isOpen, plac
aria-expanded={isOpen}
data-testid="select-button"
>
<HeaderContent>
{displayValue}
{!displayValue &&
(values.length
? selectedOptions.map(({ value, label }, index) => (
<Fragment key={value}>
{index > 0 && ', '}
{label}
</Fragment>
))
: placeholder)}
</HeaderContent>
<HeaderContent>{filteredChildren.length ? filteredChildren : placeholder}</HeaderContent>
</HeaderButton>
);
};

const { array, bool, func, node, oneOfType, string } = PropTypes;
const { bool, func, node, string } = PropTypes;

/** Prop types. */
Header.propTypes = {
children: node,
disabled: bool,
options: array,
isOpen: bool,
values: array,
displayValue: oneOfType([node, string]),
onClick: func.isRequired,
placeholder: string,
};

/** Default props. */
Header.defaultProps = {
children: null,
disabled: false,
isOpen: false,
options: [],
values: [],
displayValue: null,
placeholder: '',
};

Expand Down
23 changes: 15 additions & 8 deletions src/Select/__stories__/Select.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,20 @@ storiesOf('Select', module)
const onToggle = action('onToggle');
const onChangeAction = action('onChange');

const HeaderComponent = ({ values, ...rest }) => (
<Select.Header
displayValue={values.length > 1 ? `${values.length} selected` : null}
values={values}
{...rest}
/>
);
const displayValue = (selected, values, options) =>
values.length === options.length ? (
<span
css={`
color: blue;
`}
>
All selected
</span>
) : values.length > 1 ? (
`${values.length} selected`
) : values.length === 1 ? (
selected[0].label
) : null;

const searchMethod = ({ options, term }) => {
return options.filter(option => option.label.toLowerCase().includes(term.toLowerCase()));
Expand All @@ -169,7 +176,7 @@ storiesOf('Select', module)
onChangeAction(values);
store.set({ values });
}}
HeaderComponent={HeaderComponent}
displayValue={displayValue}
values={state.values}
placeholder="Select some values"
>
Expand Down
7 changes: 3 additions & 4 deletions src/Select/__tests__/Select.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,12 @@ describe('<Select />', () => {
});

test('should display displayedValue prop', () => {
const HeaderComponent = props => (
<Select.Header displayValue="The current value is 3" {...props} />
);
const displayValue = selected =>
selected.length ? `The current value is ${selected[0].value}` : 'No value';

const props = {
values: ['3'],
HeaderComponent,
displayValue,
};

const { getByText } = render(
Expand Down
46 changes: 43 additions & 3 deletions src/Select/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable react/require-default-props */

import React, { PureComponent, Children } from 'react';
import React, { PureComponent, Children, Fragment } from 'react';
import PropTypes from 'prop-types';

import Popover from '../Popover';
Expand All @@ -10,8 +10,17 @@ import { Container } from './elements';

import DefaultHeader from './Header';

/**
* A fake component to pass options as JSX instead of props.
*/
const Option = () => null;

/**
* Tansform the Option component above into array of object to pass as options props to OptionsList.
*
* @param {*} options
* @param {*} values
*/
const transformChildrenToOptions = children => {
return Children.toArray(children)
.map(({ props, type }) =>
Expand All @@ -25,6 +34,23 @@ const transformChildrenToOptions = children => {
.filter(Boolean);
};

/**
* Default function to use to display selected values.
*
* @param {array} selected - the selected options
* @param {array} values - the selected values
* @param {array} options - the options available
*/
// eslint-disable-next-line no-unused-vars
const defaultDisplayValue = (selected, _values, _options) => {
return selected.map(({ value, label }, index) => (
<Fragment key={value}>
{index > 0 && ', '}
{label}
</Fragment>
));
};

/**
* A Dropdown displays content through its children prop that must be components wrapping text.
* The trigger is a button displaying text provided by the prop `title`
Expand Down Expand Up @@ -165,6 +191,7 @@ class Select extends PureComponent {
usePortal,
onChange /* unused in render */,
disabled,
displayValue,
...optionsListProps
} = this.props;

Expand All @@ -175,6 +202,10 @@ class Select extends PureComponent {

const Header = HeaderComponent || DefaultHeader;
const selectOptions = options || transformChildrenToOptions(children);
const selected = values
.map(value => selectOptions.find(option => option.value === value))
.filter(Boolean);
const value = displayValue || defaultDisplayValue;

return (
<Container className={className} disabled={disabled} data-testid="select">
Expand Down Expand Up @@ -210,7 +241,9 @@ class Select extends PureComponent {
isOpen={isOpen}
placeholder={placeholder}
disabled={disabled}
/>
>
{value && typeof value === 'function' ? value(selected, values, selectOptions) : value}
</Header>
</Popover>
</Container>
);
Expand All @@ -220,7 +253,7 @@ class Select extends PureComponent {
/**
* PropTypes Validation
*/
const { array, bool, func, node, object, string } = PropTypes;
const { array, bool, func, node, object, oneOfType, string } = PropTypes;

Select.propTypes = {
/**
Expand Down Expand Up @@ -253,6 +286,13 @@ Select.propTypes = {
*/
disabled: bool,

/**
* A value or function to display the selected values.
* It receives selected, values and options as arguments.
* Falsy returned value will display the placeholder.
*/
displayValue: oneOfType([node, func]),

/**
* Overrides header component for custom render
*/
Expand Down

0 comments on commit 342f31e

Please sign in to comment.