Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 87 additions & 2 deletions packages/react-tei/src/SidePanel/unitex/UnitexAnnotation.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import { describe, expect, it, vi } from "vitest";
import { render } from "vitest-browser-react";
import { I18nProvider } from "../../i18n/I18nProvider";
import {
DIRECTION_NEXT,
DIRECTION_PREVIOUS,
type DocumentNavigationContextValue,
} from "../../navigation/DocumentNavigationContext";
import { TestDocumentNavigationContextProvider } from "../../navigation/TestDocumentNavigationContextProvider";
import { UnitexAnnotation } from "./UnitexAnnotation";

function TestWrapper({ children }: { children: React.ReactNode }) {
return <I18nProvider>{children}</I18nProvider>;
function TestWrapper({
navigateToBodyTargetSelector,
children,
}: {
navigateToBodyTargetSelector?: DocumentNavigationContextValue["navigateToBodyTargetSelector"];
children: React.ReactNode;
}) {
return (
<TestDocumentNavigationContextProvider
value={{
navigateToBodyTargetSelector,
}}
>
<I18nProvider>{children}</I18nProvider>
</TestDocumentNavigationContextProvider>
);
}

describe("UnitexAnnotation", () => {
Expand Down Expand Up @@ -46,4 +66,69 @@ describe("UnitexAnnotation", () => {

expect(onToggle).toHaveBeenCalledTimes(1);
});

it("should disable navigation buttons when annotation is not displayed", async () => {
const screen = await render(
<UnitexAnnotation
annotation={{ term: "example", frequency: 5, displayed: false }}
color="blue"
onToggle={() => {}}
/>,
{
wrapper: TestWrapper,
},
);

const previousButton = screen.getByRole("button", {
name: "Aller au précédent",
});
const nextButton = screen.getByRole("button", {
name: "Aller au suivant",
});

await expect.element(previousButton).toBeDisabled();
await expect.element(nextButton).toBeDisabled();
});

it("should call navigateToBodyTargetSelector when navigation buttons are clicked", async () => {
const navigateToBodyTargetSelector = vi.fn();
const screen = await render(
<UnitexAnnotation
annotation={{ term: "example", frequency: 5, displayed: true }}
color="blue"
onToggle={() => {}}
/>,
{
wrapper: ({ children }) => (
<TestWrapper
navigateToBodyTargetSelector={navigateToBodyTargetSelector}
>
{children}
</TestWrapper>
),
},
);

const previousButton = screen.getByRole("button", {
name: "Aller au précédent",
});
const nextButton = screen.getByRole("button", {
name: "Aller au suivant",
});

await expect.element(previousButton).toBeEnabled();
await expect.element(nextButton).toBeEnabled();

await previousButton.click();
expect(navigateToBodyTargetSelector).toHaveBeenCalledWith(
'[data-term="example"]',
DIRECTION_PREVIOUS,
);

await nextButton.click();
expect(navigateToBodyTargetSelector).toHaveBeenCalledWith(
'[data-term="example"]',
DIRECTION_NEXT,
);
});
});
31 changes: 29 additions & 2 deletions packages/react-tei/src/SidePanel/unitex/UnitexAnnotation.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import ArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import ArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import Box from "@mui/material/Box";
import Checkbox from "@mui/material/Checkbox";
import Chip from "@mui/material/Chip";
import { grey } from "@mui/material/colors";
import IconButton from "@mui/material/IconButton";
import Stack from "@mui/material/Stack";
import Tooltip from "@mui/material/Tooltip";
import { useTranslation } from "react-i18next";
import type { TermStatistic } from "../../unitex/parseUnitexEnrichment";
import { useUnitexAnnotationNavigation } from "./useUnitexAnnotationNavigation";

export function UnitexAnnotation({
annotation,
Expand All @@ -18,6 +23,10 @@ export function UnitexAnnotation({
term: annotation.term,
});

const { goToNext, goToPrevious } = useUnitexAnnotationNavigation(
annotation.term,
);

return (
<>
<Tooltip title={checkBoxLabel} placement="left">
Expand Down Expand Up @@ -57,14 +66,32 @@ export function UnitexAnnotation({
slotProps={{
root: {
sx: {
backgroundColor: annotation.displayed ? color : grey[600],
color: "white",
backgroundColor: annotation.displayed ? color : grey[100],
color: "black",
},
},
}}
/>
</Tooltip>
</Box>
<Stack gap={0.5} direction="row">
<IconButton
size="small"
disabled={!annotation.displayed}
onClick={goToPrevious}
aria-label={t("unitex.previous")}
>
<ArrowUpIcon />
</IconButton>
<IconButton
size="small"
disabled={!annotation.displayed}
onClick={goToNext}
aria-label={t("unitex.next")}
>
<ArrowDownIcon />
</IconButton>
</Stack>
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { render } from "vitest-browser-react";

import { DocumentContextProvider } from "../../DocumentContextProvider";
import { I18nProvider } from "../../i18n/I18nProvider";
import { TestDocumentNavigationContextProvider } from "../../navigation/TestDocumentNavigationContextProvider";
import type { TermStatistic } from "../../unitex/parseUnitexEnrichment";
import { UnitexAnnotationBlock } from "./UnitexAnnotationBlock";
import type { UnitexAnnotationBlockType } from "./unitexAnnotationBlocks";
Expand All @@ -22,7 +23,9 @@ function TestWrapper({ children }: { children: React.ReactNode }) {
jsonDocument={[]}
jsonUnitexEnrichment={enrichments}
>
{children}
<TestDocumentNavigationContextProvider>
{children}
</TestDocumentNavigationContextProvider>
</DocumentContextProvider>
</I18nProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function UnitexAnnotationBlock({ block }: UnitexAnnotationBlockProps) {
<Box
sx={{
display: "grid",
gridTemplateColumns: "max-content 1fr",
gridTemplateColumns: "max-content 1fr max-content",
gridTemplateRows: "auto",
columnGap: 0.5,
rowGap: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { render } from "vitest-browser-react";

import { DocumentContextProvider } from "../../DocumentContextProvider";
import { I18nProvider } from "../../i18n/I18nProvider";
import { TestDocumentNavigationContextProvider } from "../../navigation/TestDocumentNavigationContextProvider";
import type { TermStatistic } from "../../unitex/parseUnitexEnrichment";
import { UnitexSection } from "./UnitexSection";
import type { UnitexAnnotationBlockType } from "./unitexAnnotationBlocks";
Expand All @@ -22,7 +23,9 @@ function TestWrapper({ children }: { children: React.ReactNode }) {
jsonDocument={[]}
jsonUnitexEnrichment={enrichments}
>
{children}
<TestDocumentNavigationContextProvider>
{children}
</TestDocumentNavigationContextProvider>
</DocumentContextProvider>
</I18nProvider>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { act } from "react";
import { describe, expect, it, vi } from "vitest";
import { renderHook } from "vitest-browser-react";
import {
DIRECTION_NEXT,
DIRECTION_PREVIOUS,
type Direction,
} from "../../navigation/DocumentNavigationContext";
import { TestDocumentNavigationContextProvider } from "../../navigation/TestDocumentNavigationContextProvider";
import { useUnitexAnnotationNavigation } from "./useUnitexAnnotationNavigation";

describe("useUnitexAnnotationNavigation", () => {
it.each<{
direction: Direction;
fnName: keyof ReturnType<typeof useUnitexAnnotationNavigation>;
}>([
{ direction: DIRECTION_PREVIOUS, fnName: "goToPrevious" },
{ direction: DIRECTION_NEXT, fnName: "goToNext" },
])('should support navigating to the "%s" occurrence of a term', async ({
direction,
fnName,
}) => {
const navigateToBodyTargetSelector = vi.fn();
const { result } = await renderHook(
() => useUnitexAnnotationNavigation("example"),
{
wrapper: ({ children }) => (
<TestDocumentNavigationContextProvider
value={{
navigateToBodyTargetSelector,
}}
>
{children}
</TestDocumentNavigationContextProvider>
),
},
);

const fn = result.current[fnName];

act(() => {
fn();
});

expect(navigateToBodyTargetSelector).toHaveBeenCalledWith(
'[data-term="example"]',
direction,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useCallback, useMemo } from "react";
import { kebabCasify } from "../../helper/kebabCasify";
import {
DIRECTION_NEXT,
DIRECTION_PREVIOUS,
} from "../../navigation/DocumentNavigationContext";
import { useDocumentNavigation } from "../../navigation/useNavigateToSection";

export function useUnitexAnnotationNavigation(term: string) {
const { navigateToBodyTargetSelector } = useDocumentNavigation();

const selector = useMemo(() => `[data-term="${kebabCasify(term)}"]`, [term]);

const goToPrevious = useCallback(() => {
navigateToBodyTargetSelector(selector, DIRECTION_PREVIOUS);
}, [navigateToBodyTargetSelector, selector]);

const goToNext = useCallback(() => {
navigateToBodyTargetSelector(selector, DIRECTION_NEXT);
}, [navigateToBodyTargetSelector, selector]);

return useMemo(() => {
return {
goToPrevious,
goToNext,
};
}, [goToPrevious, goToNext]);
}
2 changes: 2 additions & 0 deletions packages/react-tei/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export const en: Translation = {
toggleBlock_hide: "Deactivate underlining of words in the text",
toggleTerm_show: 'Activate underlining for the term "{{term}}"',
toggleTerm_hide: 'Deactivate underlining for the term "{{term}}"',
previous: "Go to previous",
next: "Go to next",

date_one: "Date ({{count}})",
date_other: "Dates ({{count}})",
Expand Down
2 changes: 2 additions & 0 deletions packages/react-tei/src/i18n/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export const fr = {
toggleBlock_hide: "Désactiver le soulignement des mots dans le texte",
toggleTerm_show: 'Activer le soulignement pour le terme "{{term}}"',
toggleTerm_hide: 'Désactiver le soulignement pour le terme "{{term}}"',
previous: "Aller au précédent",
next: "Aller au suivant",

date_one: "Date ({{count}})",
date_other: "Dates ({{count}})",
Expand Down
25 changes: 20 additions & 5 deletions packages/react-tei/src/navigation/DocumentNavigationContext.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { createContext, useCallback, useEffect, useMemo, useRef } from "react";
import { useDocumentContext } from "../DocumentContextProvider";

export const DIRECTION_NEXT = "next";
export const DIRECTION_PREVIOUS = "previous";
export type Direction = typeof DIRECTION_NEXT | typeof DIRECTION_PREVIOUS;

export type DocumentNavigationContextValue = {
navigateToBodyTargetSelector(querySelector: string): void;
navigateToBodyTargetSelector(
querySelector: string,
direction?: Direction,
): void;
navigateToHeading(headingId: string): void;
navigateToFootnote(footnoteId: string): void;
navigateToFootnoteRef(id: string): void;
Expand Down Expand Up @@ -91,7 +98,11 @@ export function DocumentNavigationContextProvider({
const currentSelectorRef = useRef<CurrentSelectorRef | null>(null);

const navigateToTargetLoop = useCallback(
(wrapperElement: HTMLElement, querySelector: string) => {
(
wrapperElement: HTMLElement,
querySelector: string,
direction: Direction = DIRECTION_NEXT,
) => {
const targetElements =
wrapperElement.querySelectorAll<HTMLElement>(querySelector);

Expand All @@ -110,8 +121,12 @@ export function DocumentNavigationContextProvider({
index: 0,
};
} else {
const directionValue = direction === "next" ? 1 : -1;
currentSelectorRef.current.index =
(currentSelectorRef.current.index + 1) % targetElements.length;
(currentSelectorRef.current.index +
directionValue +
targetElements.length) %
targetElements.length;
}

const index = currentSelectorRef.current.index;
Expand All @@ -133,15 +148,15 @@ export function DocumentNavigationContextProvider({
);

const navigateToBodyTargetSelector = useCallback(
Comment thread
jonathanarnault marked this conversation as resolved.
(querySelector: string) => {
(querySelector: string, direction: Direction = DIRECTION_NEXT) => {
const documentElement = documentRef.current;

if (!documentElement) {
console.error("Document element not found");
return;
}

navigateToTargetLoop(documentElement, querySelector);
navigateToTargetLoop(documentElement, querySelector, direction);
},
[documentRef, navigateToTargetLoop],
);
Expand Down
Loading