diff --git a/CHANGELOG.md b/CHANGELOG.md index b6232c41..5123f2b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to - 💄(frontend) update DocsGridOptions component #432 - 💄(frontend) update DocHeader ui #446 - 💄(frontend) update doc versioning ui #463 +- 💄(frontend) update doc summary ui #473 ## [1.10.0] - 2024-12-17 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts index 5fd42f6f..9b4d8788 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { createDoc, goToGridDoc, verifyDocName } from './common'; +import { createDoc, verifyDocName } from './common'; test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -19,20 +19,13 @@ test.describe('Doc Table Content', () => { await verifyDocName(page, randomDoc); - await page.getByLabel('Open the document options').click(); - await page - .getByRole('button', { - name: 'Table of contents', - }) - .click(); - - const panel = page.getByLabel('Document panel'); const editor = page.locator('.ProseMirror'); await editor.locator('.bn-block-outer').last().fill('/'); + await page.getByText('Heading 1').click(); - await page.keyboard.type('Hello World'); - await editor.getByText('Hello').dblclick(); + await page.keyboard.type('Level 1'); + await editor.getByText('Level 1').dblclick(); await page.getByRole('button', { name: 'Strike' }).click(); await page.locator('.bn-block-outer').first().click(); @@ -40,101 +33,44 @@ test.describe('Doc Table Content', () => { await page.locator('.bn-block-outer').last().click(); // Create space to fill the viewport - for (let i = 0; i < 10; i++) { + for (let i = 0; i < 2; i++) { await page.keyboard.press('Enter'); } await editor.locator('.bn-block-outer').last().fill('/'); await page.getByText('Heading 2').click(); - await page.keyboard.type('Super World', { delay: 100 }); + await page.keyboard.type('Level 2'); await page.locator('.bn-block-outer').last().click(); // Create space to fill the viewport - for (let i = 0; i < 10; i++) { + for (let i = 0; i < 2; i++) { await page.keyboard.press('Enter'); } await editor.locator('.bn-block-outer').last().fill('/'); await page.getByText('Heading 3').click(); - await page.keyboard.type('Another World'); + await page.keyboard.type('Level 3'); - const hello = panel.getByText('Hello World'); - const superW = panel.getByText('Super World'); - const another = panel.getByText('Another World'); + expect(true).toBe(true); - await expect(hello).toBeVisible(); - await expect(hello).toHaveCSS('font-size', /17/); - await expect(hello).toHaveAttribute('aria-selected', 'true'); + const summaryContainer = page.locator('#summaryContainer'); + await summaryContainer.hover(); - await expect(superW).toBeVisible(); - await expect(superW).toHaveCSS('font-size', /14/); - await expect(superW).toHaveAttribute('aria-selected', 'false'); + const level1 = summaryContainer.getByText('Level 1'); + const level2 = summaryContainer.getByText('Level 2'); + const level3 = summaryContainer.getByText('Level 3'); - await expect(another).toBeVisible(); - await expect(another).toHaveCSS('font-size', /12/); - await expect(another).toHaveAttribute('aria-selected', 'false'); + await expect(level1).toBeVisible(); + await expect(level1).toHaveCSS('padding', /4px 0px/); + await expect(level1).toHaveAttribute('aria-selected', 'true'); - await hello.click(); + await expect(level2).toBeVisible(); + await expect(level2).toHaveCSS('padding-left', /14.4px/); + await expect(level2).toHaveAttribute('aria-selected', 'false'); - await expect(editor.getByText('Hello World')).toBeInViewport(); - await expect(hello).toHaveAttribute('aria-selected', 'true'); - await expect(superW).toHaveAttribute('aria-selected', 'false'); - - await another.click(); - - await expect(editor.getByText('Hello World')).not.toBeInViewport(); - await expect(hello).toHaveAttribute('aria-selected', 'false'); - await expect(superW).toHaveAttribute('aria-selected', 'true'); - - await panel.getByText('Back to top').click(); - await expect(editor.getByText('Hello World')).toBeInViewport(); - await expect(hello).toHaveAttribute('aria-selected', 'true'); - await expect(superW).toHaveAttribute('aria-selected', 'false'); - - await panel.getByText('Go to bottom').click(); - await expect(editor.getByText('Hello World')).not.toBeInViewport(); - await expect(superW).toHaveAttribute('aria-selected', 'true'); - }); - - test('it checks that table contents panel is opened automaticaly if more that 2 headings', async ({ - page, - browserName, - }) => { - const [randomDoc] = await createDoc( - page, - 'doc-table-content', - browserName, - 1, - ); - - await verifyDocName(page, randomDoc); - await expect(page.getByLabel('Open the panel')).toBeHidden(); - - const editor = page.locator('.ProseMirror'); - - await editor.locator('.bn-block-outer').last().fill('/'); - await page.getByText('Heading 1').click(); - await page.keyboard.type('Hello World', { delay: 100 }); - - await page.keyboard.press('Enter'); - - await editor.locator('.bn-block-outer').last().fill('/'); - await page.getByText('Heading 2').click(); - await page.keyboard.type('Super World', { delay: 100 }); - - await goToGridDoc(page, { - title: randomDoc, - }); - - await expect(page.getByLabel('Close the panel')).toBeVisible(); - - const panel = page.getByLabel('Document panel'); - await expect(panel.getByText('Hello World')).toBeVisible(); - await expect(panel.getByText('Super World')).toBeVisible(); - - await page.getByLabel('Close the panel').click(); - - await expect(panel).toHaveAttribute('aria-hidden', 'true'); + await expect(level3).toBeVisible(); + await expect(level3).toHaveCSS('padding-left', /24px/); + await expect(level3).toHaveAttribute('aria-selected', 'false'); }); }); diff --git a/src/frontend/apps/impress/src/components/separators/HorizontalSeparator.tsx b/src/frontend/apps/impress/src/components/separators/HorizontalSeparator.tsx index b660e259..cd617985 100644 --- a/src/frontend/apps/impress/src/components/separators/HorizontalSeparator.tsx +++ b/src/frontend/apps/impress/src/components/separators/HorizontalSeparator.tsx @@ -9,10 +9,12 @@ export enum SeparatorVariant { type Props = { variant?: SeparatorVariant; + $withPadding?: boolean; }; export const HorizontalSeparator = ({ variant = SeparatorVariant.LIGHT, + $withPadding = true, }: Props) => { const { colorsTokens } = useCunninghamTheme(); @@ -20,7 +22,7 @@ export const HorizontalSeparator = ({ ` &, & > .bn-container, & .ProseMirror { height:100% }; - & .bn-editor { - padding-right: 30px; - ${readonly && `padding-left: 30px;`} - }; + & .bn-inline-content code { background-color: gainsboro; padding: 2px; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx index fa39f405..5893c4f5 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx @@ -2,9 +2,10 @@ import { Alert, Loader, VariantType } from '@openfun/cunningham-react'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; import * as Y from 'yjs'; -import { Box, Card, Text, TextErrors } from '@/components'; +import { Box, Text, TextErrors } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; import { DocHeader, DocVersionHeader } from '@/features/docs/doc-header/'; import { @@ -12,11 +13,11 @@ import { base64ToBlocknoteXmlFragment, useProviderStore, } from '@/features/docs/doc-management'; +import { TableContent } from '@/features/docs/doc-table-content/'; import { Versions, useDocVersion } from '@/features/docs/doc-versioning/'; import { useResponsiveStore } from '@/stores'; import { BlockNoteEditor, BlockNoteEditorVersion } from './BlockNoteEditor'; -import { IconOpenPanelEditor, PanelEditor } from './PanelEditor'; interface DocEditorProps { doc: Doc; @@ -25,9 +26,9 @@ interface DocEditorProps { export const DocEditor = ({ doc, versionId }: DocEditorProps) => { const { t } = useTranslation(); - const { isMobile } = useResponsiveStore(); + const { isDesktop } = useResponsiveStore(); - const isVersion = versionId && typeof versionId === 'string'; + const isVersion = !!versionId && typeof versionId === 'string'; const { colorsTokens } = useCunninghamTheme(); @@ -39,41 +40,49 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => { return ( <> - {isVersion ? ( - - ) : ( - - )} - - {!doc.abilities.partial_update && ( - - - {t(`Read only, you cannot edit this document.`)} - + {isDesktop && !isVersion && ( + + )} + + + {isVersion ? ( + + ) : ( + + )} + - - + + {t(`Read only, you cannot edit this document.`)} + + + )} + + - {isVersion ? ( - - ) : ( - - )} - {!isMobile && !isVersion && } - - {!isVersion && } + + {isVersion ? ( + + ) : ( + + )} + + ); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/PanelEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/PanelEditor.tsx deleted file mode 100644 index 729fe6c2..00000000 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/PanelEditor.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { Box, BoxButton, Card, IconBG, Text } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; -import { TableContent } from '@/features/docs/doc-table-content'; -import { useResponsiveStore } from '@/stores'; - -import { useHeadingStore, usePanelEditorStore } from '../stores'; - -export const PanelEditor = () => { - const { t } = useTranslation(); - const { colorsTokens } = useCunninghamTheme(); - const { isMobile } = useResponsiveStore(); - const { isPanelTableContentOpen, setIsPanelTableContentOpen, isPanelOpen } = - usePanelEditorStore(); - - return ( - - - {isMobile && } - - - setIsPanelTableContentOpen(true)} - $zIndex={1} - > - - {t('Table of content')} - - - - - - - ); -}; - -export const IconOpenPanelEditor = () => { - const { headings } = useHeadingStore(); - const { t } = useTranslation(); - const { setIsPanelOpen, isPanelOpen, setIsPanelTableContentOpen } = - usePanelEditorStore(); - const [hasBeenOpen, setHasBeenOpen] = useState(isPanelOpen); - const { isMobile } = useResponsiveStore(); - - const setClosePanel = () => { - setHasBeenOpen(true); - setIsPanelOpen(!isPanelOpen); - }; - - // Open the panel if there are more than 1 heading - useEffect(() => { - if (headings?.length && headings.length > 1 && !hasBeenOpen && !isMobile) { - setIsPanelTableContentOpen(true); - setIsPanelOpen(true); - setHasBeenOpen(true); - } - }, [ - headings, - setIsPanelTableContentOpen, - setIsPanelOpen, - hasBeenOpen, - isMobile, - ]); - - // If open from the doc header we set the state as well - useEffect(() => { - if (isPanelOpen && !hasBeenOpen) { - setHasBeenOpen(true); - } - }, [hasBeenOpen, isPanelOpen]); - - // Close the panel unmount - useEffect(() => { - return () => { - setIsPanelOpen(false); - }; - }, [setIsPanelOpen]); - - return ( - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx index 05012188..d90026fb 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx @@ -35,7 +35,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => { <> @@ -92,7 +92,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => { - + ); diff --git a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx index 5e92efa5..4146db10 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx @@ -5,10 +5,10 @@ import { BoxButton, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; import { useResponsiveStore } from '@/stores'; -const sizeMap: { [key: number]: string } = { - 1: '1.1rem', +const leftPaddingMap: { [key: number]: string } = { + 3: '1.5rem', 2: '0.9rem', - 3: '0.8rem', + 1: '0.3', }; export type HeadingsHighlight = { @@ -34,9 +34,12 @@ export const Heading = ({ const [isHover, setIsHover] = useState(isHighlight); const { colorsTokens } = useCunninghamTheme(); const { isMobile } = useResponsiveStore(); + const isActive = isHighlight || isHover; return ( setIsHover(true)} onMouseLeave={() => setIsHover(false)} @@ -47,23 +50,24 @@ export const Heading = ({ } editor.setTextCursorPosition(headingId, 'end'); + document.querySelector(`[data-id="${headingId}"]`)?.scrollIntoView({ behavior: 'smooth', + inline: 'start', block: 'start', }); }} + $radius="4px" + $background={isActive ? `${colorsTokens()['greyscale-100']}` : 'none'} $css="text-align: left;" > {text} diff --git a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx index 5fe946b5..f271aa6d 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx @@ -1,21 +1,22 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; -import { Box, BoxButton, Text } from '@/components'; +import { Box, Icon, Text } from '@/components'; import { useEditorStore, useHeadingStore } from '@/features/docs/doc-editor'; import { MAIN_LAYOUT_ID } from '@/layouts/conf'; -import { useResponsiveStore } from '@/stores'; import { Heading } from './Heading'; export const TableContent = () => { const { headings } = useHeadingStore(); const { editor } = useEditorStore(); - const { isMobile } = useResponsiveStore(); - const { t } = useTranslation(); + const [headingIdHighlight, setHeadingIdHighlight] = useState(); - // To highlight the first heading in the viewport + const { t } = useTranslation(); + const [isHover, setIsHover] = useState(false); + useEffect(() => { const handleScroll = () => { if (!headings) { @@ -62,69 +63,84 @@ export const TableContent = () => { } return ( - - - {headings?.map( - (heading) => - heading.contentText && ( - - ), - )} - - - { - // With mobile the focus open the keyboard and the scroll is not working - if (!isMobile) { - editor.focus(); - } + { + setIsHover(true); + setTimeout(() => { + const element = document.getElementById( + `heading-${headingIdHighlight}`, + ); - document.querySelector(`.bn-editor`)?.scrollIntoView({ + element?.scrollIntoView({ behavior: 'smooth', - block: 'start', + inline: 'center', + block: 'center', }); - }} - $align="start" - > - - {t('Back to top')} - - - { - // With mobile the focus open the keyboard and the scroll is not working - if (!isMobile) { - editor.focus(); - } + }, 250); // 300ms is the transition time of the box + }} + onMouseLeave={() => { + setIsHover(false); + }} + id="summaryContainer" + $effect="show" + $width="40px" + $height="40px" + $zIndex={1000} + $align="center" + $padding="xs" + $justify="center" + $css={css` + border: 1px solid #ccc; + overflow: hidden; + border-radius: var(--c--theme--spacings--3xs); + background: var(--c--theme--colors--greyscale-000); - document - .querySelector( - `.bn-editor > .bn-block-group > .bn-block-outer:last-child`, - ) - ?.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }); - }} - $align="start" - > - - {t('Go to bottom')} - - + &:hover { + overflow-y: auto; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + gap: var(--c--theme--spacings--2xs); + width: 200px; + height: auto; + max-height: calc(100vh - 60px - 15vh); + } + `} + > + {!isHover && ( + + + + )} + {isHover && ( + + + + {t('Summary')} + + + + {headings?.map( + (heading) => + heading.contentText && ( + + ), + )} + + )} ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/index.ts b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/index.ts index 323f1534..d1a13b2a 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/index.ts @@ -1 +1,2 @@ export * from './TableContent'; +export * from './Heading'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalSelectVersion.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalSelectVersion.tsx index cc0adb09..7cc79f05 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalSelectVersion.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalSelectVersion.tsx @@ -64,7 +64,7 @@ export const ModalSelectVersion = ({ flex: 1; `} > - + {selectedVersionId && ( )}