From 90828d7acc0a12041ea99601c8fc3a3811d224a2 Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:27:59 +0200 Subject: [PATCH 1/4] feat: add click-to-open functionality for images in markdown content Images in comments and other markdown content can now be clicked to open in a new tab, matching the existing behavior for post thumbnail images. --- packages/shared/src/components/Markdown.tsx | 16 +++++++++++++++- .../shared/src/components/markdown.module.css | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/components/Markdown.tsx b/packages/shared/src/components/Markdown.tsx index c5f0ac1679..bc72161379 100644 --- a/packages/shared/src/components/Markdown.tsx +++ b/packages/shared/src/components/Markdown.tsx @@ -1,4 +1,4 @@ -import type { MouseEventHandler, ReactElement } from 'react'; +import type { MouseEvent, MouseEventHandler, ReactElement } from 'react'; import React, { useCallback, useState } from 'react'; import classNames from 'classnames'; import { useQuery } from '@tanstack/react-query'; @@ -10,6 +10,10 @@ import { useDomPurify } from '../hooks/useDomPurify'; import { getUserShortInfo } from '../graphql/users'; import { generateQueryKey, RequestKey } from '../lib/query'; +function isImageElement(element: Element | EventTarget): element is HTMLImageElement { + return element instanceof HTMLImageElement; +} + const UserEntityCard = dynamic(() => import('./cards/entity/UserEntityCard'), { ssr: false, }); @@ -81,6 +85,15 @@ export default function Markdown({ [cancelUserClearing, userId, clearUser], ); + const onImageClick = useCallback((e: MouseEvent) => { + const element = e.target; + + if (isImageElement(element) && element.src) { + e.preventDefault(); + window.open(element.src, '_blank', 'noopener,noreferrer'); + } + }, []); + return ( } > diff --git a/packages/shared/src/components/markdown.module.css b/packages/shared/src/components/markdown.module.css index f66b4bdb68..f75a6e6932 100644 --- a/packages/shared/src/components/markdown.module.css +++ b/packages/shared/src/components/markdown.module.css @@ -84,7 +84,7 @@ @apply border-border-subtlest-secondary my-10; } & :where(img) { - @apply rounded-16 max-h-img-mobile tablet:max-h-img-desktop; + @apply rounded-16 max-h-img-mobile tablet:max-h-img-desktop cursor-pointer; } & > :where(:first-child) { @apply mt-0; From 629cfc77250153b82d22d27bc7d42f3abf2ca026 Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:32:28 +0200 Subject: [PATCH 2/4] refactor: use stopPropagation instead of preventDefault for image clicks preventDefault is unnecessary for img elements (no default behavior). stopPropagation prevents click from bubbling to parent handlers. --- packages/shared/src/components/Markdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/components/Markdown.tsx b/packages/shared/src/components/Markdown.tsx index bc72161379..02125634e9 100644 --- a/packages/shared/src/components/Markdown.tsx +++ b/packages/shared/src/components/Markdown.tsx @@ -89,7 +89,7 @@ export default function Markdown({ const element = e.target; if (isImageElement(element) && element.src) { - e.preventDefault(); + e.stopPropagation(); window.open(element.src, '_blank', 'noopener,noreferrer'); } }, []); From 6e30cb96a4ee55a8f585cbee85acabf5039def93 Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:38:10 +0200 Subject: [PATCH 3/4] fix: resolve lint errors for image click handler - Fix prettier formatting for isImageElement function - Add eslint-disable comment for accessibility rules since images in dangerouslySetInnerHTML cannot be made focusable without modifying the markdown rendering pipeline (planned as follow-up work) --- packages/shared/src/components/Markdown.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/components/Markdown.tsx b/packages/shared/src/components/Markdown.tsx index 02125634e9..c66f6f248a 100644 --- a/packages/shared/src/components/Markdown.tsx +++ b/packages/shared/src/components/Markdown.tsx @@ -10,7 +10,9 @@ import { useDomPurify } from '../hooks/useDomPurify'; import { getUserShortInfo } from '../graphql/users'; import { generateQueryKey, RequestKey } from '../lib/query'; -function isImageElement(element: Element | EventTarget): element is HTMLImageElement { +function isImageElement( + element: Element | EventTarget, +): element is HTMLImageElement { return element instanceof HTMLImageElement; } @@ -104,6 +106,7 @@ export default function Markdown({ side="top" appendTo={appendTooltipTo?.()} trigger={ + // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
Date: Thu, 15 Jan 2026 09:43:44 +0200 Subject: [PATCH 4/4] feat: add full accessibility support for clickable images in markdown - Add useEffect to set accessibility attributes on images after render: - tabindex="0" for keyboard navigation - role="button" to indicate interactivity - aria-label="Open image in new tab" for screen readers - Add onKeyDown handler for Enter/Space key activation - Add focus-visible styles with ring indicator for keyboard users - Extract openImageInNewTab helper function for reuse This addresses the accessibility concerns from code review, ensuring keyboard and screen reader users can navigate and activate images. --- packages/shared/src/components/Markdown.tsx | 51 +++++++++++++++++-- .../shared/src/components/markdown.module.css | 4 ++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/components/Markdown.tsx b/packages/shared/src/components/Markdown.tsx index c66f6f248a..40c3457040 100644 --- a/packages/shared/src/components/Markdown.tsx +++ b/packages/shared/src/components/Markdown.tsx @@ -1,5 +1,10 @@ -import type { MouseEvent, MouseEventHandler, ReactElement } from 'react'; -import React, { useCallback, useState } from 'react'; +import type { + KeyboardEvent, + MouseEvent, + MouseEventHandler, + ReactElement, +} from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import classNames from 'classnames'; import { useQuery } from '@tanstack/react-query'; import dynamic from 'next/dynamic'; @@ -16,6 +21,10 @@ function isImageElement( return element instanceof HTMLImageElement; } +function openImageInNewTab(src: string): void { + window.open(src, '_blank', 'noopener,noreferrer'); +} + const UserEntityCard = dynamic(() => import('./cards/entity/UserEntityCard'), { ssr: false, }); @@ -53,6 +62,7 @@ export default function Markdown({ appendTooltipTo, }: MarkdownProps): ReactElement { const purify = useDomPurify(); + const containerRef = useRef(null); const [userId, setUserId] = useState(''); const [offset, setOffset] = useState([0, 0]); const [clearUser, cancelUserClearing] = useDebounceFn( @@ -67,6 +77,21 @@ export default function Markdown({ enabled: !!userId, }); + // Add accessibility attributes to images after render + useEffect(() => { + const container = containerRef.current; + if (!container) { + return; + } + + const images = container.querySelectorAll('img[src]'); + images.forEach((img) => { + img.setAttribute('tabindex', '0'); + img.setAttribute('role', 'button'); + img.setAttribute('aria-label', 'Open image in new tab'); + }); + }, [content]); + const onHoverHandler: MouseEventHandler = useCallback( (e) => { const element = e.target; @@ -92,7 +117,21 @@ export default function Markdown({ if (isImageElement(element) && element.src) { e.stopPropagation(); - window.open(element.src, '_blank', 'noopener,noreferrer'); + openImageInNewTab(element.src); + } + }, []); + + const onImageKeyDown = useCallback((e: KeyboardEvent) => { + const element = e.target; + + if ( + isImageElement(element) && + element.src && + (e.key === 'Enter' || e.key === ' ') + ) { + e.preventDefault(); + e.stopPropagation(); + openImageInNewTab(element.src); } }, []); @@ -106,8 +145,11 @@ export default function Markdown({ side="top" appendTo={appendTooltipTo?.()} trigger={ - // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events + /* Event delegation: click/keyboard handlers capture events from images inside. + Images are made accessible via useEffect (tabindex, role, aria-label). */ + // eslint-disable-next-line jsx-a11y/no-static-element-interactions
} > diff --git a/packages/shared/src/components/markdown.module.css b/packages/shared/src/components/markdown.module.css index f75a6e6932..27d54d0b06 100644 --- a/packages/shared/src/components/markdown.module.css +++ b/packages/shared/src/components/markdown.module.css @@ -85,6 +85,10 @@ } & :where(img) { @apply rounded-16 max-h-img-mobile tablet:max-h-img-desktop cursor-pointer; + + &:focus-visible { + @apply outline-none ring-2 ring-accent-cabbage-default ring-offset-2 ring-offset-background-default; + } } & > :where(:first-child) { @apply mt-0;