Skip to content

Commit

Permalink
feat(Select): add opportunity to apply maxHeight style to popup via p…
Browse files Browse the repository at this point in the history
…opupClassName property (#537)
  • Loading branch information
korvin89 authored Feb 16, 2023
1 parent 4f287b3 commit 16786e0
Show file tree
Hide file tree
Showing 15 changed files with 107 additions and 121 deletions.
3 changes: 2 additions & 1 deletion src/components/List/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class List<T = unknown> extends React.Component<ListProps<T>, ListState<T
}

render() {
const {emptyPlaceholder, virtualized, className, itemsClassName} = this.props;
const {emptyPlaceholder, virtualized, className, itemsClassName, qa} = this.props;

const {items} = this.state;

Expand All @@ -106,6 +106,7 @@ export class List<T = unknown> extends React.Component<ListProps<T>, ListState<T
{({mobile}) => (
<div
className={b({mobile}, className)}
data-qa={qa}
tabIndex={-1}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
Expand Down
3 changes: 2 additions & 1 deletion src/components/List/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type {ReactNode} from 'react';
import type {TextInputSize} from '../TextInput';
import type {QAProps} from '../types';

export type ListSortHandleAlign = 'left' | 'right';

export type ListSortParams = {oldIndex: number; newIndex: number};

export type ListItemData<T> = T & {disabled?: boolean};

export type ListProps<T = unknown> = {
export type ListProps<T = unknown> = QAProps & {
items: ListItemData<T>[];
className?: string;
itemClassName?: string;
Expand Down
22 changes: 2 additions & 20 deletions src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ import {
getSelectedOptionsContent,
getListItems,
getActiveItem,
getListHeight,
getPopupMinWidth,
getPopupVerticalOffset,
getFilteredFlattenOptions,
findItemIndexByQuickSearch,
activateFirstClickableItem,
Expand Down Expand Up @@ -71,7 +68,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
filterable = false,
disablePortal,
} = props;
const [{controlRect, filter}, dispatch] = React.useReducer(reducer, initialState);
const [{filter}, dispatch] = React.useReducer(reducer, initialState);
const controlRef = React.useRef<HTMLElement>(null);
const filterRef = React.useRef<SelectFilterRef>(null);
const listRef = React.useRef<List<FlattenOption>>(null);
Expand All @@ -98,15 +95,6 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(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]);

Expand Down Expand Up @@ -182,13 +170,10 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(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: ''}});
}
Expand Down Expand Up @@ -235,11 +220,10 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
className={popupClassName}
controlRef={controlRef}
width={popupWidth}
minWidth={popupMinWidth}
verticalOffset={popupVerticalOffset}
open={open}
handleClose={handleClose}
disablePortal={disablePortal}
virtualized={virtualized}
>
{filterable && (
<SelectFilter
Expand All @@ -258,8 +242,6 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
size={size}
value={value}
flattenOptions={filteredFlattenOptions}
listHeight={listHeight}
filterHeight={filterHeight}
multiple={multiple}
virtualized={virtualized}
onOptionClick={handleOptionClick}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export const SelectFilter = React.forwardRef<SelectFilterRef, SelectFilterProps>
React.useImperativeHandle(
ref,
() => ({
getHeight: () => wrapRef.current?.getBoundingClientRect().height,
focus: () => inputRef.current?.focus({preventScroll: true}),
}),
[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
39 changes: 18 additions & 21 deletions src/components/Select/components/SelectList/SelectList.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -15,8 +15,6 @@ type SelectListProps = {
size: NonNullable<SelectProps['size']>;
value: NonNullable<SelectProps['value']>;
flattenOptions: FlattenOption[];
listHeight: number;
filterHeight: number;
multiple?: boolean;
virtualized?: boolean;
};
Expand All @@ -29,11 +27,14 @@ export const SelectList = React.forwardRef<List<FlattenOption>, SelectListProps>
size,
flattenOptions,
value,
listHeight,
filterHeight,
multiple,
virtualized,
} = props;
const optionsHeight = getOptionsHeight({
options: flattenOptions,
getOptionHeight,
size,
});

const getItemHeight = React.useCallback(
(option: FlattenOption, index: number) => {
Expand Down Expand Up @@ -61,23 +62,19 @@ export const SelectList = React.forwardRef<List<FlattenOption>, SelectListProps>
);

return (
<div
<List
ref={ref}
className={selectListBlock({size, virtualized})}
style={{maxHeight: `calc(90vh - ${filterHeight}px)`}}
data-qa={SelectQa.LIST}
>
<List
ref={ref}
itemClassName={selectListBlock('item')}
itemHeight={getItemHeight}
itemsHeight={virtualized ? listHeight : undefined}
items={flattenOptions}
filterable={false}
virtualized={virtualized}
renderItem={renderItem}
onItemClick={onOptionClick}
/>
</div>
qa={SelectQa.LIST}
itemClassName={selectListBlock('item')}
itemHeight={getItemHeight}
itemsHeight={virtualized ? optionsHeight : undefined}
items={flattenOptions}
filterable={false}
virtualized={virtualized}
renderItem={renderItem}
onItemClick={onOptionClick}
/>
);
});

Expand Down
9 changes: 9 additions & 0 deletions src/components/Select/components/SelectPopup/SelectPopup.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@use '../../../variables';

$block: '.#{variables.$ns-new}select-popup';

#{$block} {
display: flex;
flex-direction: column;
max-height: 90vh;
}
28 changes: 11 additions & 17 deletions src/components/Select/components/SelectPopup/SelectPopup.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLElement>;
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) => (
<Popup
className={className}
className={b(null, className)}
qa={SelectQa.POPUP}
style={{width, minWidth}}
anchorRef={controlRef}
offset={[BORDER_WIDTH, verticalOffset]}
placement={['bottom-start', 'top-start']}
placement={['bottom-start', 'bottom-end', 'top-start', 'top-end']}
offset={[BORDER_WIDTH, BORDER_WIDTH]}
open={open}
onClose={handleClose}
disablePortal={disablePortal}
restoreFocus
restoreFocusRef={controlRef}
modifiers={getModifiers({width, disablePortal, virtualized})}
>
{children}
</Popup>
Expand Down
47 changes: 47 additions & 0 deletions src/components/Select/components/SelectPopup/modifiers.ts
Original file line number Diff line number Diff line change
@@ -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<SelectPopupProps, 'width' | 'disablePortal' | 'virtualized'>,
) => {
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<Modifier<'preventOverflow', {}>, 'name' | 'options'> = {
name: 'preventOverflow',
options: {padding: 10, altBoundary: disablePortal, altAxis: true},
};

return [sameWidth, preventOverflow];
};
12 changes: 12 additions & 0 deletions src/components/Select/components/SelectPopup/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type React from 'react';

export type SelectPopupProps = {
handleClose: () => void;
width?: number;
open?: boolean;
controlRef?: React.RefObject<HTMLElement>;
children?: React.ReactNode;
className?: string;
disablePortal?: boolean;
virtualized?: boolean;
};
2 changes: 0 additions & 2 deletions src/components/Select/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ export const SIZE_TO_ITEM_HEIGHT: Record<NonNullable<SelectProps['size']>, 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;
Expand Down
4 changes: 0 additions & 4 deletions src/components/Select/store/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
3 changes: 1 addition & 2 deletions src/components/Select/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Action>;
1 change: 0 additions & 1 deletion src/components/Select/types-misc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export type SelectFilterRef = {
getHeight: () => number | undefined;
focus: () => void;
};
Loading

0 comments on commit 16786e0

Please sign in to comment.