mirror of
https://github.com/discourse/discourse.git
synced 2025-03-24 01:46:39 +08:00
DEV: A different approach to breadcrumbs (#27365)
Really fully authored by Jarek, I only made the PR :) The `DBreadcrumbItem` and `DBreadcrumbContainer` components introduced in 1239178f496cba5d864adb7c118b17902b8b72dc 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…
x
Reference in New Issue
Block a user