-Subproject commit a47ae3f825f74ad7a6106b92e21d3c66b10d3fe8
+Subproject commit 084ce539fff5253317d6460598b10a6867999c20
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>
<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>
? 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
}`}
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>
}`}
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>
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>
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>
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
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 ${
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>
? i18n.t('delete')
: i18n.t('restore')
}
+ aria-label={
+ !cv.comment.deleted
+ ? i18n.t('delete')
+ : i18n.t('restore')
+ }
>
<svg
class={`icon icon-inline ${
{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}
{cv.subscribed ? (
<span
class="pointer btn-link"
+ role="button"
onClick={linkEvent(
cv.community.id,
this.handleUnsubscribe
) : (
<span
class="pointer btn-link"
+ role="button"
onClick={linkEvent(
cv.community.id,
this.handleSubscribe
import { pictrsUri } from '../env';
import { UserService } from '../services';
import { toast, randomStr } from '../utils';
+import { i18n } from '../i18next';
interface ImageUploadFormProps {
uploadTitle: string;
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>
<li className="list-inline-item">
<span
class="pointer"
+ role="button"
onClick={linkEvent(this, this.markAllAsRead)}
>
{i18n.t('mark_all_as_read')}
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
<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">
<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">
<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">
<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">
<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">
<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">
<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">
<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">
<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">
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)}
</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)}
interface PictrsImageProps {
src: string;
+ alt?: string;
icon?: boolean;
thumbnail?: boolean;
nsfw?: boolean;
<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 '}
${
this.props.thumbnail && !this.props.icon
? 'thumbnail rounded '
: 'img-expanded '
- }
+ }
${this.props.thumbnail && this.props.nsfw && 'img-blur '}
${this.props.icon && 'rounded-circle img-icon mr-2 '}
${this.props.iconOverlay && 'ml-2 mb-0 rounded-circle avatar-overlay '}
return out;
}
+
+ alt(): string {
+ if (this.props.icon) {
+ return '';
+ }
+ return this.props.alt || '';
+ }
}
{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', {
</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 && (
<>
<PictrsImage
src={src}
thumbnail
+ alt=""
nsfw={post_view.post.nsfw || post_view.community.nsfw}
/>
);
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">
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">
}`}
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>
}`}
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>
<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">
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
}`}
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>
}`}
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>
<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')
}
<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">
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 ${
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>
{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>
</li>
<li className="list-inline-item">
<div
+ role="button"
className="pointer text-monospace"
onClick={linkEvent(this, this.handleMessageCollapse)}
>
? 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 ${
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>
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>
? i18n.t('delete')
: i18n.t('restore')
}
+ aria-label={
+ !message_view.private_message.deleted
+ ? i18n.t('delete')
+ : i18n.t('restore')
+ }
>
<svg
class={`icon icon-inline ${
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 ${
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}
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>
<span>
<CommunityLink community={community_view.community} />
</span>
- <span>{` -
+ <span>{` -
${i18n.t('number_of_subscribers', {
count: community_view.counts.subscribers,
})}
<>
<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>
<li className="list-inline-item-action">
<span
class="pointer"
+ role="button"
onClick={linkEvent(
this,
this.handleShowConfirmLeaveModTeamClick
<li className="list-inline-item-action">
<span
class="pointer"
+ role="button"
onClick={linkEvent(this, this.handleLeaveModTeamClick)}
>
{i18n.t('yes')}
<li className="list-inline-item-action">
<span
class="pointer"
+ role="button"
onClick={linkEvent(
this,
this.handleCancelLeaveModTeamClick
? i18n.t('delete')
: i18n.t('restore')
}
+ aria-label={
+ !community_view.community.deleted
+ ? i18n.t('delete')
+ : i18n.t('restore')
+ }
>
<svg
class={`icon icon-inline ${
{!this.props.community_view.community.removed ? (
<span
class="pointer"
+ role="button"
onClick={linkEvent(this, this.handleModRemoveShow)}
>
{i18n.t('remove')}
) : (
<span
class="pointer"
+ role="button"
onClick={linkEvent(this, this.handleModRemoveSubmit)}
>
{i18n.t('restore')}
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>,
{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>
/>
</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>
/>
</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')}
</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"