A11y - fix anchors-as-buttons (#7444)

* add link_inline accessiblebutton variant

* valid anchors in SecurityRoomSettingsTab

Signed-off-by: Kerry Archibald <kerrya@element.io>

* new room intro link button

Signed-off-by: Kerry Archibald <kerrya@element.io>

* replace anchor with button in rerequest encryption keys message

Signed-off-by: Kerry Archibald <kerrya@element.io>

* inline button in UrlPreviewSettings

Signed-off-by: Kerry Archibald <kerrya@element.io>

* ButtonResetDefault mixin

Signed-off-by: Kerry Archibald <kerrya@element.io>

* inline link buttons in TextForEvent

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix anchors in InviteDialog

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix anchors in DevToolsDialog

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix anchors in login/registration/reset pword flows

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix types after fixing anchors in devtools

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix anchors in MemberEventListSummary

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix anchors in ReactionsRow and RoomUpgrade

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix anchors in ReplyChain

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix more anchors

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix anchors in auth comps

* stylelint fixes

Signed-off-by: Kerry Archibald <kerrya@element.io>

* remove ignore of jsx-a11y rule that is not added yet

Signed-off-by: Kerry Archibald <kerrya@element.io>

* devtools style important explainer

Signed-off-by: Kerry Archibald <kerrya@element.io>

* translate button alt in devtools dialog

Signed-off-by: Kerry Archibald <kerrya@element.io>

* AccessibleButton is reactionsrow

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix viewsourcevent button placement, use AccessibleButton

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use AccessibleButton in EventTile

Signed-off-by: Kerry Archibald <kerrya@element.io>

* unignore jsx-a11y/anchor-is-valid

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix lint issue in test jsx

Signed-off-by: Kerry Archibald <kerrya@element.io>

* update coment

Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
Kerry 2022-01-07 10:40:53 +01:00 committed by GitHub
parent 2b9eed5357
commit fed53a268b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 251 additions and 109 deletions

View file

@ -43,7 +43,6 @@ module.exports = {
// There are too many a11y violations to fix at once // There are too many a11y violations to fix at once
// Turn violated rules off until they are fixed // Turn violated rules off until they are fixed
"jsx-a11y/alt-text": "off", "jsx-a11y/alt-text": "off",
"jsx-a11y/anchor-is-valid": "off",
"jsx-a11y/aria-activedescendant-has-tabindex": "off", "jsx-a11y/aria-activedescendant-has-tabindex": "off",
"jsx-a11y/click-events-have-key-events": "off", "jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/iframe-has-title": "off", "jsx-a11y/iframe-has-title": "off",

View file

@ -423,9 +423,9 @@ legend {
* We should go through and have one consistent set of styles for buttons throughout the app. * We should go through and have one consistent set of styles for buttons throughout the app.
* For now, I am duplicating the selectors here for mx_Dialog and mx_DialogButtons. * For now, I am duplicating the selectors here for mx_Dialog and mx_DialogButtons.
*/ */
.mx_Dialog button:not(.mx_Dialog_nonDialogButton), .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton),
.mx_Dialog input[type="submit"], .mx_Dialog input[type="submit"],
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton), .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton),
.mx_Dialog_buttons input[type="submit"] { .mx_Dialog_buttons input[type="submit"] {
@mixin mx_DialogButton; @mixin mx_DialogButton;
margin-left: 0px; margin-left: 0px;
@ -440,20 +440,20 @@ legend {
font-family: inherit; font-family: inherit;
} }
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):last-child { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):last-child {
margin-right: 0px; margin-right: 0px;
} }
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):hover, .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):hover,
.mx_Dialog input[type="submit"]:hover, .mx_Dialog input[type="submit"]:hover,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):hover, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):hover,
.mx_Dialog_buttons input[type="submit"]:hover { .mx_Dialog_buttons input[type="submit"]:hover {
@mixin mx_DialogButton_hover; @mixin mx_DialogButton_hover;
} }
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):focus, .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus,
.mx_Dialog input[type="submit"]:focus, .mx_Dialog input[type="submit"]:focus,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus,
.mx_Dialog_buttons input[type="submit"]:focus { .mx_Dialog_buttons input[type="submit"]:focus {
filter: brightness($focus-brightness); filter: brightness($focus-brightness);
} }
@ -482,9 +482,9 @@ legend {
color: $alert; color: $alert;
} }
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):disabled, .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled,
.mx_Dialog input[type="submit"]:disabled, .mx_Dialog input[type="submit"]:disabled,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled,
.mx_Dialog_buttons input[type="submit"]:disabled { .mx_Dialog_buttons input[type="submit"]:disabled {
background-color: $light-fg-color; background-color: $light-fg-color;
border: solid 1px $light-fg-color; border: solid 1px $light-fg-color;
@ -655,3 +655,15 @@ legend {
outline-style: auto; outline-style: auto;
} }
} }
@define-mixin ButtonResetDefault {
appearance: none;
background: none;
border: none;
padding: 0;
margin: 0;
font-size: inherit;
font-family: inherit;
line-height: inherit;
cursor: pointer;
}

View file

@ -17,8 +17,9 @@ limitations under the License.
.mx_SetupEncryptionBody_reset { .mx_SetupEncryptionBody_reset {
color: $light-fg-color; color: $light-fg-color;
margin-top: $font-14px; margin-top: $font-14px;
}
a.mx_SetupEncryptionBody_reset_link:is(:link, :hover, :visited) {
color: $alert; .mx_SetupEncryptionBody_reset_link {
} @mixin ButtonResetDefault;
color: $alert;
} }

View file

@ -257,3 +257,10 @@ limitations under the License.
margin-bottom: 8px; margin-bottom: 8px;
} }
} }
.mx_DevTools_SettingsExplorer_setting {
// override default link button color
// as it is the same as the background highlight
// used on focus
color: $links !important;
}

View file

@ -107,6 +107,16 @@ limitations under the License.
opacity: 0.4; opacity: 0.4;
} }
.mx_AccessibleButton_kind_link_inline {
color: $accent;
font-size: inherit;
padding: 0 2px;
}
.mx_AccessibleButton_kind_link_inline.mx_AccessibleButton_disabled {
opacity: 0.4;
}
.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_link_sm { .mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_link_sm {
padding: 5px 12px; padding: 5px 12px;
color: $accent; color: $accent;

View file

@ -24,7 +24,12 @@ limitations under the License.
border-radius: 2px; border-radius: 2px;
.mx_ReplyChain_show { .mx_ReplyChain_show {
cursor: pointer; @mixin ButtonResetDefault;
color: inherit;
&:hover {
color: $links;
}
} }
&.mx_ReplyChain_color1 { &.mx_ReplyChain_color1 {

View file

@ -57,15 +57,13 @@ limitations under the License.
} }
.mx_ReactionsRow_showAll { .mx_ReactionsRow_showAll {
@mixin ButtonResetDefault;
text-decoration: none; text-decoration: none;
font-size: $font-12px; font-size: $font-12px;
line-height: $font-20px; line-height: $font-20px;
margin-left: 4px; margin-left: 4px;
vertical-align: middle; vertical-align: middle;
color: $tertiary-content;
&:link, &:visited {
color: $tertiary-content;
}
&:hover { &:hover {
color: $primary-content; color: $primary-content;

View file

@ -18,6 +18,7 @@ limitations under the License.
display: flex; display: flex;
opacity: 0.6; opacity: 0.6;
font-size: $font-12px; font-size: $font-12px;
width: 100%;
pre, code { pre, code {
flex: 1; flex: 1;
@ -29,11 +30,15 @@ limitations under the License.
} }
.mx_ViewSourceEvent_toggle { .mx_ViewSourceEvent_toggle {
visibility: hidden;
// override styles from AccessibleButton
border-radius: 0;
padding: 0;
// icon
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: 0 center; mask-position: 0 center;
mask-size: auto 12px; mask-size: auto 12px;
width: 12px; width: 12px;
visibility: hidden;
background-color: $accent; background-color: $accent;
mask-image: url("$(res)/img/element-icons/maximise-expand.svg"); mask-image: url("$(res)/img/element-icons/maximise-expand.svg");
} }

View file

@ -607,7 +607,8 @@ $left-gutter: 64px;
opacity: 0.5; opacity: 0.5;
} }
.mx_EventTile_keyRequestInfo_text a { .mx_EventTile_keyRequestInfo_text .mx_AccessibleButton {
@mixin ButtonResetDefault;
color: $primary-content; color: $primary-content;
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;

View file

@ -32,6 +32,7 @@ import { Action } from './dispatcher/actions';
import defaultDispatcher from './dispatcher/dispatcher'; import defaultDispatcher from './dispatcher/dispatcher';
import { MatrixClientPeg } from "./MatrixClientPeg"; import { MatrixClientPeg } from "./MatrixClientPeg";
import { ROOM_SECURITY_TAB } from "./components/views/dialogs/RoomSettingsDialog"; import { ROOM_SECURITY_TAB } from "./components/views/dialogs/RoomSettingsDialog";
import AccessibleButton from './components/views/elements/AccessibleButton';
import RightPanelStore from './stores/right-panel/RightPanelStore'; import RightPanelStore from './stores/right-panel/RightPanelStore';
// These functions are frequently used just to check whether an event has // These functions are frequently used just to check whether an event has
@ -229,9 +230,9 @@ function textForJoinRulesEvent(ev: MatrixEvent, allowJSX: boolean): () => string
{ _t('%(senderDisplayName)s changed who can join this room. <a>View settings</a>.', { { _t('%(senderDisplayName)s changed who can join this room. <a>View settings</a>.', {
senderDisplayName, senderDisplayName,
}, { }, {
"a": (sub) => <a onClick={onViewJoinRuleSettingsClick}> "a": (sub) => <AccessibleButton kind='link_inline' onClick={onViewJoinRuleSettingsClick}>
{ sub } { sub }
</a>, </AccessibleButton>,
}) } }) }
</span>; </span>;
} }
@ -528,13 +529,13 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string
{ senderName }, { senderName },
{ {
"a": (sub) => "a": (sub) =>
<a onClick={(e) => onPinnedOrUnpinnedMessageClick(messageId, roomId)}> <AccessibleButton kind='link_inline' onClick={(e) => onPinnedOrUnpinnedMessageClick(messageId, roomId)}>
{ sub } { sub }
</a>, </AccessibleButton>,
"b": (sub) => "b": (sub) =>
<a onClick={onPinnedMessagesClick}> <AccessibleButton kind='link_inline' onClick={onPinnedMessagesClick}>
{ sub } { sub }
</a>, </AccessibleButton>,
}, },
) } ) }
</span> </span>
@ -556,13 +557,13 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string
{ senderName }, { senderName },
{ {
"a": (sub) => "a": (sub) =>
<a onClick={(e) => onPinnedOrUnpinnedMessageClick(messageId, roomId)}> <AccessibleButton kind='link_inline' onClick={(e) => onPinnedOrUnpinnedMessageClick(messageId, roomId)}>
{ sub } { sub }
</a>, </AccessibleButton>,
"b": (sub) => "b": (sub) =>
<a onClick={onPinnedMessagesClick}> <AccessibleButton kind='link_inline' onClick={onPinnedMessagesClick}>
{ sub } { sub }
</a>, </AccessibleButton>,
}, },
) } ) }
</span> </span>
@ -578,7 +579,12 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string
{ _t( { _t(
"%(senderName)s changed the <a>pinned messages</a> for the room.", "%(senderName)s changed the <a>pinned messages</a> for the room.",
{ senderName }, { senderName },
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> }, {
"a": (sub) =>
<AccessibleButton kind='link_inline' onClick={onPinnedMessagesClick}>
{ sub }
</AccessibleButton>,
},
) } ) }
</span> </span>
); );

View file

@ -2162,9 +2162,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
<div className="mx_MatrixChat_splash"> <div className="mx_MatrixChat_splash">
{ errorBox } { errorBox }
<Spinner /> <Spinner />
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}> <AccessibleButton kind='link_inline' className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
{ _t('Logout') } { _t('Logout') }
</a> </AccessibleButton>
</div> </div>
); );
} }

View file

@ -38,6 +38,7 @@ import ErrorDialog from "../../views/dialogs/ErrorDialog";
import AuthHeader from "../../views/auth/AuthHeader"; import AuthHeader from "../../views/auth/AuthHeader";
import AuthBody from "../../views/auth/AuthBody"; import AuthBody from "../../views/auth/AuthBody";
import PassphraseConfirmField from "../../views/auth/PassphraseConfirmField"; import PassphraseConfirmField from "../../views/auth/PassphraseConfirmField";
import AccessibleButton from '../../views/elements/AccessibleButton';
enum Phase { enum Phase {
// Show the forgot password inputs // Show the forgot password inputs
@ -338,9 +339,9 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
value={_t('Send Reset Email')} value={_t('Send Reset Email')}
/> />
</form> </form>
<a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#"> <AccessibleButton kind='link_inline' className="mx_AuthBody_changeFlow" onClick={this.onLoginClick}>
{ _t('Sign in instead') } { _t('Sign in instead') }
</a> </AccessibleButton>
</div>; </div>;
} }

View file

@ -38,6 +38,7 @@ import ServerPicker from "../../views/elements/ServerPicker";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import AuthBody from "../../views/auth/AuthBody"; import AuthBody from "../../views/auth/AuthBody";
import AuthHeader from "../../views/auth/AuthHeader"; import AuthHeader from "../../views/auth/AuthHeader";
import AccessibleButton from '../../views/elements/AccessibleButton';
// These are used in several places, and come from the js-sdk's autodiscovery // These are used in several places, and come from the js-sdk's autodiscovery
// stuff. We define them here so that they'll be picked up by i18n. // stuff. We define them here so that they'll be picked up by i18n.
@ -588,7 +589,10 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
footer = ( footer = (
<span className="mx_AuthBody_changeFlow"> <span className="mx_AuthBody_changeFlow">
{ _t("New? <a>Create account</a>", {}, { { _t("New? <a>Create account</a>", {}, {
a: sub => <a onClick={this.onTryRegisterClick} href="#">{ sub }</a>, a: sub =>
<AccessibleButton kind='link_inline' onClick={this.onTryRegisterClick}>
{ sub }
</AccessibleButton>,
}) } }) }
</span> </span>
); );

View file

@ -538,16 +538,20 @@ export default class Registration extends React.Component<IProps, IState> {
const signIn = <span className="mx_AuthBody_changeFlow"> const signIn = <span className="mx_AuthBody_changeFlow">
{ _t("Already have an account? <a>Sign in here</a>", {}, { { _t("Already have an account? <a>Sign in here</a>", {}, {
a: sub => <a onClick={this.onLoginClick} href="#">{ sub }</a>, a: sub => <AccessibleButton kind='link_inline' onClick={this.onLoginClick}>{ sub }</AccessibleButton>,
}) } }) }
</span>; </span>;
// Only show the 'go back' button if you're not looking at the form // Only show the 'go back' button if you're not looking at the form
let goBack; let goBack;
if (this.state.doingUIAuth) { if (this.state.doingUIAuth) {
goBack = <a className="mx_AuthBody_changeFlow" onClick={this.onGoToFormClicked} href="#"> goBack = <AccessibleButton
kind='link_inline'
className="mx_AuthBody_changeFlow"
onClick={this.onGoToFormClicked}
>
{ _t('Go back') } { _t('Go back') }
</a>; </AccessibleButton>;
} }
let body; let body;

View file

@ -121,7 +121,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
store.returnAfterSkip(); store.returnAfterSkip();
}; };
private onResetClick = (ev: React.MouseEvent<HTMLAnchorElement>) => { private onResetClick = (ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault(); ev.preventDefault();
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
store.reset(); store.reset();
@ -214,10 +214,9 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
</div> </div>
<div className="mx_SetupEncryptionBody_reset"> <div className="mx_SetupEncryptionBody_reset">
{ _t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, { { _t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, {
a: (sub) => <a a: (sub) => <button
href=""
onClick={this.onResetClick} onClick={this.onResetClick}
className="mx_SetupEncryptionBody_reset_link">{ sub }</a>, className="mx_SetupEncryptionBody_reset_link">{ sub }</button>,
}) } }) }
</div> </div>
</div> </div>

View file

@ -755,7 +755,7 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
@replaceableComponent("views.auth.FallbackAuthEntry") @replaceableComponent("views.auth.FallbackAuthEntry")
export class FallbackAuthEntry extends React.Component<IAuthEntryProps> { export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
private popupWindow: Window; private popupWindow: Window;
private fallbackButton = createRef<HTMLAnchorElement>(); private fallbackButton = createRef<HTMLButtonElement>();
constructor(props) { constructor(props) {
super(props); super(props);
@ -814,9 +814,9 @@ export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
} }
return ( return (
<div> <div>
<a href="" ref={this.fallbackButton} onClick={this.onShowFallbackClick}>{ <AccessibleButton kind='link_inline' inputRef={this.fallbackButton} onClick={this.onShowFallbackClick}>{
_t("Start authentication") _t("Start authentication")
}</a> }</AccessibleButton>
{ errorSection } { errorSection }
</div> </div>
); );

View file

@ -37,6 +37,7 @@ import AddressSelector from '../elements/AddressSelector';
import AddressTile from '../elements/AddressTile'; import AddressTile from '../elements/AddressTile';
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import AccessibleButton from '../elements/AccessibleButton';
const TRUNCATE_QUERY_LIST = 40; const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -712,8 +713,14 @@ export default class AddressPickerDialog extends React.Component<IProps, IState>
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
}, },
{ {
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{ sub }</a>, default: sub => (
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>, <AccessibleButton kind="link_inline" onClick={this.onUseDefaultIdentityServerClick}>
{ sub }
</AccessibleButton>
),
settings: sub => <AccessibleButton kind="link_inline" onClick={this.onManageSettingsClick}>
{ sub }
</AccessibleButton>,
}, },
) }</div>; ) }</div>;
} else { } else {
@ -721,7 +728,9 @@ export default class AddressPickerDialog extends React.Component<IProps, IState>
"Use an identity server to invite by email. " + "Use an identity server to invite by email. " +
"Manage in <settings>Settings</settings>.", "Manage in <settings>Settings</settings>.",
{}, { {}, {
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>, settings: sub => <AccessibleButton kind="link_inline" onClick={this.onManageSettingsClick}>
{ sub }
</AccessibleButton>,
}, },
) }</div>; ) }</div>;
} }

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { useState, useEffect, ChangeEvent, MouseEvent } from 'react'; import React, { useState, useEffect, ChangeEvent } from 'react';
import { import {
PHASE_UNSENT, PHASE_UNSENT,
PHASE_REQUESTED, PHASE_REQUESTED,
@ -44,6 +44,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import { SettingLevel } from '../../../settings/SettingLevel'; import { SettingLevel } from '../../../settings/SettingLevel';
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import TruncatedList from "../elements/TruncatedList"; import TruncatedList from "../elements/TruncatedList";
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
interface IGenericEditorProps { interface IGenericEditorProps {
onBack: () => void; onBack: () => void;
@ -965,12 +966,12 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
} }
}; };
private onViewClick = (ev: MouseEvent, settingId: string) => { private onViewClick = (ev: ButtonEvent, settingId: string) => {
ev.preventDefault(); ev.preventDefault();
this.setState({ viewSetting: settingId }); this.setState({ viewSetting: settingId });
}; };
private onEditClick = (ev: MouseEvent, settingId: string) => { private onEditClick = (ev: ButtonEvent, settingId: string) => {
ev.preventDefault(); ev.preventDefault();
this.setState({ this.setState({
editSetting: settingId, editSetting: settingId,
@ -1078,16 +1079,16 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
{ allSettings.map(i => ( { allSettings.map(i => (
<tr key={i}> <tr key={i}>
<td> <td>
<a href="" onClick={(e) => this.onViewClick(e, i)}> <AccessibleButton kind='link_inline' className='mx_DevTools_SettingsExplorer_setting' onClick={(e) => this.onViewClick(e, i)}>
<code>{ i }</code> <code>{ i }</code>
</a> </AccessibleButton>
<a <AccessibleButton
href="" alt={_t('Edit setting')}
onClick={(e) => this.onEditClick(e, i)} onClick={(e) => this.onEditClick(e, i)}
className='mx_DevTools_SettingsExplorer_edit' className='mx_DevTools_SettingsExplorer_edit'
> >
</a> </AccessibleButton>
</td> </td>
<td> <td>
<code>{ this.renderSettingValue(SettingsStore.getValue(i)) }</code> <code>{ this.renderSettingValue(SettingsStore.getValue(i)) }</code>

View file

@ -1255,8 +1255,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
}, },
{ {
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{ sub }</a>, default: sub =>
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>, <AccessibleButton kind='link_inline' onClick={this.onUseDefaultIdentityServerClick}>
{ sub }
</AccessibleButton>,
settings: sub =>
<AccessibleButton kind='link_inline' onClick={this.onManageSettingsClick}>
{ sub }
</AccessibleButton>,
}, },
) }</div> ) }</div>
); );
@ -1266,7 +1272,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
"Use an identity server to invite by email. " + "Use an identity server to invite by email. " +
"Manage in <settings>Settings</settings>.", "Manage in <settings>Settings</settings>.",
{}, { {}, {
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>, settings: sub =>
<AccessibleButton kind='link_inline' onClick={this.onManageSettingsClick}>
{ sub }
</AccessibleButton>,
}, },
) }</div> ) }</div>
); );

View file

@ -29,6 +29,7 @@ import BugReportDialog from './BugReportDialog';
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import ProgressBar from "../elements/ProgressBar"; import ProgressBar from "../elements/ProgressBar";
import AccessibleButton from '../elements/AccessibleButton';
export interface IFinishedOpts { export interface IFinishedOpts {
continue: boolean; continue: boolean;
@ -135,7 +136,9 @@ export default class RoomUpgradeWarningDialog extends React.Component<IProps, IS
}, },
{ {
"a": (sub) => { "a": (sub) => {
return <a href='#' onClick={this.openBugReportDialog}>{ sub }</a>; return <AccessibleButton kind='link_inline' onClick={this.openBugReportDialog}>
{ sub }
</AccessibleButton>;
}, },
}, },
) } ) }

View file

@ -24,6 +24,7 @@ import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import BugReportDialog from "./BugReportDialog"; import BugReportDialog from "./BugReportDialog";
import { IDialogProps } from "./IDialogProps"; import { IDialogProps } from "./IDialogProps";
import AccessibleButton from '../elements/AccessibleButton';
interface IProps extends IDialogProps { } interface IProps extends IDialogProps { }
@ -45,7 +46,9 @@ export default class StorageEvictedDialog extends React.Component<IProps> {
"To help us prevent this in future, please <a>send us logs</a>.", "To help us prevent this in future, please <a>send us logs</a>.",
{}, {},
{ {
a: text => <a href="#" onClick={this.sendBugReport}>{ text }</a>, a: text => <AccessibleButton kind='link_inline' onClick={this.sendBugReport}>
{ text }
</AccessibleButton>,
}, },
); );
} }

View file

@ -287,10 +287,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
const resetButton = ( const resetButton = (
<div className="mx_AccessSecretStorageDialog_reset"> <div className="mx_AccessSecretStorageDialog_reset">
{ _t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, { { _t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, {
a: (sub) => <a a: (sub) => <AccessibleButton
href="" kind="link_inline"
onClick={this.onResetAllClick} onClick={this.onResetAllClick}
className="mx_AccessSecretStorageDialog_reset_link">{ sub }</a>, className="mx_AccessSecretStorageDialog_reset_link">{ sub }</AccessibleButton>,
}) } }) }
</div> </div>
); );

View file

@ -21,6 +21,19 @@ import { Key } from '../../../Keyboard';
export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element> | React.FormEvent<Element>; export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element> | React.FormEvent<Element>;
type AccessibleButtonKind = | 'primary'
| 'primary_outline'
| 'primary_sm'
| 'secondary'
| 'danger'
| 'danger_outline'
| 'danger_sm'
| 'link'
| 'link_inline'
| 'link_sm'
| 'confirm_sm'
| 'cancel_sm';
/** /**
* children: React's magic prop. Represents all children given to the element. * children: React's magic prop. Represents all children given to the element.
* element: (optional) The base element type. "div" by default. * element: (optional) The base element type. "div" by default.
@ -32,7 +45,7 @@ interface IProps extends React.InputHTMLAttributes<Element> {
element?: keyof ReactHTML; element?: keyof ReactHTML;
// The kind of button, similar to how Bootstrap works. // The kind of button, similar to how Bootstrap works.
// See available classes for AccessibleButton for options. // See available classes for AccessibleButton for options.
kind?: string; kind?: AccessibleButtonKind | string;
// The ARIA role // The ARIA role
role?: string; role?: string;
// The tabIndex // The tabIndex

View file

@ -23,6 +23,7 @@ import SdkConfig from "../../../SdkConfig";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { UserTab } from "../dialogs/UserSettingsDialog"; import { UserTab } from "../dialogs/UserSettingsDialog";
import AccessibleButton from "./AccessibleButton";
export enum WarningKind { export enum WarningKind {
Files, Files,
@ -41,15 +42,19 @@ export default function DesktopBuildsNotice({ isRoomEncrypted, kind }: IProps) {
if (EventIndexPeg.error) { if (EventIndexPeg.error) {
return <> return <>
{ _t("Message search initialisation failed, check <a>your settings</a> for more information", {}, { { _t("Message search initialisation failed, check <a>your settings</a> for more information", {}, {
a: sub => (<a onClick={(evt) => { a: sub => (
evt.preventDefault(); <AccessibleButton
dis.dispatch({ kind="link_inline"
action: Action.ViewUserSettings, onClick={(evt) => {
initialTabId: UserTab.Security, evt.preventDefault();
}); dis.dispatch({
}}> action: Action.ViewUserSettings,
{ sub } initialTabId: UserTab.Security,
</a>), });
}}
>
{ sub }
</AccessibleButton>),
}) } }) }
</>; </>;
} }

View file

@ -30,6 +30,7 @@ import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePha
import { jsxJoin } from '../../../utils/ReactUtils'; import { jsxJoin } from '../../../utils/ReactUtils';
import { Layout } from '../../../settings/enums/Layout'; import { Layout } from '../../../settings/enums/Layout';
import RightPanelStore from '../../../stores/right-panel/RightPanelStore'; import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
import AccessibleButton from './AccessibleButton';
const onPinnedMessagesClick = (): void => { const onPinnedMessagesClick = (): void => {
RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false); RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false);
@ -322,10 +323,18 @@ export default class MemberEventListSummary extends React.Component<IProps> {
res = (userCount > 1) res = (userCount > 1)
? _t("%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.", ? _t("%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
{ severalUsers: "", count: repeats }, { severalUsers: "", count: repeats },
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> }) {
"a": (sub) => <AccessibleButton kind='link_inline' onClick={onPinnedMessagesClick}>
{ sub }
</AccessibleButton>,
})
: _t("%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.", : _t("%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
{ oneUser: "", count: repeats }, { oneUser: "", count: repeats },
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> }); {
"a": (sub) => <AccessibleButton kind='link_inline' onClick={onPinnedMessagesClick}>
{ sub }
</AccessibleButton>,
});
break; break;
} }

View file

@ -37,6 +37,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from './Spinner'; import Spinner from './Spinner';
import ReplyTile from "../rooms/ReplyTile"; import ReplyTile from "../rooms/ReplyTile";
import Pill from './Pill'; import Pill from './Pill';
import { ButtonEvent } from './AccessibleButton';
/** /**
* This number is based on the previous behavior - if we have message of height * This number is based on the previous behavior - if we have message of height
@ -344,7 +345,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
this.initialize(); this.initialize();
}; };
private onQuoteClick = async (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>): Promise<void> => { private onQuoteClick = async (event: ButtonEvent): Promise<void> => {
const events = [this.state.loadedEv, ...this.state.events]; const events = [this.state.loadedEv, ...this.state.events];
let loadedEv = null; let loadedEv = null;
@ -380,7 +381,11 @@ export default class ReplyChain extends React.Component<IProps, IState> {
header = <blockquote className={`mx_ReplyChain ${this.getReplyChainColorClass(ev)}`}> header = <blockquote className={`mx_ReplyChain ${this.getReplyChainColorClass(ev)}`}>
{ {
_t('<a>In reply to</a> <pill>', {}, { _t('<a>In reply to</a> <pill>', {}, {
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_ReplyChain_show">{ sub }</a>, 'a': (sub) => (
<button onClick={this.onQuoteClick} className="mx_ReplyChain_show">
{ sub }
</button>
),
'pill': ( 'pill': (
<Pill <Pill
type={Pill.TYPE_USER_MENTION} type={Pill.TYPE_USER_MENTION}

View file

@ -258,7 +258,9 @@ export default class MFileBody extends React.Component<IProps, IState> {
* We'll use it to learn how the download link * We'll use it to learn how the download link
* would have been styled if it was rendered inline. * would have been styled if it was rendered inline.
*/ } */ }
{ /* eslint-disable-next-line jsx-a11y/anchor-has-content */ } { /* this violates multiple eslint rules
so ignore it completely */ }
{ /* eslint-disable-next-line */ }
<a ref={this.dummyLink} /> <a ref={this.dummyLink} />
</div> </div>
{ /* { /*

View file

@ -19,6 +19,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from '../elements/AccessibleButton';
interface IProps { interface IProps {
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
@ -40,7 +41,11 @@ export default class MjolnirBody extends React.Component<IProps> {
return ( return (
<div className='mx_MjolnirBody'><i>{ _t( <div className='mx_MjolnirBody'><i>{ _t(
"You have ignored this user, so their message is hidden. <a>Show anyways.</a>", "You have ignored this user, so their message is hidden. <a>Show anyways.</a>",
{}, { a: (sub) => <a href="#" onClick={this.onAllowClick}>{ sub }</a> }, {}, {
a: (sub) => <AccessibleButton kind="link_inline" onClick={this.onAllowClick}>
{ sub }
</AccessibleButton>,
},
) }</i></div> ) }</i></div>
); );
} }

View file

@ -27,6 +27,7 @@ import ContextMenu, { aboveLeftOf, useContextMenu } from "../../structures/Conte
import ReactionPicker from "../emojipicker/ReactionPicker"; import ReactionPicker from "../emojipicker/ReactionPicker";
import ReactionsRowButton from "./ReactionsRowButton"; import ReactionsRowButton from "./ReactionsRowButton";
import RoomContext from "../../../contexts/RoomContext"; import RoomContext from "../../../contexts/RoomContext";
import AccessibleButton from "../elements/AccessibleButton";
// The maximum number of reactions to initially show on a message. // The maximum number of reactions to initially show on a message.
const MAX_ITEMS_WHEN_LIMITED = 8; const MAX_ITEMS_WHEN_LIMITED = 8;
@ -201,13 +202,13 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
let showAllButton; let showAllButton;
if ((items.length > MAX_ITEMS_WHEN_LIMITED + 1) && !showAll) { if ((items.length > MAX_ITEMS_WHEN_LIMITED + 1) && !showAll) {
items = items.slice(0, MAX_ITEMS_WHEN_LIMITED); items = items.slice(0, MAX_ITEMS_WHEN_LIMITED);
showAllButton = <a showAllButton = <AccessibleButton
kind="link_inline"
className="mx_ReactionsRow_showAll" className="mx_ReactionsRow_showAll"
href="#"
onClick={this.onShowAllClick} onClick={this.onShowAllClick}
> >
{ _t("Show all") } { _t("Show all") }
</a>; </AccessibleButton>;
} }
let addReactionButton; let addReactionButton;

View file

@ -45,6 +45,7 @@ import EditMessageComposer from '../rooms/EditMessageComposer';
import LinkPreviewGroup from '../rooms/LinkPreviewGroup'; import LinkPreviewGroup from '../rooms/LinkPreviewGroup';
import { IBodyProps } from "./IBodyProps"; import { IBodyProps } from "./IBodyProps";
import RoomContext from "../../../contexts/RoomContext"; import RoomContext from "../../../contexts/RoomContext";
import AccessibleButton from '../elements/AccessibleButton';
const MAX_HIGHLIGHT_LENGTH = 4096; const MAX_HIGHLIGHT_LENGTH = 4096;
@ -529,9 +530,9 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
if (this.props.highlightLink) { if (this.props.highlightLink) {
body = <a href={this.props.highlightLink}>{ body }</a>; body = <a href={this.props.highlightLink}>{ body }</a>;
} else if (content.data && typeof content.data["org.matrix.neb.starter_link"] === "string") { } else if (content.data && typeof content.data["org.matrix.neb.starter_link"] === "string") {
body = <a href="#" body = <AccessibleButton kind="link_inline"
onClick={this.onStarterLinkClick.bind(this, content.data["org.matrix.neb.starter_link"])} onClick={this.onStarterLinkClick.bind(this, content.data["org.matrix.neb.starter_link"])}
>{ body }</a>; >{ body }</AccessibleButton>;
} }
let widgets; let widgets;

View file

@ -23,6 +23,7 @@ import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import BugReportDialog from '../dialogs/BugReportDialog'; import BugReportDialog from '../dialogs/BugReportDialog';
import AccessibleButton from '../elements/AccessibleButton';
interface IProps { interface IProps {
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
@ -67,9 +68,9 @@ export default class TileErrorBoundary extends React.Component<IProps, IState> {
let submitLogsButton; let submitLogsButton;
if (SdkConfig.get().bug_report_endpoint_url) { if (SdkConfig.get().bug_report_endpoint_url) {
submitLogsButton = <a onClick={this.onBugReport} href="#"> submitLogsButton = <AccessibleButton kind="link_inline" onClick={this.onBugReport}>
{ _t("Submit logs") } { _t("Submit logs") }
</a>; </AccessibleButton>;
} }
return (<div className={classNames(classes)}> return (<div className={classNames(classes)}>

View file

@ -21,6 +21,7 @@ import classNames from 'classnames';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
interface IProps { interface IProps {
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
@ -76,7 +77,8 @@ export default class ViewSourceEvent extends React.PureComponent<IProps, IState>
return <span className={classes}> return <span className={classes}>
{ content } { content }
<button <AccessibleButton
kind='link_inline'
title={_t('toggle event')} title={_t('toggle event')}
className="mx_ViewSourceEvent_toggle" className="mx_ViewSourceEvent_toggle"
onClick={this.onToggle} onClick={this.onToggle}

View file

@ -29,6 +29,7 @@ import { SettingLevel } from "../../../settings/SettingLevel";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import SettingsFlag from "../elements/SettingsFlag"; import SettingsFlag from "../elements/SettingsFlag";
import SettingsFieldset from '../settings/SettingsFieldset'; import SettingsFieldset from '../settings/SettingsFieldset';
import AccessibleButton from '../elements/AccessibleButton';
interface IProps { interface IProps {
room: Room; room: Room;
@ -55,13 +56,21 @@ export default class UrlPreviewSettings extends React.Component<IProps> {
if (accountEnabled) { if (accountEnabled) {
previewsForAccount = ( previewsForAccount = (
_t("You have <a>enabled</a> URL previews by default.", {}, { _t("You have <a>enabled</a> URL previews by default.", {}, {
'a': (sub)=><a onClick={this.onClickUserSettings} href=''>{ sub }</a>, 'a': (sub) => <AccessibleButton
kind='link_inline'
onClick={this.onClickUserSettings}>
{ sub }
</AccessibleButton>,
}) })
); );
} else { } else {
previewsForAccount = ( previewsForAccount = (
_t("You have <a>disabled</a> URL previews by default.", {}, { _t("You have <a>disabled</a> URL previews by default.", {}, {
'a': (sub)=><a onClick={this.onClickUserSettings} href=''>{ sub }</a>, 'a': (sub) => <AccessibleButton
kind='link_inline'
onClick={this.onClickUserSettings}>
{ sub }
</AccessibleButton>,
}) })
); );
} }

View file

@ -72,6 +72,7 @@ import { ThreadNotificationState } from '../../../stores/notifications/ThreadNot
import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNotificationStateStore'; import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNotificationStateStore';
import { NotificationStateEvents } from '../../../stores/notifications/NotificationState'; import { NotificationStateEvents } from '../../../stores/notifications/NotificationState';
import { NotificationColor } from '../../../stores/notifications/NotificationColor'; import { NotificationColor } from '../../../stores/notifications/NotificationColor';
import AccessibleButton from '../elements/AccessibleButton';
import { CardContext } from '../right_panel/BaseCard'; import { CardContext } from '../right_panel/BaseCard';
const eventTileTypes = { const eventTileTypes = {
@ -1262,7 +1263,17 @@ export default class EventTile extends React.Component<IProps, IState> {
_t( _t(
'<requestLink>Re-request encryption keys</requestLink> from your other sessions.', '<requestLink>Re-request encryption keys</requestLink> from your other sessions.',
{}, {},
{ 'requestLink': (sub) => <a tabIndex={0} onClick={this.onRequestKeysClick}>{ sub }</a> }, {
'requestLink': (sub) =>
<AccessibleButton
className="mx_EventTile_rerequestKeysCta"
kind='link_inline'
tabIndex={0}
onClick={this.onRequestKeysClick}
>
{ sub }
</AccessibleButton>,
},
); );
const keyRequestInfo = isEncryptionFailure && !isRedacted ? const keyRequestInfo = isEncryptionFailure && !isRedacted ?

View file

@ -207,7 +207,7 @@ const NewRoomIntro = () => {
let subButton; let subButton;
if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.get())) { if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.get())) {
subButton = ( subButton = (
<a onClick={openRoomSettings} href="#"> { _t("Enable encryption in settings.") }</a> <AccessibleButton kind='link_inline' onClick={openRoomSettings}>{ _t("Enable encryption in settings.") }</AccessibleButton>
); );
} }

View file

@ -37,6 +37,7 @@ import CreateRoomDialog from '../../../dialogs/CreateRoomDialog';
import JoinRuleSettings from "../../JoinRuleSettings"; import JoinRuleSettings from "../../JoinRuleSettings";
import ErrorDialog from "../../../dialogs/ErrorDialog"; import ErrorDialog from "../../../dialogs/ErrorDialog";
import SettingsFieldset from '../../SettingsFieldset'; import SettingsFieldset from '../../SettingsFieldset';
import ExternalLink from '../../../elements/ExternalLink';
interface IProps { interface IProps {
roomId: string; roomId: string;
@ -139,12 +140,13 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
"To avoid these issues, create a <a>new encrypted room</a> for " + "To avoid these issues, create a <a>new encrypted room</a> for " +
"the conversation you plan to have.", "the conversation you plan to have.",
null, null,
{ "a": (sub) => <a {
className="mx_linkButton" "a": (sub) => <AccessibleButton kind='link_inline'
onClick={() => { onClick={() => {
dialog.close(); dialog.close();
this.createNewRoom(false, true); this.createNewRoom(false, true);
}}> { sub } </a> }, }}> { sub } </AccessibleButton>,
},
) } </p> ) } </p>
</div>, </div>,
@ -163,11 +165,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
"may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>", "may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>",
{}, {},
{ {
a: sub => <a a: sub => <ExternalLink
href="https://element.io/help#encryption" href="https://element.io/help#encryption"
rel="noreferrer noopener" >{ sub }</ExternalLink>,
target="_blank"
>{ sub }</a>,
}, },
), ),
onFinished: (confirm) => { onFinished: (confirm) => {
@ -306,12 +306,12 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
"you plan to have.", "you plan to have.",
null, null,
{ {
"a": (sub) => <a "a": (sub) => <AccessibleButton
className="mx_linkButton" kind='link_inline'
onClick={() => { onClick={() => {
dialog.close(); dialog.close();
this.createNewRoom(true, false); this.createNewRoom(true, false);
}}> { sub } </a>, }}> { sub } </AccessibleButton>,
}, },
) } </p> ) } </p>
</div>, </div>,

View file

@ -2476,6 +2476,7 @@
"Setting ID": "Setting ID", "Setting ID": "Setting ID",
"Value": "Value", "Value": "Value",
"Value in this room": "Value in this room", "Value in this room": "Value in this room",
"Edit setting": "Edit setting",
"Setting:": "Setting:", "Setting:": "Setting:",
"Caution:": "Caution:", "Caution:": "Caution:",
"This UI does NOT check the types of the values. Use at your own risk.": "This UI does NOT check the types of the values. Use at your own risk.", "This UI does NOT check the types of the values. Use at your own risk.": "This UI does NOT check the types of the values. Use at your own risk.",

View file

@ -40,7 +40,7 @@ describe('<SettingsFieldset />', () => {
}); });
it('renders fieldset with react description', () => { it('renders fieldset with react description', () => {
const description = <><p>Test</p><a href='#'>a link</a></>; const description = <><p>Test</p><a href='#test'>a link</a></>;
expect(getComponent({ description })).toMatchSnapshot(); expect(getComponent({ description })).toMatchSnapshot();
}); });
}); });

View file

@ -38,7 +38,7 @@ exports[`<SettingsFieldset /> renders fieldset with react description 1`] = `
Test Test
</p> </p>
<a <a
href="#" href="#test"
> >
a link a link
</a> </a>