mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
RolePicker: Fix submenu position on horizontal space overflow (#50769)
This commit is contained in:
parent
1af63ba5f9
commit
2fbe99c1be
@ -5,7 +5,7 @@ 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';
|
import { MENU_MAX_HEIGHT, ROLE_PICKER_WIDTH } from './constants';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
builtInRole?: OrgRole;
|
builtInRole?: OrgRole;
|
||||||
@ -37,7 +37,7 @@ 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 [offset, setOffset] = useState({ vertical: 0, horizontal: 0 });
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -50,14 +50,22 @@ export const RolePicker = ({
|
|||||||
if (!dimensions || !isOpen) {
|
if (!dimensions || !isOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { bottom, top } = dimensions;
|
const { bottom, top, left, right } = dimensions;
|
||||||
const distance = window.innerHeight - bottom;
|
const distance = window.innerHeight - bottom;
|
||||||
const offset = bottom - top + 10; // Add extra 10px to offset to account for border and outline
|
const offsetVertical = bottom - top + 10; // Add extra 10px to offset to account for border and outline
|
||||||
|
const offsetHorizontal = right - left;
|
||||||
|
let horizontal = -offsetHorizontal;
|
||||||
|
let vertical = -offsetVertical;
|
||||||
|
|
||||||
if (distance < MENU_MAX_HEIGHT + 20) {
|
if (distance < MENU_MAX_HEIGHT + 20) {
|
||||||
setOffset(offset);
|
vertical = offsetVertical;
|
||||||
} else {
|
|
||||||
setOffset(-offset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (window.innerWidth - right < ROLE_PICKER_WIDTH) {
|
||||||
|
horizontal = offsetHorizontal;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOffset({ horizontal, vertical });
|
||||||
}, [isOpen, selectedRoles]);
|
}, [isOpen, selectedRoles]);
|
||||||
|
|
||||||
const onOpen = useCallback(
|
const onOpen = useCallback(
|
||||||
|
@ -42,7 +42,7 @@ interface RolePickerMenuProps {
|
|||||||
onUpdate: (newRoles: Role[], newBuiltInRole?: OrgRole) => void;
|
onUpdate: (newRoles: Role[], newBuiltInRole?: OrgRole) => void;
|
||||||
onClear?: () => void;
|
onClear?: () => void;
|
||||||
updateDisabled?: boolean;
|
updateDisabled?: boolean;
|
||||||
offset: number;
|
offset: { vertical: number; horizontal: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RolePickerMenu = ({
|
export const RolePickerMenu = ({
|
||||||
@ -181,9 +181,10 @@ export const RolePickerMenu = ({
|
|||||||
className={cx(
|
className={cx(
|
||||||
styles.menu,
|
styles.menu,
|
||||||
customStyles.menuWrapper,
|
customStyles.menuWrapper,
|
||||||
|
{ [customStyles.menuLeft]: offset.horizontal > 0 },
|
||||||
css`
|
css`
|
||||||
bottom: ${offset > 0 ? `${offset}px` : 'unset'};
|
bottom: ${offset.vertical > 0 ? `${offset.vertical}px` : 'unset'};
|
||||||
top: ${offset < 0 ? `${Math.abs(offset)}px` : 'unset'};
|
top: ${offset.vertical < 0 ? `${Math.abs(offset.vertical)}px` : 'unset'};
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -226,6 +227,7 @@ export const RolePickerMenu = ({
|
|||||||
selectedOptions={selectedOptions}
|
selectedOptions={selectedOptions}
|
||||||
onSelect={onChange}
|
onSelect={onChange}
|
||||||
onClear={onClearSubMenu}
|
onClear={onClearSubMenu}
|
||||||
|
showOnLeft={offset.horizontal > 0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</RoleMenuGroupOption>
|
</RoleMenuGroupOption>
|
||||||
@ -278,7 +280,7 @@ export const RolePickerMenu = ({
|
|||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref={subMenuNode}></div>
|
<div ref={subMenuNode} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -317,6 +319,7 @@ interface RolePickerSubMenuProps {
|
|||||||
disabledOptions?: Role[];
|
disabledOptions?: Role[];
|
||||||
onSelect: (option: Role) => void;
|
onSelect: (option: Role) => void;
|
||||||
onClear?: () => void;
|
onClear?: () => void;
|
||||||
|
showOnLeft?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RolePickerSubMenu = ({
|
export const RolePickerSubMenu = ({
|
||||||
@ -325,6 +328,7 @@ export const RolePickerSubMenu = ({
|
|||||||
disabledOptions,
|
disabledOptions,
|
||||||
onSelect,
|
onSelect,
|
||||||
onClear,
|
onClear,
|
||||||
|
showOnLeft,
|
||||||
}: RolePickerSubMenuProps): JSX.Element => {
|
}: RolePickerSubMenuProps): JSX.Element => {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getSelectStyles(theme);
|
const styles = getSelectStyles(theme);
|
||||||
@ -337,7 +341,10 @@ export const RolePickerSubMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={customStyles.subMenu} aria-label="Role picker submenu">
|
<div
|
||||||
|
className={cx(customStyles.subMenu, { [customStyles.subMenuLeft]: showOnLeft })}
|
||||||
|
aria-label="Role picker submenu"
|
||||||
|
>
|
||||||
<CustomScrollbar autoHide={false} autoHeightMax={`${MENU_MAX_HEIGHT}px`} 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) => (
|
||||||
@ -506,7 +513,7 @@ export const RoleMenuGroupOption = React.forwardRef<HTMLDivElement, RoleMenuGrou
|
|||||||
/>
|
/>
|
||||||
<div className={cx(styles.optionBody, customStyles.menuOptionBody)}>
|
<div className={cx(styles.optionBody, customStyles.menuOptionBody)}>
|
||||||
<span>{data.displayName || data.name}</span>
|
<span>{data.displayName || data.name}</span>
|
||||||
<span className={customStyles.menuOptionExpand}></span>
|
<span className={customStyles.menuOptionExpand} />
|
||||||
</div>
|
</div>
|
||||||
{root && children && (
|
{root && children && (
|
||||||
<Portal className={customStyles.subMenuPortal} root={root}>
|
<Portal className={customStyles.subMenuPortal} root={root}>
|
||||||
@ -552,19 +559,25 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
padding-top: ${theme.spacing(1)};
|
padding-top: ${theme.spacing(1)};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
menuLeft: css`
|
||||||
|
right: 0;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
`,
|
||||||
subMenu: css`
|
subMenu: css`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 260px;
|
min-width: 260px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-left-style: solid;
|
border-left: 1px solid ${theme.components.input.borderColor};
|
||||||
border-left-width: 1px;
|
|
||||||
border-left-color: ${theme.components.input.borderColor};
|
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
padding-top: ${theme.spacing(1)};
|
padding-top: ${theme.spacing(1)};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
subMenuLeft: css`
|
||||||
|
border-right: 1px solid ${theme.components.input.borderColor};
|
||||||
|
border-left: unset;
|
||||||
|
`,
|
||||||
groupHeader: css`
|
groupHeader: css`
|
||||||
padding: ${theme.spacing(0, 4)};
|
padding: ${theme.spacing(0, 4)};
|
||||||
display: flex;
|
display: flex;
|
||||||
|
Loading…
Reference in New Issue
Block a user