diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx index e9b7d59f4..6faec0ffb 100644 --- a/src/components/List/List.tsx +++ b/src/components/List/List.tsx @@ -97,7 +97,7 @@ export class List extends React.Component, ListState extends React.Component, ListState (
= T & {disabled?: boolean}; -export type ListProps = { +export type ListProps = QAProps & { items: ListItemData[]; className?: string; itemClassName?: string; diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx index 2729a2309..bb4ad818e 100644 --- a/src/components/Select/Select.tsx +++ b/src/components/Select/Select.tsx @@ -14,9 +14,6 @@ import { getSelectedOptionsContent, getListItems, getActiveItem, - getListHeight, - getPopupMinWidth, - getPopupVerticalOffset, getFilteredFlattenOptions, findItemIndexByQuickSearch, activateFirstClickableItem, @@ -71,7 +68,7 @@ export const Select = React.forwardRef(function filterable = false, disablePortal, } = props; - const [{controlRect, filter}, dispatch] = React.useReducer(reducer, initialState); + const [{filter}, dispatch] = React.useReducer(reducer, initialState); const controlRef = React.useRef(null); const filterRef = React.useRef(null); const listRef = React.useRef>(null); @@ -98,15 +95,6 @@ export const Select = React.forwardRef(function renderSelectedOption, ); const virtualized = filteredFlattenOptions.length >= virtualizationThreshold; - const listHeight = getListHeight({ - options: filteredFlattenOptions, - getOptionHeight, - size, - }); - const filterHeight = filterRef.current?.getHeight() || 0; - const popupHeight = listHeight + filterHeight; - const popupMinWidth = getPopupMinWidth(virtualized, controlRect); - const popupVerticalOffset = getPopupVerticalOffset({height: popupHeight, controlRect}); const handleClose = React.useCallback(() => setOpen(false), [setOpen]); @@ -182,13 +170,10 @@ export const Select = React.forwardRef(function React.useEffect(() => { if (open) { activateFirstClickableItem(listRef); - const nextControlRect = controlRef.current?.getBoundingClientRect(); if (filterable) { filterRef.current?.focus(); } - - dispatch({type: 'SET_CONTROL_RECT', payload: {controlRect: nextControlRect}}); } else { dispatch({type: 'SET_FILTER', payload: {filter: ''}}); } @@ -235,11 +220,10 @@ export const Select = React.forwardRef(function className={popupClassName} controlRef={controlRef} width={popupWidth} - minWidth={popupMinWidth} - verticalOffset={popupVerticalOffset} open={open} handleClose={handleClose} disablePortal={disablePortal} + virtualized={virtualized} > {filterable && ( (function size={size} value={value} flattenOptions={filteredFlattenOptions} - listHeight={listHeight} - filterHeight={filterHeight} multiple={multiple} virtualized={virtualized} onOptionClick={handleOptionClick} diff --git a/src/components/Select/components/SelectFilter/SelectFilter.tsx b/src/components/Select/components/SelectFilter/SelectFilter.tsx index bd37039a4..9d1b3aeb5 100644 --- a/src/components/Select/components/SelectFilter/SelectFilter.tsx +++ b/src/components/Select/components/SelectFilter/SelectFilter.tsx @@ -25,7 +25,6 @@ export const SelectFilter = React.forwardRef React.useImperativeHandle( ref, () => ({ - getHeight: () => wrapRef.current?.getBoundingClientRect().height, focus: () => inputRef.current?.focus({preventScroll: true}), }), [], diff --git a/src/components/Select/components/SelectList/SelectList.scss b/src/components/Select/components/SelectList/SelectList.scss index b3ff36dc9..e9502b8da 100644 --- a/src/components/Select/components/SelectList/SelectList.scss +++ b/src/components/Select/components/SelectList/SelectList.scss @@ -15,7 +15,7 @@ $xl-hor-padding: '12px'; #{$block} { display: flex; margin: 4px 0; - overflow: hidden auto; + overflow: hidden; #{$popupBlock} &:first-child, #{$popupBlock} &:last-child { diff --git a/src/components/Select/components/SelectList/SelectList.tsx b/src/components/Select/components/SelectList/SelectList.tsx index 36a8f02c9..face6ba75 100644 --- a/src/components/Select/components/SelectList/SelectList.tsx +++ b/src/components/Select/components/SelectList/SelectList.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {List} from '../../../List'; import {SelectProps} from '../../types'; -import {FlattenOption, getPopupItemHeight} from '../../utils'; +import {FlattenOption, getOptionsHeight, getPopupItemHeight} from '../../utils'; import {selectListBlock, SelectQa} from '../../constants'; import {GroupLabel} from './GroupLabel'; import {OptionWrap} from './OptionWrap'; @@ -15,8 +15,6 @@ type SelectListProps = { size: NonNullable; value: NonNullable; flattenOptions: FlattenOption[]; - listHeight: number; - filterHeight: number; multiple?: boolean; virtualized?: boolean; }; @@ -29,11 +27,14 @@ export const SelectList = React.forwardRef, SelectListProps> size, flattenOptions, value, - listHeight, - filterHeight, multiple, virtualized, } = props; + const optionsHeight = getOptionsHeight({ + options: flattenOptions, + getOptionHeight, + size, + }); const getItemHeight = React.useCallback( (option: FlattenOption, index: number) => { @@ -61,23 +62,19 @@ export const SelectList = React.forwardRef, SelectListProps> ); return ( -
- -
+ qa={SelectQa.LIST} + itemClassName={selectListBlock('item')} + itemHeight={getItemHeight} + itemsHeight={virtualized ? optionsHeight : undefined} + items={flattenOptions} + filterable={false} + virtualized={virtualized} + renderItem={renderItem} + onItemClick={onOptionClick} + /> ); }); diff --git a/src/components/Select/components/SelectPopup/SelectPopup.scss b/src/components/Select/components/SelectPopup/SelectPopup.scss new file mode 100644 index 000000000..1110a2cfc --- /dev/null +++ b/src/components/Select/components/SelectPopup/SelectPopup.scss @@ -0,0 +1,9 @@ +@use '../../../variables'; + +$block: '.#{variables.$ns-new}select-popup'; + +#{$block} { + display: flex; + flex-direction: column; + max-height: 90vh; +} diff --git a/src/components/Select/components/SelectPopup/SelectPopup.tsx b/src/components/Select/components/SelectPopup/SelectPopup.tsx index 42c11db35..788d2869a 100644 --- a/src/components/Select/components/SelectPopup/SelectPopup.tsx +++ b/src/components/Select/components/SelectPopup/SelectPopup.tsx @@ -1,42 +1,36 @@ import React from 'react'; import {Popup} from '../../../Popup'; +import {blockNew} from '../../../utils/cn'; import {BORDER_WIDTH, SelectQa} from '../../constants'; +import type {SelectPopupProps} from './types'; +import {getModifiers} from './modifiers'; -type SelectPopupProps = { - handleClose: () => void; - verticalOffset: number; - width?: number; - minWidth?: number; - open?: boolean; - controlRef?: React.RefObject; - children?: React.ReactNode; - className?: string; - disablePortal?: boolean; -}; +import './SelectPopup.scss'; + +const b = blockNew('select-popup'); export const SelectPopup = ({ handleClose, - verticalOffset, width, - minWidth, open, controlRef, children, className, disablePortal, + virtualized, }: SelectPopupProps) => ( {children} diff --git a/src/components/Select/components/SelectPopup/modifiers.ts b/src/components/Select/components/SelectPopup/modifiers.ts new file mode 100644 index 000000000..2bf75f0bb --- /dev/null +++ b/src/components/Select/components/SelectPopup/modifiers.ts @@ -0,0 +1,47 @@ +import type {Modifier} from '@popperjs/core'; +import {BORDER_WIDTH, POPUP_MIN_WIDTH_IN_VIRTUALIZE_CASE} from '../../constants'; +import type {SelectPopupProps} from './types'; + +const getMinWidth = (referenceWidth: number, virtualized?: boolean) => { + if (virtualized) { + return referenceWidth > POPUP_MIN_WIDTH_IN_VIRTUALIZE_CASE + ? referenceWidth + : POPUP_MIN_WIDTH_IN_VIRTUALIZE_CASE; + } + + return referenceWidth - BORDER_WIDTH * 2; +}; + +export const getModifiers = ( + args: Pick, +) => { + const {width, disablePortal, virtualized} = args; + + // set popper width styles according anchor rect + const sameWidth: Modifier<'sameWidth', {}> = { + name: 'sameWidth', + enabled: true, + phase: 'beforeWrite', + requires: ['computeStyles'], + fn: ({state}) => { + // prevents styles applying after popup being opened (in case of multiple selection) + if (!state.attributes.popper['data-width-set']) { + const minWidth = getMinWidth(state.rects.reference.width, virtualized); + state.attributes.popper['data-width-set'] = true; + state.styles.popper.minWidth = `${minWidth}px`; + } + + if (typeof width === 'number') { + state.styles.popper.width = `${width}px`; + } + }, + }; + + // prevents the popper from being cut off by moving it so that it stays visible within its boundary area + const preventOverflow: Pick, 'name' | 'options'> = { + name: 'preventOverflow', + options: {padding: 10, altBoundary: disablePortal, altAxis: true}, + }; + + return [sameWidth, preventOverflow]; +}; diff --git a/src/components/Select/components/SelectPopup/types.ts b/src/components/Select/components/SelectPopup/types.ts new file mode 100644 index 000000000..628f8f03f --- /dev/null +++ b/src/components/Select/components/SelectPopup/types.ts @@ -0,0 +1,12 @@ +import type React from 'react'; + +export type SelectPopupProps = { + handleClose: () => void; + width?: number; + open?: boolean; + controlRef?: React.RefObject; + children?: React.ReactNode; + className?: string; + disablePortal?: boolean; + virtualized?: boolean; +}; diff --git a/src/components/Select/constants.ts b/src/components/Select/constants.ts index 2d33f2f51..4c271f357 100644 --- a/src/components/Select/constants.ts +++ b/src/components/Select/constants.ts @@ -16,8 +16,6 @@ export const SIZE_TO_ITEM_HEIGHT: Record, numbe export const GROUP_ITEM_MARGIN_TOP = 5; -export const CONTAINER_VERTICAL_MARGIN = 4; - export const BORDER_WIDTH = 1; export const POPUP_MIN_WIDTH_IN_VIRTUALIZE_CASE = 100; diff --git a/src/components/Select/store/reducer.ts b/src/components/Select/store/reducer.ts index e21267861..075664545 100644 --- a/src/components/Select/store/reducer.ts +++ b/src/components/Select/store/reducer.ts @@ -4,10 +4,6 @@ export const initialState: State = {filter: ''}; export const reducer = (state: State = initialState, action: Action) => { switch (action.type) { - case 'SET_CONTROL_RECT': { - const {controlRect} = action.payload; - return {...state, controlRect}; - } case 'SET_FILTER': { const {filter} = action.payload; return {...state, filter}; diff --git a/src/components/Select/store/types.ts b/src/components/Select/store/types.ts index 7d2c08025..8920c737a 100644 --- a/src/components/Select/store/types.ts +++ b/src/components/Select/store/types.ts @@ -5,9 +5,8 @@ export type State = { controlRect?: DOMRect; }; -type SetControlRect = {type: 'SET_CONTROL_RECT'; payload: {controlRect?: DOMRect}}; type SetFilter = {type: 'SET_FILTER'; payload: {filter: string}}; -export type Action = SetControlRect | SetFilter; +export type Action = SetFilter; export type Dispatch = React.Dispatch; diff --git a/src/components/Select/types-misc.ts b/src/components/Select/types-misc.ts index 7bb9aa21d..a68b66211 100644 --- a/src/components/Select/types-misc.ts +++ b/src/components/Select/types-misc.ts @@ -1,4 +1,3 @@ export type SelectFilterRef = { - getHeight: () => number | undefined; focus: () => void; }; diff --git a/src/components/Select/utils.tsx b/src/components/Select/utils.tsx index cb074e1e8..89e3f8863 100644 --- a/src/components/Select/utils.tsx +++ b/src/components/Select/utils.tsx @@ -1,16 +1,9 @@ import React from 'react'; -import {PopupPlacement} from '../Popup'; import {List, ListItemData} from '../List'; import {KeyCode} from '../constants'; import {SelectProps, SelectOption, SelectOptionGroup} from './types'; import {Option, OptionGroup} from './tech-components'; -import { - BORDER_WIDTH, - CONTAINER_VERTICAL_MARGIN, - GROUP_ITEM_MARGIN_TOP, - SIZE_TO_ITEM_HEIGHT, - POPUP_MIN_WIDTH_IN_VIRTUALIZE_CASE, -} from './constants'; +import {GROUP_ITEM_MARGIN_TOP, SIZE_TO_ITEM_HEIGHT} from './constants'; // "disable" property needs to deactivate group title item in List type GroupTitleItem = {label: string; disabled: true}; @@ -48,7 +41,7 @@ export const getPopupItemHeight = (args: { return getOptionHeight ? getOptionHeight(option) : SIZE_TO_ITEM_HEIGHT[size]; }; -export const getListHeight = (args: { +export const getOptionsHeight = (args: { getOptionHeight?: SelectProps['getOptionHeight']; size: NonNullable; options: FlattenOption[]; @@ -59,35 +52,6 @@ export const getListHeight = (args: { }, 0); }; -export const getPopupVerticalOffset = (args: {height: number; controlRect?: DOMRect}) => { - const {height, controlRect} = args; - - if (!controlRect) { - return BORDER_WIDTH; - } - - const vh = window.innerHeight / 100; - const heigth5vh = vh * 5; - const heigth90vh = vh * 90; - const containerHeight = heigth90vh < height ? heigth90vh : height; - const popupPlacement: PopupPlacement = - controlRect.y + controlRect.height / 2 < window.innerHeight / 2 - ? 'bottom-start' - : 'top-start'; - const screenOffset = - popupPlacement === 'bottom-start' - ? window.innerHeight - controlRect.y - controlRect.height - : controlRect.y; - - let offset = BORDER_WIDTH; - - if (containerHeight > screenOffset) { - offset = (containerHeight - screenOffset) * -1 - heigth5vh - CONTAINER_VERTICAL_MARGIN; - } - - return offset; -}; - const getOptionText = (option: SelectOption): string => { if (typeof option.content === 'string') { return option.content; @@ -181,18 +145,6 @@ export const getOptionsFromChildren = (children: SelectProps['children']) => { }, [] as (SelectOption | SelectOptionGroup)[]); }; -export const getPopupMinWidth = (virtualized?: boolean, controlRect?: DOMRect) => { - const controlWidth = controlRect?.width; - - if (virtualized && controlWidth) { - return controlWidth > POPUP_MIN_WIDTH_IN_VIRTUALIZE_CASE - ? controlWidth - : POPUP_MIN_WIDTH_IN_VIRTUALIZE_CASE; - } - - return controlWidth ? controlWidth - BORDER_WIDTH * 2 : undefined; -}; - export const getNextQuickSearch = (keyCode: string, quickSearch: string) => { // https://www.w3.org/TR/uievents-code/#key-alphanumeric-writing-system const writingSystemKeyPressed = keyCode.length === 1;