diff --git a/package.json b/package.json index 9fb3150c..65f4cb5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eqworks/lumen-labs", - "version": "0.1.0-alpha.123", + "version": "0.1.0-alpha.134", "description": "", "main": "dist/index.js", "source": "src/index.js", diff --git a/src/base-components/input-base.js b/src/base-components/input-base.js index ca88e631..7d5a0fc7 100644 --- a/src/base-components/input-base.js +++ b/src/base-components/input-base.js @@ -22,6 +22,7 @@ const InputBase = forwardRef(({ required = false, disabled = false, refocus = false, + isPlaceholderValue = false, ...rest }, ref) => { const baseClasses = Object.freeze({ @@ -92,7 +93,7 @@ const InputBase = forwardRef(({ /> {suffix && {suffix}} {endIcon && !((value || _value) && hideIcon) &&
{endIcon}
} - {deleteButton && (value || _value) && + {deleteButton && (value || _value || isPlaceholderValue) &&
@@ -119,6 +120,7 @@ InputBase.propTypes = { required: PropTypes.bool, disabled: PropTypes.bool, refocus: PropTypes.bool, + isPlaceholderValue: PropTypes.bool, } InputBase.displayName = 'InputBase' diff --git a/src/components/dropdown-multi-search.js b/src/components/dropdown-multi-search.js index 4cadf99b..ac37da3d 100644 --- a/src/components/dropdown-multi-search.js +++ b/src/components/dropdown-multi-search.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import { DropdownSelect, TextField, makeStyles } from '..' import { useComponentIsActive } from '../hooks' +import { includesAny } from '../utils/string' const DropdownMultiSearch = ({ @@ -31,32 +32,42 @@ const DropdownMultiSearch = ({ onOpenClose = () => {}, noOptionsMessage = '', clearSearch = true, + deleteButton = true, + initOpen = false, ...rest }) => { const { ref, componentIsActive, setComponentIsActive } = useComponentIsActive() const [searchTerm, setSearchTerm] = useState('') - const [openMenu, setOpenMnu] = useState(false) const [filteredOptions, setFilteredOptions] = useState(data || []) const [selectedFilters, setSelectedFilters] = useState([]) const defaultOption = 'any value' const displayValue = selectedFilters?.length ? selectedFilters.join(' or ') : defaultOption - const inputPlaceholder = openMenu ? 'Type to search' : `is ${displayValue}` + const inputPlaceholder = componentIsActive ? 'Type to search' : `is ${displayValue}` const styles = makeStyles({ root: { position: 'relative', + heigh: '100%', + display: 'flex', + alignItems: 'center', + '.dropdown-multi-search-textfield-root': { height: '2.35rem', }, '.dropdown-multi-search-textfield-input': { - cursor: openMenu ? 'text' : 'pointer', - caretColor: openMenu ? 'inherit' : 'transparent', + cursor: componentIsActive ? 'text' : 'pointer', + caretColor: componentIsActive ? 'inherit' : 'transparent', '&::placeholder': { - color: openMenu || displayValue === defaultOption ? '#C8C8C8' : 'inherit', + color: componentIsActive || displayValue === defaultOption ? '#C8C8C8' : 'inherit', + }, + }, + '.dropdown-multi-search-textfield-container': { + '& .textfield__end-icon-container': { + display: 'flex', + alignItems: 'center', }, }, - '.dropdown-multi-search-textfield-container': {}, '.dropdown-multi-search-dropdown-root':{ position: 'absolute', display: 'block', @@ -102,7 +113,6 @@ const DropdownMultiSearch = ({ const handleOnClick = () => { onClick() setComponentIsActive( (state) => !state) - setOpenMnu(!openMenu) } const handleOnSelect = (_, selected) => { @@ -114,15 +124,40 @@ const DropdownMultiSearch = ({ } } + const handleDelete = (e) => { + if (searchTerm) { + setSearchTerm('') + onDelete(e, 'search') + } + + if (!searchTerm && selectedFilters.length > 0) { + setSelectedFilters([]) + onDelete(e, 'select') + } + } + useEffect(() => { setFilteredOptions(data) }, [data]) - if (!componentIsActive && openMenu) { - setOpenMnu(false) - setSearchTerm('') - setFilteredOptions(data) - } + useEffect(() => { + if (value.length > 0) { + setSelectedFilters(value) + } + }, [value]) + + useEffect(() => { + if (!componentIsActive) { + setSearchTerm('') + setFilteredOptions(data) + } + }, [componentIsActive, data]) + + useEffect(() => { + if (initOpen) { + setComponentIsActive(true) + } + }, [initOpen, setComponentIsActive]) return (
@@ -139,12 +174,15 @@ const DropdownMultiSearch = ({ refocus={componentIsActive} size={size} disabled={disabled} + isPlaceholderValue={!componentIsActive && includesAny(displayValue, selectedFilters)} + onDelete={handleDelete} + deleteButton={deleteButton} />
@@ -193,6 +232,8 @@ DropdownMultiSearch.propTypes = { disabled: PropTypes.bool, noOptionsMessage: PropTypes.string, clearSearch: PropTypes.bool, + deleteButton: PropTypes.bool, + initOpen: PropTypes.bool, } DropdownMultiSearch.displayName = 'DropdownMultiSearch' diff --git a/src/components/dropdown-select.js b/src/components/dropdown-select.js index 29e4d601..9dadf0cf 100644 --- a/src/components/dropdown-select.js +++ b/src/components/dropdown-select.js @@ -9,14 +9,14 @@ import { useComponentIsActive } from '../hooks' import clsx from 'clsx' -const _contentSize = (size) => { +const _contentSize = (size, hideSelected) => { let contentSize = '' switch(size) { case 'lg': contentSize = { optionSize: 'mb-9px', - itemContainer: 'py-5px', + itemContainer: hideSelected ? '' : 'py-5px', contentContainer: 'py-5px', type: 'py-2.5', description: 'text-xs', @@ -26,7 +26,7 @@ const _contentSize = (size) => { case 'md': contentSize = { optionSize: 'mb-5px', - itemContainer: 'py-3px', + itemContainer: hideSelected ? '' : 'py-3px', contentContainer: 'py-3px', type: 'py-1.5', description: 'text-11px', @@ -76,9 +76,11 @@ const DropdownSelect = ({ allowClear = true, simple = false, preventDeselect = false, + hideSelected = false, onOpenClose = () => {}, alwaysOpen = false, noOptionsMessage = 'No options', + initOpen = false, ...rest }) => { const [options, setOptions] = useState([]) @@ -98,6 +100,13 @@ const DropdownSelect = ({ } }, [fallbackEmptyValue, uncontrolled, value]) + useEffect(() => { + if (initOpen && ref.current) { + setComponentIsActive(true) + setOpen(true) + } + }, [initOpen, ref, setComponentIsActive]) + const simpleData = useMemo(() => ([{ items: data.map(d => ({ title: d })) }]), [data]) const simpleSelectedOptions = useMemo(() => ( multiSelect @@ -108,7 +117,7 @@ const DropdownSelect = ({ const finalSelectedOptions = useMemo(() => simple ? simpleSelectedOptions : selectedOptions, [selectedOptions, simple, simpleSelectedOptions]) const finalData = useMemo(() => simple ? simpleData : data, [data, simple, simpleData]) - const contentSize = _contentSize(size) + const contentSize = _contentSize(size, hideSelected) const dropdownSelectClasses = Object.freeze({ listContainer: `dropdown-select__list-containercapitalize ${classes.listContainer}`, itemContainer: `dropdown-select__item-container text-secondary-600 ${contentSize.itemContainer}`, @@ -117,11 +126,11 @@ const DropdownSelect = ({ contentHeader: `dropdown-select__content-header w-full flex flex-row items-center justify-between cursor-pointer ${classes.contentHeader}`, type: `dropdown-select__type-container px-5px flex items-center font-semibold text-secondary-400 ${contentSize.type} ${classes.type}`, description: `dropdown-select__description-container pt-5px font-normal text-secondary-500 ${contentSize.description} ${classes.description}`, - dividerContainer: `dropdown-select__divider-container px-2.5 flex flex-row items-center font-bold text-secondary-600 border-t border-secondary-300 cursor-pointer + dividerContainer: `dropdown-select__divider-container px-2.5 flex flex-row items-center font-bold text-secondary-600 border-b border-secondary-300 cursor-pointer ${contentSize.dividerContainer} ${classes.dividerContainer}`, startIcon: 'dropdown-select__start-icon-container mr-2.5 fill-current stroke-current', endIcon: 'dropdown-select__end-icon-container ml-2.5 fill-current stroke-current', - selected: 'dropdown-select__selected font-semibold text-secondary-900 bg-interactive-100 hover:text-secondary-900 hover:bg-interactive-100', + selected: `dropdown-select__selected font-semibold text-secondary-900 bg-interactive-100 hover:text-secondary-900 hover:bg-interactive-100 ${hideSelected ? 'hidden' : ''}`, selectedOptionTitle: `dropdown-select__selected-title-container ${classes.selectedOptionTitle ? classes.selectedOptionTitle : 'mr-2.5 text-secondary-800'}`, noOptionsMessage: `dropdown-select__no-options-message-container text-secondary-600 flex items-center justify-center p-2.5 text-center ${classes.noOptionsMessage}`, }) @@ -186,7 +195,7 @@ const DropdownSelect = ({ } const renderList = ({ items, type }) => ( - items.map((item, index) => + items.map((item, index) => (
{item.description}
} - , - ) + + )) ) const renderListItem = (item) => { @@ -273,12 +282,13 @@ const DropdownSelect = ({ const onClickClose = (e, value) => { e.stopPropagation() handleOnClick('', value, 'chip') + onDelete(e, 'multi', value) } const onClickDelete = (e) => { e.stopPropagation() setSelectedOptions('') - onDelete(e) + onDelete(e, 'select') } return ( @@ -318,8 +328,8 @@ const DropdownSelect = ({ className={`list-container-${index} ${dropdownSelectClasses.listContainer}`} > {showType && el.type && } + {el.divider &&
{renderListItem(el.divider)}
} {renderList(el)} - {el.divider &&
{renderListItem(el.divider)}
} ) })} @@ -351,6 +361,7 @@ DropdownSelect.propTypes = { title: PropTypes.string, startIcon: PropTypes.node, endIcon: PropTypes.node, + onClcik: PropTypes.func, }), }), ), @@ -383,6 +394,8 @@ DropdownSelect.propTypes = { onOpenClose: PropTypes.func, alwaysOpen: PropTypes.bool, noOptionsMessage: PropTypes.string, + initOpen: PropTypes.bool, + hideSelected: PropTypes.bool, } DropdownSelect.displayName = 'DropdownSelect' diff --git a/src/components/text-field.js b/src/components/text-field.js index d5a23ae2..3104afdb 100644 --- a/src/components/text-field.js +++ b/src/components/text-field.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, forwardRef } from 'react' import PropTypes from 'prop-types' import clsx from 'clsx' @@ -108,10 +108,13 @@ const _borderlessClasses = ({ root, input, isEdited, error, focus, disabled }) = }), }) -const renderLabel = ({ label, required, textFieldClasses, id }) => ( -
- {label && } - {required && *} +const renderLabel = ({ label, required, textFieldClasses, id, labelOptions }) => ( +
+
+ {label && } + {required && *} +
+ {labelOptions && labelOptions}
) const renderFooter = ({ helperText, maxLength, value, textFieldClasses }) => ( @@ -138,7 +141,7 @@ const linkedValsFormatHelper = (value, linkedValues, linkedFields) => { return vals } -const TextField = ({ +const TextField = forwardRef(({ classes = { root: '', input: '', container: '' }, size = 'md', inputProps = {}, @@ -157,9 +160,11 @@ const TextField = ({ variant = 'default', linkedFields = 0, refocus = false, - id='', + id = '', + isPlaceholderValue = false, + labelOptions = null, ...rest -}) => { +}, ref) => { const [filled, setFilled] = useState(false) const [value, setValue] = useState(false) const [focus, setFocus] = useState(false) @@ -292,7 +297,7 @@ const TextField = ({ if (variant === 'linked') { return (
- {label && renderLabel({ label, required, textFieldClasses, inputID })} + {label && renderLabel({ label, required, textFieldClasses, inputID, labelOptions })}
{linkedValues.map((val, i) => { return ( @@ -332,9 +337,10 @@ const TextField = ({ } }} > - {variant !== 'borderless' && label && renderLabel({ label, required, textFieldClasses, id })} + {variant !== 'borderless' && label && renderLabel({ label, required, textFieldClasses, id, labelOptions })} {variant !== 'borderless' && renderFooter({ helperText, maxLength, value, textFieldClasses })} ) -} +}) TextField.propTypes = { classes: PropTypes.object, @@ -374,8 +381,12 @@ TextField.propTypes = { linkedFields: PropTypes.number, refocus: PropTypes.bool, id: PropTypes.string, + isPlaceholderValue: PropTypes.bool, + labelOptions: PropTypes.node, } TextField.Area = Area +TextField.displayName = 'TextField' + export default TextField diff --git a/src/hooks/component-is-active.js b/src/hooks/component-is-active.js index 43f56ac8..e3ef67e3 100644 --- a/src/hooks/component-is-active.js +++ b/src/hooks/component-is-active.js @@ -14,8 +14,8 @@ export const useComponentIsActive = () => { } useEffect(() => { - document.addEventListener('click', outOfComponentClick) - return () => document.removeEventListener('click', outOfComponentClick) + document.addEventListener('pointerdown', outOfComponentClick) + return () => document.removeEventListener('pointerdown', outOfComponentClick) }) return { ref, componentIsActive, setComponentIsActive } diff --git a/src/utils/string.js b/src/utils/string.js new file mode 100644 index 00000000..da8db900 --- /dev/null +++ b/src/utils/string.js @@ -0,0 +1,3 @@ +export const includesAny = (string, values) => { + return values.some(item => string.includes(item)) +} diff --git a/stories/data/dropdown-data.js b/stories/data/dropdown-data.js index c1ef3b12..66dbff39 100644 --- a/stories/data/dropdown-data.js +++ b/stories/data/dropdown-data.js @@ -68,6 +68,7 @@ export const sampleDataDivider = [ ], divider: { title: 'Reset', + onClick: () => {}, }, }, ] diff --git a/stories/dropdown.stories.js b/stories/dropdown.stories.js index 0cfb6f80..203a5086 100644 --- a/stories/dropdown.stories.js +++ b/stories/dropdown.stories.js @@ -24,6 +24,7 @@ import { sampleDataSubLinked, categoriesData, } from './data/dropdown-data' +import { useComponentIsActive } from '../src/hooks' export default { @@ -88,7 +89,8 @@ export const Simple = () => {

Default

- } placeholder='Select a word' @@ -96,7 +98,9 @@ export const Simple = () => {

multi

- } placeholder='Select some words' @@ -138,6 +142,7 @@ export const Simple = () => { * title: string, name of the dividir * startIcon: node, icon on left side of divider title * endIcon: node, icon on right side of divider title + * onClick: func, callback function onClick divider item * } * [button] - node, custom onClick element to trigger select/dropdown menu * [size] - string, control component size - supported sizes ['md', 'lg'], default = 'md' @@ -157,6 +162,8 @@ export const Simple = () => { * [allowClear] - bool, enable clearing button when an option is selected, default = true * [simple] - bool, accept arrays of strings instead of the more complex data shape outlined above, default = false * [preventDeselect] - bool, disable default deselect when an item is selected, default = false + * [initOpen] - bool, enable open dropdown menu after it gets rendered, default = false + * [hideSelected] - bool, hide selected options from the list, default = false * [...rest] - any div element properties */ @@ -247,7 +254,7 @@ export const MultiSelect = () => {

Default - horizontal

- } placeholder='Select a subject' multiSelect/> + } placeholder='Select a subject' multiSelect hideSelected/>

Default - vertical

@@ -515,7 +522,7 @@ export const CustomButton = () => { const button = return ( <> - } placeholder='Select a subject' showType/> + } placeholder='Select a subject' showType initOpen/> ) } @@ -630,15 +637,68 @@ export const DropdownMultiSearchSelection = () => { return (
+ data={sampleDataBasic} + /> + disabled={!data.length} + /> + disabled={!data.length} + />
) } + +export const InitialOpenWithAnotherDropdown = () => { + const { ref, componentIsActive, setComponentIsActive } = useComponentIsActive() + const [show, setShow] = useState('') + + const button = + + return ( + <> +
+
+

Default

+ } + placeholder='Select a word' + onSelect={(_,val) => { + setShow(val) + setComponentIsActive(false) + }} + button={button} + open={componentIsActive} + /> +
+
+

multi

+ } + placeholder='Select some words' + initOpen={show === 'hello'} + /> +
+ { show === 'test' && +
+

multi search

+ +
+ } +
+ + ) +}