@automattic/vip-design-system 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +12 -0
  2. package/build/system/Button/Button.js +5 -0
  3. package/build/system/Button/Button.stories.js +8 -0
  4. package/build/system/Heading/Heading.js +7 -3
  5. package/build/system/Heading/Heading.stories.js +0 -3
  6. package/build/system/Link/Link.js +2 -1
  7. package/build/system/NewForm/FormSelect.test.js +79 -7
  8. package/build/system/Progress/Progress.js +45 -16
  9. package/build/system/Progress/Progress.stories.js +21 -4
  10. package/build/system/Table/Table.js +7 -3
  11. package/build/system/Wizard/Wizard.js +1 -3
  12. package/build/system/Wizard/Wizard.stories.js +18 -8
  13. package/build/system/Wizard/WizardStep.js +19 -12
  14. package/build/system/Wizard/WizardStepHorizontal.js +18 -8
  15. package/build/system/index.js +0 -5
  16. package/package.json +2 -1
  17. package/src/system/Button/Button.js +5 -0
  18. package/src/system/Button/Button.stories.jsx +3 -0
  19. package/src/system/Heading/Heading.js +18 -12
  20. package/src/system/Heading/Heading.stories.jsx +0 -1
  21. package/src/system/Link/Link.js +1 -0
  22. package/src/system/NewForm/FormSelect.test.js +38 -0
  23. package/src/system/Progress/Progress.js +55 -26
  24. package/src/system/Progress/Progress.stories.jsx +15 -8
  25. package/src/system/Table/Table.js +6 -2
  26. package/src/system/Wizard/Wizard.js +1 -2
  27. package/src/system/Wizard/Wizard.stories.jsx +6 -6
  28. package/src/system/Wizard/WizardStep.js +23 -25
  29. package/src/system/Wizard/WizardStepHorizontal.js +13 -7
  30. package/src/system/index.js +0 -3
  31. package/src/system/ResourceList/ResourceItem.js +0 -66
  32. package/src/system/ResourceList/ResourceList.js +0 -108
  33. package/src/system/ResourceList/ResourceList.stories.jsx +0 -306
  34. package/src/system/ResourceList/index.js +0 -7
@@ -3,24 +3,30 @@
3
3
  /**
4
4
  * External dependencies
5
5
  */
6
+ import React from 'react';
6
7
  import { Heading as ThemeHeading } from 'theme-ui';
7
8
  import PropTypes from 'prop-types';
8
9
  import classNames from 'classnames';
9
10
 
10
- const Heading = ( { variant = 'h3', sx, className = null, ...props } ) => (
11
- <ThemeHeading
12
- as={ variant }
13
- sx={ {
14
- color: 'heading',
15
- // pass variant prop to sx
16
- variant: `text.${ variant }`,
17
- ...sx,
18
- } }
19
- className={ classNames( 'vip-heading-component', className ) }
20
- { ...props }
21
- />
11
+ const Heading = React.forwardRef(
12
+ ( { variant = 'h3', sx, className = null, ...props }, forwardRef ) => (
13
+ <ThemeHeading
14
+ as={ variant }
15
+ sx={ {
16
+ color: 'heading',
17
+ // pass variant prop to sx
18
+ variant: `text.${ variant }`,
19
+ ...sx,
20
+ } }
21
+ className={ classNames( 'vip-heading-component', className ) }
22
+ ref={ forwardRef }
23
+ { ...props }
24
+ />
25
+ )
22
26
  );
23
27
 
28
+ Heading.displayName = 'Heading';
29
+
24
30
  Heading.propTypes = {
25
31
  variant: PropTypes.string,
26
32
  sx: PropTypes.object,
@@ -15,6 +15,5 @@ export const Default = () => (
15
15
  <Heading variant="h3">Heading Three</Heading>
16
16
  <Heading variant="h4">Heading Four</Heading>
17
17
  <Heading variant="h5">Heading Five</Heading>
18
- <Heading variant="caps">Heading Caps</Heading>
19
18
  </Box>
20
19
  );
@@ -21,6 +21,7 @@ const Link = ( { active = false, sx, ...props } ) => (
21
21
  },
22
22
  '&:hover, &:focus': {
23
23
  color: 'links.hover',
24
+ textDecoration: 'none',
24
25
  },
25
26
  '&:focus': theme => theme.outline,
26
27
  '&:focus-visible': theme => theme.outline,
@@ -21,10 +21,48 @@ const defaultProps = {
21
21
  options,
22
22
  };
23
23
 
24
+ const groupedProps = {
25
+ ...defaultProps,
26
+ options: [
27
+ {
28
+ label: 'Group name',
29
+ options: [ options[ 0 ] ],
30
+ },
31
+ {
32
+ label: 'Another Group name',
33
+ options: [ options[ 1 ], options[ 2 ] ],
34
+ },
35
+ ],
36
+ };
37
+
24
38
  describe( '<FormSelect />', () => {
25
39
  it( 'renders the FormSelect component', async () => {
26
40
  const { container } = render( <FormSelect id="my_desert_list" { ...defaultProps } /> );
27
41
 
42
+ expect( screen.getByLabelText( defaultProps.label ) ).toBeInTheDocument();
43
+ expect( screen.getByRole( 'combobox' ) ).toBeInTheDocument();
44
+ expect( screen.getAllByRole( 'option' ) ).toHaveLength( 3 );
45
+ expect( screen.queryByRole( 'group' ) ).not.toBeInTheDocument();
46
+
47
+ // Check for accessibility issues
48
+ await expect( await axe( container ) ).toHaveNoViolations();
49
+ } );
50
+
51
+ it( 'renders the FormSelect component with optgroup when options are grouped', async () => {
52
+ const { container } = render( <FormSelect id="my_desert_list" { ...groupedProps } /> );
53
+
54
+ expect( screen.getByLabelText( defaultProps.label ) ).toBeInTheDocument();
55
+ expect( screen.getByRole( 'combobox' ) ).toBeInTheDocument();
56
+ expect( screen.getAllByRole( 'option' ) ).toHaveLength( 3 );
57
+ expect( screen.getAllByRole( 'group' ) ).toHaveLength( 2 );
58
+
59
+ // Check for accessibility issues
60
+ await expect( await axe( container ) ).toHaveNoViolations();
61
+ } );
62
+
63
+ it( 'renders the FormSelect component when isInline is true', async () => {
64
+ const { container } = render( <FormSelect id="my_desert_list" isInline { ...defaultProps } /> );
65
+
28
66
  expect( screen.getByLabelText( defaultProps.label ) ).toBeInTheDocument();
29
67
  expect( screen.getByRole( 'combobox' ) ).toBeInTheDocument();
30
68
 
@@ -3,45 +3,74 @@
3
3
  /**
4
4
  * External dependencies
5
5
  */
6
+ import React from 'react';
6
7
  import { Progress as ThemeProgress } from 'theme-ui';
7
8
  import PropTypes from 'prop-types';
9
+ import classNames from 'classnames';
8
10
 
9
11
  /**
10
12
  * Internal dependencies
11
13
  */
12
14
  import { Spinner } from '../Spinner';
13
- import { Box, Heading, Text, Flex } from '../';
14
- import classNames from 'classnames';
15
+ import { MdCheck } from 'react-icons/md';
16
+ import { Box, Text, Flex } from '../';
17
+
18
+ const prefix = 'vip-progress-component';
19
+ const uniqueID = () => Math.random().toString( 36 ).substring( 7 );
20
+
21
+ const Progress = React.forwardRef(
22
+ ( { steps, activeStep, sx, forLabel = '', className, ...props }, forwardRef ) => {
23
+ const stepsTotal = steps.length;
24
+ const isDone = activeStep === stepsTotal - 1;
25
+ const instance = uniqueID();
26
+ const htmlFor = `${ prefix }-${ instance }`;
27
+ const currentValue = activeStep + 1;
15
28
 
16
- const Progress = ( { steps, activeStep, sx, className, ...props } ) => (
17
- <Box>
18
- <ThemeProgress
19
- className={ classNames( 'vip-progress-component', className ) }
20
- { ...props }
21
- sx={ {
22
- color: 'primary',
23
- ...sx,
24
- } }
25
- max={ steps.length }
26
- value={ activeStep + 1 }
27
- />
28
- { steps && (
29
- <Flex sx={ { alignItems: 'center', mt: 2 } }>
30
- <Spinner size={ 24 } />
31
- <Heading variant="h4" sx={ { ml: 2, mb: 0 } }>
32
- { `${ activeStep + 1 } of ${ steps.length }` }:{ ' ' }
33
- <Text as="span" sx={ { color: 'muted' } }>
34
- { steps[ activeStep ] }
35
- </Text>
36
- </Heading>
37
- </Flex>
38
- ) }
39
- </Box>
29
+ return (
30
+ <Box className={ classNames( prefix, className ) }>
31
+ <ThemeProgress
32
+ sx={ {
33
+ color: 'primary',
34
+ backgroundColor: 'background',
35
+ ...sx,
36
+ } }
37
+ max={ stepsTotal }
38
+ value={ currentValue }
39
+ id={ htmlFor }
40
+ aria-label={ forLabel }
41
+ ref={ forwardRef }
42
+ { ...props }
43
+ />
44
+
45
+ { steps && (
46
+ <Flex
47
+ sx={ { alignItems: 'center', mt: 2 } }
48
+ aria-live="polite"
49
+ aria-atomic="true"
50
+ aria-describedby={ htmlFor }
51
+ >
52
+ { ! isDone && <Spinner size={ 24 } aria-hidden="true" /> }
53
+ { isDone && <MdCheck size={ 24 } aria-hidden="true" /> }
54
+
55
+ <Text sx={ { ml: 2, mb: 0 } }>
56
+ <strong>{ `${ currentValue } of ${ stepsTotal }` }: </strong>
57
+ <Text as="span" sx={ { color: 'muted' } }>
58
+ { steps[ activeStep ] }
59
+ </Text>
60
+ </Text>
61
+ </Flex>
62
+ ) }
63
+ </Box>
64
+ );
65
+ }
40
66
  );
41
67
 
68
+ Progress.displayName = 'Progress';
69
+
42
70
  Progress.propTypes = {
43
71
  steps: PropTypes.array,
44
72
  activeStep: PropTypes.number,
73
+ forLabel: PropTypes.node,
45
74
  sx: PropTypes.object,
46
75
  className: PropTypes.any,
47
76
  };
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Internal dependencies
3
3
  */
4
+ import React, { useEffect } from 'react';
4
5
  import { Progress } from '..';
5
6
 
6
7
  export default {
@@ -8,11 +9,17 @@ export default {
8
9
  component: Progress,
9
10
  };
10
11
 
11
- export const Default = () => (
12
- <Progress
13
- max={ 1 }
14
- value={ 1 / 2 }
15
- steps={ [ 'Downloading Data', 'Importing Data...', 'Finalizing' ] }
16
- activeStep={ 1 }
17
- />
18
- );
12
+ export const Default = () => {
13
+ const [ counter, setCounter ] = React.useState( 0 );
14
+ const steps = [ 'Downloading Data', 'Importing Data...', 'Finalizing', 'Done' ];
15
+
16
+ useEffect( () => {
17
+ setTimeout( () => {
18
+ if ( counter < steps.length - 1 ) {
19
+ setCounter( counter + 1 );
20
+ }
21
+ }, 2000 );
22
+ }, [ counter, setCounter ] );
23
+
24
+ return <Progress forLabel="Update site progress" steps={ steps } activeStep={ counter } />;
25
+ };
@@ -3,18 +3,22 @@
3
3
  /**
4
4
  * External dependencies
5
5
  */
6
+ import React from 'react';
6
7
  import PropTypes from 'prop-types';
7
8
  import classNames from 'classnames';
8
9
 
9
- const Table = ( { sx, className, ...props } ) => (
10
+ const Table = React.forwardRef( ( { sx, className, ...props }, forwardRef ) => (
10
11
  <table
11
12
  sx={ { width: '100%', minWidth: 1024, ...sx } }
12
13
  cellPadding={ 0 }
13
14
  cellSpacing={ 0 }
14
15
  className={ classNames( 'vip-table-component', className ) }
16
+ ref={ forwardRef }
15
17
  { ...props }
16
18
  />
17
- );
19
+ ) );
20
+
21
+ Table.displayName = 'Table';
18
22
 
19
23
  Table.propTypes = {
20
24
  sx: PropTypes.object,
@@ -42,7 +42,7 @@ const Wizard = ( { steps, activeStep, variant, completed = [], className = null,
42
42
  { steps[ activeStep ].children }
43
43
  </Box>
44
44
  ) : (
45
- steps.map( ( { title, subTitle, children, titleVariant }, index ) => (
45
+ steps.map( ( { title, subTitle, children }, index ) => (
46
46
  <WizardStep
47
47
  active={ index === activeStep }
48
48
  complete={ completed.includes( index ) }
@@ -50,7 +50,6 @@ const Wizard = ( { steps, activeStep, variant, completed = [], className = null,
50
50
  order={ index + 1 }
51
51
  subTitle={ subTitle }
52
52
  title={ title }
53
- titleVariant={ titleVariant }
54
53
  >
55
54
  { children }
56
55
  </WizardStep>
@@ -1,3 +1,5 @@
1
+ /** @jsxImportSource theme-ui */
2
+
1
3
  /**
2
4
  * External dependencies
3
5
  */
@@ -52,16 +54,14 @@ export const Default = () => {
52
54
  );
53
55
  };
54
56
 
55
- export const CustomHeadingVariant = () => {
57
+ export const CustomTitle = () => {
56
58
  const steps = [
57
59
  {
58
- title: 'Choose Domain',
59
- titleVariant: 'h1',
60
- subTitle: <h2>You can bring a domain name you already own, or buy a new one.</h2>,
60
+ title: <h3 sx={ { m: 0 } }>Choose Domain</h3>,
61
+ subTitle: <span>You can bring a domain name you already own, or buy a new one.</span>,
61
62
  },
62
63
  {
63
- title: 'Configure DNS',
64
- titleVariant: 'h1',
64
+ title: <h3 sx={ { m: 0 } }>Configure DNS</h3>,
65
65
  },
66
66
  ];
67
67
  return (
@@ -9,18 +9,10 @@ import PropTypes from 'prop-types';
9
9
  /**
10
10
  * Internal dependencies
11
11
  */
12
- import { Card, Heading, Text } from '..';
12
+ import { Card, Heading, Text, Flex } from '..';
13
13
 
14
- const WizardStep = ( {
15
- title,
16
- subTitle,
17
- complete = false,
18
- children,
19
- active,
20
- order,
21
- titleVariant = 'h4',
22
- } ) => {
23
- let borderLeftColor = 'borders.2';
14
+ const WizardStep = ( { title, subTitle, complete = false, children, active, order } ) => {
15
+ let borderLeftColor = 'border';
24
16
 
25
17
  if ( complete ) {
26
18
  borderLeftColor = 'success';
@@ -56,19 +48,26 @@ const WizardStep = ( {
56
48
  data-step={ order }
57
49
  data-active={ active || undefined }
58
50
  >
59
- <Heading
60
- variant={ titleVariant }
61
- sx={ {
62
- mb: 0,
63
- display: 'flex',
64
- alignItems: 'center',
65
- color,
66
- fontSize: 2,
67
- } }
68
- >
69
- <MdCheckCircle sx={ { mr: 2 } } size={ 18 } />
70
- { title }
71
- </Heading>
51
+ { typeof title === 'string' ? (
52
+ <Heading
53
+ variant="h4"
54
+ sx={ {
55
+ mb: 0,
56
+ display: 'flex',
57
+ alignItems: 'center',
58
+ color: color,
59
+ } }
60
+ >
61
+ <MdCheckCircle aria-hidden="true" sx={ { mr: 2 } } />
62
+ { title }
63
+ </Heading>
64
+ ) : (
65
+ <Flex sx={ { alignItems: 'center', color } }>
66
+ <MdCheckCircle aria-hidden="true" sx={ { mr: 2 } } />
67
+ { title }
68
+ </Flex>
69
+ ) }
70
+
72
71
  { subTitle && active && <Text sx={ { mb: 3 } }>{ subTitle }</Text> }
73
72
 
74
73
  { active && children }
@@ -83,7 +82,6 @@ WizardStep.propTypes = {
83
82
  order: PropTypes.number.isRequired,
84
83
  subTitle: PropTypes.node,
85
84
  title: PropTypes.node,
86
- titleVariant: PropTypes.string,
87
85
  };
88
86
 
89
87
  export { WizardStep };
@@ -9,24 +9,31 @@ import PropTypes from 'prop-types';
9
9
  /**
10
10
  * Internal dependencies
11
11
  */
12
- import { Heading } from '..';
12
+ import { Heading, Flex } from '..';
13
13
 
14
- const WizardStepHorizontal = ( { title, active, order, titleVariant = 'h4' } ) => {
15
- return (
14
+ const WizardStepHorizontal = ( { title, active, order } ) => {
15
+ const color = active ? 'heading' : 'muted';
16
+
17
+ return typeof title === 'string' ? (
16
18
  <Heading
17
- variant={ titleVariant }
19
+ variant="h4"
18
20
  sx={ {
19
21
  mb: 0,
20
22
  display: 'flex',
21
23
  alignItems: 'center',
22
- color: active ? 'heading' : 'muted',
24
+ color,
23
25
  } }
24
26
  data-step={ order }
25
27
  data-active={ active || undefined }
26
28
  >
27
- <MdCheckCircle sx={ { mr: 2 } } />
29
+ <MdCheckCircle aria-hidden="true" sx={ { mr: 2 } } />
28
30
  { title }
29
31
  </Heading>
32
+ ) : (
33
+ <Flex sx={ { alignItems: 'center', color } }>
34
+ <MdCheckCircle aria-hidden="true" sx={ { mr: 2 } } />
35
+ { title }
36
+ </Flex>
30
37
  );
31
38
  };
32
39
 
@@ -35,7 +42,6 @@ WizardStepHorizontal.propTypes = {
35
42
  order: PropTypes.number.isRequired,
36
43
  subTitle: PropTypes.node,
37
44
  title: PropTypes.node,
38
- titleVariant: PropTypes.string,
39
45
  };
40
46
 
41
47
  export { WizardStepHorizontal };
@@ -44,7 +44,6 @@ import { Link } from './Link';
44
44
  import { Notice } from './Notice';
45
45
  import { Progress } from './Progress';
46
46
  import { Spinner } from './Spinner';
47
- import { ResourceList, ResourceItem } from './ResourceList';
48
47
  import { Tooltip } from './Tooltip';
49
48
  import { Time } from './Time';
50
49
  import { Timeline } from './Timeline';
@@ -94,8 +93,6 @@ export {
94
93
  Select,
95
94
  Radio,
96
95
  RadioBoxGroup,
97
- ResourceList,
98
- ResourceItem,
99
96
  Textarea,
100
97
  Progress,
101
98
  Text,
@@ -1,66 +0,0 @@
1
- /** @jsxImportSource theme-ui */
2
-
3
- /**
4
- * External dependencies
5
- */
6
- import PropTypes from 'prop-types';
7
-
8
- /**
9
- * Internal dependencies
10
- */
11
- import { Box, Flex, Time } from '..';
12
-
13
- const ResourceItem = ( {
14
- children,
15
- item,
16
- renderActions,
17
- relativeTime = false,
18
- timeOnly = false,
19
- dateKey,
20
- icon = null,
21
- } ) => {
22
- return (
23
- <Flex
24
- sx={ {
25
- alignItems: 'center',
26
- gap: 3,
27
- } }
28
- >
29
- { icon }
30
- <Box sx={ { flex: '1 1 auto' } }>{ children }</Box>
31
- <Flex
32
- sx={ {
33
- flex: '0 0 auto',
34
- alignItems: 'center',
35
- gap: 3,
36
- } }
37
- >
38
- <Time
39
- className="time"
40
- relativeTime={ relativeTime }
41
- timeOnly={ timeOnly }
42
- time={ item[ dateKey ] }
43
- sx={ { color: 'muted', mb: 0, textAlign: 'right', flex: '0 0 auto' } }
44
- />
45
- { renderActions && (
46
- <Flex className="actions" sx={ { alignItems: 'center', gap: 3 } }>
47
- <Box sx={ { width: 4, height: 4, borderRadius: 4, bg: 'border' } } />
48
- { renderActions( item ) }
49
- </Flex>
50
- ) }
51
- </Flex>
52
- </Flex>
53
- );
54
- };
55
-
56
- ResourceItem.propTypes = {
57
- children: PropTypes.node,
58
- item: PropTypes.object,
59
- icon: PropTypes.node,
60
- relativeTime: PropTypes.bool,
61
- timeOnly: PropTypes.bool,
62
- dateKey: PropTypes.string,
63
- renderActions: PropTypes.func,
64
- };
65
-
66
- export { ResourceItem };
@@ -1,108 +0,0 @@
1
- /** @jsxImportSource theme-ui */
2
-
3
- /**
4
- * External dependencies
5
- */
6
- import PropTypes from 'prop-types';
7
- import { useMemo } from 'react';
8
-
9
- /**
10
- * Internal dependencies
11
- */
12
- import { Box, Heading } from '..';
13
-
14
- const formatterOptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
15
-
16
- const formatDate = date => {
17
- const today = new Date();
18
- if ( date.getFullYear() !== today.getFullYear() ) {
19
- return date.toLocaleDateString( formatterOptions );
20
- } else if ( date.getMonth() !== today.getMonth() ) {
21
- return date.toLocaleDateString( formatterOptions );
22
- } else if ( date.getDate() < today.getDate() - 1 ) {
23
- return date.toLocaleDateString( formatterOptions );
24
- } else if ( date.getDate() === today.getDate() - 1 ) {
25
- return 'Yesterday';
26
- }
27
- return 'Today';
28
- };
29
-
30
- const StyledListItem = props => (
31
- <Box
32
- as="li"
33
- sx={ {
34
- py: 2,
35
- borderBottom: '1px solid',
36
- borderColor: 'borders.2',
37
- listStyleType: 'none',
38
- margin: 0,
39
- px: 0,
40
- } }
41
- { ...props }
42
- />
43
- );
44
-
45
- const ResourceList = ( { groupedByDay = false, items, renderItem, dateKey } ) => {
46
- let groupedItems = {};
47
- if ( groupedByDay ) {
48
- groupedItems = items?.reduce( ( itemGroups, item ) => {
49
- const formattedDate = formatDate( item[ dateKey ] );
50
- const itemsAtDate = itemGroups[ formattedDate ];
51
-
52
- return {
53
- ...itemGroups,
54
- [ formattedDate ]: itemsAtDate ? [ ...itemsAtDate, item ] : [ item ],
55
- };
56
- }, {} );
57
- }
58
-
59
- const renderItemList = itemsList =>
60
- itemsList.map( ( item, index ) => (
61
- <StyledListItem key={ index }>{ renderItem( item ) }</StyledListItem>
62
- ) );
63
-
64
- const renderGoupedItems = () =>
65
- useMemo(
66
- () =>
67
- Object.keys( groupedItems ).map( ( groupName, index ) => (
68
- <Box sx={ { mb: 4 } } key={ index } as="li">
69
- <Heading variant="h4" as="h4" sx={ { mb: 3 } }>
70
- { groupName }
71
- </Heading>
72
- <Box
73
- as="ul"
74
- sx={ {
75
- listStyleType: 'none',
76
- m: 0,
77
- p: 0,
78
- borderTop: '1px solid',
79
- borderColor: 'border',
80
- } }
81
- >
82
- { renderItemList( groupedItems[ groupName ] ) }
83
- </Box>
84
- </Box>
85
- ) ),
86
- [ groupedItems ]
87
- );
88
-
89
- return (
90
- <Box
91
- as="ul"
92
- sx={ { listStyleType: 'none', m: 0, p: 0 } }
93
- className="vip-resource-list-component"
94
- >
95
- { groupedByDay ? renderGoupedItems( groupedItems ) : renderItemList( items ) }
96
- </Box>
97
- );
98
- };
99
-
100
- ResourceList.propTypes = {
101
- groupedByDay: PropTypes.bool,
102
- items: PropTypes.array,
103
- renderItem: PropTypes.func,
104
- relativeTime: PropTypes.bool,
105
- dateKey: PropTypes.string,
106
- };
107
-
108
- export { ResourceList };