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 { ClickOutsideWrapper, HorizontalGroup, Spinner } from '@grafana/ui';
import { Role, OrgRole } from 'app/types'; import { Role, OrgRole } from 'app/types';
import { RolePickerInput } from './RolePickerInput'; import { RolePickerInput } from './RolePickerInput';
import { RolePickerMenu } from './RolePickerMenu'; import { RolePickerMenu } from './RolePickerMenu';
import { MENU_MAX_HEIGHT } from './constants';
export interface Props { export interface Props {
builtInRole?: OrgRole; builtInRole?: OrgRole;
@ -23,7 +24,6 @@ export const RolePicker = ({
builtInRole, builtInRole,
appliedRoles, appliedRoles,
roleOptions, roleOptions,
builtInRoles,
disabled, disabled,
isLoading, isLoading,
builtinRolesDisabled, builtinRolesDisabled,
@ -35,11 +35,28 @@ export const RolePicker = ({
const [selectedRoles, setSelectedRoles] = useState<Role[]>(appliedRoles); const [selectedRoles, setSelectedRoles] = useState<Role[]>(appliedRoles);
const [selectedBuiltInRole, setSelectedBuiltInRole] = useState<OrgRole | undefined>(builtInRole); const [selectedBuiltInRole, setSelectedBuiltInRole] = useState<OrgRole | undefined>(builtInRole);
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [offset, setOffset] = useState(0);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
setSelectedRoles(appliedRoles); setSelectedRoles(appliedRoles);
}, [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( const onOpen = useCallback(
(event: FormEvent<HTMLElement>) => { (event: FormEvent<HTMLElement>) => {
if (!disabled) { if (!disabled) {
@ -103,7 +120,7 @@ export const RolePicker = ({
} }
return ( return (
<div data-testid="role-picker" style={{ position: 'relative' }}> <div data-testid="role-picker" style={{ position: 'relative' }} ref={ref}>
<ClickOutsideWrapper onClick={onClickOutside}> <ClickOutsideWrapper onClick={onClickOutside}>
<RolePickerInput <RolePickerInput
builtInRole={selectedBuiltInRole} builtInRole={selectedBuiltInRole}
@ -120,7 +137,6 @@ export const RolePicker = ({
<RolePickerMenu <RolePickerMenu
options={getOptions()} options={getOptions()}
builtInRole={selectedBuiltInRole} builtInRole={selectedBuiltInRole}
builtInRoles={builtInRoles}
appliedRoles={appliedRoles} appliedRoles={appliedRoles}
onBuiltInRoleSelect={onBuiltInRoleSelect} onBuiltInRoleSelect={onBuiltInRoleSelect}
onSelect={onSelect} onSelect={onSelect}
@ -128,6 +144,7 @@ export const RolePicker = ({
showGroups={query.length === 0 || query.trim() === ''} showGroups={query.length === 0 || query.trim() === ''}
builtinRolesDisabled={builtinRolesDisabled} builtinRolesDisabled={builtinRolesDisabled}
showBuiltInRole={showBuiltInRole} showBuiltInRole={showBuiltInRole}
offset={offset}
/> />
)} )}
</ClickOutsideWrapper> </ClickOutsideWrapper>

View File

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

View File

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