Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion addon/components/o-s-s/anchor.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{{#if this.isInternalRoute}}
<LinkTo @route={{@link}} rel={{this.rel}} ...attributes>{{yield}}</LinkTo>
<LinkTo @route={{@link}} @current-when={{this.currentWhen}} rel={{this.rel}} ...attributes>{{yield}}</LinkTo>
{{else}}
<a href={{@link}} rel={{this.rel}} ...attributes>{{yield}}</a>
{{/if}}
21 changes: 21 additions & 0 deletions addon/components/o-s-s/anchor.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -56,6 +76,7 @@ const DefaultUsageTemplate = (args) => ({
export const BasicUsage = DefaultUsageTemplate.bind({});
BasicUsage.args = {
link: 'https://www.upfluence.com',
disableAutoActive: false,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storybook args defined but not used in template

Low Severity

The disableAutoActive property is defined in argTypes and added to BasicUsage.args, but the story template at line 71 does not pass @disableAutoActive={{this.disableAutoActive}} or @routePrefix={{this.routePrefix}} to the component. This creates non-functional Storybook controls that appear in the UI but have no effect when changed, which is confusing for developers using Storybook to explore the component's API.

Additional Locations (1)

Fix in Cursor Fix in Web

noopener: true,
noreferrer: true
};
9 changes: 8 additions & 1 deletion addon/components/o-s-s/anchor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OSSAnchorArgs> {
Expand All @@ -19,6 +21,10 @@ export default class OSSAnchor extends Component<OSSAnchorArgs> {
return this.args.noreferrer ?? true;
}

get currentWhen(): boolean | undefined {
return this.args.disableAutoActive !== undefined ? !this.args.disableAutoActive : undefined;
}

get rel(): string {
const relations: string[] = [];

Expand All @@ -34,8 +40,9 @@ export default class OSSAnchor extends Component<OSSAnchorArgs> {
}

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));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LinkTo route mismatch between validation and navigation

High Severity

The isInternalRoute getter validates whether routePrefix + '.' + link exists as a route, but the template passes only @link (without the prefix) to <LinkTo @route={{@link}}>. When routePrefix="display" and link="index", the component validates that "display.index" exists, but then creates a link that navigates to "index" instead. This causes navigation to the wrong route or a route error.

Additional Locations (1)

Fix in Cursor Fix in Web

} catch (error) {
return false;
}
Expand Down
8 changes: 6 additions & 2 deletions addon/components/o-s-s/layout/sidebar.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<div class={{this.computedClasses}} ...attributes>
{{#if (or @homeParameters (and @logo @homeURL))}}
{{#if (has-block "header")}}
<div class="oss-sidebar-container__header">
{{yield (hash expanded=this.expanded) to="header"}}
</div>
{{else if (or @homeParameters (and @logo @homeURL))}}
<div class="logo-container">
<OSS::Layout::Sidebar::Item
@link={{or @homeParameters.url @homeURL}}
Expand All @@ -19,7 +23,7 @@
{{yield (hash expanded=this.expanded) to="content"}}
</div>

{{#if @expandable}}
{{#if this.displayExpandedStateToggle}}
<div class="oss-sidebar-container__expand">
<OSS::Layout::Sidebar::Item
@icon={{if this.expanded "fa-arrow-left-to-line" "fa-arrow-right-to-line"}}
Expand Down
15 changes: 13 additions & 2 deletions addon/components/o-s-s/layout/sidebar.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ export default {
defaultValue: { summary: 'false' }
},
control: { type: 'boolean' }
},
alwaysExpanded: {
description: 'Whether the sidebar is always expanded and cannot be collapsed',
table: {
type: {
summary: 'boolean'
},
defaultValue: { summary: 'false' }
},
control: { type: 'boolean' }
}
},
parameters: {
Expand All @@ -40,13 +50,14 @@ const defaultArgs = {
homeParameters: {
logo: '/assets/images/brand-icon.svg',
url: 'https://www.upfluence.com'
}
},
alwaysExpanded: false
};

const Template = (args) => ({
template: hbs`
<div style="height:100vh; padding:5px;">
<OSS::Layout::Sidebar @expandable={{this.expandable}} @homeParameters={{this.homeParameters}} style="height:95vh; overflow: visible">
<OSS::Layout::Sidebar @expandable={{this.expandable}} @homeParameters={{this.homeParameters}} @alwaysExpanded={{this.alwaysExpanded}} style="height:95vh; overflow: visible">
<:content as |sidebar|>
<OSS::Layout::Sidebar::Item @expanded={{sidebar.expanded}} @icon="far fa-search" @label="Search" class="active" @homeAction={{this.homeAction}} />
<OSS::Layout::Sidebar::Item @expanded={{sidebar.expanded}} @icon="far fa-list" @label="Community" />
Expand Down
9 changes: 8 additions & 1 deletion addon/components/o-s-s/layout/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface OSSLayoutSidebarArgs {
logo?: string;
homeURL?: string;
expandable?: boolean;
alwaysExpanded?: boolean;
}

export default class OSSLayoutSidebar extends Component<OSSLayoutSidebarArgs> {
Expand All @@ -41,6 +42,10 @@ export default class OSSLayoutSidebar extends Component<OSSLayoutSidebarArgs> {
return classes.join(' ');
}

get displayExpandedStateToggle(): boolean {
return Boolean(this.args.expandable) && !this.args.alwaysExpanded;
}

@action
toggleExpandedState(): void {
this.expanded = !this.expanded;
Expand All @@ -52,7 +57,9 @@ export default class OSSLayoutSidebar extends Component<OSSLayoutSidebarArgs> {
}

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)'
Expand Down
2 changes: 2 additions & 0 deletions addon/components/o-s-s/layout/sidebar/group.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}
/>
Expand Down
2 changes: 2 additions & 0 deletions addon/components/o-s-s/layout/sidebar/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
6 changes: 4 additions & 2 deletions addon/components/o-s-s/layout/sidebar/item.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<OSS::Anchor
@link={{@link}}
@routePrefix={{@routePrefix}}
@disableAutoActive={{@disableAutoActive}}
class={{this.computedClasses}}
disabled={{this.locked}}
{{on "click" this.onClick}}
Expand All @@ -16,13 +18,13 @@
<div class="oss-sidebar-item__icon">
{{yield (hash expanded=@expanded) to="icon"}}
</div>
{{else}}
{{else if @icon}}
<div class="oss-sidebar-item__icon">
<OSS::Icon @style={{fa-icon-style @icon}} @icon={{fa-icon-value @icon}} />
</div>
{{/if}}

<div class="oss-sidebar-item__label">
<div class={{concat "oss-sidebar-item__label" (unless @icon " oss-sidebar-item__label--no-icon")}}>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Label no-icon class ignores icon named block usage

Medium Severity

The oss-sidebar-item__label--no-icon class is applied based solely on @icon being falsy via (unless @icon ...), but this doesn't account for when an icon is rendered via the named block. When using <:icon> without providing @icon (a valid pattern used in sidebar.hbs for the home logo), the label incorrectly receives the --no-icon class with extra left padding, even though an icon is visually present.

Fix in Cursor Fix in Web

{{@label}}
</div>

Expand Down
10 changes: 10 additions & 0 deletions addon/components/o-s-s/layout/sidebar/item.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 2 additions & 0 deletions addon/components/o-s-s/layout/sidebar/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OSSTagArgs, 'label' | 'skin' | 'icon'>;
expanded?: boolean;
label?: string;
disableAutoActive?: boolean;
lockedAction?(): void;
action?(): void;
}
Expand Down
13 changes: 13 additions & 0 deletions app/styles/organisms/sidebar.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand Down
53 changes: 48 additions & 5 deletions tests/integration/components/o-s-s/anchor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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`<OSS::Anchor @link="http://www.google.fr" target="_blank" >test</OSS::Anchor>`);
test('When link is not registered in router it renders as a anchor element', async function (assert) {
await render(hbs`<OSS::Anchor @link="http://www.google.fr" target="_blank">test</OSS::Anchor>`);

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`<OSS::Anchor @link="index" >test</OSS::Anchor>`);
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`<OSS::Anchor @link="index" @routePrefix="display">test</OSS::Anchor>`);

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`<OSS::Anchor @link="index">test</OSS::Anchor>`);

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`<OSS::Anchor @link="index">test</OSS::Anchor>`);

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`<OSS::Anchor @link="index" @disableAutoActive={{true}}>test</OSS::Anchor>`);

assert.dom('a').hasClass('ember-view');
assert.dom('a').hasClass('ember-view');
assert.dom('a').doesNotHaveClass('active');
});
});
});
Loading
Loading