@faststore/components 2.0.39-alpha.0 → 2.0.42-alpha.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 (67) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/assets/ArrowElbowDownRight.d.ts +3 -0
  3. package/dist/assets/ArrowElbowDownRight.js +8 -0
  4. package/dist/assets/ArrowElbowDownRight.js.map +1 -0
  5. package/dist/assets/index.d.ts +1 -0
  6. package/dist/assets/index.js +1 -0
  7. package/dist/assets/index.js.map +1 -1
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +1 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/molecules/Dropdown/Dropdown.d.ts +9 -0
  12. package/dist/molecules/Dropdown/Dropdown.js +66 -0
  13. package/dist/molecules/Dropdown/Dropdown.js.map +1 -0
  14. package/dist/molecules/Dropdown/DropdownButton.d.ts +10 -0
  15. package/dist/molecules/Dropdown/DropdownButton.js +12 -0
  16. package/dist/molecules/Dropdown/DropdownButton.js.map +1 -0
  17. package/dist/molecules/Dropdown/DropdownItem.d.ts +15 -0
  18. package/dist/molecules/Dropdown/DropdownItem.js +29 -0
  19. package/dist/molecules/Dropdown/DropdownItem.js.map +1 -0
  20. package/dist/molecules/Dropdown/DropdownMenu.d.ts +26 -0
  21. package/dist/molecules/Dropdown/DropdownMenu.js +66 -0
  22. package/dist/molecules/Dropdown/DropdownMenu.js.map +1 -0
  23. package/dist/molecules/Dropdown/contexts/DropdownContext.d.ts +41 -0
  24. package/dist/molecules/Dropdown/contexts/DropdownContext.js +11 -0
  25. package/dist/molecules/Dropdown/contexts/DropdownContext.js.map +1 -0
  26. package/dist/molecules/Dropdown/hooks/useDropdown.d.ts +6 -0
  27. package/dist/molecules/Dropdown/hooks/useDropdown.js +14 -0
  28. package/dist/molecules/Dropdown/hooks/useDropdown.js.map +1 -0
  29. package/dist/molecules/Dropdown/hooks/useDropdownPosition.d.ts +8 -0
  30. package/dist/molecules/Dropdown/hooks/useDropdownPosition.js +25 -0
  31. package/dist/molecules/Dropdown/hooks/useDropdownPosition.js.map +1 -0
  32. package/dist/molecules/Dropdown/index.d.ts +8 -0
  33. package/dist/molecules/Dropdown/index.js +5 -0
  34. package/dist/molecules/Dropdown/index.js.map +1 -0
  35. package/dist/molecules/Table/Table.d.ts +4 -6
  36. package/dist/molecules/Table/Table.js +1 -1
  37. package/dist/molecules/Table/Table.js.map +1 -1
  38. package/dist/molecules/Table/TableBody.d.ts +4 -6
  39. package/dist/molecules/Table/TableBody.js.map +1 -1
  40. package/dist/molecules/Table/TableCell.d.ts +4 -2
  41. package/dist/molecules/Table/TableCell.js +1 -1
  42. package/dist/molecules/Table/TableCell.js.map +1 -1
  43. package/dist/molecules/Table/TableFooter.d.ts +4 -6
  44. package/dist/molecules/Table/TableFooter.js.map +1 -1
  45. package/dist/molecules/Table/TableHead.d.ts +4 -6
  46. package/dist/molecules/Table/TableHead.js.map +1 -1
  47. package/dist/molecules/Table/TableRow.d.ts +4 -6
  48. package/dist/molecules/Table/TableRow.js +1 -1
  49. package/dist/molecules/Table/TableRow.js.map +1 -1
  50. package/package.json +2 -2
  51. package/src/assets/ArrowElbowDownRight.tsx +34 -0
  52. package/src/assets/index.ts +1 -0
  53. package/src/index.ts +12 -0
  54. package/src/molecules/Dropdown/Dropdown.tsx +101 -0
  55. package/src/molecules/Dropdown/DropdownButton.tsx +43 -0
  56. package/src/molecules/Dropdown/DropdownItem.tsx +75 -0
  57. package/src/molecules/Dropdown/DropdownMenu.tsx +154 -0
  58. package/src/molecules/Dropdown/contexts/DropdownContext.ts +53 -0
  59. package/src/molecules/Dropdown/hooks/useDropdown.ts +18 -0
  60. package/src/molecules/Dropdown/hooks/useDropdownPosition.ts +33 -0
  61. package/src/molecules/Dropdown/index.ts +11 -0
  62. package/src/molecules/Table/Table.tsx +21 -23
  63. package/src/molecules/Table/TableBody.tsx +14 -17
  64. package/src/molecules/Table/TableCell.tsx +29 -28
  65. package/src/molecules/Table/TableFooter.tsx +14 -17
  66. package/src/molecules/Table/TableHead.tsx +14 -17
  67. package/src/molecules/Table/TableRow.tsx +11 -17
@@ -1,14 +1,12 @@
1
- import type { HTMLAttributes } from 'react';
2
1
  import React from 'react';
2
+ import type { HTMLAttributes } from 'react';
3
3
  export interface TableFooterProps extends HTMLAttributes<HTMLTableSectionElement> {
4
4
  /**
5
5
  * ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
6
6
  */
7
7
  testId?: string;
8
- /**
9
- * Children for TableFooter components.
10
- */
11
- children: React.ReactNode;
12
8
  }
13
- declare const TableFooter: React.ForwardRefExoticComponent<TableFooterProps & React.RefAttributes<HTMLTableSectionElement>>;
9
+ declare const TableFooter: React.ForwardRefExoticComponent<TableFooterProps & {
10
+ children?: React.ReactNode;
11
+ } & React.RefAttributes<HTMLTableSectionElement>>;
14
12
  export default TableFooter;
@@ -1 +1 @@
1
- {"version":3,"file":"TableFooter.js","sourceRoot":"","sources":["../../../src/molecules/Table/TableFooter.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAczC,MAAM,WAAW,GAAG,UAAU,CAC5B,SAAS,WAAW,CAClB,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,EAAE,GAAG,UAAU,EAAE,EACvD,GAAG;IAEH,OAAO,CACL,+BAAO,GAAG,EAAE,GAAG,iBAAe,MAAM,mCAA2B,UAAU,IACtE,QAAQ,CACH,CACT,CAAA;AACH,CAAC,CACF,CAAA;AAED,eAAe,WAAW,CAAA"}
1
+ {"version":3,"file":"TableFooter.js","sourceRoot":"","sources":["../../../src/molecules/Table/TableFooter.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAWzC,MAAM,WAAW,GAAG,UAAU,CAG5B,SAAS,WAAW,CACpB,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,EAAE,GAAG,UAAU,EAAE,EACvD,GAAG;IAEH,OAAO,CACL,+BAAO,GAAG,EAAE,GAAG,iBAAe,MAAM,mCAA2B,UAAU,IACtE,QAAQ,CACH,CACT,CAAA;AACH,CAAC,CAAC,CAAA;AAEF,eAAe,WAAW,CAAA"}
@@ -1,14 +1,12 @@
1
- import type { HTMLAttributes } from 'react';
2
1
  import React from 'react';
2
+ import type { HTMLAttributes } from 'react';
3
3
  export interface TableHeadProps extends HTMLAttributes<HTMLTableSectionElement> {
4
4
  /**
5
5
  * ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
6
6
  */
7
7
  testId?: string;
8
- /**
9
- * Children for TableHead components.
10
- */
11
- children: React.ReactNode;
12
8
  }
13
- declare const TableHead: React.ForwardRefExoticComponent<TableHeadProps & React.RefAttributes<HTMLTableSectionElement>>;
9
+ declare const TableHead: React.ForwardRefExoticComponent<TableHeadProps & {
10
+ children?: React.ReactNode;
11
+ } & React.RefAttributes<HTMLTableSectionElement>>;
14
12
  export default TableHead;
@@ -1 +1 @@
1
- {"version":3,"file":"TableHead.js","sourceRoot":"","sources":["../../../src/molecules/Table/TableHead.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAczC,MAAM,SAAS,GAAG,UAAU,CAC1B,SAAS,SAAS,CAChB,EAAE,QAAQ,EAAE,MAAM,GAAG,eAAe,EAAE,GAAG,UAAU,EAAE,EACrD,GAAG;IAEH,OAAO,CACL,+BAAO,GAAG,EAAE,GAAG,iBAAe,MAAM,iCAAyB,UAAU,IACpE,QAAQ,CACH,CACT,CAAA;AACH,CAAC,CACF,CAAA;AAED,eAAe,SAAS,CAAA"}
1
+ {"version":3,"file":"TableHead.js","sourceRoot":"","sources":["../../../src/molecules/Table/TableHead.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAWzC,MAAM,SAAS,GAAG,UAAU,CAG1B,SAAS,SAAS,CAClB,EAAE,QAAQ,EAAE,MAAM,GAAG,eAAe,EAAE,GAAG,UAAU,EAAE,EACrD,GAAG;IAEH,OAAO,CACL,+BAAO,GAAG,EAAE,GAAG,iBAAe,MAAM,iCAAyB,UAAU,IACpE,QAAQ,CACH,CACT,CAAA;AACH,CAAC,CAAC,CAAA;AAEF,eAAe,SAAS,CAAA"}
@@ -1,14 +1,12 @@
1
- import type { HTMLAttributes } from 'react';
2
1
  import React from 'react';
2
+ import type { HTMLAttributes } from 'react';
3
3
  export interface TableRowProps extends HTMLAttributes<HTMLTableRowElement> {
4
4
  /**
5
5
  * ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
6
6
  */
7
7
  testId?: string;
8
- /**
9
- * Children for TableRow components.
10
- */
11
- children: React.ReactNode;
12
8
  }
13
- declare const TableRow: React.ForwardRefExoticComponent<TableRowProps & React.RefAttributes<HTMLTableRowElement>>;
9
+ declare const TableRow: React.ForwardRefExoticComponent<TableRowProps & {
10
+ children?: React.ReactNode;
11
+ } & React.RefAttributes<HTMLTableRowElement>>;
14
12
  export default TableRow;
@@ -1,5 +1,5 @@
1
1
  import React, { forwardRef } from 'react';
2
- const TableRow = forwardRef(function TableRow({ testId = 'fs-table-row', children, ...otherProps }, ref) {
2
+ const TableRow = forwardRef(function TableRow({ children, testId = 'fs-table-row', ...otherProps }, ref) {
3
3
  return (React.createElement("tr", { ref: ref, "data-fs-table-row": true, "data-testid": testId, ...otherProps }, children));
4
4
  });
5
5
  export default TableRow;
@@ -1 +1 @@
1
- {"version":3,"file":"TableRow.js","sourceRoot":"","sources":["../../../src/molecules/Table/TableRow.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAazC,MAAM,QAAQ,GAAG,UAAU,CACzB,SAAS,QAAQ,CACf,EAAE,MAAM,GAAG,cAAc,EAAE,QAAQ,EAAE,GAAG,UAAU,EAAE,EACpD,GAAG;IAEH,OAAO,CACL,4BAAI,GAAG,EAAE,GAAG,4CAAiC,MAAM,KAAM,UAAU,IAChE,QAAQ,CACN,CACN,CAAA;AACH,CAAC,CACF,CAAA;AAED,eAAe,QAAQ,CAAA"}
1
+ {"version":3,"file":"TableRow.js","sourceRoot":"","sources":["../../../src/molecules/Table/TableRow.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAUzC,MAAM,QAAQ,GAAG,UAAU,CAGzB,SAAS,QAAQ,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,cAAc,EAAE,GAAG,UAAU,EAAE,EAAE,GAAG;IAC3E,OAAO,CACL,4BAAI,GAAG,EAAE,GAAG,4CAAiC,MAAM,KAAM,UAAU,IAChE,QAAQ,CACN,CACN,CAAA;AACH,CAAC,CAAC,CAAA;AAEF,eAAe,QAAQ,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/components",
3
- "version": "2.0.39-alpha.0",
3
+ "version": "2.0.42-alpha.0",
4
4
  "module": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
6
6
  "author": "Emerson Laurentino @emersonlaurentino",
@@ -30,5 +30,5 @@
30
30
  "node": "16.18.0",
31
31
  "yarn": "1.19.1"
32
32
  },
33
- "gitHead": "37b0633963fb020169849932535505cad22f8fd6"
33
+ "gitHead": "47ce2b7c180d5a90d1ebadd7628ee47d0ceb1280"
34
34
  }
@@ -0,0 +1,34 @@
1
+ import React from 'react'
2
+ import type { FC } from 'react'
3
+
4
+ // Icon from Phosphor Icons
5
+ const ArrowElbowDownRight: FC = () => (
6
+ <svg
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ viewBox="0 0 256 256"
9
+ fill="currentColor"
10
+ strokeWidth="16"
11
+ width={24}
12
+ height={24}
13
+ >
14
+ <rect width="256" height="256" fill="none"></rect>
15
+ <polyline
16
+ points="160 128 208 176 160 224"
17
+ fill="none"
18
+ stroke="currentColor"
19
+ stroke-linecap="round"
20
+ stroke-linejoin="round"
21
+ stroke-width="16"
22
+ ></polyline>
23
+ <polyline
24
+ points="64 32 64 176 208 176"
25
+ fill="none"
26
+ stroke="currentColor"
27
+ stroke-linecap="round"
28
+ stroke-linejoin="round"
29
+ stroke-width="16"
30
+ ></polyline>
31
+ </svg>
32
+ )
33
+
34
+ export default ArrowElbowDownRight
@@ -1,4 +1,5 @@
1
1
  export { default as ArrowRight } from './ArrowRight'
2
+ export { default as ArrowElbowDownRight } from './ArrowElbowDownRight'
2
3
  export { default as CaretDown } from './CaretDown'
3
4
  export { default as Checked } from './Checked'
4
5
  export { default as House } from './House'
package/src/index.ts CHANGED
@@ -48,6 +48,18 @@ export { default as IconButton } from './molecules/IconButton'
48
48
  export type { IconButtonProps } from './molecules/IconButton'
49
49
  export { default as DiscountBadge } from './molecules/DiscountBadge'
50
50
  export type { DiscountBadgeProps } from './molecules/DiscountBadge'
51
+ export {
52
+ default as Dropdown,
53
+ DropdownButton,
54
+ DropdownItem,
55
+ DropdownMenu,
56
+ } from './molecules/Dropdown'
57
+ export type {
58
+ DropdownProps,
59
+ DropdownButtonProps,
60
+ DropdownItemProps,
61
+ DropdownMenuProps,
62
+ } from './molecules/Dropdown'
51
63
  export { default as InputField } from './molecules/InputField'
52
64
  export type { InputFieldProps } from './molecules/InputField'
53
65
  export { default as LinkButton } from './molecules/LinkButton'
@@ -0,0 +1,101 @@
1
+ import type { ReactNode } from 'react'
2
+ import React, { useRef, useMemo, useState, useEffect, useCallback } from 'react'
3
+
4
+ import DropdownContext from '../Dropdown/contexts/DropdownContext'
5
+
6
+ export type DropdownProps = {
7
+ children: ReactNode
8
+ onDismiss?(): void
9
+ isOpen?: boolean
10
+ id?: string
11
+ }
12
+
13
+ const Dropdown = ({
14
+ children,
15
+ isOpen: isOpenDefault = false,
16
+ onDismiss,
17
+ id = 'fs-dropdown',
18
+ }: DropdownProps) => {
19
+ const [isOpen, setIsOpen] = useState(isOpenDefault)
20
+ const dropdownItemsRef = useRef<HTMLButtonElement[]>([])
21
+ const selectedDropdownItemIndexRef = useRef(0)
22
+ const dropdownButtonRef = useRef<HTMLButtonElement>(null)
23
+
24
+ const close = useCallback(() => {
25
+ setIsOpen(false)
26
+ onDismiss?.()
27
+ }, [onDismiss])
28
+
29
+ const open = () => {
30
+ setIsOpen(true)
31
+ }
32
+
33
+ const toggle = useCallback(() => {
34
+ setIsOpen((old) => {
35
+ if (old) {
36
+ onDismiss?.()
37
+ dropdownButtonRef.current?.focus()
38
+ }
39
+
40
+ return !old
41
+ })
42
+ }, [onDismiss])
43
+
44
+ useEffect(() => {
45
+ setIsOpen(isOpenDefault)
46
+ }, [isOpenDefault])
47
+
48
+ useEffect(() => {
49
+ isOpen && dropdownItemsRef?.current[0]?.focus()
50
+ }, [isOpen])
51
+
52
+ useEffect(() => {
53
+ let firstClick = true
54
+
55
+ const event = (e: MouseEvent) => {
56
+ const someItemWasClicked = dropdownItemsRef?.current.some(
57
+ (item) => e.target === item
58
+ )
59
+
60
+ if (firstClick) {
61
+ firstClick = false
62
+
63
+ return
64
+ }
65
+
66
+ !someItemWasClicked && close()
67
+ }
68
+
69
+ if (isOpen) {
70
+ document.addEventListener('click', event)
71
+ } else {
72
+ document.removeEventListener('click', event)
73
+ }
74
+
75
+ return () => {
76
+ document.removeEventListener('click', event)
77
+ }
78
+ }, [close, isOpen])
79
+
80
+ const value = useMemo(() => {
81
+ return {
82
+ isOpen,
83
+ close,
84
+ open,
85
+ toggle,
86
+ dropdownButtonRef,
87
+ onDismiss,
88
+ selectedDropdownItemIndexRef,
89
+ dropdownItemsRef,
90
+ id,
91
+ }
92
+ }, [close, id, isOpen, onDismiss, toggle])
93
+
94
+ return (
95
+ <DropdownContext.Provider value={value}>
96
+ {children}
97
+ </DropdownContext.Provider>
98
+ )
99
+ }
100
+
101
+ export default Dropdown
@@ -0,0 +1,43 @@
1
+ import React, { forwardRef, useImperativeHandle } from 'react'
2
+ import Button, { ButtonProps } from '../../atoms/Button'
3
+
4
+ import { useDropdown } from './hooks/useDropdown'
5
+
6
+ export interface DropdownButtonProps
7
+ extends Omit<ButtonProps, "variant" | "inverse" >{
8
+ /**
9
+ * ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
10
+ */
11
+ testId?: string
12
+ }
13
+
14
+ const DropdownButton = forwardRef<HTMLButtonElement, DropdownButtonProps>(
15
+ function DropdownButton(
16
+ { children, testId = 'fs-dropdown-button', ...otherProps },
17
+ ref
18
+ ) {
19
+ const { toggle, dropdownButtonRef, isOpen, id } = useDropdown()
20
+
21
+ useImperativeHandle(ref, () => dropdownButtonRef!.current!, [
22
+ dropdownButtonRef,
23
+ ])
24
+
25
+ return (
26
+ <Button
27
+ data-fs-dropdown-button
28
+ onClick={toggle}
29
+ data-testid={testId}
30
+ ref={dropdownButtonRef}
31
+ aria-expanded={isOpen}
32
+ aria-haspopup="menu"
33
+ aria-controls={id}
34
+ variant='tertiary'
35
+ {...otherProps}
36
+ >
37
+ {children}
38
+ </Button>
39
+ )
40
+ }
41
+ )
42
+
43
+ export default DropdownButton
@@ -0,0 +1,75 @@
1
+ import type { ButtonHTMLAttributes } from 'react'
2
+ import Icon, { IconProps } from '../../atoms/Icon'
3
+ import React, { useImperativeHandle, forwardRef, useRef, useState } from 'react'
4
+
5
+ import { useDropdown } from './hooks/useDropdown'
6
+
7
+ export interface DropdownItemProps
8
+ extends ButtonHTMLAttributes<HTMLButtonElement> {
9
+ /**
10
+ * ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
11
+ */
12
+ testId?: string
13
+ /**
14
+ * A React component that will be rendered as an icon.
15
+ */
16
+ icon?: IconProps["component"]
17
+ }
18
+
19
+ const DropdownItem = forwardRef<HTMLButtonElement, DropdownItemProps>(
20
+ function Button(
21
+ { children, icon, onClick, testId = 'fs-dropdown-item', ...otherProps },
22
+ ref
23
+ ) {
24
+ const { dropdownItemsRef, selectedDropdownItemIndexRef, close } =
25
+ useDropdown()
26
+
27
+ const [dropdownItemIndex, setDropdownItemIndex] = useState(0)
28
+ const dropdownItemRef = useRef<HTMLButtonElement>()
29
+
30
+ const addToRefs = (el: HTMLButtonElement) => {
31
+ if (el && !dropdownItemsRef?.current.includes(el)) {
32
+ dropdownItemsRef?.current.push(el)
33
+ setDropdownItemIndex(
34
+ dropdownItemsRef?.current.findIndex((element) => element === el) ?? 0
35
+ )
36
+ }
37
+
38
+ dropdownItemRef.current = el
39
+ }
40
+
41
+ const onFocusItem = () => {
42
+ selectedDropdownItemIndexRef!.current = dropdownItemIndex
43
+ dropdownItemsRef?.current[selectedDropdownItemIndexRef!.current]?.focus()
44
+ }
45
+
46
+ const handleOnClickItem = (
47
+ event: React.MouseEvent<HTMLButtonElement, MouseEvent>
48
+ ) => {
49
+ onClick?.(event)
50
+ close?.()
51
+ }
52
+
53
+ useImperativeHandle(ref, () => dropdownItemRef.current!, [])
54
+
55
+ return (
56
+ <button
57
+ data-fs-dropdown-item
58
+ data-testid={testId}
59
+ ref={addToRefs}
60
+ onFocus={onFocusItem}
61
+ onMouseEnter={onFocusItem}
62
+ onClick={handleOnClickItem}
63
+ role="menuitem"
64
+ tabIndex={-1}
65
+ data-index={dropdownItemIndex}
66
+ {...otherProps}
67
+ >
68
+ {icon && <Icon data-fs-dropdown-item-icon component={icon} />}
69
+ {children}
70
+ </button>
71
+ )
72
+ }
73
+ )
74
+
75
+ export default DropdownItem
@@ -0,0 +1,154 @@
1
+ import type {
2
+ AriaAttributes,
3
+ KeyboardEvent,
4
+ PropsWithChildren,
5
+ MouseEvent,
6
+ ReactNode,
7
+ DetailedHTMLProps,
8
+ HTMLAttributes
9
+ } from 'react'
10
+ import React from 'react'
11
+ import { createPortal } from 'react-dom'
12
+
13
+ import { useDropdown } from './hooks/useDropdown'
14
+ import { useDropdownPosition } from './hooks/useDropdownPosition'
15
+
16
+ //TODO: Replace by ModalContentProps when Modal component be brought
17
+ type BaseModalProps = Omit<
18
+ DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
19
+ 'ref' | 'onClick'
20
+ >
21
+
22
+ export interface DropdownMenuProps extends BaseModalProps {
23
+ /**
24
+ * ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
25
+ */
26
+ testId?: string
27
+ /**
28
+ * Identifies the element (or elements) that labels the current element.
29
+ * @see aria-labelledby https://www.w3.org/TR/wai-aria-1.1/#aria-labelledby
30
+ */
31
+ 'aria-labelledby'?: AriaAttributes['aria-label']
32
+
33
+ /**
34
+ * This function is called whenever the user hits "Escape" or clicks outside
35
+ * the dialog.
36
+ */
37
+ onDismiss?: (event: MouseEvent | KeyboardEvent) => void
38
+
39
+ /**
40
+ * Specifies the size variant
41
+ */
42
+ size?: 'small' | 'regular'
43
+
44
+ children: ReactNode[] | ReactNode
45
+ }
46
+
47
+ /*
48
+ * This component is based on @reach/dialog.
49
+ * https://github.com/reach/reach-ui/blob/main/packages/dialog/src/index.tsx
50
+ * https://reach.tech/dialog
51
+ */
52
+
53
+ const DropdownMenu = ({
54
+ children,
55
+ testId = 'fs-dropdown-menu',
56
+ size = 'regular',
57
+ style,
58
+ ...otherProps
59
+ }: PropsWithChildren<DropdownMenuProps>) => {
60
+ const { isOpen, close, dropdownItemsRef, selectedDropdownItemIndexRef, dropdownButtonRef, id } =
61
+ useDropdown()
62
+
63
+ const dropdownPosition = useDropdownPosition()
64
+
65
+ const childrenLength = React.Children.toArray(children).length
66
+
67
+ const handleDownPress = () => {
68
+ if (selectedDropdownItemIndexRef!.current < childrenLength - 1) {
69
+ selectedDropdownItemIndexRef!.current++
70
+ } else {
71
+ selectedDropdownItemIndexRef!.current = 0
72
+ }
73
+
74
+ dropdownItemsRef?.current[selectedDropdownItemIndexRef!.current]?.focus()
75
+ }
76
+
77
+ const handleUpPress = () => {
78
+ if (selectedDropdownItemIndexRef!.current > 0) {
79
+ selectedDropdownItemIndexRef!.current--
80
+ } else {
81
+ selectedDropdownItemIndexRef!.current = childrenLength - 1
82
+ }
83
+
84
+ dropdownItemsRef?.current[selectedDropdownItemIndexRef!.current]?.focus()
85
+ }
86
+
87
+ const handleHomePress = () => {
88
+ selectedDropdownItemIndexRef!.current = 0
89
+ dropdownItemsRef?.current[selectedDropdownItemIndexRef!.current]?.focus()
90
+ }
91
+
92
+ const handleEndPress = () => {
93
+ selectedDropdownItemIndexRef!.current = childrenLength - 1
94
+ dropdownItemsRef?.current[selectedDropdownItemIndexRef!.current]?.focus()
95
+ }
96
+
97
+ const handleEscapePress = () => {
98
+ close?.()
99
+ dropdownButtonRef?.current?.focus()
100
+ }
101
+
102
+ const handleBackdropKeyDown = (event: KeyboardEvent) => {
103
+ if (event.defaultPrevented || event.key === 'Enter') {
104
+ return
105
+ }
106
+
107
+ event.preventDefault()
108
+
109
+ event.key === 'Escape' && handleEscapePress()
110
+
111
+ event.key === 'ArrowDown' && handleDownPress()
112
+
113
+ event.key === 'ArrowUp' && handleUpPress()
114
+
115
+ event.key === 'Home' && handleHomePress()
116
+
117
+ event.key === 'End' && handleEndPress()
118
+
119
+ event.stopPropagation()
120
+ }
121
+
122
+ const clearChildrenReferences = () => {
123
+ dropdownItemsRef!.current = []
124
+
125
+ return null
126
+ }
127
+
128
+ return isOpen
129
+ ? createPortal(
130
+ <div
131
+ role="presentation"
132
+ data-fs-dropdown-overlay
133
+ onKeyDown={handleBackdropKeyDown}
134
+ data-testid={`${testId}-overlay`}
135
+ >
136
+ <div
137
+ role="menu"
138
+ aria-orientation="vertical"
139
+ data-fs-dropdown-menu
140
+ data-fs-dropdown-menu-size={size}
141
+ data-testid={testId}
142
+ style={{ ...dropdownPosition, ...style }}
143
+ id={id}
144
+ {...otherProps}
145
+ >
146
+ {children}
147
+ </div>
148
+ </div>,
149
+ document.body
150
+ )
151
+ : clearChildrenReferences()
152
+ }
153
+
154
+ export default DropdownMenu
@@ -0,0 +1,53 @@
1
+ import { createContext } from 'react'
2
+
3
+ export type DropdownContextState = {
4
+ /**
5
+ * Control de Dropdown state as Opened (true) or Closed (false).
6
+ */
7
+ isOpen: boolean
8
+ /**
9
+ * Reference to DropdownButton, used to calculate a position for the DropdownMenu.
10
+ */
11
+ dropdownButtonRef: React.RefObject<HTMLButtonElement> | null
12
+ /**
13
+ * Reference to a selected DropdownItem, used to manipulate focus.
14
+ */
15
+ selectedDropdownItemIndexRef: React.MutableRefObject<number> | null
16
+ /**
17
+ * Array of References to dropdownItems in a DropdownMenu.
18
+ */
19
+ dropdownItemsRef: React.MutableRefObject<HTMLButtonElement[]> | null
20
+ /**
21
+ * Close DropdownMenu event inherited from Modal.
22
+ */
23
+ onDismiss?(): void
24
+ /**
25
+ * Function responsible for close the DropdownMenu in this context.
26
+ */
27
+ close?(): void
28
+ /**
29
+ * Function responsible for open the DropdownMenu in this context.
30
+ */
31
+ open?(): void
32
+ /**
33
+ * Function responsible for switch the the DropdownMenu state in this context.
34
+ */
35
+ toggle?(): void
36
+
37
+ /**
38
+ * Identifier to be used in aria-controls
39
+ */
40
+ id: string
41
+ }
42
+
43
+ const defaultState: DropdownContextState = {
44
+ isOpen: false,
45
+ dropdownButtonRef: null,
46
+ selectedDropdownItemIndexRef: null,
47
+ dropdownItemsRef: null,
48
+ id: 'fs-dropdown',
49
+ }
50
+
51
+ const DropdownContext = createContext<DropdownContextState>(defaultState)
52
+
53
+ export default DropdownContext
@@ -0,0 +1,18 @@
1
+ import { useContext } from 'react'
2
+
3
+ import type { DropdownContextState } from '../contexts/DropdownContext'
4
+ import DropdownContext from '../contexts/DropdownContext'
5
+
6
+ /**
7
+ * Hook to use the Dropdown context.
8
+ * @returns Dropdown context.
9
+ */
10
+ export const useDropdown = () => {
11
+ const context = useContext<DropdownContextState>(DropdownContext)
12
+
13
+ if (context === undefined) {
14
+ throw new Error('Do not use useDropdown hook outside the Dropdown context.')
15
+ }
16
+
17
+ return context
18
+ }
@@ -0,0 +1,33 @@
1
+ import { useDropdown } from './useDropdown'
2
+
3
+ type DropdownPosition = Pick<React.CSSProperties, 'position' | 'top' | 'left'>
4
+
5
+ /**
6
+ * Hook used to find the DropdownMenu position in relation to DropdownButton
7
+ * @returns Style with positions.
8
+ */
9
+ export const useDropdownPosition = (): DropdownPosition => {
10
+ const { dropdownButtonRef } = useDropdown()
11
+
12
+ // Necessary to use this component in SSR
13
+ const isBrowser = typeof window !== 'undefined'
14
+
15
+ const buttonRect = dropdownButtonRef?.current?.getBoundingClientRect()
16
+ const topLevel = buttonRect?.top ?? 0
17
+ const topOffset = buttonRect?.height ?? 0
18
+ const leftLevel = buttonRect?.left ?? 0
19
+
20
+ // The scroll properties fix the position of DropdownMenu when the scroll is activated.
21
+ const scrollTop = isBrowser ? document?.documentElement?.scrollTop : 0
22
+ const scrollLeft = isBrowser ? document?.documentElement?.scrollLeft : 0
23
+
24
+ const topPosition = topLevel + topOffset + scrollTop
25
+
26
+ const leftPosition = leftLevel + scrollLeft
27
+
28
+ return {
29
+ position: 'absolute',
30
+ top: topPosition,
31
+ left: leftPosition,
32
+ }
33
+ }
@@ -0,0 +1,11 @@
1
+ export { default } from './Dropdown'
2
+ export type { DropdownProps } from './Dropdown'
3
+
4
+ export { default as DropdownButton } from './DropdownButton'
5
+ export type { DropdownButtonProps } from './DropdownButton'
6
+
7
+ export { default as DropdownItem } from './DropdownItem'
8
+ export type { DropdownItemProps } from './DropdownItem'
9
+
10
+ export { default as DropdownMenu } from './DropdownMenu'
11
+ export type { DropdownMenuProps } from './DropdownMenu'