Commit 75fe3c83 by Boyko Committed by GitHub

Select: scroll into view when navigate with up/down arrows (#22503)

* scroll into view when move item up/down

Signed-off-by: blalov <boiskila@gmail.com>

* update test snapshots

Signed-off-by: blalov <boiskila@gmail.com>
parent 3c21a37b
import React, { Component } from 'react';
import React, { Component, RefObject } from 'react';
import isNil from 'lodash/isNil';
import classNames from 'classnames';
import Scrollbars from 'react-custom-scrollbars';
......@@ -17,6 +17,7 @@ interface Props {
setScrollTop: (event: any) => void;
autoHeightMin?: number | string;
updateAfterMountMs?: number;
scrollRef?: RefObject<Scrollbars>;
}
/**
......@@ -37,7 +38,7 @@ export class CustomScrollbar extends Component<Props> {
constructor(props: Props) {
super(props);
this.ref = React.createRef<Scrollbars>();
this.ref = props.scrollRef || React.createRef<Scrollbars>();
}
updateScroll() {
......@@ -51,7 +52,6 @@ export class CustomScrollbar extends Component<Props> {
componentDidMount() {
this.updateScroll();
// this logic is to make scrollbar visible when content is added body after mount
if (this.props.updateAfterMountMs) {
setTimeout(() => this.updateAfterMount(), this.props.updateAfterMountMs);
......
......@@ -24,6 +24,7 @@ import { SingleValue } from './SingleValue';
import { MultiValueContainer, MultiValueRemove } from './MultiValue';
import { useTheme } from '../../../themes';
import { getSelectStyles } from './getSelectStyles';
import { withSelectArrowNavigation } from '../../Select/withSelectArrowNavigation';
type SelectValue<T> = T | SelectableValue<T> | T[] | Array<SelectableValue<T>>;
......@@ -268,10 +269,10 @@ export function SelectBase<T>({
defaultOptions,
};
}
const NavigatableSelect = withSelectArrowNavigation(ReactSelectComponent);
return (
<>
<ReactSelectComponent
<NavigatableSelect
components={{
MenuList: SelectMenu,
Group: SelectOptionGroup,
......
import React from 'react';
import React, { RefObject, PropsWithChildren, forwardRef } from 'react';
import { useTheme } from '../../../themes/ThemeContext';
import { getSelectStyles } from './getSelectStyles';
import { cx } from 'emotion';
import { SelectableValue } from '@grafana/data';
import { Icon } from '../../Icon/Icon';
import { CustomScrollbar } from '../../CustomScrollbar/CustomScrollbar';
import { ExtendedOptionProps } from '../../Select/SelectOption';
import Scrollbars from 'react-custom-scrollbars';
interface SelectMenuProps {
maxHeight: number;
innerRef: React.Ref<any>;
innerProps: {};
scrollRef: RefObject<Scrollbars>;
}
export const SelectMenu = React.forwardRef<HTMLDivElement, React.PropsWithChildren<SelectMenuProps>>((props, ref) => {
const theme = useTheme();
const styles = getSelectStyles(theme);
const { children, maxHeight, innerRef, innerProps } = props;
export const SelectMenu = forwardRef<HTMLDivElement, PropsWithChildren<SelectMenuProps & ExtendedOptionProps>>(
(props, ref = props.innerRef) => {
const theme = useTheme();
const styles = getSelectStyles(theme);
const { children, maxHeight, innerProps, scrollRef } = props;
return (
<div {...innerProps} className={styles.menu} ref={innerRef} style={{ maxHeight }} aria-label="Select options menu">
<CustomScrollbar autoHide={false} autoHeightMax="inherit" hideHorizontalTrack>
{children}
</CustomScrollbar>
</div>
);
});
return (
<div {...innerProps} className={styles.menu} ref={ref} style={{ maxHeight }} aria-label="Select options menu">
<CustomScrollbar scrollRef={scrollRef} autoHide={false} autoHeightMax="inherit" hideHorizontalTrack>
{children}
</CustomScrollbar>
</div>
);
}
);
SelectMenu.displayName = 'SelectMenu';
......
......@@ -27,6 +27,8 @@ import { PopoverContent } from '../Tooltip/Tooltip';
import { Tooltip } from '../Tooltip/Tooltip';
import { SelectableValue } from '@grafana/data';
import { withSelectArrowNavigation } from './withSelectArrowNavigation';
/**
* Changes in new selects:
* - noOptionsMessage & loadingMessage is of string type
......@@ -42,7 +44,7 @@ interface AsyncProps<T> extends LegacyCommonProps<T>, Omit<SelectAsyncProps<T>,
value?: SelectableValue<T>;
}
interface LegacySelectProps<T> extends LegacyCommonProps<T> {
export interface LegacySelectProps<T> extends LegacyCommonProps<T> {
tooltipContent?: PopoverContent;
noOptionsMessage?: () => string;
isDisabled?: boolean;
......@@ -52,7 +54,7 @@ interface LegacySelectProps<T> extends LegacyCommonProps<T> {
export const MenuList = (props: any) => {
return (
<components.MenuList {...props}>
<CustomScrollbar autoHide={false} autoHeightMax="inherit">
<CustomScrollbar scrollRef={props.scrollRef} autoHide={false} autoHeightMax="inherit">
{props.children}
</CustomScrollbar>
</components.MenuList>
......@@ -125,6 +127,7 @@ export class Select<T> extends PureComponent<LegacySelectProps<T>> {
SelectComponent = Creatable;
creatableOptions.formatCreateLabel = formatCreateLabel ?? ((input: string) => input);
}
const NavigatableSelect = withSelectArrowNavigation(SelectComponent);
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
const selectComponents = { ...Select.defaultProps.components, ...components };
......@@ -132,7 +135,7 @@ export class Select<T> extends PureComponent<LegacySelectProps<T>> {
<WrapInTooltip onCloseMenu={onCloseMenu} onOpenMenu={onOpenMenu} tooltipContent={tooltipContent} isOpen={isOpen}>
{(onOpenMenuInternal, onCloseMenuInternal) => {
return (
<SelectComponent
<NavigatableSelect
captureMenuScroll={false}
classNamePrefix="gf-form-select-box"
className={selectClassNames}
......@@ -170,6 +173,8 @@ export class Select<T> extends PureComponent<LegacySelectProps<T>> {
}
}
const NavigatableAsyncSelect = withSelectArrowNavigation(ReactAsyncSelect);
export class AsyncSelect<T> extends PureComponent<AsyncProps<T>> {
static defaultProps: Partial<AsyncProps<any>> = {
className: '',
......@@ -226,7 +231,7 @@ export class AsyncSelect<T> extends PureComponent<AsyncProps<T>> {
<WrapInTooltip onCloseMenu={onCloseMenu} onOpenMenu={onOpenMenu} tooltipContent={tooltipContent} isOpen={isOpen}>
{(onOpenMenuInternal, onCloseMenuInternal) => {
return (
<ReactAsyncSelect
<NavigatableAsyncSelect
captureMenuScroll={false}
classNamePrefix="gf-form-select-box"
className={selectClassNames}
......
import React from 'react';
import React, { forwardRef } from 'react';
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
// @ts-ignore
......@@ -13,11 +13,10 @@ export interface ExtendedOptionProps extends OptionProps<any> {
};
}
export const SelectOption = (props: ExtendedOptionProps) => {
export const SelectOption = forwardRef((props: ExtendedOptionProps, ref) => {
const { children, isSelected, data } = props;
return (
<components.Option {...props}>
<components.Option {...props} innerRef={ref}>
<div className="gf-form-select-box__desc-option">
{data.imgUrl && <img className="gf-form-select-box__desc-option__img" src={data.imgUrl} />}
<div className="gf-form-select-box__desc-option__body">
......@@ -28,6 +27,6 @@ export const SelectOption = (props: ExtendedOptionProps) => {
</div>
</components.Option>
);
};
});
export default SelectOption;
import React, { useRef, Component, createRef, RefObject, ComponentType, KeyboardEvent } from 'react';
import { ExtendedOptionProps } from './SelectOption';
const scrollIntoView = (optionRef: RefObject<HTMLElement> | null, scrollRef: RefObject<any>) => {
if (!optionRef || !optionRef.current || !scrollRef || !scrollRef.current || !scrollRef.current.container) {
return;
}
const { container, scrollTop } = scrollRef.current;
const option = optionRef.current;
const containerRect = container.getBoundingClientRect();
const optionRect = option.getBoundingClientRect();
if (optionRect.bottom > containerRect.bottom) {
scrollTop(option.offsetTop + option.clientHeight - container.offsetHeight);
} else if (optionRect.top < containerRect.top) {
scrollTop(option.offsetTop);
}
};
export const withSelectArrowNavigation = <P extends any>(WrappedComponent: ComponentType<P>) => {
return class Select extends Component<P> {
focusedOptionRef: RefObject<HTMLElement> | null = null;
scrollRef = createRef();
render() {
const { components } = this.props;
return (
<WrappedComponent
{...this.props}
components={{
...components,
MenuList: (props: ExtendedOptionProps) => {
return <components.MenuList {...props} scrollRef={this.scrollRef} />;
},
Option: (props: ExtendedOptionProps) => {
const innerRef = useRef<HTMLElement>(null);
if (props.isFocused) {
this.focusedOptionRef = innerRef;
}
return <components.Option {...props} ref={innerRef} />;
},
}}
onKeyDown={(e: KeyboardEvent) => {
const { onKeyDown } = this.props;
onKeyDown && onKeyDown(e);
if (e.keyCode === 38 || e.keyCode === 40) {
setTimeout(() => {
scrollIntoView(this.focusedOptionRef, this.scrollRef);
});
}
}}
/>
);
}
};
};
......@@ -97,7 +97,10 @@ exports[`Render when feature toggle editorsCanAdmin is turned off should not ren
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -187,7 +190,10 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......
......@@ -31,7 +31,10 @@ exports[`Render should disable access key id field 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -138,7 +141,10 @@ exports[`Render should disable access key id field 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -211,7 +217,10 @@ exports[`Render should render component 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -318,7 +327,10 @@ exports[`Render should render component 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -391,7 +403,10 @@ exports[`Render should should show access key and secret access key fields 1`] =
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -498,7 +513,10 @@ exports[`Render should should show access key and secret access key fields 1`] =
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -571,7 +589,10 @@ exports[`Render should should show arn role field 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -678,7 +699,10 @@ exports[`Render should should show arn role field 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -751,7 +775,10 @@ exports[`Render should should show credentials profile name field 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -858,7 +885,10 @@ exports[`Render should should show credentials profile name field 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......
......@@ -52,7 +52,10 @@ exports[`Render should disable log analytics credentials form 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -148,7 +151,10 @@ exports[`Render should enable azure log analytics load workspaces button 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -244,7 +250,10 @@ exports[`Render should render component 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......
......@@ -27,7 +27,10 @@ exports[`Render should disable azure monitor secret input 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -173,7 +176,10 @@ exports[`Render should disable azure monitor secret input 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -244,7 +250,10 @@ exports[`Render should enable azure monitor load subscriptions button 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -380,7 +389,10 @@ exports[`Render should enable azure monitor load subscriptions button 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -451,7 +463,10 @@ exports[`Render should render component 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -587,7 +602,10 @@ exports[`Render should render component 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......
......@@ -125,7 +125,10 @@ exports[`Render should disable basic auth password input 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -342,7 +345,10 @@ exports[`Render should hide basic auth fields when switch off 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -559,7 +565,10 @@ exports[`Render should hide white listed cookies input when browser access chose
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -776,7 +785,10 @@ exports[`Render should render component 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......
......@@ -86,7 +86,10 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......@@ -153,7 +156,10 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"Option": Object {
"$$typeof": Symbol(react.forward_ref),
"render": [Function],
},
"SingleValue": [Function],
}
}
......
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