RolePicker: Fix submenu position on horizontal space overflow (#50769)

This commit is contained in:
Alex Khomenko 2022-06-15 09:51:32 +03:00 committed by GitHub
parent 1af63ba5f9
commit 2fbe99c1be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 37 additions and 16 deletions

View File

@ -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(

View File

@ -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;