diff --git a/CHANGELOG.md b/CHANGELOG.md index 53dd6ff375..8695024f99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to ### Added +- ✨(frontend) make the full last-update date available #1215 - ♿️(frontend) restore skip to content link after header redesign #2510 - 🌐(i18n) rename cn_CN to zh_CN, add eo_PL and zh_TW locales #2486 - ✨(backend) conditional email notification in server to server api #2554 diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderInfo.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderInfo.spec.tsx new file mode 100644 index 0000000000..2f36fc5474 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderInfo.spec.tsx @@ -0,0 +1,111 @@ +import { act, fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import i18next from 'i18next'; +import { DateTime } from 'luxon'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { Doc, LinkReach, Role } from '@/docs/doc-management'; +import { AppWrapper } from '@/tests/utils'; + +import { DocHeaderInfo } from '../components/DocHeaderInfo'; + +vi.mock('@/core', async () => { + const actual = await vi.importActual('@/core'); + + return { + ...actual, + useConfig: () => ({ data: {} }), + }; +}); + +const updatedAt = '2026-08-14T14:23:00'; +const doc = { + id: 'doc-1', + abilities: { + partial_update: true, + }, + ancestors_link_reach: LinkReach.RESTRICTED, + computed_link_reach: LinkReach.RESTRICTED, + deleted_at: null, + link_reach: LinkReach.RESTRICTED, + nb_accesses_ancestors: 0, + nb_accesses_direct: 1, + updated_at: updatedAt, + user_role: Role.OWNER, +} as Doc; + +describe('', () => { + beforeEach(() => { + const now = DateTime.now().set({ + year: 2026, + month: 8, + day: 14, + hour: 14, + minute: 28, + second: 0, + millisecond: 0, + }); + vi.spyOn(DateTime, 'now').mockReturnValue(now); + }); + + afterEach(async () => { + await act(async () => { + await i18next.changeLanguage('en'); + }); + vi.restoreAllMocks(); + }); + + it('keeps the relative date and exposes the full date on focus', async () => { + const user = userEvent.setup(); + + render(, { wrapper: AppWrapper }); + + const relativeDate = screen.getByText('5 minutes ago'); + expect(relativeDate).toHaveTextContent('5 minutes ago'); + expect(relativeDate.tagName).toBe('TIME'); + expect(relativeDate).toHaveAttribute('datetime', updatedAt); + expect(relativeDate).not.toHaveAttribute('role', 'button'); + expect(relativeDate).toHaveAccessibleName( + '5 minutes ago. 08/14/2026, 02:23 PM', + ); + + await user.tab(); + + expect(relativeDate).toHaveFocus(); + expect(await screen.findByRole('tooltip')).toHaveTextContent( + '08/14/2026, 02:23 PM', + ); + }); + + it('uses the current locale for the relative and full dates', async () => { + const user = userEvent.setup(); + + await act(async () => { + await i18next.changeLanguage('fr'); + }); + + render(, { wrapper: AppWrapper }); + + const relativeDate = screen.getByText('il y a 5 minutes'); + await user.tab(); + + expect(relativeDate).toHaveFocus(); + expect(await screen.findByRole('tooltip')).toHaveTextContent( + '14/08/2026 14:23', + ); + }); + + it('exposes the full date on hover', async () => { + const user = userEvent.setup(); + + render(, { wrapper: AppWrapper }); + + const relativeDate = screen.getByText('5 minutes ago'); + fireEvent.pointerMove(relativeDate, { pointerType: 'mouse' }); + await user.hover(relativeDate); + + expect(await screen.findByRole('tooltip')).toHaveTextContent( + '08/14/2026, 02:23 PM', + ); + }); +}); diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx index c5f7f8d739..390beeb984 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx @@ -1,4 +1,6 @@ +import { Tooltip } from '@gouvfr-lasuite/cunningham-react'; import { t } from 'i18next'; +import { ComponentPropsWithoutRef, forwardRef } from 'react'; import PublicSVG from '@/assets/icons/ui-kit/public.svg'; import ProtedtedSVG from '@/assets/icons/ui-kit/vpn_lock.svg'; @@ -21,10 +23,11 @@ interface DocHeaderInfoProps { export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => { const { transRole } = useTrans(); const { isEditable } = useIsCollaborativeEditable(doc); - const { relativeDate, calculateDaysLeft } = useDate(); + const { relativeDate, formatDate, calculateDaysLeft } = useDate(); const { data: config } = useConfig(); const relativeOnly = relativeDate(doc.updated_at); + const fullDate = formatDate(doc.updated_at); const trashbinCutoff = config?.TRASHBIN_CUTOFF_DAYS; @@ -62,13 +65,38 @@ export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => { {dateLabel}   - - {dateValue} + + {trashbinCutoff && doc.deleted_at ? ( + dateValue + ) : ( + + + {relativeOnly} + + + )} ); }; +const FocusableTime = forwardRef< + HTMLTimeElement, + ComponentPropsWithoutRef<'time'> +>((props, ref) =>