-
Notifications
You must be signed in to change notification settings - Fork 64
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
base: main
Are you sure you want to change the base?
Conversation
|
6377a1d
to
9387526
Compare
9387526
to
627303a
Compare
e2f82a8
to
4cd6443
Compare
import { IconProps } from '@bigcommerce/big-design-icons'; | ||
import React, { AnchorHTMLAttributes, FunctionComponent, ReactElement } from 'react'; | ||
|
||
import { StyledFeatureTag, StyledFeatureTagIcon, StyledFeatureTagLabel } from './styled'; | ||
|
||
export interface FeatureTagProps extends AnchorHTMLAttributes<HTMLAnchorElement> { | ||
icon?: ReactElement<IconProps>; | ||
isActive?: boolean; | ||
label: string; | ||
} | ||
|
||
export const FeatureTag: FunctionComponent<FeatureTagProps> = ({ | ||
label, | ||
icon, | ||
isActive, | ||
href, | ||
target, | ||
}) => { | ||
return label ? ( | ||
<StyledFeatureTag | ||
className={isActive ? 'active' : ''} | ||
href={href} | ||
target={target} | ||
title={label} | ||
> | ||
{icon ? <StyledFeatureTagIcon>{icon}</StyledFeatureTagIcon> : null} | ||
<StyledFeatureTagLabel>{label}</StyledFeatureTagLabel> | ||
</StyledFeatureTag> | ||
) : null; | ||
}; | ||
|
||
FeatureTag.displayName = 'FeatureTag'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ton's of things here:
- For icon props, we just use
ReactNode
as the type. Allows consumers to pass in high-order components/icons if needed. - I know this might be a contention point, but let's remove the
isActive
prop. It feels like it's a link state, rather than prop state here. - Use
ComponentPropsWithoutRef
instead ofHTML<Element>Attributes
to extend from. - Avoid using
FunctionComponent
, instead use({ some, prop, ...props }: Props) => {...}
as React now implies it's a functional component. - You'll see in
styled.tsx
some simplification there, which simplifies the HTML and styles in the component. - Spread the rest of the props as we are extending a link component.
import { IconProps } from '@bigcommerce/big-design-icons'; | |
import React, { AnchorHTMLAttributes, FunctionComponent, ReactElement } from 'react'; | |
import { StyledFeatureTag, StyledFeatureTagIcon, StyledFeatureTagLabel } from './styled'; | |
export interface FeatureTagProps extends AnchorHTMLAttributes<HTMLAnchorElement> { | |
icon?: ReactElement<IconProps>; | |
isActive?: boolean; | |
label: string; | |
} | |
export const FeatureTag: FunctionComponent<FeatureTagProps> = ({ | |
label, | |
icon, | |
isActive, | |
href, | |
target, | |
}) => { | |
return label ? ( | |
<StyledFeatureTag | |
className={isActive ? 'active' : ''} | |
href={href} | |
target={target} | |
title={label} | |
> | |
{icon ? <StyledFeatureTagIcon>{icon}</StyledFeatureTagIcon> : null} | |
<StyledFeatureTagLabel>{label}</StyledFeatureTagLabel> | |
</StyledFeatureTag> | |
) : null; | |
}; | |
FeatureTag.displayName = 'FeatureTag'; | |
import React, { ComponentPropsWithoutRef, forwardRef, ReactNode } from 'react'; | |
import { StyledFeatureTag, StyledFeatureTagLabel } from './styled'; | |
export interface FeatureTagProps extends ComponentPropsWithoutRef<'a'> { | |
icon?: ReactNode; | |
label: string; | |
} | |
export const FeatureTag = forwardRef<HTMLAnchorElement, FeatureTagProps>( | |
({ label, icon, ...props }, ref) => { | |
return label ? ( | |
<StyledFeatureTag ref={ref} {...props}> | |
{icon} | |
<StyledFeatureTagLabel>{label}</StyledFeatureTagLabel> | |
</StyledFeatureTag> | |
) : null; | |
}, | |
); | |
FeatureTag.displayName = 'FeatureTag'; |
import { theme as defaultTheme } from '@bigcommerce/big-design-theme'; | ||
import styled from 'styled-components'; | ||
|
||
export const StyledFeatureTag = styled.a` | ||
align-items: center; | ||
background-color: ${({ theme }) => theme.colors.secondary20}; | ||
border-radius: ${({ theme }) => theme.borderRadius.normal}; | ||
display: inline-flex; | ||
color: ${({ theme }) => theme.colors.secondary60}; | ||
cursor: pointer; | ||
fill: ${({ theme }) => theme.colors.secondary60}; | ||
flex-wrap: nowrap; | ||
font-size: ${({ theme }) => theme.helpers.remCalc(14)}; | ||
gap: ${({ theme }) => theme.spacing.xxSmall}; | ||
justify-content: center; | ||
line-height: ${({ theme }) => theme.helpers.remCalc(20)}; | ||
max-width: fit-content; | ||
outline: none; | ||
padding-block: calc(${({ theme }) => theme.spacing.xSmall} / 4); | ||
padding-inline: ${({ theme }) => theme.spacing.xSmall}; | ||
text-decoration: none; | ||
user-select: none; | ||
|
||
&.active { | ||
background-color: ${({ theme }) => theme.colors.primary10}; | ||
color: ${({ theme }) => theme.colors.primary50}; | ||
fill: ${({ theme }) => theme.colors.primary}; | ||
} | ||
|
||
&:hover, | ||
&.active:hover { | ||
background-color: ${({ theme }) => theme.colors.primary20}; | ||
color: ${({ theme }) => theme.colors.primary60}; | ||
fill: ${({ theme }) => theme.colors.primary40}; | ||
} | ||
|
||
&:focus { | ||
outline: 4px solid ${({ theme }) => theme.colors.primary20}; | ||
} | ||
|
||
&:active, | ||
&.active:active { | ||
background-color: ${({ theme }) => theme.colors.primary30}; | ||
color: ${({ theme }) => theme.colors.primary60}; | ||
fill: ${({ theme }) => theme.colors.primary70}; | ||
} | ||
|
||
&:focused:active { | ||
background-color: ${({ theme }) => theme.colors.primary10}; | ||
color: ${({ theme }) => theme.colors.primary60}; | ||
fill: ${({ theme }) => theme.colors.primary60}; | ||
outline: 4px solid ${({ theme }) => theme.colors.primary30}; | ||
} | ||
`; | ||
|
||
StyledFeatureTag.defaultProps = { theme: defaultTheme }; | ||
|
||
export const StyledFeatureTagIcon = styled.div` | ||
align-items: center; | ||
display: flex; | ||
flex: 0 0 auto; | ||
height: ${({ theme }) => theme.helpers.remCalc(18)}; | ||
justify-content: center; | ||
overflow: hidden; | ||
width: ${({ theme }) => theme.helpers.remCalc(18)}; | ||
|
||
& > svg { | ||
transform: scale(0.75); | ||
transform-origin: center; | ||
} | ||
`; | ||
|
||
StyledFeatureTagIcon.defaultProps = { theme: defaultTheme }; | ||
|
||
export const StyledFeatureTagLabel = styled.span` | ||
overflow: hidden; | ||
text-overflow: ellipsis; | ||
white-space: nowrap; | ||
`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- We don't need
fill
set as our icons usecolor: currentColor
for the stroke/fill it needs. - Switch over to the
theme.typography.fontSize
+theme.lineHeight
settings, instead of usingremCalc
(UseremCalc
when we don't have the value on the theme). - Use the
& > svg
to size the SVG instead of needing another HTML element. - Use
ellipsis
frompolished
. - Cleaned up link state styles.
import { theme as defaultTheme } from '@bigcommerce/big-design-theme'; | |
import styled from 'styled-components'; | |
export const StyledFeatureTag = styled.a` | |
align-items: center; | |
background-color: ${({ theme }) => theme.colors.secondary20}; | |
border-radius: ${({ theme }) => theme.borderRadius.normal}; | |
display: inline-flex; | |
color: ${({ theme }) => theme.colors.secondary60}; | |
cursor: pointer; | |
fill: ${({ theme }) => theme.colors.secondary60}; | |
flex-wrap: nowrap; | |
font-size: ${({ theme }) => theme.helpers.remCalc(14)}; | |
gap: ${({ theme }) => theme.spacing.xxSmall}; | |
justify-content: center; | |
line-height: ${({ theme }) => theme.helpers.remCalc(20)}; | |
max-width: fit-content; | |
outline: none; | |
padding-block: calc(${({ theme }) => theme.spacing.xSmall} / 4); | |
padding-inline: ${({ theme }) => theme.spacing.xSmall}; | |
text-decoration: none; | |
user-select: none; | |
&.active { | |
background-color: ${({ theme }) => theme.colors.primary10}; | |
color: ${({ theme }) => theme.colors.primary50}; | |
fill: ${({ theme }) => theme.colors.primary}; | |
} | |
&:hover, | |
&.active:hover { | |
background-color: ${({ theme }) => theme.colors.primary20}; | |
color: ${({ theme }) => theme.colors.primary60}; | |
fill: ${({ theme }) => theme.colors.primary40}; | |
} | |
&:focus { | |
outline: 4px solid ${({ theme }) => theme.colors.primary20}; | |
} | |
&:active, | |
&.active:active { | |
background-color: ${({ theme }) => theme.colors.primary30}; | |
color: ${({ theme }) => theme.colors.primary60}; | |
fill: ${({ theme }) => theme.colors.primary70}; | |
} | |
&:focused:active { | |
background-color: ${({ theme }) => theme.colors.primary10}; | |
color: ${({ theme }) => theme.colors.primary60}; | |
fill: ${({ theme }) => theme.colors.primary60}; | |
outline: 4px solid ${({ theme }) => theme.colors.primary30}; | |
} | |
`; | |
StyledFeatureTag.defaultProps = { theme: defaultTheme }; | |
export const StyledFeatureTagIcon = styled.div` | |
align-items: center; | |
display: flex; | |
flex: 0 0 auto; | |
height: ${({ theme }) => theme.helpers.remCalc(18)}; | |
justify-content: center; | |
overflow: hidden; | |
width: ${({ theme }) => theme.helpers.remCalc(18)}; | |
& > svg { | |
transform: scale(0.75); | |
transform-origin: center; | |
} | |
`; | |
StyledFeatureTagIcon.defaultProps = { theme: defaultTheme }; | |
export const StyledFeatureTagLabel = styled.span` | |
overflow: hidden; | |
text-overflow: ellipsis; | |
white-space: nowrap; | |
`; | |
import { theme as defaultTheme } from '@bigcommerce/big-design-theme'; | |
import { ellipsis } from 'polished'; | |
import styled from 'styled-components'; | |
export const StyledFeatureTag = styled.a` | |
align-items: center; | |
background-color: ${({ theme }) => theme.colors.secondary20}; | |
border-radius: ${({ theme }) => theme.borderRadius.normal}; | |
display: inline-flex; | |
color: ${({ theme }) => theme.colors.secondary60}; | |
cursor: pointer; | |
flex-wrap: nowrap; | |
font-size: ${({ theme }) => theme.typography.fontSize.small}; | |
gap: ${({ theme }) => theme.spacing.xxSmall}; | |
justify-content: center; | |
line-height: ${({ theme }) => theme.lineHeight.small}; | |
max-width: fit-content; | |
outline: none; | |
padding-block: ${({ theme }) => theme.helpers.remCalc(2)}; | |
padding-inline: ${({ theme }) => theme.spacing.xSmall}; | |
text-decoration: none; | |
user-select: none; | |
&:hover { | |
background-color: ${({ theme }) => theme.colors.primary20}; | |
color: ${({ theme }) => theme.colors.primary60}; | |
} | |
&:focus-visible { | |
outline: 4px solid ${({ theme }) => theme.colors.primary20}; | |
} | |
&:active { | |
background-color: ${({ theme }) => theme.colors.primary10}; | |
color: ${({ theme }) => theme.colors.primary50}; | |
} | |
&:focus-visible:active { | |
background-color: ${({ theme }) => theme.colors.primary10}; | |
color: ${({ theme }) => theme.colors.primary60}; | |
} | |
& > svg { | |
height: ${({ theme }) => theme.spacing.medium}; | |
flex-shrink: 0; | |
width: ${({ theme }) => theme.spacing.medium}; | |
} | |
`; | |
StyledFeatureTag.defaultProps = { theme: defaultTheme }; | |
export const StyledFeatureTagLabel = styled.span` | |
${ellipsis()} | |
`; |
Before you convert this from draft to regular PR make sure to add documentation. |
What?
Introducing the FeatureTag component
Why?
The feature tag component is a stylized link/badge interface element that describes categories of a particular application or feature within the BC properties.
Screenshots/Screen Recordings
Figma
Prototype
Testing/Proof
dev.tsx
code