1
0
mirror of https://github.com/flarum/core.git synced 2025-08-06 08:27:42 +02:00

feat: skippable discussion list pane

This commit is contained in:
Sami Mazouz
2025-04-18 13:15:50 +01:00
parent 24d497111c
commit 06c2e0bc94
6 changed files with 52 additions and 7 deletions

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
/**
* 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();
console.log('intercepted');
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

@@ -2,6 +2,7 @@ import app from '../../forum/app';
import DiscussionList from './DiscussionList'; import DiscussionList from './DiscussionList';
import Component from '../../common/Component'; import Component from '../../common/Component';
import DiscussionPage from './DiscussionPage'; import DiscussionPage from './DiscussionPage';
import { prepareSkipLinks } from '../../common/utils/a11y';
const hotEdge = (e) => { const hotEdge = (e) => {
if (e.pageX < 10) app.pane.show(); if (e.pageX < 10) app.pane.show();
@@ -22,7 +23,14 @@ export default class DiscussionListPane extends Component {
return; 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) { 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 // and hide the pane respectively. We also create a 10px 'hot edge' on the
// left of the screen to activate the pane. // left of the screen to activate the pane.
const pane = app.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); $(document).on('mousemove', hotEdge);

View File

@@ -5,6 +5,7 @@ import type Mithril from 'mithril';
import classList from '../../common/utils/classList'; import classList from '../../common/utils/classList';
import ItemList from '../../common/utils/ItemList'; import ItemList from '../../common/utils/ItemList';
import LoadingIndicator from '../../common/components/LoadingIndicator'; import LoadingIndicator from '../../common/components/LoadingIndicator';
import { prepareSkipLinks } from '../../common/utils/a11y';
export interface PageStructureAttrs extends ComponentAttrs { export interface PageStructureAttrs extends ComponentAttrs {
hero?: () => Mithril.Children; hero?: () => Mithril.Children;
@@ -52,7 +53,11 @@ export default class PageStructure<CustomAttrs extends PageStructureAttrs = Page
} }
main(): Mithril.Children { 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> { containerItems(): ItemList<Mithril.Children> {
@@ -73,7 +78,7 @@ export default class PageStructure<CustomAttrs extends PageStructureAttrs = Page
items.add( items.add(
'skipToMainContent', 'skipToMainContent',
<a href="#main-content" className="sr-only sr-only-focusable-custom"> <a href="#main-content" className="sr-only sr-only-focusable-custom" oncreate={() => prepareSkipLinks()}>
{app.translator.trans('core.forum.index.skip_to_main_content')} {app.translator.trans('core.forum.index.skip_to_main_content')}
</a>, </a>,
200 200

View File

@@ -459,6 +459,7 @@ core:
empty_text: It looks as though there are no discussions here. empty_text: It looks as though there are no discussions here.
load_more_button: => core.ref.load_more load_more_button: => core.ref.load_more
replied_text: "{username} replied {ago}" replied_text: "{username} replied {ago}"
skip_discussion_list_pane: Skip the discussion list pane
started_text: "{username} started {ago}" started_text: "{username} started {ago}"
total_replies_a11y_label: "{count, plural, one {# reply} other {# replies}}" 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." unread_replies_a11y_label: "{count, plural, one {# unread reply} other {# unread replies}}. Mark unread {count, plural, one {reply} other {replies}} as read."