From 6ef73f900c78309c92f3f05125509297c609d860 Mon Sep 17 00:00:00 2001 From: Olympe Lespagnon Date: Mon, 19 Jan 2026 15:57:11 +0100 Subject: [PATCH 01/45] Add feedback message parameter to banner component --- addon/components/o-s-s/banner.hbs | 60 +++++++++++-------- addon/components/o-s-s/banner.stories.js | 10 ++++ addon/components/o-s-s/banner.ts | 8 ++- app/styles/banner.less | 4 ++ tests/dummy/app/templates/index.hbs | 18 ++++++ .../components/o-s-s/banner-test.ts | 27 +++++++++ 6 files changed, 100 insertions(+), 27 deletions(-) diff --git a/addon/components/o-s-s/banner.hbs b/addon/components/o-s-s/banner.hbs index 63a77dd20..d612e8919 100644 --- a/addon/components/o-s-s/banner.hbs +++ b/addon/components/o-s-s/banner.hbs @@ -1,30 +1,38 @@ -
- {{#if (has-block "custom-icon")}} -
{{yield to="custom-icon"}}
- {{else if @icon}} - - {{else if @image}} - banner - {{/if}} -
- {{#if @title}} -
- {{@title}} - {{#if (has-block "title-suffix")}} - {{yield to="title-suffix"}} - {{/if}} -
- {{/if}} - {{#if @subtitle}} - {{@subtitle}} +
+
+ {{#if (has-block "custom-icon")}} +
{{yield to="custom-icon"}}
+ {{else if @icon}} + + {{else if @image}} + banner {{/if}} - {{#if (has-block "secondary-actions")}} - {{yield to="secondary-actions"}} +
+ {{#if @title}} +
+ {{@title}} + {{#if (has-block "title-suffix")}} + {{yield to="title-suffix"}} + {{/if}} +
+ {{/if}} + {{#if @subtitle}} + {{@subtitle}} + {{/if}} + {{#if (has-block "secondary-actions")}} + {{yield to="secondary-actions"}} + {{/if}} +
+ {{#if (has-block "actions")}} +
{{yield to="actions"}}
{{/if}}
- {{#if (has-block "actions")}} -
{{yield to="actions"}}
+ + {{#if @feedbackMessage.value}} + {{@feedbackMessage.value}} {{/if}} -
+
\ No newline at end of file diff --git a/addon/components/o-s-s/banner.stories.js b/addon/components/o-s-s/banner.stories.js index e56161f60..bb9bf76fb 100644 --- a/addon/components/o-s-s/banner.stories.js +++ b/addon/components/o-s-s/banner.stories.js @@ -77,6 +77,16 @@ export default { type: 'boolean' } }, + feedbackMessage: { + description: 'An error message that will be displayed below the banner.', + table: { + type: { + summary: '{ type: string, value: string }' + }, + defaultValue: { summary: 'undefined' } + }, + control: { type: 'object' } + }, size: { description: 'Allows to adjust the size of the component. Currently available options are `sm`, `md` and `lg`. Defaults to `md`.', diff --git a/addon/components/o-s-s/banner.ts b/addon/components/o-s-s/banner.ts index 97623b832..5bf9cf305 100644 --- a/addon/components/o-s-s/banner.ts +++ b/addon/components/o-s-s/banner.ts @@ -1,5 +1,6 @@ import { isBlank } from '@ember/utils'; import Component from '@glimmer/component'; +import type { FeedbackMessage } from './input-container'; type SizeType = 'sm' | 'md' | 'lg'; @@ -8,6 +9,7 @@ interface OSSBannerArgs { plain?: boolean; selected?: boolean; disabled?: boolean; + feedbackMessage?: FeedbackMessage; } const SIZE_CLASSES: Record = { @@ -24,6 +26,10 @@ export default class OSSBanner extends Component { return this.args.selected ? 'upf-banner--selected' : ''; } + get erroredClass(): string { + return this.args.feedbackMessage ? 'upf-banner--errored' : ''; + } + get plainClass(): string { return this.args.plain ? 'background-color-gray-50' : 'background-color-white'; } @@ -33,7 +39,7 @@ export default class OSSBanner extends Component { } get modifierClasses(): string { - return [this.disabledClass, this.selectedClass, this.plainClass, this.sizeClass] + return [this.disabledClass, this.selectedClass, this.plainClass, this.sizeClass, this.erroredClass] .filter((mc) => !isBlank(mc)) .join(' '); } diff --git a/app/styles/banner.less b/app/styles/banner.less index 62e2d904d..f14082876 100644 --- a/app/styles/banner.less +++ b/app/styles/banner.less @@ -9,6 +9,10 @@ transition: ease-in-out 0.25s; } + &--errored { + border: 1px solid var(--color-error-500); + } + &--disabled { background-color: var(--color-gray-100); border: 1px solid var(--color-border-default); diff --git a/tests/dummy/app/templates/index.hbs b/tests/dummy/app/templates/index.hbs index 3d7e413b5..7bc5dc8b4 100644 --- a/tests/dummy/app/templates/index.hbs +++ b/tests/dummy/app/templates/index.hbs @@ -296,6 +296,24 @@
+
+ + + <:actions> + + + +
diff --git a/tests/integration/components/o-s-s/banner-test.ts b/tests/integration/components/o-s-s/banner-test.ts index 878ca38b3..71e6c9da7 100644 --- a/tests/integration/components/o-s-s/banner-test.ts +++ b/tests/integration/components/o-s-s/banner-test.ts @@ -156,4 +156,31 @@ module('Integration | Component | o-s-s/banner', function (hooks) { assert.dom('.upf-banner.upf-banner--size-lg').doesNotExist(); }); }); + + module('@feedbackMessage parameter', function () { + hooks.beforeEach(function () { + this.feedbackMessage = { + type: 'error', + value: 'This is a feedback message' + }; + }); + + test('When parameter is passed, it adds upf-banner--errored class', async function (assert) { + await render(hbs``); + + assert.dom('.upf-banner.upf-banner--errored').exists(); + }); + + test('When parameter is passed, the feedback message is swhown', async function (assert) { + await render(hbs``); + + assert.dom('.font-color-error-500').hasText('This is a feedback message'); + }); + + test('When parameter is not passed, it does not add upf-banner--errored class', async function (assert) { + await render(hbs``); + + assert.dom('.upf-banner.upf-banner--errored').doesNotExist(); + }); + }); }); From c006d4eab7c1157a87ce18cbf28bee95e2f81b21 Mon Sep 17 00:00:00 2001 From: Olympe Lespagnon Date: Mon, 19 Jan 2026 16:38:10 +0100 Subject: [PATCH 02/45] Fix test --- tests/integration/components/o-s-s/banner-test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/integration/components/o-s-s/banner-test.ts b/tests/integration/components/o-s-s/banner-test.ts index 71e6c9da7..a45be7160 100644 --- a/tests/integration/components/o-s-s/banner-test.ts +++ b/tests/integration/components/o-s-s/banner-test.ts @@ -152,6 +152,7 @@ module('Integration | Component | o-s-s/banner', function (hooks) { test("when the value is undefined, it doesn't add the size class", async function (assert) { await render(hbs``); + assert.dom('.upf-banner.upf-banner--size-sm').doesNotExist(); assert.dom('.upf-banner.upf-banner--size-lg').doesNotExist(); }); @@ -172,13 +173,13 @@ module('Integration | Component | o-s-s/banner', function (hooks) { }); test('When parameter is passed, the feedback message is swhown', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.font-color-error-500').hasText('This is a feedback message'); }); test('When parameter is not passed, it does not add upf-banner--errored class', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.upf-banner.upf-banner--errored').doesNotExist(); }); From a2f5de793c2a74b39051f4f7efcad3b9fc1eeb3c Mon Sep 17 00:00:00 2001 From: Olympe Lespagnon Date: Mon, 19 Jan 2026 17:22:44 +0100 Subject: [PATCH 03/45] Review --- addon/components/o-s-s/banner.hbs | 4 ++-- addon/components/o-s-s/banner.ts | 12 ++++++++--- app/styles/banner.less | 2 +- .../components/o-s-s/banner-test.ts | 20 +++++++++++++++---- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/addon/components/o-s-s/banner.hbs b/addon/components/o-s-s/banner.hbs index d612e8919..7fb0e0e65 100644 --- a/addon/components/o-s-s/banner.hbs +++ b/addon/components/o-s-s/banner.hbs @@ -32,7 +32,7 @@ {{/if}} - {{#if @feedbackMessage.value}} - {{@feedbackMessage.value}} + {{#if this.feedbackMessage}} + {{this.feedbackMessage}} {{/if}} \ No newline at end of file diff --git a/addon/components/o-s-s/banner.ts b/addon/components/o-s-s/banner.ts index 5bf9cf305..c3c9b0613 100644 --- a/addon/components/o-s-s/banner.ts +++ b/addon/components/o-s-s/banner.ts @@ -26,8 +26,8 @@ export default class OSSBanner extends Component { return this.args.selected ? 'upf-banner--selected' : ''; } - get erroredClass(): string { - return this.args.feedbackMessage ? 'upf-banner--errored' : ''; + get errorClass(): string { + return this.args.feedbackMessage ? 'upf-banner--error' : ''; } get plainClass(): string { @@ -38,8 +38,14 @@ export default class OSSBanner extends Component { return SIZE_CLASSES[this.args.size ?? 'md'] ?? ''; } + get feedbackMessage(): string | undefined { + return this.args.feedbackMessage && !isBlank(this.args.feedbackMessage.value) + ? this.args.feedbackMessage.value + : undefined; + } + get modifierClasses(): string { - return [this.disabledClass, this.selectedClass, this.plainClass, this.sizeClass, this.erroredClass] + return [this.disabledClass, this.selectedClass, this.plainClass, this.sizeClass, this.errorClass] .filter((mc) => !isBlank(mc)) .join(' '); } diff --git a/app/styles/banner.less b/app/styles/banner.less index f14082876..498a86c73 100644 --- a/app/styles/banner.less +++ b/app/styles/banner.less @@ -9,7 +9,7 @@ transition: ease-in-out 0.25s; } - &--errored { + &--error { border: 1px solid var(--color-error-500); } diff --git a/tests/integration/components/o-s-s/banner-test.ts b/tests/integration/components/o-s-s/banner-test.ts index a45be7160..9933dfda0 100644 --- a/tests/integration/components/o-s-s/banner-test.ts +++ b/tests/integration/components/o-s-s/banner-test.ts @@ -166,10 +166,10 @@ module('Integration | Component | o-s-s/banner', function (hooks) { }; }); - test('When parameter is passed, it adds upf-banner--errored class', async function (assert) { + test('When parameter is passed, it adds upf-banner--error class', async function (assert) { await render(hbs``); - assert.dom('.upf-banner.upf-banner--errored').exists(); + assert.dom('.upf-banner.upf-banner--error').exists(); }); test('When parameter is passed, the feedback message is swhown', async function (assert) { @@ -178,10 +178,22 @@ module('Integration | Component | o-s-s/banner', function (hooks) { assert.dom('.font-color-error-500').hasText('This is a feedback message'); }); - test('When parameter is not passed, it does not add upf-banner--errored class', async function (assert) { + test('When parameter is not passed, it does not add upf-banner--error class', async function (assert) { await render(hbs``); - assert.dom('.upf-banner.upf-banner--errored').doesNotExist(); + assert.dom('.upf-banner.upf-banner--error').doesNotExist(); + }); + + test('When parameter is passed with empty value, upf-banner--error class is added but no error message is displayed', async function (assert) { + this.feedbackMessage = { + type: 'error', + value: '' + }; + + await render(hbs``); + + assert.dom('.upf-banner.upf-banner--error').exists(); + assert.dom('.font-color-error-500').doesNotExist(); }); }); }); From ce5f0e3299819f892c485de1ca2dfc2d44a78031 Mon Sep 17 00:00:00 2001 From: Olympe Lespagnon Date: Mon, 19 Jan 2026 18:23:35 +0100 Subject: [PATCH 04/45] Facultative feedbackMessage.value --- addon/components/o-s-s/banner.hbs | 4 ++-- addon/components/o-s-s/banner.ts | 6 ------ tests/integration/components/o-s-s/banner-test.ts | 12 ------------ 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/addon/components/o-s-s/banner.hbs b/addon/components/o-s-s/banner.hbs index 7fb0e0e65..d612e8919 100644 --- a/addon/components/o-s-s/banner.hbs +++ b/addon/components/o-s-s/banner.hbs @@ -32,7 +32,7 @@ {{/if}} - {{#if this.feedbackMessage}} - {{this.feedbackMessage}} + {{#if @feedbackMessage.value}} + {{@feedbackMessage.value}} {{/if}} \ No newline at end of file diff --git a/addon/components/o-s-s/banner.ts b/addon/components/o-s-s/banner.ts index c3c9b0613..f745963e5 100644 --- a/addon/components/o-s-s/banner.ts +++ b/addon/components/o-s-s/banner.ts @@ -38,12 +38,6 @@ export default class OSSBanner extends Component { return SIZE_CLASSES[this.args.size ?? 'md'] ?? ''; } - get feedbackMessage(): string | undefined { - return this.args.feedbackMessage && !isBlank(this.args.feedbackMessage.value) - ? this.args.feedbackMessage.value - : undefined; - } - get modifierClasses(): string { return [this.disabledClass, this.selectedClass, this.plainClass, this.sizeClass, this.errorClass] .filter((mc) => !isBlank(mc)) diff --git a/tests/integration/components/o-s-s/banner-test.ts b/tests/integration/components/o-s-s/banner-test.ts index 9933dfda0..3d80eafa3 100644 --- a/tests/integration/components/o-s-s/banner-test.ts +++ b/tests/integration/components/o-s-s/banner-test.ts @@ -183,17 +183,5 @@ module('Integration | Component | o-s-s/banner', function (hooks) { assert.dom('.upf-banner.upf-banner--error').doesNotExist(); }); - - test('When parameter is passed with empty value, upf-banner--error class is added but no error message is displayed', async function (assert) { - this.feedbackMessage = { - type: 'error', - value: '' - }; - - await render(hbs``); - - assert.dom('.upf-banner.upf-banner--error').exists(); - assert.dom('.font-color-error-500').doesNotExist(); - }); }); }); From 61f35aaa8a5cfeea7d63a5a7e8ca5e9d49e91a93 Mon Sep 17 00:00:00 2001 From: Julien Vannier Date: Wed, 28 Jan 2026 18:24:32 +0100 Subject: [PATCH 05/45] Support zero values for skins in the OSS::ProgressBar --- addon/components/o-s-s/progress-bar.hbs | 35 ++++++++++++------- tests/dummy/app/templates/visual.hbs | 11 ++++++ .../components/o-s-s/progress-bar-test.ts | 35 +++++++++++-------- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/addon/components/o-s-s/progress-bar.hbs b/addon/components/o-s-s/progress-bar.hbs index 1bafa09ce..7420804ad 100644 --- a/addon/components/o-s-s/progress-bar.hbs +++ b/addon/components/o-s-s/progress-bar.hbs @@ -6,18 +6,29 @@
{{#if this.hasSkins}}
- {{#each this.progressSegments as |skin|}} - {{#if (this.isSegmentVisible skin=skin)}} -
- {{/if}} - {{/each}} + {{#if (eq this.totalProgress 0)}} +
+ {{else}} + {{#each this.progressSegments as |skin|}} + {{#if (this.isSegmentVisible skin=skin)}} +
+ {{/if}} + {{/each}} + {{/if}}
{{else}}
+ + `); + await render(hbs``); assert.dom('.oss-progress-bar').hasClass('oss-progress-bar--multi-skin'); }); test('if the value is defined, the progress bar has the correct success, warning, and danger classes', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-progress-bar__inner').hasClass('oss-progress-bar__inner--pending'); assert.dom('.oss-progress-bar__inner:nth-of-type(2)').hasClass('oss-progress-bar__inner--success'); @@ -200,26 +200,35 @@ module('Integration | Component | o-s-s/progress-bar', function (hooks) { assert.dom('.oss-progress-bar__inner:nth-of-type(4)').hasClass('oss-progress-bar__inner--danger'); }); + test('if the values are equals to 0, it renders an empty progress bar', async function (assert) { + this.skins = { pending: 0, success: 0, warning: 0, danger: 0 }; + await render(hbs``); + + assert + .dom('.oss-progress-bar .oss-progress-bar__inner') + .hasAttribute('style', 'width: 0%;--progress-bar-animation-width: 0%'); + }); + test('if the value is defined, the progress bar has the correct pending value', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-progress-bar__inner--pending').hasAttribute('aria-valuenow', '10'); }); test('if the value is defined, the progress bar has the correct success value', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-progress-bar__inner--success').hasAttribute('aria-valuenow', '30'); }); test('if the value is defined, the progress bar has the correct warning value', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-progress-bar__inner--warning').hasAttribute('aria-valuenow', '25'); }); test('if the value is defined, the progress bar has the correct danger value', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-progress-bar__inner--danger').hasAttribute('aria-valuenow', '15'); }); @@ -232,13 +241,11 @@ module('Integration | Component | o-s-s/progress-bar', function (hooks) { ); }); - await render( - hbs`` - ); + await render(hbs``); }); test('if the value is updated, the classes also update', async function (assert) { - await render(hbs``); + await render(hbs``); this.set('skins', { success: 50, warning: 30, danger: 20 }); @@ -249,28 +256,28 @@ module('Integration | Component | o-s-s/progress-bar', function (hooks) { }); test('if only the pending value is defined, the progress bar has the correct pending value', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-progress-bar__inner--pending').exists(); assert.dom('.oss-progress-bar__inner--pending').hasAttribute('aria-valuenow', '33'); }); test('if only the success value is defined, the progress bar has the correct success value', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-progress-bar__inner--success').exists(); assert.dom('.oss-progress-bar__inner--success').hasAttribute('aria-valuenow', '33'); }); test('if only the warning value is defined, the progress bar has the correct warning value', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-progress-bar__inner--warning').exists(); assert.dom('.oss-progress-bar__inner--warning').hasAttribute('aria-valuenow', '33'); }); test('if only the danger value is defined, the progress bar has the correct danger value', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('.oss-progress-bar__inner--danger').exists(); assert.dom('.oss-progress-bar__inner--danger').hasAttribute('aria-valuenow', '33'); From 69f6d5eb2b415c2b66102446f8eb88aa41194439 Mon Sep 17 00:00:00 2001 From: Julien Vannier Date: Thu, 29 Jan 2026 09:51:51 +0100 Subject: [PATCH 06/45] Simplify code logic for OSS::ProgressBar --- addon/components/o-s-s/progress-bar.hbs | 23 +++++------------------ addon/components/o-s-s/progress-bar.ts | 8 ++------ tests/dummy/app/controllers/visual.ts | 24 ++++++++++++++++++++++++ tests/dummy/app/templates/visual.hbs | 5 +++++ 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/addon/components/o-s-s/progress-bar.hbs b/addon/components/o-s-s/progress-bar.hbs index 7420804ad..3dadf794c 100644 --- a/addon/components/o-s-s/progress-bar.hbs +++ b/addon/components/o-s-s/progress-bar.hbs @@ -6,29 +6,16 @@
{{#if this.hasSkins}}
- {{#if (eq this.totalProgress 0)}} + {{#each this.progressSegments as |skin|}}
- {{else}} - {{#each this.progressSegments as |skin|}} - {{#if (this.isSegmentVisible skin=skin)}} -
- {{/if}} - {{/each}} - {{/if}} + {{/each}}
{{else}}
{ } } - isSegmentVisible = helper((_, { skin }: { skin: ProgressBarSkins }): boolean => { - return this.hasSkins && (this.skins[skin] || 0) > 0; - }); - getSegmentValue = helper((_, { skin }: { skin: ProgressBarSkins }): number => { - return this.hasSkins ? this.skins[skin] || 0 : 0; + return this.skins[skin] ?? 0; }); progressBarStyle = helper((_, { value }: { value: number }): ReturnType => { @@ -59,7 +55,7 @@ export default class OSSProgressBar extends Component { } get progressSegments(): ProgressBarSkins[] { - return this.hasSkins ? (Object.keys(this.skins) as ProgressBarSkins[]) : []; + return Object.keys(this.skins) as ProgressBarSkins[]; } get computedStyles(): string { diff --git a/tests/dummy/app/controllers/visual.ts b/tests/dummy/app/controllers/visual.ts index 5adb0a37a..02407c670 100644 --- a/tests/dummy/app/controllers/visual.ts +++ b/tests/dummy/app/controllers/visual.ts @@ -116,9 +116,14 @@ export default class Visual extends Controller { @tracked progressBarWarning: number = 25; @tracked progressBarDanger: number = 15; + @tracked progressBarSuccess2: number = 30; + @tracked progressBarWarning2: number = 25; + @tracked progressBarDanger2: number = 15; + constructor() { super(); this.liveProgressBar(); + this.liveProgressBarZero(); } get progressBarSkins(): Record { @@ -129,6 +134,14 @@ export default class Visual extends Controller { }; } + get progressBarSkins2(): Record { + return { + success: this.progressBarSuccess2, + warning: this.progressBarWarning2, + danger: this.progressBarDanger2 + }; + } + liveProgressBar = (negative: boolean = false): void => { const value = negative ? -1 : 1; setTimeout(() => { @@ -140,6 +153,17 @@ export default class Visual extends Controller { }, 2000); }; + liveProgressBarZero = (negative: boolean = false): void => { + const value = negative ? -1 : 1; + setTimeout(() => { + this.progressBarSuccess2 = value === 1 ? 0 : 20; + this.progressBarWarning2 = value === 1 ? 0 : 10; + this.progressBarDanger2 = value === 1 ? 0 : 5; + + this.liveProgressBarZero(!negative); + }, 2000); + }; + @action redirectTo(route: string): void { console.log('Redirect to', route); diff --git a/tests/dummy/app/templates/visual.hbs b/tests/dummy/app/templates/visual.hbs index d9309f4e6..0d9f5a749 100644 --- a/tests/dummy/app/templates/visual.hbs +++ b/tests/dummy/app/templates/visual.hbs @@ -42,6 +42,11 @@ @displayValue={{true}} @label="Multi-segment example (0%)" /> + Date: Thu, 29 Jan 2026 15:00:50 +0100 Subject: [PATCH 07/45] 3.89.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ea8954522..b0a4815c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@upfluence/oss-components", - "version": "3.88.15", + "version": "3.89.0", "description": "The default blueprint for ember-cli addons.", "keywords": [ "ember-addon" From 32507a3093303450a6ff00a3c9e28e31919e84d1 Mon Sep 17 00:00:00 2001 From: Nathalie Date: Mon, 2 Feb 2026 17:51:21 +0100 Subject: [PATCH 08/45] Added error warning and success feedback option --- addon/components/o-s-s/banner.hbs | 63 +++++++++-------- addon/components/o-s-s/banner.ts | 23 +++++-- app/styles/banner.less | 18 ++++- tests/dummy/app/templates/index.hbs | 21 +++--- .../components/o-s-s/banner-test.ts | 68 +++++++++++++++---- 5 files changed, 133 insertions(+), 60 deletions(-) diff --git a/addon/components/o-s-s/banner.hbs b/addon/components/o-s-s/banner.hbs index d612e8919..6cdf3e012 100644 --- a/addon/components/o-s-s/banner.hbs +++ b/addon/components/o-s-s/banner.hbs @@ -1,38 +1,37 @@ -
-
- {{#if (has-block "custom-icon")}} -
{{yield to="custom-icon"}}
- {{else if @icon}} - - {{else if @image}} - banner +
+ {{#if (has-block "custom-icon")}} +
{{yield to="custom-icon"}}
+ {{else if @icon}} + + {{else if @image}} + banner + {{/if}} +
+ {{#if @title}} +
+ {{@title}} + {{#if (has-block "title-suffix")}} + {{yield to="title-suffix"}} + {{/if}} +
{{/if}} -
- {{#if @title}} -
- {{@title}} - {{#if (has-block "title-suffix")}} - {{yield to="title-suffix"}} - {{/if}} -
- {{/if}} - {{#if @subtitle}} - {{@subtitle}} - {{/if}} - {{#if (has-block "secondary-actions")}} - {{yield to="secondary-actions"}} - {{/if}} -
- {{#if (has-block "actions")}} -
{{yield to="actions"}}
+ {{#if @subtitle}} + {{@subtitle}} + {{/if}} + {{#if (has-block "secondary-actions")}} + {{yield to="secondary-actions"}} {{/if}}
- + {{#if (has-block "actions")}} +
{{yield to="actions"}}
+ {{/if}} {{#if @feedbackMessage.value}} - {{@feedbackMessage.value}} + {{/if}}
\ No newline at end of file diff --git a/addon/components/o-s-s/banner.ts b/addon/components/o-s-s/banner.ts index f745963e5..966a1a3ae 100644 --- a/addon/components/o-s-s/banner.ts +++ b/addon/components/o-s-s/banner.ts @@ -9,6 +9,7 @@ interface OSSBannerArgs { plain?: boolean; selected?: boolean; disabled?: boolean; + hasError?: boolean; feedbackMessage?: FeedbackMessage; } @@ -26,10 +27,6 @@ export default class OSSBanner extends Component { return this.args.selected ? 'upf-banner--selected' : ''; } - get errorClass(): string { - return this.args.feedbackMessage ? 'upf-banner--error' : ''; - } - get plainClass(): string { return this.args.plain ? 'background-color-gray-50' : 'background-color-white'; } @@ -38,8 +35,24 @@ export default class OSSBanner extends Component { return SIZE_CLASSES[this.args.size ?? 'md'] ?? ''; } + get borderColorClass(): string { + if (this.args.hasError) return 'upf-banner--error'; + return `upf-banner--${this.args.feedbackMessage?.type}`; + } + + get feedbackMessageClass(): string { + return this.args.feedbackMessage?.value ? 'margin-bottom-px-24' : ''; + } + get modifierClasses(): string { - return [this.disabledClass, this.selectedClass, this.plainClass, this.sizeClass, this.errorClass] + return [ + this.disabledClass, + this.selectedClass, + this.plainClass, + this.sizeClass, + this.borderColorClass, + this.feedbackMessageClass + ] .filter((mc) => !isBlank(mc)) .join(' '); } diff --git a/app/styles/banner.less b/app/styles/banner.less index 498a86c73..60ba8c0a4 100644 --- a/app/styles/banner.less +++ b/app/styles/banner.less @@ -1,4 +1,6 @@ .upf-banner { + position: relative; + border: 1px solid var(--color-border-default); border-radius: var(--border-radius-lg); padding: var(--spacing-px-18); @@ -10,7 +12,15 @@ } &--error { - border: 1px solid var(--color-error-500); + .border-color-error; + } + + &--warning { + .border-color-warning; + } + + &--success { + .border-color-success; } &--disabled { @@ -37,6 +47,12 @@ transition: ease-in-out 0.25s; } +.upf-banner--feedback { + position: absolute; + bottom: -25px; + left: 0px; +} + .bg-color-white { background-color: var(--color-white); } diff --git a/tests/dummy/app/templates/index.hbs b/tests/dummy/app/templates/index.hbs index 7bc5dc8b4..fcc696a49 100644 --- a/tests/dummy/app/templates/index.hbs +++ b/tests/dummy/app/templates/index.hbs @@ -297,22 +297,23 @@
+ + - <:actions> - - - + @subtitle="A banner with a success feedback message" + @feedbackMessage={{hash type="success" value="This is a success feedback message"}} + />
diff --git a/tests/integration/components/o-s-s/banner-test.ts b/tests/integration/components/o-s-s/banner-test.ts index 3d80eafa3..f75de78ae 100644 --- a/tests/integration/components/o-s-s/banner-test.ts +++ b/tests/integration/components/o-s-s/banner-test.ts @@ -3,6 +3,8 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; +const ERROR_TYPES = ['error', 'warning', 'success']; + module('Integration | Component | o-s-s/banner', function (hooks) { setupRenderingTest(hooks); @@ -158,30 +160,72 @@ module('Integration | Component | o-s-s/banner', function (hooks) { }); }); + module('@hasError parameter', function () { + test('when the value is truthy, the border is errored', async function (assert) { + await render(hbs``); + assert.dom('.upf-banner.upf-banner--error').exists(); + }); + + test('when the value is falsy, the border is not errored', async function (assert) { + await render(hbs``); + assert.dom('.upf-banner.upf-banner--error').doesNotExist(); + }); + + test('when the value is undefined, the border is not errored', async function (assert) { + await render(hbs``); + assert.dom('.upf-banner.upf-banner--error').doesNotExist(); + }); + }); + module('@feedbackMessage parameter', function () { - hooks.beforeEach(function () { + module('feedback message margin class', function () { + test('When feedback message is passed, it adds a margin-bottom class to accomodate the message', async function (assert) { + this.feedbackMessage = { + type: 'error', + value: 'This is a feedback message' + }; + await render(hbs``); + + assert.dom('.upf-banner.margin-bottom-px-24').exists(); + }); + + test('When no feedback message is passed, it does not add a margin-bottom class', async function (assert) { + await render(hbs``); + + assert.dom('.upf-banner.margin-bottom-px-24').doesNotExist(); + }); + }); + + test('the feedback message is rendered below the banner', async function (assert) { this.feedbackMessage = { type: 'error', value: 'This is a feedback message' }; - }); - - test('When parameter is passed, it adds upf-banner--error class', async function (assert) { await render(hbs``); - assert.dom('.upf-banner.upf-banner--error').exists(); + assert.dom('.upf-banner--feedback').hasText('This is a feedback message'); }); - test('When parameter is passed, the feedback message is swhown', async function (assert) { - await render(hbs``); + ERROR_TYPES.forEach((type) => { + test(`When feedback type is ${type}, the border has the corresponding class`, async function (assert) { + this.feedbackMessage = { + type: type, + value: 'This is a feedback message' + }; + await render(hbs``); - assert.dom('.font-color-error-500').hasText('This is a feedback message'); - }); + assert.dom(`.upf-banner.upf-banner--${type}`).exists(); + }); - test('When parameter is not passed, it does not add upf-banner--error class', async function (assert) { - await render(hbs``); + test(`when feedback message is ${type}, the feedback text has the corresponding class`, async function (assert) { + this.feedbackMessage = { + type: type, + value: 'This is a feedback message' + }; + await render(hbs``); - assert.dom('.upf-banner.upf-banner--error').doesNotExist(); + assert.dom('.upf-banner--feedback').hasClass(`font-color-${type}-500`); + }); }); }); }); From 918f6123f2b2906a5904846499aef1bf8070c19d Mon Sep 17 00:00:00 2001 From: Nathalie Date: Mon, 2 Feb 2026 17:59:49 +0100 Subject: [PATCH 09/45] Updated storybook --- addon/components/o-s-s/banner.stories.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/addon/components/o-s-s/banner.stories.js b/addon/components/o-s-s/banner.stories.js index bb9bf76fb..a48d50df9 100644 --- a/addon/components/o-s-s/banner.stories.js +++ b/addon/components/o-s-s/banner.stories.js @@ -1,6 +1,7 @@ import { hbs } from 'ember-cli-htmlbars'; const COMPONENT_SIZES = ['sm', 'md', 'lg']; +const ERROR_TYPES = ['error', 'warning', 'success']; export default { title: 'Components/OSS::Banner', @@ -77,11 +78,21 @@ export default { type: 'boolean' } }, + hasError: { + description: 'Displays the errrored version of the banner with border in red', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'false' } + }, + control: { + type: 'boolean' + } + }, feedbackMessage: { - description: 'An error message that will be displayed below the banner.', + description: 'A feedback message that will be displayed below the banner. Its color changes based on its type', table: { type: { - summary: '{ type: string, value: string }' + summary: `{ type: ${ERROR_TYPES.join(' | ')}, value: string }` }, defaultValue: { summary: 'undefined' } }, @@ -89,7 +100,7 @@ export default { }, size: { description: - 'Allows to adjust the size of the component. Currently available options are `sm`, `md` and `lg`. Defaults to `md`.', + 'Allows to adjust the size of the component. Currently available options are `sm`, `md` and `lg`. Defaults to `md`', table: { type: COMPONENT_SIZES.join('|'), defaultValue: { summary: 'md' } @@ -103,7 +114,7 @@ export default { parameters: { docs: { description: { - component: 'A configurable Banner component. Can display a badge or an image, a title and a subtitle.' + component: 'A configurable Banner component. Can display a badge or an image, a title and a subtitle' }, iframeHeight: 120 } @@ -118,6 +129,8 @@ const defaultArgs = { plain: false, selected: false, disabled: false, + hasError: false, + feedbackMessage: undefined, size: undefined }; From 09971863a2525d854c2a886aaae2f4317faecb93 Mon Sep 17 00:00:00 2001 From: Nathalie Date: Tue, 3 Feb 2026 16:47:36 +0100 Subject: [PATCH 10/45] Fixed PR feedback --- addon/components/o-s-s/banner.hbs | 6 ++-- addon/components/o-s-s/banner.stories.js | 16 ++++++---- addon/components/o-s-s/banner.ts | 20 +++++++++---- addon/components/o-s-s/input-container.ts | 7 +++-- app/styles/banner.less | 2 +- .../components/o-s-s/banner-test.ts | 30 +++++++++++++++++-- 6 files changed, 60 insertions(+), 21 deletions(-) diff --git a/addon/components/o-s-s/banner.hbs b/addon/components/o-s-s/banner.hbs index 6cdf3e012..5dacbe040 100644 --- a/addon/components/o-s-s/banner.hbs +++ b/addon/components/o-s-s/banner.hbs @@ -29,9 +29,9 @@ {{#if (has-block "actions")}}
{{yield to="actions"}}
{{/if}} - {{#if @feedbackMessage.value}} - \ No newline at end of file diff --git a/addon/components/o-s-s/banner.stories.js b/addon/components/o-s-s/banner.stories.js index a48d50df9..4a65488d0 100644 --- a/addon/components/o-s-s/banner.stories.js +++ b/addon/components/o-s-s/banner.stories.js @@ -1,7 +1,7 @@ import { hbs } from 'ember-cli-htmlbars'; const COMPONENT_SIZES = ['sm', 'md', 'lg']; -const ERROR_TYPES = ['error', 'warning', 'success']; +const FEEDBACK_TYPES = ['error', 'warning', 'success']; export default { title: 'Components/OSS::Banner', @@ -92,7 +92,7 @@ export default { description: 'A feedback message that will be displayed below the banner. Its color changes based on its type', table: { type: { - summary: `{ type: ${ERROR_TYPES.join(' | ')}, value: string }` + summary: `{ type: ${FEEDBACK_TYPES.join(' | ')}, value: string }` }, defaultValue: { summary: 'undefined' } }, @@ -137,7 +137,8 @@ const defaultArgs = { const Template = (args) => ({ template: hbs` + @image={{this.image}} @selected={{this.selected}} @disabled={{this.disabled}} @size={{this.size}} + @hasError={{this.hasError}} @feedbackMessage={{this.feedbackMessage}} /> `, context: args }); @@ -145,7 +146,8 @@ const Template = (args) => ({ const CustomTitleTemplate = (args) => ({ template: hbs` + @image={{this.image}} @selected={{this.selected}} @disabled={{this.disabled}} @size={{this.size}} + @hasError={{this.hasError}} @feedbackMessage={{this.feedbackMessage}}> <:title-suffix>
Custom title @@ -159,7 +161,8 @@ const CustomTitleTemplate = (args) => ({ const CustomIconTemplate = (args) => ({ template: hbs` + @image={{this.image}} @selected={{this.selected}} @disabled={{this.disabled}} @size={{this.size}} + @hasError={{this.hasError}} @feedbackMessage={{this.feedbackMessage}}> <:custom-icon> @@ -171,7 +174,8 @@ const CustomIconTemplate = (args) => ({ const ActionTemplate = (args) => ({ template: hbs` + @image={{this.image}} @selected={{this.selected}} @disabled={{this.disabled}} @size={{this.size}} + @hasError={{this.hasError}} @feedbackMessage={{this.feedbackMessage}}> <:actions> diff --git a/addon/components/o-s-s/banner.ts b/addon/components/o-s-s/banner.ts index 966a1a3ae..d14b07624 100644 --- a/addon/components/o-s-s/banner.ts +++ b/addon/components/o-s-s/banner.ts @@ -1,6 +1,6 @@ import { isBlank } from '@ember/utils'; import Component from '@glimmer/component'; -import type { FeedbackMessage } from './input-container'; +import { FEEDBACK_TYPES, type FeedbackMessage } from './input-container'; type SizeType = 'sm' | 'md' | 'lg'; @@ -35,13 +35,21 @@ export default class OSSBanner extends Component { return SIZE_CLASSES[this.args.size ?? 'md'] ?? ''; } - get borderColorClass(): string { + get feedbackMessage(): FeedbackMessage | undefined { + if (this.args.feedbackMessage && FEEDBACK_TYPES.includes(this.args.feedbackMessage.type)) { + return this.args.feedbackMessage; + } + return undefined; + } + + get borderColorClass(): string | undefined { if (this.args.hasError) return 'upf-banner--error'; - return `upf-banner--${this.args.feedbackMessage?.type}`; + if (this.feedbackMessage) return `upf-banner--${this.args.feedbackMessage?.type}`; + return ''; } - get feedbackMessageClass(): string { - return this.args.feedbackMessage?.value ? 'margin-bottom-px-24' : ''; + get feedbackMarginClass(): string { + return this.feedbackMessage?.value ? 'margin-bottom-px-24' : ''; } get modifierClasses(): string { @@ -51,7 +59,7 @@ export default class OSSBanner extends Component { this.plainClass, this.sizeClass, this.borderColorClass, - this.feedbackMessageClass + this.feedbackMarginClass ] .filter((mc) => !isBlank(mc)) .join(' '); diff --git a/addon/components/o-s-s/input-container.ts b/addon/components/o-s-s/input-container.ts index dc5baf75c..9e43d4f21 100644 --- a/addon/components/o-s-s/input-container.ts +++ b/addon/components/o-s-s/input-container.ts @@ -2,8 +2,11 @@ import { action } from '@ember/object'; import { next } from '@ember/runloop'; import Component from '@glimmer/component'; +export const FEEDBACK_TYPES = ['error', 'warning', 'success']; +export type FeedbackType = (typeof FEEDBACK_TYPES)[number]; + export type FeedbackMessage = { - type: 'error' | 'warning' | 'success'; + type: FeedbackType; value: string; }; @@ -23,7 +26,7 @@ export const AutocompleteValues = ['on', 'off']; export default class OSSInputContainer extends Component { get feedbackMessage(): FeedbackMessage | undefined { - if (this.args.feedbackMessage && ['error', 'warning', 'success'].includes(this.args.feedbackMessage.type)) { + if (this.args.feedbackMessage && FEEDBACK_TYPES.includes(this.args.feedbackMessage.type)) { return this.args.feedbackMessage; } diff --git a/app/styles/banner.less b/app/styles/banner.less index 60ba8c0a4..54b1f0f39 100644 --- a/app/styles/banner.less +++ b/app/styles/banner.less @@ -49,7 +49,7 @@ .upf-banner--feedback { position: absolute; - bottom: -25px; + bottom: calc(var(--spacing-px-24) * -1); left: 0px; } diff --git a/tests/integration/components/o-s-s/banner-test.ts b/tests/integration/components/o-s-s/banner-test.ts index f75de78ae..ce8752a08 100644 --- a/tests/integration/components/o-s-s/banner-test.ts +++ b/tests/integration/components/o-s-s/banner-test.ts @@ -2,8 +2,7 @@ import { hbs } from 'ember-cli-htmlbars'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; - -const ERROR_TYPES = ['error', 'warning', 'success']; +import { FEEDBACK_TYPES, type FeedbackType } from '@upfluence/oss-components/components/o-s-s/input-container'; module('Integration | Component | o-s-s/banner', function (hooks) { setupRenderingTest(hooks); @@ -206,7 +205,7 @@ module('Integration | Component | o-s-s/banner', function (hooks) { assert.dom('.upf-banner--feedback').hasText('This is a feedback message'); }); - ERROR_TYPES.forEach((type) => { + FEEDBACK_TYPES.forEach((type: FeedbackType) => { test(`When feedback type is ${type}, the border has the corresponding class`, async function (assert) { this.feedbackMessage = { type: type, @@ -227,5 +226,30 @@ module('Integration | Component | o-s-s/banner', function (hooks) { assert.dom('.upf-banner--feedback').hasClass(`font-color-${type}-500`); }); }); + + module('invalid feedback type', function (hooks) { + hooks.beforeEach(function () { + this.feedbackMessage = { + type: 'invalid-type', + value: 'This is a feedback message' + }; + }); + + test('when feedback type is invalid, the border does not have any feedback class', async function (assert) { + await render(hbs``); + + assert + .dom('.upf-banner') + .doesNotHaveClass('upf-banner--error') + .doesNotHaveClass('upf-banner--warning') + .doesNotHaveClass('upf-banner--success'); + }); + + test('when feedback type is invalid, the feedback text is not displayed', async function (assert) { + await render(hbs``); + + assert.dom('.upf-banner--feedback').doesNotExist(); + }); + }); }); }); From b8815235fc3c97ea872c17da023f1b58853d8ea2 Mon Sep 17 00:00:00 2001 From: Nathalie Date: Tue, 3 Feb 2026 17:28:26 +0100 Subject: [PATCH 11/45] Fixed forgotten as const --- addon/components/o-s-s/input-container.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/components/o-s-s/input-container.ts b/addon/components/o-s-s/input-container.ts index 9e43d4f21..b8d6f9dc1 100644 --- a/addon/components/o-s-s/input-container.ts +++ b/addon/components/o-s-s/input-container.ts @@ -2,7 +2,7 @@ import { action } from '@ember/object'; import { next } from '@ember/runloop'; import Component from '@glimmer/component'; -export const FEEDBACK_TYPES = ['error', 'warning', 'success']; +export const FEEDBACK_TYPES = ['error', 'warning', 'success'] as const; export type FeedbackType = (typeof FEEDBACK_TYPES)[number]; export type FeedbackMessage = { From 2800a26e3555c5a8ba0ec823a9385bcfec856676 Mon Sep 17 00:00:00 2001 From: Nathalie Date: Wed, 4 Feb 2026 15:24:33 +0100 Subject: [PATCH 12/45] Removed hasError parameter as feedbackMessage value is to become optional --- addon/components/o-s-s/banner.stories.js | 19 +++---------- addon/components/o-s-s/banner.ts | 6 ++-- app/styles/banner.less | 10 +++---- tests/dummy/app/templates/index.hbs | 1 - .../components/o-s-s/banner-test.ts | 28 ++++++++----------- 5 files changed, 22 insertions(+), 42 deletions(-) diff --git a/addon/components/o-s-s/banner.stories.js b/addon/components/o-s-s/banner.stories.js index 4a65488d0..f88ad64a6 100644 --- a/addon/components/o-s-s/banner.stories.js +++ b/addon/components/o-s-s/banner.stories.js @@ -78,16 +78,6 @@ export default { type: 'boolean' } }, - hasError: { - description: 'Displays the errrored version of the banner with border in red', - table: { - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' } - }, - control: { - type: 'boolean' - } - }, feedbackMessage: { description: 'A feedback message that will be displayed below the banner. Its color changes based on its type', table: { @@ -129,7 +119,6 @@ const defaultArgs = { plain: false, selected: false, disabled: false, - hasError: false, feedbackMessage: undefined, size: undefined }; @@ -138,7 +127,7 @@ const Template = (args) => ({ template: hbs` + @feedbackMessage={{this.feedbackMessage}} /> `, context: args }); @@ -147,7 +136,7 @@ const CustomTitleTemplate = (args) => ({ template: hbs` + @feedbackMessage={{this.feedbackMessage}}> <:title-suffix>
Custom title @@ -162,7 +151,7 @@ const CustomIconTemplate = (args) => ({ template: hbs` + @feedbackMessage={{this.feedbackMessage}}> <:custom-icon> @@ -175,7 +164,7 @@ const ActionTemplate = (args) => ({ template: hbs` + @feedbackMessage={{this.feedbackMessage}}> <:actions> diff --git a/addon/components/o-s-s/banner.ts b/addon/components/o-s-s/banner.ts index d14b07624..fa083c166 100644 --- a/addon/components/o-s-s/banner.ts +++ b/addon/components/o-s-s/banner.ts @@ -9,7 +9,6 @@ interface OSSBannerArgs { plain?: boolean; selected?: boolean; disabled?: boolean; - hasError?: boolean; feedbackMessage?: FeedbackMessage; } @@ -42,9 +41,8 @@ export default class OSSBanner extends Component { return undefined; } - get borderColorClass(): string | undefined { - if (this.args.hasError) return 'upf-banner--error'; - if (this.feedbackMessage) return `upf-banner--${this.args.feedbackMessage?.type}`; + get borderColorClass(): string { + if (this.feedbackMessage) return `upf-banner--${this.feedbackMessage.type}`; return ''; } diff --git a/app/styles/banner.less b/app/styles/banner.less index 54b1f0f39..ede6252ae 100644 --- a/app/styles/banner.less +++ b/app/styles/banner.less @@ -45,12 +45,12 @@ } transition: ease-in-out 0.25s; -} -.upf-banner--feedback { - position: absolute; - bottom: calc(var(--spacing-px-24) * -1); - left: 0px; + .upf-banner--feedback { + position: absolute; + bottom: calc(var(--spacing-px-24) * -1); + left: 0px; + } } .bg-color-white { diff --git a/tests/dummy/app/templates/index.hbs b/tests/dummy/app/templates/index.hbs index fcc696a49..cc4dd6413 100644 --- a/tests/dummy/app/templates/index.hbs +++ b/tests/dummy/app/templates/index.hbs @@ -297,7 +297,6 @@
- `); - assert.dom('.upf-banner.upf-banner--error').exists(); - }); - - test('when the value is falsy, the border is not errored', async function (assert) { - await render(hbs``); - assert.dom('.upf-banner.upf-banner--error').doesNotExist(); - }); - - test('when the value is undefined, the border is not errored', async function (assert) { - await render(hbs``); - assert.dom('.upf-banner.upf-banner--error').doesNotExist(); - }); - }); - module('@feedbackMessage parameter', function () { module('feedback message margin class', function () { test('When feedback message is passed, it adds a margin-bottom class to accomodate the message', async function (assert) { @@ -251,5 +234,16 @@ module('Integration | Component | o-s-s/banner', function (hooks) { assert.dom('.upf-banner--feedback').doesNotExist(); }); }); + + test('when feedback value is undefined, the feedback text is not displayed but the border color changes accordingly', async function (assert) { + this.feedbackMessage = { + type: 'success', + value: undefined + }; + await render(hbs``); + + assert.dom('.upf-banner--feedback').doesNotExist(); + assert.dom('.upf-banner').hasClass('upf-banner--success'); + }); }); }); From 29e4bc8929344677419f3787d6ad9496210b9263 Mon Sep 17 00:00:00 2001 From: Nathalie Date: Wed, 4 Feb 2026 15:27:05 +0100 Subject: [PATCH 13/45] Cleaned up dummy app --- tests/dummy/app/templates/index.hbs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/dummy/app/templates/index.hbs b/tests/dummy/app/templates/index.hbs index cc4dd6413..739d76e93 100644 --- a/tests/dummy/app/templates/index.hbs +++ b/tests/dummy/app/templates/index.hbs @@ -297,19 +297,24 @@
+ From fa813ac63d092a04c7eab9946c64b506e1f39b1e Mon Sep 17 00:00:00 2001 From: Nathalie Date: Thu, 5 Feb 2026 17:20:03 +0100 Subject: [PATCH 14/45] Added index on Power Select option items --- addon/components/o-s-s/power-select.hbs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addon/components/o-s-s/power-select.hbs b/addon/components/o-s-s/power-select.hbs index b0d705ef7..381c61df6 100644 --- a/addon/components/o-s-s/power-select.hbs +++ b/addon/components/o-s-s/power-select.hbs @@ -47,8 +47,8 @@ {{on "click" this.noop}} {{on-click-outside this.onClickOutside useCapture=@captureClickOutside}} > - <:option as |item|> - {{yield item to="option-item"}} + <:option as |item index|> + {{yield item index to="option-item"}} <:empty-state> @@ -71,8 +71,8 @@ {{on "click" this.noop}} {{on-click-outside this.onClickOutside useCapture=@captureClickOutside}} > - <:option as |item|> - {{yield item to="option-item"}} + <:option as |item index|> + {{yield item index to="option-item"}} {{/if}} From 9e03584d1c793a838f747b2c814a3b3c991a4fc9 Mon Sep 17 00:00:00 2001 From: Antoine Prentout Date: Mon, 9 Feb 2026 10:46:20 +0100 Subject: [PATCH 15/45] [Fix] sync issue with package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b0a4815c9..d977838e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@upfluence/oss-components", - "version": "3.89.0", + "version": "3.89.1", "description": "The default blueprint for ember-cli addons.", "keywords": [ "ember-addon" From fed86253d6089545d4cfb1a0ae7525687c7f395e Mon Sep 17 00:00:00 2001 From: Nathalie Date: Mon, 9 Feb 2026 10:47:42 +0100 Subject: [PATCH 16/45] 3.89.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d977838e8..1548a064d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@upfluence/oss-components", - "version": "3.89.1", + "version": "3.89.2", "description": "The default blueprint for ember-cli addons.", "keywords": [ "ember-addon" From 38ec42c66a13303b4365df0d32c9664c89fcc27c Mon Sep 17 00:00:00 2001 From: Maximilien B Date: Mon, 9 Feb 2026 14:31:40 +0100 Subject: [PATCH 17/45] Fixed: InfineSelect scroll-behavior prop at higher container level --- app/styles/base/_infinite-select.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/base/_infinite-select.less b/app/styles/base/_infinite-select.less index 6667a9480..1deb9388f 100644 --- a/app/styles/base/_infinite-select.less +++ b/app/styles/base/_infinite-select.less @@ -1,6 +1,7 @@ .upf-infinite-select { height: 100%; width: 100%; + overscroll-behavior: contain; ul { padding: 0; @@ -19,7 +20,6 @@ .upf-infinite-select__items-container { margin: 0; overflow: hidden auto; - overscroll-behavior: contain; } .upf-infinite-select__item { From 65a9ac2c797b73aa62f481691f5d5c1bf1aa5d02 Mon Sep 17 00:00:00 2001 From: Nathalie Date: Tue, 10 Feb 2026 09:44:35 +0100 Subject: [PATCH 18/45] 3.89.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1548a064d..1e9b15e9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@upfluence/oss-components", - "version": "3.89.2", + "version": "3.89.3", "description": "The default blueprint for ember-cli addons.", "keywords": [ "ember-addon" From 313a8e1fbd7dfc0f06796fbbeaa6d0a1b5919080 Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Tue, 10 Feb 2026 10:51:51 +0100 Subject: [PATCH 19/45] oss/avatar: ensure loading avatar stays round --- app/styles/atoms/avatar.less | 1 + 1 file changed, 1 insertion(+) diff --git a/app/styles/atoms/avatar.less b/app/styles/atoms/avatar.less index df3127350..705b81e45 100644 --- a/app/styles/atoms/avatar.less +++ b/app/styles/atoms/avatar.less @@ -68,5 +68,6 @@ &--loading { .upf-skeleton-effect; + .border-radius-round; } } From 8480e677c2b4bfb70d2ee84bafc6f6810aeb6f98 Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Tue, 10 Feb 2026 10:52:07 +0100 Subject: [PATCH 20/45] 3.89.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e9b15e9f..9a46e19ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@upfluence/oss-components", - "version": "3.89.3", + "version": "3.89.4", "description": "The default blueprint for ember-cli addons.", "keywords": [ "ember-addon" From 78e31df40576fa8b8527757260169ec83dc6c3ce Mon Sep 17 00:00:00 2001 From: Maximilien B Date: Tue, 10 Feb 2026 15:25:37 +0100 Subject: [PATCH 21/45] 3.89.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a46e19ff..12f3fe2cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@upfluence/oss-components", - "version": "3.89.4", + "version": "3.89.5", "description": "The default blueprint for ember-cli addons.", "keywords": [ "ember-addon" From 3075f0aece81b77411c030eaff470b55950fdef0 Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Tue, 10 Feb 2026 17:44:22 +0100 Subject: [PATCH 22/45] add support for Tag in NavTab tab item --- addon/components/o-s-s/nav-tab.hbs | 13 ++++++++++--- addon/components/o-s-s/nav-tab.stories.js | 12 ++++++++++-- addon/components/o-s-s/nav-tab.ts | 9 ++++++--- tests/integration/components/o-s-s/nav-tab-test.ts | 12 ++++++++++++ 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/addon/components/o-s-s/nav-tab.hbs b/addon/components/o-s-s/nav-tab.hbs index 02243ee78..1f2ca5784 100644 --- a/addon/components/o-s-s/nav-tab.hbs +++ b/addon/components/o-s-s/nav-tab.hbs @@ -1,7 +1,11 @@
{{#each @tabArray as |tab|}} - {{/each}} -
+
\ No newline at end of file diff --git a/addon/components/o-s-s/nav-tab.stories.js b/addon/components/o-s-s/nav-tab.stories.js index 25bca00b3..584e24166 100644 --- a/addon/components/o-s-s/nav-tab.stories.js +++ b/addon/components/o-s-s/nav-tab.stories.js @@ -19,7 +19,7 @@ export default { tabArray: { type: { required: true }, description: - 'Array of TabDefinition which has the following parameters:
-icon?: string;
-label?: string;
-infoCircle?: boolean;
-notificationDot?: boolean;
-selected: boolean;
-disabled: boolean;
@label or @icon is mandatory for each element of tabArray', + 'Array of TabDefinition which has the following parameters:
-icon?: string;
-label?: string;
-infoCircle?: boolean;
-notificationDot?: boolean;
-selected: boolean;
-disabled: boolean;
-tag: OSS::Tag arg;
@label or @icon is mandatory for each element of tabArray', table: { type: { summary: 'TabDefinition[]' @@ -40,7 +40,15 @@ const defaultArgs = { { label: 'Tab', icon: 'far fa-thumbs-up', infoCircle: true, notificationDot: true }, { label: 'Tab', icon: 'far fa-thumbs-up', infoCircle: true, notificationDot: true, selected: true }, { label: 'Tab', icon: 'far fa-thumbs-up', infoCircle: true, notificationDot: true, disabled: true }, - { label: 'Tab', icon: 'far fa-thumbs-up', infoCircle: true, notificationDot: true, selected: true, disabled: true } + { label: 'Tab', icon: 'far fa-thumbs-up', infoCircle: true, notificationDot: true, selected: true, disabled: true }, + { + label: 'Tab', + icon: 'far fa-thumbs-up', + infoCircle: true, + notificationDot: true, + selected: true, + tag: { label: 'X', skin: 'danger' } + } ], onSelection: action('onSelection') }; diff --git a/addon/components/o-s-s/nav-tab.ts b/addon/components/o-s-s/nav-tab.ts index f7a50661a..4783af9d3 100644 --- a/addon/components/o-s-s/nav-tab.ts +++ b/addon/components/o-s-s/nav-tab.ts @@ -1,6 +1,8 @@ import { assert } from '@ember/debug'; -import Component from '@glimmer/component'; import { action } from '@ember/object'; +import Component from '@glimmer/component'; + +import type { OSSTagArgs } from './tag'; interface OSSNavTabArgs { onSelection(selectedTab: TabDefinition): void; @@ -8,12 +10,13 @@ interface OSSNavTabArgs { } export interface TabDefinition { + selected: boolean; + disabled: boolean; icon?: string; label?: string; infoCircle?: boolean; notificationDot?: boolean; - selected: boolean; - disabled: boolean; + tag?: OSSTagArgs; } export default class OSSNavTab extends Component { diff --git a/tests/integration/components/o-s-s/nav-tab-test.ts b/tests/integration/components/o-s-s/nav-tab-test.ts index 8a6cfb1ef..1944589db 100644 --- a/tests/integration/components/o-s-s/nav-tab-test.ts +++ b/tests/integration/components/o-s-s/nav-tab-test.ts @@ -58,6 +58,18 @@ module('Integration | Component | o-s-s/nav-tab', function (hooks) { assert.dom('.tab-content .fas.fa-circle').exists(); }); + test('Tab tag displays properly', async function (assert) { + this.tabArray.push({ label: 'Tab', tag: { label: '1', skin: 'danger' } }); + + await render(hbs``); + + assert.dom('.tab-container').exists(); + assert.dom('.tab-container .tab').exists(); + assert.dom('.tab-container .upf-tag').exists(); + assert.dom('.tab-container .upf-tag').hasText('1'); + assert.dom('.tab-container .upf-tag').hasClass('upf-tag--critical'); + }); + test('Tab displays selected state properly', async function (assert) { this.tabArray.push({ label: 'Tab', selected: true }); await render(hbs``); From 14bcc32c62efb47b5110a6ea23a327ae7f5fee1e Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Wed, 11 Feb 2026 16:44:01 +0100 Subject: [PATCH 23/45] 3.89.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 12f3fe2cc..3910b90d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@upfluence/oss-components", - "version": "3.89.5", + "version": "3.89.6", "description": "The default blueprint for ember-cli addons.", "keywords": [ "ember-addon" From d6a5b2116acc1193e164b649eecc55c2603dfa68 Mon Sep 17 00:00:00 2001 From: Owen Coogan Date: Thu, 12 Feb 2026 17:38:18 +0100 Subject: [PATCH 24/45] feat: added @suffixIcon arg to oss::button --- addon/components/o-s-s/button.hbs | 11 +++-- addon/components/o-s-s/button.stories.js | 41 ++++++++++++++++++- addon/components/o-s-s/button.ts | 21 ++++++++++ .../components/o-s-s/button-test.ts | 23 +++++++++-- 4 files changed, 89 insertions(+), 7 deletions(-) diff --git a/addon/components/o-s-s/button.hbs b/addon/components/o-s-s/button.hbs index 43a9b23e6..81172f7be 100644 --- a/addon/components/o-s-s/button.hbs +++ b/addon/components/o-s-s/button.hbs @@ -20,9 +20,14 @@ {{else if @iconUrl}} icon {{/if}} +
+ {{#if @label}} + {{@label}} + {{/if}} - {{#if @label}} - {{@label}} - {{/if}} + {{#if @suffixIcon}} + + {{/if}} +
{{/if}} \ No newline at end of file diff --git a/addon/components/o-s-s/button.stories.js b/addon/components/o-s-s/button.stories.js index 18e3374ee..4cbd47e85 100644 --- a/addon/components/o-s-s/button.stories.js +++ b/addon/components/o-s-s/button.stories.js @@ -20,6 +20,7 @@ const SkinTypes = [ ]; const SizeTypes = ['xs', 'sm', 'md', 'lg']; const ThemeTypes = ['light', 'dark']; +const GapTypes = ['fx-gap-px-3', 'fx-gap-px-6', 'fx-gap-px-9', 'fx-gap-px-12', 'fx-gap-px-18', 'fx-gap-px-24']; export default { title: 'Components/OSS::Button', @@ -97,6 +98,25 @@ export default { type: 'text' } }, + suffixIcon: { + description: 'Font Awesome class for an icon displayed after the label (e.g. fas fa-chevron-down)', + table: { + type: { summary: 'string' }, + defaultValue: { summary: 'undefined' } + }, + control: { + type: 'text' + } + }, + gap: { + description: 'Gap between icon, label, and suffix icon. Must be a valid fx-gap-px-* class. Defaults to fx-gap-px-6.', + table: { + type: { summary: GapTypes.join('|') }, + defaultValue: { summary: 'fx-gap-px-6' } + }, + options: GapTypes, + control: { type: 'select' } + }, square: { description: 'Displays the button as a square. Useful for icon buttons.', table: { @@ -164,6 +184,8 @@ const defaultArgs = { countDown: undefined, loadingOptions: undefined, iconUrl: undefined, + suffixIcon: undefined, + gap: undefined, disabled: undefined }; @@ -172,7 +194,8 @@ const Template = (args) => ({ + @iconUrl={{this.iconUrl}} @suffixIcon={{this.suffixIcon}} @gap={{this.gap}} + @loadingOptions={{this.loadingOptions}} disabled={{this.disabled}} /> `, context: args }); @@ -201,3 +224,19 @@ WithIconUrl.args = { iconUrl: '/@upfluence/oss-components/assets/heart.svg' } }; + +export const WithSuffixIcon = Template.bind({}); +WithSuffixIcon.args = { + ...defaultArgs, + ...{ + suffixIcon: 'fas fa-chevron-down' + } +}; + +export const WithCustomGap = Template.bind({}); +WithCustomGap.args = { + ...defaultArgs, + ...{ + gap: 'fx-gap-px-12' + } +}; diff --git a/addon/components/o-s-s/button.ts b/addon/components/o-s-s/button.ts index 3a0329d88..ec64711be 100644 --- a/addon/components/o-s-s/button.ts +++ b/addon/components/o-s-s/button.ts @@ -36,6 +36,17 @@ type ThemeDefType = { }; type LoadingOptions = { showLabel?: boolean }; +const VALID_GAP_CLASSES = [ + 'fx-gap-px-3', + 'fx-gap-px-6', + 'fx-gap-px-9', + 'fx-gap-px-12', + 'fx-gap-px-18', + 'fx-gap-px-24' +] as const; + +export type GapClassType = (typeof VALID_GAP_CLASSES)[number]; + const SkinDefinition: SkinDefType = { default: 'default', primary: 'primary', @@ -77,6 +88,8 @@ export interface OSSButtonArgs { loadingOptions?: LoadingOptions; icon?: string; iconUrl?: string; + suffixIcon?: string; + gap?: GapClassType; label?: string; theme?: string; square?: boolean; @@ -106,6 +119,10 @@ export default class OSSButton extends Component { "[component][OSS::Button] You must pass either a hash with 'callback' value to @countDown argument.", args.countDown ? args.countDown.callback : true ); + assert( + '[component][OSS::Button] When @gap is provided, it must be a valid fx-gap-px-* class.', + !this.args.gap || VALID_GAP_CLASSES.includes(this.args.gap) + ); } get skin(): string { @@ -130,6 +147,10 @@ export default class OSSButton extends Component { return this.args.theme; } + get suffixGapClass(): string { + return this.args.gap ?? 'fx-gap-px-6'; + } + get computedClass() { let classes = [this.args.square ? SQUARE_CLASS : BASE_CLASS, `upf-btn--${this.skin}`]; diff --git a/tests/integration/components/o-s-s/button-test.ts b/tests/integration/components/o-s-s/button-test.ts index 30189c303..eeb0d96cb 100644 --- a/tests/integration/components/o-s-s/button-test.ts +++ b/tests/integration/components/o-s-s/button-test.ts @@ -50,6 +50,23 @@ module('Integration | Component | o-s-s/button', function (hooks) { assert.dom('.upf-btn').hasText('Test'); }); + test('it applies custom gap when @gap is provided', async function (assert) { + await render(hbs``); + + assert.dom('.upf-btn .fx-row').hasClass('fx-gap-px-12'); + }); + + test('it fails when an invalid @gap class is provided', async function (assert) { + setupOnerror((err: { message: string }) => { + assert.equal( + err.message, + 'Assertion Failed: [component][OSS::Button] When @gap is provided, it must be a valid fx-gap-px-* class.' + ); + }); + + await render(hbs``); + }); + module('it renders with the correct skin', function () { test('when using an unknown skin, it is set to default', async function (assert) { await render(hbs``); @@ -138,7 +155,7 @@ module('Integration | Component | o-s-s/button', function (hooks) { assert.dom('.upf-btn i.fas').exists(); assert.dom('.upf-btn i.fas').hasClass('fa-circle-notch'); assert.dom('.upf-btn i.fas').hasClass('fa-spin'); - assert.dom('.upf-btn span.margin-left-px-6').doesNotExist(); + assert.dom('.upf-btn span').doesNotExist(); }); test('when loading and the showLabel loading option is truthy, the label is displayed', async function (assert) { @@ -148,8 +165,8 @@ module('Integration | Component | o-s-s/button', function (hooks) { assert.dom('.upf-btn i.fas').exists(); assert.dom('.upf-btn i.fas').hasClass('fa-circle-notch'); assert.dom('.upf-btn i.fas').hasClass('fa-spin'); - assert.dom('.upf-btn span.margin-left-px-6').exists(); - assert.dom('.upf-btn span.margin-left-px-6').hasText('Test'); + assert.dom('.upf-btn span').exists(); + assert.dom('.upf-btn span').hasText('Test'); }); }); From 8dd1a1aeac670b7743172ccd122b659573eba116 Mon Sep 17 00:00:00 2001 From: Owen Coogan Date: Fri, 13 Feb 2026 13:05:37 +0100 Subject: [PATCH 25/45] fix: fixed linter issue --- addon/components/o-s-s/button.stories.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addon/components/o-s-s/button.stories.js b/addon/components/o-s-s/button.stories.js index 4cbd47e85..d907f2ef0 100644 --- a/addon/components/o-s-s/button.stories.js +++ b/addon/components/o-s-s/button.stories.js @@ -109,7 +109,8 @@ export default { } }, gap: { - description: 'Gap between icon, label, and suffix icon. Must be a valid fx-gap-px-* class. Defaults to fx-gap-px-6.', + description: + 'Gap between icon, label, and suffix icon. Must be a valid fx-gap-px-* class. Defaults to fx-gap-px-6.', table: { type: { summary: GapTypes.join('|') }, defaultValue: { summary: 'fx-gap-px-6' } From ff828704bf2c45c84b8689ca873a595ee3b1e089 Mon Sep 17 00:00:00 2001 From: Owen Coogan Date: Fri, 13 Feb 2026 14:35:50 +0100 Subject: [PATCH 26/45] fix: removed dynamic gap for suffix icon --- addon/components/o-s-s/button.hbs | 19 ++++++++-------- addon/components/o-s-s/button.stories.js | 22 +------------------ addon/components/o-s-s/button.ts | 20 ----------------- .../components/o-s-s/button-test.ts | 17 +++++--------- 4 files changed, 17 insertions(+), 61 deletions(-) diff --git a/addon/components/o-s-s/button.hbs b/addon/components/o-s-s/button.hbs index 81172f7be..794170fdd 100644 --- a/addon/components/o-s-s/button.hbs +++ b/addon/components/o-s-s/button.hbs @@ -20,14 +20,15 @@ {{else if @iconUrl}} icon {{/if}} -
- {{#if @label}} - {{@label}} - {{/if}} - - {{#if @suffixIcon}} - - {{/if}} -
+ {{#if @label}} + {{@label}} + {{/if}} + {{#if @suffixIcon}} + + {{/if}} {{/if}} \ No newline at end of file diff --git a/addon/components/o-s-s/button.stories.js b/addon/components/o-s-s/button.stories.js index d907f2ef0..3e05f6f23 100644 --- a/addon/components/o-s-s/button.stories.js +++ b/addon/components/o-s-s/button.stories.js @@ -20,7 +20,6 @@ const SkinTypes = [ ]; const SizeTypes = ['xs', 'sm', 'md', 'lg']; const ThemeTypes = ['light', 'dark']; -const GapTypes = ['fx-gap-px-3', 'fx-gap-px-6', 'fx-gap-px-9', 'fx-gap-px-12', 'fx-gap-px-18', 'fx-gap-px-24']; export default { title: 'Components/OSS::Button', @@ -108,16 +107,6 @@ export default { type: 'text' } }, - gap: { - description: - 'Gap between icon, label, and suffix icon. Must be a valid fx-gap-px-* class. Defaults to fx-gap-px-6.', - table: { - type: { summary: GapTypes.join('|') }, - defaultValue: { summary: 'fx-gap-px-6' } - }, - options: GapTypes, - control: { type: 'select' } - }, square: { description: 'Displays the button as a square. Useful for icon buttons.', table: { @@ -186,7 +175,6 @@ const defaultArgs = { loadingOptions: undefined, iconUrl: undefined, suffixIcon: undefined, - gap: undefined, disabled: undefined }; @@ -195,7 +183,7 @@ const Template = (args) => ({ `, context: args @@ -233,11 +221,3 @@ WithSuffixIcon.args = { suffixIcon: 'fas fa-chevron-down' } }; - -export const WithCustomGap = Template.bind({}); -WithCustomGap.args = { - ...defaultArgs, - ...{ - gap: 'fx-gap-px-12' - } -}; diff --git a/addon/components/o-s-s/button.ts b/addon/components/o-s-s/button.ts index ec64711be..6cd3fc2d9 100644 --- a/addon/components/o-s-s/button.ts +++ b/addon/components/o-s-s/button.ts @@ -36,17 +36,6 @@ type ThemeDefType = { }; type LoadingOptions = { showLabel?: boolean }; -const VALID_GAP_CLASSES = [ - 'fx-gap-px-3', - 'fx-gap-px-6', - 'fx-gap-px-9', - 'fx-gap-px-12', - 'fx-gap-px-18', - 'fx-gap-px-24' -] as const; - -export type GapClassType = (typeof VALID_GAP_CLASSES)[number]; - const SkinDefinition: SkinDefType = { default: 'default', primary: 'primary', @@ -89,7 +78,6 @@ export interface OSSButtonArgs { icon?: string; iconUrl?: string; suffixIcon?: string; - gap?: GapClassType; label?: string; theme?: string; square?: boolean; @@ -119,10 +107,6 @@ export default class OSSButton extends Component { "[component][OSS::Button] You must pass either a hash with 'callback' value to @countDown argument.", args.countDown ? args.countDown.callback : true ); - assert( - '[component][OSS::Button] When @gap is provided, it must be a valid fx-gap-px-* class.', - !this.args.gap || VALID_GAP_CLASSES.includes(this.args.gap) - ); } get skin(): string { @@ -147,10 +131,6 @@ export default class OSSButton extends Component { return this.args.theme; } - get suffixGapClass(): string { - return this.args.gap ?? 'fx-gap-px-6'; - } - get computedClass() { let classes = [this.args.square ? SQUARE_CLASS : BASE_CLASS, `upf-btn--${this.skin}`]; diff --git a/tests/integration/components/o-s-s/button-test.ts b/tests/integration/components/o-s-s/button-test.ts index eeb0d96cb..74bc1d686 100644 --- a/tests/integration/components/o-s-s/button-test.ts +++ b/tests/integration/components/o-s-s/button-test.ts @@ -50,21 +50,16 @@ module('Integration | Component | o-s-s/button', function (hooks) { assert.dom('.upf-btn').hasText('Test'); }); - test('it applies custom gap when @gap is provided', async function (assert) { - await render(hbs``); + test('it applies margin-left-px-6 to suffix icon when label is present', async function (assert) { + await render(hbs``); - assert.dom('.upf-btn .fx-row').hasClass('fx-gap-px-12'); + assert.dom('.upf-btn i.fa-chevron-down').hasClass('margin-left-px-6'); }); - test('it fails when an invalid @gap class is provided', async function (assert) { - setupOnerror((err: { message: string }) => { - assert.equal( - err.message, - 'Assertion Failed: [component][OSS::Button] When @gap is provided, it must be a valid fx-gap-px-* class.' - ); - }); + test('it does not apply margin to suffix icon when label is absent', async function (assert) { + await render(hbs``); - await render(hbs``); + assert.dom('.upf-btn i.fa-chevron-down').doesNotHaveClass('margin-left-px-6'); }); module('it renders with the correct skin', function () { From 00e18536b7345dfaea89dd0ceb40c7bf5be51f3e Mon Sep 17 00:00:00 2001 From: Owen Coogan Date: Fri, 13 Feb 2026 14:41:00 +0100 Subject: [PATCH 27/45] fix: added fix for margin size --- addon/components/o-s-s/button.hbs | 2 +- tests/integration/components/o-s-s/button-test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/addon/components/o-s-s/button.hbs b/addon/components/o-s-s/button.hbs index 794170fdd..7bd3d5fe6 100644 --- a/addon/components/o-s-s/button.hbs +++ b/addon/components/o-s-s/button.hbs @@ -27,7 +27,7 @@ {{/if}} {{/if}} diff --git a/tests/integration/components/o-s-s/button-test.ts b/tests/integration/components/o-s-s/button-test.ts index 74bc1d686..49bcc4b7d 100644 --- a/tests/integration/components/o-s-s/button-test.ts +++ b/tests/integration/components/o-s-s/button-test.ts @@ -53,13 +53,13 @@ module('Integration | Component | o-s-s/button', function (hooks) { test('it applies margin-left-px-6 to suffix icon when label is present', async function (assert) { await render(hbs``); - assert.dom('.upf-btn i.fa-chevron-down').hasClass('margin-left-px-6'); + assert.dom('.upf-btn i.fa-chevron-down').hasClass('margin-left-px-12'); }); test('it does not apply margin to suffix icon when label is absent', async function (assert) { await render(hbs``); - assert.dom('.upf-btn i.fa-chevron-down').doesNotHaveClass('margin-left-px-6'); + assert.dom('.upf-btn i.fa-chevron-down').doesNotHaveClass('margin-left-px-12'); }); module('it renders with the correct skin', function () { From 9ef4f256811d3a4ad79cf2151e24dbe98fb90a04 Mon Sep 17 00:00:00 2001 From: Owen Coogan Date: Fri, 13 Feb 2026 15:43:02 +0100 Subject: [PATCH 28/45] fix: fixed gap based on design input --- addon/components/o-s-s/button.hbs | 2 +- tests/integration/components/o-s-s/button-test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addon/components/o-s-s/button.hbs b/addon/components/o-s-s/button.hbs index 7bd3d5fe6..1cb5eb7a3 100644 --- a/addon/components/o-s-s/button.hbs +++ b/addon/components/o-s-s/button.hbs @@ -27,7 +27,7 @@ {{/if}} {{/if}} diff --git a/tests/integration/components/o-s-s/button-test.ts b/tests/integration/components/o-s-s/button-test.ts index 49bcc4b7d..025038bd8 100644 --- a/tests/integration/components/o-s-s/button-test.ts +++ b/tests/integration/components/o-s-s/button-test.ts @@ -50,16 +50,16 @@ module('Integration | Component | o-s-s/button', function (hooks) { assert.dom('.upf-btn').hasText('Test'); }); - test('it applies margin-left-px-6 to suffix icon when label is present', async function (assert) { + test('it applies margin-left-px-18 to suffix icon when label is present', async function (assert) { await render(hbs``); - assert.dom('.upf-btn i.fa-chevron-down').hasClass('margin-left-px-12'); + assert.dom('.upf-btn i.fa-chevron-down').hasClass('margin-left-px-18'); }); test('it does not apply margin to suffix icon when label is absent', async function (assert) { await render(hbs``); - assert.dom('.upf-btn i.fa-chevron-down').doesNotHaveClass('margin-left-px-12'); + assert.dom('.upf-btn i.fa-chevron-down').doesNotHaveClass('margin-left-px-18'); }); module('it renders with the correct skin', function () { From 2943e380769caabe05a168ca7e44e73f6c49dadf Mon Sep 17 00:00:00 2001 From: Antoine Prentout <111493996+aprentout@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:21:06 +0100 Subject: [PATCH 29/45] [Infinite select] Add ability to group by items (#622) * [Infinite select] Add ability to group by items * Fix pr feedbacks --- addon/components/o-s-s/infinite-select.hbs | 48 +++++++----- .../o-s-s/infinite-select.stories.js | 11 +++ addon/components/o-s-s/infinite-select.ts | 29 ++++++- app/styles/base/_infinite-select.less | 18 ++++- tests/dummy/app/controllers/input.ts | 10 ++- .../components/o-s-s/infinite-select-test.js | 78 +++++++++++++++++++ 6 files changed, 167 insertions(+), 27 deletions(-) diff --git a/addon/components/o-s-s/infinite-select.hbs b/addon/components/o-s-s/infinite-select.hbs index 593f6e77f..2f8b6a692 100644 --- a/addon/components/o-s-s/infinite-select.hbs +++ b/addon/components/o-s-s/infinite-select.hbs @@ -28,25 +28,34 @@ {{#if (and @loading (not @loadingMore))}} {{else}} - {{#each this.items as |item index|}} -
  • - {{#if (has-block "option")}} - {{yield item index to="option"}} - {{else}} - {{get item this.itemLabel}} + {{#each-in this.groups as |groupKey items|}} +
      + {{#each items as |item|}} + {{#let (this.findItemIndex item=item) as |index|}} +
    • + {{#if (has-block "option")}} + {{yield item index to="option"}} + {{else}} + {{get item this.itemLabel}} + {{/if}} +
    • + {{/let}} + {{/each}} + {{#if (not-eq groupKey this.lastKey)}} +
      {{/if}} - +
    {{else}}
    {{#if (has-block "empty-state")}} @@ -68,8 +77,7 @@
    {{/if}}
  • - {{/each}} - + {{/each-in}} {{#if @loadingMore}} {{/if}} diff --git a/addon/components/o-s-s/infinite-select.stories.js b/addon/components/o-s-s/infinite-select.stories.js index e53b655e8..0f2cfb865 100644 --- a/addon/components/o-s-s/infinite-select.stories.js +++ b/addon/components/o-s-s/infinite-select.stories.js @@ -237,3 +237,14 @@ EmptyState.args = { items: [] } }; + +export const WithGroupsBlock = Template.bind({}); +WithGroupsBlock.args = { + ...defaultArgs, + ...{ + items: FAKE_DATA.map((item, index) => ({ + ...item, + groupKey: index % 2 === 0 ? 'Group A' : 'Group B' + })) + } +}; diff --git a/addon/components/o-s-s/infinite-select.ts b/addon/components/o-s-s/infinite-select.ts index e4eda6d49..7a138dd59 100644 --- a/addon/components/o-s-s/infinite-select.ts +++ b/addon/components/o-s-s/infinite-select.ts @@ -2,6 +2,7 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { assert } from '@ember/debug'; import { action } from '@ember/object'; +import { helper } from '@ember/component/helper'; import { guidFor } from '@ember/object/internals'; import type { SkinType } from './button'; @@ -35,8 +36,11 @@ interface InfiniteSelectArgs { type InfinityItem = { selected: boolean; + groupKey?: string; }; +type InfinityItemByGroup = Record; + const DEFAULT_ITEM_LABEL = 'name'; export default class OSSInfiniteSelect extends Component { @@ -58,6 +62,29 @@ export default class OSSInfiniteSelect extends Component { assert('[component][OSS::InfiniteSelect] `onSelect` action is mandatory', typeof this.args.onSelect === 'function'); } + findItemIndex = helper((_, { item }: { item: InfinityItem }): number => { + return Object.values(this.groups) + .flat() + .findIndex((element) => element === item); + }); + + get groups(): InfinityItemByGroup { + return (this.args.items ?? []).reduce((groups, item) => { + const groupKey = item.groupKey ?? '_ungrouped_'; + if (!groups[groupKey]) { + groups[groupKey] = []; + } + + groups[groupKey]!.push(item); + + return groups; + }, {}); + } + + get lastKey(): string | undefined { + return Object.keys(this.groups).slice(-1)[0]; + } + get enableKeyboard(): boolean { return this.args.enableKeyboard ?? false; } @@ -170,7 +197,7 @@ export default class OSSInfiniteSelect extends Component { } @action - handleItemHover(index: number): void { + handleItemHover(index: number, event: MouseEvent): void { if (document.activeElement === this.searchInput) { return; } diff --git a/app/styles/base/_infinite-select.less b/app/styles/base/_infinite-select.less index 1deb9388f..90243301b 100644 --- a/app/styles/base/_infinite-select.less +++ b/app/styles/base/_infinite-select.less @@ -6,6 +6,16 @@ ul { padding: 0; } + + ul.group-container { + display: flex; + flex-direction: column; + gap: var(--spacing-px-3); + } + + hr.group-separator { + margin: 0 var(--spacing-px-12); + } } .upf-infinite-select--absolute { @@ -20,15 +30,15 @@ .upf-infinite-select__items-container { margin: 0; overflow: hidden auto; + overscroll-behavior: contain; + display: flex; + flex-direction: column; + gap: var(--spacing-px-6); } .upf-infinite-select__item { .upf-floating-menu__item; - &:not(:first-child) { - margin-top: var(--spacing-px-3); - } - &:hover { background-color: var(--color-gray-100); } diff --git a/tests/dummy/app/controllers/input.ts b/tests/dummy/app/controllers/input.ts index 557b3f967..b21bc54b9 100644 --- a/tests/dummy/app/controllers/input.ts +++ b/tests/dummy/app/controllers/input.ts @@ -26,9 +26,15 @@ export default class Input extends Controller { 'Black Panther', 'Captain Marvel' ]; - @tracked items: { name: string; label: string }[] = [ + @tracked items: { name: string; label: string; groupKey?: string }[] = [ { name: 'foo', label: 'foo' }, - { name: 'bar', label: 'bar' } + { name: 'bar', label: 'bar' }, + { name: 'banana', label: 'banana', groupKey: 'fruit' }, + { name: 'lettuce', label: 'lettuce', groupKey: 'vegetable' }, + { name: 'orange', label: 'orange', groupKey: 'fruit' }, + { name: 'carrot', label: 'carrot', groupKey: 'vegetable' }, + { name: 'apple', label: 'apple', groupKey: 'fruit' }, + { name: 'spinach', label: 'spinach', groupKey: 'vegetable' } ]; @tracked selectedItem: { name: string; label: string } | undefined = this.items[0]; @tracked emailInputValue: string = ''; diff --git a/tests/integration/components/o-s-s/infinite-select-test.js b/tests/integration/components/o-s-s/infinite-select-test.js index 30edb3b82..1dd910487 100644 --- a/tests/integration/components/o-s-s/infinite-select-test.js +++ b/tests/integration/components/o-s-s/infinite-select-test.js @@ -23,6 +23,16 @@ const FAKE_DATA = [ { name: 'Wolverine', characters: 'James Howlett' } ]; +const FAKE_DATA_GROUPED = [ + { name: 'banana', label: 'banana', groupKey: 'fruit' }, + { name: 'lettuce', label: 'lettuce', groupKey: 'vegetable' }, + { name: 'orange', label: 'orange', groupKey: 'fruit' }, + { name: 'carrot', label: 'carrot', groupKey: 'vegetable' }, + { name: 'apple', label: 'apple', groupKey: 'fruit' }, + { name: 'spinach', label: 'spinach', groupKey: 'vegetable' }, + { name: 'other', label: 'other' } +]; + module('Integration | Component | o-s-s/infinite-select', function (hooks) { setupRenderingTest(hooks); setupIntl(hooks); @@ -508,4 +518,72 @@ module('Integration | Component | o-s-s/infinite-select', function (hooks) { }); }); }); + + module('When data has groupKey', function (hooks) { + hooks.beforeEach(function () { + this.items = FAKE_DATA_GROUPED; + this.onSelect = () => {}; + }); + + test('For each different groupKey, a group is created', async function (assert) { + await renderGrouped(); + + assert.dom('.upf-infinite-select__items-container ul').exists({ count: 3 }); + assert + .dom('.upf-infinite-select__items-container ul:nth-of-type(1)') + .hasAttribute('data-control-name', 'infinite-select-group-fruit'); + assert + .dom('.upf-infinite-select__items-container ul:nth-of-type(2)') + .hasAttribute('data-control-name', 'infinite-select-group-vegetable'); + assert + .dom('.upf-infinite-select__items-container ul:nth-of-type(3)') + .hasAttribute('data-control-name', 'infinite-select-group-_ungrouped_'); + }); + + test('Items are placed in their respective group', async function (assert) { + await renderGrouped(); + + const groupFruitItems = Array.from( + document.querySelectorAll('.upf-infinite-select__items-container ul:nth-of-type(1) .upf-infinite-select__item') + ).map((el) => el.textContent.trim()); + const groupVegetableItems = Array.from( + document.querySelectorAll('.upf-infinite-select__items-container ul:nth-of-type(2) .upf-infinite-select__item') + ).map((el) => el.textContent.trim()); + const groupUngroupedItems = Array.from( + document.querySelectorAll('.upf-infinite-select__items-container ul:nth-of-type(3) .upf-infinite-select__item') + ).map((el) => el.textContent.trim()); + + assert.deepEqual(groupFruitItems, ['banana', 'orange', 'apple']); + assert.deepEqual(groupVegetableItems, ['lettuce', 'carrot', 'spinach']); + assert.deepEqual(groupUngroupedItems, ['other']); + }); + + module('Separators', function () { + test('A separator is rendered between each group', async function (assert) { + await renderGrouped(); + + assert.dom('.upf-infinite-select__items-container hr.group-separator').exists({ count: 2 }); + }); + + test('There is no separator after the last group', async function (assert) { + await renderGrouped(); + + assert.dom('.upf-infinite-select__items-container ul:last-of-type + hr.group-separator').doesNotExist(); + }); + }); + }); + + async function renderGrouped(): Promise { + await render( + hbs` + <:option as |item index|> +
    {{item.label}}
    + +
    ` + ); + } }); From aefbdde7c00364b4f1d57574e8f5e836fb4f0d1c Mon Sep 17 00:00:00 2001 From: Owen Coogan Date: Wed, 28 Jan 2026 11:49:25 +0100 Subject: [PATCH 30/45] feat: added hasError param to text area --- addon/components/o-s-s/text-area.hbs | 2 +- addon/components/o-s-s/text-area.stories.js | 25 +++++++++++- addon/components/o-s-s/text-area.ts | 6 +++ .../components/o-s-s/text-area-test.ts | 40 +++++++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/addon/components/o-s-s/text-area.hbs b/addon/components/o-s-s/text-area.hbs index bbe26c361..b1df4cb62 100644 --- a/addon/components/o-s-s/text-area.hbs +++ b/addon/components/o-s-s/text-area.hbs @@ -1,5 +1,5 @@
    -
    +