mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
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:
@@ -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'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>
|
||||
|
||||
+4
-4
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user