From 09af9f923382c17bd579a326f678df27bfb1cbb2 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 3 Nov 2023 10:55:38 +0700 Subject: [PATCH] refactor(input-autocomplete): refactoring (#918) --- .changeset/clever-snakes-turn.md | 22 ++ .changeset/giant-avocados-guess.md | 6 + packages/base-modal/src/Component.tsx | 4 +- packages/input-autocomplete/package.json | 2 +- .../src/Component.desktop.tsx | 47 +-- .../src/Component.mobile.tsx | 276 +++++---------- .../src/Component.modal.mobile.tsx | 228 +------------ .../src/Component.responsive.tsx | 40 +-- .../input-autocomplete/src/Component.test.tsx | 108 +++--- ...-mobile-interactions-fill-value-2-snap.png | 4 +- ...al-mobile-interactions-fill-value-snap.png | 4 +- .../src/__snapshots__/Component.test.tsx.snap | 66 ++-- .../src/autocomplete-field/Component.tsx | 4 +- .../autocomplete-mobile-field/Component.tsx | 6 +- .../src/component.screenshots.test.tsx | 5 +- .../input-autocomplete/src/desktop/index.ts | 3 +- .../src/docs/Component.stories.tsx | 256 ++++---------- .../src/docs/description.mdx | 314 +++--------------- packages/input-autocomplete/src/index.ts | 6 +- .../input-autocomplete/src/mobile.module.css | 13 +- .../input-autocomplete/src/mobile/index.ts | 9 +- packages/input-autocomplete/src/types.ts | 77 +++++ packages/input-autocomplete/src/utils.ts | 3 + packages/input-autocomplete/tsconfig.json | 3 +- .../src/Component.desktop.tsx | 70 +--- .../src/Component.mobile.tsx | 141 +------- ...-mobile-interactions-autocomplete-snap.png | 4 +- .../Component.tsx | 70 ++-- .../src/docs/description.mdx | 2 + .../international-phone-input/src/types.ts | 13 +- .../src/utils/index.ts | 6 +- packages/intl-phone-input/src/component.tsx | 8 +- .../src/components/base-select/Component.tsx | 58 ++-- .../src/components/footer/Component.tsx | 26 +- .../select/src/components/footer/index.ts | 2 +- .../src/presets/useSelectWithApply/hook.tsx | 12 +- packages/select/src/shared/index.ts | 1 + packages/select/src/typings.ts | 5 +- 38 files changed, 632 insertions(+), 1292 deletions(-) create mode 100644 .changeset/clever-snakes-turn.md create mode 100644 .changeset/giant-avocados-guess.md create mode 100644 packages/input-autocomplete/src/types.ts create mode 100644 packages/input-autocomplete/src/utils.ts diff --git a/.changeset/clever-snakes-turn.md b/.changeset/clever-snakes-turn.md new file mode 100644 index 0000000000..c8309e2b80 --- /dev/null +++ b/.changeset/clever-snakes-turn.md @@ -0,0 +1,22 @@ +--- +'@alfalab/core-components-input-autocomplete': major +--- + +- Мобильный компонент приведен в соответствие с десктопным, теперь оба компонента имеют одинаковый список пропсов, за некоторым исключением. +- Удалены пропы onFilter, filter, onClearFilter и др, которые раньше использовались только в мобильном компоненте + +## Миграция с предыдущей версии +Из мобильного компонента удалено дополнительное состояние для фильтра, +соответственно были удалены пропы onFilter, filter, onClearFilter. +Теперь при открытии шторки в инпут будет попадать состояние, переданное через проп value, как и у десктопного компонента, +а при вводе значения в инпут будет вызываться коллбэк onInput. При нажатии кнопок "Отмена" и "Продолжить" также будет вызываться onInput. +После апдейта нужно заменить +```jsx + +``` +на +```jsx + +``` + +Примеры можете посмотреть в нашем [сторибуке](https://core-ds.github.io/core-components/master/?path=/docs/inputautocomplete--docs) diff --git a/.changeset/giant-avocados-guess.md b/.changeset/giant-avocados-guess.md new file mode 100644 index 0000000000..d75eded7a3 --- /dev/null +++ b/.changeset/giant-avocados-guess.md @@ -0,0 +1,6 @@ +--- +'@alfalab/core-components-select': minor +--- + +- В searchProps добавлена возможность прокинуть кастомную функцию фильтрации +- Фокус в поле поиска устанавливается после вызова transitionProps.onEntered, а не по таймауту как раньше diff --git a/packages/base-modal/src/Component.tsx b/packages/base-modal/src/Component.tsx index b4d767cd8b..3ef0a02058 100644 --- a/packages/base-modal/src/Component.tsx +++ b/packages/base-modal/src/Component.tsx @@ -333,7 +333,7 @@ export const BaseModal = forwardRef( if (hasHeader) { setHeaderHighlighted( !isScrolledToTop(scrollableNodeRef.current) && - componentNodeRef.current.getBoundingClientRect().top - headerOffset <= 0, + componentNodeRef.current.getBoundingClientRect().top - headerOffset <= 1, ); } @@ -341,7 +341,7 @@ export const BaseModal = forwardRef( setFooterHighlighted( !isScrolledToBottom(scrollableNodeRef.current) && componentNodeRef.current.getBoundingClientRect().bottom >= - window.innerHeight, + window.innerHeight - 1, ); } }, [hasFooter, hasHeader, headerOffset]); diff --git a/packages/input-autocomplete/package.json b/packages/input-autocomplete/package.json index 9d34392988..944ad13cc6 100644 --- a/packages/input-autocomplete/package.json +++ b/packages/input-autocomplete/package.json @@ -14,12 +14,12 @@ "react": "^16.9.0 || ^17.0.1 || ^18.0.0" }, "dependencies": { - "@alfalab/core-components-button": "^9.1.0", "@alfalab/core-components-form-control": "^10.2.0", "@alfalab/core-components-input": "^12.3.0", "@alfalab/core-components-popover": "^6.1.0", "@alfalab/core-components-select": "^15.3.0", "@alfalab/core-components-shared": "^0.7.0", + "@alfalab/core-components-mq": "^4.2.0", "@alfalab/hooks": "^1.13.0", "classnames": "^2.3.1", "lodash.throttle": "^4.1.1", diff --git a/packages/input-autocomplete/src/Component.desktop.tsx b/packages/input-autocomplete/src/Component.desktop.tsx index 13404ada1a..da5516ac62 100644 --- a/packages/input-autocomplete/src/Component.desktop.tsx +++ b/packages/input-autocomplete/src/Component.desktop.tsx @@ -1,59 +1,18 @@ -import React, { ChangeEvent, FC, forwardRef, RefAttributes } from 'react'; +import React, { forwardRef } from 'react'; -import { InputProps } from '@alfalab/core-components-input'; import { Popover } from '@alfalab/core-components-popover'; import { AnyObject, BaseSelect, - BaseSelectProps, Optgroup as DefaultOptgroup, Option as DefaultOption, OptionsList as DefaultOptionsList, } from '@alfalab/core-components-select/shared'; import { AutocompleteField } from './autocomplete-field'; +import { InputAutocompleteCommonProps } from './types'; -export type InputAutocompleteDesktopProps = Omit< - BaseSelectProps, - 'Field' | 'nativeSelect' | 'searchProps' | 'showSearch' | 'Search' -> & { - /** - * Компонент ввода значения - */ - Input?: FC>; - - /** - * Пропсы, которые будут прокинуты в инпут - */ - inputProps?: InputProps & Record; - - /** - * Значение поля ввода - */ - value?: string; - - /** - * Поле доступно только для чтения - */ - readOnly?: InputProps['readOnly']; - - /** - * Отображение иконки успеха - */ - success?: boolean; - - /** - * Обработчик ввода - */ - onInput?: (event: ChangeEvent) => void; - - /** - * Хранит функцию, с помощью которой можно обновить положение поповера - */ - updatePopover?: BaseSelectProps['updatePopover']; -}; - -export const InputAutocompleteDesktop = forwardRef( +export const InputAutocompleteDesktop = forwardRef( ( { OptionsList = DefaultOptionsList, diff --git a/packages/input-autocomplete/src/Component.mobile.tsx b/packages/input-autocomplete/src/Component.mobile.tsx index 59541cefe7..56e5f8cd4a 100644 --- a/packages/input-autocomplete/src/Component.mobile.tsx +++ b/packages/input-autocomplete/src/Component.mobile.tsx @@ -1,140 +1,75 @@ -import React, { ChangeEvent, ElementType, RefObject, useMemo, useRef, useState } from 'react'; +import React, { Ref, useMemo, useRef, useState } from 'react'; import mergeRefs from 'react-merge-refs'; import cn from 'classnames'; import throttle from 'lodash.throttle'; -import { ButtonMobile, ButtonMobileProps } from '@alfalab/core-components-button/mobile'; -import { Input as CoreInput } from '@alfalab/core-components-input'; -import { SelectMobile, SelectMobileProps } from '@alfalab/core-components-select/mobile'; -import type { - BaseSelectChangePayload, - BaseSelectProps, +import { + SelectMobile, + SelectMobileProps, + SelectModalMobile, +} from '@alfalab/core-components-select/mobile'; +import { + AnyObject, BottomSheetSelectMobileProps, + Footer, + ModalSelectMobileProps, } from '@alfalab/core-components-select/shared'; import { AutocompleteMobileField } from './autocomplete-mobile-field'; +import { InputAutocompleteMobileProps } from './types'; +import { searchFilterStub } from './utils'; import styles from './mobile.module.css'; -export type InputAutocompleteMobileProps = Omit< - BaseSelectProps, - | 'OptionsList' - | 'Checkmark' - | 'onScroll' - | 'nativeSelect' - | 'autocomplete' - | 'valueRenderer' - | 'searchProps' - | 'showSearch' - | 'Search' -> & { - /** - * Обработчик выбора - */ - onChange: (payload: string | BaseSelectChangePayload) => void; - - /** - * Обработчик ввода фильтра. - */ - onFilter: (event: ChangeEvent) => void; - - /** - * Значение поля ввода - */ - value?: string; - - /** - * Значение фильтра. - */ - filter?: string; - - /** - * Обработчик нажатия на кнопку "Отмена". - */ - onCancel?: () => void; - - /** - * Обработчик нажатия на крестик в инпуте фильтра. - */ - onClearFilter?: () => void; - - /** - * Дополнительные пропсы компонента BottomSheet - */ - bottomSheetProps?: BottomSheetSelectMobileProps['bottomSheetProps']; - - /** - * Дополнительные пропсы на слот под заголовком компонента BottomSheet - */ - bottomSheetHeaderAddonsProps?: Record; - - /** - * Дополнительные пропсы на кнопку "продолжить" - */ - continueButtonProps?: ButtonMobileProps; - - /** - * Дополнительные пропсы на кнопку "отмена" - */ - cancelButtonProps?: ButtonMobileProps; - - /** - * Кастомный инпут - */ - Input?: ElementType; -}; - -const SELECTED: string[] = []; - export const InputAutocompleteMobile = React.forwardRef( ( { Input, - bottomSheetProps = {}, - bottomSheetHeaderAddonsProps = {}, - value = '', - filter = '', + value, name, Arrow = null, label, - placeholder, + placeholder = '', size = 's', open: openProp, - onFilter, - onChange, + onInput, onOpen, - onCancel, - onClearFilter, - continueButtonProps, - cancelButtonProps, - selected, multiple, + inputProps, + isBottomSheet = true, + dataTestId, + transitionProps, ...restProps }: InputAutocompleteMobileProps, ref, ) => { const [open, setOpen] = useState(false); - const bottomSheetInputRef = useRef(null); + const frozenValue = useRef(''); + const searchInputRef = useRef(null); const targetRef = useRef(null); - const setBottomSheetVisibility = (isOpen: boolean) => { + const restorePrevValue = () => onInput?.(null, { value: frozenValue.current }); + + const setModalVisibility = (isOpen: boolean) => { if (openProp === undefined) { setOpen(isOpen); } - if (onOpen) { - onOpen({ open: isOpen, name }); + if (isOpen) { + frozenValue.current = value || ''; } + + onOpen?.({ open: isOpen, name }); }; const handleOpen: SelectMobileProps['onOpen'] = (payload) => { - setBottomSheetVisibility(Boolean(payload.open)); + setModalVisibility(Boolean(payload.open)); }; const handleOptionsListTouchMove = useMemo( () => throttle(() => { - const input = bottomSheetInputRef.current; + const input = searchInputRef.current; if (input && document.activeElement === input) { input.blur(); @@ -143,119 +78,86 @@ export const InputAutocompleteMobile = React.forwardRef( [], ); - const handleApply = () => { - setBottomSheetVisibility(false); - onChange(filter); - }; - - const handleChange: SelectMobileProps['onChange'] = (payload) => { - onChange(payload); - - if (multiple) { - // После выбора опции возвращаем фокус в поле ввода. - bottomSheetInputRef.current?.focus(); - } - }; + const handleApply = () => setModalVisibility(false); const handleCancel = () => { - setBottomSheetVisibility(false); - - if (onCancel) { - onCancel(); - } + setModalVisibility(false); + restorePrevValue(); }; - const handleInputFocus = (event: React.FocusEvent) => { - const input = bottomSheetInputRef.current; - - // Перед закрытием шторки снимаем фокус с инпута, чтобы предотвратить скачок шторки. - if ( - event.relatedTarget === targetRef.current && - input && - input === document.activeElement - ) { - input.blur(); - } + const handleExiting = (node: HTMLElement) => { + targetRef.current?.focus(); + transitionProps?.onExiting?.(node); }; - const getBottomSheetProps = (): InputAutocompleteMobileProps['bottomSheetProps'] => { - const Component = Input || CoreInput; - - return { - actionButton: ( -
- - Отмена - - - Продолжить - -
- ), - title: label || placeholder, - bottomAddons: ( - , - ])} - /> - ), - initialHeight: 'full', - ...bottomSheetProps, - containerProps: { - onTouchMove: handleOptionsListTouchMove, - ...bottomSheetProps.containerProps, - }, - }; + const isOpen = Boolean(open || openProp); + + const Component = isBottomSheet ? SelectMobile : SelectModalMobile; + + const componentProps: + | ModalSelectMobileProps['modalProps'] + | BottomSheetSelectMobileProps['bottomSheetProps'] = { + title: label || placeholder, + onClose: restorePrevValue, + transitionProps: { + ...transitionProps, + onExiting: handleExiting, + }, + [isBottomSheet ? 'containerProps' : 'componentDivProps']: { + onTouchMove: handleOptionsListTouchMove, + }, }; return ( - ]), + onChange: onInput, + }, + }} + Search={Input} ref={mergeRefs([targetRef, ref])} - selected={selected || SELECTED} - open={Boolean(open || openProp)} + open={isOpen} onOpen={handleOpen} Arrow={Arrow} - onChange={handleChange} placeholder={placeholder} label={label} size={size} name={name} multiple={multiple} - bottomSheetProps={getBottomSheetProps()} optionsListProps={{ - showFooter: false, - ...(restProps.optionsListProps as Record), + footer: ( +