feat(core): show floating sidebar when hovering sidebar swtich (#8393)
web: https://github.com/user-attachments/assets/3cafe094-7938-4241-8d57-cfd5ccaadf25 client: https://github.com/user-attachments/assets/ca218a45-de92-4e0a-ad83-c0f47aee2962
This commit is contained in:
@@ -6,6 +6,7 @@ export const resizeHandleVerticalPadding = createVar(
|
|||||||
'resize-handle-vertical-padding'
|
'resize-handle-vertical-padding'
|
||||||
);
|
);
|
||||||
export const animationTimeout = createVar();
|
export const animationTimeout = createVar();
|
||||||
|
|
||||||
export const root = style({
|
export const root = style({
|
||||||
vars: {
|
vars: {
|
||||||
[panelWidthVar]: '256px',
|
[panelWidthVar]: '256px',
|
||||||
@@ -15,23 +16,28 @@ export const root = style({
|
|||||||
width: panelWidthVar,
|
width: panelWidthVar,
|
||||||
minWidth: panelWidthVar,
|
minWidth: panelWidthVar,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
selectors: {
|
|
||||||
'&[data-is-floating="true"]': {
|
|
||||||
position: 'absolute',
|
|
||||||
width: `calc(${panelWidthVar})`,
|
|
||||||
zIndex: 4,
|
zIndex: 4,
|
||||||
},
|
transform: 'translateX(0)',
|
||||||
'&[data-open="true"]': {
|
|
||||||
maxWidth: '50%',
|
maxWidth: '50%',
|
||||||
},
|
selectors: {
|
||||||
'&[data-open="false"][data-handle-position="right"]': {
|
'&[data-open="false"][data-handle-position="right"],&[data-is-floating="true"][data-handle-position="right"]':
|
||||||
|
{
|
||||||
marginLeft: `calc(${panelWidthVar} * -1)`,
|
marginLeft: `calc(${panelWidthVar} * -1)`,
|
||||||
},
|
},
|
||||||
'&[data-open="false"][data-handle-position="left"]': {
|
'&[data-open="false"][data-handle-position="left"],&[data-is-floating="true"][data-handle-position="left"]':
|
||||||
|
{
|
||||||
marginRight: `calc(${panelWidthVar} * -1)`,
|
marginRight: `calc(${panelWidthVar} * -1)`,
|
||||||
},
|
},
|
||||||
|
'&[data-open="true"][data-handle-position="right"][data-is-floating="true"]':
|
||||||
|
{
|
||||||
|
transform: `translateX(${panelWidthVar})`,
|
||||||
|
},
|
||||||
|
'&[data-open="true"][data-handle-position="left"][data-is-floating="true"]':
|
||||||
|
{
|
||||||
|
transform: `translateX(-${panelWidthVar})`,
|
||||||
|
},
|
||||||
'&[data-enable-animation="true"]': {
|
'&[data-enable-animation="true"]': {
|
||||||
transition: `margin-left ${animationTimeout} .05s, margin-right ${animationTimeout} .05s, width ${animationTimeout} .05s`,
|
transition: `margin-left ${animationTimeout}, margin-right ${animationTimeout}, transform ${animationTimeout}, background ${animationTimeout}`,
|
||||||
},
|
},
|
||||||
'&[data-transition-state="exited"]': {
|
'&[data-transition-state="exited"]': {
|
||||||
// avoid focus on hidden panel
|
// avoid focus on hidden panel
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import { forwardRef, useCallback, useLayoutEffect, useRef } from 'react';
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { useTransition } from 'react-transition-state';
|
import { useTransition } from 'react-transition-state';
|
||||||
|
|
||||||
import * as styles from './resize-panel.css';
|
import * as styles from './resize-panel.css';
|
||||||
@@ -129,17 +122,6 @@ const ResizeHandle = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// delay initial animation to avoid flickering
|
|
||||||
function useEnableAnimation() {
|
|
||||||
const [enable, setEnable] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
window.setTimeout(() => {
|
|
||||||
setEnable(true);
|
|
||||||
}, 500);
|
|
||||||
}, []);
|
|
||||||
return enable;
|
|
||||||
}
|
|
||||||
|
|
||||||
const animationTimeout = 300;
|
const animationTimeout = 300;
|
||||||
|
|
||||||
export const ResizePanel = forwardRef<HTMLDivElement, ResizePanelProps>(
|
export const ResizePanel = forwardRef<HTMLDivElement, ResizePanelProps>(
|
||||||
@@ -152,7 +134,7 @@ export const ResizePanel = forwardRef<HTMLDivElement, ResizePanelProps>(
|
|||||||
maxWidth,
|
maxWidth,
|
||||||
width,
|
width,
|
||||||
floating,
|
floating,
|
||||||
enableAnimation: _enableAnimation = true,
|
enableAnimation = true,
|
||||||
open,
|
open,
|
||||||
unmountOnExit,
|
unmountOnExit,
|
||||||
onOpen,
|
onOpen,
|
||||||
@@ -165,7 +147,6 @@ export const ResizePanel = forwardRef<HTMLDivElement, ResizePanelProps>(
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const enableAnimation = useEnableAnimation() && _enableAnimation;
|
|
||||||
const safeWidth = Math.min(maxWidth, Math.max(minWidth, width));
|
const safeWidth = Math.min(maxWidth, Math.max(minWidth, width));
|
||||||
const [{ status }, toggle] = useTransition({
|
const [{ status }, toggle] = useTransition({
|
||||||
timeout: animationTimeout,
|
timeout: animationTimeout,
|
||||||
|
|||||||
@@ -18,14 +18,8 @@ interface HeaderPros {
|
|||||||
export const Header = ({ left, center, right }: HeaderPros) => {
|
export const Header = ({ left, center, right }: HeaderPros) => {
|
||||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||||
const open = useLiveData(appSidebarService.open$);
|
const open = useLiveData(appSidebarService.open$);
|
||||||
const appSidebarFloating = useLiveData(appSidebarService.responsiveFloating$);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={clsx(style.header)} data-open={open} data-testid="header">
|
||||||
className={clsx(style.header)}
|
|
||||||
data-open={open}
|
|
||||||
data-sidebar-floating={appSidebarFloating}
|
|
||||||
data-testid="header"
|
|
||||||
>
|
|
||||||
<div className={clsx(style.headerSideContainer)}>
|
<div className={clsx(style.headerSideContainer)}>
|
||||||
<div className={clsx(style.headerItem, 'left')}>
|
<div className={clsx(style.headerItem, 'left')}>
|
||||||
<div>{left}</div>
|
<div>{left}</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { cssVar, lightCssVariables } from '@toeverything/theme';
|
import { cssVar, lightCssVariables } from '@toeverything/theme';
|
||||||
import { globalStyle, style } from '@vanilla-extract/css';
|
import { createVar, globalStyle, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const panelWidthVar = createVar('panel-width');
|
||||||
|
|
||||||
export const appStyle = style({
|
export const appStyle = style({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -48,7 +50,7 @@ export const mainContainerStyle = style({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
overflow: 'clip',
|
overflow: 'clip',
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
transition: 'margin-left 0.2s ease',
|
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[data-client-border="true"]': {
|
'&[data-client-border="true"]': {
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ export const MainContainer = forwardRef<
|
|||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
PropsWithChildren<MainContainerProps>
|
PropsWithChildren<MainContainerProps>
|
||||||
>(function MainContainer({ className, children, ...props }, ref): ReactElement {
|
>(function MainContainer({ className, children, ...props }, ref): ReactElement {
|
||||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
|
||||||
const appSideBarOpen = useLiveData(appSidebarService.open$);
|
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
|
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||||
|
const open = useLiveData(appSidebarService.open$);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -60,7 +60,7 @@ export const MainContainer = forwardRef<
|
|||||||
data-is-desktop={BUILD_CONFIG.isElectron}
|
data-is-desktop={BUILD_CONFIG.isElectron}
|
||||||
data-transparent={false}
|
data-transparent={false}
|
||||||
data-client-border={appSettings.clientBorder}
|
data-client-border={appSettings.clientBorder}
|
||||||
data-side-bar-open={appSideBarOpen}
|
data-side-bar-open={open}
|
||||||
data-testid="main-container"
|
data-testid="main-container"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { useRegisterCopyLinkCommands } from '@affine/core/components/hooks/affin
|
|||||||
import { useDocCollectionPageTitle } from '@affine/core/components/hooks/use-block-suite-workspace-page-title';
|
import { useDocCollectionPageTitle } from '@affine/core/components/hooks/use-block-suite-workspace-page-title';
|
||||||
import { useJournalInfoHelper } from '@affine/core/components/hooks/use-journal';
|
import { useJournalInfoHelper } from '@affine/core/components/hooks/use-journal';
|
||||||
import { HeaderDivider } from '@affine/core/components/pure/header';
|
import { HeaderDivider } from '@affine/core/components/pure/header';
|
||||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
|
||||||
import { EditorService } from '@affine/core/modules/editor';
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
|
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
|
||||||
import type { Doc } from '@blocksuite/affine/store';
|
import type { Doc } from '@blocksuite/affine/store';
|
||||||
@@ -34,16 +33,8 @@ const Header = forwardRef<
|
|||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
>(({ children, style, className }, ref) => {
|
>(({ children, style, className }, ref) => {
|
||||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
|
||||||
const appSidebarFloating = useLiveData(appSidebarService.responsiveFloating$);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div data-testid="header" style={style} className={className} ref={ref}>
|
||||||
data-testid="header"
|
|
||||||
style={style}
|
|
||||||
className={className}
|
|
||||||
ref={ref}
|
|
||||||
data-sidebar-floating={appSidebarFloating}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { map } from 'rxjs';
|
|||||||
|
|
||||||
import type { AppSidebarState } from '../providers/storage';
|
import type { AppSidebarState } from '../providers/storage';
|
||||||
|
|
||||||
const isMobile = !BUILD_CONFIG.isElectron && window.innerWidth < 768;
|
|
||||||
|
|
||||||
enum APP_SIDEBAR_STATE {
|
enum APP_SIDEBAR_STATE {
|
||||||
OPEN = 'open',
|
OPEN = 'open',
|
||||||
WIDTH = 'width',
|
WIDTH = 'width',
|
||||||
@@ -15,11 +13,15 @@ export class AppSidebar extends Entity {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether the sidebar is open,
|
||||||
|
* even if the sidebar is not open, hovering can show the floating sidebar
|
||||||
|
*/
|
||||||
open$ = LiveData.from(
|
open$ = LiveData.from(
|
||||||
this.appSidebarState
|
this.appSidebarState
|
||||||
.watch<boolean>(APP_SIDEBAR_STATE.OPEN)
|
.watch<boolean>(APP_SIDEBAR_STATE.OPEN)
|
||||||
.pipe(map(value => value ?? !isMobile)),
|
.pipe(map(value => value ?? true)),
|
||||||
!isMobile
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
width$ = LiveData.from(
|
width$ = LiveData.from(
|
||||||
@@ -28,8 +30,16 @@ export class AppSidebar extends Entity {
|
|||||||
.pipe(map(value => value ?? 248)),
|
.pipe(map(value => value ?? 248)),
|
||||||
248
|
248
|
||||||
);
|
);
|
||||||
responsiveFloating$ = new LiveData<boolean>(isMobile);
|
|
||||||
hoverFloating$ = new LiveData<boolean>(false);
|
/**
|
||||||
|
* hovering can show the floating sidebar, without open it
|
||||||
|
*/
|
||||||
|
hovering$ = new LiveData<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* small screen mode, will disable hover effect
|
||||||
|
*/
|
||||||
|
smallScreenMode$ = new LiveData<boolean>(false);
|
||||||
resizing$ = new LiveData<boolean>(false);
|
resizing$ = new LiveData<boolean>(false);
|
||||||
|
|
||||||
getCachedAppSidebarOpenState = () => {
|
getCachedAppSidebarOpenState = () => {
|
||||||
@@ -42,23 +52,15 @@ export class AppSidebar extends Entity {
|
|||||||
|
|
||||||
setOpen = (open: boolean) => {
|
setOpen = (open: boolean) => {
|
||||||
this.appSidebarState.set(APP_SIDEBAR_STATE.OPEN, open);
|
this.appSidebarState.set(APP_SIDEBAR_STATE.OPEN, open);
|
||||||
if (!open && this.hoverFloating$.value) {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
this.setHoverFloating(false);
|
|
||||||
}, 500);
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
setResponsiveFloating = (floating: boolean) => {
|
setSmallScreenMode = (smallScreenMode: boolean) => {
|
||||||
this.responsiveFloating$.next(floating);
|
this.smallScreenMode$.next(smallScreenMode);
|
||||||
};
|
};
|
||||||
|
|
||||||
setHoverFloating = (hoverFloating: boolean) => {
|
setHovering = (hoverFloating: boolean) => {
|
||||||
this.hoverFloating$.next(hoverFloating);
|
this.hovering$.next(hoverFloating);
|
||||||
};
|
};
|
||||||
|
|
||||||
setResizing = (resizing: boolean) => {
|
setResizing = (resizing: boolean) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
export const floatingMaxWidth = 768;
|
export const floatingMaxWidth = 768;
|
||||||
export const navWrapperStyle = style({
|
export const navWrapperStyle = style({
|
||||||
@@ -10,16 +11,45 @@ export const navWrapperStyle = style({
|
|||||||
},
|
},
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[data-has-border=true]': {
|
'&[data-has-border=true]': {
|
||||||
borderRight: `0.5px solid ${cssVar('borderColor')}`,
|
borderRight: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||||
},
|
},
|
||||||
'&[data-is-floating="true"]': {
|
'&[data-is-floating="true"]': {
|
||||||
backgroundColor: cssVar('backgroundPrimaryColor'),
|
backgroundColor: cssVarV2('layer/background/primary'),
|
||||||
},
|
},
|
||||||
'&[data-client-border="true"]': {
|
'&[data-client-border="true"]': {
|
||||||
paddingBottom: 8,
|
paddingBottom: 8,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
export const hoverNavWrapperStyle = style({
|
||||||
|
selectors: {
|
||||||
|
'&[data-is-floating="true"]': {
|
||||||
|
backgroundColor: cssVarV2('layer/background/primary'),
|
||||||
|
height: 'calc(100% - 60px)',
|
||||||
|
marginTop: '52px',
|
||||||
|
marginLeft: '4px',
|
||||||
|
boxShadow: cssVar('--affine-popover-shadow'),
|
||||||
|
borderRadius: '6px',
|
||||||
|
},
|
||||||
|
'&[data-is-floating="true"][data-is-electron="true"]': {
|
||||||
|
height: '100%',
|
||||||
|
marginTop: '-4px',
|
||||||
|
},
|
||||||
|
'&[data-is-floating="true"][data-client-border="true"]': {
|
||||||
|
backgroundColor: cssVarV2('layer/background/overlayPanel'),
|
||||||
|
},
|
||||||
|
'&[data-is-floating="true"][data-client-border="true"]::before': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
opacity: `var(--affine-noise-opacity, 0)`,
|
||||||
|
backgroundRepeat: 'repeat',
|
||||||
|
backgroundSize: '50px',
|
||||||
|
// TODO(@Peng): figure out how to use vanilla-extract webpack plugin to inject img url
|
||||||
|
backgroundImage: `var(--noise-background)`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
export const navHeaderButton = style({
|
export const navHeaderButton = style({
|
||||||
width: '32px',
|
width: '32px',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
@@ -62,7 +92,7 @@ export const sidebarFloatMaskStyle = style({
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: '100%',
|
right: '100%',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
background: cssVar('backgroundModalColor'),
|
background: cssVarV2('layer/background/modal'),
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[data-open="true"][data-is-floating="true"]': {
|
'&[data-open="true"][data-is-floating="true"]': {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
|
|||||||
@@ -9,14 +9,16 @@ import {
|
|||||||
useServiceOptional,
|
useServiceOptional,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import type { PropsWithChildren, ReactElement } from 'react';
|
import type { PropsWithChildren, ReactElement } from 'react';
|
||||||
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { AppSidebarService } from '../services/app-sidebar';
|
import { AppSidebarService } from '../services/app-sidebar';
|
||||||
import * as styles from './fallback.css';
|
import * as styles from './fallback.css';
|
||||||
import {
|
import {
|
||||||
floatingMaxWidth,
|
floatingMaxWidth,
|
||||||
|
hoverNavWrapperStyle,
|
||||||
navBodyStyle,
|
navBodyStyle,
|
||||||
navHeaderStyle,
|
navHeaderStyle,
|
||||||
navStyle,
|
navStyle,
|
||||||
@@ -32,6 +34,7 @@ export type History = {
|
|||||||
|
|
||||||
const MAX_WIDTH = 480;
|
const MAX_WIDTH = 480;
|
||||||
const MIN_WIDTH = 248;
|
const MIN_WIDTH = 248;
|
||||||
|
const isMacosDesktop = BUILD_CONFIG.isElectron && environment.isMacOs;
|
||||||
|
|
||||||
export function AppSidebar({ children }: PropsWithChildren) {
|
export function AppSidebar({ children }: PropsWithChildren) {
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
@@ -42,8 +45,35 @@ export function AppSidebar({ children }: PropsWithChildren) {
|
|||||||
|
|
||||||
const open = useLiveData(appSidebarService.open$);
|
const open = useLiveData(appSidebarService.open$);
|
||||||
const width = useLiveData(appSidebarService.width$);
|
const width = useLiveData(appSidebarService.width$);
|
||||||
const floating = useLiveData(appSidebarService.responsiveFloating$);
|
const smallScreenMode = useLiveData(appSidebarService.smallScreenMode$);
|
||||||
|
const hovering = useLiveData(appSidebarService.hovering$) && open !== true;
|
||||||
const resizing = useLiveData(appSidebarService.resizing$);
|
const resizing = useLiveData(appSidebarService.resizing$);
|
||||||
|
const [deferredHovering, setDeferredHovering] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
// if open, we don't need to show the floating sidebar
|
||||||
|
setDeferredHovering(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// we make a little delay here.
|
||||||
|
// this allow the sidebar close animation to complete.
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setDeferredHovering(hovering);
|
||||||
|
}, 150);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
}, [hovering, open]);
|
||||||
|
|
||||||
|
const sidebarState = smallScreenMode
|
||||||
|
? open
|
||||||
|
? 'floating-with-mask'
|
||||||
|
: 'close'
|
||||||
|
: open
|
||||||
|
? 'open'
|
||||||
|
: deferredHovering
|
||||||
|
? 'floating'
|
||||||
|
: 'close';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// do not float app sidebar on desktop
|
// do not float app sidebar on desktop
|
||||||
@@ -55,19 +85,8 @@ export function AppSidebar({ children }: PropsWithChildren) {
|
|||||||
const isFloatingMaxWidth = window.matchMedia(
|
const isFloatingMaxWidth = window.matchMedia(
|
||||||
`(max-width: ${floatingMaxWidth}px)`
|
`(max-width: ${floatingMaxWidth}px)`
|
||||||
).matches;
|
).matches;
|
||||||
const isOverflowWidth = window.matchMedia(
|
const isFloating = isFloatingMaxWidth;
|
||||||
`(max-width: ${width / 0.4}px)`
|
appSidebarService.setSmallScreenMode(isFloating);
|
||||||
).matches;
|
|
||||||
const isFloating = isFloatingMaxWidth || isOverflowWidth;
|
|
||||||
if (
|
|
||||||
open === undefined &&
|
|
||||||
appSidebarService.getCachedAppSidebarOpenState() === undefined
|
|
||||||
) {
|
|
||||||
// give the initial value,
|
|
||||||
// so that the sidebar can be closed on mobile by default
|
|
||||||
appSidebarService.setOpen(!isFloating);
|
|
||||||
}
|
|
||||||
appSidebarService.setResponsiveFloating(isFloating);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dOnResize = debounce(onResize, 50);
|
const dOnResize = debounce(onResize, 50);
|
||||||
@@ -75,10 +94,9 @@ export function AppSidebar({ children }: PropsWithChildren) {
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', dOnResize);
|
window.removeEventListener('resize', dOnResize);
|
||||||
};
|
};
|
||||||
}, [appSidebarService, open, width]);
|
}, [appSidebarService]);
|
||||||
|
|
||||||
const hasRightBorder = !BUILD_CONFIG.isElectron && !clientBorder;
|
const hasRightBorder = !BUILD_CONFIG.isElectron && !clientBorder;
|
||||||
const isMacosDesktop = BUILD_CONFIG.isElectron && environment.isMacOs;
|
|
||||||
|
|
||||||
const handleOpenChange = useCallback(
|
const handleOpenChange = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
@@ -105,11 +123,21 @@ export function AppSidebar({ children }: PropsWithChildren) {
|
|||||||
appSidebarService.setOpen(false);
|
appSidebarService.setOpen(false);
|
||||||
}, [appSidebarService]);
|
}, [appSidebarService]);
|
||||||
|
|
||||||
|
const onMouseEnter = useCallback(() => {
|
||||||
|
appSidebarService.setHovering(true);
|
||||||
|
}, [appSidebarService]);
|
||||||
|
|
||||||
|
const onMouseLeave = useCallback(() => {
|
||||||
|
appSidebarService.setHovering(false);
|
||||||
|
}, [appSidebarService]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ResizePanel
|
<ResizePanel
|
||||||
floating={floating}
|
floating={
|
||||||
open={open}
|
sidebarState === 'floating' || sidebarState === 'floating-with-mask'
|
||||||
|
}
|
||||||
|
open={sidebarState !== 'close'}
|
||||||
resizing={resizing}
|
resizing={resizing}
|
||||||
maxWidth={MAX_WIDTH}
|
maxWidth={MAX_WIDTH}
|
||||||
minWidth={MIN_WIDTH}
|
minWidth={MIN_WIDTH}
|
||||||
@@ -118,18 +146,25 @@ export function AppSidebar({ children }: PropsWithChildren) {
|
|||||||
onOpen={handleOpenChange}
|
onOpen={handleOpenChange}
|
||||||
onResizing={handleResizing}
|
onResizing={handleResizing}
|
||||||
onWidthChange={handleWidthChange}
|
onWidthChange={handleWidthChange}
|
||||||
className={navWrapperStyle}
|
className={clsx(navWrapperStyle, {
|
||||||
|
[hoverNavWrapperStyle]: sidebarState === 'floating',
|
||||||
|
})}
|
||||||
resizeHandleOffset={0}
|
resizeHandleOffset={0}
|
||||||
resizeHandleVerticalPadding={clientBorder ? 16 : 0}
|
resizeHandleVerticalPadding={clientBorder ? 16 : 0}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
data-transparent
|
data-transparent
|
||||||
data-open={open}
|
data-open={sidebarState !== 'close'}
|
||||||
data-has-border={hasRightBorder}
|
data-has-border={hasRightBorder}
|
||||||
data-testid="app-sidebar-wrapper"
|
data-testid="app-sidebar-wrapper"
|
||||||
data-is-macos-electron={isMacosDesktop}
|
data-is-macos-electron={isMacosDesktop}
|
||||||
data-client-border={clientBorder}
|
data-client-border={clientBorder}
|
||||||
|
data-is-electron={BUILD_CONFIG.isElectron}
|
||||||
>
|
>
|
||||||
<nav className={navStyle} data-testid="app-sidebar">
|
<nav className={navStyle} data-testid="app-sidebar">
|
||||||
{!BUILD_CONFIG.isElectron && <SidebarHeader />}
|
{!BUILD_CONFIG.isElectron && sidebarState !== 'floating' && (
|
||||||
|
<SidebarHeader />
|
||||||
|
)}
|
||||||
<div className={navBodyStyle} data-testid="sliderBar-inner">
|
<div className={navBodyStyle} data-testid="sliderBar-inner">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
@@ -138,7 +173,7 @@ export function AppSidebar({ children }: PropsWithChildren) {
|
|||||||
<div
|
<div
|
||||||
data-testid="app-sidebar-float-mask"
|
data-testid="app-sidebar-float-mask"
|
||||||
data-open={open}
|
data-open={open}
|
||||||
data-is-floating={floating}
|
data-is-floating={sidebarState === 'floating-with-mask'}
|
||||||
className={sidebarFloatMaskStyle}
|
className={sidebarFloatMaskStyle}
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { IconButton } from '@affine/component';
|
|||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { SidebarIcon } from '@blocksuite/icons/rc';
|
import { SidebarIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
import { AppSidebarService } from '../../services/app-sidebar';
|
import { AppSidebarService } from '../../services/app-sidebar';
|
||||||
import * as styles from './sidebar-switch.css';
|
import * as styles from './sidebar-switch.css';
|
||||||
@@ -16,21 +16,33 @@ export const SidebarSwitch = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const appSidebarService = useService(AppSidebarService).sidebar;
|
const appSidebarService = useService(AppSidebarService).sidebar;
|
||||||
const open = useLiveData(appSidebarService.open$);
|
const open = useLiveData(appSidebarService.open$);
|
||||||
|
const switchRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleMouseEnter = useCallback(() => {
|
||||||
|
appSidebarService.setHovering(true);
|
||||||
|
}, [appSidebarService]);
|
||||||
|
|
||||||
|
const handleClickSwitch = useCallback(() => {
|
||||||
|
appSidebarService.toggleSidebar();
|
||||||
|
}, [appSidebarService]);
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback(() => {
|
||||||
|
appSidebarService.setHovering(false);
|
||||||
|
}, [appSidebarService]);
|
||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const tooltipContent = open
|
const tooltipContent = open
|
||||||
? t['com.affine.sidebarSwitch.collapse']()
|
? t['com.affine.sidebarSwitch.collapse']()
|
||||||
: t['com.affine.sidebarSwitch.expand']();
|
: t['com.affine.sidebarSwitch.expand']();
|
||||||
|
|
||||||
const toggleSidebar = useCallback(() => {
|
|
||||||
appSidebarService.toggleSidebar();
|
|
||||||
}, [appSidebarService]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={switchRef}
|
||||||
data-show={show}
|
data-show={show}
|
||||||
className={styles.sidebarSwitchClip}
|
className={styles.sidebarSwitchClip}
|
||||||
data-testid={`app-sidebar-arrow-button-${open ? 'collapse' : 'expand'}`}
|
data-testid={`app-sidebar-arrow-button-${open ? 'collapse' : 'expand'}`}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip={tooltipContent}
|
tooltip={tooltipContent}
|
||||||
@@ -41,7 +53,7 @@ export const SidebarSwitch = ({
|
|||||||
style={{
|
style={{
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
}}
|
}}
|
||||||
onClick={toggleSidebar}
|
onClick={handleClickSwitch}
|
||||||
>
|
>
|
||||||
<SidebarIcon />
|
<SidebarIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const SidebarContainer = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.sidebarContainerInner, className)} {...props}>
|
<div className={clsx(styles.sidebarContainerInner, className)} {...props}>
|
||||||
<Header floating={false} onToggle={handleToggleOpen}>
|
<Header onToggle={handleToggleOpen}>
|
||||||
<SidebarHeaderSwitcher />
|
<SidebarHeaderSwitcher />
|
||||||
</Header>
|
</Header>
|
||||||
{sidebarTabs.length > 0 ? (
|
{sidebarTabs.length > 0 ? (
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { RightSidebarIcon } from '@blocksuite/icons/rc';
|
|||||||
import * as styles from './sidebar-header.css';
|
import * as styles from './sidebar-header.css';
|
||||||
|
|
||||||
export type HeaderProps = {
|
export type HeaderProps = {
|
||||||
floating: boolean;
|
|
||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
@@ -13,20 +12,13 @@ function Container({
|
|||||||
children,
|
children,
|
||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
floating,
|
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
floating?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div data-testid="header" style={style} className={className}>
|
||||||
data-testid="header"
|
|
||||||
style={style}
|
|
||||||
className={className}
|
|
||||||
data-sidebar-floating={floating}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -40,9 +32,9 @@ const ToggleButton = ({ onToggle }: { onToggle?: () => void }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Header = ({ floating, children, onToggle }: HeaderProps) => {
|
export const Header = ({ children, onToggle }: HeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<Container className={styles.header} floating={floating}>
|
<Container className={styles.header}>
|
||||||
{children}
|
{children}
|
||||||
{!BUILD_CONFIG.isElectron && (
|
{!BUILD_CONFIG.isElectron && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ test('Collapse Sidebar', async ({ page }) => {
|
|||||||
await page
|
await page
|
||||||
.locator('[data-testid=app-sidebar-arrow-button-collapse][data-show=true]')
|
.locator('[data-testid=app-sidebar-arrow-button-collapse][data-show=true]')
|
||||||
.click();
|
.click();
|
||||||
|
await page.mouse.move(500, 500);
|
||||||
const sliderBarArea = page.getByTestId('app-sidebar');
|
const sliderBarArea = page.getByTestId('app-sidebar');
|
||||||
await expect(sliderBarArea).not.toBeInViewport();
|
await expect(sliderBarArea).not.toBeInViewport();
|
||||||
});
|
});
|
||||||
@@ -100,6 +101,7 @@ test('Expand Sidebar', async ({ page }) => {
|
|||||||
await page
|
await page
|
||||||
.locator('[data-testid=app-sidebar-arrow-button-collapse][data-show=true]')
|
.locator('[data-testid=app-sidebar-arrow-button-collapse][data-show=true]')
|
||||||
.click();
|
.click();
|
||||||
|
await page.mouse.move(500, 500);
|
||||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||||
await expect(sliderBarArea).not.toBeInViewport();
|
await expect(sliderBarArea).not.toBeInViewport();
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ test('Collapse Sidebar', async ({ page }) => {
|
|||||||
.locator('[data-testid=app-sidebar-arrow-button-collapse][data-show=true]')
|
.locator('[data-testid=app-sidebar-arrow-button-collapse][data-show=true]')
|
||||||
.click();
|
.click();
|
||||||
const sliderBarArea = page.getByTestId('app-sidebar');
|
const sliderBarArea = page.getByTestId('app-sidebar');
|
||||||
|
await sliderBarArea.hover();
|
||||||
|
await page.mouse.move(300, 300);
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
await expect(sliderBarArea).not.toBeInViewport();
|
await expect(sliderBarArea).not.toBeInViewport();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,6 +23,9 @@ test('Expand Sidebar', async ({ page }) => {
|
|||||||
.locator('[data-testid=app-sidebar-arrow-button-collapse][data-show=true]')
|
.locator('[data-testid=app-sidebar-arrow-button-collapse][data-show=true]')
|
||||||
.click();
|
.click();
|
||||||
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
const sliderBarArea = page.getByTestId('sliderBar-inner');
|
||||||
|
await sliderBarArea.hover();
|
||||||
|
await page.mouse.move(300, 300);
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
await expect(sliderBarArea).not.toBeInViewport();
|
await expect(sliderBarArea).not.toBeInViewport();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
|
|||||||
Reference in New Issue
Block a user