mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
New Select: Fix the overflow issue and menu positioning (#22039)
* Fix the overflow issue and menu positioning * Add portal as a property
This commit is contained in:
parent
e9bc8afac8
commit
eba0390502
@ -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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user