From 4a775d469bacd022caf635f9b1e57555fd657c26 Mon Sep 17 00:00:00 2001 From: Miranda Fluharty Date: Thu, 20 Feb 2025 17:44:36 -0700 Subject: [PATCH 1/2] feat(GlStackedProgressBar): Implement GlStackedProgressBar component Duplicate GlProgressBar component, modify to allow multiple bars: - accept an array of segments with values and classes - render a bar for each segment with proportional width, apply classes - add test coverage --- .../base/progress_bar/progress_bar.stories.js | 22 ++++ .../progress_bar/stacked_progress_bar.spec.js | 104 ++++++++++++++++++ .../progress_bar/stacked_progress_bar.vue | 66 +++++++++++ translations.js | 1 + 4 files changed, 193 insertions(+) create mode 100644 src/components/base/progress_bar/stacked_progress_bar.spec.js create mode 100644 src/components/base/progress_bar/stacked_progress_bar.vue diff --git a/src/components/base/progress_bar/progress_bar.stories.js b/src/components/base/progress_bar/progress_bar.stories.js index 0f03c66d52..d8c159e953 100644 --- a/src/components/base/progress_bar/progress_bar.stories.js +++ b/src/components/base/progress_bar/progress_bar.stories.js @@ -1,5 +1,6 @@ import { progressBarVariantOptions } from '../../../utils/constants'; import GlProgressBar from './progress_bar.vue'; +import GlStackedProgressBar from './stacked_progress_bar.vue'; const generateProps = ({ value = 30, @@ -37,6 +38,27 @@ export const Variants = (args, { argTypes = {} }) => ({ Variants.args = generateProps(); Variants.parameters = { controls: { disable: true } }; +export const Stacked = (args, { argTypes = {} }) => ({ + components: { GlStackedProgressBar }, + props: Object.keys(argTypes), + template: ` + + `, +}); +Stacked.args = { + segments: [ + { id: 'critical-segment', value: 800, classes: ['gl-bg-red-800'] }, + { id: 'high-segment', value: 200, classes: ['gl-bg-red-600'] }, + { id: 'medium-segment', value: 108, classes: ['gl-bg-orange-400'] }, + { id: 'low-segment', value: 100, classes: ['gl-bg-orange-300'] }, + ], + max: 1208, +}; +Stacked.parameters = { controls: { disable: true } }; + export default { title: 'base/progress-bar', component: GlProgressBar, diff --git a/src/components/base/progress_bar/stacked_progress_bar.spec.js b/src/components/base/progress_bar/stacked_progress_bar.spec.js new file mode 100644 index 0000000000..bc54741b15 --- /dev/null +++ b/src/components/base/progress_bar/stacked_progress_bar.spec.js @@ -0,0 +1,104 @@ +import { mount } from '@vue/test-utils'; +import StackedProgressBar from './stacked_progress_bar.vue'; + +describe('GlStackedProgressBar', () => { + let wrapper; + + const segments = [ + { id: 1, value: 25 }, + { id: 2, value: 10 }, + ]; + + const createWrapper = ( + propsData = { + segments, + } + ) => { + wrapper = mount(StackedProgressBar, { + propsData, + }); + }; + + const findProgressAt = (i) => wrapper.findAll('[role="progressbar"]').at(i); + + beforeEach(() => { + createWrapper(); + }); + + describe('default', () => { + it('renders wrapper with expected classes and style', () => { + expect(wrapper.classes()).toMatchObject(['gl-stacked-progress-bar', 'progress']); + expect(wrapper.attributes('style')).toBeUndefined(); + }); + + it.each` + index | value + ${0} | ${25} + ${1} | ${10} + `('renders children with expected classes and attributes', ({ index, value }) => { + const progress = findProgressAt(index); + + expect(progress.classes()).toMatchObject(['gl-progress']); + expect(progress.attributes('style')).toBe(`width: ${value}%;`); + expect(progress.attributes('aria-label')).toBe('Stacked progress bar'); + expect(progress.attributes('aria-valuemin')).toBe('0'); + expect(progress.attributes('aria-valuemax')).toBe('100'); + expect(progress.attributes('aria-valuenow')).toBe(`${value}`); + }); + }); + + describe('value', () => { + it('sets style and attributes correctly when setting `value`', () => { + const value = 65.6; + createWrapper({ segments: [{ id: 1, value }] }); + + const progress = findProgressAt(0); + const computedValue = parseFloat(value); + + expect(progress.attributes('style')).toBe(`width: ${computedValue}%;`); + expect(progress.attributes('aria-valuenow')).toBe(`${parseFloat(value)}`); + }); + }); + + describe('ariaLabel', () => { + it('sets value from prop', () => { + const label = 'Progress'; + createWrapper({ segments, ariaLabel: label }); + + const progress = findProgressAt(0); + + expect(progress.attributes('aria-label')).toBe('Progress'); + }); + }); + + describe('max', () => { + it('sets style and attributes correctly when using custom `max`', () => { + const value = 45.1; + const max = 75.5; + createWrapper({ segments: [{ id: 1, value }], max }); + + const progress = findProgressAt(0); + const computedValue = (parseFloat(value) / parseFloat(max)) * 100; + + expect(progress.attributes('style')).toBe(`width: ${computedValue}%;`); + expect(progress.attributes('aria-valuemax')).toBe(`${parseFloat(max)}`); + expect(progress.attributes('aria-valuenow')).toBe(`${parseFloat(value)}`); + }); + }); + + describe('segment classes', () => { + it.each(['primary', 'gl-bg-red-800'])('sets correct css class for variant %s', (cssClass) => { + createWrapper({ segments: [{ id: 1, value: 50, classes: [cssClass] }] }); + + expect(findProgressAt(0).classes()).toMatchObject(['gl-progress', cssClass]); + }); + }); + + describe('height', () => { + it('sets height correctly', () => { + createWrapper({ segments, height: '5px' }); + + expect(wrapper.attributes('style')).toBe('height: 5px;'); + }); + }); +}); diff --git a/src/components/base/progress_bar/stacked_progress_bar.vue b/src/components/base/progress_bar/stacked_progress_bar.vue new file mode 100644 index 0000000000..c52eb90e01 --- /dev/null +++ b/src/components/base/progress_bar/stacked_progress_bar.vue @@ -0,0 +1,66 @@ + + + diff --git a/translations.js b/translations.js index 3102880405..5e9b49ed3e 100644 --- a/translations.js +++ b/translations.js @@ -26,5 +26,6 @@ export default { 'GlSearchBoxByType.input.placeholder': 'Search', 'GlSorting.sortAscending': 'Sort direction: ascending', 'GlSorting.sortDescending': 'Sort direction: descending', + 'GlStackedProgressBar.ariaLabel': 'Stacked progress bar', 'GlToken.closeButtonTitle': 'Remove', }; -- GitLab From dac1c49af26d0ef6aa5a31530cc0f7d22190cdb8 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 25 Feb 2025 00:12:04 +0000 Subject: [PATCH 2/2] chore: update snapshots --- ...ryshots-base-progress-bar-stacked-1-snap.png | Bin 0 -> 10095 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/__image_snapshots__/storyshots-spec-js-image-storyshots-base-progress-bar-stacked-1-snap.png diff --git a/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-base-progress-bar-stacked-1-snap.png b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-base-progress-bar-stacked-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..819c24784de66c43587e07adf2cc00fd80634d25 GIT binary patch literal 10095 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>iV_;yIRn}C%z@Wh3>Eakt5%>14VRme( z%<+f+mx^hH2$$@%xVdWf=58l(aaMNol}$T3*E<&!eH7iH!PgZae$ag3g56id6fc=g zaMjv-{6Y}NvS%_;KW0?TUt{@jy3NmJA0D=)nab{a8^7CZUCobsJHDRwe|+xXzYlH= z42(=H90CzaA8t+dp7-g@HjY0(55C)0vy%U7>ALIZ?XMiSeJ8)gvbfI2TECY&zsBa= zTO0E*_WC!z^Y{Pyw4*lpec}IGHUC&R1QZ+^9CQ>Ml0F32`8CY>!+Yx*!_M`aEO%S$ z#r>;Vv#;v=+n2w--=Fu%^7j|8Vur@c8VU}6kISF#|2>DLgJEX${Vn@U!shS&aOTVP ze`j~5@0|~En?iw)LxaT8Kt@I-hi7$4X8ty&Ve{>*m+fSStN5V6$e1L~l=72p+u!gF z@4PuDJe~bf1?)405B!XbNkSkwmJ_zxjGJnV=czb=z0K0WJ4fL`_y)gu?F@nn{}>e^ z!g7HJv=#a}e%SvmU{rFL=5tG1!F!60Ggu3UO@Kp##4)g2ru}i@n4s!452}r&gZB&< zhXA9I^`yh>EGL9i&x3-5fsu*n2wNH}$b+(-43SPxBthY=;PBu+%O9?3xiX>x3QQ>{ z{afr^cDS50hJ<57xI<1tm_q}{gq;)b{B>@L-}m|2m*4#N&)UBJ3sJ$)n9LN%Xvnp` zLFazM|F-g;%-YN~d2xSBe?9sde(bGu_35uRo)8xcScnKHq$xN|%TKt?oKjp?9<%4q z*$ChE{NMXyWy9^N>hJvedA#s>LjBG~>YQLFH?9>EP>BBf*8bbk-=b?Kf8Vf{aT6#7 z`0uyS+w=3J#rDnn#1-mFAh8Guht+8KC^(F!kJ0=wT0V@HkD#&;QYenr52N+tX!`-$1|F>+N8691?dQ?{ z!)X5r+(jMjKeBL)wvR^JN2Bed(e@E!&~0@5WOV#wbo^v={A6_e1U9KK+CCa>AC0z; zM%zb2v34BceJlNT9=L1WutZ3Xk9iG>oR)OUw?QvuAkihlu7wK Q0|Nttr>mdKI;Vst0Q0HWPXGV_ literal 0 HcmV?d00001 -- GitLab