DEV: A different approach to breadcrumbs (#27365)
Really fully authored by Jarek, I only made the PR :)
The `DBreadcrumbItem` and `DBreadcrumbContainer` components
introduced in 1239178f49
have
some limitations, mainly that the container has no awareness of
its items, so nothing that requires positional knowledge can
be used. This is needed to use `aria-current` on the last breadcrumb
item, see https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/examples/breadcrumb/.
We change `DBreadcrumbItem` to always be a link, removing
the need for `LinkTo`. Then, we introduce a service to keep
track of containers and items (since all items are rendered into
all containers) and make the item itself responsible for registering
to the service, and introduce the needed `aria-current` behaviour.
---------
Co-authored-by: Jarek Radosz <jradosz@gmail.com>
This commit is contained in:
parent
36dbf06fe9
commit
aef3f17b56
|
@ -1,6 +1,5 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
import DBreadcrumbsContainer from "discourse/components/d-breadcrumbs-container";
|
||||
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
@ -28,27 +27,16 @@ export default class AdminPluginConfigPage extends Component {
|
|||
<div class="admin-plugin-config-page">
|
||||
<DBreadcrumbsContainer />
|
||||
|
||||
<DBreadcrumbsItem as |linkClass|>
|
||||
<LinkTo @route="admin" class={{linkClass}}>
|
||||
{{i18n "admin_title"}}
|
||||
</LinkTo>
|
||||
</DBreadcrumbsItem>
|
||||
|
||||
<DBreadcrumbsItem as |linkClass|>
|
||||
<LinkTo @route="adminPlugins" class={{linkClass}}>
|
||||
{{i18n "admin.plugins.title"}}
|
||||
</LinkTo>
|
||||
</DBreadcrumbsItem>
|
||||
|
||||
<DBreadcrumbsItem as |linkClass|>
|
||||
<LinkTo
|
||||
@route="adminPlugins.show"
|
||||
@model={{@plugin}}
|
||||
class={{linkClass}}
|
||||
>
|
||||
{{@plugin.nameTitleized}}
|
||||
</LinkTo>
|
||||
</DBreadcrumbsItem>
|
||||
<DBreadcrumbsItem @route="admin" @label={{i18n "admin_title"}} />
|
||||
<DBreadcrumbsItem
|
||||
@route="adminPlugins"
|
||||
@label={{i18n "admin.plugins.title"}}
|
||||
/>
|
||||
<DBreadcrumbsItem
|
||||
@route="adminPlugins.show"
|
||||
@model={{@plugin}}
|
||||
@label={{@plugin.nameTitleized}}
|
||||
/>
|
||||
|
||||
<AdminPluginConfigMetadata @plugin={{@plugin}} />
|
||||
|
||||
|
|
|
@ -1,16 +1,7 @@
|
|||
<DBreadcrumbsContainer />
|
||||
|
||||
<DBreadcrumbsItem as |linkClass|>
|
||||
<LinkTo @route="admin" class={{linkClass}}>
|
||||
{{i18n "admin_title"}}
|
||||
</LinkTo>
|
||||
</DBreadcrumbsItem>
|
||||
|
||||
<DBreadcrumbsItem as |linkClass|>
|
||||
<LinkTo @route="adminPlugins" class={{linkClass}}>
|
||||
{{i18n "admin.plugins.title"}}
|
||||
</LinkTo>
|
||||
</DBreadcrumbsItem>
|
||||
<DBreadcrumbsItem @route="admin" @label={{i18n "admin_title"}} />
|
||||
<DBreadcrumbsItem @route="adminPlugins" @label={{i18n "admin.plugins.title"}} />
|
||||
|
||||
<div class="admin-plugins-list-container">
|
||||
{{#if this.model.length}}
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
<DBreadcrumbsItem as |linkClass|>
|
||||
<LinkTo
|
||||
@route="adminPlugins.show.settings"
|
||||
@model={{@model.plugin}}
|
||||
class={{linkClass}}
|
||||
>
|
||||
{{i18n "admin.plugins.change_settings_short"}}
|
||||
</LinkTo>
|
||||
</DBreadcrumbsItem>
|
||||
<DBreadcrumbsItem
|
||||
@route="adminPlugins.show.settings"
|
||||
@model={{@model.plugin}}
|
||||
@label={{i18n "admin.plugins.change_settings_short"}}
|
||||
/>
|
||||
|
||||
<div
|
||||
class="content-body admin-plugin-config-area__settings admin-detail pull-left"
|
||||
|
|
|
@ -1,17 +1,37 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { service } from "@ember/service";
|
||||
import { modifier } from "ember-modifier";
|
||||
import { eq } from "truth-helpers";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import dBreadcrumbsContainerModifier from "discourse/modifiers/d-breadcrumbs-container-modifier";
|
||||
|
||||
const DBreadcrumbsContainer = <template>
|
||||
<ul
|
||||
class="d-breadcrumbs"
|
||||
{{dBreadcrumbsContainerModifier
|
||||
itemClass=(concatClass "d-breadcrumbs__item" @additionalItemClasses)
|
||||
linkClass=(concatClass "d-breadcrumbs__link" @additionalLinkClasses)
|
||||
}}
|
||||
...attributes
|
||||
>
|
||||
{{yield}}
|
||||
</ul>
|
||||
</template>;
|
||||
export default class DBreadcrumbsContainer extends Component {
|
||||
@service breadcrumbs;
|
||||
|
||||
export default DBreadcrumbsContainer;
|
||||
registerContainer = modifier((element) => {
|
||||
const container = { element };
|
||||
|
||||
this.breadcrumbs.containers.add(container);
|
||||
return () => this.breadcrumbs.containers.delete(container);
|
||||
});
|
||||
|
||||
get lastItemIndex() {
|
||||
return this.breadcrumbs.items.size - 1;
|
||||
}
|
||||
|
||||
<template>
|
||||
<ul {{this.registerContainer}} class="d-breadcrumbs" ...attributes>
|
||||
{{#each this.breadcrumbs.items as |item index|}}
|
||||
{{#let item.templateForContainer as |Template|}}
|
||||
<Template
|
||||
@linkClass={{concatClass
|
||||
"d-breadcrumbs__link"
|
||||
@additionalLinkClasses
|
||||
}}
|
||||
aria-current={{if (eq index this.lastItemIndex) "page"}}
|
||||
class={{concatClass "d-breadcrumbs__item" @additionalItemClasses}}
|
||||
/>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
</template>
|
||||
}
|
||||
|
|
|
@ -1,16 +1,39 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { service } from "@ember/service";
|
||||
|
||||
export default class DBreadcrumbsItem extends Component {
|
||||
@service breadcrumbsService;
|
||||
@service breadcrumbs;
|
||||
@service router;
|
||||
|
||||
<template>
|
||||
{{#each this.breadcrumbsService.containers as |container|}}
|
||||
{{#in-element container.element insertBefore=null}}
|
||||
<li class={{container.itemClass}} ...attributes>
|
||||
{{yield container.linkClass}}
|
||||
</li>
|
||||
{{/in-element}}
|
||||
{{/each}}
|
||||
</template>
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.breadcrumbs.items.add(this);
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
this.breadcrumbs.items.delete(this);
|
||||
}
|
||||
|
||||
get url() {
|
||||
if (this.args.model) {
|
||||
return this.router.urlFor(this.args.route, this.args.model);
|
||||
} else {
|
||||
return this.router.urlFor(this.args.route);
|
||||
}
|
||||
}
|
||||
|
||||
get templateForContainer() {
|
||||
// Those are evaluated in a different context than the `@linkClass`
|
||||
const { label } = this.args;
|
||||
const url = this.url;
|
||||
|
||||
return <template>
|
||||
<li ...attributes>
|
||||
<a href={{url}} class={{@linkClass}}>
|
||||
{{label}}
|
||||
</a>
|
||||
</li>
|
||||
</template>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import { registerDestructor } from "@ember/destroyable";
|
||||
import { inject as service } from "@ember/service";
|
||||
import Modifier from "ember-modifier";
|
||||
|
||||
export default class DBreadcrumbsContainerModifier extends Modifier {
|
||||
@service breadcrumbsService;
|
||||
|
||||
container = null;
|
||||
|
||||
modify(element, _, { itemClass, linkClass }) {
|
||||
if (this.container) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.container = { element, itemClass, linkClass };
|
||||
|
||||
this.breadcrumbsService.registerContainer(this.container);
|
||||
|
||||
registerDestructor(this, unregisterContainer);
|
||||
}
|
||||
}
|
||||
|
||||
function unregisterContainer(instance) {
|
||||
if (instance.container) {
|
||||
instance.breadcrumbsService.unregisterContainer(instance.container);
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import { warn } from "@ember/debug";
|
||||
import Service from "@ember/service";
|
||||
|
||||
export default class BreadcrumbsService extends Service {
|
||||
@tracked containers = [];
|
||||
#containers = [];
|
||||
|
||||
registerContainer(container) {
|
||||
if (this.#isContainerRegistered(container)) {
|
||||
warn(
|
||||
"[BreadcrumbsService] A breadcrumb container with the same DOM element has already been registered before."
|
||||
);
|
||||
}
|
||||
|
||||
this.#containers = [...this.#containers, container];
|
||||
|
||||
this.containers = this.#containers;
|
||||
}
|
||||
|
||||
unregisterContainer(container) {
|
||||
if (!this.#isContainerRegistered(container)) {
|
||||
warn(
|
||||
"[BreadcrumbsService] No breadcrumb container was found with this DOM element."
|
||||
);
|
||||
}
|
||||
|
||||
this.#containers = this.#containers.filter((registeredContainer) => {
|
||||
return container.element !== registeredContainer.element;
|
||||
});
|
||||
|
||||
this.containers = this.#containers;
|
||||
}
|
||||
|
||||
#isContainerRegistered(container) {
|
||||
return this.#containers.some((registeredContainer) => {
|
||||
return container.element === registeredContainer.element;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Service from "@ember/service";
|
||||
import { DeferredTrackedSet } from "discourse/lib/tracked-tools";
|
||||
|
||||
export default class Breadcrumbs extends Service {
|
||||
containers = new DeferredTrackedSet();
|
||||
items = new DeferredTrackedSet();
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
import { render } from "@ember/test-helpers";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { module, test } from "qunit";
|
||||
import DBreadcrumbsContainer from "discourse/components/d-breadcrumbs-container";
|
||||
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
||||
module(
|
||||
"Component | DBreadcrumbsContainer and DBreadcrumbsItem",
|
||||
|
@ -9,19 +11,11 @@ module(
|
|||
setupRenderingTest(hooks);
|
||||
|
||||
test("it renders a DBreadcrumbsContainer with multiple DBreadcrumbsItems", async function (assert) {
|
||||
await render(hbs`
|
||||
<DBreadcrumbsContainer />
|
||||
<DBreadcrumbsItem as |linkClass|>
|
||||
<LinkTo @route="admin" class={{linkClass}}>
|
||||
{{i18n "admin_title"}}
|
||||
</LinkTo>
|
||||
</DBreadcrumbsItem>
|
||||
<DBreadcrumbsItem as |linkClass|>
|
||||
<LinkTo @route="about" class={{linkClass}}>
|
||||
{{i18n "about.simple_title"}}
|
||||
</LinkTo>
|
||||
</DBreadcrumbsItem>
|
||||
`);
|
||||
await render(<template>
|
||||
<DBreadcrumbsContainer />
|
||||
<DBreadcrumbsItem @route="admin" @label={{i18n "admin_title"}} />
|
||||
<DBreadcrumbsItem @route="about" @label={{i18n "about.simple_title"}} />
|
||||
</template>);
|
||||
|
||||
assert
|
||||
.dom(".d-breadcrumbs .d-breadcrumbs__item .d-breadcrumbs__link")
|
||||
|
@ -29,14 +23,13 @@ module(
|
|||
});
|
||||
|
||||
test("it renders a DBreadcrumbsItem with additional link and item classes", async function (assert) {
|
||||
await render(hbs`
|
||||
<DBreadcrumbsContainer @additionalLinkClasses="some-class" @additionalItemClasses="other-class" />
|
||||
<DBreadcrumbsItem as |linkClass|>
|
||||
<LinkTo @route="admin" class={{linkClass}}>
|
||||
{{i18n "admin_title"}}
|
||||
</LinkTo>
|
||||
</DBreadcrumbsItem>
|
||||
`);
|
||||
await render(<template>
|
||||
<DBreadcrumbsContainer
|
||||
@additionalLinkClasses="some-class"
|
||||
@additionalItemClasses="other-class"
|
||||
/>
|
||||
<DBreadcrumbsItem @route="admin" @label={{i18n "admin_title"}} />
|
||||
</template>);
|
||||
|
||||
assert.dom(".d-breadcrumbs .d-breadcrumbs__item.other-class").exists();
|
||||
assert
|
||||
|
@ -47,15 +40,11 @@ module(
|
|||
});
|
||||
|
||||
test("it renders multiple DBreadcrumbsContainer elements with the same DBreadcrumbsItem links", async function (assert) {
|
||||
await render(hbs`
|
||||
<DBreadcrumbsContainer />
|
||||
<DBreadcrumbsContainer />
|
||||
<DBreadcrumbsItem as |linkClass|>
|
||||
<LinkTo @route="admin" class={{linkClass}}>
|
||||
{{i18n "admin_title"}}
|
||||
</LinkTo>
|
||||
</DBreadcrumbsItem>
|
||||
`);
|
||||
await render(<template>
|
||||
<DBreadcrumbsContainer />
|
||||
<DBreadcrumbsContainer />
|
||||
<DBreadcrumbsItem @route="admin" @label={{i18n "admin_title"}} />
|
||||
</template>);
|
||||
|
||||
assert.dom(".d-breadcrumbs").exists({ count: 2 });
|
||||
assert.dom(".d-breadcrumbs .d-breadcrumbs__item").exists({ count: 2 });
|
Loading…
Reference in New Issue