feat(core): mobile renderer for explorer (#7942)

This commit is contained in:
CatsJuice
2024-08-29 06:09:45 +00:00
parent b96ad57568
commit f37051dc87
8 changed files with 210 additions and 82 deletions
@@ -64,3 +64,30 @@ export const collapseIcon = style({
}, },
}, },
}); });
// ------------- mobile -------------
export const mobileRoot = style([
root,
{
height: 25,
padding: '0 16px',
selectors: {
'&[data-collapsible="true"]:hover': {
backgroundColor: 'transparent',
},
'&[data-collapsible="true"]:active': {
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
},
},
},
]);
export const mobileLabel = style([
label,
{
color: cssVarV2('text/primary'),
fontSize: 20,
lineHeight: '25px',
letterSpacing: -0.45,
fontWeight: 400,
},
]);
@@ -9,6 +9,7 @@ export type CategoryDividerProps = PropsWithChildren<
label: string; label: string;
className?: string; className?: string;
collapsed?: boolean; collapsed?: boolean;
mobile?: boolean;
setCollapsed?: (collapsed: boolean) => void; setCollapsed?: (collapsed: boolean) => void;
} & { } & {
[key: `data-${string}`]: unknown; [key: `data-${string}`]: unknown;
@@ -22,6 +23,7 @@ export const CategoryDivider = forwardRef(
children, children,
className, className,
collapsed, collapsed,
mobile,
setCollapsed, setCollapsed,
...otherProps ...otherProps
}: CategoryDividerProps, }: CategoryDividerProps,
@@ -31,14 +33,15 @@ export const CategoryDivider = forwardRef(
return ( return (
<div <div
className={clsx([styles.root, className])} className={clsx(mobile ? styles.mobileRoot : styles.root, className)}
ref={ref} ref={ref}
onClick={() => setCollapsed?.(!collapsed)} onClick={() => setCollapsed?.(!collapsed)}
data-mobile={mobile}
data-collapsed={collapsed} data-collapsed={collapsed}
data-collapsible={collapsible} data-collapsible={collapsible}
{...otherProps} {...otherProps}
> >
<div className={styles.label}> <div className={mobile ? styles.mobileLabel : styles.label}>
{label} {label}
{collapsible ? ( {collapsible ? (
<ToggleCollapseIcon <ToggleCollapseIcon
@@ -8,6 +8,7 @@ import { ExplorerSection } from './entities/explore-section';
import { ExplorerService } from './services/explorer'; import { ExplorerService } from './services/explorer';
export { ExplorerService } from './services/explorer'; export { ExplorerService } from './services/explorer';
export type { CollapsibleSectionName } from './types'; export type { CollapsibleSectionName } from './types';
export { ExplorerMobileContext } from './views/mobile.context';
export { ExplorerCollections } from './views/sections/collections'; export { ExplorerCollections } from './views/sections/collections';
export { ExplorerFavorites } from './views/sections/favorites'; export { ExplorerFavorites } from './views/sections/favorites';
export { ExplorerMigrationFavorites } from './views/sections/migration-favorites'; export { ExplorerMigrationFavorites } from './views/sections/migration-favorites';
@@ -13,3 +13,8 @@ export const header = style({
}, },
}, },
}); });
// mobile
export const mobileContent = style({
paddingTop: 8,
});
@@ -7,11 +7,18 @@ import {
type ReactNode, type ReactNode,
type RefObject, type RefObject,
useCallback, useCallback,
useContext,
} from 'react'; } from 'react';
import { ExplorerService } from '../../services/explorer'; import { ExplorerService } from '../../services/explorer';
import type { CollapsibleSectionName } from '../../types'; import type { CollapsibleSectionName } from '../../types';
import { content, header, root } from './collapsible-section.css'; import { ExplorerMobileContext } from '../mobile.context';
import {
content,
header,
mobileContent,
root,
} from './collapsible-section.css';
interface CollapsibleSectionProps extends PropsWithChildren { interface CollapsibleSectionProps extends PropsWithChildren {
name: CollapsibleSectionName; name: CollapsibleSectionName;
@@ -43,6 +50,7 @@ export const CollapsibleSection = ({
contentClassName, contentClassName,
}: CollapsibleSectionProps) => { }: CollapsibleSectionProps) => {
const mobile = useContext(ExplorerMobileContext);
const section = useService(ExplorerService).sections[name]; const section = useService(ExplorerService).sections[name];
const collapsed = useLiveData(section.collapsed$); const collapsed = useLiveData(section.collapsed$);
@@ -62,6 +70,7 @@ export const CollapsibleSection = ({
data-testid={testId} data-testid={testId}
> >
<CategoryDivider <CategoryDivider
mobile={mobile}
data-testid={headerTestId} data-testid={headerTestId}
label={title} label={title}
setCollapsed={setCollapsed} setCollapsed={setCollapsed}
@@ -69,9 +78,11 @@ export const CollapsibleSection = ({
ref={headerRef} ref={headerRef}
className={clsx(header, headerClassName)} className={clsx(header, headerClassName)}
> >
{actions} {mobile ? null : actions}
</CategoryDivider> </CategoryDivider>
<Collapsible.Content className={clsx(content, contentClassName)}> <Collapsible.Content
className={clsx(mobile ? mobileContent : content, contentClassName)}
>
{children} {children}
</Collapsible.Content> </Collapsible.Content>
</Collapsible.Root> </Collapsible.Root>
@@ -0,0 +1,8 @@
import { createContext } from 'react';
/**
* To enable mobile manually
* > Using `environment.isMobile` directly will affect current web entry on mobile
* > So we control it manually for now
*/
export const ExplorerMobileContext = createContext(false);
@@ -36,6 +36,14 @@ export const itemRoot = style({
}, },
}, },
}); });
export const itemMain = style({
display: 'flex',
alignItems: 'center',
width: 0,
flex: 1,
position: 'relative',
gap: 12,
});
export const itemRenameAnchor = style({ export const itemRenameAnchor = style({
pointerEvents: 'none', pointerEvents: 'none',
position: 'absolute', position: 'absolute',
@@ -55,31 +63,26 @@ export const itemContent = style({
export const postfix = style({ export const postfix = style({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
right: '4px', right: 0,
position: 'absolute', position: 'absolute',
opacity: 0, opacity: 0,
pointerEvents: 'none', pointerEvents: 'none',
selectors: { selectors: {
[`${itemRoot}:hover &`]: { [`${itemRoot}:hover &`]: {
justifySelf: 'flex-end',
position: 'initial',
opacity: 1, opacity: 1,
pointerEvents: 'all', pointerEvents: 'initial',
position: 'initial',
}, },
}, },
}); });
export const icon = style({ export const iconContainer = style({
color: cssVarV2('icon/primary'),
fontSize: '20px',
});
export const emojiIcon = style({
width: '20px',
height: '20px',
display: 'flex', display: 'flex',
justifyContent: 'center', alignContent: 'center',
alignItems: 'center', alignItems: 'center',
width: 20,
height: 20,
color: cssVarV2('icon/primary'), color: cssVarV2('icon/primary'),
fontSize: cssVar('--affine-font-sm'), fontSize: 20,
}); });
export const collapsedIconContainer = style({ export const collapsedIconContainer = style({
width: '16px', width: '16px',
@@ -103,13 +106,6 @@ export const collapsedIconContainer = style({
}, },
}, },
}); });
export const iconsContainer = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
width: '44px',
flexShrink: 0,
});
export const collapsedIcon = style({ export const collapsedIcon = style({
transition: 'transform 0.2s ease-in-out', transition: 'transform 0.2s ease-in-out',
selectors: { selectors: {
@@ -182,3 +178,67 @@ export const draggedOverEffect = style({
}, },
}, },
}); });
// ---------- mobile ----------
export const mobileItemRoot = style([
itemRoot,
{
padding: '8px',
borderRadius: 0,
flexDirection: 'row-reverse',
gap: 12,
selectors: {
'&:hover': {
background: 'transparent',
},
'&:active': {
background: cssVar('hoverColor'),
},
'&[data-active="true"]': {
background: 'transparent',
},
},
':after': {
content: '',
width: `calc(100% + ${levelIndent})`,
height: 0.5,
background: cssVar('borderColor'),
bottom: 0,
position: 'absolute',
right: 0,
},
},
]);
export const mobileItemMain = style([itemMain, {}]);
export const mobileIconContainer = style([
iconContainer,
{
width: 32,
height: 32,
fontSize: 24,
},
]);
export const mobileCollapsedIconContainer = style([
collapsedIconContainer,
{
fontSize: 16,
},
]);
export const mobileItemContent = style([
itemContent,
{
fontSize: 17,
lineHeight: '22px',
letterSpacing: -0.43,
fontWeight: 400,
},
]);
export const mobileContentContainer = style([
contentContainer,
{
marginTop: 0,
},
]);
@@ -37,6 +37,7 @@ import {
useState, useState,
} from 'react'; } from 'react';
import { ExplorerMobileContext } from '../mobile.context';
import { ExplorerTreeContext } from './context'; import { ExplorerTreeContext } from './context';
import { DropEffect } from './drop-effect'; import { DropEffect } from './drop-effect';
import * as styles from './node.css'; import * as styles from './node.css';
@@ -108,6 +109,7 @@ export const ExplorerTreeNode = ({
onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void; onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void;
dropEffect?: ExplorerTreeNodeDropEffect; dropEffect?: ExplorerTreeNodeDropEffect;
} & { [key in `data-${string}`]?: any }) => { } & { [key in `data-${string}`]?: any }) => {
const mobile = useContext(ExplorerMobileContext);
const t = useI18n(); const t = useI18n();
const cid = useId(); const cid = useId();
const context = useContext(ExplorerTreeContext); const context = useContext(ExplorerTreeContext);
@@ -306,35 +308,74 @@ export const ExplorerTreeNode = ({
const content = ( const content = (
<div <div
onClick={handleClick} onClick={handleClick}
className={styles.itemRoot} className={mobile ? styles.mobileItemRoot : styles.itemRoot}
data-active={active} data-active={active}
data-disabled={disabled} data-disabled={disabled}
> >
<div className={styles.iconsContainer}> <div
<div data-disabled={disabled}
data-disabled={disabled} onClick={handleCollapsedChange}
onClick={handleCollapsedChange} data-testid="explorer-collapsed-button"
data-testid="explorer-collapsed-button" className={
className={styles.collapsedIconContainer} mobile
> ? styles.mobileCollapsedIconContainer
<ArrowDownSmallIcon : styles.collapsedIconContainer
className={styles.collapsedIcon} }
data-collapsed={collapsed !== false} >
/> <ArrowDownSmallIcon
</div> className={styles.collapsedIcon}
{emoji ? ( data-collapsed={collapsed !== false}
<div className={styles.emojiIcon}>{emoji}</div> />
) : (
Icon && (
<Icon
className={styles.icon}
draggedOver={draggedOver && !isSelfDraggedOver}
treeInstruction={treeInstruction}
collapsed={collapsed}
/>
)
)}
</div> </div>
<div className={clsx(mobile ? styles.mobileItemMain : styles.itemMain)}>
<div
className={mobile ? styles.mobileIconContainer : styles.iconContainer}
>
{emoji ??
(Icon && (
<Icon
draggedOver={draggedOver && !isSelfDraggedOver}
treeInstruction={treeInstruction}
collapsed={collapsed}
/>
))}
</div>
<div className={mobile ? styles.mobileItemContent : styles.itemContent}>
{name}
</div>
{postfix}
<div
className={styles.postfix}
onClick={e => {
// prevent jump to page
e.stopPropagation();
e.preventDefault();
}}
>
{inlineOperations.map(({ view }, index) => (
<Fragment key={index}>{view}</Fragment>
))}
{menuOperations.length > 0 && (
<Menu
items={menuOperations.map(({ view }, index) => (
<Fragment key={index}>{view}</Fragment>
))}
>
<IconButton
size="16"
data-testid="explorer-tree-node-operation-button"
style={{ marginLeft: 4 }}
>
<MoreHorizontalIcon />
</IconButton>
</Menu>
)}
</div>
</div>
{renameable && renaming && ( {renameable && renaming && (
<RenameModal <RenameModal
open open
@@ -346,37 +387,6 @@ export const ExplorerTreeNode = ({
<div className={styles.itemRenameAnchor} /> <div className={styles.itemRenameAnchor} />
</RenameModal> </RenameModal>
)} )}
<div className={styles.itemContent}>{name}</div>
{postfix}
<div
className={styles.postfix}
onClick={e => {
// prevent jump to page
e.stopPropagation();
e.preventDefault();
}}
>
{inlineOperations.map(({ view }, index) => (
<Fragment key={index}>{view}</Fragment>
))}
{menuOperations.length > 0 && (
<Menu
items={menuOperations.map(({ view }, index) => (
<Fragment key={index}>{view}</Fragment>
))}
>
<IconButton
size="16"
data-testid="explorer-tree-node-operation-button"
style={{ marginLeft: 4 }}
>
<MoreHorizontalIcon />
</IconButton>
</Menu>
)}
</div>
</div> </div>
); );
@@ -391,7 +401,10 @@ export const ExplorerTreeNode = ({
{...otherProps} {...otherProps}
> >
<div <div
className={clsx(styles.contentContainer, styles.draggedOverEffect)} className={clsx(
mobile ? styles.mobileContentContainer : styles.contentContainer,
styles.draggedOverEffect
)}
data-open={!collapsed} data-open={!collapsed}
data-self-dragged-over={isSelfDraggedOver} data-self-dragged-over={isSelfDraggedOver}
ref={dropTargetRef} ref={dropTargetRef}