mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Select: Portaling for Select (#22040)
This commit is contained in:
parent
eba0390502
commit
25431f32f0
@ -74,7 +74,6 @@ export function ButtonSelect<T>({
|
||||
return (
|
||||
<SelectBase
|
||||
{...selectProps}
|
||||
portal={document.body}
|
||||
renderControl={React.forwardRef<any, CustomControlProps<T>>(({ onBlur, onClick, value, isOpen }, ref) => {
|
||||
return (
|
||||
<SelectButton {...buttonProps} innerRef={ref} onBlur={onBlur} onClick={onClick} isOpen={isOpen}>
|
||||
|
@ -237,7 +237,6 @@ export const customizedControl = () => {
|
||||
setValue(v);
|
||||
}}
|
||||
size="md"
|
||||
portal={document.body}
|
||||
renderControl={React.forwardRef(({ isOpen, value, ...otherProps }, ref) => {
|
||||
return (
|
||||
<Button {...otherProps} ref={ref}>
|
||||
|
@ -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} portal={props.portal || document.body} />;
|
||||
return <SelectBase {...props} />;
|
||||
}
|
||||
|
||||
export function MultiSelect<T>(props: MultiSelectCommonProps<T>) {
|
||||
// @ts-ignore
|
||||
return <SelectBase {...props} portal={props.portal || document.body} isMulti />;
|
||||
return <SelectBase {...props} 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} portal={props.portal || document.body} />;
|
||||
return <SelectBase {...props} />;
|
||||
}
|
||||
|
||||
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} portal={props.portal || document.body} isMulti />;
|
||||
return <SelectBase {...props} isMulti />;
|
||||
}
|
||||
|
@ -18,11 +18,11 @@ const options: Array<SelectableValue<number>> = [
|
||||
|
||||
describe('SelectBase', () => {
|
||||
it('renders without error', () => {
|
||||
mount(<SelectBase portal={null} onChange={onChangeHandler} />);
|
||||
mount(<SelectBase onChange={onChangeHandler} />);
|
||||
});
|
||||
|
||||
it('renders empty options information', () => {
|
||||
const container = mount(<SelectBase portal={null} onChange={onChangeHandler} isOpen />);
|
||||
const container = mount(<SelectBase 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 portal={null} onChange={onChangeHandler} openMenuOnFocus />);
|
||||
const container = mount(<SelectBase 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 portal={null} onChange={onChangeHandler} />);
|
||||
const container = mount(<SelectBase 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 portal={null} options={options} onChange={onChangeHandler} isOpen />);
|
||||
const container = mount(<SelectBase 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 portal={null} options={options} onChange={handler} isOpen />);
|
||||
const container = mount(<SelectBase options={options} onChange={handler} isOpen />);
|
||||
const menuOptions = container.find({ 'aria-label': 'Select option' });
|
||||
expect(menuOptions).toHaveLength(2);
|
||||
const menuOption = menuOptions.first();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import { SelectableValue, deprecationWarning } from '@grafana/data';
|
||||
// @ts-ignore
|
||||
import { default as ReactSelect } from '@torkelo/react-select';
|
||||
@ -24,6 +24,7 @@ import { SingleValue } from './SingleValue';
|
||||
import { MultiValueContainer, MultiValueRemove } from './MultiValue';
|
||||
import { useTheme } from '../../../themes';
|
||||
import { getSelectStyles } from './getSelectStyles';
|
||||
import { RefForwardingPortal } from '../../Portal/Portal';
|
||||
|
||||
type SelectValue<T> = T | SelectableValue<T> | T[] | Array<SelectableValue<T>>;
|
||||
|
||||
@ -64,8 +65,6 @@ export interface SelectCommonProps<T> {
|
||||
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> {
|
||||
@ -84,7 +83,6 @@ 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> {
|
||||
@ -177,10 +175,10 @@ export function SelectBase<T>({
|
||||
renderControl,
|
||||
width,
|
||||
invalid,
|
||||
portal,
|
||||
components,
|
||||
}: SelectBaseProps<T>) {
|
||||
const theme = useTheme();
|
||||
const portalRef = useRef<HTMLDivElement>();
|
||||
const styles = getSelectStyles(theme);
|
||||
let ReactSelectComponent: ReactSelect | Creatable = ReactSelect;
|
||||
const creatableProps: any = {};
|
||||
@ -238,7 +236,7 @@ export function SelectBase<T>({
|
||||
renderControl,
|
||||
captureMenuScroll: false,
|
||||
blurInputOnSelect: true,
|
||||
menuPortalTarget: portal,
|
||||
menuPortalTarget: portalRef.current,
|
||||
menuPlacement: 'auto',
|
||||
};
|
||||
|
||||
@ -265,95 +263,98 @@ export function SelectBase<T>({
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactSelectComponent
|
||||
components={{
|
||||
MenuList: SelectMenu,
|
||||
Group: SelectOptionGroup,
|
||||
ValueContainer: ValueContainer,
|
||||
Placeholder: (props: any) => (
|
||||
<div
|
||||
{...props.innerProps}
|
||||
className={cx(
|
||||
css(props.getStyles('placeholder', props)),
|
||||
css`
|
||||
display: inline-block;
|
||||
color: hsl(0, 0%, 50%);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
box-sizing: border-box;
|
||||
line-height: 1;
|
||||
`
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
),
|
||||
SelectContainer: (props: any) => (
|
||||
<div
|
||||
{...props.innerProps}
|
||||
className={cx(
|
||||
css(props.getStyles('container', props)),
|
||||
css`
|
||||
position: relative;
|
||||
`,
|
||||
inputSizes()[size]
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
),
|
||||
IndicatorsContainer: IndicatorsContainer,
|
||||
IndicatorSeparator: () => <></>,
|
||||
Control: CustomControl,
|
||||
Option: SelectMenuOptions,
|
||||
ClearIndicator: (props: any) => {
|
||||
const { clearValue } = props;
|
||||
return (
|
||||
<Icon
|
||||
name="times"
|
||||
onMouseDown={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
clearValue();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
LoadingIndicator: (props: any) => {
|
||||
return <Icon name="spinner" className="fa fa-spin" />;
|
||||
},
|
||||
LoadingMessage: (props: any) => {
|
||||
return <div className={styles.loadingMessage}>{loadingMessage}</div>;
|
||||
},
|
||||
NoOptionsMessage: (props: any) => {
|
||||
return (
|
||||
<div className={styles.loadingMessage} aria-label="No options provided">
|
||||
{noOptionsMessage}
|
||||
<>
|
||||
<ReactSelectComponent
|
||||
components={{
|
||||
MenuList: SelectMenu,
|
||||
Group: SelectOptionGroup,
|
||||
ValueContainer: ValueContainer,
|
||||
Placeholder: (props: any) => (
|
||||
<div
|
||||
{...props.innerProps}
|
||||
className={cx(
|
||||
css(props.getStyles('placeholder', props)),
|
||||
css`
|
||||
display: inline-block;
|
||||
color: hsl(0, 0%, 50%);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
box-sizing: border-box;
|
||||
line-height: 1;
|
||||
`
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
DropdownIndicator: (props: any) => <DropdownIndicator isOpen={props.selectProps.menuIsOpen} />,
|
||||
SingleValue: SingleValue,
|
||||
MultiValueContainer: MultiValueContainer,
|
||||
MultiValueRemove: MultiValueRemove,
|
||||
...components,
|
||||
}}
|
||||
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}
|
||||
{...creatableProps}
|
||||
{...asyncSelectProps}
|
||||
/>
|
||||
),
|
||||
SelectContainer: (props: any) => (
|
||||
<div
|
||||
{...props.innerProps}
|
||||
className={cx(
|
||||
css(props.getStyles('container', props)),
|
||||
css`
|
||||
position: relative;
|
||||
`,
|
||||
inputSizes()[size]
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
),
|
||||
IndicatorsContainer: IndicatorsContainer,
|
||||
IndicatorSeparator: () => <></>,
|
||||
Control: CustomControl,
|
||||
Option: SelectMenuOptions,
|
||||
ClearIndicator: (props: any) => {
|
||||
const { clearValue } = props;
|
||||
return (
|
||||
<Icon
|
||||
name="times"
|
||||
onMouseDown={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
clearValue();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
LoadingIndicator: (props: any) => {
|
||||
return <Icon name="spinner" className="fa fa-spin" />;
|
||||
},
|
||||
LoadingMessage: (props: any) => {
|
||||
return <div className={styles.loadingMessage}>{loadingMessage}</div>;
|
||||
},
|
||||
NoOptionsMessage: (props: any) => {
|
||||
return (
|
||||
<div className={styles.loadingMessage} aria-label="No options provided">
|
||||
{noOptionsMessage}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
DropdownIndicator: (props: any) => <DropdownIndicator isOpen={props.selectProps.menuIsOpen} />,
|
||||
SingleValue: SingleValue,
|
||||
MultiValueContainer: MultiValueContainer,
|
||||
MultiValueRemove: MultiValueRemove,
|
||||
...components,
|
||||
}}
|
||||
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}
|
||||
{...creatableProps}
|
||||
{...asyncSelectProps}
|
||||
/>
|
||||
<RefForwardingPortal ref={portalRef as any} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import ReactDOM from 'react-dom';
|
||||
interface Props {
|
||||
className?: string;
|
||||
root?: HTMLElement;
|
||||
forwardedRef?: any;
|
||||
}
|
||||
|
||||
export class Portal extends PureComponent<Props> {
|
||||
@ -29,8 +30,14 @@ export class Portal extends PureComponent<Props> {
|
||||
render() {
|
||||
// Default z-index is high to make sure
|
||||
return ReactDOM.createPortal(
|
||||
<div style={{ zIndex: 1051, position: 'relative' }}>{this.props.children}</div>,
|
||||
<div style={{ zIndex: 1051, position: 'relative' }} ref={this.props.forwardedRef}>
|
||||
{this.props.children}
|
||||
</div>,
|
||||
this.node
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const RefForwardingPortal = React.forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
return <Portal {...props} forwardedRef={ref} />;
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user