Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(component): implement FeatureTag component #1567

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions .changeset/thick-coats-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@bigcommerce/big-design': minor
'@bigcommerce/docs': minor
---

Added FeatureSet component
8 changes: 4 additions & 4 deletions packages/big-design/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ module.exports = {
setupFilesAfterEnv: ['<rootDir>/setupTests.ts'],
coverageThreshold: {
global: {
statements: 95.73,
branches: 86.97,
functions: 97,
lines: 95.77,
statements: 96.35,
branches: 88.7,
functions: 97.55,
lines: 96.73,
},
},
};
24 changes: 24 additions & 0 deletions packages/big-design/src/components/FeatureSet/FeatureSet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { ComponentPropsWithoutRef, memo } from 'react';

import { MarginProps } from '../../helpers';

import { StyledUl } from './styled';
import { Tag, TagProps } from './Tag';

export interface FeatureSetProps extends ComponentPropsWithoutRef<'ul'>, MarginProps {
tags: TagProps[];
}

export const FeatureSet: React.FC<FeatureSetProps> = memo(
({ tags, className, style, ...props }) => {
return tags && tags.length > 0 ? (
<StyledUl {...props}>
{tags.map((tag, index) => (
<Tag {...tag} key={index} />
))}
</StyledUl>
) : null;
},
);

FeatureSet.displayName = 'FeatureSet';
25 changes: 25 additions & 0 deletions packages/big-design/src/components/FeatureSet/Tag/Tag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { memo, ReactNode, useId } from 'react';

import { StyleableSmall } from '../../Typography/private';

import { StyledLi } from './styled';

export interface TagProps {
icon?: ReactNode;
label: string;
}

export const Tag: React.FC<TagProps> = memo(({ icon, label }) => {
const id = useId();

return label ? (
<StyledLi aria-labelledby={id}>
{icon}
<StyleableSmall as="span" color="currentColor" ellipsis id={id} margin="none">
{label}
</StyleableSmall>
</StyledLi>
) : null;
});

Tag.displayName = 'Tag';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Tag, type TagProps } from './Tag';
21 changes: 21 additions & 0 deletions packages/big-design/src/components/FeatureSet/Tag/styled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { theme as defaultTheme } from '@bigcommerce/big-design-theme';
import styled from 'styled-components';

export const StyledLi = styled.li.attrs({ theme: defaultTheme })`
align-items: center;
background-color: ${({ theme }) => theme.colors.secondary20};
border-radius: ${({ theme }) => theme.borderRadius.normal};
display: inline-flex;
color: ${({ theme }) => theme.colors.secondary60};
flex-wrap: nowrap;
gap: ${({ theme }) => theme.spacing.xxSmall};
justify-content: center;
padding-block: ${({ theme }) => theme.helpers.remCalc(2)};
padding-inline: ${({ theme }) => theme.spacing.xSmall};

& > svg {
height: ${({ theme }) => theme.spacing.medium};
flex-shrink: 0;
width: ${({ theme }) => theme.spacing.medium};
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`render feature set 1`] = `
.c0 {
list-style-type: none;
margin: 0;
padding: 0;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
gap: 0.75rem;
}

.c2 {
color: currentColor;
margin: 0 0 1rem;
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: normal;
color: currentColor;
font-size: 0.875rem;
font-weight: 400;
line-height: 1.25rem;
margin: 0 0 0.75rem;
margin: 0;
}

.c2:last-child {
margin-bottom: 0;
}

.c1 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
background-color: #ECEEF5;
border-radius: 0.25rem;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
color: #5E637A;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
gap: 0.25rem;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
padding-block: 0.125rem;
padding-inline: 0.5rem;
}

.c1 > svg {
height: 1rem;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
width: 1rem;
}

<ul
class="c0"
>
<li
aria-labelledby=":r0:"
class="c1"
>
<span
class="c2"
color="currentColor"
id=":r0:"
>
Feature 1
</span>
</li>
<li
aria-labelledby=":r1:"
class="c1"
>
<span
class="c2"
color="currentColor"
id=":r1:"
>
Feature 2
</span>
</li>
</ul>
`;
1 change: 1 addition & 0 deletions packages/big-design/src/components/FeatureSet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FeatureSet, type FeatureSetProps } from './FeatureSet';
58 changes: 58 additions & 0 deletions packages/big-design/src/components/FeatureSet/spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { AutoAwesomeIcon } from '@bigcommerce/big-design-icons';
import { render, screen } from '@testing-library/react';
import React from 'react';

import 'jest-styled-components';

import { FeatureSet } from './index';

test('render feature set', () => {
const { container } = render(
<FeatureSet tags={[{ label: 'Feature 1' }, { label: 'Feature 2' }]} />,
);

expect(container.firstChild).toMatchSnapshot();
});

test("doesn't forward styles", () => {
const { container } = render(
<FeatureSet
className="test"
style={{ background: 'red' }}
tags={[{ label: 'Feature 1' }, { label: 'Feature 2' }]}
/>,
);

expect(container.getElementsByClassName('test')[0]).toBeUndefined();
expect(screen.getByRole('list')).not.toHaveStyle('background: red');
});

test("doesn't render if tags are empty", () => {
render(<FeatureSet tags={[]} />);

expect(screen.queryByRole('list')).not.toBeInTheDocument();
});

test('allows margins to be set', () => {
render(<FeatureSet margin="medium" tags={[{ label: 'Feature 1' }, { label: 'Feature 2' }]} />);

expect(screen.getByRole('list')).toHaveStyle('margin: 1rem');
});

test('renders tag with icon', () => {
render(
<FeatureSet
tags={[{ label: 'Feature 1', icon: <AutoAwesomeIcon /> }, { label: 'Feature 2' }]}
/>,
);

expect(
screen.getByRole('listitem', { name: 'Feature 1' }).getElementsByTagName('svg')[0],
).toBeInTheDocument();
});

test("doesn't render tag with invalid label", () => {
render(<FeatureSet tags={[{ label: '' }]} />);

expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
});
14 changes: 14 additions & 0 deletions packages/big-design/src/components/FeatureSet/styled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { theme as defaultTheme } from '@bigcommerce/big-design-theme';
import styled from 'styled-components';

import { withMargins } from '../../helpers';

export const StyledUl = styled.ul.attrs({ theme: defaultTheme })`
${({ theme }) => theme.helpers.listReset}

${withMargins()};

display: inline-flex;
flex-wrap: wrap;
gap: ${({ theme }) => theme.spacing.small};
`;
3 changes: 2 additions & 1 deletion packages/big-design/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ export * from './Chip';
export * from './Collapse';
export * from './Datepicker';
export * from './Dropdown';
export * from './FeatureSet';
export * from './Flex';
export * from './Fieldset';
export * from './FileUploader';
export * from './Form';
export * from './GlobalStyles';
export * from './Grid';
Expand Down Expand Up @@ -43,4 +45,3 @@ export * from './Tooltip';
export * from './Toggle';
export * from './Typography';
export * from './Worksheet';
export * from './FileUploader';
55 changes: 55 additions & 0 deletions packages/docs/PropTables/FeatureSetPropTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';

import { Code, NextLink, Prop, PropTable, PropTableWrapper } from '../components';

const featureSetProps: Prop[] = [
{
name: 'tags',
types: (
<NextLink href={{ hash: 'feature-tag-prop-table', query: { props: 'feature-tag' } }}>
TagProps[]
</NextLink>
),
description: (
<>
See{' '}
<NextLink href={{ hash: 'feature-tag-prop-table', query: { props: 'feature-tag' } }}>
TagProps
</NextLink>{' '}
for usage.
</>
),
required: true,
},
];

export const FeatureSetPropTable: React.FC<PropTableWrapper> = (props) => (
<PropTable propList={featureSetProps} title="Feature Set" {...props} />
);

const featureTagProps: Prop[] = [
{
name: 'icon',
types: <NextLink href="/icons">Icon</NextLink>,
description: (
<>
Pass in an <NextLink href="/icons">Icon</NextLink> component to display at the start of the
tag.
</>
),
},
{
name: 'label',
types: 'string',
description: (
<>
Defines the <Code primary>FeatureTag</Code> text.
</>
),
required: true,
},
];

export const FeatureTagPropTable: React.FC<PropTableWrapper> = (props) => (
<PropTable propList={featureTagProps} title="FeatureTag" {...props} />
);
1 change: 1 addition & 0 deletions packages/docs/components/SideNav/SideNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const SideNav: React.FC = () => {
<SideNavGroup title="Status &amp; Feedback">
<SideNavLink href="/alert">Alert</SideNavLink>
<SideNavLink href="/badge">Badge</SideNavLink>
<SideNavLink href="/feature-set">FeatureSet</SideNavLink>
<SideNavLink href="/inline-message">InlineMessage</SideNavLink>
<SideNavLink href="/message">Message</SideNavLink>
<SideNavLink href="/progress-bar">ProgressBar</SideNavLink>
Expand Down
Loading