@automattic/vip-design-system 2.18.1 → 2.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 (57) hide show
  1. package/.storybook/preview-head.html +1 -0
  2. package/build/system/Badge/Badge.js +2 -1
  3. package/build/system/DescriptionList/DescriptionList.js +0 -1
  4. package/build/system/Form/Label.d.ts +1 -3
  5. package/build/system/Form/Label.js +1 -3
  6. package/build/system/Form/RadioBoxGroup.jsx +12 -1
  7. package/build/system/Form/RadioBoxGroup.stories.jsx +6 -1
  8. package/build/system/Heading/Heading.js +2 -3
  9. package/build/system/Heading/Heading.stories.js +15 -2
  10. package/build/system/Nav/styles/variants/menu.js +1 -2
  11. package/build/system/NewForm/FormAutocompleteMultiselect.jsx +143 -12
  12. package/build/system/NewForm/FormAutocompleteMultiselect.stories.d.ts +22 -2
  13. package/build/system/NewForm/FormAutocompleteMultiselect.stories.jsx +21 -0
  14. package/build/system/NewForm/FormAutocompleteMultiselect.test.jsx +67 -1
  15. package/build/system/NewForm/FormAutocompleteMultiselectInlineChip.d.ts +7 -0
  16. package/build/system/NewForm/FormAutocompleteMultiselectInlineChip.js +62 -0
  17. package/build/system/Notice/Notice.js +1 -1
  18. package/build/system/Pagination/styles.js +1 -4
  19. package/build/system/Table/TableCell.js +1 -1
  20. package/build/system/Text/Text.js +0 -1
  21. package/build/system/Text/Text.stories.js +16 -13
  22. package/build/system/Toolbar/Logo.js +22 -6
  23. package/build/system/Wizard/Wizard.stories.js +11 -11
  24. package/build/system/Wizard/WizardStep.js +0 -2
  25. package/build/system/theme/generated/valet-theme-dark.json +224 -227
  26. package/build/system/theme/generated/valet-theme-light.json +224 -227
  27. package/build/system/theme/getPropValue.js +3 -7
  28. package/build/system/theme/index.d.ts +20 -12
  29. package/build/system/theme/index.js +27 -20
  30. package/docs/SETUP.md +1 -1
  31. package/package.json +1 -1
  32. package/src/system/Badge/Badge.tsx +2 -1
  33. package/src/system/DescriptionList/DescriptionList.tsx +0 -1
  34. package/src/system/Form/Label.tsx +1 -3
  35. package/src/system/Form/RadioBoxGroup.jsx +12 -1
  36. package/src/system/Form/RadioBoxGroup.stories.jsx +6 -1
  37. package/src/system/Heading/Heading.stories.tsx +10 -1
  38. package/src/system/Heading/Heading.tsx +1 -2
  39. package/src/system/Nav/styles/variants/menu.ts +1 -2
  40. package/src/system/NewForm/FormAutocompleteMultiselect.jsx +143 -12
  41. package/src/system/NewForm/FormAutocompleteMultiselect.stories.jsx +21 -0
  42. package/src/system/NewForm/FormAutocompleteMultiselect.test.jsx +67 -1
  43. package/src/system/NewForm/FormAutocompleteMultiselectInlineChip.tsx +72 -0
  44. package/src/system/Notice/Notice.tsx +1 -1
  45. package/src/system/Pagination/styles.ts +1 -4
  46. package/src/system/Table/TableCell.tsx +1 -1
  47. package/src/system/Text/Text.stories.tsx +7 -4
  48. package/src/system/Text/Text.tsx +0 -1
  49. package/src/system/Toolbar/Logo.tsx +19 -2
  50. package/src/system/Wizard/Wizard.stories.tsx +11 -11
  51. package/src/system/Wizard/WizardStep.tsx +0 -2
  52. package/src/system/theme/generated/valet-theme-dark.json +224 -227
  53. package/src/system/theme/generated/valet-theme-light.json +224 -227
  54. package/src/system/theme/getPropValue.ts +1 -8
  55. package/src/system/theme/index.ts +33 -18
  56. package/tokens/valet-core/valet-core.json +39 -9
  57. package/tokens/valet-core/wpvip-product-core.json +88 -125
@@ -16,6 +16,7 @@ var _ThemeBuilder = ThemeBuilder(Valet),
16
16
  ValetTheme = _ThemeBuilder.ValetTheme,
17
17
  getHeadingStyles = _ThemeBuilder.getHeadingStyles;
18
18
  var light = ColorBuilder(ValetTheme);
19
+ var supportLabelDefaultTypography = getPropValue('support', 'label-default');
19
20
 
20
21
  // Dark
21
22
  var _ThemeBuilder2 = ThemeBuilder(ValetDark),
@@ -29,12 +30,6 @@ var outline = {
29
30
  outlineWidth: '1px',
30
31
  boxShadow: "0 0 0 1px " + getPropValue('focus', 'inset') + ", 0 0 0 3px " + getPropValue('focus')
31
32
  };
32
- var fonts = {
33
- body: '-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
34
- heading: 'inherit',
35
- monospace: '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace',
36
- serif: 'recoletaregular, Georgia, serif'
37
- };
38
33
  var getComponentColors = function getComponentColors(theme, gColor, gVariants) {
39
34
  return {
40
35
  // Valet Theme Colors
@@ -175,13 +170,15 @@ var getComponentColors = function getComponentColors(theme, gColor, gVariants) {
175
170
  export default {
176
171
  outline: outline,
177
172
  space: getVariants('space'),
178
- fonts: fonts,
173
+ fonts: {
174
+ monospace: getPropValue('fontFamily', 'monospace'),
175
+ "default": getPropValue('fontFamily', 'default')
176
+ },
179
177
  fontSizes: getVariants('fontSize.static'),
180
178
  breakpoints: generateBreakpoints(getVariants('breakpoint')),
181
179
  fontWeights: {
182
- body: getPropValue('fontWeight', 'body'),
183
- heading: getPropValue('fontWeight', 'heading'),
184
180
  regular: getPropValue('fontWeight', 'regular'),
181
+ semibold: getPropValue('fontWeight', 'semibold'),
185
182
  bold: getPropValue('fontWeight', 'bold'),
186
183
  medium: getPropValue('fontWeight', 'medium'),
187
184
  light: getPropValue('fontWeight', 'light')
@@ -276,9 +273,13 @@ export default {
276
273
  }
277
274
  }
278
275
  },
276
+ forms: {
277
+ label: _extends({}, supportLabelDefaultTypography)
278
+ },
279
279
  buttons: {
280
- primary: {
281
- fontFamily: 'body',
280
+ primary: _extends({}, supportLabelDefaultTypography, {
281
+ // Button label weight: theme `medium` (500).
282
+ fontWeight: 'medium',
282
283
  color: 'button.primary.label.default',
283
284
  bg: 'button.primary.background.default',
284
285
  border: '1px solid transparent',
@@ -287,7 +288,6 @@ export default {
287
288
  minHeight: '38px',
288
289
  display: 'inline-flex',
289
290
  cursor: 'pointer',
290
- fontWeight: 'medium',
291
291
  boxShadow: 'none',
292
292
  borderRadius: 1,
293
293
  '&:hover': {
@@ -326,7 +326,7 @@ export default {
326
326
  backgroundColor: 'button.secondary.background.disabled',
327
327
  color: 'button.secondary.label.default'
328
328
  }
329
- },
329
+ }),
330
330
  secondary: {
331
331
  variant: 'buttons.primary',
332
332
  color: 'button.secondary.label.default',
@@ -490,7 +490,14 @@ export default {
490
490
  variant: 'buttons.tertiary'
491
491
  }
492
492
  },
493
- text: getHeadingStyles(),
493
+ text: _extends({}, getHeadingStyles(), getVariants('body'), {
494
+ 'support-helper-text': getPropValue('support', 'helper-text'),
495
+ 'support-label-xs': getPropValue('support', 'label-xs'),
496
+ 'support-label-small': getPropValue('support', 'label-small'),
497
+ 'support-label-default': getPropValue('support', 'label-default'),
498
+ 'support-label-default-quiet': getPropValue('support', 'label-default-quiet'),
499
+ 'support-label-default-loud': getPropValue('support', 'label-default-loud')
500
+ }),
494
501
  dialog: {
495
502
  modal: {
496
503
  position: 'fixed',
@@ -569,10 +576,10 @@ export default {
569
576
  }
570
577
  },
571
578
  styles: {
572
- root: _extends({
573
- fontFamily: 'body',
574
- lineHeight: 'body',
575
- fontWeight: 'body',
579
+ root: {
580
+ fontFamily: getPropValue('body', 'default').fontFamily,
581
+ lineHeight: getPropValue('body', 'default').lineHeight,
582
+ fontWeight: getPropValue('body', 'default').fontWeight,
576
583
  color: 'text',
577
584
  backgroundColor: 'backgrounds.primary',
578
585
  webkitFontSmoothing: 'antialiased',
@@ -589,11 +596,11 @@ export default {
589
596
  display: 'block'
590
597
  },
591
598
  pre: {
592
- fontFamily: 'body'
599
+ fontFamily: 'default'
593
600
  },
594
601
  p: {
595
602
  color: 'text'
596
603
  }
597
- }, getHeadingStyles())
604
+ }
598
605
  }
599
606
  };
package/docs/SETUP.md CHANGED
@@ -41,7 +41,7 @@ Run:
41
41
  npm run dev
42
42
  ```
43
43
 
44
- You can then visit [http://localhost:60006/](http://localhost:60006/) to view.
44
+ You can then visit [http://localhost:6006/](http://localhost:6006/) to view.
45
45
 
46
46
  ## Updating dependencies
47
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/vip-design-system",
3
- "version": "2.18.1",
3
+ "version": "2.20.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/Automattic/vip-design-system"
@@ -27,6 +27,7 @@ export const Badge = forwardRef< HTMLDivElement, BadgeProps >(
27
27
  as="span"
28
28
  sx={ {
29
29
  fontSize: 0,
30
+ letterSpacing: '0.01em',
30
31
  padding: 0, // do we need padding declared twice here?
31
32
  bg: `tag.${ variant }.background`,
32
33
  color: `tag.${ variant }.text`,
@@ -35,7 +36,7 @@ export const Badge = forwardRef< HTMLDivElement, BadgeProps >(
35
36
  px: 2,
36
37
  display: 'inline-block',
37
38
  borderRadius: 1,
38
- fontWeight: 'heading',
39
+ fontWeight: 'medium',
39
40
  a: {
40
41
  color: `tag.${ variant }.text`,
41
42
  '&:hover, &:focus, &:active': {
@@ -42,7 +42,6 @@ const TableComponent = ( { list, className, sx, title }: DescriptionListProps )
42
42
  minWidth: 'auto',
43
43
  '> tbody > tr': {
44
44
  '> td, > th': {
45
- fontWeight: 'heading',
46
45
  border: 'none',
47
46
  pl: 0,
48
47
  '&:first-of-type': { pl: 0 },
@@ -13,9 +13,7 @@ import { RequiredLabel } from './RequiredLabel';
13
13
 
14
14
  export const baseLabelColor = 'input.label.default';
15
15
  export const baseLabelStyle = {
16
- fontWeight: 'heading',
17
- fontSize: 2,
18
- lineHeight: 1.5,
16
+ variant: 'forms.label',
19
17
  color: baseLabelColor,
20
18
  };
21
19
 
@@ -72,7 +72,14 @@ const RadioOption = ( {
72
72
  { ...restOption }
73
73
  />
74
74
  <div
75
- sx={ { mb: 0, color: 'input.radio-box.label.primary.default', p: 3, pr: 0, flex: 'auto' } }
75
+ sx={ {
76
+ mb: 0,
77
+ color: 'input.radio-box.label.primary.default',
78
+ fontWeight: 'semibold',
79
+ p: 3,
80
+ pr: 0,
81
+ flex: 'auto',
82
+ } }
76
83
  >
77
84
  <label htmlFor={ forLabel } { ...labelProps }>
78
85
  { label }
@@ -82,7 +89,11 @@ const RadioOption = ( {
82
89
  sx={ {
83
90
  color: 'input.radio-box.label.secondary.default',
84
91
  mb: 0,
92
+ mt: 1,
85
93
  fontSize: 1,
94
+ fontWeight: 'regular',
95
+ letterSpacing: '0.01em',
96
+ lineHeight: '140%',
86
97
  display: 'block',
87
98
  } }
88
99
  id={ describedById }
@@ -60,7 +60,12 @@ export const Primary = {
60
60
  args: {
61
61
  defaultValue: 'one',
62
62
  options: [
63
- { label: 'One', value: 'one', description: 'This is a description' },
63
+ {
64
+ label: 'One',
65
+ value: 'one',
66
+ description:
67
+ 'This is a longer description that allows us to see the text wrap and determine if the line height is correct',
68
+ },
64
69
  { label: 'Two', value: 'two', description: 'This is a description' },
65
70
  { label: 'Three', value: 'three', description: 'This is a description' },
66
71
  ],
@@ -27,7 +27,16 @@ export const Default: Story = {
27
27
  <Heading variant="h3">Heading Three</Heading>
28
28
  <Heading variant="h4">Heading Four</Heading>
29
29
  <Heading variant="h5">Heading Five</Heading>
30
- <Heading variant="h6">Heading Six</Heading>
30
+
31
+ <Heading variant="h3" as="h1">
32
+ Heading One with Heading Three Styles
33
+ </Heading>
34
+ <Heading as="p" sx={ { variant: 'text.caps' } }>
35
+ Paragraph with Caps Styles
36
+ </Heading>
37
+ <Heading as="h3" sx={ { variant: 'text.caps' } }>
38
+ Heading Three with Caps Styles
39
+ </Heading>
31
40
  </Box>
32
41
  ),
33
42
  };
@@ -20,10 +20,9 @@ export const Heading = forwardRef< HTMLHeadingElement, HeadingProps >(
20
20
  ( { variant = 'h3', sx, className, ...rest }: HeadingProps, ref: Ref< HTMLHeadingElement > ) => (
21
21
  <ThemeHeading
22
22
  as={ variant }
23
+ variant={ variant }
23
24
  sx={ {
24
25
  color: 'heading',
25
- // pass variant prop to sx
26
- variant: `text.${ variant.toString() }`,
27
26
  ...sx,
28
27
  } }
29
28
  className={ classNames( 'vip-heading-component', className ) }
@@ -51,8 +51,7 @@ export const menuItemLinkStyles: MixedStyleProp = {
51
51
  borderRadius: 1,
52
52
  color: 'text',
53
53
  display: 'inline-flex',
54
- fontSize: '0.875rem',
55
- fontWeight: 'body',
54
+ variant: 'text.default',
56
55
  height: 38,
57
56
  mx: 0,
58
57
  mb: 0,
@@ -13,6 +13,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
13
13
  */
14
14
  import { FormAutocompleteMultiselectBadge } from './FormAutocompleteMultiselectBadge';
15
15
  import { FormAutocompleteMultiselectButton } from './FormAutocompleteMultiselectButton';
16
+ import { FormAutocompleteMultiselectInlineChip } from './FormAutocompleteMultiselectInlineChip';
16
17
  import { FormSelectArrow } from './FormSelectArrow';
17
18
  import { FormSelectContent } from './FormSelectContent';
18
19
  import { FormSelectLoading } from './FormSelectLoading';
@@ -22,6 +23,14 @@ import { Validation } from '../Form';
22
23
  import { baseControlBorderStyle, inputBaseText } from '../Form/Input.styles';
23
24
  import { Label } from '../Form/Label';
24
25
 
26
+ const escapeHtml = str =>
27
+ String( str )
28
+ .replace( /&/g, '&amp;' )
29
+ .replace( /</g, '&lt;' )
30
+ .replace( />/g, '&gt;' )
31
+ .replace( /"/g, '&quot;' )
32
+ .replace( /'/g, '&#039;' );
33
+
25
34
  const baseBorderTextColors = {
26
35
  ...baseControlBorderStyle,
27
36
  backgroundColor: 'layer.2',
@@ -96,7 +105,33 @@ const searchIconStyles = {
96
105
  },
97
106
  };
98
107
 
99
- const DefaultArrow = config => <FormSelectArrow classNames={ config.className } />;
108
+ const inlineChipsContainerStyles = {
109
+ ...defaultStyles,
110
+ display: 'flex',
111
+ flexWrap: 'wrap',
112
+ alignItems: 'center',
113
+ p: 1,
114
+ pr: 0,
115
+ position: 'relative',
116
+ '& .autocomplete__input': {
117
+ ...defaultStyles[ '& .autocomplete__input' ],
118
+ lineHeight: '24px',
119
+ minHeight: '24px',
120
+ },
121
+ '&:focus-within': theme => theme.outline,
122
+ '& .autocomplete__wrapper': {
123
+ position: 'static',
124
+ width: '100%',
125
+ lineHeight: '24px',
126
+ minHeight: '24px',
127
+ '& .autocomplete__dropdown-arrow-down': {
128
+ top: 'unset',
129
+ bottom: '6px',
130
+ },
131
+ },
132
+ };
133
+
134
+ const DefaultArrow = config => <FormSelectArrow className={ config.className } />;
100
135
 
101
136
  const AddSelectionStatus = ( { status } ) => {
102
137
  return (
@@ -156,10 +191,12 @@ const FormAutocompleteMultiselect = React.forwardRef(
156
191
  listType = 'button',
157
192
  initialValue = [],
158
193
  allowCustom = false,
194
+ variant,
159
195
  ...props
160
196
  },
161
197
  forwardRef
162
198
  ) => {
199
+ const isInlineChips = variant === 'inline-chips';
163
200
  const OPTION_ACTION = {
164
201
  ADD: 'add',
165
202
  REMOVE: 'remove',
@@ -175,6 +212,7 @@ const FormAutocompleteMultiselect = React.forwardRef(
175
212
  option: null,
176
213
  index: -1,
177
214
  } );
215
+ const justSelectedRef = React.useRef( false );
178
216
  let debounceTimeout;
179
217
  forwardRef = forwardRef || React.createRef();
180
218
 
@@ -283,11 +321,44 @@ const FormAutocompleteMultiselect = React.forwardRef(
283
321
  data = handleTypeChange( query );
284
322
  }
285
323
  const optionForDisplay = data?.map( option => optionLabel( option ) );
286
- populateResults(
287
- optionForDisplay.filter( option => ! selectedOptions.includes( option ) )
288
- );
324
+ if ( isInlineChips ) {
325
+ populateResults( optionForDisplay );
326
+ } else {
327
+ populateResults(
328
+ optionForDisplay.filter( option => ! selectedOptions.includes( option ) )
329
+ );
330
+ }
331
+ },
332
+ [ autoFilter, isDirty, isInlineChips, onInputChange, options, selectedOptions ]
333
+ );
334
+
335
+ const onValueChangeInlineChips = useCallback(
336
+ inputValue => {
337
+ if ( ! inputValue ) {
338
+ return;
339
+ }
340
+ justSelectedRef.current = true;
341
+ if ( selectedOptions.includes( inputValue ) ) {
342
+ unselectValue( inputValue, selectedOptions.indexOf( inputValue ) );
343
+ } else {
344
+ setCurrentOption( { action: OPTION_ACTION.ADD, option: inputValue } );
345
+ setSelectedOptions( [ ...selectedOptions, inputValue ] );
346
+ }
289
347
  },
290
- [ autoFilter, isDirty, onInputChange, options, selectedOptions ]
348
+ [ selectedOptions, unselectValue ]
349
+ );
350
+
351
+ const inlineChipsTemplates = useMemo(
352
+ () => ( {
353
+ suggestion: suggestion => {
354
+ const isSelected = selectedOptions.includes( suggestion );
355
+ const check = isSelected ? '&#10003;' : '';
356
+ return `<span style="display:flex;align-items:center;gap:8px"><span style="width:16px;flex-shrink:0">${ check }</span>${ escapeHtml(
357
+ suggestion
358
+ ) }</span>`;
359
+ },
360
+ } ),
361
+ [ selectedOptions ]
291
362
  );
292
363
 
293
364
  useEffect( () => {
@@ -332,7 +403,16 @@ const FormAutocompleteMultiselect = React.forwardRef(
332
403
  selectedOptions,
333
404
  selectedOptions.map( option => option?.label || option )
334
405
  );
335
- resetInputState();
406
+ if ( isInlineChips && justSelectedRef.current && forwardRef?.current ) {
407
+ justSelectedRef.current = false;
408
+ forwardRef.current.setState( {
409
+ ...forwardRef.current.state,
410
+ query: '',
411
+ menuOpen: true,
412
+ } );
413
+ } else {
414
+ resetInputState();
415
+ }
336
416
  }, [ selectedOptions ] );
337
417
 
338
418
  // Update the select status for screen readers
@@ -340,15 +420,65 @@ const FormAutocompleteMultiselect = React.forwardRef(
340
420
  if ( currentOption.action === OPTION_ACTION.ADD ) {
341
421
  setAddStatus( `${ currentOption.option } added to the list.` );
342
422
  setCurrentOption( { action: OPTION_ACTION.NONE, option: null } );
343
- } else if ( currentOption.index === selectedOptions.length && selectedOptions.length > 0 ) {
344
- // Move focus to the first selected item, if the last element is removed and there are other elements in the list
345
- global.document.querySelector( '.vip-button-component' )?.focus();
346
- } else if ( selectedOptions.length === 0 ) {
347
- // Move focus to the input field if the last element is removed and there are no other elements in the list
348
- global.document.querySelector( '.autocomplete__input' )?.focus();
423
+ } else if ( currentOption.action === OPTION_ACTION.REMOVE ) {
424
+ setAddStatus( `${ currentOption.option } removed from the list.` );
425
+ if ( isInlineChips ) {
426
+ global.document.querySelector( `#${ forLabel }` )?.focus();
427
+ } else if ( currentOption.index === selectedOptions.length && selectedOptions.length > 0 ) {
428
+ global.document.querySelector( '.vip-button-component' )?.focus();
429
+ } else if ( selectedOptions.length === 0 ) {
430
+ global.document.querySelector( '.autocomplete__input' )?.focus();
431
+ }
432
+ setCurrentOption( { action: OPTION_ACTION.NONE, option: null } );
349
433
  }
350
434
  }, [ currentOption ] );
351
435
 
436
+ if ( isInlineChips ) {
437
+ return (
438
+ <div className={ classNames( 'vip-form-autocomplete-component', className ) }>
439
+ { label && <SelectLabel /> }
440
+ <div sx={ inlineChipsContainerStyles }>
441
+ { selectedOptions.map( ( option, idx ) => (
442
+ <FormAutocompleteMultiselectInlineChip
443
+ key={ option }
444
+ index={ idx }
445
+ option={ option }
446
+ unselectValue={ unselectValue }
447
+ />
448
+ ) ) }
449
+ <div sx={ { flex: '1 1 120px', minWidth: '120px' } }>
450
+ <Autocomplete
451
+ id={ forLabel }
452
+ aria-busy={ loading }
453
+ showAllValues={ true }
454
+ ref={ forwardRef }
455
+ source={ source || suggest }
456
+ defaultValue={ value }
457
+ displayMenu={ displayMenu }
458
+ onConfirm={ onValueChangeInlineChips }
459
+ tNoResults={ noOptionsMessage }
460
+ required={ required }
461
+ dropdownArrow={ dropdownArrow }
462
+ confirmOnBlur={ false }
463
+ templates={ inlineChipsTemplates }
464
+ { ...props }
465
+ placeholder={ selectedOptions.length > 0 ? '' : props.placeholder || '' }
466
+ />
467
+ </div>
468
+ { addStatus && <AddSelectionStatus status={ addStatus } /> }
469
+ { loading && <FormSelectLoading sx={ { right: 7 } } /> }
470
+ </div>
471
+ { hasError && errorMessage && (
472
+ <Flex sx={ { mt: 2 } }>
473
+ <Validation isValid={ false } describedId={ forLabel }>
474
+ { errorMessage }
475
+ </Validation>
476
+ </Flex>
477
+ ) }
478
+ </div>
479
+ );
480
+ }
481
+
352
482
  return (
353
483
  <div className={ classNames( 'vip-form-autocomplete-component', className ) }>
354
484
  { label && ! isInline && <SelectLabel /> }
@@ -435,6 +565,7 @@ FormAutocompleteMultiselect.propTypes = {
435
565
  dropdownArrow: PropTypes.node,
436
566
  initialValue: PropTypes.array,
437
567
  allowCustom: PropTypes.bool,
568
+ variant: PropTypes.string,
438
569
  };
439
570
 
440
571
  FormAutocompleteMultiselect.displayName = 'FormAutocompleteMultiselect';
@@ -134,6 +134,27 @@ export const WithStaticData = {
134
134
  },
135
135
  };
136
136
 
137
+ export const InlineChips = {
138
+ render: props => <DefaultComponent { ...props } width={ 500 } />,
139
+ args: {
140
+ label: 'Post Categories',
141
+ options: [
142
+ { value: 'breaking-news', label: 'Breaking News' },
143
+ { value: 'world-news', label: 'World News' },
144
+ { value: 'us-news', label: 'U.S. News' },
145
+ { value: 'climate-environment', label: 'Climate & Environment' },
146
+ { value: 'obituaries', label: 'Obituaries' },
147
+ { value: 'technology', label: 'Technology' },
148
+ { value: 'entertainment', label: 'Entertainment' },
149
+ { value: 'real-estate', label: 'Real Estate' },
150
+ ],
151
+ variant: 'inline-chips',
152
+ showAllValues: true,
153
+ placeholder: 'Search categories...',
154
+ initialValue: [ 'Breaking News' ],
155
+ },
156
+ };
157
+
137
158
  export const WithDynamicData = {
138
159
  render: () => {
139
160
  const [ selectedValues, setSelectedValues ] = useState( [] );
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { render, screen } from '@testing-library/react';
4
+ import { fireEvent, render, screen } from '@testing-library/react';
5
5
  import { axe } from 'jest-axe';
6
6
 
7
7
  /**
@@ -37,3 +37,69 @@ describe( '<FormAutocompleteMultiselect />', () => {
37
37
  await expect( await axe( container ) ).toHaveNoViolations();
38
38
  } );
39
39
  } );
40
+
41
+ describe( '<FormAutocompleteMultiselect variant="inline-chips" />', () => {
42
+ it( 'renders the inline-chips variant', async () => {
43
+ const { container } = render(
44
+ <FormAutocompleteMultiselect
45
+ forLabel="my_inline_chips"
46
+ label="Categories"
47
+ options={ options }
48
+ variant="inline-chips"
49
+ showAllValues
50
+ />
51
+ );
52
+ expect( screen.getByLabelText( 'Categories' ) ).toBeInTheDocument();
53
+ await expect( await axe( container ) ).toHaveNoViolations();
54
+ } );
55
+
56
+ it( 'renders initial values as inline chips', () => {
57
+ render(
58
+ <FormAutocompleteMultiselect
59
+ forLabel="my_inline_chips_init"
60
+ label="Categories"
61
+ options={ options }
62
+ variant="inline-chips"
63
+ showAllValues
64
+ initialValue={ [ 'Chocolate', 'Vanilla' ] }
65
+ />
66
+ );
67
+ expect( screen.getByText( 'Chocolate' ) ).toBeInTheDocument();
68
+ expect( screen.getByText( 'Vanilla' ) ).toBeInTheDocument();
69
+ expect( screen.getByRole( 'button', { name: 'Remove Chocolate' } ) ).toBeInTheDocument();
70
+ expect( screen.getByRole( 'button', { name: 'Remove Vanilla' } ) ).toBeInTheDocument();
71
+ } );
72
+
73
+ it( 'removes a chip when the close button is clicked', () => {
74
+ render(
75
+ <FormAutocompleteMultiselect
76
+ forLabel="my_inline_chips_remove"
77
+ label="Categories"
78
+ options={ options }
79
+ variant="inline-chips"
80
+ showAllValues
81
+ initialValue={ [ 'Chocolate', 'Vanilla' ] }
82
+ />
83
+ );
84
+ expect( screen.getByText( 'Chocolate' ) ).toBeInTheDocument();
85
+ fireEvent.click( screen.getByRole( 'button', { name: 'Remove Chocolate' } ) );
86
+ expect( screen.queryByText( 'Chocolate' ) ).not.toBeInTheDocument();
87
+ expect( screen.getByText( 'Vanilla' ) ).toBeInTheDocument();
88
+ } );
89
+
90
+ it( 'announces removal to screen readers', () => {
91
+ const { container } = render(
92
+ <FormAutocompleteMultiselect
93
+ forLabel="my_inline_chips_a11y"
94
+ label="Categories"
95
+ options={ options }
96
+ variant="inline-chips"
97
+ showAllValues
98
+ initialValue={ [ 'Chocolate', 'Vanilla' ] }
99
+ />
100
+ );
101
+ fireEvent.click( screen.getByRole( 'button', { name: 'Remove Chocolate' } ) );
102
+ const statusEl = container.querySelector( '#vip-autocompletemultiselect-status' );
103
+ expect( statusEl ).toHaveTextContent( 'Chocolate removed from the list.' );
104
+ } );
105
+ } );
@@ -0,0 +1,72 @@
1
+ /** @jsxImportSource theme-ui */
2
+
3
+ /**
4
+ * External dependencies
5
+ */
6
+ import { MdClose } from 'react-icons/md';
7
+
8
+ const FormAutocompleteMultiselectInlineChip = ( {
9
+ index,
10
+ option,
11
+ unselectValue,
12
+ }: {
13
+ index: number;
14
+ option: string;
15
+ unselectValue: ( option: string, index: number ) => void;
16
+ } ) => {
17
+ return (
18
+ <span
19
+ sx={ {
20
+ display: 'inline-flex',
21
+ alignItems: 'center',
22
+ gap: 1,
23
+ px: 2,
24
+ py: '2px',
25
+ m: 1,
26
+ bg: 'layer.1',
27
+ borderRadius: 1,
28
+ fontSize: 1,
29
+ lineHeight: '16px',
30
+ whiteSpace: 'nowrap',
31
+ maxWidth: '100%',
32
+ } }
33
+ >
34
+ <span
35
+ sx={ {
36
+ overflow: 'hidden',
37
+ textOverflow: 'ellipsis',
38
+ whiteSpace: 'nowrap',
39
+ } }
40
+ >
41
+ { option }
42
+ </span>
43
+ <button
44
+ type="button"
45
+ aria-label={ `Remove ${ option }` }
46
+ onClick={ e => {
47
+ e.preventDefault();
48
+ e.stopPropagation();
49
+ unselectValue( option, index );
50
+ } }
51
+ sx={ {
52
+ display: 'inline-flex',
53
+ alignItems: 'center',
54
+ justifyContent: 'center',
55
+ p: 0,
56
+ border: 'none',
57
+ bg: 'transparent',
58
+ cursor: 'pointer',
59
+ color: 'text',
60
+ lineHeight: 0,
61
+ '&:hover': {
62
+ opacity: 0.7,
63
+ },
64
+ } }
65
+ >
66
+ <MdClose size={ 14 } />
67
+ </button>
68
+ </span>
69
+ );
70
+ };
71
+
72
+ export { FormAutocompleteMultiselectInlineChip };
@@ -240,7 +240,7 @@ export const Notice = React.forwardRef< HTMLDivElement, NoticeProps >(
240
240
  as={ headingVariant }
241
241
  sx={ {
242
242
  color: textColor,
243
- mb: 0,
243
+ mb: 1,
244
244
  fontSize: 2,
245
245
  fontWeight: 'bold',
246
246
  } }