diff --git a/addon/components/o-s-s/anchor.hbs b/addon/components/o-s-s/anchor.hbs index 9872feed6..0d1afd4c7 100644 --- a/addon/components/o-s-s/anchor.hbs +++ b/addon/components/o-s-s/anchor.hbs @@ -1,5 +1,5 @@ {{#if this.isInternalRoute}} - {{yield}} + {{yield}} {{else}} {{yield}} {{/if}} \ No newline at end of file diff --git a/addon/components/o-s-s/anchor.stories.js b/addon/components/o-s-s/anchor.stories.js index 5394b58e2..6afa4246b 100644 --- a/addon/components/o-s-s/anchor.stories.js +++ b/addon/components/o-s-s/anchor.stories.js @@ -14,6 +14,26 @@ export default { }, control: { type: 'text' } }, + routePrefix: { + description: 'Optional route prefix (engine) name to use for routing', + table: { + type: { + summary: 'string' + }, + defaultValue: { summary: '' } + }, + control: { type: 'text' } + }, + disableAutoActive: { + description: 'Disables automatic active state based on current route', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'false' } + }, + control: { + type: 'boolean' + } + }, noreferrer: { description: 'Enables the noreferrer rel attribute', table: { @@ -56,6 +76,7 @@ const DefaultUsageTemplate = (args) => ({ export const BasicUsage = DefaultUsageTemplate.bind({}); BasicUsage.args = { link: 'https://www.upfluence.com', + disableAutoActive: false, noopener: true, noreferrer: true }; diff --git a/addon/components/o-s-s/anchor.ts b/addon/components/o-s-s/anchor.ts index aaecda242..c58b21df8 100644 --- a/addon/components/o-s-s/anchor.ts +++ b/addon/components/o-s-s/anchor.ts @@ -4,8 +4,10 @@ import RouterService from '@ember/routing/router-service'; interface OSSAnchorArgs { link: string; + routePrefix?: string; noopener?: boolean; noreferrer?: boolean; + disableAutoActive?: boolean; } export default class OSSAnchor extends Component { @@ -19,6 +21,10 @@ export default class OSSAnchor extends Component { return this.args.noreferrer ?? true; } + get currentWhen(): boolean | undefined { + return this.args.disableAutoActive !== undefined ? !this.args.disableAutoActive : undefined; + } + get rel(): string { const relations: string[] = []; @@ -34,8 +40,9 @@ export default class OSSAnchor extends Component { } get isInternalRoute(): boolean { + const route = this.args.routePrefix ? this.args.routePrefix + '.' + this.args.link : this.args.link; try { - return Boolean(this.router.urlFor(this.args.link)); + return Boolean(this.router.urlFor(route)); } catch (error) { return false; } diff --git a/addon/components/o-s-s/layout/sidebar.hbs b/addon/components/o-s-s/layout/sidebar.hbs index 5962e7154..7606a1dbe 100644 --- a/addon/components/o-s-s/layout/sidebar.hbs +++ b/addon/components/o-s-s/layout/sidebar.hbs @@ -1,5 +1,9 @@
- {{#if (or @homeParameters (and @logo @homeURL))}} + {{#if (has-block "header")}} +
+ {{yield (hash expanded=this.expanded) to="header"}} +
+ {{else if (or @homeParameters (and @logo @homeURL))}}
- {{#if @expandable}} + {{#if this.displayExpandedStateToggle}}
({ template: hbs`
- + <:content as |sidebar|> diff --git a/addon/components/o-s-s/layout/sidebar.ts b/addon/components/o-s-s/layout/sidebar.ts index eff2ec6c3..4806a2cd0 100644 --- a/addon/components/o-s-s/layout/sidebar.ts +++ b/addon/components/o-s-s/layout/sidebar.ts @@ -18,6 +18,7 @@ interface OSSLayoutSidebarArgs { logo?: string; homeURL?: string; expandable?: boolean; + alwaysExpanded?: boolean; } export default class OSSLayoutSidebar extends Component { @@ -41,6 +42,10 @@ export default class OSSLayoutSidebar extends Component { return classes.join(' '); } + get displayExpandedStateToggle(): boolean { + return Boolean(this.args.expandable) && !this.args.alwaysExpanded; + } + @action toggleExpandedState(): void { this.expanded = !this.expanded; @@ -52,7 +57,9 @@ export default class OSSLayoutSidebar extends Component { } private initializeSidebarState(): void { - this.expanded = Boolean(this.args.expandable) && this.upfLocalStorage.getItem(SIDEBAR_EXPANDED_STATE) !== 'false'; + this.expanded = + this.args.alwaysExpanded || + (Boolean(this.args.expandable) && this.upfLocalStorage.getItem(SIDEBAR_EXPANDED_STATE) !== 'false'); document.documentElement.style.setProperty( '--sidebar-width', 'var(--sidebar-' + (this.expanded ? 'expanded' : 'default') + '-width)' diff --git a/addon/components/o-s-s/layout/sidebar/group.hbs b/addon/components/o-s-s/layout/sidebar/group.hbs index e38294490..fb0f9f8f4 100644 --- a/addon/components/o-s-s/layout/sidebar/group.hbs +++ b/addon/components/o-s-s/layout/sidebar/group.hbs @@ -59,8 +59,10 @@ @locked={{item.locked}} @hasNotifications={{item.hasNotifications}} @link={{item.link}} + @routePrefix={{item.routePrefix}} @lockedAction={{item.lockedAction}} @action={{item.action}} + @disableAutoActive={{item.disableAutoActive}} data-control-name={{item.dataControlName}} class={{if item.active "active"}} /> diff --git a/addon/components/o-s-s/layout/sidebar/group.ts b/addon/components/o-s-s/layout/sidebar/group.ts index 1ffa5af48..d02295dc0 100644 --- a/addon/components/o-s-s/layout/sidebar/group.ts +++ b/addon/components/o-s-s/layout/sidebar/group.ts @@ -9,8 +9,10 @@ export type GroupItem = { label?: string; hasNotifications?: boolean; link: string; + routePrefix?: string; active: boolean; dataControlName?: string; + disableAutoActive?: boolean; lockedAction?(): unknown; action?(): void; }; diff --git a/addon/components/o-s-s/layout/sidebar/item.hbs b/addon/components/o-s-s/layout/sidebar/item.hbs index 19a864cb3..c4ee48e28 100644 --- a/addon/components/o-s-s/layout/sidebar/item.hbs +++ b/addon/components/o-s-s/layout/sidebar/item.hbs @@ -1,5 +1,7 @@ {{yield (hash expanded=@expanded) to="icon"}}
- {{else}} + {{else if @icon}}
{{/if}} -
+
{{@label}}
diff --git a/addon/components/o-s-s/layout/sidebar/item.stories.js b/addon/components/o-s-s/layout/sidebar/item.stories.js index 73bd4e5f7..56fd9adcc 100644 --- a/addon/components/o-s-s/layout/sidebar/item.stories.js +++ b/addon/components/o-s-s/layout/sidebar/item.stories.js @@ -65,6 +65,16 @@ export default { type: 'text' } }, + disableAutoActive: { + description: 'Disables automatic active state based on current route, managed by Ember by default', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'false' } + }, + control: { + type: 'boolean' + } + }, lockedAction: { description: 'Function to be called on click when item is locked', table: { diff --git a/addon/components/o-s-s/layout/sidebar/item.ts b/addon/components/o-s-s/layout/sidebar/item.ts index f2707f618..f61149a3e 100644 --- a/addon/components/o-s-s/layout/sidebar/item.ts +++ b/addon/components/o-s-s/layout/sidebar/item.ts @@ -5,12 +5,14 @@ import type { OSSTagArgs } from '@upfluence/oss-components/components/o-s-s/tag' interface OSSLayoutSidebarItemArgs { link: string; + routePrefix?: string; icon?: string; locked?: boolean; hasNotifications?: boolean; tag?: Pick; expanded?: boolean; label?: string; + disableAutoActive?: boolean; lockedAction?(): void; action?(): void; } diff --git a/app/styles/organisms/sidebar.less b/app/styles/organisms/sidebar.less index 184e3d4c8..aa110a436 100644 --- a/app/styles/organisms/sidebar.less +++ b/app/styles/organisms/sidebar.less @@ -52,6 +52,13 @@ } } + &__header { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + } + &__content { padding: var(--spacing-px-12) var(--spacing-px-9); display: flex; @@ -87,6 +94,8 @@ a.oss-sidebar-item { display: flex; justify-content: flex-start; align-items: center; + min-width: 28px; + min-height: 28px; color: var(--sidebar-font-color); font-size: var(--font-size-sm); @@ -108,6 +117,10 @@ a.oss-sidebar-item { overflow: hidden; white-space: nowrap; transition: width 300ms ease-in-out, opacity 300ms ease-in-out; + + &--no-icon { + padding-left: var(--spacing-px-9); + } } &__notification { diff --git a/tests/integration/components/o-s-s/anchor-test.ts b/tests/integration/components/o-s-s/anchor-test.ts index a07916f36..262b48143 100644 --- a/tests/integration/components/o-s-s/anchor-test.ts +++ b/tests/integration/components/o-s-s/anchor-test.ts @@ -3,6 +3,13 @@ import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import sinon from 'sinon'; +import Service from '@ember/service'; + +class RoutingMock extends Service { + currentState = 'index'; + generateURL = sinon.stub().returns('/'); + isActiveForRoute = sinon.stub(); +} module('Integration | Component | o-s-s/anchor', function (hooks) { setupRenderingTest(hooks); @@ -13,15 +20,51 @@ module('Integration | Component | o-s-s/anchor', function (hooks) { this.transitionToStub = sinon.stub(this.router, 'transitionTo'); }); - test('When link is registered in router it renders as a anchor element', async function (assert) { - await render(hbs`test`); + test('When link is not registered in router it renders as a anchor element', async function (assert) { + await render(hbs`test`); assert.dom('a').hasNoClass('ember-view'); + assert.dom('a').hasAttribute('href', 'http://www.google.fr'); }); - test('When link is registered in router it renders as a linkTo helper', async function (assert) { - await render(hbs`test`); + module('When link is registered in router', function (hooks) { + hooks.beforeEach(function () { + this.owner.register('service:-routing', RoutingMock); + this.routing = this.owner.lookup('service:-routing'); + }); + + test('When routePrefix is defined and an internal route is given, it renders as a linkTo helper', async function (assert) { + const urlForStub = sinon.stub(this.router, 'urlFor').returns('/display'); + await render(hbs`test`); + + assert.ok(urlForStub.calledWithExactly('display.index')); + assert.dom('a').hasClass('ember-view'); + assert.dom('a').hasAttribute('href', '/'); + }); + + test('With an internal route, it renders as a linkTo helper', async function (assert) { + const urlForSpy = sinon.spy(this.router, 'urlFor'); + await render(hbs`test`); + + assert.ok(urlForSpy.calledWithExactly('index')); + assert.dom('a').hasClass('ember-view'); + assert.dom('a').hasAttribute('href', '/'); + }); + + test('It renders as active by default when on the correct route', async function (assert) { + this.routing.isActiveForRoute.returns(true); + await render(hbs`test`); + + assert.dom('a').hasClass('ember-view'); + assert.dom('a').hasClass('active'); + }); + + test('When disableAutoActive is true and the route matches, it does not render as active', async function (assert) { + this.routing.isActiveForRoute.returns(false); + await render(hbs`test`); - assert.dom('a').hasClass('ember-view'); + assert.dom('a').hasClass('ember-view'); + assert.dom('a').doesNotHaveClass('active'); + }); }); }); diff --git a/tests/integration/components/o-s-s/layout/sidebar-test.ts b/tests/integration/components/o-s-s/layout/sidebar-test.ts index 21ae5abf1..e29b07f8e 100644 --- a/tests/integration/components/o-s-s/layout/sidebar-test.ts +++ b/tests/integration/components/o-s-s/layout/sidebar-test.ts @@ -1,7 +1,7 @@ import { hbs } from 'ember-cli-htmlbars'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { click, render, settled } from '@ember/test-helpers'; +import { click, render } from '@ember/test-helpers'; import { setupIntl } from 'ember-intl/test-support'; import UPFLocalStorage from '@upfluence/oss-components/utils/upf-local-storage'; @@ -58,6 +58,18 @@ module('Integration | Component | o-s-s/layout/sidebar', function (hooks) { }); module('Named blocks', () => { + test('The header named-block is properly displayed', async function (assert) { + await render( + hbs` + + <:header> +

This is the header

+ +
` + ); + assert.dom('.oss-sidebar-container__header').hasText('This is the header'); + }); + test('The content named-block is properly displayed', async function (assert) { await render( hbs` @@ -133,7 +145,6 @@ module('Integration | Component | o-s-s/layout/sidebar', function (hooks) { assert.dom('.oss-sidebar-container').hasClass('oss-sidebar-container--expanded'); await click('[data-control-name="sidebar-expanded-state-toggle"]'); - await settled(); assert.dom('.oss-sidebar-container').doesNotHaveClass('oss-sidebar-container--expanded'); assert.equal(window.localStorage.getItem('_upf.oss-layout-sidebar-expanded'), 'false'); @@ -144,4 +155,43 @@ module('Integration | Component | o-s-s/layout/sidebar', function (hooks) { assert.equal(window.localStorage.getItem('_upf.oss-layout-sidebar-expanded'), 'true'); }); }); + + module('AlwaysExpanded behavior', (hooks) => { + hooks.beforeEach(function () { + this.expandable = true; + this.homeParameters = { + logo: '/assets/images/brand-icon.svg', + url: '/home' + }; + }); + + test('the toggle button is not displayed when alwaysExpanded is true', async function (assert) { + await render( + hbs`` + ); + assert.dom('.oss-sidebar-container__expand').doesNotExist(); + }); + + test('the toggle button is displayed when alwaysExpanded is false', async function (assert) { + await render( + hbs`` + ); + assert.dom('.oss-sidebar-container__expand').exists(); + assert.dom('.oss-sidebar-container__expand').hasText(this.intl.t('oss-components.sidebar.collapse')); + }); + + test('the sidebar is expanded when alwaysExpanded is true and expandable is true', async function (assert) { + await render( + hbs`` + ); + assert.dom('.oss-sidebar-container').hasClass('oss-sidebar-container--expanded'); + }); + + test('the sidebar is expanded when alwaysExpanded is true and expandable is false', async function (assert) { + await render( + hbs`` + ); + assert.dom('.oss-sidebar-container').hasClass('oss-sidebar-container--expanded'); + }); + }); }); diff --git a/tests/integration/components/o-s-s/layout/sidebar/item-test.ts b/tests/integration/components/o-s-s/layout/sidebar/item-test.ts index ba42b9274..0343470c2 100644 --- a/tests/integration/components/o-s-s/layout/sidebar/item-test.ts +++ b/tests/integration/components/o-s-s/layout/sidebar/item-test.ts @@ -8,17 +8,24 @@ module('Integration | Component | oss/layout/sidebar/item', function (hooks) { setupRenderingTest(hooks); test('it renders', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-sidebar-item').exists(); }); test('it renders the icon when present', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-sidebar-item .oss-sidebar-item__icon i').hasClass('fa-search'); }); + test('it does not render the icon div when no icon is provided', async function (assert) { + await render(hbs``); + + assert.dom('.oss-sidebar-item .oss-sidebar-item__icon').doesNotExist(); + assert.dom('.oss-sidebar-item__label--no-icon').exists(); + }); + test('it renders the icon named block instead of the icon argument when present', async function (assert) { await render( hbs`<:icon>` @@ -29,17 +36,17 @@ module('Integration | Component | oss/layout/sidebar/item', function (hooks) { module('Arguments', () => { test('Default value for locked is false', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-sidebar-item__lock').doesNotExist(); }); test('When locked is true', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-sidebar-item__lock').exists(); }); test('Default value for hasNotification is false', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-sidebar-item__notification').doesNotExist(); }); @@ -50,12 +57,12 @@ module('Integration | Component | oss/layout/sidebar/item', function (hooks) { module('Expanded state', () => { test('the wrapper container is applied', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-sidebar-item').hasClass('oss-sidebar-item--expanded'); }); test('the label is displayed', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-sidebar-item__label').exists(); assert.dom('.oss-sidebar-item__label').hasText('Label'); }); @@ -72,7 +79,7 @@ module('Integration | Component | oss/layout/sidebar/item', function (hooks) { test('on click, it redirect to the @link attribute', async function (assert) { const router = this.owner.lookup('service:router'); await render( - hbs`` + hbs`` ); assert.equal(router.currentRouteName, null); @@ -82,7 +89,7 @@ module('Integration | Component | oss/layout/sidebar/item', function (hooks) { test('When locked is true lockedAction is triggered', async function (assert) { await render( - hbs`` + hbs`` ); await click('.oss-sidebar-item'); @@ -92,7 +99,7 @@ module('Integration | Component | oss/layout/sidebar/item', function (hooks) { test('on click, when item is not locked, the action is called ', async function (assert) { await render( - hbs`` + hbs`` ); await click('.oss-sidebar-item'); @@ -103,12 +110,12 @@ module('Integration | Component | oss/layout/sidebar/item', function (hooks) { module('Extra attributes', () => { test('passing an extra class is applied to the component', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.my-extra-class').exists(); }); test('passing data-control-name works', async function (assert) { - await render(hbs``); + await render(hbs``); let inputWrapper: Element | null = find('.oss-sidebar-item'); assert.equal(inputWrapper?.getAttribute('data-control-name'), 'layout-sidebar'); });