diff --git a/src/components/base/progress_bar/progress_bar.stories.js b/src/components/base/progress_bar/progress_bar.stories.js
index 0f03c66d5206d45d12c60a91e57929bc703e6c5f..d8c159e9532f3c6e8714f57ab084fcf0133364b1 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 0000000000000000000000000000000000000000..bc54741b1558b5213724c6fa77dceb86cc5eb9a0
--- /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 0000000000000000000000000000000000000000..c52eb90e01410ec8b710e46d8fca7b8c6948ca0e
--- /dev/null
+++ b/src/components/base/progress_bar/stacked_progress_bar.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
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
Binary files /dev/null and b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-base-progress-bar-stacked-1-snap.png differ
diff --git a/translations.js b/translations.js
index 31028804052e55a82916c8376b183a194b17cee2..5e9b49ed3eb5b03eddcd4d04fcba3409ba62d872 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',
};