Commit eba03905 by Tobias Skarhed Committed by GitHub

New Select: Fix the overflow issue and menu positioning (#22039)

* Fix the overflow issue and menu positioning

* Add portal as a property
parent e9bc8afa
......@@ -17,9 +17,10 @@ interface ButtonSelectProps<T> extends Omit<SelectCommonProps<T>, 'renderControl
interface SelectButtonProps extends Omit<ButtonProps, 'icon'> {
icon?: IconType;
isOpen?: boolean;
innerRef: any;
}
const SelectButton: React.FC<SelectButtonProps> = ({ icon, children, isOpen, ...buttonProps }) => {
const SelectButton: React.FC<SelectButtonProps> = ({ icon, children, isOpen, innerRef, ...buttonProps }) => {
const theme = useTheme();
const styles = {
wrapper: css`
......@@ -42,7 +43,7 @@ const SelectButton: React.FC<SelectButtonProps> = ({ icon, children, isOpen, ...
const buttonIcon = `fa fa-${icon}`;
const caretIcon = isOpen ? 'caret-up' : 'caret-down';
return (
<Button {...buttonProps} icon={buttonIcon}>
<Button {...buttonProps} ref={innerRef} icon={buttonIcon}>
<span className={styles.wrapper}>
<span>{children}</span>
<span className={styles.caretWrap}>
......@@ -73,9 +74,10 @@ export function ButtonSelect<T>({
return (
<SelectBase
{...selectProps}
renderControl={React.forwardRef<any, CustomControlProps<T>>(({ onBlur, onClick, value, isOpen }, _ref) => {
portal={document.body}
renderControl={React.forwardRef<any, CustomControlProps<T>>(({ onBlur, onClick, value, isOpen }, ref) => {
return (
<SelectButton {...buttonProps} onBlur={onBlur} onClick={onClick} isOpen={isOpen}>
<SelectButton {...buttonProps} innerRef={ref} onBlur={onBlur} onClick={onClick} isOpen={isOpen}>
{value ? value.label : placeholder}
</SelectButton>
);
......
......@@ -11,7 +11,7 @@ import { getIconKnob } from '../../../utils/storybook/knobs';
import kebabCase from 'lodash/kebabCase';
export default {
title: 'General/Select',
title: 'Forms/Select',
component: Select,
decorators: [withCenteredStory, withHorizontallyCenteredStory],
};
......@@ -237,14 +237,40 @@ export const customizedControl = () => {
setValue(v);
}}
size="md"
renderControl={({ isOpen, value, ...otherProps }) => {
return <Button {...otherProps}> {isOpen ? 'Open' : 'Closed'}</Button>;
}}
portal={document.body}
renderControl={React.forwardRef(({ isOpen, value, ...otherProps }, ref) => {
return (
<Button {...otherProps} ref={ref}>
{' '}
{isOpen ? 'Open' : 'Closed'}
</Button>
);
})}
{...getDynamicProps()}
/>
);
};
export const autoMenuPlacement = () => {
const [value, setValue] = useState<SelectableValue<string>>();
return (
<>
<div style={{ height: '95vh', display: 'flex', alignItems: 'flex-end' }}>
<Select
options={generateOptions()}
value={value}
onChange={v => {
setValue(v);
}}
size="md"
{...getDynamicProps()}
/>
</div>
</>
);
};
export const customValueCreation = () => {
const [value, setValue] = useState<SelectableValue<string>>();
const [customOptions, setCustomOptions] = useState<Array<SelectableValue<string>>>([]);
......
......@@ -3,12 +3,12 @@ import { SelectableValue } from '@grafana/data';
import { SelectCommonProps, SelectBase, MultiSelectCommonProps, SelectAsyncProps } from './SelectBase';
export function Select<T>(props: SelectCommonProps<T>) {
return <SelectBase {...props} />;
return <SelectBase {...props} portal={props.portal || document.body} />;
}
export function MultiSelect<T>(props: MultiSelectCommonProps<T>) {
// @ts-ignore
return <SelectBase {...props} isMulti />;
return <SelectBase {...props} portal={props.portal || document.body} isMulti />;
}
interface AsyncSelectProps<T> extends Omit<SelectCommonProps<T>, 'options'>, SelectAsyncProps<T> {
......@@ -17,7 +17,7 @@ interface AsyncSelectProps<T> extends Omit<SelectCommonProps<T>, 'options'>, Sel
}
export function AsyncSelect<T>(props: AsyncSelectProps<T>) {
return <SelectBase {...props} />;
return <SelectBase {...props} portal={props.portal || document.body} />;
}
interface AsyncMultiSelectProps<T> extends Omit<MultiSelectCommonProps<T>, 'options'>, SelectAsyncProps<T> {
......@@ -27,5 +27,5 @@ interface AsyncMultiSelectProps<T> extends Omit<MultiSelectCommonProps<T>, 'opti
export function AsyncMultiSelect<T>(props: AsyncMultiSelectProps<T>) {
// @ts-ignore
return <SelectBase {...props} isMulti />;
return <SelectBase {...props} portal={props.portal || document.body} isMulti />;
}
......@@ -18,11 +18,11 @@ const options: Array<SelectableValue<number>> = [
describe('SelectBase', () => {
it('renders without error', () => {
mount(<SelectBase onChange={onChangeHandler} />);
mount(<SelectBase portal={null} onChange={onChangeHandler} />);
});
it('renders empty options information', () => {
const container = mount(<SelectBase onChange={onChangeHandler} isOpen />);
const container = mount(<SelectBase portal={null} onChange={onChangeHandler} isOpen />);
const noopt = container.find({ 'aria-label': 'No options provided' });
expect(noopt).toHaveLength(1);
});
......@@ -30,7 +30,7 @@ describe('SelectBase', () => {
describe('when openMenuOnFocus prop', () => {
describe('is provided', () => {
it('opens on focus', () => {
const container = mount(<SelectBase onChange={onChangeHandler} openMenuOnFocus />);
const container = mount(<SelectBase portal={null} onChange={onChangeHandler} openMenuOnFocus />);
container.find('input').simulate('focus');
const menu = findMenuElement(container);
......@@ -44,7 +44,7 @@ describe('SelectBase', () => {
${'ArrowUp'}
${' '}
`('opens on arrow down/up or space', ({ key }) => {
const container = mount(<SelectBase onChange={onChangeHandler} />);
const container = mount(<SelectBase portal={null} onChange={onChangeHandler} />);
const input = container.find('input');
input.simulate('focus');
input.simulate('keydown', { key });
......@@ -56,14 +56,14 @@ describe('SelectBase', () => {
describe('options', () => {
it('renders menu with provided options', () => {
const container = mount(<SelectBase options={options} onChange={onChangeHandler} isOpen />);
const container = mount(<SelectBase portal={null} options={options} onChange={onChangeHandler} isOpen />);
const menuOptions = container.find({ 'aria-label': 'Select option' });
expect(menuOptions).toHaveLength(2);
});
it('call onChange handler when option is selected', () => {
const spy = jest.fn();
const handler = (value: SelectableValue<number>) => spy(value);
const container = mount(<SelectBase options={options} onChange={handler} isOpen />);
const container = mount(<SelectBase portal={null} options={options} onChange={handler} isOpen />);
const menuOptions = container.find({ 'aria-label': 'Select option' });
expect(menuOptions).toHaveLength(2);
const menuOption = menuOptions.first();
......
......@@ -62,7 +62,10 @@ export interface SelectCommonProps<T> {
size?: FormInputSize;
/** item to be rendered in front of the input */
prefix?: JSX.Element | string | null;
/** Use a custom element to control Select. A proper ref to the renderControl is needed if 'portal' isn't set to null*/
renderControl?: ControlComponent<T>;
/** An element where the dropdown menu should be rendered. In all Select implementations it defaults to document.body .*/
portal?: HTMLElement | null;
}
export interface SelectAsyncProps<T> {
......@@ -81,6 +84,7 @@ export interface MultiSelectCommonProps<T> extends Omit<SelectCommonProps<T>, 'o
export interface SelectBaseProps<T> extends SelectCommonProps<T>, SelectAsyncProps<T> {
invalid?: boolean;
portal: HTMLElement | null;
}
export interface CustomControlProps<T> {
......@@ -173,6 +177,7 @@ export function SelectBase<T>({
renderControl,
width,
invalid,
portal,
components,
}: SelectBaseProps<T>) {
const theme = useTheme();
......@@ -233,6 +238,8 @@ export function SelectBase<T>({
renderControl,
captureMenuScroll: false,
blurInputOnSelect: true,
menuPortalTarget: portal,
menuPlacement: 'auto',
};
// width property is deprecated in favor of size or className
......@@ -334,6 +341,14 @@ export function SelectBase<T>({
}}
styles={{
...resetSelectStyles(),
//These are required for the menu positioning to function
menu: ({ top, bottom, width, position }: any) => ({
top,
bottom,
width,
position,
marginBottom: !!bottom ? '10px' : '0',
}),
}}
className={widthClass}
{...commonSelectProps}
......
......@@ -8,15 +8,16 @@ import { CustomScrollbar } from '../../CustomScrollbar/CustomScrollbar';
interface SelectMenuProps {
maxHeight: number;
innerRef: React.Ref<any>;
innerProps: {};
}
export const SelectMenu = React.forwardRef<HTMLDivElement, React.PropsWithChildren<SelectMenuProps>>((props, ref) => {
const theme = useTheme();
const styles = getSelectStyles(theme);
const { children, maxHeight, innerRef } = props;
const { children, maxHeight, innerRef, innerProps } = props;
return (
<div className={styles.menu} ref={innerRef} style={{ maxHeight }} aria-label="Select options menu">
<div {...innerProps} className={styles.menu} ref={innerRef} style={{ maxHeight }} aria-label="Select options menu">
<CustomScrollbar autoHide={false} autoHeightMax="inherit" hideHorizontalTrack>
{children}
</CustomScrollbar>
......
......@@ -14,7 +14,7 @@ export const getSelectStyles = stylesFactory((theme: GrafanaTheme) => {
menu: css`
background: ${bgColor};
box-shadow: 0px 4px 4px ${menuShadowColor};
position: absolute;
position: relative;
min-width: 100%;
`,
option: css`
......
......@@ -7,7 +7,7 @@ import { UseState } from '../../utils/storybook/UseState';
import { SelectableValue } from '@grafana/data';
import { ButtonSelect } from './ButtonSelect';
const ButtonSelectStories = storiesOf('Panel/Select/ButtonSelect', module);
const ButtonSelectStories = storiesOf('General/Select/ButtonSelect', module);
ButtonSelectStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment