Skip to content

Commit 72b0d3b

Browse files
committed
fix: wrap operation card with div instead of anchor
This resolves a violation of HTML spec: an anchor element cannot contain interactive content (button, anchor, etc.) https://html.spec.whatwg.org/multipage/dom.html#interactive-content-2
1 parent ea80664 commit 72b0d3b

File tree

3 files changed

+76
-7
lines changed

3 files changed

+76
-7
lines changed

src/components/OperationCard.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { Tooltip2 } from '@blueprintjs/popover2'
44
import clsx from 'clsx'
55
import { copyShortCode, handleDownloadJSON } from 'services/operation'
66

7-
import { ReLink } from 'components/ReLink'
87
import { RelativeTime } from 'components/RelativeTime'
98
import { AddToOperationSetButton } from 'components/operation-set/AddToOperationSet'
109
import { OperationRating } from 'components/viewer/OperationRating'
@@ -13,6 +12,7 @@ import { OpDifficulty, Operation } from 'models/operation'
1312
import { useLevels } from '../apis/level'
1413
import { createCustomLevel, findLevelByStageName } from '../models/level'
1514
import { Paragraphs } from './Paragraphs'
15+
import { ReLinkDiv } from './ReLinkDiv'
1616
import { EDifficulty } from './entity/EDifficulty'
1717
import { EDifficultyLevel, NeoELevel } from './entity/ELevel'
1818

@@ -21,9 +21,9 @@ export const NeoOperationCard = ({ operation }: { operation: Operation }) => {
2121

2222
return (
2323
<Card interactive={true} elevation={Elevation.TWO} className="relative">
24-
<ReLink
24+
<ReLinkDiv
2525
search={{ op: operation.id }}
26-
className="no-underline h-full flex flex-col gap-2"
26+
className="h-full flex flex-col gap-2"
2727
>
2828
<div className="flex">
2929
<Tooltip2
@@ -120,7 +120,7 @@ export const OperationCard = ({ operation }: { operation: Operation }) => {
120120
elevation={Elevation.TWO}
121121
className="relative mb-4 sm:mb-2 last:mb-0"
122122
>
123-
<ReLink search={{ op: operation.id }} className="block no-underline">
123+
<ReLinkDiv search={{ op: operation.id }}>
124124
<div className="flex flex-wrap mb-4 sm:mb-2">
125125
{/* title */}
126126
<div className="flex flex-col gap-3">
@@ -192,7 +192,7 @@ export const OperationCard = ({ operation }: { operation: Operation }) => {
192192
<OperatorTags operation={operation} />
193193
</div>
194194
</div>
195-
</ReLink>
195+
</ReLinkDiv>
196196

197197
<CardActions
198198
className="absolute top-4 xl:top-12 right-[18px]"

src/components/OperationSetCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const NeoOperationSetCard = ({
2222
>
2323
<ReLink
2424
search={{ opset: operationSet.id }}
25-
className="block no-underline"
25+
className="block no-underline hover:no-underline hover:text-inherit"
2626
>
2727
{/* title */}
2828
<div className="flex gap-1">
@@ -91,7 +91,7 @@ export const OperationSetCard = ({
9191
>
9292
<ReLink
9393
search={{ opset: operationSet.id }}
94-
className="block no-underline"
94+
className="block no-underline hover:no-underline hover:text-inherit"
9595
>
9696
<div className="flex flex-wrap mb-4 sm:mb-2">
9797
{/* title */}

src/components/ReLinkDiv.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { isString } from '@sentry/utils'
2+
3+
import { noop } from 'lodash-es'
4+
import { AnchorHTMLAttributes, HtmlHTMLAttributes } from 'react'
5+
import {
6+
RelativeRoutingType,
7+
To,
8+
useLinkClickHandler,
9+
useSearchParams,
10+
} from 'react-router-dom'
11+
12+
interface ReLinkProps extends HtmlHTMLAttributes<HTMLDivElement> {
13+
search?: Record<string, string | number | boolean | undefined>
14+
target?: AnchorHTMLAttributes<HTMLAnchorElement>['target']
15+
16+
to?: To
17+
replace?: boolean
18+
state?: any
19+
preventScrollReset?: boolean
20+
relative?: RelativeRoutingType
21+
}
22+
23+
// div 版的 ReLink
24+
export function ReLinkDiv({
25+
className,
26+
search,
27+
to,
28+
replace = false,
29+
state,
30+
target,
31+
onClick,
32+
...props
33+
}: ReLinkProps) {
34+
const [searchParams] = useSearchParams()
35+
36+
if (search) {
37+
for (const [key, value] of Object.entries(search)) {
38+
if (value === undefined) {
39+
searchParams.delete(key)
40+
} else {
41+
searchParams.set(key, String(value))
42+
}
43+
}
44+
}
45+
46+
to = isString(to) ? to : { ...to, search: searchParams.toString() }
47+
48+
const handleClick = useLinkClickHandler<HTMLDivElement>(to, {
49+
replace,
50+
state,
51+
target,
52+
})
53+
54+
return (
55+
<div
56+
role="link"
57+
tabIndex={0}
58+
className={className}
59+
onClick={(event) => {
60+
onClick?.(event)
61+
if (!event.defaultPrevented) {
62+
handleClick(event)
63+
}
64+
}}
65+
onKeyUp={noop}
66+
{...props}
67+
/>
68+
)
69+
}

0 commit comments

Comments
 (0)