From 028d5387a9797528e7572353935e5192f10af8d9 Mon Sep 17 00:00:00 2001 From: Kharya1337 Date: Tue, 13 Jun 2023 18:10:38 +0300 Subject: [PATCH 1/6] chore(react-components): add render options as combobox to Autocomplete --- .../src/Autocomplete/autocomplete.tsx | 223 +++++++++++++----- .../src/hooks/use_autocomplete.ts | 63 ++++- 2 files changed, 229 insertions(+), 57 deletions(-) diff --git a/packages/react-components/src/Autocomplete/autocomplete.tsx b/packages/react-components/src/Autocomplete/autocomplete.tsx index c3bf3ab8..eaba559a 100644 --- a/packages/react-components/src/Autocomplete/autocomplete.tsx +++ b/packages/react-components/src/Autocomplete/autocomplete.tsx @@ -6,6 +6,7 @@ import { AutocompleteValue, } from '../hooks'; import { Popover } from '../Popover'; +import { Popper } from '../Popper'; import { TextField } from '../TextField'; import { Typography } from '../Typography'; import { Box } from '../Box'; @@ -63,6 +64,10 @@ export type AutocompleteProps css({ position: 'relative', }); +const stylesRootSearchWrapper = (size: AutocompleteProps['size']) => css({ + label: 'Autocomplete', + position: 'relative', + display: 'flex', + flexWrap: 'wrap', + alignItems: 'center', + borderRadius: '4px', + minHeight: 'var(--pv-size-base-8)', + ...(size === 'small' && { + minHeight: 'var(--pv-size-base-6)', + }), + ...(size === 'medium' && { + minHeight: 'var(--pv-size-base-7)', + }), + padding: '0 calc(var(--pv-size-base-2) + 24px) 0 var(--pv-size-base-2)', + backgroundColor: 'var(--pv-color-gray-1)', + boxSizing: 'border-box', + transition: 'background-color 200ms, color 200ms, border-color 200ms', + borderStyle: 'solid', + borderWidth: '1px', + borderColor: 'var(--pv-color-gray-8)', + '&:hover': { + backgroundColor: 'var(--pv-color-gray-3)', + borderColor: 'var(--pv-color-gray-7)', + }, + '&:hover input': { + backgroundColor: 'var(--pv-color-gray-3)', + borderColor: 'var(--pv-color-gray-7)', + }, + '&:focus-within': { + backgroundColor: 'var(--pv-color-secondary-tint-5)', + borderColor: 'var(--pv-color-secondary-tint-3)', + }, +}); + const stylesRoot = (size: AutocompleteProps['size']) => css({ label: 'Autocomplete-root', outline: 'none', @@ -138,12 +178,12 @@ const stylesRoot = (size: AutocompleteProps['size']) => css({ textAlign: 'left', cursor: 'pointer', fontFamily: 'inherit', - height: 'var(--pv-size-base-8)', + minHeight: 'var(--pv-size-base-8)', ...(size === 'small' && { - height: 'var(--pv-size-base-6)', + minHeight: 'var(--pv-size-base-6)', }), ...(size === 'medium' && { - height: 'var(--pv-size-base-7)', + minHeight: 'var(--pv-size-base-7)', }), '&:hover': { backgroundColor: 'var(--pv-color-gray-3)', @@ -235,6 +275,17 @@ const stylesInputSearch = () => css({ padding: 'var(--pv-size-base-3) var(--pv-size-base-3) var(--pv-size-base-2)', }); +const stylesRootInputSearch = () => css({ + label: 'Autocomplete-root-input-search', + flex: 1, + marginLeft: 5, + minWidth: '30%', + '& input': { + border: 'none', + padding: '0', + }, +}); + const stylesListBoxState = () => css({ label: 'Autocomplete-listbox-state', padding: 'var(--pv-size-base-3) var(--pv-size-base-2)', @@ -243,19 +294,26 @@ const stylesListBoxState = () => css({ const stylesPopover = () => css({ label: 'Autocomplete-popover', minWidth: 240, + outline: 0, + maxWidth: 'calc(100% - 32px)', + maxHeight: 'calc(100% - 32px)', + minHeight: '16px', + boxShadow: 'var(--pv-shadow-light-low)', }); -const stylesTagsList = () => css({ +const stylesTagsList = (isEmbedded = false) => css({ label: 'Autocomplete-tags-list', overflow: 'hidden', - width: '100%', + ...(!isEmbedded && { + width: '100%', + }), }); const stylesTag = (tagsLength: number, limitTags: number, size: AutocompleteProps['size']) => css({ label: 'Autocomplete-tag', borderRadius: '2px', borderColor: 'var(--pv-color-gray-7)', - margin: '0 var(--pv-size-base) 0 0', + margin: '2px var(--pv-size-base) 2px 0', ...(tagsLength === 1 && { maxWidth: 'calc(100% - var(--pv-size-base))', }), @@ -319,11 +377,12 @@ export const Autocomplete = limitTags) ? (value.length - limitTags) : 0; const valueLimits = more > 0 ? value.slice(0, limitTags) : value; return ( <>
{valueLimits.map((v, index) => ( ( + + {getOptionLabel(v)} + + )); + } + return getOptionLabel(value as T); }; const renderedValue = renderValue(); const isValueEmpty = renderedValue === null; - const defaultRenderRoot: AutocompleteProps['renderRoot'] = (propsRoot, valueRoot) => ( -
- ['renderRoot'] = (propsRoot, valueRoot) => { + if (combobox) { + return ( +
+ {multiple ? renderedValue : null} + + + { }} + /> +
+ ); + } + + return ( +
- {isValueEmpty ? placeholder : renderedValue} - - - {}} - /> -
- ); + + {isValueEmpty ? placeholder : renderedValue} + + + { }} + /> +
+ ); + }; const renderOption = renderOptionProp || defaultRenderOption; const renderRoot = renderRootProp || defaultRenderRoot; + const PopperComponent = combobox ? Popper : Popover; const renderListOption = (option: T, index: number) => { const optionProps = getOptionProps(option, index); @@ -505,13 +620,14 @@ export const Autocomplete = )} - - {!disableSearch && ( + {!disableSearch && !combobox && ( )} - + ); }; @@ -604,7 +720,6 @@ Autocomplete.defaultProps = { noOptionsText: 'No options', loading: false, loadingText: 'Loading...', - limitTags: 2, required: false, allowCreateOption: false, createOptionText: 'Create new', diff --git a/packages/react-components/src/hooks/use_autocomplete.ts b/packages/react-components/src/hooks/use_autocomplete.ts index d5f7c8e1..bfa0713c 100644 --- a/packages/react-components/src/hooks/use_autocomplete.ts +++ b/packages/react-components/src/hooks/use_autocomplete.ts @@ -10,7 +10,7 @@ import type { PopoverProps } from '../Popover'; export type AutocompleteHighlightChangeReason = ('auto' | 'mouse' | 'keyboard'); export type AutocompleteHighlightChangeDirectionType = ('next' | 'previous'); export type AutocompleteHighlightChangeDiffType = (number | 'reset' | 'start' | 'end'); -export type AutocompleteChangeReason = ('selectOption' | 'removeOption'); +export type AutocompleteChangeReason = ('selectOption' | 'removeOption' | 'blur'); export interface AutocompleteChangeDetails { option: T; @@ -171,13 +171,22 @@ export function useAutocomplete(false); const [searchValue, setSearchValue] = React.useState(''); + const [inputPristine, setInputPristine] = React.useState(true); const [value, setValue] = useControllableState({ value: valueProp, defaultValue, }); + const inputValueIsSelectedValue = !multiple && value != null + && searchValue === getOptionLabel(value as T); + const filteredOptions = popupOpen - ? filterOptions(options, searchValue, value, getOptionLabel) + ? filterOptions( + options, + inputValueIsSelectedValue && inputPristine ? '' : searchValue, + value, + getOptionLabel, + ) : []; const validOptionIndex = (index: number, direction: AutocompleteHighlightChangeDirectionType) => { @@ -235,6 +244,9 @@ export function useAutocomplete); @@ -446,6 +465,11 @@ export function useAutocomplete) => { + if (listboxRef.current !== null + && listboxRef.current.parentElement?.contains(document.activeElement)) { + return; + } + + handleClose(event); + }; + const handleKeyDown = (event: React.KeyboardEvent) => { // Wait until IME is settled. if (event.which !== 229) { @@ -489,6 +522,25 @@ export function useAutocomplete 0) { + const lastOptionIndex = value.length - 1; + + selectNewValue(event, value[lastOptionIndex], lastOptionIndex, 'removeOption'); + } + break; + default: } } @@ -535,6 +587,10 @@ export function useAutocomplete ({ ref: handleListboxRef, + onMouseDown: (event) => { + // Prevent blur + event.preventDefault(); + }, role: 'listbox', tabIndex: -1, id: `${id}-listbox`, @@ -547,6 +603,7 @@ export function useAutocomplete ({ tabIndex: -1, From e82b1b85a752f51a1d50f091e91652b790525505 Mon Sep 17 00:00:00 2001 From: Kharya1337 Date: Thu, 6 Jul 2023 14:13:25 +0300 Subject: [PATCH 2/6] chore(react-components): Autocomplete small style fixes --- packages/react-components/src/Autocomplete/autocomplete.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-components/src/Autocomplete/autocomplete.tsx b/packages/react-components/src/Autocomplete/autocomplete.tsx index eaba559a..3404da17 100644 --- a/packages/react-components/src/Autocomplete/autocomplete.tsx +++ b/packages/react-components/src/Autocomplete/autocomplete.tsx @@ -295,6 +295,8 @@ const stylesPopover = () => css({ label: 'Autocomplete-popover', minWidth: 240, outline: 0, + marginTop: 1, + borderRadius: 4, maxWidth: 'calc(100% - 32px)', maxHeight: 'calc(100% - 32px)', minHeight: '16px', From 62df182823da9f7f8ca28d4e83b086b66c35a681 Mon Sep 17 00:00:00 2001 From: Kharya1337 Date: Tue, 11 Jul 2023 12:54:24 +0300 Subject: [PATCH 3/6] chore(react-components): add icon rotation for open Autocomplete state --- .../react-components/src/Autocomplete/autocomplete.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/react-components/src/Autocomplete/autocomplete.tsx b/packages/react-components/src/Autocomplete/autocomplete.tsx index 3404da17..110aefc6 100644 --- a/packages/react-components/src/Autocomplete/autocomplete.tsx +++ b/packages/react-components/src/Autocomplete/autocomplete.tsx @@ -213,7 +213,7 @@ const stylesRootMultiple = () => css({ alignItems: 'center', }); -const stylesInputArrowIcon = () => css({ +const stylesInputArrowIcon = (open: boolean) => css({ label: 'Autocomplete-arrow-icon', position: 'absolute', right: '0px', @@ -221,6 +221,9 @@ const stylesInputArrowIcon = () => css({ pointerEvents: 'none', margin: '0px var(--pv-size-base)', color: 'var(--pv-color-gray-10)', + ...(open && { + transform: 'rotate(180deg)', + }), '&[aria-disabled="true"]': { color: 'var(--pv-color-gray-7)', }, @@ -533,7 +536,7 @@ export const Autocomplete = @@ -576,7 +579,7 @@ export const Autocomplete = From dbf0e4382bf99989250ee725a2d5965608ab824e Mon Sep 17 00:00:00 2001 From: Kharya1337 Date: Mon, 7 Aug 2023 19:27:07 +0300 Subject: [PATCH 4/6] chore(react-components): polish styles --- .../src/Autocomplete/autocomplete.story.tsx | 9 +++++++++ .../src/Autocomplete/autocomplete.tsx | 11 +++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/react-components/src/Autocomplete/autocomplete.story.tsx b/packages/react-components/src/Autocomplete/autocomplete.story.tsx index 70d4380d..27ece5e3 100644 --- a/packages/react-components/src/Autocomplete/autocomplete.story.tsx +++ b/packages/react-components/src/Autocomplete/autocomplete.story.tsx @@ -138,6 +138,15 @@ Default.args = { getOptionLabel: (option) => option.title, }; +export const Combobox = Template.bind({}); + +Combobox.args = { + options: top100Films, + placeholder: 'Select a movie', + getOptionLabel: (option) => option.title, + combobox: true, +}; + // @ts-ignore export const Multiple: ComponentStory> = Template .bind({}); diff --git a/packages/react-components/src/Autocomplete/autocomplete.tsx b/packages/react-components/src/Autocomplete/autocomplete.tsx index 4d51b1b7..1a8b061e 100644 --- a/packages/react-components/src/Autocomplete/autocomplete.tsx +++ b/packages/react-components/src/Autocomplete/autocomplete.tsx @@ -238,6 +238,7 @@ const stylesListBox = () => css({ margin: 0, listStyleType: 'none', position: 'relative', + backgroundColor: 'var(--pv-color-gray-1)', padding: '10px 0', }); @@ -280,7 +281,7 @@ const stylesInputSearch = () => css({ padding: 'var(--pv-size-base-3) var(--pv-size-base-3) var(--pv-size-base-2)', }); -const stylesRootInputSearch = () => css({ +const stylesRootInputSearch = (size: AutocompleteProps['size']) => css({ label: 'Autocomplete-root-input-search', flex: 1, marginLeft: 5, @@ -288,6 +289,11 @@ const stylesRootInputSearch = () => css({ '& input': { border: 'none', padding: '0', + // Set height for combobox search same as tag height + height: 'var(--pv-size-base-6)', + ...(size === 'small' && { + height: 'var(--pv-size-base-5)', + }), }, }); @@ -529,7 +535,7 @@ export const Autocomplete = {!disableSearch && !combobox && ( From b9cc5346f0eefb2944703ee424445364bc37444a Mon Sep 17 00:00:00 2001 From: Kharya1337 Date: Thu, 10 Aug 2023 12:17:52 +0300 Subject: [PATCH 5/6] chore(react-components): fix background issue --- packages/react-components/src/Autocomplete/autocomplete.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/src/Autocomplete/autocomplete.tsx b/packages/react-components/src/Autocomplete/autocomplete.tsx index 1a8b061e..c811eaf6 100644 --- a/packages/react-components/src/Autocomplete/autocomplete.tsx +++ b/packages/react-components/src/Autocomplete/autocomplete.tsx @@ -238,7 +238,6 @@ const stylesListBox = () => css({ margin: 0, listStyleType: 'none', position: 'relative', - backgroundColor: 'var(--pv-color-gray-1)', padding: '10px 0', }); @@ -311,6 +310,7 @@ const stylesPopover = () => css({ maxWidth: 'calc(100% - 32px)', maxHeight: 'calc(100% - 32px)', minHeight: '16px', + backgroundColor: 'var(--pv-color-white)', boxShadow: 'var(--pv-shadow-light-low)', }); From c2275d96af466ccac458eee6b88994123d0eb0df Mon Sep 17 00:00:00 2001 From: mariktar <100trk@gmail.com> Date: Fri, 18 Aug 2023 12:42:15 +0300 Subject: [PATCH 6/6] chore(react-components): fixed overlaying and disabled mode --- .../src/Autocomplete/autocomplete.tsx | 114 +++++++++--------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/packages/react-components/src/Autocomplete/autocomplete.tsx b/packages/react-components/src/Autocomplete/autocomplete.tsx index c811eaf6..54d79ad0 100644 --- a/packages/react-components/src/Autocomplete/autocomplete.tsx +++ b/packages/react-components/src/Autocomplete/autocomplete.tsx @@ -128,41 +128,6 @@ const stylesContainer = () => css({ width: '100%', }); -const stylesRootSearchWrapper = (size: AutocompleteProps['size']) => css({ - label: 'Autocomplete', - position: 'relative', - display: 'flex', - flexWrap: 'wrap', - alignItems: 'center', - borderRadius: '4px', - minHeight: 'var(--pv-size-base-8)', - ...(size === 'small' && { - minHeight: 'var(--pv-size-base-6)', - }), - ...(size === 'medium' && { - minHeight: 'var(--pv-size-base-7)', - }), - padding: '0 calc(var(--pv-size-base-2) + 24px) 0 var(--pv-size-base-2)', - backgroundColor: 'var(--pv-color-gray-1)', - boxSizing: 'border-box', - transition: 'background-color 200ms, color 200ms, border-color 200ms', - borderStyle: 'solid', - borderWidth: '1px', - borderColor: 'var(--pv-color-gray-8)', - '&:hover': { - backgroundColor: 'var(--pv-color-gray-3)', - borderColor: 'var(--pv-color-gray-7)', - }, - '&:hover input': { - backgroundColor: 'var(--pv-color-gray-3)', - borderColor: 'var(--pv-color-gray-7)', - }, - '&:focus-within': { - backgroundColor: 'var(--pv-color-secondary-tint-5)', - borderColor: 'var(--pv-color-secondary-tint-3)', - }, -}); - const stylesRoot = (size: AutocompleteProps['size']) => css({ label: 'Autocomplete-root', outline: 'none', @@ -209,6 +174,24 @@ const stylesRoot = (size: AutocompleteProps['size']) => css({ }, }); +const stylesRootSearchWrapper = (size: AutocompleteProps['size']) => css({ + display: 'flex', + alignItems: 'center', + flexWrap: 'wrap', + columnGap: 'var(--pv-size-base)', + minHeight: 'var(--pv-size-base-8)', + ...(size === 'small' && { + minHeight: 'var(--pv-size-base-6)', + }), + ...(size === 'medium' && { + minHeight: 'var(--pv-size-base-7)', + }), + '&:focus-within': { + backgroundColor: 'var(--pv-color-secondary-tint-5)', + borderColor: 'var(--pv-color-secondary-tint-3)', + }, +}); + const stylesRootMultiple = () => css({ label: 'multiple', display: 'inline-flex', @@ -283,9 +266,10 @@ const stylesInputSearch = () => css({ const stylesRootInputSearch = (size: AutocompleteProps['size']) => css({ label: 'Autocomplete-root-input-search', flex: 1, - marginLeft: 5, + marginLeft: 'var(--pv-size-base)', minWidth: '30%', '& input': { + backgroundColor: 'transparent', border: 'none', padding: '0', // Set height for combobox search same as tag height @@ -293,6 +277,9 @@ const stylesRootInputSearch = (size: AutocompleteProps['size']) => css({ ...(size === 'small' && { height: 'var(--pv-size-base-5)', }), + '&:hover': { + backgroundColor: 'transparent', + }, }, }); @@ -303,15 +290,16 @@ const stylesListBoxState = () => css({ const stylesPopover = () => css({ label: 'Autocomplete-popover', - minWidth: 240, + minWidth: '240px', outline: 0, - marginTop: 1, - borderRadius: 4, + marginTop: '1px', + borderRadius: '4px', maxWidth: 'calc(100% - 32px)', maxHeight: 'calc(100% - 32px)', minHeight: '16px', backgroundColor: 'var(--pv-color-white)', boxShadow: 'var(--pv-shadow-light-low)', + zIndex: 1300, }); const stylesTagsList = (isEmbedded = false) => css({ @@ -326,7 +314,7 @@ const stylesTag = (tagsLength: number, limitTags: number, size: AutocompleteProp label: 'Autocomplete-tag', borderRadius: '2px', borderColor: 'var(--pv-color-gray-7)', - margin: '2px var(--pv-size-base) 2px 0', + margin: '2px 0', ...(tagsLength === 1 && { maxWidth: 'calc(100% - var(--pv-size-base))', }), @@ -395,7 +383,7 @@ export const Autocomplete = limitTags) ? (value.length - limitTags) : 0; const valueLimits = more > 0 ? value.slice(0, limitTags) : value; @@ -528,21 +516,33 @@ export const Autocomplete = ['renderRoot'] = (propsRoot, valueRoot) => { if (combobox) { return ( -
- {multiple ? renderedValue : null} - +
+ + {multiple ? renderedValue : null} + +