fix(react): address Doctor findings (#1143)

Accessibility/semantics pass aligned with React Doctor: role-based spans/divs become real buttons, role=status becomes <output>, modals use native <dialog>, author/peer flags render as <img>, and embed/challenge iframes are sandboxed. Includes review follow-ups: correct button chrome/centering/themed dialog colors and focus rings, the missing aria-label i18n keys (all languages), the Yandex image-search URL, clearer .bso setup steps, a keyboard-accessible image-search dropdown, and removal of the post-edit ('comment edited') display (mod edits unchanged).
This commit is contained in:
Tommaso Casaburi
2026-05-29 17:09:07 +07:00
committed by GitHub
parent ac95e58433
commit e082c7b428
132 changed files with 1753 additions and 1224 deletions
@@ -2,7 +2,8 @@ import * as React from 'react';
import { createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import Embed, { canEmbed } from '../embed';
import Embed from '../embed';
import { canEmbed } from '../embed-utils';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
+60
View File
@@ -0,0 +1,60 @@
export const youtubeHosts = new Set<string>([
'youtube.com',
'www.youtube.com',
'youtu.be',
'www.youtu.be',
'm.youtube.com',
'music.youtube.com',
// working Invidious instances - https://docs.invidious.io/instances/ - https://uptime.invidious.io/
'yewtu.be',
'inv.nadeko.net',
'yt.artemislena.eu',
'invidious.nerdvpn.de',
]);
export const xHosts = new Set<string>(['twitter.com', 'www.twitter.com', 'x.com', 'www.x.com']);
export const getXTweetId = (parsedUrl: URL): string | undefined => {
const pathParts = parsedUrl.pathname.split('/').filter(Boolean);
const statusIndex = pathParts.findIndex((part) => part === 'status' || part === 'statuses');
const statusId = statusIndex === -1 ? undefined : pathParts[statusIndex + 1];
return statusId && /^\d+$/.test(statusId) ? statusId : undefined;
};
export const redditHosts = new Set<string>(['reddit.com', 'www.reddit.com', 'old.reddit.com']);
export const twitchHosts = new Set<string>(['twitch.tv', 'www.twitch.tv']);
export const tiktokHosts = new Set<string>(['tiktok.com', 'www.tiktok.com']);
export const instagramHosts = new Set<string>(['instagram.com', 'www.instagram.com']);
export const odyseeHosts = new Set<string>(['odysee.com', 'www.odysee.com']);
export const bitchuteHosts = new Set<string>(['bitchute.com', 'www.bitchute.com']);
export const streamableHosts = new Set<string>(['streamable.com', 'www.streamable.com']);
export const spotifyHosts = new Set<string>(['spotify.com', 'www.spotify.com', 'open.spotify.com']);
export const soundcloudHosts = new Set<string>(['soundcloud.com', 'www.soundcloud.com', 'on.soundcloud.com', 'api.soundcloud.com', 'w.soundcloud.com']);
const canEmbedHosts = new Set<string>([
...youtubeHosts,
...xHosts,
...redditHosts,
...twitchHosts,
...tiktokHosts,
...instagramHosts,
...odyseeHosts,
...bitchuteHosts,
...soundcloudHosts,
...streamableHosts,
...spotifyHosts,
]);
export const canEmbed = (parsedUrl: URL): boolean => {
if (xHosts.has(parsedUrl.host)) {
return Boolean(getXTweetId(parsedUrl));
}
if (redditHosts.has(parsedUrl.host)) {
// Reddit posts are not embeddable if the URL does not include '/comments/'
return parsedUrl.pathname.includes('/comments/');
}
return canEmbedHosts.has(parsedUrl.host) || (parsedUrl.host.startsWith('yt.') && parsedUrl.searchParams.has('v'));
};
+22 -69
View File
@@ -1,4 +1,18 @@
import styles from './embed.module.css';
import {
bitchuteHosts,
getXTweetId,
instagramHosts,
odyseeHosts,
redditHosts,
soundcloudHosts,
spotifyHosts,
streamableHosts,
tiktokHosts,
twitchHosts,
xHosts,
youtubeHosts,
} from './embed-utils';
interface EmbedProps {
url: string;
@@ -47,20 +61,7 @@ interface EmbedComponentProps {
}
const srcDocSandbox = 'allow-scripts allow-popups allow-popups-to-escape-sandbox';
const youtubeHosts = new Set<string>([
'youtube.com',
'www.youtube.com',
'youtu.be',
'www.youtu.be',
'm.youtube.com',
'music.youtube.com',
// working Invidious instances - https://docs.invidious.io/instances/ - https://uptime.invidious.io/
'yewtu.be',
'inv.nadeko.net',
'yt.artemislena.eu',
'invidious.nerdvpn.de',
]);
const remoteEmbedSandbox = `${srcDocSandbox} allow-forms allow-presentation allow-same-origin`;
const YoutubeEmbed = ({ parsedUrl }: EmbedComponentProps) => {
let embedSrc = '';
@@ -100,6 +101,7 @@ const YoutubeEmbed = ({ parsedUrl }: EmbedComponentProps) => {
referrerPolicy='origin'
allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'
allowFullScreen
sandbox={remoteEmbedSandbox}
title={parsedUrl.href}
src={embedSrc}
/>
@@ -108,16 +110,6 @@ const YoutubeEmbed = ({ parsedUrl }: EmbedComponentProps) => {
return null;
};
const xHosts = new Set<string>(['twitter.com', 'www.twitter.com', 'x.com', 'www.x.com']);
const getXTweetId = (parsedUrl: URL): string | undefined => {
const pathParts = parsedUrl.pathname.split('/').filter(Boolean);
const statusIndex = pathParts.findIndex((part) => part === 'status' || part === 'statuses');
const statusId = statusIndex === -1 ? undefined : pathParts[statusIndex + 1];
return statusId && /^\d+$/.test(statusId) ? statusId : undefined;
};
const XEmbed = ({ parsedUrl }: EmbedComponentProps) => {
const tweetId = getXTweetId(parsedUrl);
if (!tweetId) {
@@ -147,8 +139,6 @@ const XEmbed = ({ parsedUrl }: EmbedComponentProps) => {
);
};
const redditHosts = new Set<string>(['reddit.com', 'www.reddit.com', 'old.reddit.com']);
const RedditEmbed = ({ parsedUrl }: EmbedComponentProps) => {
return (
<iframe
@@ -175,8 +165,6 @@ const RedditEmbed = ({ parsedUrl }: EmbedComponentProps) => {
);
};
const twitchHosts = new Set<string>(['twitch.tv', 'www.twitch.tv']);
const TwitchEmbed = ({ parsedUrl }: EmbedComponentProps) => {
let iframeUrl;
if (parsedUrl.pathname.startsWith('/videos/')) {
@@ -194,14 +182,13 @@ const TwitchEmbed = ({ parsedUrl }: EmbedComponentProps) => {
referrerPolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
allowFullScreen
sandbox={remoteEmbedSandbox}
title={parsedUrl.href}
src={iframeUrl}
/>
);
};
const tiktokHosts = new Set<string>(['tiktok.com', 'www.tiktok.com']);
const TiktokEmbed = ({ parsedUrl }: EmbedComponentProps) => {
const videoId = parsedUrl.pathname.replace(/.+\/video\//, '').replaceAll('/', '');
return (
@@ -223,8 +210,6 @@ const TiktokEmbed = ({ parsedUrl }: EmbedComponentProps) => {
);
};
const instagramHosts = new Set<string>(['instagram.com', 'www.instagram.com']);
const InstagramEmbed = ({ parsedUrl }: EmbedComponentProps) => {
const pathNames = parsedUrl.pathname.replace(/\/+$/, '').split('/');
const id = pathNames[pathNames.length - 1];
@@ -247,8 +232,6 @@ const InstagramEmbed = ({ parsedUrl }: EmbedComponentProps) => {
);
};
const odyseeHosts = new Set<string>(['odysee.com', 'www.odysee.com']);
const OdyseeEmbed = ({ parsedUrl }: EmbedComponentProps) => {
const iframeUrl = `https://odysee.com/$/embed${parsedUrl.pathname}`;
return (
@@ -259,14 +242,13 @@ const OdyseeEmbed = ({ parsedUrl }: EmbedComponentProps) => {
referrerPolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
allowFullScreen
sandbox={remoteEmbedSandbox}
title={parsedUrl.href}
src={iframeUrl}
/>
);
};
const bitchuteHosts = new Set<string>(['bitchute.com', 'www.bitchute.com']);
const BitchuteEmbed = ({ parsedUrl }: EmbedComponentProps) => {
const videoId = parsedUrl.pathname.replace(/\/video\//, '').replaceAll('/', '');
return (
@@ -277,14 +259,13 @@ const BitchuteEmbed = ({ parsedUrl }: EmbedComponentProps) => {
referrerPolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
allowFullScreen
sandbox={remoteEmbedSandbox}
title={parsedUrl.href}
src={`https://www.bitchute.com/embed/${videoId}/`}
/>
);
};
const streamableHosts = new Set<string>(['streamable.com', 'www.streamable.com']);
const StreamableEmbed = ({ parsedUrl }: EmbedComponentProps) => {
const videoId = parsedUrl.pathname.replaceAll('/', '');
return (
@@ -295,14 +276,13 @@ const StreamableEmbed = ({ parsedUrl }: EmbedComponentProps) => {
referrerPolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
allowFullScreen
sandbox={remoteEmbedSandbox}
title={parsedUrl.href}
src={`https://streamable.com/e/${videoId}`}
/>
);
};
const spotifyHosts = new Set<string>(['spotify.com', 'www.spotify.com', 'open.spotify.com']);
const SpotifyEmbed = ({ parsedUrl }: EmbedComponentProps) => {
const iframeUrl = `https://open.spotify.com/embed${parsedUrl.pathname}?theme=0`;
return (
@@ -313,14 +293,13 @@ const SpotifyEmbed = ({ parsedUrl }: EmbedComponentProps) => {
referrerPolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
allowFullScreen
sandbox={remoteEmbedSandbox}
title={parsedUrl.href}
src={iframeUrl}
/>
);
};
const soundcloudHosts = new Set(['soundcloud.com', 'www.soundcloud.com', 'on.soundcloud.com', 'api.soundcloud.com', 'w.soundcloud.com']);
// not officially documented https://stackoverflow.com/questions/20870270/how-to-get-soundcloud-embed-code-by-soundcloud-com-url
const SoundcloudEmbed = ({ parsedUrl }: EmbedComponentProps) => {
return (
@@ -331,37 +310,11 @@ const SoundcloudEmbed = ({ parsedUrl }: EmbedComponentProps) => {
referrerPolicy='no-referrer'
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
allowFullScreen
sandbox={remoteEmbedSandbox}
title={parsedUrl.href}
src={`https://w.soundcloud.com/player/?url=${parsedUrl.href}`}
/>
);
};
const canEmbedHosts = new Set<string>([
...youtubeHosts,
...xHosts,
...redditHosts,
...twitchHosts,
...tiktokHosts,
...instagramHosts,
...odyseeHosts,
...bitchuteHosts,
...soundcloudHosts,
...streamableHosts,
...spotifyHosts,
]);
export const canEmbed = (parsedUrl: URL): boolean => {
if (xHosts.has(parsedUrl.host)) {
return Boolean(getXTweetId(parsedUrl));
}
if (redditHosts.has(parsedUrl.host)) {
// Reddit posts are not embeddable if the URL does not include '/comments/'
return parsedUrl.pathname.includes('/comments/');
}
return canEmbedHosts.has(parsedUrl.host) || (parsedUrl.host.startsWith('yt.') && parsedUrl.searchParams.has('v'));
};
export default Embed;
+2 -1
View File
@@ -1 +1,2 @@
export { default, canEmbed } from './embed';
export { default } from './embed';
export { canEmbed } from './embed-utils';