From e0eecdd05b84370cb18e4cf8c1985f49c4f1f8e1 Mon Sep 17 00:00:00 2001 From: hackbnw <420178+hackbnw@users.noreply.github.com> Date: Fri, 1 Sep 2023 10:15:31 +0200 Subject: [PATCH] feat: add error Tag and SelectList variants (#372) * fix: add Open Sans font to storybook * feat: add variants of tag component in Tag and SelectList (#354) * feat: add variants of tag component, resolve comments --------- Co-authored-by: Yurii Pavlovskyi --- .storybook/preview-head.html | 7 ++ src/components/SelectList/SelectList.spec.tsx | 62 +++++++++++ src/components/SelectList/SelectList.tsx | 98 +++++++++++++---- .../SelectList/docs/SelectList.stories.tsx | 33 ++++++ src/components/Tag/Tag.spec.tsx | 32 +++++- src/components/Tag/Tag.tsx | 100 ++++++++++++++---- .../Tag/__snapshots__/Tag.spec.tsx.snap | 15 ++- 7 files changed, 307 insertions(+), 40 deletions(-) create mode 100644 src/components/SelectList/SelectList.spec.tsx diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 11d42c2bd..f5f0a55a6 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -3,5 +3,12 @@ + + + diff --git a/src/components/SelectList/SelectList.spec.tsx b/src/components/SelectList/SelectList.spec.tsx new file mode 100644 index 000000000..01c5247b1 --- /dev/null +++ b/src/components/SelectList/SelectList.spec.tsx @@ -0,0 +1,62 @@ +import { render, screen } from '@testing-library/react'; +import * as React from 'react'; +import { SelectList } from './SelectList'; +import { SemanticColors } from '../../essentials'; + +describe('SelectList', () => { + it('renders options in multi select', () => { + const options = [ + { + label: 'Sales', + value: 'sales' + }, + { + label: 'Marketing', + value: 'marketing', + error: true + } + ]; + + render(); + + const normalTag = screen.getByText('Sales').parentElement; + expect(normalTag).toHaveStyle(` + background-color: ${SemanticColors.background.info}; + border-color: ${SemanticColors.border.infoEmphasized}; + `); + + const errorTag = screen.getByText('Marketing').parentElement; + expect(errorTag).toHaveStyle(` + background-color: transparent; + border-color: ${SemanticColors.border.dangerEmphasized}; + `); + }); + + it('disables options in multi select when control is disabled', () => { + const options = [ + { + label: 'Sales', + value: 'sales' + }, + { + label: 'Marketing', + value: 'marketing', + error: true + } + ]; + + render(); + + const normalTag = screen.getByText('Sales').parentElement; + expect(normalTag).toHaveStyle(` + background-color: transparent; + border-color: ${SemanticColors.border.primary}; + `); + + const errorTag = screen.getByText('Marketing').parentElement; + expect(errorTag).toHaveStyle(` + background-color: transparent; + border-color: ${SemanticColors.border.primary}; + `); + }); +}); diff --git a/src/components/SelectList/SelectList.tsx b/src/components/SelectList/SelectList.tsx index 1c47091b1..d78a7134f 100644 --- a/src/components/SelectList/SelectList.tsx +++ b/src/components/SelectList/SelectList.tsx @@ -21,6 +21,23 @@ import { SelectListProps } from './types'; type WithSelectProps = T & { selectProps: SelectListProps }; +const getOptionError = (option: unknown): boolean => + typeof option === 'object' && 'error' in option && Boolean(option.error); + +const getOptionVariant = (selectProps: Props, option: unknown): 'default' | 'disabled' | 'error' => { + if (selectProps.isDisabled) { + return 'disabled'; + } + + if (getOptionError(option)) { + return 'error'; + } + + return 'default'; +}; + +const getColor = (key: string, props: Props) => String(get(key)(props)); + const customStyles: StylesConfig = { container: (provided, { selectProps }: WithSelectProps) => { const bSize = { @@ -169,35 +186,79 @@ const customStyles: StylesConfig = { cursor: state.isDisabled ? 'not-allowed' : 'default' }; }, - multiValue: (provided, { selectProps }: { selectProps: Props }) => { + multiValue: (provided, { selectProps, data }) => { + const optionVariant = getOptionVariant(selectProps, data); + const styles = { ...provided, - color: Colors.ACTION_BLUE_900, - border: `0.0625rem solid ${Colors.ACTION_BLUE_900}`, + border: '0.0625rem solid', borderRadius: '1rem', - backgroundColor: Colors.ACTION_BLUE_50, marginRight: '0.375rem', marginTop: '0.125rem', marginLeft: 0, marginBottom: '0.125rem', maxWidth: 'calc(100% - 0.5rem)', - transition: 'color 125ms ease, background-color 125ms ease', - '&:hover': { - backgroundColor: Colors.ACTION_BLUE_900, - color: Colors.WHITE - } + transition: 'color 125ms ease, background-color 125ms ease' }; - if (selectProps.isDisabled) { - return { - ...styles, - color: Colors.AUTHENTIC_BLUE_200, - backgroundColor: 'transparent', - borderColor: Colors.AUTHENTIC_BLUE_200 - }; - } + switch (optionVariant) { + case 'disabled': + return { + ...styles, + + color: getColor('semanticColors.text.disabled', selectProps), + backgroundColor: 'transparent', + borderColor: getColor('semanticColors.border.primary', selectProps), + + '> [role="button"]': { + color: getColor('semanticColors.icon.disabled', selectProps) + } + }; + case 'error': + return { + ...styles, + color: getColor('semanticColors.text.dangerInverted', selectProps), + backgroundColor: 'transparent', + borderColor: getColor('semanticColors.border.dangerEmphasized', selectProps), + + '> [role="button"]': { + color: getColor('semanticColors.icon.danger', selectProps) + }, + + '&:hover': { + color: getColor('semanticColors.text.primaryInverted', selectProps), + backgroundColor: getColor('semanticColors.background.dangerEmphasized', selectProps), + borderColor: getColor('semanticColors.border.dangerEmphasized', selectProps), - return styles; + '> [role="button"]': { + color: getColor('semanticColors.icon.primaryInverted', selectProps) + } + } + }; + case 'default': + default: + return { + ...styles, + + color: getColor('semanticColors.text.link', selectProps), + backgroundColor: getColor('semanticColors.background.info', selectProps), + borderColor: getColor('semanticColors.border.infoEmphasized', selectProps), + + '> [role="button"]': { + color: getColor('semanticColors.icon.action', selectProps) + }, + + '&:hover': { + color: getColor('semanticColors.text.primaryInverted', selectProps), + backgroundColor: getColor('semanticColors.background.infoEmphasized', selectProps), + borderColor: getColor('semanticColors.border.infoEmphasized', selectProps), + + '> [role="button"]': { + color: getColor('semanticColors.icon.primaryInverted', selectProps) + } + } + }; + } }, multiValueLabel: (provided, { selectProps }) => ({ ...provided, @@ -214,6 +275,7 @@ const customStyles: StylesConfig = { paddingLeft: '0', paddingRight: '0.25rem', paddingTop: '0', + transition: 'color 125ms ease', '&:hover': { color: 'inherit', background: 'transparent' diff --git a/src/components/SelectList/docs/SelectList.stories.tsx b/src/components/SelectList/docs/SelectList.stories.tsx index 29c36a2f2..5b0a9eac7 100644 --- a/src/components/SelectList/docs/SelectList.stories.tsx +++ b/src/components/SelectList/docs/SelectList.stories.tsx @@ -75,6 +75,39 @@ export const WithError: Story = { } }; +const errorOptions = [ + { + label: 'Sales', + value: 'sales' + }, + { + label: 'Marketing', + value: 'marketing', + error: true + } +]; + +export const MultiSelectError: Story = { + args: { + label: 'Multi select with error', + error: true, + isMulti: true, + options: errorOptions, + value: errorOptions + } +}; + +export const MultiSelectDisabled: Story = { + args: { + label: 'Disabled multi select', + error: true, + isMulti: true, + isDisabled: true, + options: errorOptions, + value: errorOptions + } +}; + export const Inverted: Story = { args: { inverted: true diff --git a/src/components/Tag/Tag.spec.tsx b/src/components/Tag/Tag.spec.tsx index f0c0c4bee..ebcba9783 100644 --- a/src/components/Tag/Tag.spec.tsx +++ b/src/components/Tag/Tag.spec.tsx @@ -1,6 +1,7 @@ -import { render, fireEvent } from '@testing-library/react'; +import { render, fireEvent, screen } from '@testing-library/react'; import * as React from 'react'; import { Tag } from './Tag'; +import { SemanticColors } from '../../essentials'; describe('Tag', () => { it('renders with default props', () => { @@ -26,4 +27,33 @@ describe('Tag', () => { expect(queryByTestId('dismiss-icon')).toBeNull(); }); + + it('renders disabled variant', () => { + const { container } = render(Lorem); + + expect(container.firstChild).toHaveStyle(` + border-color: ${SemanticColors.border.primary}; + `); + expect(screen.getByText('Lorem')).toHaveStyle(` + color: ${SemanticColors.text.disabled}; + `); + expect(screen.getByTestId('dismiss-icon')).toHaveStyle(` + color: ${SemanticColors.icon.disabled}; + `); + }); + + it('renders error variant', () => { + const { container } = render(Lorem); + + expect(container.firstChild).toHaveStyle(` + background-color: ${SemanticColors.background.danger}; + border-color: ${SemanticColors.border.dangerEmphasized}; + `); + expect(screen.getByText('Lorem')).toHaveStyle(` + color: ${SemanticColors.text.dangerInverted}; + `); + expect(screen.getByTestId('dismiss-icon')).toHaveStyle(` + color: ${SemanticColors.icon.danger}; + `); + }); }); diff --git a/src/components/Tag/Tag.tsx b/src/components/Tag/Tag.tsx index 1422ecf68..15a2444a0 100644 --- a/src/components/Tag/Tag.tsx +++ b/src/components/Tag/Tag.tsx @@ -1,6 +1,6 @@ import React, { FC, MouseEvent, PropsWithChildren } from 'react'; import styled from 'styled-components'; -import { margin, MarginProps } from 'styled-system'; +import { margin, MarginProps, variant } from 'styled-system'; import { Colors } from '../../essentials'; import { theme } from '../../essentials/theme'; @@ -17,10 +17,14 @@ interface TagProps extends MarginProps { * The prop to determine whether the dismiss functionality is enabled */ dismissible?: boolean; + /** + * Set the appropriate semantic tag color. + * @default default + */ + variant?: 'default' | 'disabled' | 'error'; } const TagText = styled(Text).attrs({ theme })>` - color: ${Colors.ACTION_BLUE_900}; margin-left: 0.75rem; margin-right: ${props => (props.dismissible ? '0.25rem' : '0.75rem')}; font-size: ${get('fontSizes.1')}; @@ -38,10 +42,75 @@ const DismissIcon = styled(CloseIcon).attrs({ size: 18 })` } `; +const tagVariant = variant({ + variants: { + default: { + backgroundColor: get('semanticColors.background.info'), + borderColor: get('semanticColors.border.infoEmphasized'), + + [`> ${TagText}`]: { + color: get('semanticColors.text.link') + }, + + [`> ${DismissIcon}`]: { + color: get('semanticColors.icon.action') + }, + + '&:hover': { + backgroundColor: get('semanticColors.background.infoEmphasized'), + borderColor: get('semanticColors.border.infoEmphasized'), + + [`> ${TagText}`]: { + color: get('semanticColors.text.primaryInverted') + }, + + [`> ${DismissIcon}`]: { + color: get('semanticColors.icon.primaryInverted') + } + } + }, + disabled: { + borderColor: get('semanticColors.border.primary'), + + [`> ${TagText}`]: { + color: get('semanticColors.text.disabled') + }, + + [`> ${DismissIcon}`]: { + color: get('semanticColors.icon.disabled') + } + }, + error: { + backgroundColor: get('semanticColors.background.danger'), + borderColor: get('semanticColors.border.dangerEmphasized'), + + [`> ${TagText}`]: { + color: get('semanticColors.text.dangerInverted') + }, + + [`> ${DismissIcon}`]: { + color: get('semanticColors.icon.danger') + }, + + '&:hover': { + backgroundColor: get('semanticColors.background.dangerEmphasized'), + borderColor: get('semanticColors.border.dangerEmphasized'), + + [`> ${TagText}`]: { + color: get('semanticColors.text.primaryInverted') + }, + + [`> ${DismissIcon}`]: { + color: get('semanticColors.icon.primaryInverted') + } + } + } + } +}); + const TagWrapper = styled.div.attrs({ theme })` box-sizing: border-box; - background-color: ${Colors.ACTION_BLUE_50}; - border: solid 0.0625rem ${Colors.ACTION_BLUE_900}; + border: solid 0.0625rem; display: inline-flex; align-items: center; border-radius: 2rem; @@ -52,22 +121,17 @@ const TagWrapper = styled.div.attrs({ theme })` transition: background-color 125ms ease; ${margin} - - &:hover { - background-color: ${Colors.ACTION_BLUE_900}; - - > ${TagText} { - color: ${Colors.WHITE}; - } - - > ${DismissIcon} { - color: ${Colors.WHITE}; - } - } + ${tagVariant} `; -const Tag: FC> = ({ children, onDismiss, dismissible = true, ...rest }) => ( - +const Tag: FC> = ({ + children, + onDismiss, + dismissible = true, + variant: variantValue = 'default', + ...rest +}) => ( + {children} {dismissible && } diff --git a/src/components/Tag/__snapshots__/Tag.spec.tsx.snap b/src/components/Tag/__snapshots__/Tag.spec.tsx.snap index b890de961..a6b868a71 100644 --- a/src/components/Tag/__snapshots__/Tag.spec.tsx.snap +++ b/src/components/Tag/__snapshots__/Tag.spec.tsx.snap @@ -10,7 +10,6 @@ exports[`Tag renders with default props 1`] = ` } .c3 { - color: #096BDB; margin-left: 0.75rem; margin-right: 0.25rem; font-size: 0.875rem; @@ -32,8 +31,7 @@ exports[`Tag renders with default props 1`] = ` .c0 { box-sizing: border-box; - background-color: #F1F7FD; - border: solid 0.0625rem #096BDB; + border: solid 0.0625rem; display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; @@ -49,10 +47,21 @@ exports[`Tag renders with default props 1`] = ` padding: 0.375rem 0; -webkit-transition: background-color 125ms ease; transition: background-color 125ms ease; + background-color: #F1F7FD; + border-color: #096BDB; +} + +.c0 > .c1 { + color: #096BDB; +} + +.c0 > .c4 { + color: #096BDB; } .c0:hover { background-color: #096BDB; + border-color: #096BDB; } .c0:hover > .c1 {