Skip to content

Commit 06c5615

Browse files
authored
Merge pull request #139 from istex/feat/70-enrichment-scroll
Feat(unitex): Add go to previous / go to next support
2 parents c9d7d12 + 7ebea6f commit 06c5615

12 files changed

Lines changed: 277 additions & 17 deletions

packages/react-tei/src/SidePanel/unitex/UnitexAnnotation.spec.tsx

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
11
import { describe, expect, it, vi } from "vitest";
22
import { render } from "vitest-browser-react";
33
import { I18nProvider } from "../../i18n/I18nProvider";
4+
import {
5+
DIRECTION_NEXT,
6+
DIRECTION_PREVIOUS,
7+
type DocumentNavigationContextValue,
8+
} from "../../navigation/DocumentNavigationContext";
9+
import { TestDocumentNavigationContextProvider } from "../../navigation/TestDocumentNavigationContextProvider";
410
import { UnitexAnnotation } from "./UnitexAnnotation";
511

6-
function TestWrapper({ children }: { children: React.ReactNode }) {
7-
return <I18nProvider>{children}</I18nProvider>;
12+
function TestWrapper({
13+
navigateToBodyTargetSelector,
14+
children,
15+
}: {
16+
navigateToBodyTargetSelector?: DocumentNavigationContextValue["navigateToBodyTargetSelector"];
17+
children: React.ReactNode;
18+
}) {
19+
return (
20+
<TestDocumentNavigationContextProvider
21+
value={{
22+
navigateToBodyTargetSelector,
23+
}}
24+
>
25+
<I18nProvider>{children}</I18nProvider>
26+
</TestDocumentNavigationContextProvider>
27+
);
828
}
929

1030
describe("UnitexAnnotation", () => {
@@ -46,4 +66,69 @@ describe("UnitexAnnotation", () => {
4666

4767
expect(onToggle).toHaveBeenCalledTimes(1);
4868
});
69+
70+
it("should disable navigation buttons when annotation is not displayed", async () => {
71+
const screen = await render(
72+
<UnitexAnnotation
73+
annotation={{ term: "example", frequency: 5, displayed: false }}
74+
color="blue"
75+
onToggle={() => {}}
76+
/>,
77+
{
78+
wrapper: TestWrapper,
79+
},
80+
);
81+
82+
const previousButton = screen.getByRole("button", {
83+
name: "Aller au précédent",
84+
});
85+
const nextButton = screen.getByRole("button", {
86+
name: "Aller au suivant",
87+
});
88+
89+
await expect.element(previousButton).toBeDisabled();
90+
await expect.element(nextButton).toBeDisabled();
91+
});
92+
93+
it("should call navigateToBodyTargetSelector when navigation buttons are clicked", async () => {
94+
const navigateToBodyTargetSelector = vi.fn();
95+
const screen = await render(
96+
<UnitexAnnotation
97+
annotation={{ term: "example", frequency: 5, displayed: true }}
98+
color="blue"
99+
onToggle={() => {}}
100+
/>,
101+
{
102+
wrapper: ({ children }) => (
103+
<TestWrapper
104+
navigateToBodyTargetSelector={navigateToBodyTargetSelector}
105+
>
106+
{children}
107+
</TestWrapper>
108+
),
109+
},
110+
);
111+
112+
const previousButton = screen.getByRole("button", {
113+
name: "Aller au précédent",
114+
});
115+
const nextButton = screen.getByRole("button", {
116+
name: "Aller au suivant",
117+
});
118+
119+
await expect.element(previousButton).toBeEnabled();
120+
await expect.element(nextButton).toBeEnabled();
121+
122+
await previousButton.click();
123+
expect(navigateToBodyTargetSelector).toHaveBeenCalledWith(
124+
'[data-term="example"]',
125+
DIRECTION_PREVIOUS,
126+
);
127+
128+
await nextButton.click();
129+
expect(navigateToBodyTargetSelector).toHaveBeenCalledWith(
130+
'[data-term="example"]',
131+
DIRECTION_NEXT,
132+
);
133+
});
49134
});

packages/react-tei/src/SidePanel/unitex/UnitexAnnotation.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import ArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
2+
import ArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
13
import Box from "@mui/material/Box";
24
import Checkbox from "@mui/material/Checkbox";
35
import Chip from "@mui/material/Chip";
46
import { grey } from "@mui/material/colors";
7+
import IconButton from "@mui/material/IconButton";
8+
import Stack from "@mui/material/Stack";
59
import Tooltip from "@mui/material/Tooltip";
610
import { useTranslation } from "react-i18next";
711
import type { TermStatistic } from "../../unitex/parseUnitexEnrichment";
12+
import { useUnitexAnnotationNavigation } from "./useUnitexAnnotationNavigation";
813

914
export function UnitexAnnotation({
1015
annotation,
@@ -18,6 +23,10 @@ export function UnitexAnnotation({
1823
term: annotation.term,
1924
});
2025

26+
const { goToNext, goToPrevious } = useUnitexAnnotationNavigation(
27+
annotation.term,
28+
);
29+
2130
return (
2231
<>
2332
<Tooltip title={checkBoxLabel} placement="left">
@@ -57,14 +66,32 @@ export function UnitexAnnotation({
5766
slotProps={{
5867
root: {
5968
sx: {
60-
backgroundColor: annotation.displayed ? color : grey[600],
61-
color: "white",
69+
backgroundColor: annotation.displayed ? color : grey[100],
70+
color: "black",
6271
},
6372
},
6473
}}
6574
/>
6675
</Tooltip>
6776
</Box>
77+
<Stack gap={0.5} direction="row">
78+
<IconButton
79+
size="small"
80+
disabled={!annotation.displayed}
81+
onClick={goToPrevious}
82+
aria-label={t("unitex.previous")}
83+
>
84+
<ArrowUpIcon />
85+
</IconButton>
86+
<IconButton
87+
size="small"
88+
disabled={!annotation.displayed}
89+
onClick={goToNext}
90+
aria-label={t("unitex.next")}
91+
>
92+
<ArrowDownIcon />
93+
</IconButton>
94+
</Stack>
6895
</>
6996
);
7097
}

packages/react-tei/src/SidePanel/unitex/UnitexAnnotationBlock.spec.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { render } from "vitest-browser-react";
33

44
import { DocumentContextProvider } from "../../DocumentContextProvider";
55
import { I18nProvider } from "../../i18n/I18nProvider";
6+
import { TestDocumentNavigationContextProvider } from "../../navigation/TestDocumentNavigationContextProvider";
67
import type { TermStatistic } from "../../unitex/parseUnitexEnrichment";
78
import { UnitexAnnotationBlock } from "./UnitexAnnotationBlock";
89
import type { UnitexAnnotationBlockType } from "./unitexAnnotationBlocks";
@@ -22,7 +23,9 @@ function TestWrapper({ children }: { children: React.ReactNode }) {
2223
jsonDocument={[]}
2324
jsonUnitexEnrichment={enrichments}
2425
>
25-
{children}
26+
<TestDocumentNavigationContextProvider>
27+
{children}
28+
</TestDocumentNavigationContextProvider>
2629
</DocumentContextProvider>
2730
</I18nProvider>
2831
);

packages/react-tei/src/SidePanel/unitex/UnitexAnnotationBlock.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function UnitexAnnotationBlock({ block }: UnitexAnnotationBlockProps) {
6868
<Box
6969
sx={{
7070
display: "grid",
71-
gridTemplateColumns: "max-content 1fr",
71+
gridTemplateColumns: "max-content 1fr max-content",
7272
gridTemplateRows: "auto",
7373
columnGap: 0.5,
7474
rowGap: 1,

packages/react-tei/src/SidePanel/unitex/UnitexSection.spec.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { render } from "vitest-browser-react";
33

44
import { DocumentContextProvider } from "../../DocumentContextProvider";
55
import { I18nProvider } from "../../i18n/I18nProvider";
6+
import { TestDocumentNavigationContextProvider } from "../../navigation/TestDocumentNavigationContextProvider";
67
import type { TermStatistic } from "../../unitex/parseUnitexEnrichment";
78
import { UnitexSection } from "./UnitexSection";
89
import type { UnitexAnnotationBlockType } from "./unitexAnnotationBlocks";
@@ -22,7 +23,9 @@ function TestWrapper({ children }: { children: React.ReactNode }) {
2223
jsonDocument={[]}
2324
jsonUnitexEnrichment={enrichments}
2425
>
25-
{children}
26+
<TestDocumentNavigationContextProvider>
27+
{children}
28+
</TestDocumentNavigationContextProvider>
2629
</DocumentContextProvider>
2730
</I18nProvider>
2831
);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { act } from "react";
2+
import { describe, expect, it, vi } from "vitest";
3+
import { renderHook } from "vitest-browser-react";
4+
import {
5+
DIRECTION_NEXT,
6+
DIRECTION_PREVIOUS,
7+
type Direction,
8+
} from "../../navigation/DocumentNavigationContext";
9+
import { TestDocumentNavigationContextProvider } from "../../navigation/TestDocumentNavigationContextProvider";
10+
import { useUnitexAnnotationNavigation } from "./useUnitexAnnotationNavigation";
11+
12+
describe("useUnitexAnnotationNavigation", () => {
13+
it.each<{
14+
direction: Direction;
15+
fnName: keyof ReturnType<typeof useUnitexAnnotationNavigation>;
16+
}>([
17+
{ direction: DIRECTION_PREVIOUS, fnName: "goToPrevious" },
18+
{ direction: DIRECTION_NEXT, fnName: "goToNext" },
19+
])('should support navigating to the "%s" occurrence of a term', async ({
20+
direction,
21+
fnName,
22+
}) => {
23+
const navigateToBodyTargetSelector = vi.fn();
24+
const { result } = await renderHook(
25+
() => useUnitexAnnotationNavigation("example"),
26+
{
27+
wrapper: ({ children }) => (
28+
<TestDocumentNavigationContextProvider
29+
value={{
30+
navigateToBodyTargetSelector,
31+
}}
32+
>
33+
{children}
34+
</TestDocumentNavigationContextProvider>
35+
),
36+
},
37+
);
38+
39+
const fn = result.current[fnName];
40+
41+
act(() => {
42+
fn();
43+
});
44+
45+
expect(navigateToBodyTargetSelector).toHaveBeenCalledWith(
46+
'[data-term="example"]',
47+
direction,
48+
);
49+
});
50+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useCallback, useMemo } from "react";
2+
import { kebabCasify } from "../../helper/kebabCasify";
3+
import {
4+
DIRECTION_NEXT,
5+
DIRECTION_PREVIOUS,
6+
} from "../../navigation/DocumentNavigationContext";
7+
import { useDocumentNavigation } from "../../navigation/useNavigateToSection";
8+
9+
export function useUnitexAnnotationNavigation(term: string) {
10+
const { navigateToBodyTargetSelector } = useDocumentNavigation();
11+
12+
const selector = useMemo(() => `[data-term="${kebabCasify(term)}"]`, [term]);
13+
14+
const goToPrevious = useCallback(() => {
15+
navigateToBodyTargetSelector(selector, DIRECTION_PREVIOUS);
16+
}, [navigateToBodyTargetSelector, selector]);
17+
18+
const goToNext = useCallback(() => {
19+
navigateToBodyTargetSelector(selector, DIRECTION_NEXT);
20+
}, [navigateToBodyTargetSelector, selector]);
21+
22+
return useMemo(() => {
23+
return {
24+
goToPrevious,
25+
goToNext,
26+
};
27+
}, [goToPrevious, goToNext]);
28+
}

packages/react-tei/src/i18n/locales/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export const en: Translation = {
4949
toggleBlock_hide: "Deactivate underlining of words in the text",
5050
toggleTerm_show: 'Activate underlining for the term "{{term}}"',
5151
toggleTerm_hide: 'Deactivate underlining for the term "{{term}}"',
52+
previous: "Go to previous",
53+
next: "Go to next",
5254

5355
date_one: "Date ({{count}})",
5456
date_other: "Dates ({{count}})",

packages/react-tei/src/i18n/locales/fr.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export const fr = {
4747
toggleBlock_hide: "Désactiver le soulignement des mots dans le texte",
4848
toggleTerm_show: 'Activer le soulignement pour le terme "{{term}}"',
4949
toggleTerm_hide: 'Désactiver le soulignement pour le terme "{{term}}"',
50+
previous: "Aller au précédent",
51+
next: "Aller au suivant",
5052

5153
date_one: "Date ({{count}})",
5254
date_other: "Dates ({{count}})",

packages/react-tei/src/navigation/DocumentNavigationContext.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import { createContext, useCallback, useEffect, useMemo, useRef } from "react";
22
import { useDocumentContext } from "../DocumentContextProvider";
33

4+
export const DIRECTION_NEXT = "next";
5+
export const DIRECTION_PREVIOUS = "previous";
6+
export type Direction = typeof DIRECTION_NEXT | typeof DIRECTION_PREVIOUS;
7+
48
export type DocumentNavigationContextValue = {
5-
navigateToBodyTargetSelector(querySelector: string): void;
9+
navigateToBodyTargetSelector(
10+
querySelector: string,
11+
direction?: Direction,
12+
): void;
613
navigateToHeading(headingId: string): void;
714
navigateToFootnote(footnoteId: string): void;
815
navigateToFootnoteRef(id: string): void;
@@ -91,7 +98,11 @@ export function DocumentNavigationContextProvider({
9198
const currentSelectorRef = useRef<CurrentSelectorRef | null>(null);
9299

93100
const navigateToTargetLoop = useCallback(
94-
(wrapperElement: HTMLElement, querySelector: string) => {
101+
(
102+
wrapperElement: HTMLElement,
103+
querySelector: string,
104+
direction: Direction = DIRECTION_NEXT,
105+
) => {
95106
const targetElements =
96107
wrapperElement.querySelectorAll<HTMLElement>(querySelector);
97108

@@ -110,8 +121,12 @@ export function DocumentNavigationContextProvider({
110121
index: 0,
111122
};
112123
} else {
124+
const directionValue = direction === "next" ? 1 : -1;
113125
currentSelectorRef.current.index =
114-
(currentSelectorRef.current.index + 1) % targetElements.length;
126+
(currentSelectorRef.current.index +
127+
directionValue +
128+
targetElements.length) %
129+
targetElements.length;
115130
}
116131

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

135150
const navigateToBodyTargetSelector = useCallback(
136-
(querySelector: string) => {
151+
(querySelector: string, direction: Direction = DIRECTION_NEXT) => {
137152
const documentElement = documentRef.current;
138153

139154
if (!documentElement) {
140155
console.error("Document element not found");
141156
return;
142157
}
143158

144-
navigateToTargetLoop(documentElement, querySelector);
159+
navigateToTargetLoop(documentElement, querySelector, direction);
145160
},
146161
[documentRef, navigateToTargetLoop],
147162
);

0 commit comments

Comments
 (0)