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 aa1ba92d1702f..de66482b3ee6e 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--light.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--light.png index bce77b83e7476..8fc4d6555b3e8 100644 Binary files a/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--light.png and b/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--light.png differ 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'