1
0
mirror of https://github.com/flarum/core.git synced 2025-08-13 20:04:24 +02:00

chore(a11y): misc a11y improvements (#4211)

This commit is contained in:
Sami Mazouz
2025-04-25 09:17:36 +01:00
committed by GitHub
parent f19007f424
commit 649be7cb03
21 changed files with 136 additions and 23 deletions

View File

@@ -18,7 +18,12 @@ app.initializers.add(
if (!this.isApproved() && !items.has('hidden')) {
items.add(
'awaitingApproval',
<Badge type="awaitingApproval" icon="fas fa-gavel" label={app.translator.trans('flarum-approval.forum.badge.awaiting_approval_tooltip')} />
<Badge
type="awaitingApproval"
icon="fas fa-gavel"
label={app.translator.trans('flarum-approval.forum.badge.awaiting_approval_tooltip')}
tabindex="0"
/>
);
}
});

View File

@@ -6,7 +6,10 @@ import Badge from 'flarum/common/components/Badge';
export default function addLockBadge() {
extend(Discussion.prototype, 'badges', function (badges) {
if (this.isLocked()) {
badges.add('locked', <Badge type="locked" label={app.translator.trans('flarum-lock.forum.badge.locked_tooltip')} icon="fas fa-lock" />);
badges.add(
'locked',
<Badge type="locked" label={app.translator.trans('flarum-lock.forum.badge.locked_tooltip')} icon="fas fa-lock" tabindex="0" />
);
}
});
}

View File

@@ -8,7 +8,7 @@ export default function addStickyBadge() {
if (this.isSticky()) {
badges.add(
'sticky',
<Badge type="sticky" label={app.translator.trans('flarum-sticky.forum.badge.sticky_tooltip')} icon="fas fa-thumbtack" />,
<Badge type="sticky" label={app.translator.trans('flarum-sticky.forum.badge.sticky_tooltip')} icon="fas fa-thumbtack" tabindex="0" />,
10
);
}

View File

@@ -9,11 +9,25 @@ export default function addSubscriptionBadge() {
switch (this.subscription()) {
case 'follow':
badge = <Badge label={app.translator.trans('flarum-subscriptions.forum.badge.following_tooltip')} icon="fas fa-star" type="following" />;
badge = (
<Badge
label={app.translator.trans('flarum-subscriptions.forum.badge.following_tooltip')}
icon="fas fa-star"
type="following"
tabindex="0"
/>
);
break;
case 'ignore':
badge = <Badge label={app.translator.trans('flarum-subscriptions.forum.badge.ignoring_tooltip')} icon="far fa-eye-slash" type="ignoring" />;
badge = (
<Badge
label={app.translator.trans('flarum-subscriptions.forum.badge.ignoring_tooltip')}
icon="far fa-eye-slash"
type="ignoring"
tabindex="0"
/>
);
break;
}

View File

@@ -28,7 +28,7 @@ app.initializers.add('flarum-suspend', () => {
if (new Date() < until) {
items.add(
'suspended',
<Badge icon="fas fa-ban" type="suspended" label={app.translator.trans('flarum-suspend.forum.user_badge.suspended_tooltip')} />,
<Badge icon="fas fa-ban" type="suspended" label={app.translator.trans('flarum-suspend.forum.user_badge.suspended_tooltip')} tabindex="0" />,
100
);
}

View File

@@ -39,6 +39,7 @@ import IExtender from './extenders/IExtender';
import AccessToken from './models/AccessToken';
import SearchManager from './SearchManager';
import { ColorScheme } from './components/ThemeMode';
import { prepareSkipLinks } from './utils/a11y';
export type FlarumScreens = 'phone' | 'tablet' | 'desktop' | 'desktop-hd';
@@ -394,6 +395,8 @@ export default class Application {
this.initColorScheme();
liveHumanTimes();
prepareSkipLinks();
}
private initColorScheme(forumDefault: string | null = null): void {

View File

@@ -123,6 +123,14 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
m.redraw();
});
this.$().on('focusout', (e: JQuery.FocusOutEvent) => {
// Check if the new focused element is outside of this dropdown
if (!this.$().has(e.relatedTarget as Element).length) {
this.$().trigger('hidden.bs.dropdown');
}
});
// Focusing out of the dropdown should close it.
}
/**

View File

@@ -131,7 +131,7 @@ export default class Discussion extends Model {
const items = new ItemList<Mithril.Children>();
if (this.isHidden()) {
items.add('hidden', <Badge type="hidden" icon="fas fa-trash" label={app.translator.trans('core.lib.badge.hidden_tooltip')} />);
items.add('hidden', <Badge type="hidden" icon="fas fa-trash" label={app.translator.trans('core.lib.badge.hidden_tooltip')} tabindex="0" />);
}
return items;

View File

@@ -0,0 +1,21 @@
/**
* Fix a11y skip links by manually focusing on the href target element.
* This prevents unwanted/unexpected reloads of the page.
*/
export function prepareSkipLinks() {
document.querySelectorAll('.sr-only-focusable-custom:not([data-prepared])').forEach((el) => {
el.addEventListener('click', function (e) {
e.preventDefault();
const target = el.getAttribute('href')!;
const $target = document.querySelector(target) as HTMLElement;
if ($target) {
$target.setAttribute('tabindex', '-1');
$target.focus();
$target.removeAttribute('tabindex');
$target.dataset.prepared = 'true';
}
});
});
}

View File

@@ -42,9 +42,10 @@ export default class AvatarEditor extends Component {
return (
<div className={classList(['AvatarEditor', 'Dropdown', this.attrs.className, this.loading && 'loading', this.isDraggedOver && 'dragover'])}>
<Avatar user={user} loading="eager" />
<a
<button
className={user.avatarUrl() ? 'Dropdown-toggle' : 'Dropdown-toggle AvatarEditor--noAvatar'}
title={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
ariaLabel={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
data-toggle="dropdown"
onclick={this.quickUpload.bind(this)}
ondragover={this.enableDragover.bind(this)}
@@ -60,7 +61,7 @@ export default class AvatarEditor extends Component {
) : (
<Icon name={'fas fa-plus-circle'} />
)}
</a>
</button>
<ul className="Dropdown-menu Menu">{listItems(this.controlItems().toArray())}</ul>
</div>
);

View File

@@ -76,15 +76,15 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
viewItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
items.add('slidableUnderneath', this.slidableUnderneathView(), 90);
items.add('content', this.contentView(), 80);
const controls = DiscussionControls.controls(this.attrs.discussion, this).toArray();
if (controls.length) {
items.add('controls', this.controlsView(controls), 100);
items.add('controls', this.controlsView(controls), 70);
}
items.add('slidableUnderneath', this.slidableUnderneathView(), 90);
items.add('content', this.contentView(), 80);
return items;
}
@@ -312,7 +312,7 @@ export default class DiscussionListItem<CustomAttrs extends IDiscussionListItemA
className="DiscussionListItem-count"
icon={showUnread ? [<Icon name={'fas fa-check _checkmark'} />, <Icon name={'fas fa-comment _comment'} />] : <Icon name={'far fa-comment'} />}
label={showUnread ? abbreviateNumber(discussion.unreadCount()) : abbreviateNumber(discussion.replyCount())}
a11yLabel={app.translator.trans(a11yKey, { count: discussion.replyCount() })}
ariaLabel={app.translator.trans(a11yKey, { count: discussion.unreadCount() })}
onclick={showUnread ? this.markAsRead.bind(this) : undefined}
/>
);

View File

@@ -2,6 +2,7 @@ import app from '../../forum/app';
import DiscussionList from './DiscussionList';
import Component from '../../common/Component';
import DiscussionPage from './DiscussionPage';
import { prepareSkipLinks } from '../../common/utils/a11y';
const hotEdge = (e) => {
if (e.pageX < 10) app.pane.show();
@@ -22,7 +23,14 @@ export default class DiscussionListPane extends Component {
return;
}
return <aside className="DiscussionListPane">{this.enoughSpace() && <DiscussionList state={this.attrs.state} />}</aside>;
return (
<aside className="DiscussionListPane">
<a href="#page-main" class="sr-only sr-only-focusable-custom" oncreate={() => prepareSkipLinks()}>
{app.translator.trans('core.forum.discussion_list.skip_discussion_list_pane')}
</a>
{this.enoughSpace() && <DiscussionList state={this.attrs.state} />}
</aside>
);
}
oncreate(vnode) {
@@ -34,7 +42,12 @@ export default class DiscussionListPane extends Component {
// and hide the pane respectively. We also create a 10px 'hot edge' on the
// left of the screen to activate the pane.
const pane = app.pane;
$list.hover(pane.show.bind(pane), pane.onmouseleave.bind(pane));
$list.on('mouseenter', pane.show.bind(pane));
$list.on('mouseleave', pane.onmouseleave.bind(pane));
// a11y: when tabbing into the pane (focus) we should also show the pane.
// and when tabbing out, we should hide the pane.
$list.on('focus', 'a, .Button', pane.show.bind(pane));
$list.on('blur', 'a, .Button', pane.onmouseleave.bind(pane));
$(document).on('mousemove', hotEdge);

View File

@@ -1,9 +1,11 @@
import app from '../app';
import Component from '../../common/Component';
import type { ComponentAttrs } from '../../common/Component';
import type Mithril from 'mithril';
import classList from '../../common/utils/classList';
import ItemList from '../../common/utils/ItemList';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import { prepareSkipLinks } from '../../common/utils/a11y';
export interface PageStructureAttrs extends ComponentAttrs {
hero?: () => Mithril.Children;
@@ -51,7 +53,11 @@ export default class PageStructure<CustomAttrs extends PageStructureAttrs = Page
}
main(): Mithril.Children {
return <div className="Page-main">{this.attrs.loading ? this.loadingItems().toArray() : this.mainItems().toArray()}</div>;
return (
<div className="Page-main" id="page-main">
{this.attrs.loading ? this.loadingItems().toArray() : this.mainItems().toArray()}
</div>
);
}
containerItems(): ItemList<Mithril.Children> {
@@ -70,6 +76,14 @@ export default class PageStructure<CustomAttrs extends PageStructureAttrs = Page
sidebarItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
items.add(
'skipToMainContent',
<a href="#main-content" className="sr-only sr-only-focusable-custom" oncreate={() => prepareSkipLinks()}>
{app.translator.trans('core.forum.index.skip_to_main_content')}
</a>,
200
);
items.add('sidebar', (this.attrs.sidebar && this.attrs.sidebar()) || null, 100);
return items;
@@ -88,6 +102,10 @@ export default class PageStructure<CustomAttrs extends PageStructureAttrs = Page
}
providedContent(): Mithril.Children {
return <div className="Page-content">{this.content}</div>;
return (
<div className="Page-content" id="main-content">
{this.content}
</div>
);
}
}

View File

@@ -20,10 +20,6 @@
border-radius: 100%;
vertical-align: top;
}
span& {
cursor: default;
}
}
.Avatar--size(@size) {

View File

@@ -199,3 +199,13 @@ blockquote ol:last-child {
.text-colored {
color: var(--color);
}
.sr-only-focusable-custom:focus {
clip: unset;
width: auto;
height: auto;
border-width: medium;
background: #000;
color: #fff;
z-index: 100;
}

View File

@@ -136,6 +136,9 @@
@screen-phone-max: (@screen-tablet - 0.02);
@screen-small-phone: 320px;
@screen-small-phone-max: (@screen-small-phone + 0.02);
@screen-tablet: 768px;
@screen-tablet-max: (@screen-desktop - 0.02);
@@ -148,6 +151,7 @@
@screen-desktop-xxxl: 3000px;
@phone: ~"(max-width: @{screen-phone-max})";
@small-phone: ~"(max-width: @{screen-small-phone-max})";
@tablet: ~"(min-width: @{screen-tablet}) and (max-width: @{screen-tablet-max})";
@desktop: ~"(min-width: @{screen-desktop}) and (max-width: @{screen-desktop-max})";
@desktop-hd: ~"(min-width: @{screen-desktop-hd})";

View File

@@ -15,6 +15,8 @@
inset: 0;
border-radius: 100%;
background: rgba(0, 0, 0, 0.6);
color: #fff;
cursor: pointer;
text-align: center;
text-decoration: none;
border: 0;
@@ -23,6 +25,7 @@
opacity: 0.7;
}
&:hover .Dropdown-toggle,
.Dropdown-toggle:focus,
&.open .Dropdown-toggle,
&.loading .Dropdown-toggle,
&.dragover .Dropdown-toggle {

View File

@@ -306,7 +306,7 @@
display: none;
}
&:hover {
&:hover, &:focus {
._checkmark {
display: block;
}
@@ -317,3 +317,9 @@
}
}
}
@media @small-phone {
.DiscussionListItem-info {
max-width: 160px;
}
}

View File

@@ -459,6 +459,7 @@ core:
empty_text: It looks as though there are no discussions here.
load_more_button: => core.ref.load_more
replied_text: "{username} replied {ago}"
skip_discussion_list_pane: Skip the discussion list pane
started_text: "{username} started {ago}"
total_replies_a11y_label: "{count, plural, one {# reply} other {# replies}}"
unread_replies_a11y_label: "{count, plural, one {# unread reply} other {# unread replies}}. Mark unread {count, plural, one {reply} other {replies}} as read."
@@ -496,6 +497,7 @@ core:
meta_title_text: => core.ref.all_discussions
refresh_tooltip: => core.ref.refresh
start_discussion_button: => core.ref.start_a_discussion
skip_to_main_content: Skip to main content
toggle_sidenav_dropdown_accessible_label: Toggle navigation dropdown menu
# These translations are used by the sorting control above the discussion list.
@@ -911,6 +913,10 @@ core:
next_page_button: => core.ref.next_page
previous_page_button: => core.ref.previous_page
# Translations in this namespace are displayed by the basic HTML forum layout.
layout:
skip_to_content: Skip to content
# Translations in this namespace are displayed by the Log Out confirmation interface.
log_out:
log_out_button: => core.ref.log_out

View File

@@ -5,6 +5,7 @@
<div id="drawer" class="App-drawer">
<header id="header" class="App-header">
<a href="#content" class="sr-only sr-only-focusable-custom">@lang('core.views.layout.skip_to_content')</a>
<div id="header-navigation" class="Header-navigation"></div>
<div class="container">
<div class="Header-title">

View File

@@ -7,6 +7,7 @@
<div id="drawer" class="App-drawer">
<header id="header" class="App-header">
<a href="#content" class="sr-only sr-only-focusable-custom">@lang('core.views.layout.skip_to_content')</a>
<div id="header-navigation" class="Header-navigation"></div>
<div class="container">
<div class="Header-title">