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
@@ -80,12 +80,12 @@ const render = (initialEntry = '/all/settings') => {
const getLocationText = () => container.querySelector('[data-testid="location"]')?.textContent ?? '';
const getLabelByText = (text: string) => {
const label = Array.from(container.querySelectorAll('label')).find((candidate) => (candidate.textContent ?? '').includes(text));
if (!label) {
throw new Error(`Label containing "${text}" not found`);
const getSectionToggleByText = (text: string) => {
const control = Array.from(container.querySelectorAll<HTMLElement>('label, button')).find((candidate) => (candidate.textContent ?? '').includes(text));
if (!control) {
throw new Error(`Section toggle containing "${text}" not found`);
}
return label;
return control;
};
describe('SettingsModal', () => {
@@ -115,7 +115,7 @@ describe('SettingsModal', () => {
expect(getLocationText()).toBe('/all/settings#account-settings');
await act(async () => {
getLabelByText('interface').click();
getSectionToggleByText('interface').click();
});
expect(getLocationText()).toBe('/all/settings#interface-settings');
@@ -123,14 +123,14 @@ describe('SettingsModal', () => {
expect(container.querySelector('[data-testid="account-settings"]')).not.toBeNull();
await act(async () => {
getLabelByText('interface').click();
getSectionToggleByText('interface').click();
});
expect(getLocationText()).toBe('/all/settings#account-settings');
expect(container.querySelector('[data-testid="interface-settings-panel"]')).toBeNull();
await act(async () => {
getLabelByText('bitsocial_account').click();
getSectionToggleByText('bitsocial_account').click();
});
expect(getLocationText()).toBe('/all/settings');
@@ -140,7 +140,7 @@ describe('SettingsModal', () => {
it('expands and collapses all settings sections', async () => {
render('/all/settings');
const expandAllControl = Array.from(container.querySelectorAll('[role="button"]')).find((candidate) => (candidate.textContent ?? '').includes('expand_all_settings'));
const expandAllControl = Array.from(container.querySelectorAll('button')).find((candidate) => (candidate.textContent ?? '').includes('expand_all_settings'));
if (!expandAllControl) {
throw new Error('expand_all_settings control not found');
}
@@ -157,9 +157,7 @@ describe('SettingsModal', () => {
expect(container.querySelector('[data-testid="advanced-settings-panel"]')).not.toBeNull();
expect(container.querySelector('[data-testid="p2p-stats-settings-panel"]')).not.toBeNull();
const collapseAllControl = Array.from(container.querySelectorAll('[role="button"]')).find((candidate) =>
(candidate.textContent ?? '').includes('collapse_all_settings'),
);
const collapseAllControl = Array.from(container.querySelectorAll('button')).find((candidate) => (candidate.textContent ?? '').includes('collapse_all_settings'));
if (!collapseAllControl) {
throw new Error('collapse_all_settings control not found');
}
@@ -186,7 +184,7 @@ describe('SettingsModal', () => {
it('closes the modal when the overlay is clicked', async () => {
render('/all/settings#interface-settings');
const overlay = container.querySelector('[role="button"]');
const overlay = container.querySelector('button');
if (!overlay) {
throw new Error('overlay not found');
}
@@ -217,13 +217,19 @@ const AccountSettingsEditor = ({
<select value={account?.name} onChange={(e) => setActiveAccount(e.target.value)}>
{accountsOptions}
</select>{' '}
<button onClick={() => navigate('/settings/account-data', { state: { returnTo: location.pathname + location.hash } })}>{t('edit')}</button>{' '}
<button onClick={handleExportAccount}>{t('download_backup')}</button>
<button type='button' onClick={() => navigate('/settings/account-data', { state: { returnTo: location.pathname + location.hash } })}>
{t('edit')}
</button>{' '}
<button type='button' onClick={handleExportAccount}>
{t('download_backup')}
</button>
<div className={styles.info}>{accountStorageInfo}</div>
</div>
<div>
<button onClick={handleImportAccount}>{t('import_account_backup')}</button>
<button className={styles.deleteAccount} onClick={() => _deleteAccount(account?.name ?? '')}>
<button type='button' onClick={handleImportAccount}>
{t('import_account_backup')}
</button>
<button type='button' className={styles.deleteAccount} onClick={() => _deleteAccount(account?.name ?? '')}>
{t('delete_account')}
</button>
</div>
@@ -64,6 +64,7 @@ const IPFSGatewaysSettings = ({ ipfsGatewayUrlsRef, mediaIpfsGatewayUrlRef }: Se
<div className={styles.ipfsGatewaysSettings}>
<div className={styles.ipfsGatewaysSetting}>
<textarea
aria-label='IPFS gateway URLs'
defaultValue={ipfsGatewayUrlsDefaultValue}
ref={ipfsGatewayUrlsRef}
disabled={isConnectedToRpc}
@@ -77,6 +78,7 @@ const IPFSGatewaysSettings = ({ ipfsGatewayUrlsRef, mediaIpfsGatewayUrlRef }: Se
<div>
<input
type='text'
aria-label='Media IPFS gateway URL'
defaultValue={mediaIpfsGatewayUrl}
ref={mediaIpfsGatewayUrlRef}
disabled={isConnectedToRpc}
@@ -100,6 +102,7 @@ const PubsubProvidersSettings = ({ pubsubProvidersRef }: SettingsProps) => {
return (
<div className={styles.pubsubProvidersSettings}>
<textarea
aria-label='Pubsub providers'
defaultValue={pubsubProvidersDefaultValue}
ref={pubsubProvidersRef}
disabled={isConnectedToRpc}
@@ -124,6 +127,7 @@ const HttpRoutersSettings = ({ httpRoutersRef }: SettingsProps) => {
return (
<div className={styles.httpRoutersSettings}>
<textarea
aria-label='HTTP routers'
defaultValue={httpRoutersDefaultValue}
ref={httpRoutersRef}
disabled={isConnectedToRpc}
@@ -147,6 +151,7 @@ const BlockchainProvidersSettings = ({ ethRpcRef }: SettingsProps) => {
<span className={styles.settingTip}>Ethereum RPC, for .eth domains</span>
<div>
<textarea
aria-label='Ethereum RPC URLs'
defaultValue={ethRpcDefaultValue}
ref={ethRpcRef}
autoCorrect='off'
@@ -168,6 +173,7 @@ const P2pRPCSettings = ({ p2pRpcRef }: SettingsProps) => {
<div className={styles.p2pRPCSettings}>
<input
type='text'
aria-label='P2P RPC clients'
defaultValue={pkcRpcClientsOptions}
placeholder='ws://<IP>:<port>/<secretAuthKey>'
ref={p2pRpcRef}
@@ -188,7 +194,16 @@ const P2pDataPathSettings = ({ p2pDataPathRef }: SettingsProps) => {
return (
<div className={styles.p2pDataPathSettings}>
<div>
<input autoCorrect='off' autoCapitalize='off' spellCheck='false' type='text' defaultValue={path} disabled={!isConnectedToRpc} ref={p2pDataPathRef} />
<input
autoCorrect='off'
autoCapitalize='off'
spellCheck='false'
type='text'
aria-label='P2P data path'
defaultValue={path}
disabled={!isConnectedToRpc}
ref={p2pDataPathRef}
/>
</div>
</div>
);
@@ -205,6 +220,7 @@ const PureP2PBrowserSettings = ({ onPureP2PBrowserChange, pureP2PBrowserEnabled,
<input
className={styles.pureP2PCheckbox}
type='checkbox'
aria-label='Pure P2P browser mode'
checked={isChecked}
ref={pureP2PBrowserRef}
onChange={(event) => onPureP2PBrowserChange?.(event.currentTarget.checked)}
@@ -363,7 +379,7 @@ const AdvancedSettings = () => {
{canConfigurePureP2PBrowser ? (
<PureP2PBrowserSettings pureP2PBrowserEnabled={browserPureP2PEnabled} pureP2PBrowserRef={pureP2PBrowserRef} onPureP2PBrowserChange={setBrowserPureP2PSelection} />
) : null}
<button className={styles.saveOptions} onClick={handleSave}>
<button type='button' className={styles.saveOptions} onClick={handleSave}>
{t('save_advanced_settings')}
</button>
</div>
@@ -178,6 +178,7 @@ const CryptoAddressSettingContent = ({ account }: { account: ReturnType<typeof u
<div className={styles.cryptoAddressInput}>
<input
type='text'
aria-label='Crypto address'
placeholder='myaddress.bso'
value={cryptoAddress}
onChange={(e) => {
@@ -186,33 +187,48 @@ const CryptoAddressSettingContent = ({ account }: { account: ReturnType<typeof u
setCryptoAddress(e.target.value);
}}
/>
<button className={styles.saveButton} onClick={saveCryptoAddress}>
<button type='button' className={styles.saveButton} onClick={saveCryptoAddress}>
{t('save')}
</button>
<button className={styles.infoButton} onClick={() => setShowCryptoAddressInfo(!showCryptoAddressInfo)}>
<button
type='button'
className={styles.infoButton}
aria-label={showCryptoAddressInfo ? 'Hide crypto address help' : 'Show crypto address help'}
onClick={() => setShowCryptoAddressInfo(!showCryptoAddressInfo)}
>
{showCryptoAddressInfo ? 'x' : '?'}
</button>
{showCryptoAddressInfo && (
<div className={styles.cryptoAddressInfo}>
steps to set a .bso account address:
<br />
A <code>.bso</code> address is just an ENS name you own, shown by 5chan with a <code>.bso</code> ending instead of <code>.eth</code>. To use one as your
account name:
<ol>
<li>
buy your desired .bso address as .eth on{' '}
Register a name (e.g. <code>yourname.eth</code>) at{' '}
<a href='https://app.ens.domains/' target='_blank' rel='noopener noreferrer'>
app.ens.domains
</a>{' '}
</a>
.
</li>
<li>
Open that name on ENS and go to <strong>Records</strong> <strong>Edit Records</strong>.
</li>
<li>
Add a <strong>text record</strong> named <code>bitsocial</code> and paste this account&apos;s public key as its value:
<br />
<code>{account?.signer?.address}</code>
</li>
<li>
Come back here, type your name ending in <code>.bso</code> (e.g. <code>yourname.bso</code>) in the field above, press <strong>Check</strong> to confirm it
points to this account, then press <strong>Save</strong>.
</li>
<li>once you own the .eth address, go to its page on ENS, click on "records", then "edit records"</li>
<li>add a new text record with name "bitsocial" and value: {account?.signer?.address}</li>
<li>enter your .bso address in the input field above, click "check" to verify it's yours, then click "save"</li>
</ol>
</div>
)}
{savedCryptoAddress && <span className={styles.saved}>{t('saved')}</span>}
</div>
<div className={styles.checkCryptoAddress}>
<button className={styles.button} onClick={checkCryptoAddress}>
<button type='button' className={styles.button} onClick={checkCryptoAddress}>
{t('check')}
</button>{' '}
<span className={(transientResolutionStatus ?? resolutionStatus).resolveClass}>{(transientResolutionStatus ?? resolutionStatus).resolveString}</span>
@@ -105,6 +105,7 @@ const CryptoWalletsForm = ({ account }: { account: Account | undefined }) => {
<span className={styles.walletFieldTitle}>{capitalize(t('chain_ticker'))}: </span>
<input
type='text'
aria-label={capitalize(t('chain_ticker'))}
onChange={(e) => setWalletsArrayProperty(selectedWallet, 'chainTicker', e.target.value)}
value={walletsArray[selectedWallet].chainTicker}
placeholder='eth/sol/matic'
@@ -114,6 +115,7 @@ const CryptoWalletsForm = ({ account }: { account: Account | undefined }) => {
<span className={styles.walletFieldTitle}>{capitalize(t('wallet_address'))}: </span>
<input
type='text'
aria-label={capitalize(t('wallet_address'))}
onChange={(e) => setWalletsArrayProperty(selectedWallet, 'address', e.target.value)}
value={walletsArray[selectedWallet].address}
placeholder='0x...'
@@ -124,7 +126,14 @@ const CryptoWalletsForm = ({ account }: { account: Account | undefined }) => {
<Trans
i18nKey='copy_message_etherscan'
components={{
1: <button key='copy-message-button' onClick={() => copyMessageToSign(walletsArray[selectedWallet], selectedWallet)} />,
1: (
<button
type='button'
key='copy-message-button'
aria-label={hasCopied ? t('copied') : t('copy')}
onClick={() => copyMessageToSign(walletsArray[selectedWallet], selectedWallet)}
/>
),
// eslint-disable-next-line
2: <a key='etherscan-link' href='https://etherscan.io/verifiedSignatures' target='_blank' rel='noopener noreferrer' />,
}}
@@ -136,6 +145,7 @@ const CryptoWalletsForm = ({ account }: { account: Account | undefined }) => {
<span className={`${styles.walletFieldTitle} ${styles.timestampfield}`}>{capitalize(t('timestamp'))}: </span>
<input
type='text'
aria-label={capitalize(t('timestamp'))}
onChange={(e) => setWalletsArrayProperty(selectedWallet, 'timestamp', Number(e.target.value))}
value={walletsArray[selectedWallet].timestamp || ''}
placeholder='1234567890'
@@ -145,18 +155,19 @@ const CryptoWalletsForm = ({ account }: { account: Account | undefined }) => {
<span className={styles.walletFieldTitle}>{capitalize(t('paste_signature'))}: </span>
<input
type='text'
aria-label={capitalize(t('paste_signature'))}
onChange={(e) => setWalletsArrayProperty(selectedWallet, 'signature', e.target.value)}
value={walletsArray[selectedWallet].signature}
placeholder='0x...'
/>
<div className={styles.buttons}>
<button className={styles.save} onClick={save}>
<button type='button' className={styles.save} onClick={save}>
{t('save_changes')}
</button>
</div>
</div>
<div className={styles.deleteWalletContainer}>
<button className={styles.removeWallet} onClick={() => _removeWallet(selectedWallet)}>
<button type='button' className={styles.removeWallet} onClick={() => _removeWallet(selectedWallet)}>
{t('delete_wallet')}
</button>
</div>
@@ -175,6 +186,7 @@ const CryptoWalletsForm = ({ account }: { account: Account | undefined }) => {
))}
</select>
<button
type='button'
onClick={() => {
const newIndex = walletsArray.length;
setWalletsArray((currentWallets) => [...currentWallets, defaultWalletObject]);
@@ -57,21 +57,36 @@ const InterfaceSettings = () => {
</div>
<div className={styles.setting}>
<label>
<input type='checkbox' checked={fitExpandedImagesToScreen} onChange={(e) => setFitExpandedImagesToScreen(e.target.checked)} />
<input
type='checkbox'
aria-label={capitalize(t('fit_expanded_images_to_screen'))}
checked={fitExpandedImagesToScreen}
onChange={(e) => setFitExpandedImagesToScreen(e.target.checked)}
/>
{capitalize(t('fit_expanded_images_to_screen'))}
</label>
<div className={styles.settingTip}>{capitalize(t('fit_expanded_images_to_screen_tip'))}</div>
</div>
<div className={styles.setting}>
<label>
<input type='checkbox' checked={unmuteExpandedVideoSound} onChange={(e) => setUnmuteExpandedVideoSound(e.target.checked)} />
<input
type='checkbox'
aria-label={capitalize(t('unmute_video_sound'))}
checked={unmuteExpandedVideoSound}
onChange={(e) => setUnmuteExpandedVideoSound(e.target.checked)}
/>
{capitalize(t('unmute_video_sound'))}
</label>
<div className={styles.settingTip}>{capitalize(t('unmute_video_sound_tip'))}</div>
</div>
<div className={styles.setting}>
<label>
<input type='checkbox' checked={enableInfiniteScroll} onChange={(e) => setEnableInfiniteScroll(e.target.checked)} />
<input
type='checkbox'
aria-label={capitalize(t('enable_infinite_scroll'))}
checked={enableInfiniteScroll}
onChange={(e) => setEnableInfiniteScroll(e.target.checked)}
/>
{capitalize(t('enable_infinite_scroll'))}
</label>
<div className={styles.settingTip}>{capitalize(t('enable_infinite_scroll_tip'))}</div>
@@ -32,13 +32,29 @@ const MediaHostingSettings = () => {
)}
<div className={styles.setting}>
<label>
<input type='radio' name={RADIO_NAME} value='random' checked={uploadMode === 'random'} onChange={() => setUploadMode('random')} disabled={isWeb} />
<input
type='radio'
aria-label={t('media_hosting_random')}
name={RADIO_NAME}
value='random'
checked={uploadMode === 'random'}
onChange={() => setUploadMode('random')}
disabled={isWeb}
/>
{t('media_hosting_random')}
</label>
</div>
<div className={styles.setting}>
<label>
<input type='radio' name={RADIO_NAME} value='preferred' checked={uploadMode === 'preferred'} onChange={() => setUploadMode('preferred')} disabled={isWeb} />
<input
type='radio'
aria-label={t('media_hosting_preferred')}
name={RADIO_NAME}
value='preferred'
checked={uploadMode === 'preferred'}
onChange={() => setUploadMode('preferred')}
disabled={isWeb}
/>
{t('media_hosting_preferred')}
</label>
{uploadMode === 'preferred' && (
@@ -51,6 +67,7 @@ const MediaHostingSettings = () => {
<label title={providerUnavailable ? t('media_hosting_provider_unavailable') : undefined}>
<input
type='radio'
aria-label={provider.label}
name={`${RADIO_NAME}-provider`}
value={provider.id}
checked={preferredProvider === provider.id}
@@ -72,7 +89,15 @@ const MediaHostingSettings = () => {
</div>
<div className={styles.setting}>
<label>
<input type='radio' name={RADIO_NAME} value='none' checked={uploadMode === 'none'} onChange={() => setUploadMode('none')} disabled={isWeb} />
<input
type='radio'
aria-label={t('media_hosting_none')}
name={RADIO_NAME}
value='none'
checked={uploadMode === 'none'}
onChange={() => setUploadMode('none')}
disabled={isWeb}
/>
{t('media_hosting_none')}
</label>
<div className={styles.settingTip}>{t('media_hosting_none_tip')}</div>
@@ -155,7 +155,7 @@ describe('P2PStatsSettings', () => {
// from libp2p observed/WebRTC addresses.
expect(fetch).toHaveBeenCalledWith('https://api.ipify.org?format=json', expect.objectContaining({ signal: expect.any(AbortSignal) }));
const yourIpRow = Array.from(container.querySelectorAll('tr')).find((row) => row.textContent?.includes('Your IP'));
expect(yourIpRow?.querySelector('[role="img"]')).not.toBeNull();
expect(yourIpRow?.querySelector('img[aria-label]')).not.toBeNull();
expect(container.textContent).not.toContain('browser Helia');
expect(container.textContent).not.toContain('seed mode');
expect(container.textContent).not.toContain('status');
@@ -325,7 +325,7 @@ describe('P2PStatsSettings', () => {
const marker = getMarkerByTitle('Your node - Da Nang, Da Nang City, VN');
expect(container.textContent).toContain('Leeching');
expect(rows.get('Your IP')).toContain('117.2.120.113');
expect(yourIpRow?.querySelector('[role="img"]')?.getAttribute('aria-label')).toBe('Vietnam');
expect(yourIpRow?.querySelector('img[aria-label]')?.getAttribute('aria-label')).toBe('Vietnam');
expect(marker?.getAttribute('data-peer-role')).toBe('leecher');
expect(Number(marker?.getAttribute('x'))).toBeCloseTo(286.72, 1);
expect(Number(marker?.getAttribute('y'))).toBeCloseTo(72.43, 1);
@@ -417,7 +417,7 @@ describe('P2PStatsSettings', () => {
const marker = getMarkerByTitle('Your node - VN');
expect(rows.get('Your IP')).toContain('104.28.68.171');
expect(rows.get('Your IP')).not.toContain('146.75.187.55');
expect(yourIpRow?.querySelector('[role="img"]')?.getAttribute('aria-label')).toBe('Vietnam');
expect(yourIpRow?.querySelector('img[aria-label]')?.getAttribute('aria-label')).toBe('Vietnam');
expect(marker).not.toBeNull();
expect(container.querySelector('svg rect title')?.textContent).not.toContain('Toronto');
expect(fetchMock).not.toHaveBeenCalledWith('https://free.freeipapi.com/api/json/146.75.187.55', expect.anything());
@@ -562,7 +562,7 @@ describe('P2PStatsSettings', () => {
expect(rows.get('Your IP')).toContain('104.28.68.171');
expect(rows.get('Your IP')).not.toContain('unknown');
const yourIpRow = Array.from(container.querySelectorAll('tr')).find((row) => row.textContent?.includes('Your IP'));
expect(yourIpRow?.querySelector('[role="img"]')).not.toBeNull();
expect(yourIpRow?.querySelector('img[aria-label]')).not.toBeNull();
expect(fetch).toHaveBeenCalledWith('https://api.ipify.org?format=json', expect.objectContaining({ signal: expect.any(AbortSignal) }));
});
@@ -140,6 +140,7 @@ type ObservedTransferStats = {
const KUBO_API_URL = 'http://localhost:50019/api/v0';
const SEEDER_REPO_URL = 'https://github.com/bitsocialnet/bitsocial-seeder';
const transparentPixelSrc = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="1" height="1"%3E%3C/svg%3E';
const STATS_REFRESH_MS = 5000;
const MAX_TRANSFER_COUNTER_DEPTH = 10;
const MAX_TRANSFER_COUNTER_OBJECTS = 400;
@@ -1028,10 +1029,11 @@ const NodeEndpointValue = ({ row }: { row: NodeEndpointStatRow }) => {
return (
<span className={styles.nodeEndpoint}>
{flagPosition && (
<span
<img
alt={countryLabel}
aria-label={countryLabel}
className={styles.peerFlag}
role='img'
src={transparentPixelSrc}
style={{ backgroundPosition: `-${flagPosition.x}px -${flagPosition.y}px` }}
title={countryLabel}
/>
@@ -1087,10 +1089,11 @@ const ConnectedPeersValue = ({ row }: { row: ConnectedPeersStatRow }) => (
</div>
<div className={styles.connectionAddressRow}>
{flagPosition && (
<span
<img
alt={getCountryLabel(countryCode)}
aria-label={getCountryLabel(countryCode)}
className={styles.peerFlag}
role='img'
src={transparentPixelSrc}
style={{ backgroundPosition: `-${flagPosition.x}px -${flagPosition.y}px` }}
title={getCountryLabel(countryCode)}
/>
@@ -1,4 +1,5 @@
.overlay {
all: unset;
position: fixed;
top: 0;
left: 0;
@@ -48,6 +49,8 @@
border-right: var(--settings-modal-border-right);
border-bottom: var(--settings-modal-border-bottom);
border-left: var(--settings-modal-border-left);
color: inherit;
margin: 0;
}
.settingsModal textarea {
@@ -74,6 +77,9 @@
}
.closeButton {
appearance: none;
background-color: transparent;
border: 0;
right: 5px;
width: 18px;
height: 18px;
@@ -84,6 +90,8 @@
background-image: var(--settings-modal-close-button-background-image);
background-position: center;
background-repeat: no-repeat;
margin: 0;
padding: 0;
}
.expandAllSettings {
@@ -91,11 +99,17 @@
text-transform: capitalize;
}
.expandAllSettings span {
.expandAllSettings span, .expandAllSettings button {
appearance: none;
background: transparent;
border: 0;
color: var(--button-desktop-text-color);
font: inherit;
margin: 0;
padding: 0;
}
.expandAllSettings span:hover {
.expandAllSettings span:hover, .expandAllSettings button:hover {
color: var(--button-desktop-text-color-hover);
cursor: pointer;
}
@@ -128,10 +142,23 @@
margin-bottom: 10px;
}
.category label {
.category label, .categoryButton {
font-weight: 700;
}
.categoryButton {
appearance: none;
background: transparent;
border: 0;
color: inherit;
cursor: pointer;
display: inline;
font: inherit;
font-weight: 700;
margin: 0;
padding: 0;
}
.subSectionHeader {
text-align: left;
font-weight: 700;
@@ -118,15 +118,15 @@ const SettingsModal = () => {
return (
<>
<div className={styles.overlay} role='button' tabIndex={0} onClick={closeModal} onKeyDown={handleKeyDown(closeModal)} />
<div className={styles.settingsModal} role='dialog' aria-modal='true' aria-labelledby='settings-modal-title'>
<button type='button' className={styles.overlay} aria-label={t('close')} tabIndex={0} onClick={closeModal} onKeyDown={handleKeyDown(closeModal)} />
<dialog open className={styles.settingsModal} aria-modal='true' aria-labelledby='settings-modal-title'>
<div className={styles.header}>
<span id='settings-modal-title' className={styles.title}>
{t('settings')}
</span>
<span
<button
type='button'
className={styles.closeButton}
role='button'
tabIndex={0}
title='close'
aria-label={t('close')}
@@ -136,30 +136,30 @@ const SettingsModal = () => {
</div>
<div className={styles.expandAllSettings}>
[
<span role='button' tabIndex={0} onClick={handleExpandAll} onKeyDown={handleKeyDown(handleExpandAll)}>
<button type='button' tabIndex={0} onClick={handleExpandAll} onKeyDown={handleKeyDown(handleExpandAll)}>
{allExpanded ? t('collapse_all_settings') : t('expand_all_settings')}
</span>
</button>
]
</div>
<div id='interface-settings' className={`${styles.setting} ${styles.category}`}>
<label onClick={() => handleCategoryClick('interface-settings')}>
<button type='button' className={styles.categoryButton} onClick={() => handleCategoryClick('interface-settings')}>
<span className={showInterfaceSettings ? styles.hideButton : styles.showButton} />
{t('interface')}
</label>
</button>
</div>
{showInterfaceSettings && <InterfaceSettings />}
<div id='media-hosting-settings' className={`${styles.setting} ${styles.category}`}>
<label onClick={() => handleCategoryClick('media-hosting-settings')}>
<button type='button' className={styles.categoryButton} onClick={() => handleCategoryClick('media-hosting-settings')}>
<span className={showMediaHostingSettings ? styles.hideButton : styles.showButton} />
{t('media_hosting')}
</label>
</button>
</div>
{showMediaHostingSettings && <MediaHostingSettings />}
<div id='account-settings' className={`${styles.setting} ${styles.category}`}>
<label onClick={() => handleCategoryClick('account-settings')}>
<button type='button' className={styles.categoryButton} onClick={() => handleCategoryClick('account-settings')}>
<span className={showAccountSettings ? styles.hideButton : styles.showButton} />
{t('bitsocial_account')}
</label>
</button>
</div>
{showAccountSettings && (
<>
@@ -171,38 +171,38 @@ const SettingsModal = () => {
</>
)}
<div id='subscriptions-settings' className={`${styles.setting} ${styles.category}`}>
<label onClick={() => handleCategoryClick('subscriptions-settings')}>
<button type='button' className={styles.categoryButton} onClick={() => handleCategoryClick('subscriptions-settings')}>
<span className={showSubscriptionsSettings ? styles.hideButton : styles.showButton} />
{t('board_subscriptions')}
</label>
</button>
</div>
{showSubscriptionsSettings && <SubscriptionsSetting />}
<div id='board-link-permissions-settings' className={`${styles.setting} ${styles.category}`}>
<label onClick={() => handleCategoryClick('board-link-permissions-settings')}>
<button type='button' className={styles.categoryButton} onClick={() => handleCategoryClick('board-link-permissions-settings')}>
<span className={showBoardLinkPermissionsSettings ? styles.hideButton : styles.showButton} />
{t('board_link_permissions')}
</label>
</button>
</div>
{showBoardLinkPermissionsSettings && <TrustedBoardLinksSetting />}
<div id='advanced-settings' className={`${styles.setting} ${styles.category}`}>
<label onClick={() => handleCategoryClick('advanced-settings')}>
<button type='button' className={styles.categoryButton} onClick={() => handleCategoryClick('advanced-settings')}>
<span className={showAdvancedSettings ? styles.hideButton : styles.showButton} />
{t('advanced_settings')}
</label>
</button>
</div>
{showAdvancedSettings && <AdvancedSettings />}
{sectionIds.includes(P2P_STATS_SECTION_ID) && (
<>
<div id={P2P_STATS_SECTION_ID} className={`${styles.setting} ${styles.category}`}>
<label onClick={() => handleCategoryClick(P2P_STATS_SECTION_ID)}>
<button type='button' className={styles.categoryButton} onClick={() => handleCategoryClick(P2P_STATS_SECTION_ID)}>
<span className={showP2PStatsSettings ? styles.hideButton : styles.showButton} />
{t('p2p_stats')}
</label>
</button>
</div>
{showP2PStatsSettings && <P2PStatsSettings />}
</>
)}
</div>
</dialog>
</>
);
};
@@ -47,7 +47,7 @@ const render = () => {
};
const getButtonByText = (text: string) => {
const button = Array.from(container.querySelectorAll('[role="button"]')).find((candidate) => (candidate.textContent ?? '').includes(text));
const button = Array.from(container.querySelectorAll('button')).find((candidate) => (candidate.textContent ?? '').includes(text));
if (!button) {
throw new Error(`Button containing "${text}" not found`);
}
@@ -12,6 +12,12 @@
}
.button {
appearance: none;
background: transparent;
border: 0;
font: inherit;
margin: 0;
padding: 0;
text-transform: capitalize;
color: var(--button-desktop-text-color);
text-decoration: var(--button-text-decoration);
@@ -26,6 +32,11 @@
cursor: pointer;
}
.button:focus-visible {
outline: 1px dotted currentcolor;
outline-offset: 1px;
}
.notSubscribed {
padding-bottom: 10px;
}
@@ -22,9 +22,9 @@ const SubscriptionButton = ({ address }: { address: string }) => {
return (
<span className={styles.subscriptionButton}>
[
<span
<button
type='button'
className={styles.button}
role='button'
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
@@ -35,7 +35,7 @@ const SubscriptionButton = ({ address }: { address: string }) => {
onClick={toggleSubscription}
>
{recentlyUnsubscribed || !subscribed ? t('subscribe') : t('unsubscribe')}
</span>
</button>
]
</span>
);
@@ -59,9 +59,9 @@ const SubscriptionsSetting = () => {
{subscriptions?.length > 1 && (
<div className={styles.unsubscribeAll}>
[
<span
<button
type='button'
className={styles.button}
role='button'
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
@@ -72,7 +72,7 @@ const SubscriptionsSetting = () => {
onClick={unsubscribeAll}
>
{t('unsubscribe_all')}
</span>
</button>
]
</div>
)}