Skip to content

Commit 3a493eb

Browse files
committed
fix(web): resolve duplicate keyboard modal and HTML entity decoding
1 parent 5ae7c8b commit 3a493eb

File tree

5 files changed

+33
-11
lines changed

5 files changed

+33
-11
lines changed

apps/web/src/components/AutocompletePage.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { IconInfoCircle, IconSearch } from "@tabler/icons-react";
2929

3030
import { AutocompleteEntityFilter } from "@/components/AutocompleteEntityFilter";
3131
import { BORDER_STYLE_GRAY_3 } from "@/config/style-constants";
32+
import { decodeHtmlEntities } from "@/utils/decode-html-entities";
3233

3334
export interface AutocompletePageProps {
3435
/** The entity type this page is for */
@@ -215,7 +216,7 @@ const AutocompleteResultCard = ({ result, entityType }: AutocompleteResultCardPr
215216
<Stack gap="xs">
216217
<Group justify="space-between" wrap="nowrap">
217218
<Anchor href={href} fw={500} size="md">
218-
{result.display_name}
219+
{decodeHtmlEntities(result.display_name ?? "")}
219220
</Anchor>
220221
<Badge size="sm" variant="light" color={metadata.color}>
221222
{metadata.displayName}
@@ -224,7 +225,7 @@ const AutocompleteResultCard = ({ result, entityType }: AutocompleteResultCardPr
224225

225226
{result.hint && (
226227
<Text size="sm" c="dimmed" lineClamp={2}>
227-
{result.hint}
228+
{decodeHtmlEntities(result.hint)}
228229
</Text>
229230
)}
230231

apps/web/src/components/layout/HeaderSearchInput.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { NOTIFICATION_DURATION } from "@/config/notification-constants";
3232
import { ICON_SIZE } from "@/config/style-constants";
3333
import { useNavigationEnhancements } from "@/hooks/useNavigationEnhancements";
3434
import { announceToScreenReader } from "@/utils/accessibility";
35+
import { decodeHtmlEntities } from "@/utils/decode-html-entities";
3536

3637
// Type for OpenAlex autocomplete API response
3738
interface OpenAlexAutocompleteItem {
@@ -188,7 +189,7 @@ export const HeaderSearchInput = () => {
188189

189190
const suggestion: SearchSuggestion = {
190191
id: item.id || `${item.entity_type}-${item.display_name}`,
191-
displayName: item.display_name,
192+
displayName: decodeHtmlEntities(item.display_name),
192193
entityType: mapEntityType(item.entity_type),
193194
description: item.entity_type,
194195
worksCount: item.works_count,

apps/web/src/components/layout/MainLayout.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -456,10 +456,6 @@ export const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
456456
<KeyboardShortcutsButton
457457
onClick={() => setShortcutsHelpOpened(true)}
458458
/>
459-
<KeyboardShortcutsHelp
460-
opened={shortcutsHelpOpened}
461-
onClose={() => setShortcutsHelpOpened(false)}
462-
/>
463459
</Group>
464460
</Group>
465461
</AppShell.Header>

apps/web/src/routes/autocomplete/index.lazy.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { BaseTable } from "@/components/tables/BaseTable";
3131
import { type TableViewMode,TableViewModeToggle } from "@/components/TableViewModeToggle";
3232
import { API, BORDER_STYLE_GRAY_3, ICON_SIZE, TEXT, TIME_MS } from '@/config/style-constants';
3333
import { useThemeColors } from "@/hooks/use-theme-colors";
34+
import { decodeHtmlEntities } from "@/utils/decode-html-entities";
3435
import { transformAutocompleteResultToGridItem } from "@/utils/entity-mappers";
3536

3637
/**
@@ -128,7 +129,7 @@ const AutocompleteGeneralRoute = () => {
128129
const routePath = routeMap[result.entity_type] || result.entity_type;
129130
return (
130131
<Anchor href={`#/${routePath}/${cleanId}`} fw={500}>
131-
{info.getValue() as string}
132+
{decodeHtmlEntities(info.getValue() as string)}
132133
</Anchor>
133134
);
134135
},

apps/web/src/utils/decode-html-entities.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ const HTML_ENTITIES: Record<string, string> = {
1919
};
2020

2121
/**
22-
* Pattern to match HTML entities we want to decode
22+
* Pattern to match HTML entities we want to decode:
23+
* - Named entities: &amp; &lt; &gt; &quot; &apos; &nbsp; &#39;
24+
* - Decimal numeric entities: &#123;
25+
* - Hex numeric entities: &#x00EF; &#xAB;
2326
*/
24-
const ENTITY_PATTERN = /&(?:amp|lt|gt|quot|apos|nbsp|#39);/g;
27+
const ENTITY_PATTERN = /&(?:amp|lt|gt|quot|apos|nbsp|#39|#\d+|#x[0-9a-fA-F]+);/g;
2528

2629
/**
2730
* Maximum iterations to prevent infinite loops
@@ -52,7 +55,27 @@ export const decodeHtmlEntities = (text: string): string => {
5255
while (iterations < MAX_DECODE_ITERATIONS) {
5356
const decoded = result.replaceAll(
5457
ENTITY_PATTERN,
55-
(match) => HTML_ENTITIES[match] ?? match
58+
(match) => {
59+
// Check named entities first
60+
if (HTML_ENTITIES[match]) {
61+
return HTML_ENTITIES[match];
62+
}
63+
// Handle hex numeric entities: &#x00EF; -> ï
64+
if (match.startsWith("&#x") || match.startsWith("&#X")) {
65+
const codePoint = parseInt(match.slice(3, -1), 16);
66+
if (!isNaN(codePoint)) {
67+
return String.fromCodePoint(codePoint);
68+
}
69+
}
70+
// Handle decimal numeric entities: &#239; -> ï
71+
if (match.startsWith("&#")) {
72+
const codePoint = parseInt(match.slice(2, -1), 10);
73+
if (!isNaN(codePoint)) {
74+
return String.fromCodePoint(codePoint);
75+
}
76+
}
77+
return match;
78+
}
5679
);
5780

5881
// No more entities to decode

0 commit comments

Comments
 (0)