Fixed issue with sidebar menu position (#3426)
* Fixed issue with sidebar menu position * Removed duplicate code * Handled small screens as well * Fixed a class name
This commit is contained in:
parent
4f7ce070bc
commit
b9ac00ef4e
7 changed files with 130 additions and 16 deletions
|
@ -163,6 +163,19 @@
|
|||
right: calc(100% - 480px + 50px);
|
||||
left: calc(240px - 50px);
|
||||
}
|
||||
|
||||
.boardMoveToCategorySubmenu {
|
||||
.menu-options {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-height: 768px) {
|
||||
.menu-options {
|
||||
max-height: min(350px, 50vh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.team-sidebar + .product-wrapper {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import React, {useCallback, useRef, useState} from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
import {useHistory, useRouteMatch} from "react-router-dom"
|
||||
|
||||
|
@ -106,12 +106,15 @@ const SidebarBoardItem = (props: Props) => {
|
|||
|
||||
}, [board.id])
|
||||
|
||||
const boardItemRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const title = board.title || intl.formatMessage({id: 'Sidebar.untitled-board', defaultMessage: '(Untitled Board)'})
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`SidebarBoardItem subitem ${props.isActive ? 'active' : ''}`}
|
||||
onClick={() => props.showBoard(board.id)}
|
||||
ref={boardItemRef}
|
||||
>
|
||||
<div className='octo-sidebar-icon'>
|
||||
{board.icon || <BoardIcon/>}
|
||||
|
@ -136,7 +139,8 @@ const SidebarBoardItem = (props: Props) => {
|
|||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu
|
||||
fixed={true}
|
||||
position='left'
|
||||
position='auto'
|
||||
parentRef={boardItemRef}
|
||||
>
|
||||
<BoardPermissionGate
|
||||
boardId={board.id}
|
||||
|
@ -155,9 +159,10 @@ const SidebarBoardItem = (props: Props) => {
|
|||
<Menu.SubMenu
|
||||
key={`moveBlock-${board.id}`}
|
||||
id='moveBlock'
|
||||
className='boardMoveToCategorySubmenu'
|
||||
name={intl.formatMessage({id: 'SidebarCategories.BlocksMenu.Move', defaultMessage: 'Move To...'})}
|
||||
icon={<CreateNewFolder/>}
|
||||
position='bottom'
|
||||
position='auto'
|
||||
>
|
||||
{generateMoveToCategoryOptions(board.id)}
|
||||
</Menu.SubMenu>
|
||||
|
|
|
@ -154,9 +154,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.Menu.noselect.left {
|
||||
.Menu.noselect:not(.SubMenu) {
|
||||
position: fixed;
|
||||
right: calc(100% - 480px + 50px);
|
||||
left: calc(240px - 50px);
|
||||
|
||||
> .left {
|
||||
right: calc(100% - 480px - 64px + 50px);
|
||||
left: calc(64px + 240px - 50px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import React, {useCallback, useRef, useState} from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
import {generatePath, useHistory, useRouteMatch} from 'react-router-dom'
|
||||
|
||||
|
@ -59,6 +59,8 @@ const SidebarCategory = (props: Props) => {
|
|||
const team = useAppSelector(getCurrentTeam)
|
||||
const teamID = team?.id || ''
|
||||
|
||||
const menuWrapperRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const showBoard = useCallback((boardId) => {
|
||||
Utils.showBoard(boardId, match, history)
|
||||
props.hideSidebar()
|
||||
|
@ -138,7 +140,7 @@ const SidebarCategory = (props: Props) => {
|
|||
}, [showBoard, deleteBoard, props.boards])
|
||||
|
||||
return (
|
||||
<div className='SidebarCategory'>
|
||||
<div className='SidebarCategory' ref={menuWrapperRef}>
|
||||
<div
|
||||
className={`octo-sidebar-item category ' ${collapsed ? 'collapsed' : 'expanded'} ${props.categoryBoards.id === props.activeCategoryId ? 'active' : ''}`}
|
||||
>
|
||||
|
@ -156,7 +158,10 @@ const SidebarCategory = (props: Props) => {
|
|||
onToggle={(open) => setCategoryMenuOpen(open)}
|
||||
>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu position='left'>
|
||||
<Menu
|
||||
position='auto'
|
||||
parentRef={menuWrapperRef}
|
||||
>
|
||||
<Menu.Text
|
||||
id='createNewCategory'
|
||||
name={intl.formatMessage({id: 'SidebarCategories.CategoryMenu.CreateNew', defaultMessage: 'Create New Category'})}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import React, {CSSProperties} from 'react'
|
||||
|
||||
import SeparatorOption from './separatorOption'
|
||||
import SwitchOption from './switchOption'
|
||||
|
@ -11,13 +11,16 @@ import LabelOption from './labelOption'
|
|||
|
||||
import './menu.scss'
|
||||
import textInputOption from './textInputOption'
|
||||
import MenuUtil from "./menuUtil"
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
position?: 'top' | 'bottom' | 'left' | 'right'
|
||||
position?: 'top' | 'bottom' | 'left' | 'right' | 'auto'
|
||||
fixed?: boolean
|
||||
parentRef?: React.RefObject<any>
|
||||
}
|
||||
|
||||
|
||||
export default class Menu extends React.PureComponent<Props> {
|
||||
static Color = ColorOption
|
||||
static SubMenu = SubMenuOption
|
||||
|
@ -27,14 +30,33 @@ export default class Menu extends React.PureComponent<Props> {
|
|||
static TextInput = textInputOption
|
||||
static Label = LabelOption
|
||||
|
||||
menuRef: React.RefObject<HTMLDivElement>
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.menuRef = React.createRef<HTMLDivElement>()
|
||||
}
|
||||
|
||||
public state = {
|
||||
hovering: null,
|
||||
menuStyle: {},
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const {position, fixed, children} = this.props
|
||||
|
||||
let style: CSSProperties = {}
|
||||
if (position === 'auto' && this.props.parentRef) {
|
||||
style = MenuUtil.openUp(this.props.parentRef).style
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`Menu noselect ${position || 'bottom'} ${fixed ? ' fixed' : ''}`}>
|
||||
<div
|
||||
className={`Menu noselect ${position || 'bottom'} ${fixed ? ' fixed' : ''}`}
|
||||
style={style}
|
||||
ref={this.menuRef}
|
||||
>
|
||||
<div className='menu-contents'>
|
||||
<div className='menu-options'>
|
||||
{React.Children.map(children, (child) => (
|
||||
|
|
40
webapp/src/widgets/menu/menuUtil.ts
Normal file
40
webapp/src/widgets/menu/menuUtil.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {CSSProperties} from 'react'
|
||||
|
||||
/**
|
||||
* Calculates if a menu should open aligned down or up around the `anchorRef` element.
|
||||
* This should be used to make sure the menues are always fullly visible in cases
|
||||
* when opening them close to the edges of screen.
|
||||
* @param anchorRef ref of the element with respect to which the menu position is to be calculated.
|
||||
* @param menuMargin a safe margin value to be ensured around the menu in the calculations.
|
||||
* this ensures the menu stick to the edges of the screen ans has some space around for ease of use.
|
||||
*/
|
||||
function openUp(anchorRef: React.RefObject<HTMLElement>, menuMargin = 40): {openUp: boolean , style: CSSProperties} {
|
||||
const ret = {
|
||||
openUp: false,
|
||||
style: {} as CSSProperties,
|
||||
}
|
||||
if (!anchorRef.current) {
|
||||
return ret
|
||||
}
|
||||
|
||||
const boundingRect = anchorRef.current.getBoundingClientRect()
|
||||
const y = typeof boundingRect?.y === 'undefined' ? boundingRect?.top : boundingRect.y
|
||||
const windowHeight = window.innerHeight
|
||||
const totalSpace = windowHeight - menuMargin
|
||||
const spaceOnTop = y || 0
|
||||
const spaceOnBottom = totalSpace - spaceOnTop
|
||||
ret.openUp = spaceOnTop > spaceOnBottom
|
||||
if (ret.openUp) {
|
||||
ret.style.bottom = spaceOnBottom + menuMargin
|
||||
} else {
|
||||
ret.style.top = spaceOnTop + menuMargin
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
export default {
|
||||
openUp,
|
||||
}
|
|
@ -1,11 +1,14 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useEffect, useState, useContext} from 'react'
|
||||
import React, {useEffect, useState, useContext, CSSProperties, useRef} from 'react'
|
||||
|
||||
import SubmenuTriangleIcon from '../icons/submenuTriangle'
|
||||
|
||||
import MenuUtil from './menuUtil'
|
||||
|
||||
import Menu from '.'
|
||||
|
||||
|
||||
import './subMenuOption.scss'
|
||||
|
||||
export const HoveringContext = React.createContext(false)
|
||||
|
@ -13,9 +16,10 @@ export const HoveringContext = React.createContext(false)
|
|||
type SubMenuOptionProps = {
|
||||
id: string
|
||||
name: string
|
||||
position?: 'bottom' | 'top' | 'left' | 'left-bottom'
|
||||
position?: 'bottom' | 'top' | 'left' | 'left-bottom' | 'auto'
|
||||
icon?: React.ReactNode
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
function SubMenuOption(props: SubMenuOptionProps): JSX.Element {
|
||||
|
@ -30,22 +34,44 @@ function SubMenuOption(props: SubMenuOptionProps): JSX.Element {
|
|||
}
|
||||
}, [isHovering])
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const styleRef = useRef<CSSProperties>({})
|
||||
|
||||
useEffect(() => {
|
||||
const newStyle: CSSProperties = {}
|
||||
if (props.position === 'auto' && ref.current) {
|
||||
const openUp = MenuUtil.openUp(ref)
|
||||
if (openUp.openUp) {
|
||||
newStyle.bottom = 0
|
||||
} else {
|
||||
newStyle.top = 0
|
||||
}
|
||||
}
|
||||
|
||||
styleRef.current = newStyle
|
||||
}, [ref.current])
|
||||
|
||||
return (
|
||||
<div
|
||||
id={props.id}
|
||||
className={`MenuOption SubMenuOption menu-option${openLeftClass}${isOpen ? ' menu-option-active' : ''}`}
|
||||
className={`MenuOption SubMenuOption menu-option${openLeftClass}${isOpen ? ' menu-option-active' : ''}${props.className ? ' ' + props.className : ''}`}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsOpen((open) => !open)
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
{(props.position === 'left' || props.position === 'left-bottom') && <SubmenuTriangleIcon/>}
|
||||
{props.icon ?? <div className='noicon'/>}
|
||||
<div className='menu-name'>{props.name}</div>
|
||||
{props.position !== 'left' && props.position !== 'left-bottom' && <SubmenuTriangleIcon/>}
|
||||
{isOpen &&
|
||||
<div className={'SubMenu Menu noselect ' + (props.position || 'bottom')}>
|
||||
<div
|
||||
className={'SubMenu Menu noselect ' + (props.position || 'bottom')}
|
||||
style={styleRef.current}
|
||||
>
|
||||
<div className='menu-contents'>
|
||||
<div className='menu-options'>
|
||||
{props.children}
|
||||
|
|
Loading…
Reference in a new issue