Added animation of sidebar between main and contextual menus

refs https://github.com/TryGhost/Team/issues/1149

- added `ember-css-transitions` library that provides a modifier that adds/removes classes used for animating elements in and out, automatically creating a clone for destroyed elements
- added a wrapper class around the `main` and `design` nav menu contents so they could be individually animated
- used the new `{{css-transition}}` modifier to animate the sidebar menus in/out
- ensured main menu doesn't animate on first render of the parent `<GhNavMenu>` component so there's no animation when Admin is loaded
This commit is contained in:
Kevin Ansfield 2021-10-20 13:04:05 +01:00
parent 53ac7d2f6d
commit 8710f5f532
7 changed files with 230 additions and 169 deletions

View File

@ -1,7 +1,7 @@
<nav class="gh-nav {{if this.ui.contextualNavMenu "gh-nav-contextual"}}" ...attributes>
<nav class="gh-nav {{if this.ui.contextualNavMenu "gh-nav-contextual"}}" {{did-insert (fn (mut this.firstRender) false)}} ...attributes>
{{#if this.ui.contextualNavMenu}}
{{component (concat "gh-nav-menu/" ui.contextualNavMenu)}}
{{else}}
<GhNavMenu::Main @icon={{this.settings.settledIcon}} />
<GhNavMenu::Main @icon={{this.settings.settledIcon}} @firstRender={{this.firstRender}} />
{{/if}}
</nav>

View File

@ -1,7 +1,10 @@
import Component from '@glimmer/component';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class GhNavMenuComponent extends Component {
@service settings;
@service ui;
@tracked firstRender = true;
}

View File

@ -1,48 +1,50 @@
<header class="gh-nav-header">
<LinkTo @route="settings" class="gh-nav-menu-back-button">{{svg-jar "arrow-left-small"}} Settings</LinkTo>
</header>
<section class="gh-nav-body gh-nav-design">
<div class="gh-nav-top" {{on "click" this.transitionBackToIndex}}>
<div class="gh-nav-list gh-nav-main">
<LinkTo @route="settings.design" class="gh-nav-menu-title" @current-when="settings.design.index" {{on "click" this.closeAllSections}}>Site design</LinkTo>
{{#let (eq this.openSection "brand") as |isOpen|}}
<button class="gh-nav-design-tab {{if isOpen "active"}}" type="button" {{on "click" (fn this.toggleSection "brand")}}>
{{svg-jar "paintbrush"}}Brand
<span class="gh-nav-button-expand">{{svg-jar (if isOpen "arrow-up-stroke" "arrow-down-stroke")}}</span>
</button>
{{#liquid-if isOpen}}
<div class="gh-nav-design-settings">
<Settings::Design::GeneralSettingsForm
@updatePreview={{perform this.themeManagement.updatePreviewHtmlTask}}
/>
</div>
{{/liquid-if}}
{{/let}}
{{#each this.customThemeSettings.settingGroups as |group|}}
{{#let (eq this.openSection group.key) as |isOpen|}}
<button class="gh-nav-design-tab {{if isOpen "active"}}" type="button" {{on "click" (fn this.toggleSection group.key)}}>
{{svg-jar group.icon}} {{group.name}}
<div class="flex flex-column h-100" {{css-transition "gh-nav-contextual"}} ...attributes>
<header class="gh-nav-header">
<LinkTo @route="settings" class="gh-nav-menu-back-button">{{svg-jar "arrow-left-small"}} Settings</LinkTo>
</header>
<section class="gh-nav-body gh-nav-design">
<div class="gh-nav-top" {{on "click" this.transitionBackToIndex}}>
<div class="gh-nav-list gh-nav-main">
<LinkTo @route="settings.design" class="gh-nav-menu-title" @current-when="settings.design.index" {{on "click" this.closeAllSections}}>Site design</LinkTo>
{{#let (eq this.openSection "brand") as |isOpen|}}
<button class="gh-nav-design-tab {{if isOpen "active"}}" type="button" {{on "click" (fn this.toggleSection "brand")}}>
{{svg-jar "paintbrush"}}Brand
<span class="gh-nav-button-expand">{{svg-jar (if isOpen "arrow-up-stroke" "arrow-down-stroke")}}</span>
</button>
{{#liquid-if isOpen}}
<div class="gh-nav-design-settings">
<Settings::Design::ThemeSettingsForm
@themeSettings={{group.settings}}
<Settings::Design::GeneralSettingsForm
@updatePreview={{perform this.themeManagement.updatePreviewHtmlTask}}
/>
</div>
{{/liquid-if}}
{{/let}}
{{/each}}
</div>
</div>
<div class="gh-nav-bottom">
<LinkTo class="gh-nav-design-tab" style="align-items: self-start" @route="settings.design.change-theme" {{on "click" (fn this.toggleSection null)}}>
<span>Themes</span>
<span class="active-theme">Current: {{this.activeTheme.name}}{{#if this.activeTheme.package.version}} - v{{this.activeTheme.package.version}}{{/if}}</span>
</LinkTo>
</div>
</section>
{{#each this.customThemeSettings.settingGroups as |group|}}
{{#let (eq this.openSection group.key) as |isOpen|}}
<button class="gh-nav-design-tab {{if isOpen "active"}}" type="button" {{on "click" (fn this.toggleSection group.key)}}>
{{svg-jar group.icon}} {{group.name}}
<span class="gh-nav-button-expand">{{svg-jar (if isOpen "arrow-up-stroke" "arrow-down-stroke")}}</span>
</button>
{{#liquid-if isOpen}}
<div class="gh-nav-design-settings">
<Settings::Design::ThemeSettingsForm
@themeSettings={{group.settings}}
@updatePreview={{perform this.themeManagement.updatePreviewHtmlTask}}
/>
</div>
{{/liquid-if}}
{{/let}}
{{/each}}
</div>
</div>
<div class="gh-nav-bottom">
<LinkTo class="gh-nav-design-tab" style="align-items: self-start" @route="settings.design.change-theme" {{on "click" (fn this.toggleSection null)}}>
<span>Themes</span>
<span class="active-theme">Current: {{this.activeTheme.name}}{{#if this.activeTheme.package.version}} - v{{this.activeTheme.package.version}}{{/if}}</span>
</LinkTo>
</div>
</section>
</div>

View File

@ -1,141 +1,143 @@
<header class="gh-nav-menu">
<div class="gh-nav-menu-details">
<div class="gh-nav-menu-icon {{this.iconClass}}" style={{this.iconStyle}}></div>
<div class="gh-nav-menu-details-sitetitle">{{this.config.blogTitle}}</div>
</div>
<div class="gh-nav-menu-search">
<button class="gh-nav-btn-search" {{action "toggleSearchModal"}} title="Search site (Ctrl/⌘ + K)"><span>{{svg-jar "search"}}</span></button>
</div>
</header>
<div class="flex flex-column h-100" {{css-transition (unless @firstRender "gh-nav-main")}} ...attributes>
<header class="gh-nav-menu">
<div class="gh-nav-menu-details">
<div class="gh-nav-menu-icon {{this.iconClass}}" style={{this.iconStyle}}></div>
<div class="gh-nav-menu-details-sitetitle">{{this.config.blogTitle}}</div>
</div>
<div class="gh-nav-menu-search">
<button class="gh-nav-btn-search" {{action "toggleSearchModal"}} title="Search site (Ctrl/⌘ + K)"><span>{{svg-jar "search"}}</span></button>
</div>
</header>
{{#if this.showSearchModal}}
<GhFullscreenModal @modal="search"
@close={{action "toggleSearchModal"}}
@modifier="action wide" />
{{/if}}
{{#if this.showSearchModal}}
<GhFullscreenModal @modal="search"
@close={{action "toggleSearchModal"}}
@modifier="action wide" />
{{/if}}
<section class="gh-nav-body">
<div class="gh-nav-top">
<ul class="gh-nav-list gh-nav-main">
{{#if (gh-user-can-admin this.session.user)}}
<li class="relative">
<LinkTo @route="dashboard" @alt="Dashboard" @title="Dashboard" data-test-nav="dashboard">{{svg-jar "house"}} Dashboard</LinkTo>
</li>
{{/if}}
<li class="relative">
<span {{action "transitionToOrRefreshSite" on="click"}}>
<LinkTo @route="site" data-test-nav="site" @current-when={{this.isOnSite}} @preventDefault={{false}}>
{{svg-jar "view-site"}} View site
</LinkTo>
</span>
<a href="{{this.config.blogUrl}}/" class="gh-secondary-action" title="Open site in new tab" target="_blank">
<span>{{svg-jar "external"}}</span>
</a>
</li>
</ul>
<ul class="gh-nav-list gh-nav-manage">
<li class="gh-nav-list-new relative">
<GhLinkToCustomViewsIndex @route="posts" @query={{reset-query-params "posts"}} data-test-nav="posts">{{svg-jar "posts"}}Posts</GhLinkToCustomViewsIndex>
<LinkTo @route="editor.new" @model="post" @classNames="gh-secondary-action gh-nav-new-post" @alt="New post" @title="New post" data-test-nav="new-story"><span>{{svg-jar "add-stroke"}}</span></LinkTo>
{{#if this.customViews.forPosts}}
<button type="button" class="gh-nav-button-expand {{if this.navigation.settings.expanded.posts "expanded"}}" {{on "click" (fn this.navigation.toggleExpansion "posts")}} aria-label="{{if this.navigation.settings.expanded.posts "Collapse custom post types" "Expand custom post types"}}">
{{svg-jar (if this.navigation.settings.expanded.posts "arrow-down-stroke" "arrow-right-stroke")}}
</button>
{{#liquid-if this.navigation.settings.expanded.posts}}
<ul class="gh-nav-view-list">
{{#each this.customViews.forPosts as |view|}}
<li>
<LinkTo @route="posts" @query={{reset-query-params "posts" view.filter}} data-test-nav-custom="{{view.route}}-{{view.name}}" title="{{view.name}}">
<span class="gh-nav-viewname">{{view.name}}</span>
<span class="flex items-center svg-{{view.color}}">
{{#unless view.icon}}
<span class="absolute circle"></span>
{{/unless}}
</span>
</LinkTo>
</li>
{{/each}}
</ul>
{{/liquid-if}}
{{/if}}
</li>
<li>
{{!-- clicking the Content link whilst on the content screen should reset the filter --}}
{{#if (eq this.router.currentRouteName "pages")}}
<LinkTo @route="pages" @query={{reset-query-params "pages"}} @classNames="active" data-test-nav="pages">{{svg-jar "page"}}Pages</LinkTo>
{{else}}
<LinkTo @route="pages" data-test-nav="pages">{{svg-jar "page"}}Pages</LinkTo>
{{/if}}
</li>
{{#if this.showTagsNavigation}}
<li><LinkTo @route="tags" data-test-nav="tags">{{svg-jar "tag"}}Tags</LinkTo></li>
{{/if}}
{{#if (gh-user-can-admin this.session.user)}}
<section class="gh-nav-body">
<div class="gh-nav-top">
<ul class="gh-nav-list gh-nav-main">
{{#if (gh-user-can-admin this.session.user)}}
<li class="relative">
{{#if (eq this.router.currentRouteName "members.index")}}
<LinkTo @route="members" @current-when="members member" @query={{reset-query-params "members.index"}} data-test-nav="members">{{svg-jar "members"}}Members
{{#unless this.memberCountLoading}}
<span class="gh-nav-member-count">{{format-number this.memberCount}}</span>
{{/unless}}
</LinkTo>
{{else}}
<LinkTo @route="members" @current-when="members member" data-test-nav="members">{{svg-jar "members"}}Members
{{#unless this.memberCountLoading}}
<span class="gh-nav-member-count">{{format-number this.memberCount}}</span>
{{/unless}}
</LinkTo>
{{/if}}
</li>
{{#if (feature "offers")}}
<li>
<LinkTo @route="offers" @alt="Offers">{{svg-jar "percentage"}}Offers</LinkTo>
</li>
{{else}}
<li>
<LinkTo @route="integrations" @alt="Integrations" @title="Integrations" data-test-nav="dashboard">{{svg-jar "module"}}Integrations</LinkTo>
<LinkTo @route="dashboard" @alt="Dashboard" @title="Dashboard" data-test-nav="dashboard">{{svg-jar "house"}} Dashboard</LinkTo>
</li>
{{/if}}
{{/if}}
{{#unless (feature "offers")}}
<li><LinkTo @route="staff" data-test-nav="staff">{{svg-jar "staff"}}Staff</LinkTo></li>
{{/unless}}
</ul>
{{#if this.session.user.isOwnerOnly}}
<ul class="gh-nav-list">
{{#if this.showBilling}}
<li class="relative">
<a href="javascript:void(0)" class={{if this.billing.billingWindowOpen "active"}} {{action "toggleBillingModal" }} data-test-nav="billing">
{{svg-jar "credit-card"}} Ghost(Pro)
<span {{action "transitionToOrRefreshSite" on="click"}}>
<LinkTo @route="site" data-test-nav="site" @current-when={{this.isOnSite}} @preventDefault={{false}}>
{{svg-jar "view-site"}} View site
</LinkTo>
</span>
<a href="{{this.config.blogUrl}}/" class="gh-secondary-action" title="Open site in new tab" target="_blank">
<span>{{svg-jar "external"}}</span>
</a>
</li>
<li class="relative gh-nav-pro">
<GhBillingUpdateButton />
</li>
{{/if}}
</ul>
{{/if}}
{{#if this.showMenuExtension}}
<ul class="gh-nav-list gh-nav-settings">
{{#if this.config.clientExtensions.menu.title}}
<li class="gh-nav-list-h">{{this.config.clientExtensions.menu.title}}</li>
{{/if}}
{{#each this.config.clientExtensions.menu.items as |menuItem| }}
<li>
<a href="{{menuItem.href}}" target="_blank">{{svg-jar menuItem.icon}}{{menuItem.text}}</a>
</li>
{{/each}}
</ul>
{{/if}}
<ul class="gh-nav-list gh-nav-manage">
<li class="gh-nav-list-new relative">
<GhLinkToCustomViewsIndex @route="posts" @query={{reset-query-params "posts"}} data-test-nav="posts">{{svg-jar "posts"}}Posts</GhLinkToCustomViewsIndex>
<LinkTo @route="editor.new" @model="post" @classNames="gh-secondary-action gh-nav-new-post" @alt="New post" @title="New post" data-test-nav="new-story"><span>{{svg-jar "add-stroke"}}</span></LinkTo>
{{#if this.customViews.forPosts}}
<button type="button" class="gh-nav-button-expand {{if this.navigation.settings.expanded.posts "expanded"}}" {{on "click" (fn this.navigation.toggleExpansion "posts")}} aria-label="{{if this.navigation.settings.expanded.posts "Collapse custom post types" "Expand custom post types"}}">
{{svg-jar (if this.navigation.settings.expanded.posts "arrow-down-stroke" "arrow-right-stroke")}}
</button>
{{#liquid-if this.navigation.settings.expanded.posts}}
<ul class="gh-nav-view-list">
{{#each this.customViews.forPosts as |view|}}
<li>
<LinkTo @route="posts" @query={{reset-query-params "posts" view.filter}} data-test-nav-custom="{{view.route}}-{{view.name}}" title="{{view.name}}">
<span class="gh-nav-viewname">{{view.name}}</span>
<span class="flex items-center svg-{{view.color}}">
{{#unless view.icon}}
<span class="absolute circle"></span>
{{/unless}}
</span>
</LinkTo>
</li>
{{/each}}
</ul>
{{/liquid-if}}
{{/if}}
</li>
<li>
{{!-- clicking the Content link whilst on the content screen should reset the filter --}}
{{#if (eq this.router.currentRouteName "pages")}}
<LinkTo @route="pages" @query={{reset-query-params "pages"}} @classNames="active" data-test-nav="pages">{{svg-jar "page"}}Pages</LinkTo>
{{else}}
<LinkTo @route="pages" data-test-nav="pages">{{svg-jar "page"}}Pages</LinkTo>
{{/if}}
</li>
{{#if this.showTagsNavigation}}
<li><LinkTo @route="tags" data-test-nav="tags">{{svg-jar "tag"}}Tags</LinkTo></li>
{{/if}}
{{#if (gh-user-can-admin this.session.user)}}
<li class="relative">
{{#if (eq this.router.currentRouteName "members.index")}}
<LinkTo @route="members" @current-when="members member" @query={{reset-query-params "members.index"}} data-test-nav="members">{{svg-jar "members"}}Members
{{#unless this.memberCountLoading}}
<span class="gh-nav-member-count">{{format-number this.memberCount}}</span>
{{/unless}}
</LinkTo>
{{else}}
<LinkTo @route="members" @current-when="members member" data-test-nav="members">{{svg-jar "members"}}Members
{{#unless this.memberCountLoading}}
<span class="gh-nav-member-count">{{format-number this.memberCount}}</span>
{{/unless}}
</LinkTo>
{{/if}}
</li>
{{#if this.showScriptExtension}}
{{{this.config.clientExtensions.script.container}}}
<script src="{{this.config.clientExtensions.script.src}}"></script>
{{/if}}
</div>
{{#if (feature "offers")}}
<li>
<LinkTo @route="offers" @alt="Offers">{{svg-jar "percentage"}}Offers</LinkTo>
</li>
{{else}}
<li>
<LinkTo @route="integrations" @alt="Integrations" @title="Integrations" data-test-nav="dashboard">{{svg-jar "module"}}Integrations</LinkTo>
</li>
{{/if}}
{{/if}}
{{#unless (feature "offers")}}
<li><LinkTo @route="staff" data-test-nav="staff">{{svg-jar "staff"}}Staff</LinkTo></li>
{{/unless}}
</ul>
<GhNavMenu::Footer />
{{#if this.session.user.isOwnerOnly}}
<ul class="gh-nav-list">
{{#if this.showBilling}}
<li class="relative">
<a href="javascript:void(0)" class={{if this.billing.billingWindowOpen "active"}} {{action "toggleBillingModal" }} data-test-nav="billing">
{{svg-jar "credit-card"}} Ghost(Pro)
</a>
</li>
<li class="relative gh-nav-pro">
<GhBillingUpdateButton />
</li>
{{/if}}
</ul>
{{/if}}
</section>
{{#if this.showMenuExtension}}
<ul class="gh-nav-list gh-nav-settings">
{{#if this.config.clientExtensions.menu.title}}
<li class="gh-nav-list-h">{{this.config.clientExtensions.menu.title}}</li>
{{/if}}
{{#each this.config.clientExtensions.menu.items as |menuItem| }}
<li>
<a href="{{menuItem.href}}" target="_blank">{{svg-jar menuItem.icon}}{{menuItem.text}}</a>
</li>
{{/each}}
</ul>
{{/if}}
{{#if this.showScriptExtension}}
{{{this.config.clientExtensions.script.container}}}
<script src="{{this.config.clientExtensions.script.src}}"></script>
{{/if}}
</div>
<GhNavMenu::Footer />
</section>
</div>

View File

@ -103,6 +103,7 @@
transform: translateX(0);
border-right: 1px solid var(--main-color-area-divider);
transition: flex-basis 250ms;
overflow: hidden;
}
.gh-nav-menu {
@ -199,6 +200,49 @@
padding: 0;
}
.gh-nav-main-enter-active,
.gh-nav-main-leave-active,
.gh-nav-contextual-enter-active,
.gh-nav-contextual-leave-active {
position: absolute;
top: 0;
height: 100%;
transition: transform 800ms ease;
}
.gh-nav-main-enter-active,
.gh-nav-main-leave-active {
width: calc(var(--mainmenu-width) - 1px);
}
.gh-nav-main-enter,
.gh-nav-main-leave-to {
transform: translateX(-100%);
}
.gh-nav-contextual-enter-active,
.gh-nav-contextual-leave-active {
width: calc(420px - 1px);
}
/* For some reason the animate-in transition wasn't working with pure transforms */
/* Using `left` correctly positioned the starting state so we could then use opposite transforms */
.gh-nav-contextual-enter-active {
left: calc(420px - 1px);
}
.gh-nav-contextual-enter-to {
transform: translateX(-100%);
}
.gh-nav-contextual-leave {
transform: translateX(0);
}
.gh-nav-contextual-leave-to {
transform: translateX(calc(var(--mainmenu-width) - 1px));
}
.gh-account-menu-header {
position: relative;
display: flex;

View File

@ -79,6 +79,7 @@
"ember-composable-helpers": "4.5.0",
"ember-concurrency": "2.1.2",
"ember-concurrency-decorators": "2.0.3",
"ember-css-transitions": "2.1.1",
"ember-data": "3.21.2",
"ember-decorators": "6.1.1",
"ember-drag-drop": "0.4.8",

View File

@ -6867,6 +6867,15 @@ ember-cookies@^0.5.0:
ember-cli-babel "^7.1.0"
ember-getowner-polyfill "^1.1.0 || ^2.0.0"
ember-css-transitions@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ember-css-transitions/-/ember-css-transitions-2.1.1.tgz#616069b8adef0175ec310cf504587587e8789f65"
integrity sha512-Kue3tMUHlmeEQvnV1YXoJSjk/wIKiywAT72ny89Yl7rRzEjgjOMcUD69HSg3ShsQNOpyzU0eOCANVtk00FjJig==
dependencies:
ember-cli-babel "^7.22.1"
ember-cli-htmlbars "^5.3.1"
ember-modifier "^2.1.0"
ember-data@3.21.2:
version "3.21.2"
resolved "https://registry.yarnpkg.com/ember-data/-/ember-data-3.21.2.tgz#d3537319a8d1c80a8a567f08d86b740722d502fe"