Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Select): add opportunity to apply maxHeight style to popup via popupClassName property #537

Merged
merged 6 commits into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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