From 4ca3c29b5d1dea94d79773cbe2cc76a168ee692d Mon Sep 17 00:00:00 2001 From: Ben White Date: Wed, 20 Mar 2024 16:12:51 +0100 Subject: [PATCH] feat: Add comma separator support for LemonInputSelect (#21031) * Add comma sepator * feat: Alt-click to edit inputselect items (#21032) --- ...ect--multiple-select-with-custom--dark.png | Bin 1646 -> 1357 bytes ...ct--multiple-select-with-custom--light.png | Bin 1696 -> 1427 bytes .../LemonInputSelect.stories.tsx | 17 +- .../LemonInputSelect/LemonInputSelect.tsx | 145 ++++++++++++------ 4 files changed, 118 insertions(+), 44 deletions(-) diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--dark.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--dark.png index aa1ba92d1702f51e6269e217cc34fde1d097d853..de66482b3ee6e84a942c809cd11d49ae5d6cb709 100644 GIT binary patch delta 1326 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 N002ovPDHLkV1ixPIZ6Nk diff --git a/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.stories.tsx b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.stories.tsx index 796d1794b4c89..f7c9212186d1f 100644 --- a/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.stories.tsx @@ -54,9 +54,24 @@ MultipleSelect.args = { export const MultipleSelectWithCustom: Story = Template.bind({}) MultipleSelectWithCustom.args = { - placeholder: 'Enter any email...', + placeholder: 'Pick a url...', mode: 'multiple', allowCustomValues: true, + options: [ + { + key: 'http://posthog.com/docs', + label: 'http://posthog.com/docs', + }, + { + key: 'http://posthog.com/pricing', + label: 'http://posthog.com/pricing', + }, + + { + key: 'http://posthog.com/products', + label: 'http://posthog.com/products', + }, + ], } export const Disabled: Story = Template.bind({}) diff --git a/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx index 967f18e323753..9e5240a275a68 100644 --- a/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx +++ b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx @@ -1,3 +1,5 @@ +import { Tooltip } from '@posthog/lemon-ui' +import { useKeyHeld } from 'lib/hooks/useKeyHeld' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { LemonSnack } from 'lib/lemon-ui/LemonSnack/LemonSnack' import { range } from 'lib/utils' @@ -49,14 +51,21 @@ export function LemonInputSelect({ const inputRef = useRef(null) const [selectedIndex, setSelectedIndex] = useState(0) const values = value ?? [] + const altKeyHeld = useKeyHeld('Alt') + + const separateOnComma = allowCustomValues && mode === 'multiple' const visibleOptions = useMemo(() => { const res: LemonInputSelectOption[] = [] const customValues = [...values] + // We show the input value if custom values are allowed and it's not in the list + if (allowCustomValues && inputValue && !values.includes(inputValue)) { + customValues.unshift(inputValue) + } + options.forEach((option) => { // Remove from the custom values list if it's in the options - if (customValues.includes(option.key)) { customValues.splice(customValues.indexOf(option.key), 1) } @@ -75,14 +84,8 @@ export function LemonInputSelect({ res.unshift({ key: value, label: value }) }) } - - // Finally we show the input value if custom values are allowed and it's not in the list - if (allowCustomValues && inputValue && !values.includes(inputValue)) { - res.unshift({ key: inputValue, label: inputValue }) - } - return res - }, [options, inputValue, value]) + }, [options, inputValue, values]) // Reset the selected index when the visible options change useEffect(() => { @@ -90,33 +93,69 @@ export function LemonInputSelect({ }, [visibleOptions.length]) const setInputValue = (newValue: string): void => { + // Special case for multiple mode with custom values + if (separateOnComma && newValue.includes(',')) { + const newValues = [...values] + + newValue.split(',').forEach((value) => { + const trimmedValue = value.trim() + if (trimmedValue && !values.includes(trimmedValue)) { + newValues.push(trimmedValue) + } + }) + + onChange?.(newValues) + newValue = '' + } + _setInputValue(newValue) onInputChange?.(inputValue) } - const _onActionItem = (item: string): void => { + const _removeItem = (item: string): void => { let newValues = [...values] - if (values.includes(item)) { - // Remove the item - if (mode === 'single') { - newValues = [] - } else { - newValues.splice(values.indexOf(item), 1) - } + // Remove the item + if (mode === 'single') { + newValues = [] } else { - // Add the item - if (mode === 'single') { - newValues = [item] - } else { + newValues.splice(values.indexOf(item), 1) + } + + onChange?.(newValues) + } + + const _addItem = (item: string): void => { + let newValues = [...values] + // Add the item + if (mode === 'single') { + newValues = [item] + } else { + if (!newValues.includes(item)) { newValues.push(item) } - - setInputValue('') } + setInputValue('') onChange?.(newValues) } + const _onActionItem = (item: string): void => { + if (altKeyHeld && allowCustomValues) { + // In this case we want to remove it if added and set input to it + if (values.includes(item)) { + _removeItem(item) + } + setInputValue(item) + return + } + + if (values.includes(item)) { + _removeItem(item) + } else { + _addItem(item) + } + } + const _onBlur = (): void => { // We need to add a delay as a click could be in the popover or the input wrapper which refocuses setTimeout(() => { @@ -143,8 +182,8 @@ export function LemonInputSelect({ const _onKeyDown = (e: React.KeyboardEvent): void => { if (e.key === 'Enter') { e.preventDefault() - const itemToAdd = visibleOptions[selectedIndex]?.key + if (itemToAdd) { _onActionItem(visibleOptions[selectedIndex]?.key) } @@ -164,33 +203,51 @@ export function LemonInputSelect({ } } - // TRICKY: We don't want the popover to affect the snack buttons - const prefix = ( - - <> - {values.map((value) => { - const option = options.find((option) => option.key === value) ?? { - label: value, - labelComponent: null, - } - return ( - <> - _onActionItem(value)}> + const prefix = useMemo( + () => ( + // TRICKY: We don't want the popover to affect the snack buttons + + <> + {values.map((value) => { + const option = options.find((option) => option.key === value) ?? { + label: value, + labelComponent: null, + } + const snack = ( + _onActionItem(value)} + onClick={allowCustomValues ? () => _onActionItem(value) : undefined} + > {option?.labelComponent ?? option?.label} - - ) - })} - - + ) + return allowCustomValues ? ( + + + click to edit + + } + > + {snack} + + ) : ( + snack + ) + })} + + + ), + [values, options, altKeyHeld, allowCustomValues] ) return ( { popoverFocusRef.current = false setShowPopover(false) @@ -219,7 +276,9 @@ export function LemonInputSelect({ {isHighlighted ? ( {' '} - {!values.includes(option.key) + {altKeyHeld && allowCustomValues + ? 'edit' + : !values.includes(option.key) ? mode === 'single' ? 'select' : 'add'