DEV: Auto expand active sections and scroll active link into view

This commit is contained in:
Sérgio Saquetim 2024-08-05 22:28:38 -03:00
parent 11369018b6
commit b7cce1a0dc
No known key found for this signature in database
GPG Key ID: B4E3D7F11E793062
7 changed files with 140 additions and 5 deletions

View File

@ -11,7 +11,11 @@ export default class SidebarApiPanels extends Component {
<template> <template>
<div class="sidebar-sections {{this.panelCssClass}}"> <div class="sidebar-sections {{this.panelCssClass}}">
<ApiSections @collapsable={{@collapsableSections}} /> <ApiSections
@collapsable={{@collapsableSections}}
@expandActiveSection={{this.sidebarState.currentPanel.expandActiveSection}}
@scrollActiveLinkIntoView={{this.sidebarState.currentPanel.scrollActiveLinkIntoView}}
/>
</div> </div>
</template> </template>
} }

View File

@ -1,3 +1,4 @@
import { and, eq } from "truth-helpers";
import Section from "./section"; import Section from "./section";
import SectionLink from "./section-link"; import SectionLink from "./section-link";
@ -14,6 +15,9 @@ const SidebarApiSection = <template>
@displaySection={{@section.displaySection}} @displaySection={{@section.displaySection}}
@hideSectionHeader={{@section.hideSectionHeader}} @hideSectionHeader={{@section.hideSectionHeader}}
@collapsedByDefault={{@section.collapsedByDefault}} @collapsedByDefault={{@section.collapsedByDefault}}
@activeLink={{@section.activeLink}}
@expandWhenActive={{@expandWhenActive}}
@scrollActiveLinkIntoView={{@scrollActiveLinkIntoView}}
> >
{{#each @section.filteredLinks key="name" as |link|}} {{#each @section.filteredLinks key="name" as |link|}}
<SectionLink <SectionLink
@ -23,6 +27,7 @@ const SidebarApiSection = <template>
@model={{link.model}} @model={{link.model}}
@query={{link.query}} @query={{link.query}}
@models={{link.models}} @models={{link.models}}
@currentWhen={{link.currentWhen}}
@href={{link.href}} @href={{link.href}}
@title={{link.title}} @title={{link.title}}
@contentCSSClass={{link.contentCSSClass}} @contentCSSClass={{link.contentCSSClass}}
@ -38,7 +43,6 @@ const SidebarApiSection = <template>
@hoverValue={{link.hoverValue}} @hoverValue={{link.hoverValue}}
@hoverAction={{link.hoverAction}} @hoverAction={{link.hoverAction}}
@hoverTitle={{link.hoverTitle}} @hoverTitle={{link.hoverTitle}}
@currentWhen={{link.currentWhen}}
@didInsert={{link.didInsert}} @didInsert={{link.didInsert}}
@willDestroy={{link.willDestroy}} @willDestroy={{link.willDestroy}}
@content={{link.text}} @content={{link.text}}
@ -46,6 +50,10 @@ const SidebarApiSection = <template>
link.contentComponent link.contentComponent
status=link.contentComponentArgs status=link.contentComponentArgs
}} }}
@scrollIntoView={{and
@scrollActiveLinkIntoView
(eq link.name @section.activeLink.name)
}}
/> />
{{/each}} {{/each}}
</Section> </Section>

View File

@ -6,6 +6,7 @@ import ApiSection from "./api-section";
import PanelHeader from "./panel-header"; import PanelHeader from "./panel-header";
export default class SidebarApiSections extends Component { export default class SidebarApiSections extends Component {
@service router;
@service sidebarState; @service sidebarState;
get sections() { get sections() {
@ -20,7 +21,7 @@ export default class SidebarApiSections extends Component {
} }
return sectionConfigs.map((Section) => { return sectionConfigs.map((Section) => {
const SidebarSection = prepareSidebarSectionClass(Section); const SidebarSection = prepareSidebarSectionClass(Section, this.router);
const sectionInstance = new SidebarSection({ const sectionInstance = new SidebarSection({
filterable: filterable:
@ -43,14 +44,19 @@ export default class SidebarApiSections extends Component {
<PanelHeader @sections={{this.filteredSections}} /> <PanelHeader @sections={{this.filteredSections}} />
{{#each this.filteredSections as |section|}} {{#each this.filteredSections as |section|}}
<ApiSection @section={{section}} @collapsable={{@collapsable}} /> <ApiSection
@section={{section}}
@collapsable={{@collapsable}}
@expandWhenActive={{@expandActiveSection}}
@scrollActiveLinkIntoView={{@scrollActiveLinkIntoView}}
/>
{{/each}} {{/each}}
</template> </template>
} }
// extends the class provided for the section to add functionality we don't want to be overridable when defining custom // extends the class provided for the section to add functionality we don't want to be overridable when defining custom
// sections using the plugin API, like for example the filtering capabilities // sections using the plugin API, like for example the filtering capabilities
function prepareSidebarSectionClass(Section) { function prepareSidebarSectionClass(Section, routerService) {
return class extends Section { return class extends Section {
constructor({ filterable, sidebarState }) { constructor({ filterable, sidebarState }) {
super(); super();
@ -82,6 +88,46 @@ function prepareSidebarSectionClass(Section) {
}); });
} }
get activeLink() {
return this.filteredLinks.find((link) => {
try {
const currentWhen = link.currentWhen;
if (typeof currentWhen === "boolean") {
return currentWhen;
}
// TODO detect active links using the href field
const queryParams = link.queryParams || {};
let models;
if (link.model) {
models = [link.model];
} else if (link.models) {
models = link.models;
} else {
models = [];
}
if (typeof currentWhen === "string") {
return currentWhen.split(" ").some((route) =>
routerService.isActive(route, ...models, {
queryParams,
})
);
}
return routerService.isActive(link.route, ...models, {
queryParams,
});
} catch (e) {
// false if ember throws an exception while checking the routes
return false;
}
});
}
get filtered() { get filtered() {
return !this.filterable || this.filteredLinks?.length > 0; return !this.filterable || this.filteredLinks?.length > 0;
} }

View File

@ -1,12 +1,16 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { hash } from "@ember/helper"; import { hash } from "@ember/helper";
import { on } from "@ember/modifier"; import { on } from "@ember/modifier";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import { LinkTo } from "@ember/routing"; import { LinkTo } from "@ember/routing";
import { schedule } from "@ember/runloop";
import { service } from "@ember/service"; import { service } from "@ember/service";
import { eq, or } from "truth-helpers"; import { eq, or } from "truth-helpers";
import concatClass from "discourse/helpers/concat-class"; import concatClass from "discourse/helpers/concat-class";
import icon from "discourse-common/helpers/d-icon"; import icon from "discourse-common/helpers/d-icon";
import deprecated from "discourse-common/lib/deprecated"; import deprecated from "discourse-common/lib/deprecated";
import { bind } from "discourse-common/utils/decorators";
import SectionLinkPrefix from "./section-link-prefix"; import SectionLinkPrefix from "./section-link-prefix";
/** /**
@ -61,6 +65,14 @@ export default class SectionLink extends Component {
classNames.push(this.args.class); classNames.push(this.args.class);
} }
if (
this.args.href &&
typeof this.args.currentWhen === "boolean" &&
this.args.currentWhen
) {
classNames.push("active");
}
return classNames.join(" "); return classNames.join(" ");
} }
@ -101,9 +113,22 @@ export default class SectionLink extends Component {
} }
} }
@bind
scrollIntoView(element) {
if (this.args.scrollIntoView) {
schedule("afterRender", () => {
element.scrollIntoView({
block: "center",
});
});
}
}
<template> <template>
{{#if this.shouldDisplay}} {{#if this.shouldDisplay}}
<li <li
{{didInsert this.scrollIntoView}}
{{didUpdate this.scrollIntoView @scrollIntoView}}
data-list-item-name={{@linkName}} data-list-item-name={{@linkName}}
class="sidebar-section-link-wrapper" class="sidebar-section-link-wrapper"
...attributes ...attributes

View File

@ -1,4 +1,5 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper"; import { hash } from "@ember/helper";
import { on } from "@ember/modifier"; import { on } from "@ember/modifier";
import { action } from "@ember/object"; import { action } from "@ember/object";
@ -19,15 +20,27 @@ import SectionHeader from "./section-header";
export default class SidebarSection extends Component { export default class SidebarSection extends Component {
@service keyValueStore; @service keyValueStore;
@service router;
@service sidebarState; @service sidebarState;
@tracked activeExpanded = false;
sidebarSectionContentId = getSidebarSectionContentId(this.args.sectionName); sidebarSectionContentId = getSidebarSectionContentId(this.args.sectionName);
collapsedSidebarSectionKey = getCollapsedSidebarSectionKey( collapsedSidebarSectionKey = getCollapsedSidebarSectionKey(
this.args.sectionName this.args.sectionName
); );
constructor() {
super(...arguments);
this.router.on("routeDidChange", this, this.expandIfActive);
}
willDestroy() { willDestroy() {
super.willDestroy(...arguments); super.willDestroy(...arguments);
this.router.off("routeDidChange", this, this.expandIfActive);
this.args.willDestroy?.(); this.args.willDestroy?.();
} }
@ -47,12 +60,20 @@ export default class SidebarSection extends Component {
); );
} }
get isActive() {
return !!this.args.activeLink;
}
@bind @bind
setExpandedState() { setExpandedState() {
if (!isEmpty(this.sidebarState.filter)) { if (!isEmpty(this.sidebarState.filter)) {
return; return;
} }
if (this.expandIfActive()) {
return;
}
if (this.isCollapsed) { if (this.isCollapsed) {
this.sidebarState.collapseSection(this.args.sectionName); this.sidebarState.collapseSection(this.args.sectionName);
} else { } else {
@ -60,11 +81,26 @@ export default class SidebarSection extends Component {
} }
} }
@bind
expandIfActive(transition) {
if (transition?.isAborted) {
return this.activeExpanded;
}
this.activeExpanded = this.args.expandWhenActive && this.isActive;
return this.activeExpanded;
}
get displaySectionContent() { get displaySectionContent() {
if (this.args.hideSectionHeader || !isEmpty(this.sidebarState.filter)) { if (this.args.hideSectionHeader || !isEmpty(this.sidebarState.filter)) {
return true; return true;
} }
if (this.activeExpanded) {
return true;
}
return !this.sidebarState.collapsedSections.has( return !this.sidebarState.collapsedSections.has(
this.collapsedSidebarSectionKey this.collapsedSidebarSectionKey
); );
@ -72,6 +108,8 @@ export default class SidebarSection extends Component {
@action @action
toggleSectionDisplay(_, event) { toggleSectionDisplay(_, event) {
this.activeExpanded = false;
if (this.displaySectionContent) { if (this.displaySectionContent) {
this.sidebarState.collapseSection(this.args.sectionName); this.sidebarState.collapseSection(this.args.sectionName);
} else { } else {
@ -134,6 +172,7 @@ export default class SidebarSection extends Component {
@sidebarSectionContentId={{this.sidebarSectionContentId}} @sidebarSectionContentId={{this.sidebarSectionContentId}}
@toggleSectionDisplay={{this.toggleSectionDisplay}} @toggleSectionDisplay={{this.toggleSectionDisplay}}
@isExpanded={{this.displaySectionContent}} @isExpanded={{this.displaySectionContent}}
@isActive={{this.isActive}}
> >
{{#if @collapsable}} {{#if @collapsable}}
<span class="sidebar-section-header-caret"> <span class="sidebar-section-header-caret">

View File

@ -57,6 +57,14 @@ export default class BaseCustomSidebarPanel {
return false; return false;
} }
get expandActiveSection() {
return false;
}
get scrollActiveLinkIntoView() {
return false;
}
/** /**
* @param {string} filter filter applied * @param {string} filter filter applied
* *

View File

@ -36,6 +36,11 @@ export default class BaseCustomSidebarSectionLink {
*/ */
get models() {} get models() {}
/**
* @returns {boolean} `query` argument for the <LinkTo> component. See See https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo.
*/
get query() {}
/** /**
* @returns {boolean} `current-when` argument for the <LinkTo> component. See See https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo. * @returns {boolean} `current-when` argument for the <LinkTo> component. See See https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo.
*/ */