From d24c4bde0a48d429936334260c1071800e27bb20 Mon Sep 17 00:00:00 2001
From: Nathaniel Waldschmidt
<77284592+NateWaldschmidt@users.noreply.github.com>
Date: Thu, 11 Apr 2024 12:56:43 -0500
Subject: [PATCH] SELF-243: Add Steps Component (#472)
* feat: add icon button
* feat: add steps component
* refactor: Ken PR review
---
CHANGELOG.md | 12 +-
package-lock.json | 4 +-
package.json | 2 +-
src/components/ConditionalWrapper.vue | 12 --
src/components/Icon/svgs/Back.svg | 3 +
.../Icon/svgs/CampaignAutomated.svg | 3 +
src/components/Icon/svgs/CampaignOneTime.svg | 3 +
src/components/Icon/svgs/Next.svg | 3 +
src/components/Icon/svgs/Success.svg | 3 +
src/components/Icon/types.ts | 5 +
src/components/IconButton/IconButton.vue | 2 +-
.../IconButton/__tests__/IconButton.spec.ts | 24 +++-
src/components/Steps/Steps.mdx | 10 ++
src/components/Steps/Steps.stories.ts | 26 ++++
src/components/Steps/Steps.vue | 119 ++++++++++++++++++
src/components/Steps/__tests__/Steps.spec.ts | 42 +++++++
src/components/Steps/constants.ts | 4 +
src/components/Steps/index.ts | 3 +
src/components/Steps/types.ts | 6 +
src/main.ts | 5 +-
src/utils/ConditionalWrapper.vue | 16 +++
.../__tests__/ConditionalWrapper.spec.ts | 39 ++++++
...ormatBytes.spec.js => formatBytes.spec.ts} | 0
.../{object.spec.js => object.spec.ts} | 4 +-
src/utils/index.js | 1 -
src/utils/stringDiff.js | 75 -----------
vite.config.js | 6 +-
27 files changed, 329 insertions(+), 103 deletions(-)
delete mode 100644 src/components/ConditionalWrapper.vue
create mode 100644 src/components/Icon/svgs/Back.svg
create mode 100644 src/components/Icon/svgs/CampaignAutomated.svg
create mode 100644 src/components/Icon/svgs/CampaignOneTime.svg
create mode 100644 src/components/Icon/svgs/Next.svg
create mode 100644 src/components/Icon/svgs/Success.svg
create mode 100644 src/components/Steps/Steps.mdx
create mode 100644 src/components/Steps/Steps.stories.ts
create mode 100644 src/components/Steps/Steps.vue
create mode 100644 src/components/Steps/__tests__/Steps.spec.ts
create mode 100644 src/components/Steps/constants.ts
create mode 100644 src/components/Steps/index.ts
create mode 100644 src/components/Steps/types.ts
create mode 100644 src/utils/ConditionalWrapper.vue
create mode 100644 src/utils/__tests__/ConditionalWrapper.spec.ts
rename src/utils/__tests__/{formatBytes.spec.js => formatBytes.spec.ts} (100%)
rename src/utils/__tests__/{object.spec.js => object.spec.ts} (93%)
delete mode 100644 src/utils/stringDiff.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 824a8289b..f674c57c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,16 @@
## v2.0.32
+- Add `` component
+- Adds the icons:
+ - `Back`
+ - `CampaignAutomated`
+ - `CampaignOneTime`
+ - `Next`
+ - `Success`
+
+## v2.0.32
+
- Add `` component
## v2.0.31
@@ -25,7 +35,7 @@
## v2.0.29
-- Make `PrimeVue` and external dependency
+- Make `PrimeVue` an external dependency
## v2.0.28
diff --git a/package-lock.json b/package-lock.json
index 68356b1db..34318447b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@lob/ui-components",
- "version": "2.0.32",
+ "version": "2.0.33",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@lob/ui-components",
- "version": "2.0.32",
+ "version": "2.0.33",
"dependencies": {
"date-fns": "^2.29.3",
"date-fns-holiday-us": "^0.3.1",
diff --git a/package.json b/package.json
index 3adf39c1f..5da2f8bd3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@lob/ui-components",
- "version": "2.0.32",
+ "version": "2.0.33",
"engines": {
"node": ">=20.2.0",
"npm": ">=10.2.0"
diff --git a/src/components/ConditionalWrapper.vue b/src/components/ConditionalWrapper.vue
deleted file mode 100644
index a3fc99457..000000000
--- a/src/components/ConditionalWrapper.vue
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/src/components/Icon/svgs/Back.svg b/src/components/Icon/svgs/Back.svg
new file mode 100644
index 000000000..927c7c622
--- /dev/null
+++ b/src/components/Icon/svgs/Back.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/Icon/svgs/CampaignAutomated.svg b/src/components/Icon/svgs/CampaignAutomated.svg
new file mode 100644
index 000000000..fa96ed142
--- /dev/null
+++ b/src/components/Icon/svgs/CampaignAutomated.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/Icon/svgs/CampaignOneTime.svg b/src/components/Icon/svgs/CampaignOneTime.svg
new file mode 100644
index 000000000..63e0f27e4
--- /dev/null
+++ b/src/components/Icon/svgs/CampaignOneTime.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/Icon/svgs/Next.svg b/src/components/Icon/svgs/Next.svg
new file mode 100644
index 000000000..36f7e8bfe
--- /dev/null
+++ b/src/components/Icon/svgs/Next.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/Icon/svgs/Success.svg b/src/components/Icon/svgs/Success.svg
new file mode 100644
index 000000000..a33f502ec
--- /dev/null
+++ b/src/components/Icon/svgs/Success.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/Icon/types.ts b/src/components/Icon/types.ts
index c95648f4a..f112b82be 100644
--- a/src/components/Icon/types.ts
+++ b/src/components/Icon/types.ts
@@ -13,10 +13,13 @@ export const IconName = {
APP_WINDOWS: 'AppWindows',
ARROW_DOWN_TO_LINE: 'ArrowDownToLine',
ARROW_TREND_UP: 'ArrowTrendUp',
+ BACK: 'Back',
BANK_ACCOUNT: 'BankAccount',
BARS: 'Bars',
BELL: 'Bell',
BULLHORN: 'Bullhorn',
+ CAMPAIGN_AUTOMATED: 'CampaignAutomated',
+ CAMPAIGN_ONE_TIME: 'CampaignOneTime',
CAR: 'Car',
CIRCLE_CHECK: 'CircleCheck',
CIRCLE_CLOSE: 'CircleClose',
@@ -41,6 +44,7 @@ export const IconName = {
LIGHTNING: 'Lightning',
LOCATION_PIN: 'LocationPin',
MONEY_BILL: 'MoneyBill',
+ NEXT: 'Next',
OPEN_BOOK: 'OpenBook',
PIE_CHART_SLICE: 'PieChartSlice',
PLAN_BUSINESS: 'PlanBusiness',
@@ -51,6 +55,7 @@ export const IconName = {
PLAN_STARTUP: 'PlanStartup',
PLUS: 'Plus',
SIGNAL: 'Signal',
+ SUCCESS: 'Success',
TRIANGLE_EXCLAMATION: 'TriangleExclamation',
USER: 'User',
USERS: 'Users',
diff --git a/src/components/IconButton/IconButton.vue b/src/components/IconButton/IconButton.vue
index 98c9bd010..618cb60f2 100644
--- a/src/components/IconButton/IconButton.vue
+++ b/src/components/IconButton/IconButton.vue
@@ -22,7 +22,7 @@
import Icon from '@/components/Icon/Icon.vue';
import { IconName } from '@/components/Icon/types';
import { Size } from '@/types';
-import ConditionalWrapper from '@/components/ConditionalWrapper.vue';
+import ConditionalWrapper from '@/utils/ConditionalWrapper.vue';
import Button from 'primevue/button';
import { AnchorHTMLAttributes, computed, defineOptions } from 'vue';
diff --git a/src/components/IconButton/__tests__/IconButton.spec.ts b/src/components/IconButton/__tests__/IconButton.spec.ts
index 4df7a8167..3374d6a40 100644
--- a/src/components/IconButton/__tests__/IconButton.spec.ts
+++ b/src/components/IconButton/__tests__/IconButton.spec.ts
@@ -1,13 +1,31 @@
import '@testing-library/jest-dom';
-import { IconName } from '@/main';
+import {
+ IconButtonColor,
+ IconButtonSize,
+ IconButtonVariant,
+ IconName
+} from '@/main';
import { render } from '@testing-library/vue';
import IconButton from '../IconButton.vue';
describe('IconButton', () => {
- it('renders', () => {
+ it.each([
+ { icon: IconName.APP_WINDOWS, size: IconButtonSize.SM },
+ { icon: IconName.APP_WINDOWS, size: IconButtonSize.MD },
+ { icon: IconName.APP_WINDOWS, size: IconButtonSize.LG },
+ { icon: IconName.APP_WINDOWS, size: IconButtonSize.XL },
+ { icon: IconName.APP_WINDOWS, color: IconButtonColor.ERROR },
+ { icon: IconName.APP_WINDOWS, color: IconButtonColor.INFO },
+ { icon: IconName.APP_WINDOWS, color: IconButtonColor.NEUTRAL },
+ { icon: IconName.APP_WINDOWS, color: IconButtonColor.SUCCESS },
+ { icon: IconName.APP_WINDOWS, color: IconButtonColor.UPGRADE },
+ { icon: IconName.APP_WINDOWS, color: IconButtonColor.WARNING },
+ { icon: IconName.APP_WINDOWS, variant: IconButtonVariant.OUTLINED },
+ { icon: IconName.APP_WINDOWS, variant: IconButtonVariant.PRIMARY }
+ ])('renders', (props) => {
const { getByTestId } = render(IconButton, {
- props: { icon: IconName.APP_WINDOWS }
+ props
});
expect(getByTestId('uic-icon-button')).toBeVisible();
});
diff --git a/src/components/Steps/Steps.mdx b/src/components/Steps/Steps.mdx
new file mode 100644
index 000000000..2a3808cf5
--- /dev/null
+++ b/src/components/Steps/Steps.mdx
@@ -0,0 +1,10 @@
+import { Canvas, ArgTypes, PRIMARY_STORY } from '@storybook/addon-docs';
+import { Primary } from './Steps.stories';
+
+# Steps
+
+
+
+## Props
+
+
diff --git a/src/components/Steps/Steps.stories.ts b/src/components/Steps/Steps.stories.ts
new file mode 100644
index 000000000..9a4c732c9
--- /dev/null
+++ b/src/components/Steps/Steps.stories.ts
@@ -0,0 +1,26 @@
+import { Meta, StoryObj } from '@storybook/vue3';
+import mdx from './Steps.mdx';
+// @ts-ignore No types from Vue file
+import Steps from './Steps.vue';
+
+const meta: Meta = {
+ title: 'Components/Steps',
+ component: Steps,
+ parameters: {
+ docs: {
+ page: mdx
+ }
+ }
+};
+
+export default meta;
+
+export const Primary: StoryObj = {
+ args: {
+ items: [
+ { label: 'Step 1', status: 'success' },
+ { label: 'Step 2' },
+ { label: 'Step 3' }
+ ]
+ }
+};
diff --git a/src/components/Steps/Steps.vue b/src/components/Steps/Steps.vue
new file mode 100644
index 000000000..f60c6ae10
--- /dev/null
+++ b/src/components/Steps/Steps.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+ {{ index + 1 }}
+
+
+ {{ label }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Steps/__tests__/Steps.spec.ts b/src/components/Steps/__tests__/Steps.spec.ts
new file mode 100644
index 000000000..acfeb4c6b
--- /dev/null
+++ b/src/components/Steps/__tests__/Steps.spec.ts
@@ -0,0 +1,42 @@
+import '@testing-library/jest-dom';
+import { render, within } from '@testing-library/vue';
+
+import Steps from '../Steps.vue';
+
+type PropsType = InstanceType['$props'];
+
+const DEFAULT_PROPS: PropsType = {
+ items: [{ label: 'Step 1' }, { label: 'Step 2' }, { label: 'Step 3' }]
+};
+
+describe('Steps', () => {
+ let props: PropsType;
+
+ beforeEach(() => {
+ props = { ...DEFAULT_PROPS };
+ });
+
+ it('renders', () => {
+ const { getByTestId } = render(Steps, { props });
+
+ const steps = getByTestId('uic-steps');
+ expect(steps).toBeVisible();
+ const { getByText: stepsGetByText } = within(steps);
+ expect(stepsGetByText('Step 1')).toBeVisible();
+ expect(stepsGetByText('Step 2')).toBeVisible();
+ expect(stepsGetByText('Step 3')).toBeVisible();
+ });
+
+ it('renders icons with statuses', async () => {
+ props = {
+ ...props,
+ items: [
+ { label: 'Step 1', status: 'success' },
+ { label: 'Step 2' },
+ { label: 'Step 3' }
+ ]
+ };
+ const { findByTestId } = render(Steps, { props });
+ expect(await findByTestId('uic-steps-icon')).toBeVisible();
+ });
+});
diff --git a/src/components/Steps/constants.ts b/src/components/Steps/constants.ts
new file mode 100644
index 000000000..66f6af3bc
--- /dev/null
+++ b/src/components/Steps/constants.ts
@@ -0,0 +1,4 @@
+export const StepsStatus = {
+ SUCCESS: 'success'
+} as const;
+export type StepsStatus = (typeof StepsStatus)[keyof typeof StepsStatus];
diff --git a/src/components/Steps/index.ts b/src/components/Steps/index.ts
new file mode 100644
index 000000000..7a2201b20
--- /dev/null
+++ b/src/components/Steps/index.ts
@@ -0,0 +1,3 @@
+export * from './constants';
+export { default as Steps } from './Steps.vue';
+export * from './types';
diff --git a/src/components/Steps/types.ts b/src/components/Steps/types.ts
new file mode 100644
index 000000000..2551de9df
--- /dev/null
+++ b/src/components/Steps/types.ts
@@ -0,0 +1,6 @@
+import { StepsStatus } from './constants';
+
+export interface StepItem {
+ label: string;
+ status?: StepsStatus;
+}
diff --git a/src/main.ts b/src/main.ts
index e2e655275..1a0bc5c2c 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -25,11 +25,12 @@ const ComponentLibrary = {
// TODO Utilize the components export but first we will have to remove global usage.
export * from './components/Badge';
+export * from './components/ColorPicker';
export * from './components/Icon';
export * from './components/IconButton';
-export * from './components/Modal';
export * from './components/ImageFileUpload';
-export * from './components/ColorPicker';
+export * from './components/Modal';
+export * from './components/Steps';
export default ComponentLibrary;
diff --git a/src/utils/ConditionalWrapper.vue b/src/utils/ConditionalWrapper.vue
new file mode 100644
index 000000000..a2182b577
--- /dev/null
+++ b/src/utils/ConditionalWrapper.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
diff --git a/src/utils/__tests__/ConditionalWrapper.spec.ts b/src/utils/__tests__/ConditionalWrapper.spec.ts
new file mode 100644
index 000000000..488fa79ea
--- /dev/null
+++ b/src/utils/__tests__/ConditionalWrapper.spec.ts
@@ -0,0 +1,39 @@
+import '@testing-library/jest-dom';
+import ConditionalWrapper from '../ConditionalWrapper.vue';
+import { render } from '@testing-library/vue';
+
+type PropsType = InstanceType['$props'] &
+ Record;
+
+const DEFAULT_SLOT = '' as const;
+
+describe('ConditionalWrapper', () => {
+ let props: PropsType;
+
+ beforeEach(() => {
+ props = {};
+ });
+
+ it('renders wrapper when provided', () => {
+ props = {
+ tag: 'h2',
+ 'data-testid': 'conditional-wrapper'
+ };
+ const { getByTestId } = render(ConditionalWrapper, {
+ props,
+ slots: { default: DEFAULT_SLOT }
+ });
+ expect(getByTestId('conditional-wrapper')).toBeVisible();
+ expect(getByTestId('conditional-wrapper').tagName).toBe('H2');
+ expect(getByTestId('conditional-wrapper-slot')).toBeVisible();
+ });
+
+ it('does not render wrapper when not provided', () => {
+ const { getByTestId, queryByRole } = render(ConditionalWrapper, {
+ props,
+ slots: { default: DEFAULT_SLOT }
+ });
+ expect(queryByRole('h2')).toBeFalsy();
+ expect(getByTestId('conditional-wrapper-slot')).toBeVisible();
+ });
+});
diff --git a/src/utils/__tests__/formatBytes.spec.js b/src/utils/__tests__/formatBytes.spec.ts
similarity index 100%
rename from src/utils/__tests__/formatBytes.spec.js
rename to src/utils/__tests__/formatBytes.spec.ts
diff --git a/src/utils/__tests__/object.spec.js b/src/utils/__tests__/object.spec.ts
similarity index 93%
rename from src/utils/__tests__/object.spec.js
rename to src/utils/__tests__/object.spec.ts
index 785daae9a..64baa52c4 100644
--- a/src/utils/__tests__/object.spec.js
+++ b/src/utils/__tests__/object.spec.ts
@@ -2,8 +2,8 @@ import { shallowEquals } from '../object';
describe('object utils', () => {
describe('shallowEquals', () => {
- let obj1;
- let obj2;
+ let obj1: Record;
+ let obj2: Record | null;
it('throws an error when either obj is null or undefined', () => {
obj1 = { a: 1, b: 2 };
diff --git a/src/utils/index.js b/src/utils/index.js
index c8f6f9402..7a38a723e 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -4,4 +4,3 @@ export * from './keyboard';
export * from './object';
export * from './formatBytes';
export * from './escapeRegExp';
-export * from './stringDiff';
diff --git a/src/utils/stringDiff.js b/src/utils/stringDiff.js
deleted file mode 100644
index 4842c6f77..000000000
--- a/src/utils/stringDiff.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * @typedef {Object} StringDiff
- * @property {string} type - The type of edit that was made. One of 'insertion', 'edit', 'deletion', or null if the strings are identical.
- * @property {string} diff - The string that was inserted, edited, or deleted.
- * @property {number} diffIndex - The index at which the edit occurred.
- * @property {string} prevValue - The previous value of the string at the edit index, or null if the edit was an insertion.
- */
-
-/**
- *
- * @param {string} str1
- * @param {string} str2
- * @returns {StringDiff}
- */
-export function stringDiff(str1, str2) {
- const len1 = str1.length;
- const len2 = str2.length;
-
- let startIndex = 0;
- while (
- startIndex < len1 &&
- startIndex < len2 &&
- str1[startIndex] === str2[startIndex]
- ) {
- startIndex++;
- }
-
- let endIndex1 = len1 - 1;
- let endIndex2 = len2 - 1;
- while (
- endIndex1 >= 0 &&
- endIndex2 >= 0 &&
- str1[endIndex1] === str2[endIndex2]
- ) {
- endIndex1--;
- endIndex2--;
- }
-
- let diff;
- let type;
- let editIndex;
- let prevValue;
- if (startIndex > endIndex1 && startIndex <= endIndex2) {
- // There was an insertion
- diff = str2.substring(startIndex, endIndex2 + 1);
- type = 'insertion';
- editIndex = startIndex;
- prevValue = null;
- } else if (startIndex <= endIndex1 && startIndex > endIndex2) {
- // There was a deletion
- diff = str1.substring(startIndex, endIndex1 + 1);
- type = 'deletion';
- editIndex = startIndex;
- prevValue = null;
- } else if (startIndex <= endIndex1 && startIndex <= endIndex2) {
- // There was a modification
- diff = str2.substring(startIndex, endIndex2 + 1);
- type = 'edit';
- editIndex = startIndex;
- prevValue = str1.substring(startIndex, endIndex1 + 1);
- } else {
- // The strings are identical
- diff = '';
- type = null;
- editIndex = null;
- prevValue = null;
- }
-
- return {
- type,
- diff,
- editIndex,
- prevValue
- };
-}
diff --git a/vite.config.js b/vite.config.js
index c0374b3f7..0f5e1ce91 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -41,12 +41,12 @@ export default defineConfig({
globals: true,
setupFiles: 'setupTests.js',
coverage: {
- include: ['src/**/*.{js,vue}'],
+ include: ['src/**/*.{js,ts,vue}'],
exclude: [
'src/**/index.{js,ts}', // No need to cover index files for exports
- 'src/main.js', // No need to cover bootstrap file
+ 'src/main.ts', // No need to cover bootstrap file
'src/**/*.spec.{js,ts}', // No need to cover test files
- 'src/**/*.stories.js', // No need to cover stories files
+ 'src/**/*.stories.{js,ts}', // No need to cover stories files
'src/theme/**', // No need to cover components just for showing theming
'src/components/Icons/**' // No need to cover components just for rendering svg icons
],