• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DLR-SC / ESID / 16518502093

25 Jul 2025 09:19AM UTC coverage: 6.555% (-47.5%) from 54.09%
16518502093

Pull #414

github

fifth-island
feat: Implement responsive zoom and UI enhancements in ArticleDialog
Pull Request #414: feat: Implement basic article search feature

418 of 542 branches covered (77.12%)

Branch coverage included in aggregate %.

283 of 61521 new or added lines in 13 files covered. (0.46%)

132 existing lines in 6 files now uncovered.

4159 of 69283 relevant lines covered (6.0%)

0.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.29
/src/components/OnboardingComponents/SemanticSearch/ArticleDialog.tsx
1
// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR)
2
// SPDX-License-Identifier: Apache-2.0
3

4
import React, {useState, useRef, useEffect, useCallback} from 'react';
1✔
5
import {
1✔
6
  Dialog,
7
  DialogTitle,
8
  DialogContent,
9
  IconButton,
10
  Typography,
11
  Box,
12
  CircularProgress,
13
  Tooltip,
14
} from '@mui/material';
15
import CloseIcon from '@mui/icons-material/Close';
1✔
16
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
1✔
17
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
1✔
18
import ZoomInIcon from '@mui/icons-material/ZoomIn';
1✔
19
import ZoomOutIcon from '@mui/icons-material/ZoomOut';
1✔
20
import {SearchResult} from 'types/semanticSearch';
21
import {Document, Page, pdfjs} from 'react-pdf';
1✔
22
import 'react-pdf/dist/Page/AnnotationLayer.css';
1✔
23
import 'react-pdf/dist/Page/TextLayer.css';
1✔
24

25
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs';
1✔
26

27
interface ArticleDialogProps {
28
  open: boolean;
29
  onClose: () => void;
30
  article: SearchResult | null;
31
}
32

33
/**
34
 * A dialog component to display the full content of a selected search result article.
35
 */
36
export default function ArticleDialog({open, onClose, article}: ArticleDialogProps): JSX.Element {
5✔
37
  const [numPages, setNumPages] = useState<number>(0);
5✔
38
  const [pageNumber, setPageNumber] = useState(1);
5✔
39
  const [containerWidth, setContainerWidth] = useState(0);
5✔
40
  const [zoom, setZoom] = useState(1);
5✔
41
  const containerRef = useRef<HTMLDivElement>(null);
5✔
42

43
  useEffect(() => {
5✔
44
    const observer = new ResizeObserver((entries) => {
4✔
NEW
45
      const entry = entries.at(0);
×
NEW
46
      if (entry) {
×
NEW
47
        setContainerWidth(entry.contentRect.width);
×
NEW
48
      }
×
49
    });
4✔
50

51
    if (containerRef.current) {
4!
NEW
52
      observer.observe(containerRef.current);
×
NEW
53
    }
×
54

55
    return () => {
4✔
56
      observer.disconnect();
4✔
57
    };
4✔
58
  }, []);
5✔
59

60
  const goToNextPage = useCallback(() => {
5✔
NEW
61
    setPageNumber((prevPageNumber) => Math.min(prevPageNumber + 1, numPages));
×
62
  }, [numPages]);
5✔
63

64
  const goToPreviousPage = useCallback(() => {
5✔
NEW
65
    setPageNumber((prevPageNumber) => Math.max(prevPageNumber - 1, 1));
×
66
  }, []);
5✔
67

68
  const handleZoomIn = useCallback(() => {
5✔
NEW
69
    setZoom((prevZoom) => Math.min(prevZoom + 0.25, 3));
×
70
  }, []);
5✔
71

72
  const handleZoomOut = useCallback(() => {
5✔
NEW
73
    setZoom((prevZoom) => Math.max(prevZoom - 0.25, 0.25));
×
74
  }, []);
5✔
75

76
  const handleZoomReset = useCallback(() => {
5✔
NEW
77
    setZoom(1);
×
78
  }, []);
5✔
79

80
  // Keyboard shortcuts for zoom and navigation
81
  useEffect(() => {
5✔
82
    const handleKeyDown = (event: KeyboardEvent) => {
4✔
NEW
83
      if (!open) return;
×
84

NEW
85
      switch (event.key) {
×
NEW
86
        case '+':
×
NEW
87
        case '=':
×
NEW
88
          if (event.ctrlKey || event.metaKey) {
×
NEW
89
            event.preventDefault();
×
NEW
90
            handleZoomIn();
×
NEW
91
          }
×
NEW
92
          break;
×
NEW
93
        case '-':
×
NEW
94
          if (event.ctrlKey || event.metaKey) {
×
NEW
95
            event.preventDefault();
×
NEW
96
            handleZoomOut();
×
NEW
97
          }
×
NEW
98
          break;
×
NEW
99
        case '0':
×
NEW
100
          if (event.ctrlKey || event.metaKey) {
×
NEW
101
            event.preventDefault();
×
NEW
102
            handleZoomReset();
×
NEW
103
          }
×
NEW
104
          break;
×
NEW
105
        case 'ArrowLeft':
×
NEW
106
          if (event.ctrlKey || event.metaKey) {
×
NEW
107
            event.preventDefault();
×
NEW
108
            goToPreviousPage();
×
NEW
109
          }
×
NEW
110
          break;
×
NEW
111
        case 'ArrowRight':
×
NEW
112
          if (event.ctrlKey || event.metaKey) {
×
NEW
113
            event.preventDefault();
×
NEW
114
            goToNextPage();
×
NEW
115
          }
×
NEW
116
          break;
×
NEW
117
      }
×
NEW
118
    };
×
119

120
    document.addEventListener('keydown', handleKeyDown);
4✔
121
    return () => {
4✔
122
      document.removeEventListener('keydown', handleKeyDown);
4✔
123
    };
4✔
124
  }, [open, handleZoomIn, handleZoomOut, handleZoomReset, goToPreviousPage, goToNextPage]);
5✔
125

126
  if (!article) {
5✔
127
    return <></>;
5✔
128
  }
5!
129

NEW
130
  function onDocumentLoadSuccess({numPages: nextNumPages}: {numPages: number}) {
×
NEW
131
    setNumPages(nextNumPages);
×
NEW
132
    setPageNumber(1); // Reset to first page on new document load
×
NEW
133
  }
×
134

NEW
135
  return (
×
NEW
136
    <Dialog
×
NEW
137
      open={open}
×
NEW
138
      onClose={onClose}
×
NEW
139
      maxWidth='lg'
×
NEW
140
      fullWidth
×
NEW
141
      scroll='paper'
×
NEW
142
      sx={{'& .MuiDialog-paper': {height: '90vh'}}}
×
143
    >
NEW
144
      <DialogTitle sx={{m: 0, p: 6, pb: 5}}>
×
NEW
145
        <Typography variant='h2' sx={{pr: '2rem'}}>
×
NEW
146
          {article.title}
×
NEW
147
        </Typography>
×
NEW
148
        <IconButton
×
NEW
149
          aria-label='close'
×
NEW
150
          onClick={onClose}
×
NEW
151
          sx={{
×
NEW
152
            position: 'absolute',
×
NEW
153
            right: 8,
×
NEW
154
            top: 8,
×
NEW
155
            color: (theme) => theme.palette.grey[500],
×
NEW
156
          }}
×
157
        >
NEW
158
          <CloseIcon />
×
NEW
159
        </IconButton>
×
NEW
160
      </DialogTitle>
×
NEW
161
      <DialogContent
×
NEW
162
        ref={containerRef}
×
NEW
163
        dividers
×
NEW
164
        sx={{display: 'flex', flexDirection: 'column', alignItems: 'center', p: 1}}
×
165
      >
NEW
166
        <Document
×
NEW
167
          file={article.hyperlink}
×
NEW
168
          onLoadSuccess={onDocumentLoadSuccess}
×
NEW
169
          loading={<CircularProgress />}
×
NEW
170
          error='Failed to load PDF file.'
×
171
        >
172
          <Page pageNumber={pageNumber} width={containerWidth > 0 ? containerWidth : undefined} scale={zoom} />
5!
173
        </Document>
5✔
174
      </DialogContent>
5✔
175
      <Box sx={{display: 'flex', justifyContent: 'center', alignItems: 'center', p: 1, gap: 2}}>
5✔
176
        <IconButton onClick={goToPreviousPage} disabled={pageNumber <= 1} aria-label='previous page'>
5✔
177
          <ArrowBackIosNewIcon />
5✔
178
        </IconButton>
5✔
179
        <Typography>
5✔
180
          {pageNumber} / {numPages}
5✔
181
        </Typography>
5✔
182
        <IconButton onClick={goToNextPage} disabled={pageNumber >= numPages} aria-label='next page'>
5✔
183
          <ArrowForwardIosIcon />
5✔
184
        </IconButton>
5✔
185
        <Box sx={{display: 'flex', alignItems: 'center', gap: 1, ml: 2}}>
5✔
186
          <Tooltip title='Zoom Out (Ctrl/Cmd + -)'>
5✔
187
            <IconButton onClick={handleZoomOut} disabled={zoom <= 0.25} aria-label='zoom out'>
5✔
188
              <ZoomOutIcon />
5✔
189
            </IconButton>
5✔
190
          </Tooltip>
5✔
191
          <Typography variant='body2' sx={{minWidth: '3rem', textAlign: 'center'}}>
5✔
192
            {Math.round(zoom * 100)}%
5✔
193
          </Typography>
5✔
194
          <Tooltip title='Zoom In (Ctrl/Cmd + +)'>
5✔
195
            <IconButton onClick={handleZoomIn} disabled={zoom >= 3} aria-label='zoom in'>
5✔
196
              <ZoomInIcon />
5✔
197
            </IconButton>
5✔
198
          </Tooltip>
5✔
199
          <Tooltip title='Reset Zoom (Ctrl/Cmd + 0)'>
5✔
200
            <IconButton onClick={handleZoomReset} disabled={zoom === 1} aria-label='reset zoom'>
5✔
201
              <Typography variant='body2' sx={{fontWeight: 'bold'}}>
5✔
202
                100%
203
              </Typography>
5✔
204
            </IconButton>
5✔
205
          </Tooltip>
5✔
206
        </Box>
5✔
207
      </Box>
5✔
208
    </Dialog>
5✔
209
  );
210
}
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc