mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-27936: Allow resizing the channel sidebar and right-hand sidebar (#23117)
* MM-27936: Resize sidebar * separate sidebar related constants into different file * reset style when sidebar expanded * change not to save user_id when saving to local_storage * prevent a separation between the rhs and the main center panel * change variable name * migrate to css-based paradigm * delete unnecessary files * update snapshot * increase the resizable range * fix transition * Made RHS and LHS widths not be saved scoped by team * scope cleanup * fix lhs header menu ellipsis/tolerances * fix lint * remove a ref from a dependency * return null instead of empty jsx * return null when disabled --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: harshil Sharma <harshilsharma63@gmail.com> Co-authored-by: Caleb Roseland <caleb@calebroseland.com>
This commit is contained in:
parent
b84236d555
commit
61473efb93
@ -7,8 +7,38 @@ import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/tea
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {LhsItemType} from 'types/store/lhs';
|
||||
import {ActionTypes} from 'utils/constants';
|
||||
import Constants, {ActionTypes} from 'utils/constants';
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||
|
||||
export const setLhsSize = (sidebarSize?: SidebarSize) => {
|
||||
let newSidebarSize = sidebarSize;
|
||||
if (!sidebarSize) {
|
||||
const width = window.innerWidth;
|
||||
|
||||
switch (true) {
|
||||
case width <= Constants.SMALL_SIDEBAR_BREAKPOINT: {
|
||||
newSidebarSize = SidebarSize.SMALL;
|
||||
break;
|
||||
}
|
||||
case width > Constants.SMALL_SIDEBAR_BREAKPOINT && width <= Constants.MEDIUM_SIDEBAR_BREAKPOINT: {
|
||||
newSidebarSize = SidebarSize.MEDIUM;
|
||||
break;
|
||||
}
|
||||
case width > Constants.MEDIUM_SIDEBAR_BREAKPOINT && width <= Constants.LARGE_SIDEBAR_BREAKPOINT: {
|
||||
newSidebarSize = SidebarSize.LARGE;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
newSidebarSize = SidebarSize.XLARGE;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: ActionTypes.SET_LHS_SIZE,
|
||||
size: newSidebarSize,
|
||||
};
|
||||
};
|
||||
|
||||
export const toggle = () => ({
|
||||
type: ActionTypes.TOGGLE_LHS,
|
||||
|
@ -33,6 +33,7 @@ import {GlobalState} from 'types/store';
|
||||
import {getPostsByIds, getPost as fetchPost} from 'mattermost-redux/actions/posts';
|
||||
|
||||
import {getChannel} from 'mattermost-redux/actions/channels';
|
||||
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||
|
||||
function selectPostFromRightHandSideSearchWithPreviousState(post: Post, previousRhsState?: RhsState) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
@ -138,6 +139,35 @@ export function updateSearchTerms(terms: string) {
|
||||
};
|
||||
}
|
||||
|
||||
export function setRhsSize(rhsSize?: SidebarSize) {
|
||||
let newSidebarSize = rhsSize;
|
||||
if (!newSidebarSize) {
|
||||
const width = window.innerWidth;
|
||||
|
||||
switch (true) {
|
||||
case width <= Constants.SMALL_SIDEBAR_BREAKPOINT: {
|
||||
newSidebarSize = SidebarSize.SMALL;
|
||||
break;
|
||||
}
|
||||
case width > Constants.SMALL_SIDEBAR_BREAKPOINT && width <= Constants.MEDIUM_SIDEBAR_BREAKPOINT: {
|
||||
newSidebarSize = SidebarSize.MEDIUM;
|
||||
break;
|
||||
}
|
||||
case width > Constants.MEDIUM_SIDEBAR_BREAKPOINT && width <= Constants.LARGE_SIDEBAR_BREAKPOINT: {
|
||||
newSidebarSize = SidebarSize.LARGE;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
newSidebarSize = SidebarSize.XLARGE;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: ActionTypes.SET_RHS_SIZE,
|
||||
size: newSidebarSize,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSearchTermsForShortcut() {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const currentChannelName = getCurrentChannelNameForSearchShortcut(getState());
|
||||
|
@ -129,6 +129,8 @@ interface FormattingBarProps {
|
||||
additionalControls?: React.ReactNodeArray;
|
||||
}
|
||||
|
||||
const DEFAULT_MIN_MODE_X_COORD = 55;
|
||||
|
||||
const FormattingBar = (props: FormattingBarProps): JSX.Element => {
|
||||
const {
|
||||
applyMarkdown,
|
||||
@ -224,10 +226,12 @@ const FormattingBar = (props: FormattingBarProps): JSX.Element => {
|
||||
}
|
||||
}, [getCurrentSelection, getCurrentMessage, applyMarkdown, showHiddenControls, toggleHiddenControls, disableControls]);
|
||||
|
||||
const leftPosition = wideMode === 'min' ? (x ?? 0) + DEFAULT_MIN_MODE_X_COORD : x ?? 0;
|
||||
|
||||
const hiddenControlsContainerStyles: React.CSSProperties = {
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
left: leftPosition,
|
||||
};
|
||||
|
||||
const showSeparators = wideMode === 'wide';
|
||||
|
@ -1,14 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useLayoutEffect, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useLayoutEffect, useState} from 'react';
|
||||
import {Instance} from '@popperjs/core';
|
||||
|
||||
import {debounce} from 'lodash';
|
||||
|
||||
import {MarkdownMode} from 'utils/markdown/apply_markdown';
|
||||
|
||||
type WideMode = 'wide' | 'normal' | 'narrow';
|
||||
type WideMode = 'wide' | 'normal' | 'narrow' | 'min';
|
||||
|
||||
export function useGetLatest<T>(val: T) {
|
||||
const ref = React.useRef<T>(val);
|
||||
@ -18,23 +18,24 @@ export function useGetLatest<T>(val: T) {
|
||||
|
||||
const useResponsiveFormattingBar = (ref: React.RefObject<HTMLDivElement>): WideMode => {
|
||||
const [wideMode, setWideMode] = useState<WideMode>('wide');
|
||||
const handleResize = debounce(() => {
|
||||
const handleResize = useCallback(debounce(() => {
|
||||
if (ref.current?.clientWidth === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ref.current.clientWidth > 640) {
|
||||
setWideMode('wide');
|
||||
}
|
||||
|
||||
if (ref.current.clientWidth >= 424 && ref.current.clientWidth <= 640) {
|
||||
setWideMode('normal');
|
||||
}
|
||||
|
||||
if (ref.current.clientWidth < 424) {
|
||||
setWideMode('narrow');
|
||||
}
|
||||
}, 10);
|
||||
|
||||
if (ref.current.clientWidth < 310) {
|
||||
setWideMode('min');
|
||||
}
|
||||
}, 10), []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!ref.current) {
|
||||
@ -58,6 +59,7 @@ const MAP_WIDE_MODE_TO_CONTROLS_QUANTITY: {[key in WideMode]: number} = {
|
||||
wide: 9,
|
||||
normal: 5,
|
||||
narrow: 3,
|
||||
min: 1,
|
||||
};
|
||||
|
||||
export const useFormattingBarControls = (
|
||||
|
@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export enum SidebarSize {
|
||||
SMALL='small',
|
||||
MEDIUM= 'medium',
|
||||
LARGE= 'large',
|
||||
XLARGE= 'xLarge',
|
||||
}
|
||||
|
||||
export enum ResizeDirection {
|
||||
LEFT = 'left',
|
||||
RIGHT = 'right',
|
||||
}
|
||||
|
||||
export enum CssVarKeyForResizable {
|
||||
LHS = 'overrideLhsWidth',
|
||||
RHS = 'overrideRhsWidth',
|
||||
}
|
||||
|
||||
export const SIDEBAR_SNAP_SIZE = 16;
|
||||
export const SIDEBAR_SNAP_SPEED_LIMIT = 5;
|
||||
|
||||
export const DEFAULT_LHS_WIDTH = 240;
|
||||
|
||||
export const RHS_MIN_MAX_WIDTH: { [size in SidebarSize]: { min: number; max: number; default: number}} = {
|
||||
[SidebarSize.SMALL]: {
|
||||
min: 400,
|
||||
max: 400,
|
||||
default: 400,
|
||||
},
|
||||
[SidebarSize.MEDIUM]: {
|
||||
min: 304,
|
||||
max: 400,
|
||||
default: 400,
|
||||
},
|
||||
[SidebarSize.LARGE]: {
|
||||
min: 304,
|
||||
max: 464,
|
||||
default: 400,
|
||||
},
|
||||
[SidebarSize.XLARGE]: {
|
||||
min: 304,
|
||||
max: 776,
|
||||
default: 500,
|
||||
},
|
||||
};
|
@ -0,0 +1,267 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {MouseEventHandler, RefObject, useEffect, useRef, useState} from 'react';
|
||||
import styled, {createGlobalStyle, css} from 'styled-components';
|
||||
import {CssVarKeyForResizable, ResizeDirection} from './constants';
|
||||
import {useGlobalState} from 'stores/hooks';
|
||||
import {isSizeLessThanSnapSize, isSnapableSpeed, shouldSnapWhenSizeGrown, shouldSnapWhenSizeShrunk} from './utils';
|
||||
import {useSelector} from 'react-redux';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
className?: string;
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
defaultWidth: number;
|
||||
globalCssVar: CssVarKeyForResizable;
|
||||
dir: ResizeDirection;
|
||||
containerRef: RefObject<HTMLElement>;
|
||||
onResizeStart?: (startWidth: number) => void;
|
||||
onResize?: (width: number, cssVarProperty: string, cssVarValue: string) => void;
|
||||
onResizeEnd?: (finalWidth: number, cssVarProperty: string, cssVarValue: string) => void;
|
||||
onDividerDoubleClick?: (prevWidth: number, cssVarProperty: string) => void;
|
||||
}
|
||||
|
||||
const Divider = styled.div<{isActive: boolean}>`
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
top: 0;
|
||||
width: 16px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
&.left {
|
||||
right: -8px;
|
||||
}
|
||||
|
||||
&.right {
|
||||
left: -8px;
|
||||
}
|
||||
&::after {
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background-color: ${({isActive}) => (isActive ? 'var(--sidebar-text-active-border)' : 'transparent')};
|
||||
content: '';
|
||||
}
|
||||
&:hover {
|
||||
&::after {
|
||||
background-color: var(--sidebar-text-active-border);
|
||||
transition: background-color 400ms step-end;
|
||||
}
|
||||
}
|
||||
&.snapped {
|
||||
&::after {
|
||||
animation: emphasis-sidebar-resize-line 800ms;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ResizableDividerGlobalStyle = createGlobalStyle<{active: boolean; varName: CssVarKeyForResizable; width: number | null}>`
|
||||
${({active}) => active && css`
|
||||
body {
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
}
|
||||
`}
|
||||
${({varName, width}) => width && css`
|
||||
:root {
|
||||
--${varName}: ${width}px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
function ResizableDivider({
|
||||
id,
|
||||
name,
|
||||
className,
|
||||
disabled,
|
||||
dir,
|
||||
containerRef,
|
||||
defaultWidth,
|
||||
onResize,
|
||||
onResizeStart,
|
||||
onResizeEnd,
|
||||
onDividerDoubleClick,
|
||||
...props
|
||||
}: Props) {
|
||||
const resizeLineRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const startWidth = useRef(0);
|
||||
const lastWidth = useRef<number | null>(null);
|
||||
|
||||
const previousClientX = useRef(0);
|
||||
|
||||
const cssVarKey = `--${props.globalCssVar}`;
|
||||
|
||||
const currentUserID = useSelector(getCurrentUserId);
|
||||
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [width, setWidth] = useGlobalState<number | null>(null, `resizable_${name}:`, currentUserID);
|
||||
|
||||
const defaultOnResizeChange = (width: number, cssVarProp: string, cssVarValue: string) => {
|
||||
containerRef.current?.style.setProperty(cssVarProp, cssVarValue);
|
||||
lastWidth.current = width;
|
||||
};
|
||||
|
||||
const handleOnResize = (width: number, cssVarProp: string, cssVarValue: string) => {
|
||||
if (onResize) {
|
||||
onResize(width, cssVarProp, cssVarValue);
|
||||
}
|
||||
|
||||
defaultOnResizeChange(width, cssVarProp, cssVarValue);
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
startWidth.current = 0;
|
||||
previousClientX.current = 0;
|
||||
lastWidth.current = null;
|
||||
};
|
||||
|
||||
const onMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
if (disabled || !containerRef.current) {
|
||||
return;
|
||||
}
|
||||
previousClientX.current = e.clientX;
|
||||
startWidth.current = containerRef.current.getBoundingClientRect().width;
|
||||
|
||||
setIsActive(true);
|
||||
|
||||
if (onResizeStart) {
|
||||
onResizeStart(startWidth.current);
|
||||
}
|
||||
|
||||
handleOnResize(startWidth.current, cssVarKey, `${startWidth.current}px`);
|
||||
|
||||
document.body.classList.add('layout-changing');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive || disabled) {
|
||||
reset();
|
||||
return undefined;
|
||||
}
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
const resizeLine = resizeLineRef.current;
|
||||
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resizeLine) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const previousWidth = lastWidth.current ?? 0;
|
||||
let widthDiff = 0;
|
||||
|
||||
switch (dir) {
|
||||
case ResizeDirection.LEFT:
|
||||
widthDiff = e.clientX - previousClientX.current;
|
||||
break;
|
||||
case ResizeDirection.RIGHT:
|
||||
widthDiff = previousClientX.current - e.clientX;
|
||||
break;
|
||||
}
|
||||
|
||||
const newWidth = previousWidth + widthDiff;
|
||||
|
||||
if (resizeLine.classList.contains('snapped')) {
|
||||
const diff = newWidth - defaultWidth;
|
||||
|
||||
if (isSizeLessThanSnapSize(diff)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleOnResize(newWidth, cssVarKey, `${newWidth}px`);
|
||||
|
||||
resizeLine.classList.remove('snapped');
|
||||
previousClientX.current = e.clientX;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
previousClientX.current = e.clientX;
|
||||
|
||||
const shouldSnap = shouldSnapWhenSizeGrown(newWidth, previousWidth, defaultWidth) || shouldSnapWhenSizeShrunk(newWidth, previousWidth, defaultWidth);
|
||||
|
||||
if (isSnapableSpeed(newWidth - previousWidth) && shouldSnap) {
|
||||
switch (dir) {
|
||||
case ResizeDirection.LEFT:
|
||||
previousClientX.current += defaultWidth - newWidth;
|
||||
break;
|
||||
case ResizeDirection.RIGHT:
|
||||
previousClientX.current += newWidth - defaultWidth;
|
||||
break;
|
||||
}
|
||||
|
||||
handleOnResize(defaultWidth, cssVarKey, `${defaultWidth}px`);
|
||||
|
||||
resizeLine.classList.add('snapped');
|
||||
return;
|
||||
}
|
||||
|
||||
handleOnResize(newWidth, cssVarKey, `${newWidth}px`);
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
const finalWidth = containerRef.current?.getBoundingClientRect().width;
|
||||
if (isActive && finalWidth) {
|
||||
setWidth(finalWidth);
|
||||
onResizeEnd?.(finalWidth, cssVarKey, `${finalWidth}px`);
|
||||
}
|
||||
|
||||
containerRef.current?.style.removeProperty(cssVarKey);
|
||||
document.body.classList.remove('layout-changing');
|
||||
reset();
|
||||
setIsActive(false);
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
}, [isActive, disabled]);
|
||||
|
||||
const onDoubleClick = () => {
|
||||
onDividerDoubleClick?.(width ?? 0, cssVarKey);
|
||||
reset();
|
||||
setWidth(null);
|
||||
};
|
||||
|
||||
if (disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider
|
||||
id={id}
|
||||
className={classNames(className, {
|
||||
left: dir === ResizeDirection.LEFT,
|
||||
right: dir === ResizeDirection.RIGHT,
|
||||
})}
|
||||
ref={resizeLineRef}
|
||||
isActive={isActive}
|
||||
onMouseDown={onMouseDown}
|
||||
onDoubleClick={onDoubleClick}
|
||||
/>
|
||||
<ResizableDividerGlobalStyle
|
||||
varName={props.globalCssVar}
|
||||
width={width}
|
||||
active={isActive}
|
||||
/>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default ResizableDivider;
|
@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {HTMLAttributes, useRef} from 'react';
|
||||
|
||||
import {DEFAULT_LHS_WIDTH, CssVarKeyForResizable, ResizeDirection} from '../constants';
|
||||
import ResizableDivider from '../resizable_divider';
|
||||
|
||||
interface Props extends HTMLAttributes<'div'> {
|
||||
children: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function ResizableLhs({
|
||||
children,
|
||||
disabled,
|
||||
id,
|
||||
className,
|
||||
}: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
className={className}
|
||||
ref={containerRef}
|
||||
>
|
||||
{children}
|
||||
<ResizableDivider
|
||||
name={'lhsResizeHandle'}
|
||||
globalCssVar={CssVarKeyForResizable.LHS}
|
||||
disabled={disabled}
|
||||
defaultWidth={DEFAULT_LHS_WIDTH}
|
||||
dir={ResizeDirection.LEFT}
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResizableLhs;
|
@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {HTMLAttributes, useEffect, useRef, useState} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getIsRhsExpanded, getRhsSize} from 'selectors/rhs';
|
||||
|
||||
import {shouldRhsOverlapChannelView} from '../utils';
|
||||
import {CssVarKeyForResizable, RHS_MIN_MAX_WIDTH, ResizeDirection} from '../constants';
|
||||
import ResizableDivider from '../resizable_divider';
|
||||
|
||||
interface Props extends HTMLAttributes<'div'> {
|
||||
children: React.ReactNode;
|
||||
rightWidthHolderRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
function ResizableRhs({
|
||||
role,
|
||||
children,
|
||||
id,
|
||||
className,
|
||||
rightWidthHolderRef,
|
||||
}: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const rhsSize = useSelector(getRhsSize);
|
||||
const isRhsExpanded = useSelector(getIsRhsExpanded);
|
||||
|
||||
const [previousRhsExpanded, setPreviousRhsExpanded] = useState(false);
|
||||
|
||||
const defaultWidth = RHS_MIN_MAX_WIDTH[rhsSize].default;
|
||||
|
||||
const shouldRhsOverlap = shouldRhsOverlapChannelView(rhsSize);
|
||||
|
||||
const handleResize = (_: number, cssVarProp: string, cssVarValue: string) => {
|
||||
const rightWidthHolderRefElement = rightWidthHolderRef.current;
|
||||
|
||||
if (!rightWidthHolderRefElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldRhsOverlap) {
|
||||
rightWidthHolderRefElement.style.setProperty(cssVarProp, cssVarValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResizeEnd = (_: number, cssVarProp: string) => {
|
||||
const rightWidthHolderRefElement = rightWidthHolderRef.current;
|
||||
|
||||
if (!rightWidthHolderRefElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
rightWidthHolderRefElement.style.removeProperty(cssVarProp);
|
||||
};
|
||||
|
||||
const handleDividerDoubleClick = (_: number, cssVarProp: string) => {
|
||||
handleResizeEnd(_, cssVarProp);
|
||||
|
||||
document.body.classList.add('layout-changing');
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.classList.remove('layout-changing');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// If max-width is applied immediately when expanded is collapsed, the transition will not work correctly.
|
||||
useEffect(() => {
|
||||
const containerRefElement = containerRef.current;
|
||||
|
||||
if (!containerRefElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPreviousRhsExpanded(isRhsExpanded);
|
||||
|
||||
if (previousRhsExpanded && !isRhsExpanded) {
|
||||
containerRefElement.classList.add('resize-disabled');
|
||||
|
||||
setTimeout(() => {
|
||||
containerRefElement.classList.remove('resize-disabled');
|
||||
}, 1000);
|
||||
}
|
||||
}, [isRhsExpanded]);
|
||||
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
className={className}
|
||||
role={role}
|
||||
ref={containerRef}
|
||||
>
|
||||
{children}
|
||||
<ResizableDivider
|
||||
name='rhsResizeHandle'
|
||||
globalCssVar={CssVarKeyForResizable.RHS}
|
||||
defaultWidth={defaultWidth}
|
||||
dir={ResizeDirection.RIGHT}
|
||||
disabled={isRhsExpanded}
|
||||
containerRef={containerRef}
|
||||
onResize={handleResize}
|
||||
onResizeEnd={handleResizeEnd}
|
||||
onDividerDoubleClick={handleDividerDoubleClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResizableRhs;
|
29
webapp/channels/src/components/resizable_sidebar/utils.ts
Normal file
29
webapp/channels/src/components/resizable_sidebar/utils.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {SidebarSize, SIDEBAR_SNAP_SIZE, SIDEBAR_SNAP_SPEED_LIMIT} from './constants';
|
||||
|
||||
export const isSizeLessThanSnapSize = (size: number) => {
|
||||
return Math.abs(size) <= SIDEBAR_SNAP_SIZE;
|
||||
};
|
||||
|
||||
export const isSnapableSpeed = (speed: number) => {
|
||||
return Math.abs(speed) <= SIDEBAR_SNAP_SPEED_LIMIT;
|
||||
};
|
||||
|
||||
export const shouldSnapWhenSizeGrown = (newWidth: number, prevWidth: number, defaultWidth: number) => {
|
||||
const diff = defaultWidth - newWidth;
|
||||
const isGrowing = newWidth > prevWidth;
|
||||
|
||||
return diff >= 0 && diff <= SIDEBAR_SNAP_SIZE && isGrowing;
|
||||
};
|
||||
|
||||
export const shouldSnapWhenSizeShrunk = (newWidth: number, prevWidth: number, defaultWidth: number) => {
|
||||
const diff = newWidth - defaultWidth;
|
||||
const isShrinking = newWidth < prevWidth;
|
||||
|
||||
return diff >= 0 && diff <= SIDEBAR_SNAP_SIZE && isShrinking;
|
||||
};
|
||||
|
||||
export const shouldRhsOverlapChannelView = (size: SidebarSize) => size === SidebarSize.MEDIUM;
|
||||
|
@ -90,6 +90,7 @@ exports[`components/Root Routes Should mount public product routes 1`] = `
|
||||
<CompassThemeProvider
|
||||
theme={Object {}}
|
||||
>
|
||||
<WindowSizeObserver />
|
||||
<Connect(ModalController) />
|
||||
<Connect(_class) />
|
||||
<Connect(SystemNotice) />
|
||||
|
@ -6,6 +6,8 @@ import {RouteComponentProps} from 'react-router-dom';
|
||||
import {shallow} from 'enzyme';
|
||||
import rudderAnalytics from 'rudder-sdk-js';
|
||||
|
||||
import matchMedia from 'tests/helpers/match_media.mock';
|
||||
|
||||
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
@ -14,7 +16,6 @@ import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import Root from 'components/root/root';
|
||||
import * as GlobalActions from 'actions/global_actions';
|
||||
import Constants, {StoragePrefixes, WindowSizes} from 'utils/constants';
|
||||
import matchMedia from 'tests/helpers/match_media.mock';
|
||||
import {ProductComponent} from 'types/store/plugins';
|
||||
import {ServiceEnvironment} from '@mattermost/types/config';
|
||||
|
||||
|
@ -91,6 +91,7 @@ import {applyLuxonDefaults} from './effects';
|
||||
|
||||
import RootProvider from './root_provider';
|
||||
import RootRedirect from './root_redirect';
|
||||
import WindowSizeObserver from 'components/window_size_observer/WindowSizeObserver';
|
||||
import {ServiceEnvironment} from '@mattermost/types/config';
|
||||
|
||||
const CreateTeam = makeAsyncComponent('CreateTeam', LazyCreateTeam);
|
||||
@ -644,6 +645,7 @@ export default class Root extends React.PureComponent<Props, State> {
|
||||
transitionDirection={Animations.Reasons.EnterFromBefore}
|
||||
/>
|
||||
)}
|
||||
<WindowSizeObserver/>
|
||||
<ModalController/>
|
||||
<AnnouncementBarController/>
|
||||
<SystemNotice/>
|
||||
|
@ -3,8 +3,9 @@
|
||||
exports[`components/sidebar should match empty div snapshot when teamId is missing 1`] = `<div />`;
|
||||
|
||||
exports[`components/sidebar should match snapshot 1`] = `
|
||||
<div
|
||||
<ResizableLhs
|
||||
className=""
|
||||
disabled={false}
|
||||
id="SidebarContainer"
|
||||
>
|
||||
<SidebarHeader
|
||||
@ -42,12 +43,13 @@ exports[`components/sidebar should match snapshot 1`] = `
|
||||
onDragStart={[Function]}
|
||||
/>
|
||||
<Connect(DataPrefetch) />
|
||||
</div>
|
||||
</ResizableLhs>
|
||||
`;
|
||||
|
||||
exports[`components/sidebar should match snapshot when direct channels modal is open 1`] = `
|
||||
<div
|
||||
<ResizableLhs
|
||||
className=""
|
||||
disabled={false}
|
||||
id="SidebarContainer"
|
||||
>
|
||||
<SidebarHeader
|
||||
@ -89,12 +91,13 @@ exports[`components/sidebar should match snapshot when direct channels modal is
|
||||
isExistingChannel={false}
|
||||
onModalDismissed={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</ResizableLhs>
|
||||
`;
|
||||
|
||||
exports[`components/sidebar should match snapshot when more channels modal is open 1`] = `
|
||||
<div
|
||||
<ResizableLhs
|
||||
className=""
|
||||
disabled={false}
|
||||
id="SidebarContainer"
|
||||
>
|
||||
<SidebarHeader
|
||||
@ -132,5 +135,5 @@ exports[`components/sidebar should match snapshot when more channels modal is op
|
||||
onDragStart={[Function]}
|
||||
/>
|
||||
<Connect(DataPrefetch) />
|
||||
</div>
|
||||
</ResizableLhs>
|
||||
`;
|
||||
|
@ -29,6 +29,7 @@ import ChannelNavigator from './channel_navigator';
|
||||
import SidebarList from './sidebar_list';
|
||||
import SidebarHeader from './sidebar_header';
|
||||
import MobileSidebarHeader from './mobile_sidebar_header';
|
||||
import ResizableLhs from 'components/resizable_sidebar/resizable_lhs';
|
||||
|
||||
type Props = {
|
||||
teamId: string;
|
||||
@ -229,12 +230,14 @@ export default class Sidebar extends React.PureComponent<Props, State> {
|
||||
const ariaLabel = Utils.localizeMessage('accessibility.sections.lhsNavigator', 'channel navigator region');
|
||||
|
||||
return (
|
||||
<div
|
||||
<ResizableLhs
|
||||
id='SidebarContainer'
|
||||
className={classNames({
|
||||
'move--right': this.props.isOpen && this.props.isMobileView,
|
||||
dragging: this.state.isDragging,
|
||||
})}
|
||||
disabled={this.props.isMobileView}
|
||||
|
||||
>
|
||||
{this.props.isMobileView ? <MobileSidebarHeader/> : (
|
||||
<SidebarHeader
|
||||
@ -270,7 +273,7 @@ export default class Sidebar extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
<DataPrefetch/>
|
||||
{this.renderModals()}
|
||||
</div>
|
||||
</ResizableLhs>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ const SidebarHeaderContainer = styled(Flex).attrs(() => ({
|
||||
}))<SidebarHeaderContainerProps>`
|
||||
height: 52px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
@ -54,11 +55,6 @@ const SidebarHeaderContainer = styled(Flex).attrs(() => ({
|
||||
}
|
||||
`;
|
||||
|
||||
const HEADING_WIDTH = 200;
|
||||
const CHEVRON_WIDTH = 26;
|
||||
const ADD_CHANNEL_DROPDOWN_WIDTH = 28;
|
||||
const TITLE_WIDTH = (HEADING_WIDTH - CHEVRON_WIDTH - ADD_CHANNEL_DROPDOWN_WIDTH).toString();
|
||||
|
||||
const SidebarHeading = styled(Heading).attrs(() => ({
|
||||
element: 'h1',
|
||||
margin: 'none',
|
||||
@ -69,7 +65,6 @@ const SidebarHeading = styled(Heading).attrs(() => ({
|
||||
display: flex;
|
||||
|
||||
.title {
|
||||
max-width: ${TITLE_WIDTH}px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
@ -26,6 +26,7 @@ import PostEditHistory from 'components/post_edit_history';
|
||||
import LoadingScreen from 'components/loading_screen';
|
||||
|
||||
import RhsPlugin from 'plugins/rhs_plugin';
|
||||
import ResizableRhs from 'components/resizable_sidebar/resizable_rhs';
|
||||
|
||||
type Props = {
|
||||
isExpanded: boolean;
|
||||
@ -65,13 +66,15 @@ type State = {
|
||||
|
||||
export default class SidebarRight extends React.PureComponent<Props, State> {
|
||||
sidebarRight: React.RefObject<HTMLDivElement>;
|
||||
sidebarRightWidthHolder: React.RefObject<HTMLDivElement>;
|
||||
previous: Partial<Props> | undefined = undefined;
|
||||
focusSearchBar?: () => void;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.sidebarRight = React.createRef();
|
||||
this.sidebarRightWidthHolder = React.createRef<HTMLDivElement>();
|
||||
this.sidebarRight = React.createRef<HTMLDivElement>();
|
||||
this.state = {
|
||||
isOpened: false,
|
||||
};
|
||||
@ -262,14 +265,20 @@ export default class SidebarRight extends React.PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'sidebar--right sidebar--right--width-holder'}/>
|
||||
<div
|
||||
className={'sidebar--right sidebar--right--width-holder'}
|
||||
ref={this.sidebarRightWidthHolder}
|
||||
/>
|
||||
<ResizableRhs
|
||||
className={containerClassName}
|
||||
id='sidebar-right'
|
||||
role='complementary'
|
||||
ref={this.sidebarRight}
|
||||
rightWidthHolderRef={this.sidebarRightWidthHolder}
|
||||
>
|
||||
<div className='sidebar-right-container'>
|
||||
<div
|
||||
className='sidebar-right-container'
|
||||
ref={this.sidebarRight}
|
||||
>
|
||||
{isRHSLoading ? (
|
||||
<div className='sidebar-right__body'>
|
||||
{/* Sometimes the channel/team is not loaded yet, so we need to wait for it */}
|
||||
@ -286,7 +295,7 @@ export default class SidebarRight extends React.PureComponent<Props, State> {
|
||||
</Search>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ResizableRhs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useCallback, useEffect} from 'react';
|
||||
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
import {setLhsSize} from 'actions/views/lhs';
|
||||
import {setRhsSize} from 'actions/views/rhs';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||
|
||||
const smallSidebarMediaQuery = window.matchMedia(`(max-width: ${Constants.SMALL_SIDEBAR_BREAKPOINT}px)`);
|
||||
const mediumSidebarMediaQuery = window.matchMedia(`(min-width: ${Constants.SMALL_SIDEBAR_BREAKPOINT + 1}px) and (max-width: ${Constants.MEDIUM_SIDEBAR_BREAKPOINT}px)`);
|
||||
const largeSidebarMediaQuery = window.matchMedia(`(min-width: ${Constants.MEDIUM_SIDEBAR_BREAKPOINT + 1}px) and (max-width: ${Constants.LARGE_SIDEBAR_BREAKPOINT}px)`);
|
||||
const xLargeSidebarMediaQuery = window.matchMedia(`(min-width: ${Constants.LARGE_SIDEBAR_BREAKPOINT + 1}px)`);
|
||||
|
||||
function WindowSizeObserver() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const updateSidebarSize = useCallback(() => {
|
||||
switch (true) {
|
||||
case xLargeSidebarMediaQuery.matches:
|
||||
dispatch(setLhsSize(SidebarSize.XLARGE));
|
||||
dispatch(setRhsSize(SidebarSize.XLARGE));
|
||||
break;
|
||||
case largeSidebarMediaQuery.matches:
|
||||
dispatch(setLhsSize(SidebarSize.LARGE));
|
||||
dispatch(setRhsSize(SidebarSize.LARGE));
|
||||
break;
|
||||
case mediumSidebarMediaQuery.matches:
|
||||
dispatch(setLhsSize(SidebarSize.MEDIUM));
|
||||
dispatch(setRhsSize(SidebarSize.MEDIUM));
|
||||
break;
|
||||
case smallSidebarMediaQuery.matches:
|
||||
dispatch(setLhsSize(SidebarSize.SMALL));
|
||||
dispatch(setRhsSize(SidebarSize.SMALL));
|
||||
break;
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
const setSidebarSizeWhenWindowResized = useCallback(throttle(() => {
|
||||
dispatch(setLhsSize());
|
||||
dispatch(setRhsSize());
|
||||
}, 100), []);
|
||||
|
||||
const handleSidebarMediaQueryChangeEvent = useCallback((e: MediaQueryListEvent) => {
|
||||
if (e.matches) {
|
||||
updateSidebarSize();
|
||||
}
|
||||
}, [updateSidebarSize]);
|
||||
|
||||
useEffect(() => {
|
||||
updateSidebarSize();
|
||||
|
||||
if (smallSidebarMediaQuery.addEventListener) {
|
||||
xLargeSidebarMediaQuery.addEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||
largeSidebarMediaQuery.addEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||
mediumSidebarMediaQuery.addEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||
smallSidebarMediaQuery.addEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||
} else if (smallSidebarMediaQuery.addListener) {
|
||||
xLargeSidebarMediaQuery.addListener(handleSidebarMediaQueryChangeEvent);
|
||||
largeSidebarMediaQuery.addListener(handleSidebarMediaQueryChangeEvent);
|
||||
mediumSidebarMediaQuery.addListener(handleSidebarMediaQueryChangeEvent);
|
||||
smallSidebarMediaQuery.addListener(handleSidebarMediaQueryChangeEvent);
|
||||
} else {
|
||||
window.addEventListener('resize', setSidebarSizeWhenWindowResized);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (smallSidebarMediaQuery.removeEventListener) {
|
||||
xLargeSidebarMediaQuery.removeEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||
largeSidebarMediaQuery.removeEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||
mediumSidebarMediaQuery.removeEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||
smallSidebarMediaQuery.removeEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||
} else if (smallSidebarMediaQuery.removeListener) {
|
||||
xLargeSidebarMediaQuery.removeListener(handleSidebarMediaQueryChangeEvent);
|
||||
largeSidebarMediaQuery.removeListener(handleSidebarMediaQueryChangeEvent);
|
||||
mediumSidebarMediaQuery.removeListener(handleSidebarMediaQueryChangeEvent);
|
||||
smallSidebarMediaQuery.removeListener(handleSidebarMediaQueryChangeEvent);
|
||||
} else {
|
||||
window.removeEventListener('resize', setSidebarSizeWhenWindowResized);
|
||||
}
|
||||
};
|
||||
}, [handleSidebarMediaQueryChangeEvent, setSidebarSizeWhenWindowResized, updateSidebarSize]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default WindowSizeObserver;
|
@ -11,6 +11,7 @@ describe('Reducers.LHS', () => {
|
||||
const initialState = {
|
||||
isOpen: false,
|
||||
currentStaticPageId: '',
|
||||
size: 'medium',
|
||||
};
|
||||
|
||||
test('initial state', () => {
|
||||
@ -18,6 +19,7 @@ describe('Reducers.LHS', () => {
|
||||
{
|
||||
isOpen: false,
|
||||
currentStaticPageId: '',
|
||||
size: 'medium',
|
||||
},
|
||||
{} as GenericAction,
|
||||
);
|
||||
@ -30,6 +32,7 @@ describe('Reducers.LHS', () => {
|
||||
{
|
||||
isOpen: true,
|
||||
currentStaticPageId: '',
|
||||
size: 'medium',
|
||||
},
|
||||
{
|
||||
type: ActionTypes.TOGGLE_LHS,
|
||||
@ -47,6 +50,7 @@ describe('Reducers.LHS', () => {
|
||||
{
|
||||
isOpen: false,
|
||||
currentStaticPageId: '',
|
||||
size: 'medium',
|
||||
},
|
||||
{
|
||||
type: ActionTypes.TOGGLE_LHS,
|
||||
@ -64,6 +68,7 @@ describe('Reducers.LHS', () => {
|
||||
{
|
||||
isOpen: false,
|
||||
currentStaticPageId: '',
|
||||
size: 'medium',
|
||||
},
|
||||
{
|
||||
type: ActionTypes.OPEN_LHS,
|
||||
@ -81,6 +86,7 @@ describe('Reducers.LHS', () => {
|
||||
{
|
||||
isOpen: true,
|
||||
currentStaticPageId: '',
|
||||
size: 'medium',
|
||||
},
|
||||
{
|
||||
type: ActionTypes.CLOSE_LHS,
|
||||
@ -104,6 +110,7 @@ describe('Reducers.LHS', () => {
|
||||
{
|
||||
isOpen: true,
|
||||
currentStaticPageId: '',
|
||||
size: 'medium',
|
||||
},
|
||||
{
|
||||
type: action,
|
||||
|
@ -6,6 +6,7 @@ import {combineReducers} from 'redux';
|
||||
import {TeamTypes, UserTypes} from 'mattermost-redux/action_types';
|
||||
import type {GenericAction} from 'mattermost-redux/types/actions';
|
||||
import {ActionTypes} from 'utils/constants';
|
||||
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||
|
||||
function isOpen(state = false, action: GenericAction) {
|
||||
switch (action.type) {
|
||||
@ -29,6 +30,15 @@ function isOpen(state = false, action: GenericAction) {
|
||||
}
|
||||
}
|
||||
|
||||
function size(state = SidebarSize.MEDIUM, action: GenericAction) {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_LHS_SIZE:
|
||||
return action.size;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function currentStaticPageId(state = '', action: GenericAction) {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SELECT_STATIC_PAGE:
|
||||
@ -42,6 +52,6 @@ function currentStaticPageId(state = '', action: GenericAction) {
|
||||
|
||||
export default combineReducers({
|
||||
isOpen,
|
||||
|
||||
size,
|
||||
currentStaticPageId,
|
||||
});
|
||||
|
@ -19,6 +19,7 @@ describe('Reducers.RHS', () => {
|
||||
searchTerms: '',
|
||||
searchType: '',
|
||||
searchResultsTerms: '',
|
||||
size: 'medium',
|
||||
pluggableId: '',
|
||||
isSearchingFlaggedPost: false,
|
||||
isSearchingPinnedPost: false,
|
||||
@ -609,6 +610,7 @@ describe('Reducers.RHS', () => {
|
||||
searchTerms: 'user_id',
|
||||
searchType: '',
|
||||
searchResultsTerms: 'user id',
|
||||
size: 'medium',
|
||||
pluggableId: 'pluggable_id',
|
||||
isSearchingFlaggedPost: true,
|
||||
isSearchingPinnedPost: true,
|
||||
|
@ -14,6 +14,7 @@ import type {GenericAction} from 'mattermost-redux/types/actions';
|
||||
import type {RhsState} from 'types/store/rhs';
|
||||
|
||||
import {ActionTypes, RHSStates} from 'utils/constants';
|
||||
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||
|
||||
function selectedPostId(state = '', action: GenericAction) {
|
||||
switch (action.type) {
|
||||
@ -182,6 +183,15 @@ function rhsState(state: RhsState = null, action: GenericAction) {
|
||||
}
|
||||
}
|
||||
|
||||
function size(state: SidebarSize = SidebarSize.MEDIUM, action: GenericAction) {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_RHS_SIZE:
|
||||
return action.size;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function searchTerms(state = '', action: GenericAction) {
|
||||
switch (action.type) {
|
||||
case ActionTypes.UPDATE_RHS_SEARCH_TERMS:
|
||||
@ -381,6 +391,7 @@ export default combineReducers({
|
||||
searchTerms,
|
||||
searchType,
|
||||
searchResultsTerms,
|
||||
size,
|
||||
pluggableId,
|
||||
isSearchingFlaggedPost,
|
||||
isSearchingPinnedPost,
|
||||
|
@ -24,15 +24,31 @@ $sidebarOpacityAnimationDuration: 0.15s;
|
||||
}
|
||||
|
||||
#SidebarContainer {
|
||||
position: relative;
|
||||
z-index: 16;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 240px;
|
||||
width: var(--overrideLhsWidth, 240px);
|
||||
min-width: 240px;
|
||||
max-width: 240px;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid rgba(var(--center-channel-color-rgb), 0.12);
|
||||
background-color: var(--sidebar-bg);
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
min-width: 200px;
|
||||
max-width: 264px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1201px) {
|
||||
max-width: 304px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1681px) {
|
||||
max-width: 440px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
@ -376,11 +392,13 @@ $sidebarOpacityAnimationDuration: 0.15s;
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.SidebarHeaderMenuWrapper {
|
||||
overflow: hidden;
|
||||
padding-left: 5px;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
@ -1159,7 +1177,7 @@ $sidebarOpacityAnimationDuration: 0.15s;
|
||||
.SidebarChannel .SidebarLink {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 240px;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
padding: 7px 16px 7px 19px;
|
||||
@ -1361,8 +1379,6 @@ $sidebarOpacityAnimationDuration: 0.15s;
|
||||
|
||||
.multi-teams {
|
||||
#SidebarContainer {
|
||||
left: 65px;
|
||||
|
||||
+.inner-wrap #app-content {
|
||||
margin-left: 305px;
|
||||
}
|
||||
@ -1396,11 +1412,15 @@ $sidebarOpacityAnimationDuration: 0.15s;
|
||||
}
|
||||
|
||||
// Manual override of the global menu (and items) styling for the sidebar dropdown menu
|
||||
#sidebarDropdownMenu .MenuItem {
|
||||
>button,
|
||||
>div,
|
||||
>a {
|
||||
padding: 0 32px 0 24px;
|
||||
#sidebarDropdownMenu {
|
||||
position: fixed;
|
||||
|
||||
.MenuItem {
|
||||
>button,
|
||||
>div,
|
||||
>a {
|
||||
padding: 0 32px 0 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,18 +5,49 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
width: 400px;
|
||||
transition: width 0.25s 0s ease-in, z-index 0.25s 0s step-end;
|
||||
body:not(.layout-changing) & {
|
||||
transition: width 0.25s 0s ease-in, z-index 0.25s 0s step-end;
|
||||
}
|
||||
|
||||
width: var(--overrideRhsWidth, 400px);
|
||||
}
|
||||
|
||||
.sidebar--right {
|
||||
width: 400px;
|
||||
width: var(--overrideRhsWidth, 400px);
|
||||
|
||||
&:not(.expanded):not(.resize-disabled) {
|
||||
min-width: 400px;
|
||||
max-width: 400px;
|
||||
|
||||
&.sidebar--right--width-holder {
|
||||
@media screen and (min-width: 901px) and (max-width: 1200px) {
|
||||
min-width: 304px;
|
||||
max-width: 304px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 901px) {
|
||||
min-width: 304px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1201px) {
|
||||
max-width: 464px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1681px) {
|
||||
max-width: 776px;
|
||||
}
|
||||
}
|
||||
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
transform: translateX(100%);
|
||||
|
||||
&.is-open {
|
||||
transition: width 0.25s 0s ease-in, z-index 0.25s 0s step-end;
|
||||
body:not(.layout-changing) & {
|
||||
transition: width 0.25s 0s ease-in, z-index 0.25s 0s step-end;
|
||||
}
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
@ -27,7 +58,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
body:not(.layout-changing) & {
|
||||
transition: width 0.25s 0s ease-in, z-index 0.15s 0.1s step-start;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
@media screen and (min-width: 1680px) {
|
||||
.sidebar--right,
|
||||
.sidebar--right--width-holder {
|
||||
width: 500px;
|
||||
width: var(--overrideRhsWidth, 500px);
|
||||
|
||||
.sidebar--right__title__channel {
|
||||
max-width: 30rem;
|
||||
|
@ -373,6 +373,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes emphasis-sidebar-resize-line {
|
||||
50% {
|
||||
transform: scaleX(1.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin simple-in-and-out($classPrefix, $transition_time: 300ms) {
|
||||
@include simple-in-and-out-before($classPrefix, $transition_time);
|
||||
@include simple-in-and-out-after($classPrefix, $transition_time);
|
||||
|
@ -9,11 +9,16 @@ import {makeGetDraftsCount} from 'selectors/drafts';
|
||||
import {
|
||||
isCollapsedThreadsEnabled,
|
||||
} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||
|
||||
export function getIsLhsOpen(state: GlobalState): boolean {
|
||||
return state.views.lhs.isOpen;
|
||||
}
|
||||
|
||||
export function getLhsSize(state: GlobalState): SidebarSize {
|
||||
return state.views.lhs.size;
|
||||
}
|
||||
|
||||
export function getCurrentStaticPageId(state: GlobalState): string {
|
||||
return state.views.lhs.currentStaticPageId;
|
||||
}
|
||||
|
@ -13,11 +13,16 @@ import {localizeMessage} from 'utils/utils';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {RhsState, FakePost, SearchType} from 'types/store/rhs';
|
||||
import {PostDraft} from 'types/store/draft';
|
||||
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||
|
||||
export function getSelectedPostId(state: GlobalState): Post['id'] {
|
||||
return state.views.rhs.selectedPostId;
|
||||
}
|
||||
|
||||
export function getRhsSize(state: GlobalState): SidebarSize {
|
||||
return state.views.rhs.size;
|
||||
}
|
||||
|
||||
export function getSelectedPostFocussedAt(state: GlobalState): number {
|
||||
return state.views.rhs.selectedPostFocussedAt;
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {makeGetGlobalItem} from 'selectors/storage';
|
||||
import {setGlobalItem} from 'actions/storage';
|
||||
|
||||
export const currentUserAndTeamSuffix = createSelector('currentUserAndTeamSuffix', [
|
||||
const currentUserAndTeamSuffix = createSelector('currentUserAndTeamSuffix', [
|
||||
getCurrentUserId,
|
||||
getCurrentTeamId,
|
||||
], (
|
||||
@ -21,14 +21,6 @@ export const currentUserAndTeamSuffix = createSelector('currentUserAndTeamSuffix
|
||||
return `:${userId}:${teamId}`;
|
||||
});
|
||||
|
||||
export const currentUserSuffix = createSelector('currentUserSuffix', [
|
||||
getCurrentUserId,
|
||||
], (
|
||||
userId,
|
||||
) => {
|
||||
return `:${userId}`;
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param initialValue
|
||||
@ -38,10 +30,12 @@ export const currentUserSuffix = createSelector('currentUserSuffix', [
|
||||
export function useGlobalState<TVal>(
|
||||
initialValue: TVal,
|
||||
name: string,
|
||||
suffix?: string,
|
||||
): [TVal, (value: TVal) => ReturnType<typeof setGlobalItem>] {
|
||||
const dispatch = useDispatch();
|
||||
const suffix = useSelector(currentUserAndTeamSuffix);
|
||||
const storedKey = `${name}${suffix}`;
|
||||
const defaultSuffix = useSelector(currentUserAndTeamSuffix);
|
||||
const suffixToUse = suffix || defaultSuffix;
|
||||
const storedKey = `${name}${suffixToUse}`;
|
||||
|
||||
const value = useSelector(makeGetGlobalItem(storedKey, initialValue), shallowEqual);
|
||||
const setValue = useCallback((newValue) => dispatch(setGlobalItem(storedKey, newValue)), [storedKey]);
|
||||
|
@ -1,10 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||
|
||||
export type LhsViewState = {
|
||||
isOpen: boolean;
|
||||
|
||||
// Static pages (e.g. Threads, etc.)
|
||||
size: SidebarSize;
|
||||
|
||||
// Static pages (e.g. Threads, Insights, etc.)
|
||||
currentStaticPageId: string;
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import {Channel} from '@mattermost/types/channels';
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {RHSStates} from 'utils/constants';
|
||||
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||
|
||||
export type SearchType = '' | 'files' | 'messages';
|
||||
|
||||
@ -37,6 +38,7 @@ export type RhsViewState = {
|
||||
isSidebarExpanded: boolean;
|
||||
isMenuOpen: boolean;
|
||||
editChannelMembers: boolean;
|
||||
size: SidebarSize;
|
||||
};
|
||||
|
||||
export type RhsState = typeof RHSStates[keyof typeof RHSStates] | null;
|
||||
|
@ -199,6 +199,8 @@ export const ActionTypes = keyMirror({
|
||||
UPDATE_RHS_SEARCH_TYPE: null,
|
||||
UPDATE_RHS_SEARCH_RESULTS_TERMS: null,
|
||||
|
||||
SET_RHS_SIZE: null,
|
||||
|
||||
RHS_GO_BACK: null,
|
||||
|
||||
SET_RHS_EXPANDED: null,
|
||||
@ -255,6 +257,7 @@ export const ActionTypes = keyMirror({
|
||||
TOGGLE_LHS: null,
|
||||
OPEN_LHS: null,
|
||||
CLOSE_LHS: null,
|
||||
SET_LHS_SIZE: null,
|
||||
SELECT_STATIC_PAGE: null,
|
||||
|
||||
SET_SHOW_PREVIEW_ON_CREATE_COMMENT: null,
|
||||
@ -1491,6 +1494,10 @@ export const Constants = {
|
||||
TABLET_SCREEN_WIDTH: 1020,
|
||||
MOBILE_SCREEN_WIDTH: 768,
|
||||
|
||||
SMALL_SIDEBAR_BREAKPOINT: 900,
|
||||
MEDIUM_SIDEBAR_BREAKPOINT: 1200,
|
||||
LARGE_SIDEBAR_BREAKPOINT: 1680,
|
||||
|
||||
POST_MODAL_PADDING: 170,
|
||||
SCROLL_DELAY: 2000,
|
||||
SCROLL_PAGE_FRACTION: 3,
|
||||
|
Loading…
Reference in New Issue
Block a user