agroptima-design-system 0.35.0-beta.13 → 0.35.0-beta.14

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroptima-design-system",
3
- "version": "0.35.0-beta.13",
3
+ "version": "0.35.0-beta.14",
4
4
  "scripts": {
5
5
  "dev": "npm run storybook",
6
6
  "storybook": "storybook dev -p 6006 --ci",
@@ -56,11 +56,17 @@ export function Multiselect({
56
56
  }: MultiselectProps): React.JSX.Element {
57
57
  const { isOpen, close, toggle } = useOpen()
58
58
  const selectRef = useRef(null)
59
+ const selectTriggerRef = useRef<HTMLButtonElement | null>(null)
59
60
  useOutsideClick(selectRef, close)
60
61
  const [selectedOptions, setSelectedOptions] = useState<string[]>(defaultValue)
61
62
  const isInvalid = Boolean(errors?.length)
62
63
  const hasSelectedOptions = selectedOptions.length > 0
63
64
 
65
+ const handleClose = () => {
66
+ close()
67
+ selectTriggerRef?.current?.focus()
68
+ }
69
+
64
70
  function handleSelectOption({ id }: Option) {
65
71
  const isOptionSelected = selectedOptions.includes(id)
66
72
  const options = isOptionSelected
@@ -76,6 +82,7 @@ export function Multiselect({
76
82
  setSelectedOptions([])
77
83
  onChange([])
78
84
  }
85
+
79
86
  const identifier = id || name
80
87
  return (
81
88
  <div
@@ -102,6 +109,7 @@ export function Multiselect({
102
109
  onClick={toggle}
103
110
  onClear={handleClear}
104
111
  isEmpty={!hasSelectedOptions}
112
+ buttonRef={selectTriggerRef}
105
113
  >
106
114
  {hasSelectedOptions
107
115
  ? `${selectedOptions.length} ${selectedLabel}`
@@ -116,6 +124,7 @@ export function Multiselect({
116
124
  selectOption={handleSelectOption}
117
125
  isSearchable={isSearchable}
118
126
  searchLabel={searchLabel}
127
+ onClose={handleClose}
119
128
  />
120
129
  )}
121
130
  <HelpText helpText={helpText} errors={errors} />
@@ -66,8 +66,7 @@ $elements-space: config.$space-2x;
66
66
  background: color_alias.$neutral-white;
67
67
  @include mixins.icon-color(color_alias.$primary-color-600);
68
68
 
69
- &:focus {
70
- outline: color_alias.$primary-color-600;
69
+ &:has(.select:focus) {
71
70
  border: 1px solid color_alias.$primary-color-600;
72
71
  }
73
72
  }
@@ -75,9 +74,9 @@ $elements-space: config.$space-2x;
75
74
  .select-options {
76
75
  border-radius: config.$corner-radius-xxs;
77
76
  background: color_alias.$neutral-white;
78
- box-shadow: 0px 9px 28px 8px rgba(0, 0, 0, 0.05),
79
- 0px 6px 16px 0px rgba(0, 0, 0, 0.08),
80
- 0px 3px 6px -4px rgba(0, 0, 0, 0.12);
77
+ box-shadow: 0 9px 28px 8px rgba(0, 0, 0, 0.05),
78
+ 0 6px 16px 0 rgba(0, 0, 0, 0.08),
79
+ 0 3px 6px -4px rgba(0, 0, 0, 0.12);
81
80
 
82
81
  .option {
83
82
  background: color_alias.$neutral-white;
@@ -61,7 +61,12 @@ export function Select({
61
61
  const isEmpty = selectedOption.id === EMPTY_OPTION.id
62
62
  const isInvalid = Boolean(errors?.length)
63
63
  const selectRef = useRef(null)
64
- useOutsideClick(selectRef, close)
64
+ const selectTriggerRef = useRef<HTMLButtonElement | null>(null)
65
+ const handleClose = () => {
66
+ close()
67
+ selectTriggerRef?.current?.focus()
68
+ }
69
+ useOutsideClick(selectRef, handleClose)
65
70
 
66
71
  function handleSelectOption(option: Option) {
67
72
  setSelectedOption(option)
@@ -93,6 +98,7 @@ export function Select({
93
98
 
94
99
  <SelectTrigger
95
100
  id={identifier}
101
+ buttonRef={selectTriggerRef}
96
102
  label={label}
97
103
  accessibilityLabel={accessibilityLabel}
98
104
  invalid={isInvalid}
@@ -110,9 +116,10 @@ export function Select({
110
116
  options={options}
111
117
  selectedOptions={[selectedOption.id]}
112
118
  selectOption={handleSelectOption}
113
- onClick={close}
119
+ onClick={handleClose}
114
120
  isSearchable={isSearchable}
115
121
  searchLabel={searchLabel}
122
+ onClose={handleClose}
116
123
  />
117
124
  )}
118
125
  <HelpText helpText={helpText} errors={errors} />
@@ -6,15 +6,29 @@ interface OptionProps {
6
6
  option: Option
7
7
  multiple?: boolean
8
8
  isSelected: boolean
9
- onClick: (option: Option) => void
9
+ onSelectOption: (option: Option) => void
10
+ onClose: () => void
10
11
  }
11
12
 
13
+ const ENTER_KEY = 'Enter'
14
+ const SPACE_KEY = ' '
15
+
12
16
  export function SelectItem({
13
17
  option,
14
18
  isSelected,
15
- onClick,
19
+ onSelectOption,
16
20
  multiple,
21
+ onClose,
17
22
  }: OptionProps) {
23
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLLIElement>) => {
24
+ if (e.key === ENTER_KEY || e.key === SPACE_KEY) {
25
+ e.preventDefault()
26
+ onSelectOption(option)
27
+ if (!multiple) {
28
+ onClose()
29
+ }
30
+ }
31
+ }
18
32
  return (
19
33
  <li
20
34
  className="option"
@@ -22,7 +36,8 @@ export function SelectItem({
22
36
  role="option"
23
37
  aria-selected={isSelected}
24
38
  data-option={option}
25
- onClick={() => onClick(option)}
39
+ onClick={() => onSelectOption(option)}
40
+ onKeyDown={handleKeyDown}
26
41
  >
27
42
  {multiple && <CheckboxIcon selected={isSelected} />}
28
43
  {option.label}
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import React, { useEffect } from 'react'
2
2
  import { useSearch } from '../../hooks/useSearch'
3
3
  import { Input } from '../Input'
4
4
  import type { Option } from './Select'
@@ -13,8 +13,11 @@ interface OptionListProps {
13
13
  onClick?: () => void
14
14
  isSearchable: boolean
15
15
  searchLabel: string
16
+ onClose: () => void
16
17
  }
17
18
 
19
+ const ESCAPE_KEY = 'Escape'
20
+
18
21
  export function SelectItems({
19
22
  id,
20
23
  options,
@@ -24,8 +27,21 @@ export function SelectItems({
24
27
  onClick,
25
28
  isSearchable,
26
29
  searchLabel,
30
+ onClose,
27
31
  }: OptionListProps) {
28
32
  const { findItems, search } = useSearch(options, 'label')
33
+ useEffect(() => {
34
+ const handleKeyDown = (event: KeyboardEvent) => {
35
+ if (event.key === ESCAPE_KEY) {
36
+ onClose()
37
+ }
38
+ }
39
+ document.addEventListener('keydown', handleKeyDown)
40
+ return () => {
41
+ document.removeEventListener('keydown', handleKeyDown)
42
+ }
43
+ }, [onClose])
44
+
29
45
  return (
30
46
  <div className="select-options-container">
31
47
  <div className="select-options" id={id}>
@@ -47,7 +63,8 @@ export function SelectItems({
47
63
  key={option.id}
48
64
  option={option}
49
65
  isSelected={selectedOptions.includes(option.id)}
50
- onClick={selectOption}
66
+ onSelectOption={selectOption}
67
+ onClose={onClose}
51
68
  />
52
69
  ))}
53
70
  </ul>
@@ -13,6 +13,7 @@ export interface SelectTriggerProps {
13
13
  onClick: () => void
14
14
  onClear: (event: React.MouseEvent) => void
15
15
  children: React.ReactNode
16
+ buttonRef: React.RefObject<HTMLButtonElement | null>
16
17
  }
17
18
 
18
19
  export function SelectTrigger({
@@ -26,6 +27,7 @@ export function SelectTrigger({
26
27
  onClear,
27
28
  isEmpty,
28
29
  children,
30
+ buttonRef,
29
31
  }: SelectTriggerProps) {
30
32
  const handleClear = (event: React.MouseEvent) => {
31
33
  if (disabled) return
@@ -35,6 +37,7 @@ export function SelectTrigger({
35
37
  <div className="select-container">
36
38
  <button
37
39
  id={id}
40
+ ref={buttonRef}
38
41
  type="button"
39
42
  role="combobox"
40
43
  className="select"
@@ -56,6 +59,7 @@ export function SelectTrigger({
56
59
  </button>
57
60
  <IconButton
58
61
  type="button"
62
+ tabIndex={-1}
59
63
  size="3"
60
64
  icon="Close"
61
65
  className="clear-button"
@@ -15,6 +15,8 @@ import { Meta } from "@storybook/blocks";
15
15
  * Add `classic-view` and `new-view` to icons
16
16
  * Add InputWithButton component
17
17
  * Fix close modal when press `Escape` key
18
+ * Add focus style to Select and MultiSelect components
19
+ * Focus in Select and MultiSelect components after close
18
20
 
19
21
  ## 0.34.9
20
22
 
@@ -166,4 +166,21 @@ describe('Select', () => {
166
166
  playstation5.label,
167
167
  )
168
168
  })
169
+ it('closes when Escape key is pressed', async () => {
170
+ const user = userEvent.setup()
171
+ render(
172
+ <Select
173
+ label="Videogames"
174
+ name="select-videogames"
175
+ isSearchable
176
+ options={OPTIONS}
177
+ />,
178
+ )
179
+
180
+ await user.click(screen.getByLabelText('Videogames'))
181
+ expect(screen.getByText(playstation5.label)).toBeInTheDocument()
182
+ await user.keyboard('{Escape}')
183
+
184
+ expect(screen.queryByText(playstation5.label)).not.toBeInTheDocument()
185
+ })
169
186
  })