feat: sticky table head in page list (#2668)

Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
Whitewater 2023-06-05 00:43:24 -07:00 committed by GitHub
parent b461a684ad
commit efae4cccd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 130 additions and 115 deletions

View File

@ -79,6 +79,7 @@ export const sidebarButtonStyle = style({
width: 'auto',
height: '32px',
color: 'var(--affine-icon-color)',
zIndex: 1,
});
export const sidebarFloatMaskStyle = style({

View File

@ -1,41 +1,14 @@
import * as ScrollArea from '@radix-ui/react-scroll-area';
import clsx from 'clsx';
import { type PropsWithChildren, useEffect, useRef, useState } from 'react';
import { type PropsWithChildren } from 'react';
import * as styles from './index.css';
import { useHasScrollTop } from './use-has-scroll-top';
export function SidebarContainer({ children }: PropsWithChildren) {
return <div className={clsx([styles.baseContainer])}>{children}</div>;
}
function useHasScrollTop() {
const ref = useRef<HTMLDivElement>(null);
const [hasScrollTop, setHasScrollTop] = useState(false);
useEffect(() => {
if (!ref.current) {
return;
}
const container = ref.current;
function updateScrollTop() {
if (container) {
const hasScrollTop = container.scrollTop > 0;
setHasScrollTop(hasScrollTop);
}
}
container.addEventListener('scroll', updateScrollTop);
updateScrollTop();
return () => {
container.removeEventListener('scroll', updateScrollTop);
};
}, []);
return [hasScrollTop, ref] as const;
}
export function SidebarScrollableContainer({ children }: PropsWithChildren) {
const [hasScrollTop, ref] = useHasScrollTop();
return (

View File

@ -0,0 +1,29 @@
import { useEffect, useRef, useState } from 'react';
export function useHasScrollTop() {
const ref = useRef<HTMLDivElement>(null);
const [hasScrollTop, setHasScrollTop] = useState(false);
useEffect(() => {
if (!ref.current) {
return;
}
const container = ref.current;
function updateScrollTop() {
if (container) {
const hasScrollTop = container.scrollTop > 0;
setHasScrollTop(hasScrollTop);
}
}
container.addEventListener('scroll', updateScrollTop);
updateScrollTop();
return () => {
container.removeEventListener('scroll', updateScrollTop);
};
}, []);
return [hasScrollTop, ref] as const;
}

View File

@ -5,13 +5,15 @@ import { useMediaQuery, useTheme } from '@mui/material';
import type React from 'react';
import { type CSSProperties } from 'react';
import { Table, TableBody, TableCell, TableHead, TableRow } from '../..';
import { Table, TableBody, TableCell, TableHead, TableHeadRow } from '../..';
import { TableBodyRow } from '../../ui/table';
import { useHasScrollTop } from '../app-sidebar/sidebar-containers/use-has-scroll-top';
import { AllPagesBody } from './all-pages-body';
import { NewPageButton } from './components/new-page-buttton';
import { TitleCell } from './components/title-cell';
import { AllPageListMobileView, TrashListMobileView } from './mobile';
import { TrashOperationCell } from './operation-cell';
import { StyledTableContainer, StyledTableRow } from './styles';
import { StyledTableContainer } from './styles';
import type { ListData, PageListProps, TrashListData } from './type';
import { useSorter } from './use-sorter';
import { formatDate, useIsSmallDevices } from './utils';
@ -66,7 +68,7 @@ const AllPagesHead = ({
return (
<TableHead>
<TableRow>
<TableHeadRow>
{titleList
.filter(({ showWhen = () => true }) => showWhen())
.map(({ key, content, proportion, sortable = true, styles }) => (
@ -97,7 +99,7 @@ const AllPagesHead = ({
</div>
</TableCell>
))}
</TableRow>
</TableHeadRow>
</TableHead>
);
};
@ -115,7 +117,7 @@ export const PageList = ({
key: DEFAULT_SORT_KEY,
order: 'desc',
});
const [hasScrollTop, ref] = useHasScrollTop();
const isSmallDevices = useIsSmallDevices();
if (isSmallDevices) {
return (
@ -138,8 +140,8 @@ export const PageList = ({
: undefined;
return (
<StyledTableContainer>
<Table>
<StyledTableContainer ref={ref}>
<Table showBorder={hasScrollTop} style={{ maxHeight: '100%' }}>
<AllPagesHead
isPublicWorkspace={isPublicWorkspace}
sorter={sorter}
@ -162,12 +164,12 @@ const TrashListHead = () => {
const t = useAFFiNEI18N();
return (
<TableHead>
<TableRow>
<TableHeadRow>
<TableCell proportion={0.5}>{t['Title']()}</TableCell>
<TableCell proportion={0.2}>{t['Created']()}</TableCell>
<TableCell proportion={0.2}>{t['Moved to Trash']()}</TableCell>
<TableCell proportion={0.1}></TableCell>
</TableRow>
</TableHeadRow>
</TableHead>
);
};
@ -179,6 +181,7 @@ export const PageListTrashView: React.FC<{
const t = useAFFiNEI18N();
const theme = useTheme();
const [hasScrollTop, ref] = useHasScrollTop();
const isSmallDevices = useMediaQuery(theme.breakpoints.down('sm'));
if (isSmallDevices) {
const mobileList = list.map(({ pageId, icon, title, onClickPage }) => ({
@ -205,7 +208,7 @@ export const PageListTrashView: React.FC<{
index
) => {
return (
<StyledTableRow
<TableBodyRow
data-testid={`page-list-item-${pageId}`}
key={`${pageId}-${index}`}
>
@ -229,14 +232,14 @@ export const PageListTrashView: React.FC<{
onOpenPage={onClickPage}
/>
</TableCell>
</StyledTableRow>
</TableBodyRow>
);
}
);
return (
<StyledTableContainer>
<Table>
<StyledTableContainer ref={ref}>
<Table showBorder={hasScrollTop}>
<TrashListHead />
<TableBody>{ListItems}</TableBody>
</Table>

View File

@ -3,18 +3,19 @@ import { useDraggable } from '@dnd-kit/core';
import type { ReactNode } from 'react';
import { Fragment } from 'react';
import { styled, TableBody, TableCell } from '../../..';
import { styled } from '../../styles';
import { TableBody, TableCell } from '../../ui/table';
import { FavoriteTag } from './components/favorite-tag';
import { TitleCell } from './components/title-cell';
import { OperationCell } from './operation-cell';
import { StyledTableRow } from './styles';
import { StyledTableBodyRow } from './styles';
import type { DateKey, DraggableTitleCellData, ListData } from './type';
import { useDateGroup } from './use-date-group';
import { formatDate, useIsSmallDevices } from './utils';
export const GroupRow = ({ children }: { children: ReactNode }) => {
return (
<StyledTableRow>
<StyledTableBodyRow>
<TableCell
style={{
color: 'var(--affine-text-secondary-color)',
@ -25,7 +26,7 @@ export const GroupRow = ({ children }: { children: ReactNode }) => {
>
{children}
</TableCell>
</StyledTableRow>
</StyledTableBodyRow>
);
};
@ -42,7 +43,7 @@ export const AllPagesBody = ({
const isSmallDevices = useIsSmallDevices();
const dataWithGroup = useDateGroup({ data, key: groupKey });
return (
<TableBody>
<TableBody style={{ overflowY: 'auto', height: '100%' }}>
{dataWithGroup.map(
(
{
@ -71,7 +72,7 @@ export const AllPagesBody = ({
dataWithGroup[index - 1].groupName !== groupName) && (
<GroupRow>{groupName}</GroupRow>
)}
<StyledTableRow data-testid={`page-list-item-${pageId}`}>
<StyledTableBodyRow data-testid={`page-list-item-${pageId}`}>
<DraggableTitleCell
pageId={pageId}
draggableData={{
@ -130,7 +131,7 @@ export const AllPagesBody = ({
/>
</TableCell>
)}
</StyledTableRow>
</StyledTableBodyRow>
</Fragment>
);
}

View File

@ -6,13 +6,13 @@ import {
TableBody,
TableCell,
TableHead,
TableRow,
TableHeadRow,
} from '../../..';
import { AllPagesBody } from './all-pages-body';
import { NewPageButton } from './components/new-page-buttton';
import {
StyledTableBodyRow,
StyledTableContainer,
StyledTableRow,
StyledTitleLink,
} from './styles';
import type { ListData } from './type';
@ -31,7 +31,7 @@ const MobileHead = ({
const t = useAFFiNEI18N();
return (
<TableHead>
<TableRow>
<TableHeadRow>
<TableCell proportion={0.8}>{t['Title']()}</TableCell>
{!isPublicWorkspace && (
<TableCell>
@ -50,7 +50,7 @@ const MobileHead = ({
</div>
</TableCell>
)}
</TableRow>
</TableHeadRow>
</TableHead>
);
};
@ -103,7 +103,7 @@ export const TrashListMobileView = ({
const ListItems = list.map(({ pageId, title, icon, onClickPage }, index) => {
return (
<StyledTableRow
<StyledTableBodyRow
data-testid={`page-list-item-${pageId}`}
key={`${pageId}-${index}`}
>
@ -115,7 +115,7 @@ export const TrashListMobileView = ({
</Content>
</StyledTitleLink>
</TableCell>
</StyledTableRow>
</StyledTableBodyRow>
);
});

View File

@ -1,13 +1,13 @@
import { displayFlex, styled } from '../../styles';
import { Content } from '../../ui/layout/content';
import { TableRow } from '../../ui/table/table-row';
import { TableBodyRow } from '../../ui/table/table-row';
export const StyledTableContainer = styled('div')(({ theme }) => {
return {
height: 'calc(100vh - 52px)',
padding: '18px 32px 80px 32px',
height: '100%',
padding: '0 32px 180px 32px',
maxWidth: '100%',
overflowY: 'auto',
overflowY: 'scroll',
[theme.breakpoints.down('sm')]: {
padding: '52px 0px',
'tr > td:first-of-type': {
@ -69,7 +69,7 @@ export const StyledTitlePreview = styled(Content)(() => {
};
});
export const StyledTableRow = styled(TableRow)(() => {
export const StyledTableBodyRow = styled(TableBodyRow)(() => {
return {
cursor: 'pointer',
'.favorite-button': {

View File

@ -113,7 +113,7 @@ AffineAllPageList.args = {
removeToTrash: () => toast('Remove to trash'),
},
{
pageId: '3',
pageId: '4',
favorite: false,
isPublicPage: false,
icon: <PageIcon />,

View File

@ -24,7 +24,11 @@ export const DropdownButton = forwardRef<
<span>{children}</span>
<span className={styles.divider} />
<span className={styles.dropdownWrapper} onClick={handleClickDropDown}>
<ArrowDownSmallIcon className={styles.icon} width={16} height={16} />
<ArrowDownSmallIcon
className={styles.dropdownIcon}
width={16}
height={16}
/>
</span>
</button>
);

View File

@ -44,10 +44,10 @@ export const dropdownWrapper = style({
paddingRight: '10px',
});
export const icon = style({
export const dropdownIcon = style({
borderRadius: '4px',
selectors: {
'&:hover': {
[`${dropdownWrapper}:hover &`]: {
background: 'var(--affine-hover-color)',
},
},

View File

@ -1,6 +1,6 @@
import type { CSSProperties } from 'react';
import { absoluteCenter, displayInlineFlex, styled } from '../../styles';
import { displayInlineFlex, styled } from '../../styles';
import type { ButtonProps } from './interface';
import { getButtonColors, getSize } from './utils';
@ -46,30 +46,13 @@ export const StyledIconButton = styled('button', {
WebkitAppRegion: 'no-drag',
color: 'var(--affine-icon-color)',
...displayInlineFlex('center', 'center'),
position: 'relative',
...(disabled ? { cursor: 'not-allowed', pointerEvents: 'none' } : {}),
transition: 'background .15s',
// TODO: we need to add @emotion/babel-plugin
'::after': {
content: '""',
width,
height,
borderRadius,
transition: 'background .15s',
...absoluteCenter({ horizontal: true, vertical: true }),
},
svg: {
position: 'relative',
zIndex: 1,
},
':hover': {
color: hoverColor ?? 'var(--affine-primary-color)',
'::after': {
background: hoverBackground || 'var(--affine-hover-color)',
},
...(hoverStyle ?? {}),
},
};

View File

@ -8,6 +8,7 @@ export const StyledEmptyContainer = styled('div')<{ style?: CSSProperties }>(
height: '100%',
...displayFlex('center', 'center'),
flexDirection: 'column',
color: 'var(--affine-text-secondary-color)',
svg: {
color: 'transparent',
width: style?.width ?? '248px',

View File

@ -18,6 +18,7 @@ const StyledIconButton = styled(IconButton)<
position: 'absolute',
top: top ?? 24,
right: right ?? 40,
zIndex: 1,
};
});

View File

@ -1,16 +1,36 @@
import { styled, textEllipsis } from '../../styles';
import type { TableCellProps } from './interface';
export const StyledTable = styled('table')(() => {
export const StyledTable = styled('table')<{ showBorder?: boolean }>(
({ showBorder }) => {
return {
fontSize: 'var(--affine-font-base)',
color: 'var(--affine-text-primary-color)',
tableLayout: 'fixed',
width: '100%',
borderCollapse: 'separate',
borderCollapse: 'collapse',
borderSpacing: '0',
...(typeof showBorder === 'boolean'
? {
thead: {
'::after': {
display: 'block',
position: 'absolute',
content: '""',
width: '100%',
height: '1px',
left: 0,
background: 'var(--affine-border-color)',
transition: 'opacity .15s',
opacity: showBorder ? 1 : 0,
},
},
}
: {}),
};
});
}
);
export const StyledTableBody = styled('tbody')(() => {
return {
@ -53,24 +73,29 @@ export const StyledTableHead = styled('thead')(() => {
return {
fontWeight: 500,
color: 'var(--affine-text-secondary-color)',
tr: {
};
});
export const StyledTHeadRow = styled('tr')(() => {
return {
td: {
whiteSpace: 'nowrap',
},
':hover': {
td: {
background: 'unset',
},
},
// How to set tbody height with overflow scroll
// see https://stackoverflow.com/questions/23989463/how-to-set-tbody-height-with-overflow-scroll
position: 'sticky',
top: 0,
background: 'var(--affine-background-primary-color)',
},
};
});
export const StyledTableRow = styled('tr')(() => {
export const StyledTBodyRow = styled('tr')(() => {
return {
td: {
transition: 'background .15s',
},
// Add border radius to table row
// see https://stackoverflow.com/questions/4094126/how-to-add-border-radius-on-table-row
'td:first-of-type': {
borderTopLeftRadius: '10px',
borderBottomLeftRadius: '10px',

View File

@ -1,12 +1,5 @@
import type { HTMLAttributes, PropsWithChildren } from 'react';
import { StyledTableHead } from './styles';
export const TableHead = ({
children,
...props
}: PropsWithChildren<HTMLAttributes<HTMLTableSectionElement>>) => {
return <StyledTableHead {...props}>{children}</StyledTableHead>;
};
export const TableHead = StyledTableHead;
export default TableHead;

View File

@ -1,4 +1,5 @@
import { StyledTableRow } from './styles';
export const TableRow = StyledTableRow;
import { StyledTBodyRow, StyledTHeadRow } from './styles';
export const TableHeadRow = StyledTHeadRow;
export const TableBodyRow = StyledTBodyRow;
export default TableRow;
export default TableHeadRow;