fix: page info styles (#5910)

fix inconsistent styles compared to the one defined in figma
fix https://github.com/toeverything/AFFiNE/issues/5904
fix https://github.com/toeverything/AFFiNE/issues/5903
This commit is contained in:
Peng Xiao 2024-02-26 14:11:24 +00:00
parent 6ccc4f1501
commit 2295685590
No known key found for this signature in database
GPG Key ID: 23F23D9E8B3971ED
10 changed files with 194 additions and 53 deletions

View File

@ -1,12 +1,10 @@
import { cssVar } from '@toeverything/theme';
import { atom } from 'jotai';
import { createContext } from 'react';
import type { PagePropertiesManager } from './page-properties-manager';
// @ts-expect-error this should always be set
export const managerContext = createContext<PagePropertiesManager>();
export const pageInfoCollapsedAtom = atom(false);
type TagColorHelper<T> = T extends `paletteLine${infer Color}` ? Color : never;
type TagColorName = TagColorHelper<Parameters<typeof cssVar>[0]>;

View File

@ -11,7 +11,13 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { Page, useLiveData, useService, Workspace } from '@toeverything/infra';
import { noop } from 'lodash-es';
import { type ChangeEventHandler, useCallback, useContext } from 'react';
import {
type ChangeEventHandler,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import { managerContext } from './common';
import * as styles from './styles.css';
@ -84,30 +90,85 @@ export const CheckboxValue = ({ property }: PropertyRowValueProps) => {
);
};
export const TextValue = ({ property, meta }: PropertyRowValueProps) => {
export const TextValue = ({ property }: PropertyRowValueProps) => {
const manager = useContext(managerContext);
const [value, setValue] = useState<string>(property.value);
const handleClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
// todo: show edit popup
}, []);
const handleOnChange: ChangeEventHandler<HTMLInputElement> = useCallback(
e => {
const handleBlur = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
manager.updateCustomProperty(property.id, {
value: e.target.value,
value: e.target.value.trim(),
});
},
[manager, property.id]
);
const handleOnChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
e => {
setValue(e.target.value);
},
[]
);
const t = useAFFiNEI18N();
const isNumber = meta.type === 'number';
useEffect(() => {
setValue(property.value);
}, [property.value]);
return (
<div onClick={handleClick} className={styles.propertyRowValueTextCell}>
<textarea
className={styles.propertyRowValueTextarea}
value={value || ''}
onChange={handleOnChange}
onClick={handleClick}
onBlur={handleBlur}
data-empty={!value}
placeholder={t[
'com.affine.page-properties.property-value-placeholder'
]()}
/>
<div className={styles.propertyRowValueTextareaInvisible}>
{value}
{value?.endsWith('\n') || value === '' ? <br /> : null}
</div>
</div>
);
};
export const NumberValue = ({ property }: PropertyRowValueProps) => {
const manager = useContext(managerContext);
const [value, setValue] = useState(property.value);
const handleClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
}, []);
const handleBlur = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
manager.updateCustomProperty(property.id, {
value: e.target.value.trim(),
});
},
[manager, property.id]
);
const handleOnChange: ChangeEventHandler<HTMLInputElement> = useCallback(
e => {
setValue(e.target.value);
},
[]
);
const t = useAFFiNEI18N();
useEffect(() => {
setValue(property.value);
}, [property.value]);
return (
<input
type={isNumber ? 'number' : 'text'}
value={property.value || ''}
className={styles.propertyRowValueNumberCell}
type={'number'}
value={value || ''}
onChange={handleOnChange}
onClick={handleClick}
className={styles.propertyRowValueTextCell}
data-empty={!property.value}
onBlur={handleBlur}
data-empty={!value}
placeholder={t['com.affine.page-properties.property-value-placeholder']()}
/>
);
@ -152,7 +213,7 @@ export const propertyValueRenderers: Record<
date: DateValue,
checkbox: CheckboxValue,
text: TextValue,
number: TextValue,
number: NumberValue,
// todo: fix following
tags: TagsValue,
progress: TextValue,

View File

@ -7,6 +7,7 @@ export const root = style({
display: 'flex',
width: '100%',
justifyContent: 'center',
fontFamily: cssVar('fontFamily'),
vars: {
[propertyNameCellWidth]: '160px',
},
@ -40,6 +41,7 @@ export const tableHeaderInfoRow = style({
color: cssVar('textSecondaryColor'),
fontSize: cssVar('fontSm'),
fontWeight: 500,
minHeight: 34,
});
export const tableHeaderSecondaryRow = style({
@ -87,7 +89,7 @@ export const tableHeaderTimestamp = style({
});
export const tableHeaderDivider = style({
height: '1px',
height: '0.5px',
width: '100%',
margin: '8px 0',
backgroundColor: cssVar('dividerColor'),
@ -118,7 +120,6 @@ export const addPropertyButton = style({
color: cssVar('textPrimaryColor'),
backgroundColor: cssVar('hoverColor'),
},
marginTop: '8px',
});
export const collapsedIcon = style({
@ -233,6 +234,7 @@ export const propertyRowNameCell = style([
propertyRowCell,
{
padding: 6,
flexShrink: 0,
color: cssVar('textSecondaryColor'),
width: propertyNameCellWidth,
gap: 6,
@ -298,13 +300,49 @@ export const propertyRowValueCell = style([
export const propertyRowValueTextCell = style([
propertyRowValueCell,
{
':focus': {
padding: 0,
position: 'relative',
':focus-within': {
border: `1px solid ${cssVar('blue700')}`,
boxShadow: cssVar('activeShadow'),
},
},
]);
export const propertyRowValueTextarea = style([
propertyRowValueCell,
{
border: 'none',
padding: '6px 8px',
height: '100%',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
overflow: 'hidden',
},
]);
export const propertyRowValueTextareaInvisible = style([
propertyRowValueCell,
{
border: 'none',
padding: '6px 8px',
visibility: 'hidden',
whiteSpace: 'break-spaces',
wordBreak: 'break-all',
overflow: 'hidden',
},
]);
export const propertyRowValueNumberCell = style([
propertyRowValueTextCell,
{
padding: '6px 8px',
},
]);
export const menuHeader = style({
display: 'flex',
flexDirection: 'row',
@ -429,7 +467,7 @@ export const propertyRowTypeItem = style({
export const propertyTypeName = style({
color: cssVar('textSecondaryColor'),
fontSize: cssVar('fontXs'),
fontSize: cssVar('fontSm'),
display: 'flex',
alignItems: 'center',
gap: 4,

View File

@ -44,7 +44,7 @@ import { SortableContext, useSortable } from '@dnd-kit/sortable';
import * as Collapsible from '@radix-ui/react-collapsible';
import clsx from 'clsx';
import { use } from 'foxact/use';
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import type React from 'react';
import {
type CSSProperties,
@ -60,7 +60,7 @@ import {
} from 'react';
import { AffinePageReference } from '../reference-link';
import { managerContext, pageInfoCollapsedAtom } from './common';
import { managerContext } from './common';
import { ConfirmDeletePropertyModal } from './confirm-delete-property-modal';
import {
getDefaultIconName,
@ -583,6 +583,8 @@ export const PagePropertyRowNameMenu = ({
interface PagePropertiesTableHeaderProps {
className?: string;
style?: React.CSSProperties;
open: boolean;
onOpenChange: (open: boolean) => void;
}
// backlinks - #no Updated yyyy-mm-dd
@ -591,6 +593,8 @@ interface PagePropertiesTableHeaderProps {
export const PagePropertiesTableHeader = ({
className,
style,
open,
onOpenChange,
}: PagePropertiesTableHeaderProps) => {
const manager = useContext(managerContext);
@ -630,10 +634,9 @@ export const PagePropertiesTableHeader = ({
);
}, [manager.createDate, manager.updatedDate, t]);
const [collapsed, setCollapsed] = useAtom(pageInfoCollapsedAtom);
const handleCollapse = useCallback(() => {
setCollapsed(prev => !prev);
}, [setCollapsed]);
onOpenChange(!open);
}, [onOpenChange, open]);
const properties = manager.getOrderedCustomProperties();
@ -652,7 +655,7 @@ export const PagePropertiesTableHeader = ({
</div>
<Divider />
<div className={styles.tableHeaderSecondaryRow}>
<div className={clsx(collapsed ? styles.pageInfoDimmed : null)}>
<div className={clsx(!open ? styles.pageInfoDimmed : null)}>
{t['com.affine.page-properties.page-info']()}
</div>
{properties.length === 0 || manager.readonly ? null : (
@ -668,10 +671,11 @@ export const PagePropertiesTableHeader = ({
<Collapsible.Trigger asChild role="button" onClick={handleCollapse}>
<IconButton
type="plain"
data-testid="page-info-collapse"
icon={
<ToggleExpandIcon
className={styles.collapsedIcon}
data-collapsed={collapsed !== false}
data-collapsed={!open}
/>
}
/>
@ -782,24 +786,27 @@ export const PagePropertiesTableBody = ({
style={style}
>
<PageTagsRow />
<div className={styles.tableBodySortable}>
<SortableProperties>
{properties =>
properties
.filter(
property =>
manager.isPropertyRequired(property.id) ||
(property.visibility !== 'hide' &&
!(
property.visibility === 'hide-if-empty' && !property.value
))
)
.map(property => (
<PagePropertyRow key={property.id} property={property} />
))
}
</SortableProperties>
</div>
<SortableProperties>
{properties =>
properties.length ? (
<div className={styles.tableBodySortable}>
{properties
.filter(
property =>
manager.isPropertyRequired(property.id) ||
(property.visibility !== 'hide' &&
!(
property.visibility === 'hide-if-empty' &&
!property.value
))
)
.map(property => (
<PagePropertyRow key={property.id} property={property} />
))}
</div>
) : null
}
</SortableProperties>
{manager.readonly ? null : <PagePropertiesAddProperty />}
<Divider />
</Collapsible.Content>
@ -1004,12 +1011,16 @@ export const PagePropertiesAddProperty = () => {
const PagePropertiesTableInner = () => {
const manager = useContext(managerContext);
const collapsed = useAtomValue(pageInfoCollapsedAtom);
const [expanded, setExpanded] = useState(false);
use(manager.workspace.blockSuiteWorkspace.doc.whenSynced);
return (
<div className={styles.root}>
<Collapsible.Root open={!collapsed} className={styles.rootCentered}>
<PagePropertiesTableHeader />
<Collapsible.Root
open={expanded}
onOpenChange={setExpanded}
className={styles.rootCentered}
>
<PagePropertiesTableHeader open={expanded} onOpenChange={setExpanded} />
<PagePropertiesTableBody />
</Collapsible.Root>
</div>

View File

@ -1,8 +1,7 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
import { globalStyle, style } from '@vanilla-extract/css';
export const tagsInlineEditor = style({
width: '100%',
selectors: {
'&[data-empty=true]': {
color: cssVar('placeholderColor'),
@ -14,7 +13,7 @@ export const tagsEditorRoot = style({
display: 'flex',
flexDirection: 'column',
width: '100%',
gap: '8px',
gap: '12px',
});
export const inlineTagsContainer = style({
@ -70,7 +69,7 @@ export const tagsEditorTagsSelectorHeader = style({
justifyContent: 'space-between',
alignItems: 'center',
padding: '0 8px',
fontSize: '14px',
fontSize: cssVar('fontXs'),
fontWeight: 500,
color: cssVar('textSecondaryColor'),
});
@ -96,6 +95,19 @@ export const tagSelectorItem = style({
},
});
export const tagEditIcon = style({
opacity: 0,
selectors: {
[`${tagSelectorItem}:hover &`]: {
opacity: 1,
},
},
});
globalStyle(`${tagEditIcon}[data-state=open]`, {
opacity: 1,
});
export const spacer = style({
flexGrow: 1,
});

View File

@ -292,7 +292,11 @@ export const TagsEditor = ({
<TagItem maxWidth="100%" tag={tag} mode="inline" />
<div className={styles.spacer} />
<EditTagMenu tag={tag}>
<IconButton type="plain" icon={<MoreHorizontalIcon />} />
<IconButton
className={styles.tagEditIcon}
type="plain"
icon={<MoreHorizontalIcon />}
/>
</EditTagMenu>
</div>
);
@ -319,7 +323,7 @@ export const TagsEditor = ({
</div>
)}
</Scrollable.Viewport>
<Scrollable.Scrollbar />
<Scrollable.Scrollbar style={{ transform: 'translateX(6px)' }} />
</Scrollable.Root>
</div>
</div>

View File

@ -82,7 +82,7 @@ export const tagInnerWrapper = style({
export const tagInline = style([
tagInnerWrapper,
{
fontSize: cssVar('fontXs'),
fontSize: cssVar('fontSm'),
borderRadius: '10px',
columnGap: '4px',
border: `1px solid ${cssVar('borderColor')}`,

View File

@ -16,6 +16,7 @@ import {
changePropertyVisibility,
clickPropertyValue,
closeTagsEditor,
ensurePagePropertiesVisible,
expectTagsVisible,
getPropertyValueLocator,
openTagsEditor,
@ -30,6 +31,7 @@ test.beforeEach(async ({ page }) => {
await clickPageModeButton(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
await ensurePagePropertiesVisible(page);
});
test('allow create tag', async ({ page }) => {
@ -48,6 +50,7 @@ test('allow create tag', async ({ page }) => {
test('allow create tag on journals page', async ({ page }) => {
await openJournalsPage(page);
await waitForEditorLoad(page);
await ensurePagePropertiesVisible(page);
await openTagsEditor(page);
await searchAndCreateTag(page, 'Test1');

View File

@ -126,6 +126,7 @@ export const createPageWithTag = async (
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('test page');
await page.getByTestId('page-info-collapse').click();
await page
.locator('[data-testid="page-property-row"][data-property="tags"]')
.click();

View File

@ -6,6 +6,19 @@ export const getPropertyValueLocator = (page: Page, property: string) => {
);
};
export const ensurePagePropertiesVisible = async (page: Page) => {
if (
await page
.getByRole('button', {
name: 'Add property',
})
.isVisible()
) {
return;
}
await page.getByTestId('page-info-collapse').click();
};
export const clickPropertyValue = async (page: Page, property: string) => {
await getPropertyValueLocator(page, property).click();
};