From efae4cccd6e8f3d880c4db9930b9d9a510c600c3 Mon Sep 17 00:00:00 2001 From: Whitewater Date: Mon, 5 Jun 2023 00:43:24 -0700 Subject: [PATCH] feat: sticky table head in page list (#2668) Co-authored-by: Himself65 --- .../src/components/app-sidebar/index.css.ts | 1 + .../app-sidebar/sidebar-containers/index.tsx | 31 +-------- .../sidebar-containers/use-has-scroll-top.tsx | 29 +++++++++ .../src/components/page-list/all-page.tsx | 29 +++++---- .../components/page-list/all-pages-body.tsx | 15 +++-- .../src/components/page-list/mobile.tsx | 12 ++-- .../src/components/page-list/styles.ts | 10 +-- .../src/stories/page-list.stories.tsx | 2 +- packages/component/src/ui/button/dropdown.tsx | 6 +- .../component/src/ui/button/styles.css.ts | 4 +- packages/component/src/ui/button/styles.ts | 23 +------ packages/component/src/ui/empty/style.ts | 1 + .../src/ui/modal/modal-close-button.tsx | 1 + packages/component/src/ui/table/styles.ts | 65 +++++++++++++------ .../component/src/ui/table/table-head.tsx | 9 +-- packages/component/src/ui/table/table-row.tsx | 7 +- 16 files changed, 130 insertions(+), 115 deletions(-) create mode 100644 packages/component/src/components/app-sidebar/sidebar-containers/use-has-scroll-top.tsx diff --git a/packages/component/src/components/app-sidebar/index.css.ts b/packages/component/src/components/app-sidebar/index.css.ts index f99576940f..50497c0c0f 100644 --- a/packages/component/src/components/app-sidebar/index.css.ts +++ b/packages/component/src/components/app-sidebar/index.css.ts @@ -79,6 +79,7 @@ export const sidebarButtonStyle = style({ width: 'auto', height: '32px', color: 'var(--affine-icon-color)', + zIndex: 1, }); export const sidebarFloatMaskStyle = style({ diff --git a/packages/component/src/components/app-sidebar/sidebar-containers/index.tsx b/packages/component/src/components/app-sidebar/sidebar-containers/index.tsx index f56198afac..60f3a963e6 100644 --- a/packages/component/src/components/app-sidebar/sidebar-containers/index.tsx +++ b/packages/component/src/components/app-sidebar/sidebar-containers/index.tsx @@ -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
{children}
; } -function useHasScrollTop() { - const ref = useRef(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 ( diff --git a/packages/component/src/components/app-sidebar/sidebar-containers/use-has-scroll-top.tsx b/packages/component/src/components/app-sidebar/sidebar-containers/use-has-scroll-top.tsx new file mode 100644 index 0000000000..34a661a45d --- /dev/null +++ b/packages/component/src/components/app-sidebar/sidebar-containers/use-has-scroll-top.tsx @@ -0,0 +1,29 @@ +import { useEffect, useRef, useState } from 'react'; + +export function useHasScrollTop() { + const ref = useRef(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; +} diff --git a/packages/component/src/components/page-list/all-page.tsx b/packages/component/src/components/page-list/all-page.tsx index 8ca419d241..d239b43646 100644 --- a/packages/component/src/components/page-list/all-page.tsx +++ b/packages/component/src/components/page-list/all-page.tsx @@ -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 ( - + {titleList .filter(({ showWhen = () => true }) => showWhen()) .map(({ key, content, proportion, sortable = true, styles }) => ( @@ -97,7 +99,7 @@ const AllPagesHead = ({ ))} - + ); }; @@ -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 ( - - + +
{ const t = useAFFiNEI18N(); return ( - + {t['Title']()} {t['Created']()} {t['Moved to Trash']()} - + ); }; @@ -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 ( - @@ -229,14 +232,14 @@ export const PageListTrashView: React.FC<{ onOpenPage={onClickPage} /> - + ); } ); return ( - -
+ +
{ListItems}
diff --git a/packages/component/src/components/page-list/all-pages-body.tsx b/packages/component/src/components/page-list/all-pages-body.tsx index 00c2337a19..54d6665d94 100644 --- a/packages/component/src/components/page-list/all-pages-body.tsx +++ b/packages/component/src/components/page-list/all-pages-body.tsx @@ -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 ( - + { > {children} - + ); }; @@ -42,7 +43,7 @@ export const AllPagesBody = ({ const isSmallDevices = useIsSmallDevices(); const dataWithGroup = useDateGroup({ data, key: groupKey }); return ( - + {dataWithGroup.map( ( { @@ -71,7 +72,7 @@ export const AllPagesBody = ({ dataWithGroup[index - 1].groupName !== groupName) && ( {groupName} )} - + )} - + ); } diff --git a/packages/component/src/components/page-list/mobile.tsx b/packages/component/src/components/page-list/mobile.tsx index 2c96d6c095..3e815e30df 100644 --- a/packages/component/src/components/page-list/mobile.tsx +++ b/packages/component/src/components/page-list/mobile.tsx @@ -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 ( - + {t['Title']()} {!isPublicWorkspace && ( @@ -50,7 +50,7 @@ const MobileHead = ({ )} - + ); }; @@ -103,7 +103,7 @@ export const TrashListMobileView = ({ const ListItems = list.map(({ pageId, title, icon, onClickPage }, index) => { return ( - @@ -115,7 +115,7 @@ export const TrashListMobileView = ({ - + ); }); diff --git a/packages/component/src/components/page-list/styles.ts b/packages/component/src/components/page-list/styles.ts index 0d111931af..40864fdb11 100644 --- a/packages/component/src/components/page-list/styles.ts +++ b/packages/component/src/components/page-list/styles.ts @@ -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': { diff --git a/packages/component/src/stories/page-list.stories.tsx b/packages/component/src/stories/page-list.stories.tsx index f57536669a..35897c067a 100644 --- a/packages/component/src/stories/page-list.stories.tsx +++ b/packages/component/src/stories/page-list.stories.tsx @@ -113,7 +113,7 @@ AffineAllPageList.args = { removeToTrash: () => toast('Remove to trash'), }, { - pageId: '3', + pageId: '4', favorite: false, isPublicPage: false, icon: , diff --git a/packages/component/src/ui/button/dropdown.tsx b/packages/component/src/ui/button/dropdown.tsx index a9d103de91..6051d4f395 100644 --- a/packages/component/src/ui/button/dropdown.tsx +++ b/packages/component/src/ui/button/dropdown.tsx @@ -24,7 +24,11 @@ export const DropdownButton = forwardRef< {children} - + ); diff --git a/packages/component/src/ui/button/styles.css.ts b/packages/component/src/ui/button/styles.css.ts index a21a7048dc..43c659b02b 100644 --- a/packages/component/src/ui/button/styles.css.ts +++ b/packages/component/src/ui/button/styles.css.ts @@ -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)', }, }, diff --git a/packages/component/src/ui/button/styles.ts b/packages/component/src/ui/button/styles.ts index f0d560973f..1df90306fb 100644 --- a/packages/component/src/ui/button/styles.ts +++ b/packages/component/src/ui/button/styles.ts @@ -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, - }, + borderRadius, ':hover': { color: hoverColor ?? 'var(--affine-primary-color)', - '::after': { - background: hoverBackground || 'var(--affine-hover-color)', - }, + background: hoverBackground || 'var(--affine-hover-color)', ...(hoverStyle ?? {}), }, }; diff --git a/packages/component/src/ui/empty/style.ts b/packages/component/src/ui/empty/style.ts index 7f2727718a..c79a718a2b 100644 --- a/packages/component/src/ui/empty/style.ts +++ b/packages/component/src/ui/empty/style.ts @@ -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', diff --git a/packages/component/src/ui/modal/modal-close-button.tsx b/packages/component/src/ui/modal/modal-close-button.tsx index 03a0bb176a..625687943e 100644 --- a/packages/component/src/ui/modal/modal-close-button.tsx +++ b/packages/component/src/ui/modal/modal-close-button.tsx @@ -18,6 +18,7 @@ const StyledIconButton = styled(IconButton)< position: 'absolute', top: top ?? 24, right: right ?? 40, + zIndex: 1, }; }); diff --git a/packages/component/src/ui/table/styles.ts b/packages/component/src/ui/table/styles.ts index cab5d79d56..cf29cdd41d 100644 --- a/packages/component/src/ui/table/styles.ts +++ b/packages/component/src/ui/table/styles.ts @@ -1,16 +1,36 @@ import { styled, textEllipsis } from '../../styles'; import type { TableCellProps } from './interface'; -export const StyledTable = styled('table')(() => { - return { - fontSize: 'var(--affine-font-base)', - color: 'var(--affine-text-primary-color)', - tableLayout: 'fixed', - width: '100%', - borderCollapse: 'separate', - borderSpacing: '0', - }; -}); +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: '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: { - td: { - whiteSpace: 'nowrap', - }, - ':hover': { - td: { - background: 'unset', - }, - }, + }; +}); + +export const StyledTHeadRow = styled('tr')(() => { + return { + td: { + whiteSpace: 'nowrap', + // 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', diff --git a/packages/component/src/ui/table/table-head.tsx b/packages/component/src/ui/table/table-head.tsx index 552460d504..1c653dfce2 100644 --- a/packages/component/src/ui/table/table-head.tsx +++ b/packages/component/src/ui/table/table-head.tsx @@ -1,12 +1,5 @@ -import type { HTMLAttributes, PropsWithChildren } from 'react'; - import { StyledTableHead } from './styles'; -export const TableHead = ({ - children, - ...props -}: PropsWithChildren>) => { - return {children}; -}; +export const TableHead = StyledTableHead; export default TableHead; diff --git a/packages/component/src/ui/table/table-row.tsx b/packages/component/src/ui/table/table-row.tsx index 740485e406..e3c4229610 100644 --- a/packages/component/src/ui/table/table-row.tsx +++ b/packages/component/src/ui/table/table-row.tsx @@ -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;