RolePicker: Fix menu position on smaller screens (#48429)

* RolePicker: Fix menu position on smaller screens

* RolePicker: Add comment

* Add offset for the bottom position
This commit is contained in:
Alex Khomenko 2022-05-06 14:05:58 +03:00 committed by GitHub
parent 5be23b40b6
commit 25b4aa8d86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 37 additions and 11 deletions

View File

@ -1,10 +1,11 @@
import React, { FormEvent, useCallback, useEffect, useState } from 'react';
import React, { FormEvent, useCallback, useEffect, useState, useRef } from 'react';
import { ClickOutsideWrapper, HorizontalGroup, Spinner } from '@grafana/ui';
import { Role, OrgRole } from 'app/types';
import { RolePickerInput } from './RolePickerInput';
import { RolePickerMenu } from './RolePickerMenu';
import { MENU_MAX_HEIGHT } from './constants';
export interface Props {
builtInRole?: OrgRole;
@ -23,7 +24,6 @@ export const RolePicker = ({
builtInRole,
appliedRoles,
roleOptions,
builtInRoles,
disabled,
isLoading,
builtinRolesDisabled,
@ -35,11 +35,28 @@ export const RolePicker = ({
const [selectedRoles, setSelectedRoles] = useState<Role[]>(appliedRoles);
const [selectedBuiltInRole, setSelectedBuiltInRole] = useState<OrgRole | undefined>(builtInRole);
const [query, setQuery] = useState('');
const [offset, setOffset] = useState(0);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
setSelectedRoles(appliedRoles);
}, [appliedRoles]);
useEffect(() => {
const dimensions = ref?.current?.getBoundingClientRect();
if (!dimensions || !isOpen) {
return;
}
const { bottom, top } = dimensions;
const distance = window.innerHeight - bottom;
const offset = bottom - top + 10; // Add extra 10px to offset to account for border and outline
if (distance < MENU_MAX_HEIGHT) {
setOffset(offset);
} else {
setOffset(-offset);
}
}, [isOpen]);
const onOpen = useCallback(
(event: FormEvent<HTMLElement>) => {
if (!disabled) {
@ -103,7 +120,7 @@ export const RolePicker = ({
}
return (
<div data-testid="role-picker" style={{ position: 'relative' }}>
<div data-testid="role-picker" style={{ position: 'relative' }} ref={ref}>
<ClickOutsideWrapper onClick={onClickOutside}>
<RolePickerInput
builtInRole={selectedBuiltInRole}
@ -120,7 +137,6 @@ export const RolePicker = ({
<RolePickerMenu
options={getOptions()}
builtInRole={selectedBuiltInRole}
builtInRoles={builtInRoles}
appliedRoles={appliedRoles}
onBuiltInRoleSelect={onBuiltInRoleSelect}
onSelect={onSelect}
@ -128,6 +144,7 @@ export const RolePicker = ({
showGroups={query.length === 0 || query.trim() === ''}
builtinRolesDisabled={builtinRolesDisabled}
showBuiltInRole={showBuiltInRole}
offset={offset}
/>
)}
</ClickOutsideWrapper>

View File

@ -17,7 +17,7 @@ import {
import { getSelectStyles } from '@grafana/ui/src/components/Select/getSelectStyles';
import { OrgRole, Role } from 'app/types';
type BuiltInRoles = Record<string, Role[]>;
import { MENU_MAX_HEIGHT } from './constants';
const BuiltinRoles = Object.values(OrgRole);
const BuiltinRoleOption: Array<SelectableValue<OrgRole>> = BuiltinRoles.map((r) => ({
@ -32,7 +32,6 @@ const fixedRoleGroupNames: Record<string, string> = {
interface RolePickerMenuProps {
builtInRole?: OrgRole;
builtInRoles?: BuiltInRoles;
options: Role[];
appliedRoles: Role[];
showGroups?: boolean;
@ -42,11 +41,11 @@ interface RolePickerMenuProps {
onBuiltInRoleSelect?: (role: OrgRole) => void;
onUpdate: (newRoles: string[], newBuiltInRole?: OrgRole) => void;
onClear?: () => void;
offset: number;
}
export const RolePickerMenu = ({
builtInRole,
builtInRoles,
options,
appliedRoles,
showGroups,
@ -56,6 +55,7 @@ export const RolePickerMenu = ({
onBuiltInRoleSelect,
onUpdate,
onClear,
offset,
}: RolePickerMenuProps): JSX.Element => {
const [selectedOptions, setSelectedOptions] = useState<Role[]>(appliedRoles);
const [selectedBuiltInRole, setSelectedBuiltInRole] = useState<OrgRole | undefined>(builtInRole);
@ -63,7 +63,6 @@ export const RolePickerMenu = ({
const [openedMenuGroup, setOpenedMenuGroup] = useState('');
const [subMenuOptions, setSubMenuOptions] = useState<Role[]>([]);
const subMenuNode = useRef<HTMLDivElement | null>(null);
const theme = useTheme2();
const styles = getSelectStyles(theme);
const customStyles = useStyles2(getStyles);
@ -175,9 +174,18 @@ export const RolePickerMenu = ({
};
return (
<div className={cx(styles.menu, customStyles.menuWrapper)}>
<div
className={cx(
styles.menu,
customStyles.menuWrapper,
css`
bottom: ${offset > 0 ? `${offset}px` : 'unset'};
top: ${offset < 0 ? `${Math.abs(offset)}px` : 'unset'};
`
)}
>
<div className={customStyles.menu} aria-label="Role picker menu">
<CustomScrollbar autoHide={false} autoHeightMax="300px" hideHorizontalTrack hideVerticalTrack>
<CustomScrollbar autoHide={false} autoHeightMax={`${MENU_MAX_HEIGHT}px`} hideHorizontalTrack hideVerticalTrack>
{showBuiltInRole && (
<div className={customStyles.menuSection}>
<div className={customStyles.groupHeader}>Built-in roles</div>
@ -327,7 +335,7 @@ export const RolePickerSubMenu = ({
return (
<div className={customStyles.subMenu} aria-label="Role picker submenu">
<CustomScrollbar autoHide={false} autoHeightMax="300px" hideHorizontalTrack>
<CustomScrollbar autoHide={false} autoHeightMax={`${MENU_MAX_HEIGHT}px`} hideHorizontalTrack>
<div className={styles.optionBody}>
{options.map((option, i) => (
<RoleMenuOption

View File

@ -0,0 +1 @@
export const MENU_MAX_HEIGHT = 300; // max height for the picker's dropdown menu