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:
@@ -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 {
|
||||||
|
@@ -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.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
22
framework/core/js/src/common/utils/a11y.ts
Normal file
22
framework/core/js/src/common/utils/a11y.ts
Normal 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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@@ -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);
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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."
|
||||||
|
Reference in New Issue
Block a user