mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
5be23b40b6
commit
25b4aa8d86
@ -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>
|
||||||
|
@ -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
|
||||||
|
1
public/app/core/components/RolePicker/constants.ts
Normal file
1
public/app/core/components/RolePicker/constants.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const MENU_MAX_HEIGHT = 300; // max height for the picker's dropdown menu
|
Loading…
Reference in New Issue
Block a user