diff --git a/ui-core/src/components/inputs/InputWithSuggestions/InputWithSuggestions.tsx b/ui-core/src/components/inputs/ComboBox/ComboBox.tsx similarity index 78% rename from ui-core/src/components/inputs/InputWithSuggestions/InputWithSuggestions.tsx rename to ui-core/src/components/inputs/ComboBox/ComboBox.tsx index a0682f4d..06a380f5 100644 --- a/ui-core/src/components/inputs/InputWithSuggestions/InputWithSuggestions.tsx +++ b/ui-core/src/components/inputs/ComboBox/ComboBox.tsx @@ -1,12 +1,12 @@ import React, { - useState, - useRef, - useEffect, type ChangeEventHandler, + type FocusEventHandler, type KeyboardEventHandler, type ReactNode, - type FocusEventHandler, + useEffect, useMemo, + useRef, + useState, } from 'react'; import { ChevronDown, X } from '@osrd-project/ui-icons'; @@ -15,17 +15,17 @@ import cx from 'classnames'; import { normalizeString } from './utils'; import Input, { type InputProps } from '../Input'; -export type InputWithSuggestionsProps = Omit & { +export type ComboBoxProps = Omit & { suggestions: Array; - onChange: (value: T) => void; getSuggestionLabel: (option: T) => string; customLabel?: ReactNode; numberOfSuggestionsToShow?: number; exactSearch?: boolean; value?: T; + onSelectSuggestion?: (option: T) => void; }; -const InputWithSuggestions = ({ +const ComboBox = ({ suggestions, onChange, getSuggestionLabel, @@ -34,11 +34,14 @@ const InputWithSuggestions = ({ exactSearch = false, value, small, + onSelectSuggestion, ...inputProps -}: InputWithSuggestionsProps) => { +}: ComboBoxProps) => { const [filteredSuggestions, setFilteredSuggestions] = useState([]); const [activeSuggestionIndex, setActiveSuggestionIndex] = useState(-1); const [inputValue, setInputValue] = useState(value ? getSuggestionLabel(value) : ''); + const [selectedOption, setSelectedOption] = useState(null); + const inputRef = useRef(null); const sortedSuggestions = useMemo( @@ -46,17 +49,18 @@ const InputWithSuggestions = ({ [suggestions, getSuggestionLabel] ); - const showSuggestions = filteredSuggestions.length > 0; + const showSuggestions = filteredSuggestions.length > 0 && !inputProps.disabled; const focusInput = () => inputRef.current?.focus(); const clearInput = () => { setInputValue(''); + setSelectedOption(null); inputRef.current?.focus(); }; const icons = [ - ...(inputValue + ...(selectedOption ? [ { icon: , @@ -70,7 +74,9 @@ const InputWithSuggestions = ({ { icon: , action: focusInput, - className: 'chevron-icon', + className: cx('chevron-icon', { + disabled: inputProps.disabled, + }), }, ] : []), // Conditionally include the chevron icon only when suggestions are not empty @@ -82,9 +88,14 @@ const InputWithSuggestions = ({ } }, [value, getSuggestionLabel]); + useEffect(() => { + setFilteredSuggestions(sortedSuggestions); + }, [sortedSuggestions]); + const handleInputChange: ChangeEventHandler = (e) => { const userInput = normalizeString(e.currentTarget.value).trim(); setInputValue(e.currentTarget.value); + onChange?.(e); if (userInput.trim() === '') { setFilteredSuggestions([]); @@ -102,8 +113,10 @@ const InputWithSuggestions = ({ const selectSuggestion = (index: number) => { const selectedSuggestion = filteredSuggestions[index]; - setInputValue(getSuggestionLabel(selectedSuggestion)); - onChange(selectedSuggestion); + const suggestionLabel = getSuggestionLabel(selectedSuggestion); + setInputValue(suggestionLabel); + setSelectedOption(selectedSuggestion); + onSelectSuggestion?.(selectedSuggestion); setFilteredSuggestions([]); setActiveSuggestionIndex(-1); }; @@ -128,11 +141,21 @@ const InputWithSuggestions = ({ }; const handleParentDivOnBlur: FocusEventHandler = () => { + const normalizedInput = normalizeString(inputValue.trim().toLowerCase()); + + const isInputInSuggestions = suggestions.some( + (suggestion) => + normalizeString(getSuggestionLabel(suggestion).toLowerCase()) === normalizedInput + ); + if (filteredSuggestions.length === 1) { selectSuggestion(0); - } else { - setFilteredSuggestions([]); + } else if (!isInputInSuggestions) { + setInputValue(''); + setSelectedOption(null); } + + setFilteredSuggestions([]); }; const handleSuggestionClick = (index: number) => { @@ -141,7 +164,7 @@ const InputWithSuggestions = ({ return (
@@ -156,7 +179,7 @@ const InputWithSuggestions = ({ withIcons={icons} small={small} /> - {showSuggestions && filteredSuggestions.length > 0 && ( + {showSuggestions && (
    {filteredSuggestions.map((suggestion, index) => (
  • ({ small, })} onClick={() => handleSuggestionClick(index)} - onMouseDown={(e) => e.preventDefault()} // Prevents the div parent (.input-with-suggestions) from losing focus + onMouseDown={(e) => e.preventDefault()} // Prevents the div parent (.combo-box) from losing focus > {getSuggestionLabel(suggestion)}
  • @@ -177,4 +200,4 @@ const InputWithSuggestions = ({ ); }; -export default InputWithSuggestions; +export default ComboBox; diff --git a/ui-core/src/components/inputs/ComboBox/index.ts b/ui-core/src/components/inputs/ComboBox/index.ts new file mode 100644 index 00000000..2d00455f --- /dev/null +++ b/ui-core/src/components/inputs/ComboBox/index.ts @@ -0,0 +1 @@ +export { default as ComboBox, type ComboBoxProps } from './ComboBox'; diff --git a/ui-core/src/components/inputs/InputWithSuggestions/utils.ts b/ui-core/src/components/inputs/ComboBox/utils.ts similarity index 100% rename from ui-core/src/components/inputs/InputWithSuggestions/utils.ts rename to ui-core/src/components/inputs/ComboBox/utils.ts diff --git a/ui-core/src/components/inputs/InputWithSuggestions/index.ts b/ui-core/src/components/inputs/InputWithSuggestions/index.ts deleted file mode 100644 index 16fcd730..00000000 --- a/ui-core/src/components/inputs/InputWithSuggestions/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - default as InputWithSuggestions, - type InputWithSuggestionsProps, -} from './InputWithSuggestions'; diff --git a/ui-core/src/index.ts b/ui-core/src/index.ts index 731e655c..2a072edb 100644 --- a/ui-core/src/index.ts +++ b/ui-core/src/index.ts @@ -1,34 +1,31 @@ import './styles/main.css'; export { default as Button, ButtonProps } from './components/buttons/Button'; +export { ComboBox, type ComboBoxProps } from './components/inputs/ComboBox'; export { - CheckboxList, - CheckboxesTree, Checkbox, + CheckboxList, CheckboxListProps, CheckboxProps, + CheckboxesTree, CheckboxesTreeProps, } from './components/inputs/Checkbox'; +export { + DatePicker, + type CalendarSlot, + type DatePickerProps, + type RangeDatePickerProps, + type SingleDatePickerProps, +} from './components/inputs/datePicker'; export { default as Input, InputProps } from './components/inputs/Input'; -export { default as TokenInput, TokenInputProps } from './components/inputs/TokenInput'; export { default as PasswordInput, PasswordInputProps } from './components/inputs/PasswordInput'; -export { default as TextArea, TextAreaProps } from './components/inputs/TextArea'; export { default as RadioButton, RadioButtonProps } from './components/inputs/RadioButton'; export { default as RadioGroup, RadioGroupProps } from './components/inputs/RadioGroup'; export { default as Select, SelectProps } from './components/Select'; +export { default as TextArea, TextAreaProps } from './components/inputs/TextArea'; export { default as TimePicker } from './components/inputs/TimePicker'; -export { - DatePicker, - type RangeDatePickerProps, - type SingleDatePickerProps, - type DatePickerProps, - type CalendarSlot, -} from './components/inputs/datePicker'; export { default as TolerancePicker, type TolerancePickerProps, } from './components/inputs/tolerancePicker/TolerancePicker'; -export { - InputWithSuggestions, - type InputWithSuggestionsProps, -} from './components/inputs/InputWithSuggestions'; +export { default as TokenInput, TokenInputProps } from './components/inputs/TokenInput'; diff --git a/ui-core/src/stories/InputWithSuggestions.stories.tsx b/ui-core/src/stories/ComboBox.stories.tsx similarity index 79% rename from ui-core/src/stories/InputWithSuggestions.stories.tsx rename to ui-core/src/stories/ComboBox.stories.tsx index 660c9638..d211ccbf 100644 --- a/ui-core/src/stories/InputWithSuggestions.stories.tsx +++ b/ui-core/src/stories/ComboBox.stories.tsx @@ -3,7 +3,7 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import '@osrd-project/ui-core/dist/theme.css'; -import { InputWithSuggestions } from '../components/inputs/InputWithSuggestions'; +import { ComboBox } from '../components/inputs/ComboBox'; type Suggestion = { id: string; label: string }; @@ -13,13 +13,14 @@ const suggestions = [ { id: '3', label: 'Manuella' }, ] as Suggestion[]; -const meta: Meta = { - component: InputWithSuggestions, +const meta: Meta = { + component: ComboBox, args: { small: false, disabled: false, readOnly: false, onChange: () => {}, + onSelectSuggestion: () => {}, getSuggestionLabel: (option) => (option as Suggestion).label, suggestions: suggestions, }, @@ -30,12 +31,12 @@ const meta: Meta = {
), ], - title: 'core/InputWithSuggestions', + title: 'core/ComboBox', tags: ['autodocs'], }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: { @@ -49,7 +50,7 @@ export const WithDefaultValue: Story = { args: { label: 'Your name', type: 'text', - value: { id: '1', label: 'Manuel' }, + value: suggestions[0], // Use a suggestion from the suggestions array }, }; @@ -96,3 +97,11 @@ export const SmallInput: Story = { small: true, }, }; + +export const WithoutSuggestions: Story = { + args: { + label: 'Your name', + type: 'text', + suggestions: [], + }, +}; diff --git a/ui-core/src/styles/inputs/inputWithSuggestions.css b/ui-core/src/styles/inputs/comboBox.css similarity index 97% rename from ui-core/src/styles/inputs/inputWithSuggestions.css rename to ui-core/src/styles/inputs/comboBox.css index 8a33e555..3aaa1ebf 100644 --- a/ui-core/src/styles/inputs/inputWithSuggestions.css +++ b/ui-core/src/styles/inputs/comboBox.css @@ -1,4 +1,4 @@ -.input-with-suggestions { +.combo-box { position: relative; width: 100%; @apply text-grey-80; diff --git a/ui-core/src/styles/inputs/index.css b/ui-core/src/styles/inputs/index.css index 078dad7c..afc3946d 100644 --- a/ui-core/src/styles/inputs/index.css +++ b/ui-core/src/styles/inputs/index.css @@ -12,4 +12,4 @@ @import './timePicker.css'; @import './tokenInput.css'; @import './tolerancePicker.css'; -@import './inputWithSuggestions.css'; +@import './comboBox.css'; diff --git a/ui-core/src/styles/inputs/input.css b/ui-core/src/styles/inputs/input.css index 9acccfdd..21a036bd 100644 --- a/ui-core/src/styles/inputs/input.css +++ b/ui-core/src/styles/inputs/input.css @@ -42,6 +42,12 @@ @apply text-primary-20; cursor: pointer; } + + .chevron-icon { + &.disabled { + @apply text-grey-30; + } + } } .leading-content-wrapper {