diff --git a/.changeset/fix-react-component-demos.md b/.changeset/fix-react-component-demos.md new file mode 100644 index 00000000..a76c8d63 --- /dev/null +++ b/.changeset/fix-react-component-demos.md @@ -0,0 +1,5 @@ +--- +"@tiny-design/react": patch +--- + +Fix React component behavior and update demos to follow theme tokens. diff --git a/apps/docs/src/containers/theme-studio/index.tsx b/apps/docs/src/containers/theme-studio/index.tsx index 00129dcf..bd06bf93 100644 --- a/apps/docs/src/containers/theme-studio/index.tsx +++ b/apps/docs/src/containers/theme-studio/index.tsx @@ -156,6 +156,12 @@ const ThemeStudioPage = (): React.ReactElement => { ', () => { expect(document.querySelector('.ty-select__empty')).toHaveTextContent('Nothing found'); }); + it('should preserve search text after closing without selecting', () => { + const { container } = render( + + ); + const selector = container.querySelector('.ty-select__selector') as HTMLElement; + const selectEl = container.querySelector('.ty-select') as HTMLElement; + + fireEvent.click(selector); + const searchInput = container.querySelector('.ty-select__search') as HTMLInputElement; + fireEvent.change(searchInput, { target: { value: 'xyz' } }); + expect(document.querySelector('.ty-select__empty')).toHaveTextContent('Nothing found'); + + fireEvent.click(document.body); + expect(selectEl).not.toHaveClass('ty-select_open'); + expect(container.querySelector('.ty-select__search')).toHaveValue('xyz'); + + fireEvent.click(selector); + expect(document.querySelector('.ty-select__empty')).toHaveTextContent('Nothing found'); + }); + + it('should show the selected option label when reopening searchable single select', () => { + const { container } = render( + + ); + const selector = container.querySelector('.ty-select__selector') as HTMLElement; + + fireEvent.click(selector); + fireEvent.click(getOptions()[1]); + expect(container.querySelector('.ty-select__selection-text')).toHaveTextContent('Banana'); + + fireEvent.click(selector); + expect(container.querySelector('.ty-select__search')).toHaveValue('Banana'); + }); + // Sizes it('should apply size classes', () => { const { container: smContainer } = render( diff --git a/packages/react/src/select/select.tsx b/packages/react/src/select/select.tsx index 2c4ffb12..cb9cce65 100644 --- a/packages/react/src/select/select.tsx +++ b/packages/react/src/select/select.tsx @@ -48,6 +48,7 @@ const Select = (props: SelectProps): React.ReactElement => { const ref = useRef(null); const searchInputRef = useRef(null); + const wasOpenRef = useRef(false); const dropdownRef = useCallback( (node: HTMLUListElement | null) => { if (!node || !scrollToSelected) return; @@ -168,6 +169,15 @@ const Select = (props: SelectProps): React.ReactElement => { [flatOptions, labelRender] ); + const getSearchTextForValue = useCallback( + (val: string): string => { + const opt = flatOptions.find((o) => o.value === val); + const label = opt?.label ?? val; + return typeof label === 'string' || typeof label === 'number' ? String(label) : val; + }, + [flatOptions] + ); + // Remove tag const handleRemoveTag = (val: string, e: React.MouseEvent): void => { e.stopPropagation(); @@ -294,11 +304,35 @@ const Select = (props: SelectProps): React.ReactElement => { if (!combo.isOpen || !showSearch || disabled) return; const frameId = requestAnimationFrame(() => { - searchInputRef.current?.focus(); + const input = searchInputRef.current; + input?.focus(); + if (!isMultiple && hasSomeValue && input?.value) { + input.select(); + } }); return () => cancelAnimationFrame(frameId); - }, [combo.isOpen, showSearch, disabled]); + }, [combo.isOpen, disabled, hasSomeValue, isMultiple, showSearch]); + + useEffect(() => { + const wasOpen = wasOpenRef.current; + wasOpenRef.current = combo.isOpen; + if ( + wasOpen || + !combo.isOpen || + isMultiple || + !showSearch || + !hasSomeValue || + searchValue + ) { + return; + } + + const currentValue = Array.isArray(selectVal) ? selectVal[0] : selectVal; + if (currentValue) { + setSearchValue(getSearchTextForValue(currentValue)); + } + }, [combo.isOpen, getSearchTextForValue, hasSomeValue, isMultiple, searchValue, selectVal, showSearch]); const hasValue = hasSomeValue; @@ -476,7 +510,7 @@ const Select = (props: SelectProps): React.ReactElement => { } // Single mode - if (showSearch && combo.isOpen) { + if (showSearch && (combo.isOpen || searchValue)) { return ( { } combo.closeDropdown(); - setSearchValue(''); }} content={renderOverlay()}>
diff --git a/packages/react/src/split-button/style/index.scss b/packages/react/src/split-button/style/index.scss index d732662d..185a3a34 100644 --- a/packages/react/src/split-button/style/index.scss +++ b/packages/react/src/split-button/style/index.scss @@ -1,7 +1,15 @@ @use '@tiny-design/tokens/scss/variables' as *; .#{$prefix}-split-button { - &__dropdown-btn{ + display: inline-flex; + white-space: nowrap; + vertical-align: middle; + + .#{$prefix}-btn { + float: none; + } + + &__dropdown-btn { padding: 0 8px; min-width: auto; }