agroptima-design-system 0.36.0-beta.3 → 0.36.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 (51) hide show
  1. package/package.json +3 -3
  2. package/src/atoms/Button/BaseButton.tsx +2 -1
  3. package/src/atoms/Button/Button.tsx +6 -2
  4. package/src/atoms/Card/Card.scss +6 -6
  5. package/src/atoms/Card/CardContent.tsx +1 -1
  6. package/src/atoms/Card/CardFooter.tsx +1 -1
  7. package/src/atoms/Card/CardHeader.tsx +4 -2
  8. package/src/atoms/Checkbox.tsx +1 -1
  9. package/src/atoms/{Collapsible.scss → Collapsible/Collapsible.scss} +27 -45
  10. package/src/atoms/{Collapsible.tsx → Collapsible/Collapsible.tsx} +10 -10
  11. package/src/atoms/Collapsible/index.ts +5 -0
  12. package/src/atoms/DatePicker/DateRangePicker.tsx +3 -0
  13. package/src/atoms/DatePicker/DateSinglePicker.tsx +3 -0
  14. package/src/atoms/Input.scss +31 -0
  15. package/src/atoms/Input.tsx +7 -1
  16. package/src/atoms/InputWithButton/InputWithButton.scss +11 -0
  17. package/src/atoms/InputWithButton/InputWithButton.tsx +10 -0
  18. package/src/atoms/InputWithButton/index.tsx +3 -0
  19. package/src/atoms/Modal/ModalDialog.tsx +18 -0
  20. package/src/atoms/Multiselect.tsx +13 -0
  21. package/src/atoms/Popover/Popover.tsx +1 -1
  22. package/src/atoms/QuantitySelector.tsx +7 -1
  23. package/src/atoms/Select/Select.scss +14 -8
  24. package/src/atoms/Select/Select.tsx +13 -2
  25. package/src/atoms/Select/SelectItem.tsx +18 -3
  26. package/src/atoms/Select/SelectItems.tsx +19 -2
  27. package/src/atoms/Select/SelectTrigger.tsx +4 -0
  28. package/src/atoms/TextArea.tsx +3 -0
  29. package/src/hooks/useOpen.ts +1 -0
  30. package/src/icons/classic-view.svg +1 -0
  31. package/src/icons/duplicate.svg +1 -0
  32. package/src/icons/index.tsx +6 -0
  33. package/src/icons/new-view.svg +1 -0
  34. package/src/stories/Button.stories.ts +4 -0
  35. package/src/stories/Changelog.mdx +14 -1
  36. package/src/stories/{Collapsible.stories.js → Collapsible.stories.tsx} +64 -37
  37. package/src/stories/DateRangePicker.stories.ts +5 -0
  38. package/src/stories/DateSinglePicker.stories.ts +5 -0
  39. package/src/stories/Drawer.stories.js +2 -2
  40. package/src/stories/Input.stories.ts +32 -0
  41. package/src/stories/InputWithButton.stories.tsx +75 -0
  42. package/src/stories/Modal.stories.tsx +341 -0
  43. package/src/stories/Multiselect.stories.ts +4 -0
  44. package/src/stories/QuantitySelector.stories.ts +5 -0
  45. package/src/stories/Select.stories.ts +4 -0
  46. package/src/stories/TextArea.stories.ts +4 -0
  47. package/tests/Card.spec.tsx +3 -3
  48. package/tests/Modal.spec.tsx +79 -11
  49. package/tests/Select.spec.tsx +17 -0
  50. package/tsconfig.json +0 -1
  51. package/src/stories/Modal.stories.js +0 -444
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroptima-design-system",
3
- "version": "0.36.0-beta.3",
3
+ "version": "0.36.0",
4
4
  "scripts": {
5
5
  "dev": "npm run storybook",
6
6
  "storybook": "storybook dev -p 6006 --ci",
@@ -36,8 +36,8 @@
36
36
  "@storybook/test": "^8.6.12",
37
37
  "@svgr/webpack": "^8.1.0",
38
38
  "@testing-library/jest-dom": "^6.6.3",
39
- "@testing-library/react": "^16.1.0",
40
- "@testing-library/user-event": "^14.5.2",
39
+ "@testing-library/react": "^16.3.0",
40
+ "@testing-library/user-event": "^14.6.1",
41
41
  "@types/jest": "^29.5.14",
42
42
  "@types/jest-axe": "^3.5.9",
43
43
  "@types/node": "^22.10.5",
@@ -1,10 +1,11 @@
1
1
  import NextLink, { type LinkProps } from 'next/link'
2
- import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react'
2
+ import type { AnchorHTMLAttributes, ButtonHTMLAttributes, Ref } from 'react'
3
3
 
4
4
  interface CommonProps {
5
5
  disabled?: boolean
6
6
  visible?: boolean
7
7
  prefetch?: boolean
8
+ ref?: Ref<HTMLAnchorElement | HTMLButtonElement | null>
8
9
  }
9
10
 
10
11
  type HtmlButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
@@ -13,6 +13,7 @@ interface CustomProps {
13
13
  variant?: ButtonVariant
14
14
  loading?: boolean
15
15
  disabled?: boolean
16
+ fullWidth?: boolean
16
17
  }
17
18
 
18
19
  export type ButtonProps = CustomProps & BaseButtonProps
@@ -43,13 +44,16 @@ export function Button({
43
44
  leftIcon,
44
45
  rightIcon,
45
46
  disabled,
47
+ className,
46
48
  variant = 'primary',
47
49
  loading = false,
48
- className,
50
+ fullWidth = false,
49
51
  ...props
50
52
  }: ButtonProps) {
51
53
  const leftIconName = loading ? 'Loading' : leftIcon
52
- const cssClasses = classNames(className, 'button', variant)
54
+ const cssClasses = classNames(className, 'button', variant, {
55
+ 'full-width': fullWidth,
56
+ })
53
57
 
54
58
  return (
55
59
  <BaseButton
@@ -27,13 +27,13 @@ a.card {
27
27
  gap: config.$space-4x;
28
28
  }
29
29
 
30
- .header {
30
+ .card-header {
31
31
  display: flex;
32
32
  flex-direction: row;
33
33
  justify-content: space-between;
34
34
  gap: config.$space-1x;
35
35
 
36
- .title {
36
+ .card-title {
37
37
  overflow: hidden;
38
38
  text-overflow: ellipsis;
39
39
  line-clamp: 2;
@@ -41,16 +41,16 @@ a.card {
41
41
  display: -webkit-box;
42
42
  }
43
43
 
44
- > .bold {
44
+ > .card-bold {
45
45
  @include typography.body-bold;
46
46
  }
47
47
  }
48
48
 
49
- .content {
49
+ .card-content {
50
50
  margin-bottom: config.$space-1x;
51
51
  }
52
52
 
53
- .footer {
53
+ .card-footer {
54
54
  display: flex;
55
55
  flex-direction: column;
56
56
  gap: config.$space-2x;
@@ -77,7 +77,7 @@ a.card {
77
77
  @include typography.body-regular-disabled;
78
78
  background: color_alias.$neutral-color-50;
79
79
 
80
- .header .bold {
80
+ .card-header .card-bold {
81
81
  color: color_alias.$neutral-color-400;
82
82
  }
83
83
  }
@@ -10,7 +10,7 @@ export function CardContent({
10
10
  children,
11
11
  ...props
12
12
  }: CardContentProps): React.JSX.Element {
13
- const cssClasses = classNames('content', className)
13
+ const cssClasses = classNames('card-content', className)
14
14
 
15
15
  return (
16
16
  <div className={cssClasses} {...props}>
@@ -10,7 +10,7 @@ export function CardFooter({
10
10
  children,
11
11
  ...props
12
12
  }: CardFooterProps): React.JSX.Element {
13
- const cssClasses = classNames('footer', className)
13
+ const cssClasses = classNames('card-footer', className)
14
14
 
15
15
  return (
16
16
  <div className={cssClasses} {...props}>
@@ -14,11 +14,13 @@ export function CardHeader({
14
14
  children,
15
15
  ...props
16
16
  }: CardHeaderProps): React.JSX.Element {
17
- const cssClasses = classNames('header', className, { bold: isBold })
17
+ const cssClasses = classNames('card-header', className, { bold: isBold })
18
18
 
19
19
  return (
20
20
  <div className={cssClasses} {...props}>
21
- <span className={classNames('title', { bold: isBold })}>{title}</span>
21
+ <span className={classNames('card-title', { 'card-bold': isBold })}>
22
+ {title}
23
+ </span>
22
24
  {children && <div className="actions">{children}</div>}
23
25
  </div>
24
26
  )
@@ -55,7 +55,7 @@ export function Checkbox({
55
55
  disabled={disabled}
56
56
  className={classNames({ 'visually-hidden': hideLabel })}
57
57
  >
58
- {children}
58
+ {children || accessibilityLabel}
59
59
  </Label>
60
60
  </div>
61
61
  )
@@ -1,7 +1,8 @@
1
- @use '../settings/color_alias';
2
- @use '../settings/typography/content' as typography;
3
- @use '../settings/config';
4
- @use '../settings/depth';
1
+ @use '../../settings/color_alias';
2
+ @use '../../settings/typography/content' as typography;
3
+ @use '../../settings/config';
4
+ @use '../../settings/depth';
5
+ @use '../../settings/mixins';
5
6
 
6
7
  details summary::-webkit-details-marker {
7
8
  display: none;
@@ -18,33 +19,22 @@ details summary::-webkit-details-marker {
18
19
  user-select: none;
19
20
  }
20
21
 
21
- .header {
22
+ .collapsible-header {
22
23
  display: flex;
23
- align-items: center !important;
24
+ align-items: center;
24
25
  gap: config.$space-3x;
25
26
  padding: config.$space-5x;
26
- padding-bottom: config.$space-5x !important;
27
27
  cursor: default;
28
28
 
29
- .icon {
30
- width: config.$icon-size-4x !important;
31
- height: config.$icon-size-4x !important;
32
- > svg {
33
- width: 100%;
34
- height: 100%;
35
- }
36
- }
37
-
38
- .title {
39
- font-size: 1rem !important;
40
- font-weight: normal !important;
29
+ .collapsible-title {
30
+ font-size: 1rem;
31
+ font-weight: normal;
41
32
  flex: 1;
42
33
  }
43
34
  }
44
35
 
45
- .content {
46
- padding: config.$space-7x;
47
- padding-bottom: config.$space-3x;
36
+ .collapsible-content {
37
+ padding: config.$space-7x config.$space-7x config.$space-3x;
48
38
 
49
39
  &.no-horizontal-padding {
50
40
  padding-left: 0;
@@ -52,54 +42,46 @@ details summary::-webkit-details-marker {
52
42
  }
53
43
  }
54
44
 
55
- &.primary {
45
+ &.primary, &.secondary {
56
46
  &[open] {
57
- .header {
47
+ .collapsible-header {
58
48
  background: transparent;
59
49
  border-bottom: 3px solid color_alias.$primary-color-600;
60
50
  }
61
51
  }
62
52
 
63
53
  &.disabled {
64
- .header {
54
+ .collapsible-header {
65
55
  background: color_alias.$neutral-color-50;
66
56
  border-bottom: 1px solid color_alias.$neutral-color-200;
67
57
  color: color_alias.$neutral-color-200;
68
-
69
- .icon {
70
- > svg {
71
- fill: color_alias.$neutral-color-200;
72
- path {
73
- fill: color_alias.$neutral-color-200;
74
- }
75
- }
76
- }
58
+ @include mixins.svg-color(color_alias.$neutral-color-200);
77
59
  }
78
60
  }
79
61
 
80
- .header {
62
+ .collapsible-header {
81
63
  color: color_alias.$neutral-color-1000;
82
64
  background: transparent;
83
65
  border-bottom: 1px solid color_alias.$neutral-color-200;
84
-
85
- .icon {
86
- > svg {
87
- fill: color_alias.$primary-color-600;
88
- path {
89
- fill: color_alias.$primary-color-600;
90
- }
91
- }
92
- }
66
+ @include mixins.svg-color(color_alias.$primary-color-600);
93
67
 
94
68
  &:hover {
95
69
  background: color_alias.$primary-color-50;
96
70
  }
97
71
  }
98
72
  }
73
+
74
+ &.secondary .collapsible-content {
75
+ padding: config.$space-3x config.$space-4x config.$space-6x;
76
+ border: 1px solid color_alias.$neutral-color-200;
77
+ border-top: none;
78
+ border-bottom-left-radius: config.$corner-radius-m;
79
+ border-bottom-right-radius: config.$corner-radius-m;
80
+ }
99
81
  }
100
82
 
101
83
  .collapsible[open] {
102
- .arrow {
84
+ .collapsible-arrow {
103
85
  transform: rotate(90deg);
104
86
  }
105
87
  }
@@ -1,11 +1,11 @@
1
1
  import './Collapsible.scss'
2
- import { classNames } from '../utils/classNames'
3
- import { Icon } from './Icon'
2
+ import type { ComponentPropsWithoutRef } from 'react'
3
+ import { classNames } from '../../utils/classNames'
4
+ import { Icon } from '../Icon'
4
5
 
5
- export type Variant = 'primary'
6
+ export type Variant = 'primary' | 'secondary'
6
7
 
7
- export interface CollapsibleProps
8
- extends React.ComponentPropsWithoutRef<'details'> {
8
+ export interface CollapsibleProps extends ComponentPropsWithoutRef<'details'> {
9
9
  title: string
10
10
  variant?: Variant
11
11
  name?: string
@@ -24,21 +24,21 @@ export function Collapsible({
24
24
  form = false,
25
25
  noHorizontalPadding = false,
26
26
  ...props
27
- }: CollapsibleProps): React.JSX.Element {
27
+ }: CollapsibleProps) {
28
28
  const cssClasses = classNames('collapsible', variant, className, {
29
29
  open: props.open,
30
30
  disabled: disabled,
31
31
  })
32
- const contentCssClasses = classNames('content', {
32
+ const contentCssClasses = classNames('collapsible-content', {
33
33
  'no-horizontal-padding': noHorizontalPadding,
34
34
  form: form,
35
35
  })
36
36
 
37
37
  return (
38
38
  <details name={name} className={cssClasses} aria-label={title} {...props}>
39
- <summary className="header">
40
- <Icon className="arrow" name="AngleRight" />
41
- <span className="title">{title}</span>
39
+ <summary className="collapsible-header">
40
+ <Icon className="collapsible-arrow" name="AngleRight" size="4" />
41
+ <span className="collapsible-title">{title}</span>
42
42
  </summary>
43
43
  <div className={contentCssClasses}>{children}</div>
44
44
  </details>
@@ -0,0 +1,5 @@
1
+ import { Collapsible } from './Collapsible'
2
+
3
+ export type { CollapsibleProps } from './Collapsible'
4
+
5
+ export { Collapsible }
@@ -29,6 +29,7 @@ export type DateRangePickerProps = {
29
29
  name?: string
30
30
  helpText?: string
31
31
  errors?: string[]
32
+ fullWidth?: boolean
32
33
  }
33
34
 
34
35
  export type DateRange = {
@@ -48,10 +49,12 @@ export function DateRangePicker({
48
49
  label = 'Date',
49
50
  errors,
50
51
  helpText,
52
+ fullWidth = false,
51
53
  }: DateRangePickerProps): JSX.Element {
52
54
  const cssClasses = classNames('date-picker', variant, className, {
53
55
  toggle: withInput,
54
56
  invalid: errors?.length,
57
+ 'full-width': fullWidth,
55
58
  })
56
59
  const helpTexts = buildHelpText(helpText, errors)
57
60
 
@@ -28,6 +28,7 @@ export type DateSinglePickerProps = {
28
28
  label?: string
29
29
  helpText?: string
30
30
  errors?: string[]
31
+ fullWidth?: boolean
31
32
  }
32
33
 
33
34
  export function DateSinglePicker({
@@ -42,6 +43,7 @@ export function DateSinglePicker({
42
43
  label = 'Date',
43
44
  errors,
44
45
  helpText,
46
+ fullWidth = false,
45
47
  }: DateSinglePickerProps): JSX.Element {
46
48
  const { isOpen, close, toggle } = useOpen(!withInput)
47
49
  const pickerRef = useRef(null)
@@ -51,6 +53,7 @@ export function DateSinglePicker({
51
53
  const cssClasses = classNames('date-picker', variant, className, {
52
54
  toggle: withInput,
53
55
  invalid: errors?.length,
56
+ 'full-width': fullWidth,
54
57
  })
55
58
 
56
59
  const [selected, setSelected] = useState<Date | undefined>(
@@ -17,16 +17,20 @@
17
17
  flex-direction: row;
18
18
  width: 100%;
19
19
  padding: config.$space-2x config.$space-3x config.$space-2x;
20
+
20
21
  input[type='date'] {
21
22
  min-width: -webkit-fill-available;
22
23
  }
24
+
23
25
  input[type='number'] {
24
26
  text-align: right;
25
27
  }
28
+
26
29
  input::placeholder,
27
30
  textarea::placeholder {
28
31
  @include typography.input-placeholder-text;
29
32
  }
33
+
30
34
  input,
31
35
  input:hover,
32
36
  input:focus,
@@ -52,10 +56,12 @@
52
56
  -moz-appearance: none;
53
57
  appearance: none;
54
58
  }
59
+
55
60
  .input-suffix {
56
61
  text-wrap: nowrap;
57
62
  color: color_alias.$neutral-color-600;
58
63
  }
64
+
59
65
  .icon {
60
66
  min-width: config.$icon-size-5x;
61
67
  width: config.$icon-size-5x;
@@ -72,6 +78,7 @@
72
78
  &.invalid .input-container {
73
79
  border: 1px solid color_alias.$error-color-1000;
74
80
  }
81
+
75
82
  .input-container {
76
83
  border-radius: config.$corner-radius-m;
77
84
  border: 1px solid color_alias.$neutral-color-600;
@@ -87,6 +94,7 @@
87
94
  &:has(textarea:disabled) {
88
95
  border: 1px solid color_alias.$neutral-color-400;
89
96
  background: color_alias.$neutral-color-50;
97
+
90
98
  input {
91
99
  color: color_alias.$neutral-color-400;
92
100
  }
@@ -96,9 +104,11 @@
96
104
  color: color_alias.$neutral-color-400;
97
105
  }
98
106
  }
107
+
99
108
  .icon {
100
109
  > svg {
101
110
  fill: color_alias.$neutral-color-400;
111
+
102
112
  path {
103
113
  fill: color_alias.$neutral-color-400;
104
114
  }
@@ -108,6 +118,7 @@
108
118
  .password-icon {
109
119
  > svg {
110
120
  fill: color_alias.$neutral-color-600;
121
+
111
122
  path {
112
123
  fill: color_alias.$neutral-color-600;
113
124
  }
@@ -115,20 +126,25 @@
115
126
  }
116
127
  }
117
128
  }
129
+
118
130
  &.file .input-container {
119
131
  padding: 0;
120
132
  border: transparent;
133
+
121
134
  &:has(input:focus) {
122
135
  border: transparent;
123
136
  }
137
+
124
138
  input {
125
139
  color: color_alias.$neutral-color-600;
140
+
126
141
  &::before {
127
142
  content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='12' fill='none'%3E%3Cpath d='M2.943 8.718h4.114V4.694H9.8L5 0 .2 4.694h2.743v4.024ZM5 1.898l1.488 1.455h-.802v4.023H4.314V3.353h-.802L5 1.898Zm-4.8 8.16h9.6V11.4H.2v-1.341Z' fill='%23EB004D'/%3E%3C/svg%3E");
128
143
  position: absolute;
129
144
  left: config.$space-3x;
130
145
  bottom: config.$space-2x;
131
146
  }
147
+
132
148
  &::file-selector-button {
133
149
  margin-right: config.$space-2x;
134
150
  padding-left: config.$space-3x + config.$icon-size-2x + config.$space-1x;
@@ -136,8 +152,23 @@
136
152
  }
137
153
  }
138
154
  }
155
+
139
156
  &.hidden {
140
157
  display: none;
141
158
  }
142
159
 
160
+ &.ellipsis {
161
+ input {
162
+ text-overflow: ellipsis;
163
+ overflow: hidden;
164
+ white-space: nowrap;
165
+ }
166
+ }
167
+
168
+ &.borderless {
169
+ .input-container {
170
+ padding-left: 0;
171
+ padding-right: 0;
172
+ }
173
+ }
143
174
  }
@@ -7,7 +7,7 @@ import type { IconType } from './Icon'
7
7
  import { Icon } from './Icon'
8
8
  import { Label } from './Label'
9
9
 
10
- export type InputVariant = 'primary'
10
+ export type InputVariant = 'primary' | 'borderless'
11
11
 
12
12
  export interface InputProps extends React.ComponentPropsWithRef<'input'> {
13
13
  label: string
@@ -20,6 +20,8 @@ export interface InputProps extends React.ComponentPropsWithRef<'input'> {
20
20
  suffix?: string
21
21
  errors?: string[]
22
22
  rightIcon?: IconType
23
+ fullWidth?: boolean
24
+ ellipsis?: boolean
23
25
  }
24
26
 
25
27
  export function Input({
@@ -37,6 +39,8 @@ export function Input({
37
39
  id,
38
40
  errors,
39
41
  rightIcon,
42
+ fullWidth = false,
43
+ ellipsis = false,
40
44
  ...props
41
45
  }: InputProps): React.JSX.Element {
42
46
  const identifier = id || name
@@ -62,6 +66,8 @@ export function Input({
62
66
  file: type === 'file',
63
67
  invalid: errors?.length,
64
68
  hidden: type === 'hidden',
69
+ 'full-width': fullWidth,
70
+ ellipsis: ellipsis,
65
71
  })}
66
72
  >
67
73
  {!hideLabel && (
@@ -0,0 +1,11 @@
1
+ @use '../../settings/config';
2
+
3
+ .input-with-button {
4
+ display: flex;
5
+ flex-wrap: nowrap;
6
+ gap: config.$space-2x;
7
+
8
+ > .button {
9
+ margin-top: 32px;
10
+ }
11
+ }
@@ -0,0 +1,10 @@
1
+ import './InputWithButton.scss'
2
+ import React from 'react'
3
+
4
+ interface InputWithButtonProps {
5
+ children: React.ReactNode
6
+ }
7
+
8
+ export function InputWithButton({ children }: InputWithButtonProps) {
9
+ return <div className="input-with-button">{children}</div>
10
+ }
@@ -0,0 +1,3 @@
1
+ import { InputWithButton } from './InputWithButton'
2
+
3
+ export { InputWithButton }
@@ -36,6 +36,22 @@ export function ModalDialog({
36
36
  }
37
37
  }, [])
38
38
 
39
+ useEffect(() => {
40
+ const dialog = dialogRef.current
41
+ if (!dialog) return
42
+
43
+ const handleCancel = (event: Event) => {
44
+ event.preventDefault()
45
+ onClose?.()
46
+ }
47
+
48
+ dialog.addEventListener('cancel', handleCancel)
49
+
50
+ return () => {
51
+ dialog.removeEventListener('cancel', handleCancel)
52
+ }
53
+ }, [onClose])
54
+
39
55
  useEffect(() => {
40
56
  if (isOpen) {
41
57
  dialogRef.current?.showModal()
@@ -46,6 +62,8 @@ export function ModalDialog({
46
62
 
47
63
  return (
48
64
  <dialog
65
+ role="dialog"
66
+ aria-modal="true"
49
67
  ref={dialogRef}
50
68
  className={classNames('modal', className, { 'modal-details': details })}
51
69
  onClick={handleClick}
@@ -30,6 +30,7 @@ export interface MultiselectProps extends InputPropsWithoutOnChange {
30
30
  onChange?: (value: string[]) => void
31
31
  isSearchable?: boolean
32
32
  searchLabel?: string
33
+ fullWidth?: boolean
33
34
  }
34
35
 
35
36
  export function Multiselect({
@@ -50,15 +51,23 @@ export function Multiselect({
50
51
  defaultValue = [],
51
52
  isSearchable = false,
52
53
  searchLabel = 'Search',
54
+ fullWidth = false,
53
55
  ...props
54
56
  }: MultiselectProps): React.JSX.Element {
55
57
  const { isOpen, close, toggle } = useOpen()
56
58
  const selectRef = useRef(null)
59
+ const selectTriggerRef = useRef<HTMLButtonElement | null>(null)
57
60
  useOutsideClick(selectRef, close)
58
61
  const [selectedOptions, setSelectedOptions] = useState<string[]>(defaultValue)
59
62
  const isInvalid = Boolean(errors?.length)
60
63
  const hasSelectedOptions = selectedOptions.length > 0
61
64
 
65
+ const handleClose = () => {
66
+ if (!isOpen) return
67
+ close()
68
+ selectTriggerRef?.current?.focus()
69
+ }
70
+
62
71
  function handleSelectOption({ id }: Option) {
63
72
  const isOptionSelected = selectedOptions.includes(id)
64
73
  const options = isOptionSelected
@@ -74,6 +83,7 @@ export function Multiselect({
74
83
  setSelectedOptions([])
75
84
  onChange([])
76
85
  }
86
+
77
87
  const identifier = id || name
78
88
  return (
79
89
  <div
@@ -81,6 +91,7 @@ export function Multiselect({
81
91
  disabled,
82
92
  filled: hasSelectedOptions,
83
93
  invalid: isInvalid,
94
+ 'full-width': fullWidth,
84
95
  })}
85
96
  ref={selectRef}
86
97
  >
@@ -99,6 +110,7 @@ export function Multiselect({
99
110
  onClick={toggle}
100
111
  onClear={handleClear}
101
112
  isEmpty={!hasSelectedOptions}
113
+ buttonRef={selectTriggerRef}
102
114
  >
103
115
  {hasSelectedOptions
104
116
  ? `${selectedOptions.length} ${selectedLabel}`
@@ -113,6 +125,7 @@ export function Multiselect({
113
125
  selectOption={handleSelectOption}
114
126
  isSearchable={isSearchable}
115
127
  searchLabel={searchLabel}
128
+ onClose={handleClose}
116
129
  />
117
130
  )}
118
131
  <HelpText helpText={helpText} errors={errors} />
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
  import './Popover.scss'
3
- import { useRef } from 'react'
3
+ import React, { useRef } from 'react'
4
4
  import { useOpen } from '../../hooks/useOpen'
5
5
  import { useOutsideClick } from '../../hooks/useOutsideClick'
6
6
  import { classNames } from '../../utils/classNames'
@@ -14,6 +14,7 @@ export interface QuantitySelectorProps extends Omit<InputProps, 'type'> {
14
14
  hideLabel?: boolean
15
15
  id?: string
16
16
  variant?: Variant
17
+ fullWidth?: boolean
17
18
  }
18
19
 
19
20
  export function QuantitySelector({
@@ -24,6 +25,7 @@ export function QuantitySelector({
24
25
  disabled,
25
26
  hideLabel = false,
26
27
  variant = 'primary',
28
+ fullWidth = false,
27
29
  ...props
28
30
  }: QuantitySelectorProps): React.JSX.Element {
29
31
  const inputRef = useRef<HTMLInputElement>(null)
@@ -41,7 +43,11 @@ export function QuantitySelector({
41
43
  }
42
44
  }
43
45
  return (
44
- <div className={classNames('quantity-selector-group', variant, className)}>
46
+ <div
47
+ className={classNames('quantity-selector-group', variant, className, {
48
+ 'full-width': fullWidth,
49
+ })}
50
+ >
45
51
  {!hideLabel && (
46
52
  <Label required={props.required} htmlFor={id}>
47
53
  {label}