@faststore/components 3.0.135 → 3.0.141

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 (65) hide show
  1. package/dist/cjs/atoms/Button/index.d.ts +1 -1
  2. package/dist/cjs/molecules/Dropdown/Dropdown.d.ts +1 -1
  3. package/dist/cjs/molecules/Dropdown/Dropdown.js +20 -16
  4. package/dist/cjs/molecules/Dropdown/Dropdown.js.map +1 -1
  5. package/dist/cjs/molecules/Dropdown/DropdownButton.d.ts +23 -5
  6. package/dist/cjs/molecules/Dropdown/DropdownButton.js +7 -7
  7. package/dist/cjs/molecules/Dropdown/DropdownButton.js.map +1 -1
  8. package/dist/cjs/molecules/Dropdown/DropdownItem.d.ts +9 -0
  9. package/dist/cjs/molecules/Dropdown/DropdownItem.js +8 -23
  10. package/dist/cjs/molecules/Dropdown/DropdownItem.js.map +1 -1
  11. package/dist/cjs/molecules/Dropdown/DropdownMenu.d.ts +7 -3
  12. package/dist/cjs/molecules/Dropdown/DropdownMenu.js +39 -11
  13. package/dist/cjs/molecules/Dropdown/DropdownMenu.js.map +1 -1
  14. package/dist/cjs/molecules/Dropdown/contexts/DropdownContext.d.ts +8 -8
  15. package/dist/cjs/molecules/Dropdown/contexts/DropdownContext.js +1 -1
  16. package/dist/cjs/molecules/Dropdown/contexts/DropdownContext.js.map +1 -1
  17. package/dist/cjs/molecules/Dropdown/hooks/useDropdown.d.ts +1 -1
  18. package/dist/cjs/molecules/Dropdown/hooks/useDropdown.js.map +1 -1
  19. package/dist/cjs/molecules/Dropdown/hooks/useDropdownItem.d.ts +15 -0
  20. package/dist/cjs/molecules/Dropdown/hooks/useDropdownItem.js +37 -0
  21. package/dist/cjs/molecules/Dropdown/hooks/useDropdownItem.js.map +1 -0
  22. package/dist/cjs/molecules/Dropdown/hooks/useDropdownPosition.d.ts +4 -2
  23. package/dist/cjs/molecules/Dropdown/hooks/useDropdownPosition.js +55 -18
  24. package/dist/cjs/molecules/Dropdown/hooks/useDropdownPosition.js.map +1 -1
  25. package/dist/cjs/molecules/Dropdown/hooks/useDropdownTrigger.d.ts +13 -0
  26. package/dist/cjs/molecules/Dropdown/hooks/useDropdownTrigger.js +20 -0
  27. package/dist/cjs/molecules/Dropdown/hooks/useDropdownTrigger.js.map +1 -0
  28. package/dist/esm/atoms/Button/index.d.ts +1 -1
  29. package/dist/esm/molecules/Dropdown/Dropdown.d.ts +1 -1
  30. package/dist/esm/molecules/Dropdown/Dropdown.js +20 -16
  31. package/dist/esm/molecules/Dropdown/Dropdown.js.map +1 -1
  32. package/dist/esm/molecules/Dropdown/DropdownButton.d.ts +23 -5
  33. package/dist/esm/molecules/Dropdown/DropdownButton.js +8 -8
  34. package/dist/esm/molecules/Dropdown/DropdownButton.js.map +1 -1
  35. package/dist/esm/molecules/Dropdown/DropdownItem.d.ts +9 -0
  36. package/dist/esm/molecules/Dropdown/DropdownItem.js +9 -24
  37. package/dist/esm/molecules/Dropdown/DropdownItem.js.map +1 -1
  38. package/dist/esm/molecules/Dropdown/DropdownMenu.d.ts +7 -3
  39. package/dist/esm/molecules/Dropdown/DropdownMenu.js +39 -11
  40. package/dist/esm/molecules/Dropdown/DropdownMenu.js.map +1 -1
  41. package/dist/esm/molecules/Dropdown/contexts/DropdownContext.d.ts +8 -8
  42. package/dist/esm/molecules/Dropdown/contexts/DropdownContext.js +1 -1
  43. package/dist/esm/molecules/Dropdown/contexts/DropdownContext.js.map +1 -1
  44. package/dist/esm/molecules/Dropdown/hooks/useDropdown.d.ts +1 -1
  45. package/dist/esm/molecules/Dropdown/hooks/useDropdown.js.map +1 -1
  46. package/dist/esm/molecules/Dropdown/hooks/useDropdownItem.d.ts +15 -0
  47. package/dist/esm/molecules/Dropdown/hooks/useDropdownItem.js +33 -0
  48. package/dist/esm/molecules/Dropdown/hooks/useDropdownItem.js.map +1 -0
  49. package/dist/esm/molecules/Dropdown/hooks/useDropdownPosition.d.ts +4 -2
  50. package/dist/esm/molecules/Dropdown/hooks/useDropdownPosition.js +55 -18
  51. package/dist/esm/molecules/Dropdown/hooks/useDropdownPosition.js.map +1 -1
  52. package/dist/esm/molecules/Dropdown/hooks/useDropdownTrigger.d.ts +13 -0
  53. package/dist/esm/molecules/Dropdown/hooks/useDropdownTrigger.js +16 -0
  54. package/dist/esm/molecules/Dropdown/hooks/useDropdownTrigger.js.map +1 -0
  55. package/package.json +2 -2
  56. package/src/atoms/Button/index.ts +1 -1
  57. package/src/molecules/Dropdown/Dropdown.tsx +27 -18
  58. package/src/molecules/Dropdown/DropdownButton.tsx +45 -32
  59. package/src/molecules/Dropdown/DropdownItem.tsx +39 -47
  60. package/src/molecules/Dropdown/DropdownMenu.tsx +58 -18
  61. package/src/molecules/Dropdown/contexts/DropdownContext.ts +11 -9
  62. package/src/molecules/Dropdown/hooks/useDropdown.ts +3 -3
  63. package/src/molecules/Dropdown/hooks/useDropdownItem.ts +56 -0
  64. package/src/molecules/Dropdown/hooks/useDropdownPosition.ts +62 -19
  65. package/src/molecules/Dropdown/hooks/useDropdownTrigger.ts +26 -0
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ type UseDropdownTriggerProps<T extends HTMLElement = HTMLElement> = {
3
+ triggerRef: React.ForwardedRef<T>;
4
+ label?: string;
5
+ };
6
+ export declare const useDropdownTrigger: <T extends HTMLElement = HTMLElement>({ triggerRef, }: UseDropdownTriggerProps<T>) => {
7
+ onClick: (() => void) | undefined;
8
+ ref: ((ref: T | null) => void) | undefined;
9
+ 'aria-expanded': boolean;
10
+ 'aria-controls': string;
11
+ 'aria-haspopup': "menu";
12
+ };
13
+ export {};
@@ -0,0 +1,16 @@
1
+ import { useDropdown } from './useDropdown';
2
+ import { useImperativeHandle } from 'react';
3
+ export const useDropdownTrigger = ({ triggerRef, }) => {
4
+ const { toggle, dropdownTriggerRef, addDropdownTriggerRef, isOpen, id } = useDropdown();
5
+ useImperativeHandle(triggerRef, () => dropdownTriggerRef.current, [
6
+ dropdownTriggerRef,
7
+ ]);
8
+ return {
9
+ onClick: toggle,
10
+ ref: addDropdownTriggerRef,
11
+ 'aria-expanded': isOpen,
12
+ 'aria-controls': id,
13
+ 'aria-haspopup': 'menu',
14
+ };
15
+ };
16
+ //# sourceMappingURL=useDropdownTrigger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDropdownTrigger.js","sourceRoot":"","sources":["../../../../../src/molecules/Dropdown/hooks/useDropdownTrigger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAc,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAA;AAOlD,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAsC,EACtE,UAAU,GACiB,EAAE,EAAE;IAC/B,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,EAAE,EAAE,EAAE,GACrE,WAAW,EAAK,CAAA;IAElB,mBAAmB,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,kBAAmB,CAAC,OAAQ,EAAE;QAClE,kBAAkB;KACnB,CAAC,CAAA;IAEF,OAAO;QACL,OAAO,EAAE,MAAM;QACf,GAAG,EAAE,qBAAqB;QAC1B,eAAe,EAAE,MAAM;QACvB,eAAe,EAAE,EAAE;QACnB,eAAe,EAAE,MAAe;KACjC,CAAA;AACH,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/components",
3
- "version": "3.0.135",
3
+ "version": "3.0.141",
4
4
  "main": "dist/cjs/index.js",
5
5
  "module": "dist/esm/index.js",
6
6
  "typings": "dist/esm/index.d.ts",
@@ -52,5 +52,5 @@
52
52
  "volta": {
53
53
  "extends": "../../package.json"
54
54
  },
55
- "gitHead": "611283638dc01f55a65d0b5fd01e4a0163d53f4b"
55
+ "gitHead": "f972d71664e32d3e5b3d74a140e966cd34d73fd7"
56
56
  }
@@ -1,2 +1,2 @@
1
1
  export { default } from './Button'
2
- export type { ButtonProps } from './Button'
2
+ export type { ButtonProps, Variant as ButtonVariant, Size as ButtonSize, IconPosition as ButtonIconPosition } from './Button'
@@ -20,38 +20,47 @@ export interface DropdownProps {
20
20
 
21
21
  const Dropdown = ({
22
22
  children,
23
- isOpen: isOpenDefault = false,
23
+ isOpen: isOpenControlled,
24
24
  onDismiss,
25
25
  id = 'fs-dropdown',
26
26
  }: PropsWithChildren<DropdownProps>) => {
27
- const [isOpen, setIsOpen] = useState(isOpenDefault)
28
- const dropdownItemsRef = useRef<HTMLButtonElement[]>([])
27
+ const [isOpenInternal, setIsOpenInternal] = useState(false)
28
+ const dropdownItemsRef = useRef<HTMLElement[]>([])
29
29
  const selectedDropdownItemIndexRef = useRef(0)
30
- const dropdownButtonRef = useRef<HTMLButtonElement>(null)
30
+ const dropdownTriggerRef = useRef<HTMLElement | null>(null)
31
+
32
+ const isOpen = isOpenControlled ?? isOpenInternal
31
33
 
32
34
  const close = useCallback(() => {
33
- setIsOpen(false)
35
+ setIsOpenInternal(false)
34
36
  onDismiss?.()
35
37
  }, [onDismiss])
36
38
 
37
39
  const open = () => {
38
- setIsOpen(true)
40
+ setIsOpenInternal(true)
39
41
  }
40
42
 
41
43
  const toggle = useCallback(() => {
42
- setIsOpen((old) => {
43
- if (old) {
44
+ setIsOpenInternal((currentIsOpen) => {
45
+ if (currentIsOpen) {
44
46
  onDismiss?.()
45
- dropdownButtonRef.current?.focus()
47
+ dropdownTriggerRef.current?.focus()
46
48
  }
47
49
 
48
- return !old
50
+ return !currentIsOpen
49
51
  })
50
52
  }, [onDismiss])
51
53
 
54
+ const addDropdownTriggerRef = useCallback(
55
+ <T extends HTMLElement = HTMLElement>(ref: T) => {
56
+ dropdownTriggerRef.current = ref
57
+ },
58
+ []
59
+ )
60
+
52
61
  useEffect(() => {
53
- setIsOpen(isOpenDefault)
54
- }, [isOpenDefault])
62
+ setIsOpenInternal(isOpenControlled ?? false)
63
+ }, [isOpenControlled])
55
64
 
56
65
  useEffect(() => {
57
66
  isOpen && dropdownItemsRef?.current[0]?.focus()
@@ -61,8 +70,8 @@ const Dropdown = ({
61
70
  let firstClick = true
62
71
 
63
72
  const event = (e: MouseEvent) => {
64
- const someItemWasClicked = dropdownItemsRef?.current.some(
65
- (item) => e.target === item
73
+ const wasSomeItemClicked = dropdownItemsRef?.current.some(
74
+ (item) => e.target === item || item.contains(e.target as Node)
66
75
  )
67
76
 
68
77
  if (firstClick) {
@@ -71,7 +80,7 @@ const Dropdown = ({
71
80
  return
72
81
  }
73
82
 
74
- !someItemWasClicked && close()
83
+ !wasSomeItemClicked && close()
75
84
  }
76
85
 
77
86
  if (isOpen) {
@@ -91,13 +100,13 @@ const Dropdown = ({
91
100
  close,
92
101
  open,
93
102
  toggle,
94
- dropdownButtonRef,
95
- onDismiss,
103
+ dropdownTriggerRef,
104
+ addDropdownTriggerRef,
96
105
  selectedDropdownItemIndexRef,
97
106
  dropdownItemsRef,
98
107
  id,
99
108
  }
100
- }, [close, id, isOpen, onDismiss, toggle])
109
+ }, [isOpen, close, toggle, addDropdownTriggerRef, id])
101
110
 
102
111
  return (
103
112
  <DropdownContext.Provider value={value}>
@@ -1,51 +1,64 @@
1
- import React, { forwardRef, useImperativeHandle, AriaAttributes } from 'react'
2
- import Button, { ButtonProps } from '../../atoms/Button'
3
-
4
- import { useDropdown } from './hooks/useDropdown'
1
+ import React, { cloneElement, forwardRef, ReactNode } from 'react'
2
+ import Button, { ButtonProps, ButtonIconPosition } from '../../atoms/Button'
3
+ import { useDropdownTrigger } from './hooks/useDropdownTrigger'
5
4
 
6
5
  export interface DropdownButtonProps
7
- extends Omit<ButtonProps, 'variant' | 'inverse'> {
6
+ extends Omit<ButtonProps, 'variant' | 'inverse' | 'icon' | 'iconPosition'> {
8
7
  /**
9
8
  * ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
10
9
  */
11
10
  testId?: string
12
11
  /**
13
- * For accessibility purposes, add an ARIA label to the element when it doesn't have a label.
12
+ * Replace the default rendered element with the provided child element, merging their props and behavior.
13
+ */
14
+ asChild?: boolean
15
+ /**
16
+ * Boolean that represents a loading state.
17
+ */
18
+ loading?: boolean
19
+ /**
20
+ * Specifies a label for loading state.
21
+ */
22
+ loadingLabel?: string
23
+ /**
24
+ * @deprecated
25
+ * A React component that will be rendered as an icon.
26
+ */
27
+ icon?: ReactNode
28
+ /**
29
+ * @deprecated
30
+ * Specifies where the icon should be positioned
14
31
  */
15
- 'aria-label'?: AriaAttributes['aria-label']
32
+ iconPosition?: ButtonIconPosition
16
33
  }
17
34
 
18
35
  const DropdownButton = forwardRef<HTMLButtonElement, DropdownButtonProps>(
19
36
  function DropdownButton(
20
- {
21
- testId = 'fs-dropdown-button',
22
- 'aria-label': ariaLabel,
23
- children,
24
- ...otherProps
25
- },
26
- ref
37
+ { testId = 'fs-dropdown-button', children, asChild = false, ...otherProps },
38
+ triggerRef
27
39
  ) {
28
- const { toggle, dropdownButtonRef, isOpen, id } = useDropdown()
40
+ const triggerProps = useDropdownTrigger({ triggerRef })
29
41
 
30
- useImperativeHandle(ref, () => dropdownButtonRef!.current!, [
31
- dropdownButtonRef,
32
- ])
42
+ const asChildrenTrigger = React.isValidElement(children)
43
+ ? cloneElement(children, { ...triggerProps, ...children.props })
44
+ : children
33
45
 
34
46
  return (
35
- <Button
36
- data-fs-dropdown-button
37
- onClick={toggle}
38
- data-testid={testId}
39
- ref={dropdownButtonRef}
40
- aria-label={ariaLabel}
41
- aria-expanded={isOpen}
42
- aria-haspopup="menu"
43
- aria-controls={id}
44
- variant="tertiary"
45
- {...otherProps}
46
- >
47
- {children}
48
- </Button>
47
+ <>
48
+ {asChild ? (
49
+ asChildrenTrigger
50
+ ) : (
51
+ <Button
52
+ data-fs-dropdown-button
53
+ data-testid={testId}
54
+ variant="tertiary"
55
+ {...triggerProps}
56
+ {...otherProps}
57
+ >
58
+ {children}
59
+ </Button>
60
+ )}
61
+ </>
49
62
  )
50
63
  }
51
64
  )
@@ -1,7 +1,7 @@
1
1
  import type { ButtonHTMLAttributes, ReactNode } from 'react'
2
- import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react'
2
+ import React, { cloneElement, forwardRef } from 'react'
3
3
 
4
- import { useDropdown } from './hooks/useDropdown'
4
+ import { useDropdownItem } from './hooks/useDropdownItem'
5
5
 
6
6
  export interface DropdownItemProps
7
7
  extends ButtonHTMLAttributes<HTMLButtonElement> {
@@ -10,63 +10,55 @@ export interface DropdownItemProps
10
10
  */
11
11
  testId?: string
12
12
  /**
13
+ * @deprecated
13
14
  * A React component that will be rendered as an icon.
14
15
  */
15
16
  icon?: ReactNode
17
+ /**
18
+ * Replace the default rendered element with the one passed as a child, merging their props and behavior.
19
+ * */
20
+ asChild?: boolean
21
+ /**
22
+ * Emit onDismiss event when the component is clicked.
23
+ */
24
+ dismissOnClick?: boolean
16
25
  }
17
26
 
18
27
  const DropdownItem = forwardRef<HTMLButtonElement, DropdownItemProps>(
19
28
  function Button(
20
- { children, icon, onClick, testId = 'fs-dropdown-item', ...otherProps },
29
+ {
30
+ children,
31
+ asChild,
32
+ icon,
33
+ onClick,
34
+ dismissOnClick = true,
35
+ testId = 'fs-dropdown-item',
36
+ ...otherProps
37
+ },
21
38
  ref
22
39
  ) {
23
- const { dropdownItemsRef, selectedDropdownItemIndexRef, close } =
24
- useDropdown()
25
-
26
- const [dropdownItemIndex, setDropdownItemIndex] = useState(0)
27
- const dropdownItemRef = useRef<HTMLButtonElement>()
28
-
29
- const addToRefs = (el: HTMLButtonElement) => {
30
- if (el && !dropdownItemsRef?.current.includes(el)) {
31
- dropdownItemsRef?.current.push(el)
32
- setDropdownItemIndex(
33
- dropdownItemsRef?.current.findIndex((element) => element === el) ?? 0
34
- )
35
- }
36
-
37
- dropdownItemRef.current = el
38
- }
39
-
40
- const onFocusItem = () => {
41
- selectedDropdownItemIndexRef!.current = dropdownItemIndex
42
- dropdownItemsRef?.current[selectedDropdownItemIndexRef!.current]?.focus()
43
- }
44
-
45
- const handleOnClickItem = (
46
- event: React.MouseEvent<HTMLButtonElement, MouseEvent>
47
- ) => {
48
- onClick?.(event)
49
- close?.()
50
- }
40
+ const itemProps = useDropdownItem({ ref, onClick, dismissOnClick })
51
41
 
52
- useImperativeHandle(ref, () => dropdownItemRef.current!, [])
42
+ const asChildrenItem = React.isValidElement(children)
43
+ ? cloneElement(children, { ...itemProps, ...children.props })
44
+ : children
53
45
 
54
46
  return (
55
- <button
56
- data-fs-dropdown-item
57
- data-testid={testId}
58
- ref={addToRefs}
59
- onFocus={onFocusItem}
60
- onMouseEnter={onFocusItem}
61
- onClick={handleOnClickItem}
62
- role="menuitem"
63
- tabIndex={-1}
64
- data-index={dropdownItemIndex}
65
- {...otherProps}
66
- >
67
- {!!icon && icon}
68
- {children}
69
- </button>
47
+ <>
48
+ {asChild ? (
49
+ asChildrenItem
50
+ ) : (
51
+ <button
52
+ data-fs-dropdown-item
53
+ data-testid={testId}
54
+ {...itemProps}
55
+ {...otherProps}
56
+ >
57
+ {!!icon && icon}
58
+ {children}
59
+ </button>
60
+ )}
61
+ </>
70
62
  )
71
63
  }
72
64
  )
@@ -22,17 +22,19 @@ export interface DropdownMenuProps extends ModalContentProps {
22
22
  * @see aria-labelledby https://www.w3.org/TR/wai-aria-1.1/#aria-labelledby
23
23
  */
24
24
  'aria-labelledby'?: AriaAttributes['aria-label']
25
-
26
25
  /**
27
26
  * This function is called whenever the user hits "Escape" or clicks outside
28
27
  * the dialog.
29
28
  */
30
29
  onDismiss?: (event: MouseEvent | KeyboardEvent) => void
31
-
32
- /**
30
+ /**
33
31
  * Specifies the size variant.
34
32
  */
35
33
  size?: 'small' | 'regular'
34
+ /**
35
+ * Alignment for the dropdown
36
+ */
37
+ align?: 'left' | 'right' | 'center'
36
38
 
37
39
  children: ReactNode[] | ReactNode
38
40
  }
@@ -47,13 +49,20 @@ const DropdownMenu = ({
47
49
  children,
48
50
  testId = 'fs-dropdown-menu',
49
51
  size = 'regular',
52
+ align = 'left',
50
53
  style,
51
54
  ...otherProps
52
55
  }: PropsWithChildren<DropdownMenuProps>) => {
53
- const { isOpen, close, dropdownItemsRef, selectedDropdownItemIndexRef, dropdownButtonRef, id } =
54
- useDropdown()
56
+ const {
57
+ isOpen,
58
+ close,
59
+ dropdownItemsRef,
60
+ selectedDropdownItemIndexRef,
61
+ dropdownTriggerRef,
62
+ id,
63
+ } = useDropdown()
55
64
 
56
- const dropdownPosition = useDropdownPosition()
65
+ const { loading: loadingPosition, ...dropdownPosition } = useDropdownPosition(align)
57
66
 
58
67
  const childrenLength = React.Children.toArray(children).length
59
68
 
@@ -89,25 +98,56 @@ const DropdownMenu = ({
89
98
 
90
99
  const handleEscapePress = () => {
91
100
  close?.()
92
- dropdownButtonRef?.current?.focus()
101
+ dropdownTriggerRef?.current?.focus()
93
102
  }
94
103
 
104
+ const handleKeyNavigatePress = (key: string) => {
105
+ const dropdownItems = dropdownItemsRef?.current ?? [];
106
+ const selectedIndex = selectedDropdownItemIndexRef!.current;
107
+
108
+ const rearrangedDropdownItems = [
109
+ ...dropdownItems.slice(selectedIndex + 1),
110
+ ...dropdownItems.slice(0, selectedIndex + 1),
111
+ ];
112
+
113
+ const matchItem = rearrangedDropdownItems.find(
114
+ (item) => item.textContent?.[0]?.toLowerCase() === key.toLowerCase()
115
+ );
116
+
117
+ if (matchItem) {
118
+ selectedDropdownItemIndexRef!.current = dropdownItems.indexOf(matchItem);
119
+ matchItem.focus();
120
+ }
121
+ };
122
+
123
+
95
124
  const handleBackdropKeyDown = (event: KeyboardEvent) => {
96
- if (event.defaultPrevented || event.key === 'Enter') {
125
+ if (event.defaultPrevented || event.key === 'Enter' || event.key === ' ') {
97
126
  return
98
127
  }
99
128
 
100
129
  event.preventDefault()
101
130
 
102
- event.key === 'Escape' && handleEscapePress()
103
-
104
- event.key === 'ArrowDown' && handleDownPress()
105
-
106
- event.key === 'ArrowUp' && handleUpPress()
107
-
108
- event.key === 'Home' && handleHomePress()
109
-
110
- event.key === 'End' && handleEndPress()
131
+ switch (event.key) {
132
+ case 'Escape':
133
+ handleEscapePress()
134
+ break
135
+ case 'ArrowDown':
136
+ handleDownPress()
137
+ break
138
+ case 'ArrowUp':
139
+ handleUpPress()
140
+ break
141
+ case 'Home':
142
+ handleHomePress()
143
+ break
144
+ case 'End':
145
+ handleEndPress()
146
+ break
147
+ default:
148
+ handleKeyNavigatePress(event.key)
149
+ break
150
+ }
111
151
 
112
152
  event.stopPropagation()
113
153
  }
@@ -118,7 +158,7 @@ const DropdownMenu = ({
118
158
  return null
119
159
  }
120
160
 
121
- return isOpen
161
+ return (isOpen && !loadingPosition)
122
162
  ? createPortal(
123
163
  <div
124
164
  role="presentation"
@@ -1,6 +1,9 @@
1
1
  import { createContext } from 'react'
2
2
 
3
- export type DropdownContextState = {
3
+ export type DropdownContextState<
4
+ T extends HTMLElement = HTMLElement,
5
+ E extends HTMLElement = HTMLElement,
6
+ > = {
4
7
  /**
5
8
  * Control de Dropdown state as Opened (true) or Closed (false).
6
9
  */
@@ -8,7 +11,7 @@ export type DropdownContextState = {
8
11
  /**
9
12
  * Reference to DropdownButton, used to calculate a position for the DropdownMenu.
10
13
  */
11
- dropdownButtonRef: React.RefObject<HTMLButtonElement> | null
14
+ dropdownTriggerRef: React.MutableRefObject<T | null> | null
12
15
  /**
13
16
  * Reference to a selected DropdownItem, used to manipulate focus.
14
17
  */
@@ -16,11 +19,7 @@ export type DropdownContextState = {
16
19
  /**
17
20
  * Array of References to dropdownItems in a DropdownMenu.
18
21
  */
19
- dropdownItemsRef: React.MutableRefObject<HTMLButtonElement[]> | null
20
- /**
21
- * Close DropdownMenu event inherited from Modal.
22
- */
23
- onDismiss?(): void
22
+ dropdownItemsRef: React.MutableRefObject<E[]> | null
24
23
  /**
25
24
  * Function responsible for close the DropdownMenu in this context.
26
25
  */
@@ -33,16 +32,19 @@ export type DropdownContextState = {
33
32
  * Function responsible for switch the the DropdownMenu state in this context.
34
33
  */
35
34
  toggle?(): void
36
-
37
35
  /**
38
36
  * Identifier to be used in aria-controls
39
37
  */
40
38
  id: string
39
+ /**
40
+ * Associates the dropdown trigger element's ref for managing its position and interaction events.
41
+ */
42
+ addDropdownTriggerRef?(ref: T | null): void
41
43
  }
42
44
 
43
45
  const defaultState: DropdownContextState = {
44
46
  isOpen: false,
45
- dropdownButtonRef: null,
47
+ dropdownTriggerRef: null,
46
48
  selectedDropdownItemIndexRef: null,
47
49
  dropdownItemsRef: null,
48
50
  id: 'fs-dropdown',
@@ -7,12 +7,12 @@ import DropdownContext from '../contexts/DropdownContext'
7
7
  * Hook to use the Dropdown context.
8
8
  * @returns Dropdown context.
9
9
  */
10
- export const useDropdown = () => {
11
- const context = useContext<DropdownContextState>(DropdownContext)
10
+ export const useDropdown = <T extends HTMLElement = HTMLElement, E extends HTMLElement = HTMLElement>() => {
11
+ const context = useContext<DropdownContextState<HTMLElement, HTMLElement>>(DropdownContext)
12
12
 
13
13
  if (context === undefined) {
14
14
  throw new Error('Do not use useDropdown hook outside the Dropdown context.')
15
15
  }
16
16
 
17
- return context
17
+ return context as DropdownContextState<T, E>
18
18
  }
@@ -0,0 +1,56 @@
1
+ import React, { useImperativeHandle, useRef, useState } from 'react'
2
+
3
+ import { useDropdown } from './useDropdown'
4
+
5
+ export type UseDropdownItemProps<E extends HTMLElement = HTMLElement> = {
6
+ ref: React.ForwardedRef<E>
7
+ onClick?: React.MouseEventHandler<E>
8
+ dismissOnClick?: boolean
9
+ }
10
+
11
+ export const useDropdownItem = <E extends HTMLElement = HTMLElement>({
12
+ ref,
13
+ onClick,
14
+ dismissOnClick = true,
15
+ }: UseDropdownItemProps<E>) => {
16
+ const { dropdownItemsRef, selectedDropdownItemIndexRef, close } = useDropdown<
17
+ never,
18
+ E
19
+ >()
20
+
21
+ const [dropdownItemIndex, setDropdownItemIndex] = useState(0)
22
+ const dropdownItemRef = useRef<E>()
23
+
24
+ const addToRefs = (el: E) => {
25
+ if (el && !dropdownItemsRef?.current.includes(el)) {
26
+ dropdownItemsRef?.current.push(el)
27
+ setDropdownItemIndex(
28
+ dropdownItemsRef?.current.findIndex((element) => element === el) ?? 0
29
+ )
30
+ }
31
+
32
+ dropdownItemRef.current = el
33
+ }
34
+
35
+ const onFocusItem = () => {
36
+ selectedDropdownItemIndexRef!.current = dropdownItemIndex
37
+ dropdownItemsRef?.current[selectedDropdownItemIndexRef!.current]?.focus()
38
+ }
39
+
40
+ const handleOnClickItem = (event: React.MouseEvent<E, MouseEvent>) => {
41
+ onClick?.(event)
42
+ dismissOnClick && close?.()
43
+ }
44
+
45
+ useImperativeHandle(ref, () => dropdownItemRef.current!, [])
46
+
47
+ return {
48
+ ref: addToRefs,
49
+ onFocus: onFocusItem,
50
+ onMouseEnter: onFocusItem,
51
+ onClick: handleOnClickItem,
52
+ role: 'menuitem',
53
+ tabIndex: -1,
54
+ 'data-index': dropdownItemIndex,
55
+ }
56
+ }