Add aria attributes where possible (#156)

* Add aria attributes where possible

* Bump submodule to get aria translations
This commit is contained in:
Mitch Lillie 2021-02-06 12:20:41 -08:00 committed by GitHub
parent ef19b9f6b8
commit 04955cc45e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 137 additions and 19 deletions

@ -1 +1 @@
Subproject commit a47ae3f825f74ad7a6106b92e21d3c66b10d3fe8
Subproject commit 084ce539fff5253317d6460598b10a6867999c20

View file

@ -14,12 +14,13 @@ export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
render() {
return (
<div class="position-relative mb-2">
{this.props.banner && <PictrsImage src={this.props.banner} />}
{this.props.banner && <PictrsImage src={this.props.banner} alt="" />}
{this.props.icon && (
<PictrsImage
src={this.props.icon}
iconOverlay
pushup={!!this.props.banner}
alt=""
/>
)}
</div>

View file

@ -198,6 +198,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<button
class="btn btn-sm text-muted"
onClick={linkEvent(this, this.handleCommentCollapse)}
aria-label={
this.state.collapsed ? i18n.t('expand') : i18n.t('collapse')
}
>
{this.state.collapsed ? '+' : '—'}
</button>
@ -248,6 +251,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
? i18n.t('mark_as_unread')
: i18n.t('mark_as_read')
}
aria-label={
this.commentOrMentionRead
? i18n.t('mark_as_unread')
: i18n.t('mark_as_read')
}
>
{this.state.readLoading ? (
this.loadingIcon
@ -270,6 +278,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}`}
onClick={linkEvent(node, this.handleCommentUpvote)}
data-tippy-content={i18n.t('upvote')}
aria-label={i18n.t('upvote')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-arrow-up1"></use>
@ -287,6 +296,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}`}
onClick={linkEvent(node, this.handleCommentDownvote)}
data-tippy-content={i18n.t('downvote')}
aria-label={i18n.t('downvote')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-arrow-down1"></use>
@ -300,6 +310,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleReplyClick)}
data-tippy-content={i18n.t('reply')}
aria-label={i18n.t('reply')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-reply1"></use>
@ -310,6 +321,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleShowAdvanced)}
data-tippy-content={i18n.t('more')}
aria-label={i18n.t('more')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-more-vertical"></use>
@ -340,6 +352,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
data-tippy-content={
cv.saved ? i18n.t('unsave') : i18n.t('save')
}
aria-label={
cv.saved ? i18n.t('unsave') : i18n.t('save')
}
>
{this.state.saveLoading ? (
this.loadingIcon
@ -357,6 +372,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleViewSource)}
data-tippy-content={i18n.t('view_source')}
aria-label={i18n.t('view_source')}
>
<svg
class={`icon icon-inline ${
@ -372,6 +388,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleEditClick)}
data-tippy-content={i18n.t('edit')}
aria-label={i18n.t('edit')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-edit"></use>
@ -388,6 +405,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
? i18n.t('delete')
: i18n.t('restore')
}
aria-label={
!cv.comment.deleted
? i18n.t('delete')
: i18n.t('restore')
}
>
<svg
class={`icon icon-inline ${
@ -667,9 +689,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{this.state.showBanDialog && (
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
<div class="form-group row">
<label class="col-form-label">{i18n.t('reason')}</label>
<label
class="col-form-label"
htmlFor={`mod-ban-reason-${cv.comment.id}`}
>
{i18n.t('reason')}
</label>
<input
type="text"
id={`mod-ban-reason-${cv.comment.id}`}
class="form-control mr-2"
placeholder={i18n.t('reason')}
value={this.state.banReason}

View file

@ -158,6 +158,7 @@ export class Communities extends Component<any, CommunitiesState> {
{cv.subscribed ? (
<span
class="pointer btn-link"
role="button"
onClick={linkEvent(
cv.community.id,
this.handleUnsubscribe
@ -168,6 +169,7 @@ export class Communities extends Component<any, CommunitiesState> {
) : (
<span
class="pointer btn-link"
role="button"
onClick={linkEvent(
cv.community.id,
this.handleSubscribe

View file

@ -2,6 +2,7 @@ import { Component, linkEvent } from 'inferno';
import { pictrsUri } from '../env';
import { UserService } from '../services';
import { toast, randomStr } from '../utils';
import { i18n } from '../i18next';
interface ImageUploadFormProps {
uploadTitle: string;
@ -48,7 +49,10 @@ export class ImageUploadForm extends Component<
this.props.rounded ? 'rounded-circle' : ''
}`}
/>
<a onClick={linkEvent(this, this.handleRemoveImage)}>
<a
onClick={linkEvent(this, this.handleRemoveImage)}
aria-label={i18n.t('remove')}
>
<svg class="icon mini-overlay">
<use xlinkHref="#icon-x"></use>
</svg>

View file

@ -172,6 +172,7 @@ export class Inbox extends Component<any, InboxState> {
<li className="list-inline-item">
<span
class="pointer"
role="button"
onClick={linkEvent(this, this.markAllAsRead)}
>
{i18n.t('mark_all_as_read')}

View file

@ -326,6 +326,7 @@ export class Login extends Component<any, State> {
class="rounded-top img-fluid"
src={this.captchaPngSrc()}
style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
alt={i18n.t('captcha')}
/>
{this.state.captcha.ok.wav && (
<button

View file

@ -508,7 +508,9 @@ export class Main extends Component<any, MainState> {
<li className="list-inline-item-action">
<span
class="pointer"
role="button"
onClick={linkEvent(this, this.handleEditClick)}
aria-label={i18n.t('edit')}
data-tippy-content={i18n.t('edit')}
>
<svg class="icon icon-inline">

View file

@ -233,6 +233,7 @@ export class MarkdownTextArea extends Component<
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('header')}
aria-label={i18n.t('header')}
onClick={linkEvent(this, this.handleInsertHeader)}
>
<svg class="icon icon-inline">
@ -242,6 +243,7 @@ export class MarkdownTextArea extends Component<
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('strikethrough')}
aria-label={i18n.t('strikethrough')}
onClick={linkEvent(this, this.handleInsertStrikethrough)}
>
<svg class="icon icon-inline">
@ -251,6 +253,7 @@ export class MarkdownTextArea extends Component<
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('quote')}
aria-label={i18n.t('quote')}
onClick={linkEvent(this, this.handleInsertQuote)}
>
<svg class="icon icon-inline">
@ -260,6 +263,7 @@ export class MarkdownTextArea extends Component<
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('list')}
aria-label={i18n.t('list')}
onClick={linkEvent(this, this.handleInsertList)}
>
<svg class="icon icon-inline">
@ -269,6 +273,7 @@ export class MarkdownTextArea extends Component<
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('code')}
aria-label={i18n.t('code')}
onClick={linkEvent(this, this.handleInsertCode)}
>
<svg class="icon icon-inline">
@ -278,6 +283,7 @@ export class MarkdownTextArea extends Component<
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('subscript')}
aria-label={i18n.t('subscript')}
onClick={linkEvent(this, this.handleInsertSubscript)}
>
<svg class="icon icon-inline">
@ -287,6 +293,7 @@ export class MarkdownTextArea extends Component<
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('superscript')}
aria-label={i18n.t('superscript')}
onClick={linkEvent(this, this.handleInsertSuperscript)}
>
<svg class="icon icon-inline">
@ -296,6 +303,7 @@ export class MarkdownTextArea extends Component<
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('spoiler')}
aria-label={i18n.t('spoiler')}
onClick={linkEvent(this, this.handleInsertSpoiler)}
>
<svg class="icon icon-inline">

View file

@ -80,11 +80,12 @@ export class PasswordChange extends Component<any, State> {
return (
<form onSubmit={linkEvent(this, this.handlePasswordChangeSubmit)}>
<div class="form-group row">
<label class="col-sm-2 col-form-label">
<label class="col-sm-2 col-form-label" htmlFor="new-password">
{i18n.t('new_password')}
</label>
<div class="col-sm-10">
<input
id="new-password"
type="password"
value={this.state.passwordChangeForm.password}
onInput={linkEvent(this, this.handlePasswordChange)}
@ -94,11 +95,12 @@ export class PasswordChange extends Component<any, State> {
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">
<label class="col-sm-2 col-form-label" htmlFor="verify-password">
{i18n.t('verify_password')}
</label>
<div class="col-sm-10">
<input
id="verify-password"
type="password"
value={this.state.passwordChangeForm.password_verify}
onInput={linkEvent(this, this.handleVerifyPasswordChange)}

View file

@ -6,6 +6,7 @@ const maxImageSize = 3000;
interface PictrsImageProps {
src: string;
alt?: string;
icon?: boolean;
thumbnail?: boolean;
nsfw?: boolean;
@ -25,6 +26,7 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
<source srcSet={this.src('jpg')} type="image/jpeg" />
<img
src={this.src('jpg')}
alt={this.alt()}
className={`
${!this.props.icon && !this.props.iconOverlay && 'img-fluid '}
${
@ -71,4 +73,11 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
return out;
}
alt(): string {
if (this.props.icon) {
return '';
}
return this.props.alt || '';
}
}

View file

@ -180,6 +180,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
{this.state.suggestedTitle && (
<div
class="mt-1 text-muted small font-weight-bold pointer"
role="button"
onClick={linkEvent(this, this.copySuggestedTitle)}
>
{i18n.t('copy_suggested_title', {
@ -227,7 +228,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</svg>
)}
{isImage(this.state.postForm.url) && (
<img src={this.state.postForm.url} class="img-fluid" />
<img src={this.state.postForm.url} class="img-fluid" alt="" />
)}
{this.state.crossPosts.length > 0 && (
<>

View file

@ -169,6 +169,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<PictrsImage
src={src}
thumbnail
alt=""
nsfw={post_view.post.nsfw || post_view.community.nsfw}
/>
);
@ -200,6 +201,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
class="float-right text-body pointer d-inline-block position-relative mb-2"
data-tippy-content={i18n.t('expand_here')}
onClick={linkEvent(this, this.handleImageExpandClick)}
role="button"
aria-label={i18n.t('expand_here')}
>
{this.imgThumb(this.getImageSrc())}
<svg class="icon mini-overlay">
@ -328,6 +331,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
previewLines(post_view.post.body)
)}
data-tippy-allowHtml={true}
aria-label={i18n.t('upvote')}
to={`/post/${post_view.post.id}`}
>
<svg class="mr-1 icon icon-inline">
@ -350,6 +354,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}`}
onClick={linkEvent(this, this.handlePostLike)}
data-tippy-content={i18n.t('upvote')}
aria-label={i18n.t('upvote')}
>
<svg class="icon upvote">
<use xlinkHref="#icon-arrow-up1"></use>
@ -368,6 +373,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}`}
onClick={linkEvent(this, this.handlePostDisLike)}
data-tippy-content={i18n.t('downvote')}
aria-label={i18n.t('downvote')}
>
<svg class="icon downvote">
<use xlinkHref="#icon-arrow-down1"></use>
@ -504,6 +510,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<button
class="btn text-muted py-0 pr-0"
data-tippy-content={this.pointsTippy}
aria-label={i18n.t('downvote')}
>
<small>
<svg class="icon icon-inline mr-1">
@ -520,6 +527,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
data-tippy-content={
post_view.saved ? i18n.t('unsave') : i18n.t('save')
}
aria-label={post_view.saved ? i18n.t('unsave') : i18n.t('save')}
>
<small>
<svg
@ -545,6 +553,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}`}
data-tippy-content={this.pointsTippy}
onClick={linkEvent(this, this.handlePostLike)}
aria-label={i18n.t('upvote')}
>
<svg class="small icon icon-inline mr-2">
<use xlinkHref="#icon-arrow-up1"></use>
@ -558,6 +567,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}`}
onClick={linkEvent(this, this.handlePostDisLike)}
data-tippy-content={this.pointsTippy}
aria-label={i18n.t('downvote')}
>
<svg class="small icon icon-inline mr-2">
<use xlinkHref="#icon-arrow-down1"></use>
@ -571,6 +581,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<button
class="btn btn-link btn-animate text-muted py-0 pl-1 pr-0"
onClick={linkEvent(this, this.handleSavePostClick)}
aria-label={post_view.saved ? i18n.t('unsave') : i18n.t('save')}
data-tippy-content={
post_view.saved ? i18n.t('unsave') : i18n.t('save')
}
@ -586,6 +597,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<button
class="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleShowMoreMobile)}
aria-label={i18n.t('more')}
data-tippy-content={i18n.t('more')}
>
<svg class="icon icon-inline">
@ -639,6 +651,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
data-tippy-content={
post_view.saved ? i18n.t('unsave') : i18n.t('save')
}
aria-label={
post_view.saved ? i18n.t('unsave') : i18n.t('save')
}
>
<svg
class={`icon icon-inline ${
@ -694,6 +709,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
class="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleShowAdvanced)}
data-tippy-content={i18n.t('more')}
aria-label={i18n.t('more')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-more-vertical"></use>

View file

@ -116,8 +116,10 @@ export class PrivateMessageForm extends Component<
{i18n.t('message')}
<span
onClick={linkEvent(this, this.handleShowDisclaimer)}
role="button"
class="ml-2 pointer text-danger"
data-tippy-content={i18n.t('disclaimer')}
aria-label={i18n.t('disclaimer')}
>
<svg class={`icon icon-inline`}>
<use xlinkHref="#icon-alert-triangle"></use>

View file

@ -77,6 +77,7 @@ export class PrivateMessage extends Component<
</li>
<li className="list-inline-item">
<div
role="button"
className="pointer text-monospace"
onClick={linkEvent(this, this.handleMessageCollapse)}
>
@ -123,6 +124,11 @@ export class PrivateMessage extends Component<
? i18n.t('mark_as_unread')
: i18n.t('mark_as_read')
}
aria-label={
message_view.private_message.read
? i18n.t('mark_as_unread')
: i18n.t('mark_as_read')
}
>
<svg
class={`icon icon-inline ${
@ -138,6 +144,7 @@ export class PrivateMessage extends Component<
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleReplyClick)}
data-tippy-content={i18n.t('reply')}
aria-label={i18n.t('reply')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-reply1"></use>
@ -153,6 +160,7 @@ export class PrivateMessage extends Component<
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleEditClick)}
data-tippy-content={i18n.t('edit')}
aria-label={i18n.t('edit')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-edit"></use>
@ -168,6 +176,11 @@ export class PrivateMessage extends Component<
? i18n.t('delete')
: i18n.t('restore')
}
aria-label={
!message_view.private_message.deleted
? i18n.t('delete')
: i18n.t('restore')
}
>
<svg
class={`icon icon-inline ${
@ -186,6 +199,7 @@ export class PrivateMessage extends Component<
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleViewSource)}
data-tippy-content={i18n.t('view_source')}
aria-label={i18n.t('view_source')}
>
<svg
class={`icon icon-inline ${

View file

@ -209,6 +209,7 @@ export class Search extends Component<any, SearchState> {
class="form-control mr-2 mb-2"
value={this.state.searchText}
placeholder={`${i18n.t('search')}...`}
aria-label={i18n.t('search')}
onInput={linkEvent(this, this.handleQChange)}
required
minLength={3}
@ -233,8 +234,11 @@ export class Search extends Component<any, SearchState> {
value={this.state.type_}
onChange={linkEvent(this, this.handleTypeChange)}
class="custom-select w-auto mb-2"
aria-label={i18n.t('type')}
>
<option disabled>{i18n.t('type')}</option>
<option disabled aria-hidden="true">
{i18n.t('type')}
</option>
<option value={SearchType.All}>{i18n.t('all')}</option>
<option value={SearchType.Comments}>{i18n.t('comments')}</option>
<option value={SearchType.Posts}>{i18n.t('posts')}</option>

View file

@ -299,9 +299,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<>
<li className="list-inline-item-action">
<span
role="button"
class="pointer"
onClick={linkEvent(this, this.handleEditClick)}
data-tippy-content={i18n.t('edit')}
aria-label={i18n.t('edit')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-edit"></use>
@ -313,6 +315,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<li className="list-inline-item-action">
<span
class="pointer"
role="button"
onClick={linkEvent(
this,
this.handleShowConfirmLeaveModTeamClick
@ -329,6 +332,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<li className="list-inline-item-action">
<span
class="pointer"
role="button"
onClick={linkEvent(this, this.handleLeaveModTeamClick)}
>
{i18n.t('yes')}
@ -337,6 +341,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<li className="list-inline-item-action">
<span
class="pointer"
role="button"
onClick={linkEvent(
this,
this.handleCancelLeaveModTeamClick
@ -357,6 +362,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
? i18n.t('delete')
: i18n.t('restore')
}
aria-label={
!community_view.community.deleted
? i18n.t('delete')
: i18n.t('restore')
}
>
<svg
class={`icon icon-inline ${
@ -375,6 +385,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{!this.props.community_view.community.removed ? (
<span
class="pointer"
role="button"
onClick={linkEvent(this, this.handleModRemoveShow)}
>
{i18n.t('remove')}
@ -382,6 +393,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
) : (
<span
class="pointer"
role="button"
onClick={linkEvent(this, this.handleModRemoveSubmit)}
>
{i18n.t('restore')}

View file

@ -40,8 +40,9 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
value={this.state.sort}
onChange={linkEvent(this, this.handleSortChange)}
class="custom-select w-auto mr-2 mb-2"
aria-label={i18n.t('sort_type')}
>
<option disabled>{i18n.t('sort_type')}</option>
<option disabled aria-hidden="true">{i18n.t('sort_type')}</option>
{!this.props.hideHot && [
<option value={SortType.Hot}>{i18n.t('hot')}</option>,
<option value={SortType.Active}>{i18n.t('active')}</option>,
@ -52,7 +53,7 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
{i18n.t('most_comments')}
</option>
)}
<option disabled></option>
<option disabled aria-hidden="true"></option>
<option value={SortType.TopDay}>{i18n.t('top_day')}</option>
<option value={SortType.TopWeek}>{i18n.t('top_week')}</option>
<option value={SortType.TopMonth}>{i18n.t('top_month')}</option>

View file

@ -526,28 +526,36 @@ export class User extends Component<any, UserState> {
/>
</div>
<div class="form-group">
<label>{i18n.t('language')}</label>
<label htmlFor="user-language">{i18n.t('language')}</label>
<select
id="user-language"
value={this.state.userSettingsForm.lang}
onChange={linkEvent(this, this.handleUserSettingsLangChange)}
class="ml-2 custom-select w-auto"
>
<option disabled>{i18n.t('language')}</option>
<option disabled aria-hidden="true">
{i18n.t('language')}
</option>
<option value="browser">{i18n.t('browser_default')}</option>
<option disabled></option>
<option disabled aria-hidden="true">
</option>
{languages.map(lang => (
<option value={lang.code}>{lang.name}</option>
))}
</select>
</div>
<div class="form-group">
<label>{i18n.t('theme')}</label>
<label htmlFor="user-theme">{i18n.t('theme')}</label>
<select
id="user-theme"
value={this.state.userSettingsForm.theme}
onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
class="ml-2 custom-select w-auto"
>
<option disabled>{i18n.t('theme')}</option>
<option disabled aria-hidden="true">
{i18n.t('theme')}
</option>
<option value="browser">{i18n.t('browser_default')}</option>
{themes.map(theme => (
<option value={theme}>{theme}</option>
@ -584,11 +592,12 @@ export class User extends Component<any, UserState> {
/>
</form>
<div class="form-group row">
<label class="col-lg-5 col-form-label">
<label class="col-lg-5 col-form-label" htmlFor="display-name">
{i18n.t('display_name')}
</label>
<div class="col-lg-7">
<input
id="display-name"
type="text"
class="form-control"
placeholder={i18n.t('optional')}
@ -636,13 +645,14 @@ export class User extends Component<any, UserState> {
</div>
</div>
<div class="form-group row">
<label class="col-lg-5 col-form-label">
<label class="col-lg-5 col-form-label" htmlFor="matrix-user-id">
<a href={elementUrl} target="_blank" rel="noopener">
{i18n.t('matrix_user_id')}
</a>
</label>
<div class="col-lg-7">
<input
id="matrix-user-id"
type="text"
class="form-control"
placeholder="@user:example.com"