Skip to content

Commit fce57e1

Browse files
committed
✨(frontend) add floating bar with collapse button
Add sticky floating bar at top of document with leftpanelcollapse btn
1 parent 17cb213 commit fce57e1

File tree

19 files changed

+381
-66
lines changed

19 files changed

+381
-66
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to
88

99
### Added
1010

11+
- ✨(frontend) add floating bar with leftpanel collapse button #1876
1112
- ✨(frontend) Can print a doc #1832
1213
- ✨(backend) manage reconciliation requests for user accounts #1878
1314

src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ test.describe('Doc Comments', () => {
4141
// We add a comment with the first user
4242
const editor = await writeInEditor({ page, text: 'Hello World' });
4343
await editor.getByText('Hello').selectText();
44-
await page.getByRole('button', { name: 'Comment' }).click();
44+
await page.getByRole('button', { name: 'Comment', exact: true }).click();
4545

4646
const thread = page.locator('.bn-thread');
4747
await thread.getByRole('paragraph').first().fill('This is a comment');
@@ -124,7 +124,7 @@ test.describe('Doc Comments', () => {
124124
// Checks add react reaction
125125
const editor = await writeInEditor({ page, text: 'Hello' });
126126
await editor.getByText('Hello').selectText();
127-
await page.getByRole('button', { name: 'Comment' }).click();
127+
await page.getByRole('button', { name: 'Comment', exact: true }).click();
128128

129129
const thread = page.locator('.bn-thread');
130130
await thread.getByRole('paragraph').first().fill('This is a comment');
@@ -191,7 +191,7 @@ test.describe('Doc Comments', () => {
191191

192192
/* Delete the last comment remove the thread */
193193
await editor.getByText('Hello').selectText();
194-
await page.getByRole('button', { name: 'Comment' }).click();
194+
await page.getByRole('button', { name: 'Comment', exact: true }).click();
195195

196196
await thread.getByRole('paragraph').first().fill('This is a new comment');
197197
await thread.locator('[data-test="save"]').click();
@@ -249,7 +249,9 @@ test.describe('Doc Comments', () => {
249249
editor.getByText('Hello, I can edit the document'),
250250
).toBeVisible();
251251
await otherEditor.getByText('Hello').selectText();
252-
await otherPage.getByRole('button', { name: 'Comment' }).click();
252+
await otherPage
253+
.getByRole('button', { name: 'Comment', exact: true })
254+
.click();
253255
const otherThread = otherPage.locator('.bn-thread');
254256
await otherThread
255257
.getByRole('paragraph')
@@ -280,7 +282,7 @@ test.describe('Doc Comments', () => {
280282
await expect(otherThread).toBeHidden();
281283
await otherEditor.getByText('Hello').selectText();
282284
await expect(
283-
otherPage.getByRole('button', { name: 'Comment' }),
285+
otherPage.getByRole('button', { name: 'Comment', exact: true }),
284286
).toBeHidden();
285287

286288
await otherPage.reload();
@@ -334,7 +336,7 @@ test.describe('Doc Comments', () => {
334336
// We add a comment in the first document
335337
const editor1 = await writeInEditor({ page, text: 'Document One' });
336338
await editor1.getByText('Document One').selectText();
337-
await page.getByRole('button', { name: 'Comment' }).click();
339+
await page.getByRole('button', { name: 'Comment', exact: true }).click();
338340

339341
const thread1 = page.locator('.bn-thread');
340342
await thread1.getByRole('paragraph').first().fill('Comment in Doc One');
@@ -388,7 +390,7 @@ test.describe('Doc Comments mobile', () => {
388390
// Checks add react reaction
389391
const editor = await writeInEditor({ page, text: 'Hello' });
390392
await editor.getByText('Hello').selectText();
391-
await page.getByRole('button', { name: 'Comment' }).click();
393+
await page.getByRole('button', { name: 'Comment', exact: true }).click();
392394

393395
const thread = page.locator('.bn-thread');
394396
await thread.getByRole('paragraph').first().fill('This is a comment');

src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,66 @@ test.beforeEach(async ({ page }) => {
2424
});
2525

2626
test.describe('Doc Editor', () => {
27+
test('shows floating bar and collapse button on desktop', async ({
28+
page,
29+
browserName,
30+
}) => {
31+
await createDoc(page, 'doc-floating-bar', browserName, 1);
32+
33+
await expect(page.getByTestId('floating-bar')).toBeVisible();
34+
35+
const collapseButton = page.getByTestId('floating-bar-toggle-left-panel');
36+
await expect(collapseButton).toBeVisible();
37+
});
38+
39+
test('toggles panel collapse from floating bar button', async ({
40+
page,
41+
browserName,
42+
}) => {
43+
const [docTitle] = await createDoc(
44+
page,
45+
'doc-floating-bar',
46+
browserName,
47+
1,
48+
);
49+
50+
const collapseButton = page.getByTestId('floating-bar-toggle-left-panel');
51+
await expect(collapseButton).toBeVisible();
52+
const initialExpanded = await collapseButton.getAttribute('aria-expanded');
53+
expect(
54+
initialExpanded === 'true' || initialExpanded === 'false',
55+
).toBeTruthy();
56+
const isInitiallyExpanded = initialExpanded === 'true';
57+
58+
if (isInitiallyExpanded) {
59+
await expect(collapseButton).not.toContainText(docTitle);
60+
} else {
61+
await expect(collapseButton).toContainText(docTitle);
62+
}
63+
64+
await collapseButton.click();
65+
await expect(collapseButton).toHaveAttribute(
66+
'aria-expanded',
67+
isInitiallyExpanded ? 'false' : 'true',
68+
);
69+
if (isInitiallyExpanded) {
70+
await expect(collapseButton).toContainText(docTitle);
71+
} else {
72+
await expect(collapseButton).not.toContainText(docTitle);
73+
}
74+
75+
await collapseButton.click();
76+
await expect(collapseButton).toHaveAttribute(
77+
'aria-expanded',
78+
isInitiallyExpanded ? 'true' : 'false',
79+
);
80+
if (isInitiallyExpanded) {
81+
await expect(collapseButton).not.toContainText(docTitle);
82+
} else {
83+
await expect(collapseButton).toContainText(docTitle);
84+
}
85+
});
86+
2787
test('it checks toolbar buttons are displayed', async ({
2888
page,
2989
browserName,
@@ -410,7 +470,7 @@ test.describe('Doc Editor', () => {
410470
const editor = page.locator('.ProseMirror');
411471
await editor.getByText('Hello').selectText();
412472

413-
await page.getByRole('button', { name: 'AI' }).click();
473+
await page.getByRole('button', { name: 'AI', exact: true }).click();
414474

415475
await expect(
416476
page.getByRole('menuitem', { name: 'Use as prompt' }),
@@ -494,11 +554,13 @@ test.describe('Doc Editor', () => {
494554
await editor.getByText('Hello').selectText();
495555

496556
if (!ai_transform && !ai_translate) {
497-
await expect(page.getByRole('button', { name: 'AI' })).toBeHidden();
557+
await expect(
558+
page.getByRole('button', { name: 'AI', exact: true }),
559+
).toBeHidden();
498560
return;
499561
}
500562

501-
await page.getByRole('button', { name: 'AI' }).click();
563+
await page.getByRole('button', { name: 'AI', exact: true }).click();
502564

503565
if (ai_transform) {
504566
await expect(

src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '@/docs/doc-management';
1313
import { TableContent } from '@/docs/doc-table-content/';
1414
import { useAuth } from '@/features/auth/';
15+
import { FloatingBar } from '@/features/floating-bar';
1516
import { useSkeletonStore } from '@/features/skeletons';
1617
import { useAnalytics } from '@/libs';
1718
import { useResponsiveStore } from '@/stores';
@@ -35,6 +36,7 @@ export const DocEditorContainer = ({
3536

3637
return (
3738
<>
39+
{isDesktop && <FloatingBar />}
3840
<Box
3941
$maxWidth="868px"
4042
$width="100%"

src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
3535
<>
3636
<Box
3737
$width="100%"
38-
$padding={{ top: isDesktop ? '50px' : 'md' }}
38+
$padding={{ top: isDesktop ? '0' : 'md' }}
3939
$gap={spacingsTokens['base']}
4040
aria-label={t('It is the card information about the document.')}
4141
className="--docs--doc-header"

src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export const TableContent = () => {
6161
$width={!isOpen ? '40px' : '200px'}
6262
$height={!isOpen ? '40px' : 'auto'}
6363
$maxHeight="calc(50vh - 60px)"
64-
$zIndex={1000}
64+
$zIndex={2000}
6565
$align="center"
6666
$padding={isOpen ? 'xs' : '0'}
6767
$justify="center"
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Button } from '@gouvfr-lasuite/cunningham-react';
2+
import { useTranslation } from 'react-i18next';
3+
4+
import { Text } from '@/components';
5+
import { useCunninghamTheme } from '@/cunningham';
6+
import { getEmojiAndTitle, useDocStore, useTrans } from '@/docs/doc-management';
7+
import { useLeftPanelStore } from '@/features/left-panel';
8+
9+
import LeftPanelIcon from '../assets/left-panel.svg';
10+
11+
export const CollapsePanel = () => {
12+
const { t } = useTranslation();
13+
const { colorsTokens } = useCunninghamTheme();
14+
const { isPanelOpen, togglePanel } = useLeftPanelStore();
15+
const { currentDoc } = useDocStore();
16+
const { untitledDocument } = useTrans();
17+
18+
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(
19+
currentDoc?.title ?? '',
20+
);
21+
const docTitle = titleWithoutEmoji || untitledDocument;
22+
const buttonTitle = emoji ? `${emoji} ${docTitle}` : docTitle;
23+
const ariaLabel = t(
24+
isPanelOpen
25+
? 'Hide the side panel for {{title}}'
26+
: 'Show the side panel for {{title}}',
27+
{ title: docTitle },
28+
);
29+
30+
return (
31+
<Button
32+
size="medium"
33+
onClick={() => togglePanel()}
34+
aria-label={ariaLabel}
35+
aria-expanded={isPanelOpen}
36+
color="neutral"
37+
variant="tertiary"
38+
icon={<LeftPanelIcon width={24} height={24} aria-hidden="true" />}
39+
data-testid="floating-bar-toggle-left-panel"
40+
>
41+
{!isPanelOpen ? (
42+
<Text $size="sm" $weight={700} $color={colorsTokens['gray-1000']}>
43+
{buttonTitle}
44+
</Text>
45+
) : undefined}
46+
</Button>
47+
);
48+
};
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { RuleSet, css } from 'styled-components';
2+
3+
import { Box } from '@/components';
4+
import { useCunninghamTheme } from '@/cunningham';
5+
import { useResponsiveStore } from '@/stores';
6+
7+
import { FloatingBarLeft } from './FloatingBarLeft';
8+
9+
export const FLOATING_BAR_HEIGHT = '64px';
10+
export const FLOATING_BAR_Z_INDEX = 1000;
11+
const FLOATING_BAR_BLUR_RADIUS = '12px';
12+
13+
/**
14+
* Sticky bar trick (desktop):
15+
* - MainContent has padding `base`; we extend the bar width and apply
16+
* matching negative margins (mainContentPadding) so it aligns with the
17+
* scroll area edges.
18+
* - `top: calc(-mainContentPadding)` keeps sticky positioning visually
19+
* aligned with the content start.
20+
*
21+
* Mobile: returns null to avoid header overlap.
22+
*/
23+
const getFloatingBarStyles = (
24+
mainContentPadding: string,
25+
barSpacing: string,
26+
blurRadius: string,
27+
whiteColor: string,
28+
): RuleSet => css`
29+
position: sticky;
30+
top: calc(-${mainContentPadding});
31+
left: 0;
32+
right: 0;
33+
align-self: stretch;
34+
width: calc(100% + ${mainContentPadding} + ${mainContentPadding});
35+
min-height: ${FLOATING_BAR_HEIGHT};
36+
padding: ${barSpacing};
37+
margin-left: calc(-${mainContentPadding});
38+
margin-right: calc(-${mainContentPadding});
39+
margin-top: calc(-${mainContentPadding});
40+
z-index: ${FLOATING_BAR_Z_INDEX};
41+
display: flex;
42+
align-items: flex-start;
43+
justify-content: flex-start;
44+
background: linear-gradient(
45+
180deg,
46+
${whiteColor} 0%,
47+
color-mix(in srgb, ${whiteColor} 70%, transparent) 100%
48+
);
49+
50+
&::before {
51+
content: '';
52+
position: absolute;
53+
top: 0;
54+
left: 0;
55+
right: 0;
56+
bottom: 0;
57+
z-index: -1;
58+
backdrop-filter: blur(${blurRadius});
59+
-webkit-backdrop-filter: blur(${blurRadius});
60+
mask: linear-gradient(180deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 100%);
61+
-webkit-mask: linear-gradient(
62+
180deg,
63+
rgba(0, 0, 0, 1) 0%,
64+
rgba(0, 0, 0, 0) 100%
65+
);
66+
pointer-events: none;
67+
}
68+
69+
> * {
70+
position: relative;
71+
z-index: 1;
72+
}
73+
`;
74+
75+
export const FloatingBar = () => {
76+
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
77+
const { isDesktop } = useResponsiveStore();
78+
const mainContentPadding =
79+
spacingsTokens['base'] || 'var(--c--globals--spacings--base)';
80+
const barSpacing = spacingsTokens['sm'] || 'var(--c--globals--spacings--sm)';
81+
const whiteColor =
82+
colorsTokens['gray-000'] ||
83+
'var(--c--contextuals--background--surface--primary)';
84+
85+
if (!isDesktop) {
86+
return null;
87+
}
88+
89+
return (
90+
<Box
91+
data-testid="floating-bar"
92+
$css={getFloatingBarStyles(
93+
mainContentPadding,
94+
barSpacing,
95+
FLOATING_BAR_BLUR_RADIUS,
96+
whiteColor,
97+
)}
98+
>
99+
<FloatingBarLeft />
100+
</Box>
101+
);
102+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { CollapsePanel } from './CollapsePanel';
2+
3+
export const FloatingBarLeft = () => {
4+
return <CollapsePanel />;
5+
};

0 commit comments

Comments
 (0)