From c4106d929cebb7ffc4650ee9233a1e3bda3001b4 Mon Sep 17 00:00:00 2001 From: Robert Douglas Date: Tue, 23 Jul 2019 16:28:08 +0200 Subject: [PATCH] feat: add `renderItem` prop to allow formatting of options (#222) --- src/components/ConnectedField/doc.mdx | 71 +++++++++++---- src/components/Icon/index.js | 11 ++- src/components/Select/doc.mdx | 96 +++++++++++++++++++-- src/components/Select/index.js | 119 +++++++++++++++----------- src/components/Select/styles.js | 13 ++- 5 files changed, 230 insertions(+), 80 deletions(-) diff --git a/src/components/ConnectedField/doc.mdx b/src/components/ConnectedField/doc.mdx index 8596731c30..0bfbec6801 100644 --- a/src/components/ConnectedField/doc.mdx +++ b/src/components/ConnectedField/doc.mdx @@ -9,6 +9,7 @@ import { Props } from 'docz' import { Playground } from '../../../docz/' import { DoczCodeBlock } from '../../../docz/CodeBlock' +import { Box } from '../Box' import { Button } from '../Button' import { Icon } from '../Icon' import { ConnectedField } from '../ConnectedField' @@ -36,21 +37,40 @@ You can find additional props for each field component in [Fields](/fields/file- {() => { // Data - const toTitleCase = str => str.charAt(0).toUpperCase() + str.slice(1) const months = [ - 'january', - 'february', - 'march', - 'april', - 'may', - 'june', - 'july', - 'august', - 'september', - 'october', - 'november', - 'december' - ].map(month => ({ label: toTitleCase(month), value: month })) + { value: 'january', label: 'January' }, + { value: 'february', label: 'February' }, + { value: 'march', label: 'March' }, + { value: 'april', label: 'April' }, + { value: 'may', label: 'May' }, + { value: 'june', label: 'June' }, + { value: 'july', label: 'July' }, + { value: 'august', label: 'August' }, + { value: 'september', label: 'September' }, + { value: 'october', label: 'October' }, + { value: 'november', label: 'November' }, + { value: 'december', label: 'December' } + ] + const colors = [ + { value: 'red', label: 'Red' }, + { value: 'orange', label: 'Orange' }, + { value: 'green', label: 'Green' }, + { value: 'blue', label: 'Blue' }, + { value: 'pink', label: 'Pink' }, + { value: 'yellow', label: 'Yellow' } + ] + const networks = [ + { value: 'behance', label: 'Behance' }, + { value: 'dribbble', label: 'Dribbble' }, + { value: 'facebook', label: 'Facebook' }, + { value: 'github', label: 'Github' }, + { value: 'instagram', label: 'Instagram' }, + { value: 'linkedin', label: 'Linkedin' }, + { value: 'stackoverflow', label: 'Stack Overflow' }, + { value: 'twitter', label: 'Twitter' }, + { value: 'xing', label: 'Xing' }, + { value: 'youtube', label: 'Youtube' } + ] // State const initialValues = { firstName: 'Fish', @@ -59,8 +79,8 @@ You can find additional props for each field component in [Fields](/fields/file- dogs: true, hungry: true, weekday: 'monday', - month: months[1], - tags: [months[3], months[2]], + month: { label: 'February', value: 'february' }, + tags: [], partyDate: Date.now() } // Render @@ -149,21 +169,34 @@ You can find additional props for each field component in [Fields](/fields/file- + ( + + {option.label} + + )} + /> { - const iconName = name.toLowerCase() - const iconConfig = icons[iconName] + if (!name) { + return null + } + + const iconConfig = icons[name] if (!iconConfig) { return null @@ -15,11 +18,11 @@ export const Icon = forwardRef(({ name, ...props }, ref) => { return ( diff --git a/src/components/Select/doc.mdx b/src/components/Select/doc.mdx index f009fc865d..068cca544b 100644 --- a/src/components/Select/doc.mdx +++ b/src/components/Select/doc.mdx @@ -9,6 +9,7 @@ import { Props } from 'docz' import { Playground } from '../../../docz/' import { Select } from './index' +import { Icon } from '../Icon' # Select @@ -26,7 +27,16 @@ import { Select } from './index' ] const [value, setValue] = useState(ITEMS[2]) const handleChange = e => setValue(e.target.value) - return + ) }} @@ -82,7 +92,7 @@ To be able to filter (i.e. search) the results, add the `isSearchable` prop. ## Required -You can normally clear the selected value of a `Select` but if it's has a `required` prop we'll prevent the ability to remove a chosen option. +You can normally clear the selected value of a `Select` but if it has a `required` prop we'll prevent the ability to remove a chosen option. {() => { @@ -96,11 +106,47 @@ You can normally clear the selected value of a `Select` but if it's has a `requi ] const [value, setValue] = useState(ITEMS[2]) const handleChange = e => { - console.debug('e', e.target) + setValue(e.target.value) + } + return + ( +
+ {option.label} +
+ )} + /> + ) + }} +
+ ## Sizes Use size property with `sm` `md` or `lg`(default). @@ -187,7 +271,9 @@ Use size property with `sm` `md` or `lg`(default). ] const [value, setValue] = useState(ITEMS[2]) const handleChange = e => setValue(e.target.value) - return + ) }} diff --git a/src/components/Select/index.js b/src/components/Select/index.js index 464ad312b8..e0296038a6 100644 --- a/src/components/Select/index.js +++ b/src/components/Select/index.js @@ -1,5 +1,5 @@ import React, { forwardRef, useEffect, useState } from 'react' -import { arrayOf, bool, func, shape, string } from 'prop-types' +import { arrayOf, bool, func, oneOfType, shape, string } from 'prop-types' import Downshift from 'downshift' import matchSorter from 'match-sorter' import kebabCase from 'lodash.kebabcase' @@ -14,10 +14,12 @@ import { createEvent } from '../../utils/' import * as S from './styles' // Helpers +const EMPTY = '' const itemToString = item => (item ? item.label : '') const ensureArray = value => (Array.isArray(value) ? value : value ? [value] : []) const getUniqueValue = (item, values) => uniqBy([...values, item], item => item.value) const isValueExisting = (value, values) => values.find(item => item.value === kebabCase(value)) +const defaultRenderOption = option => (option ? option.label : EMPTY) export const Select = forwardRef( ( @@ -33,15 +35,17 @@ export const Select = forwardRef( onChange, onFocus, placeholder = 'Choose from…', + renderItem = defaultRenderOption, required, size = 'lg', value: defaultValue, - variant + variant, + ...rest }, ref ) => { const selectedItem = (!isMultiple && defaultValue) || null - const defaultInputValue = selectedItem ? defaultValue.label : '' + const defaultInputValue = selectedItem ? defaultValue.label : EMPTY // Values will always be an array internally const [values, setValues] = useState(ensureArray(defaultValue)) const [inputValue, setInputValue] = useState(defaultInputValue) @@ -54,11 +58,19 @@ export const Select = forwardRef( }, [defaultValue, defaultInputValue]) // Update results if searchable - const handleInputChange = value => { + const handleInputChange = (value, openMenu) => { if (isSearchable) { + // Update const results = matchSorter(options, value, { keys: ['label'] }) setInputValue(value) setResults(results) + openMenu() + + // We have to manage the cursor position when searching on field that isMultiple + const selection = window.getSelection() + const node = selection.focusNode + const offset = selection.focusOffset + setImmediate(() => selection.setPosition(node, offset)) } } @@ -107,7 +119,6 @@ export const Select = forwardRef( return ( { const isShowCreate = isCreatable && inputValue && !isValueExisting(inputValue, values) const isShowMenu = isOpen && (results.length || isShowCreate) const isShowDeleteIcon = inputValue && !isOpen && !required + const inputProps = getInputProps({ + autoComplete: 'off', + autoFocus, + contentEditable: isSearchable, + disabled, + name, + onBlur, + onClick: toggleMenu, + onFocus, + placeholder, + readOnly: !isSearchable, + ref: ref, + size, + value: inputValue || EMPTY, + variant: isOpen ? 'focused' : variant + }) + + let content = EMPTY + if (isMultiple) { + content = inputValue + } else if (values.length) { + content = renderItem(values[0]) + } return ( - + + {...inputProps} + onInput={e => handleInputChange(e.target.innerText, openMenu)} + suppressContentEditableWarning + > + {content} + {isShowDeleteIcon ? ( {isShowMenu ? ( - {results.map((item, index) => { - return ( - - {item.label} - - ) - })} + {results.map((item, index) => ( + + {renderItem(item)} + + ))} {isShowCreate && ( css` ${fieldStyles}; ${overflowEllipsis}; padding-right: ${th(`fields.sizes.${size}.height`)}; cursor: default; + + br { + display: none; + } + + &:empty::before { + content: attr(placeholder); + opacity: 0.5; + } ` ) export const Menu = styled.ul` - ${system}; ${th('fields.select.default')}; position: absolute; z-index: 2;