@faststore/components 2.0.54-alpha.0 → 2.0.57-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 (50) hide show
  1. package/dist/atoms/Skeleton/Skeleton.d.ts +37 -0
  2. package/dist/atoms/Skeleton/Skeleton.js +10 -0
  3. package/dist/atoms/Skeleton/Skeleton.js.map +1 -0
  4. package/dist/atoms/Skeleton/index.d.ts +2 -0
  5. package/dist/atoms/Skeleton/index.js +2 -0
  6. package/dist/atoms/Skeleton/index.js.map +1 -0
  7. package/dist/hooks/UIProvider.d.ts +33 -0
  8. package/dist/hooks/UIProvider.js +74 -0
  9. package/dist/hooks/UIProvider.js.map +1 -0
  10. package/dist/hooks/index.d.ts +3 -0
  11. package/dist/hooks/index.js +4 -0
  12. package/dist/hooks/index.js.map +1 -0
  13. package/dist/hooks/useFadeEffect.d.ts +5 -0
  14. package/dist/hooks/useFadeEffect.js +18 -0
  15. package/dist/hooks/useFadeEffect.js.map +1 -0
  16. package/dist/hooks/useTrapFocus.d.ts +8 -0
  17. package/dist/hooks/useTrapFocus.js +75 -0
  18. package/dist/hooks/useTrapFocus.js.map +1 -0
  19. package/dist/index.d.ts +8 -3
  20. package/dist/index.js +6 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/molecules/Modal/Modal.d.ts +34 -0
  23. package/dist/molecules/Modal/Modal.js +38 -0
  24. package/dist/molecules/Modal/Modal.js.map +1 -0
  25. package/dist/molecules/Modal/ModalBody.d.ts +6 -0
  26. package/dist/molecules/Modal/ModalBody.js +4 -0
  27. package/dist/molecules/Modal/ModalBody.js.map +1 -0
  28. package/dist/molecules/Modal/ModalContent.d.ts +10 -0
  29. package/dist/molecules/Modal/ModalContent.js +23 -0
  30. package/dist/molecules/Modal/ModalContent.js.map +1 -0
  31. package/dist/molecules/Modal/ModalHeader.d.ts +19 -0
  32. package/dist/molecules/Modal/ModalHeader.js +11 -0
  33. package/dist/molecules/Modal/ModalHeader.js.map +1 -0
  34. package/dist/molecules/Modal/index.d.ts +5 -0
  35. package/dist/molecules/Modal/index.js +4 -0
  36. package/dist/molecules/Modal/index.js.map +1 -0
  37. package/dist/molecules/Table/Table.d.ts +1 -1
  38. package/package.json +2 -2
  39. package/src/atoms/Skeleton/Skeleton.tsx +76 -0
  40. package/src/atoms/Skeleton/index.ts +2 -0
  41. package/src/hooks/UIProvider.tsx +152 -0
  42. package/src/hooks/index.ts +3 -0
  43. package/src/hooks/useFadeEffect.ts +21 -0
  44. package/src/hooks/useTrapFocus.ts +108 -0
  45. package/src/index.ts +10 -13
  46. package/src/molecules/Modal/Modal.tsx +106 -0
  47. package/src/molecules/Modal/ModalBody.tsx +13 -0
  48. package/src/molecules/Modal/ModalContent.tsx +90 -0
  49. package/src/molecules/Modal/ModalHeader.tsx +47 -0
  50. package/src/molecules/Modal/index.tsx +5 -0
@@ -22,5 +22,5 @@ export interface TableProps extends DetailedHTMLProps<TableHTMLAttributes<HTMLTa
22
22
  */
23
23
  nonce?: string | undefined;
24
24
  }
25
- declare const Table: React.ForwardRefExoticComponent<Pick<PropsWithChildren<TableProps>, "slot" | "style" | "summary" | "title" | "testId" | "variant" | "aria-label" | "children" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "className" | "contentEditable" | "contextMenu" | "dir" | "draggable" | "hidden" | "id" | "lang" | "nonce" | "placeholder" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "is" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onResize" | "onResizeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "width" | "key" | "align" | "bgcolor" | "border" | "cellPadding" | "cellSpacing" | "frame" | "rules"> & React.RefAttributes<HTMLTableElement>>;
25
+ declare const Table: React.ForwardRefExoticComponent<Pick<PropsWithChildren<TableProps>, "slot" | "style" | "summary" | "title" | "children" | "testId" | "variant" | "aria-label" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "className" | "contentEditable" | "contextMenu" | "dir" | "draggable" | "hidden" | "id" | "lang" | "nonce" | "placeholder" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "is" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onResize" | "onResizeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "width" | "key" | "align" | "bgcolor" | "border" | "cellPadding" | "cellSpacing" | "frame" | "rules"> & React.RefAttributes<HTMLTableElement>>;
26
26
  export default Table;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/components",
3
- "version": "2.0.54-alpha.0",
3
+ "version": "2.0.57-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": "7173ea0bec153905a6b800d002401b5c27fd3213"
33
+ "gitHead": "a9310e34894883ae71d8bdb32402c3a17b419078"
34
34
  }
@@ -0,0 +1,76 @@
1
+ import React, { forwardRef } from 'react'
2
+ import type { HTMLAttributes, PropsWithChildren } from 'react'
3
+
4
+ interface Size {
5
+ width: string
6
+ height: string
7
+ }
8
+
9
+ export type BorderStyle = 'regular' | 'pill' | 'circle'
10
+
11
+ export interface SkeletonProps extends HTMLAttributes<HTMLDivElement> {
12
+ /**
13
+ * ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
14
+ */
15
+ testId?: string
16
+ /**
17
+ * Control whether skeleton should be visible or not.
18
+ */
19
+ loading?: boolean
20
+ /**
21
+ * Control whether the shimmer effect should be displayed or not.
22
+ */
23
+ shimmer?: boolean
24
+ /**
25
+ * Specifies the skeleton element size (width, height).
26
+ */
27
+ size: Size
28
+ /**
29
+ * Specifies the skeleton element border radius ('regular' | 'pill' | 'circle').
30
+ */
31
+ border?: BorderStyle
32
+ /**
33
+ * Custom border radius for skeleton elements.
34
+ */
35
+ borderRadius?: string
36
+ }
37
+
38
+ const Skeleton = forwardRef<HTMLDivElement, PropsWithChildren<SkeletonProps>>(
39
+ function Skeleton(
40
+ {
41
+ testId = 'fs-skeleton',
42
+ loading = true,
43
+ shimmer = true,
44
+ children,
45
+ size,
46
+ border,
47
+ borderRadius,
48
+ ...otherProps
49
+ },
50
+ ref
51
+ ) {
52
+ const styles = {
53
+ width: size.width,
54
+ height: size.height,
55
+ }
56
+
57
+ return loading ? (
58
+ <div
59
+ ref={ref}
60
+ data-fs-skeleton
61
+ data-testid={testId}
62
+ data-fs-skeleton-border={border ? border : null}
63
+ style={
64
+ borderRadius ? { ...styles, borderRadius: borderRadius } : styles
65
+ }
66
+ {...otherProps}
67
+ >
68
+ {shimmer && <div data-fs-skeleton-shimmer />}
69
+ </div>
70
+ ) : (
71
+ <>{children}</>
72
+ )
73
+ }
74
+ )
75
+
76
+ export default Skeleton
@@ -0,0 +1,2 @@
1
+ export { default } from './Skeleton'
2
+ export type { SkeletonProps } from './Skeleton'
@@ -0,0 +1,152 @@
1
+ import React from 'react'
2
+ import { createContext, useContext, useMemo, useReducer } from 'react'
3
+ import type { PropsWithChildren } from 'react'
4
+
5
+ interface Toast {
6
+ message: string
7
+ status: 'ERROR' | 'WARNING' | 'INFO'
8
+ title?: string
9
+ icon?: string
10
+ }
11
+
12
+ interface State {
13
+ /** Cart sidebar */
14
+ cart: boolean
15
+ /** Region modal */
16
+ modal: boolean
17
+ /** Menu slider */
18
+ navbar: boolean
19
+ /** Search page filter slider */
20
+ filter: boolean
21
+ toasts: Toast[]
22
+ }
23
+
24
+ type UIElement = 'navbar' | 'cart' | 'modal' | 'filter'
25
+
26
+ type Action =
27
+ | {
28
+ type: 'open'
29
+ payload: UIElement
30
+ }
31
+ | {
32
+ type: 'close'
33
+ payload: UIElement
34
+ }
35
+ | {
36
+ type: 'pushToast'
37
+ payload: Toast
38
+ }
39
+ | {
40
+ type: 'popToast'
41
+ }
42
+
43
+ const reducer = (state: State, action: Action): State => {
44
+ const { type } = action
45
+
46
+ switch (type) {
47
+ case 'open': {
48
+ const { payload } = action
49
+
50
+ document.body.classList.add('no-scroll')
51
+
52
+ return {
53
+ ...state,
54
+ [payload]: true,
55
+ }
56
+ }
57
+
58
+ case 'close': {
59
+ const { payload } = action
60
+
61
+ document.body.classList.remove('no-scroll')
62
+
63
+ return {
64
+ ...state,
65
+ [payload]: false,
66
+ }
67
+ }
68
+
69
+ case 'pushToast': {
70
+ return {
71
+ ...state,
72
+ toasts: [...state.toasts, action.payload],
73
+ }
74
+ }
75
+
76
+ case 'popToast': {
77
+ return {
78
+ ...state,
79
+ toasts: state.toasts.slice(1),
80
+ }
81
+ }
82
+
83
+ default:
84
+ throw new Error(`Action ${type} not implemented`)
85
+ }
86
+ }
87
+
88
+ const initializer = (): State => ({
89
+ cart: false,
90
+ modal: false,
91
+ navbar: false,
92
+ filter: false,
93
+ toasts: [],
94
+ })
95
+
96
+ interface Context extends State {
97
+ closeNavbar: () => void
98
+ openNavbar: () => void
99
+ closeFilter: () => void
100
+ openFilter: () => void
101
+ openCart: () => void
102
+ closeCart: () => void
103
+ openModal: () => void
104
+ closeModal: () => void
105
+ pushToast: (data: Toast) => void
106
+ popToast: () => void
107
+ }
108
+
109
+ const UIContext = createContext<Context | undefined>(undefined)
110
+
111
+ function UIProvider({ children }: PropsWithChildren) {
112
+ const [ui, dispatch] = useReducer(reducer, undefined, initializer)
113
+
114
+ const callbacks = useMemo(
115
+ () => ({
116
+ openFilter: () => dispatch({ type: 'open', payload: 'filter' }),
117
+ closeFilter: () => dispatch({ type: 'close', payload: 'filter' }),
118
+ openNavbar: () => dispatch({ type: 'open', payload: 'navbar' }),
119
+ closeNavbar: () => dispatch({ type: 'close', payload: 'navbar' }),
120
+ openCart: () => dispatch({ type: 'open', payload: 'cart' }),
121
+ closeCart: () => dispatch({ type: 'close', payload: 'cart' }),
122
+ openModal: () => dispatch({ type: 'open', payload: 'modal' }),
123
+ closeModal: () => dispatch({ type: 'close', payload: 'modal' }),
124
+ pushToast: (toast: Toast) =>
125
+ dispatch({ type: 'pushToast', payload: toast }),
126
+ popToast: () => dispatch({ type: 'popToast' }),
127
+ }),
128
+ []
129
+ )
130
+
131
+ const value = useMemo(
132
+ () => ({
133
+ ...ui,
134
+ ...callbacks,
135
+ }),
136
+ [callbacks, ui]
137
+ )
138
+
139
+ return <UIContext.Provider value={value}>{children}</UIContext.Provider>
140
+ }
141
+
142
+ export function useUI() {
143
+ const context = useContext(UIContext)
144
+
145
+ if (context === undefined) {
146
+ throw new Error('Missing UI context on React tree')
147
+ }
148
+
149
+ return context
150
+ }
151
+
152
+ export default UIProvider
@@ -0,0 +1,3 @@
1
+ export { default as UIProvider, useUI } from './UIProvider'
2
+ export { useFadeEffect } from './useFadeEffect'
3
+ export { useTrapFocus } from './useTrapFocus'
@@ -0,0 +1,21 @@
1
+ import { useState, useCallback, useEffect } from 'react'
2
+
3
+ export const useFadeEffect = () => {
4
+ const [fade, setFade] = useState<'in' | 'out'>('out')
5
+ const fadeOut = useCallback(() => setFade('out'), [])
6
+ const fadeIn = useCallback(() => setFade('in'), [])
7
+
8
+ useEffect(() => {
9
+ fadeIn()
10
+
11
+ return () => {
12
+ fadeOut()
13
+ }
14
+ }, [fadeIn, fadeOut])
15
+
16
+ return {
17
+ fade,
18
+ fadeIn,
19
+ fadeOut,
20
+ }
21
+ }
@@ -0,0 +1,108 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import type { RefObject } from 'react'
3
+ import type { FocusableElement } from 'tabbable'
4
+ import { tabbable } from 'tabbable'
5
+
6
+ interface TrapFocusParams {
7
+ beforeElementRef: RefObject<HTMLElement>
8
+ trapFocusRef: RefObject<HTMLElement>
9
+ afterElementRef: RefObject<HTMLElement>
10
+ }
11
+
12
+ /*
13
+ * Element that will maintain the focus inside trapFocusRef, focus the first element,
14
+ * and focus back on the element that was in focus when useTrapFocus was triggered.
15
+ *
16
+ * Inspired by Reakit useTrapFocus https://github.com/reakit/reakit/blob/a211d94da9f3b683182568a56479b91afb1b85ae/packages/reakit/src/Dialog/__utils/useFocusTrap.ts
17
+ */
18
+ export const useTrapFocus = ({
19
+ trapFocusRef,
20
+ beforeElementRef,
21
+ afterElementRef,
22
+ }: TrapFocusParams) => {
23
+ const tabbableNodesRef = useRef<FocusableElement[]>()
24
+ const nodeToRestoreRef = useRef<HTMLElement | null>(
25
+ document.hasFocus() ? (document.activeElement as HTMLElement) : null
26
+ )
27
+
28
+ // Focus back on the element that was focused when useTrapFocus is triggered.
29
+ useEffect(() => {
30
+ const nodeToRestore = nodeToRestoreRef.current
31
+
32
+ return () => {
33
+ nodeToRestore?.focus()
34
+ }
35
+ }, [nodeToRestoreRef])
36
+
37
+ // Set focus on first tabbable element
38
+ useEffect(() => {
39
+ if (!trapFocusRef.current) {
40
+ return
41
+ }
42
+
43
+ if (!tabbableNodesRef.current) {
44
+ tabbableNodesRef.current = tabbable(trapFocusRef.current)
45
+ }
46
+
47
+ const [firstTabbable] = tabbableNodesRef.current
48
+
49
+ if (!firstTabbable) {
50
+ trapFocusRef.current.focus()
51
+
52
+ return
53
+ }
54
+
55
+ firstTabbable.focus()
56
+ }, [trapFocusRef])
57
+
58
+ // Handle loop focus. Set keydown and focusin event listeners
59
+ useEffect(() => {
60
+ if (
61
+ !trapFocusRef.current ||
62
+ !beforeElementRef.current ||
63
+ !afterElementRef.current
64
+ ) {
65
+ return
66
+ }
67
+
68
+ const beforeElement = beforeElementRef.current
69
+ const afterElement = afterElementRef.current
70
+ const trapFocus = trapFocusRef.current
71
+
72
+ const handleLoopFocus = (nativeEvent: FocusEvent) => {
73
+ if (!document.hasFocus()) {
74
+ return
75
+ }
76
+
77
+ tabbableNodesRef.current = tabbable(trapFocusRef.current!)
78
+
79
+ if (!tabbableNodesRef.current.length) {
80
+ trapFocus.focus()
81
+ }
82
+
83
+ /*
84
+ * Handle loop focus from beforeElementRef. This node can only be focused if the user press shift tab.
85
+ * It will focus the last element of the trapFocusRef.
86
+ */
87
+ if (nativeEvent.target === beforeElement) {
88
+ tabbableNodesRef.current[tabbableNodesRef.current.length - 1]?.focus()
89
+ }
90
+
91
+ /*
92
+ * Handle loop focus from afterElementRef. This node can only be focused if the user press tab.
93
+ * It will focus the first element of the trapFocusRef.
94
+ */
95
+ if (nativeEvent.target === afterElement) {
96
+ tabbableNodesRef.current[0]?.focus()
97
+ }
98
+ }
99
+
100
+ beforeElement?.addEventListener('focusin', handleLoopFocus)
101
+ afterElement?.addEventListener('focusin', handleLoopFocus)
102
+
103
+ return () => {
104
+ beforeElement?.removeEventListener('focusin', handleLoopFocus)
105
+ afterElement?.removeEventListener('focusin', handleLoopFocus)
106
+ }
107
+ }, [tabbableNodesRef, afterElementRef, beforeElementRef, trapFocusRef])
108
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  // Assets
2
2
  export * from './assets'
3
3
 
4
+ // Hooks
5
+ export * from './hooks'
6
+
4
7
  // Atoms
5
8
  export { default as Badge } from './atoms/Badge'
6
9
  export type { BadgeProps } from './atoms/Badge'
@@ -26,6 +29,8 @@ export { default as Price } from './atoms/Price'
26
29
  export type { PriceProps } from './atoms/Price'
27
30
  export { default as Radio } from './atoms/Radio'
28
31
  export type { RadioProps } from './atoms/Radio'
32
+ export { default as Skeleton } from './atoms/Skeleton'
33
+ export type { SkeletonProps } from './atoms/Skeleton'
29
34
  export { default as Select } from './atoms/Select'
30
35
  export type { SelectProps } from './atoms/Select'
31
36
  export { default as Slider } from './atoms/Slider'
@@ -66,11 +71,7 @@ export type {
66
71
  DropdownItemProps,
67
72
  DropdownMenuProps,
68
73
  } from './molecules/Dropdown'
69
- export {
70
- default as Gift,
71
- GiftContent,
72
- GiftImage,
73
- } from './molecules/Gift'
74
+ export { default as Gift, GiftContent, GiftImage } from './molecules/Gift'
74
75
  export type {
75
76
  GiftProps,
76
77
  GiftContentProps,
@@ -80,16 +81,12 @@ export { default as InputField } from './molecules/InputField'
80
81
  export type { InputFieldProps } from './molecules/InputField'
81
82
  export { default as LinkButton } from './molecules/LinkButton'
82
83
  export type { LinkButtonProps } from './molecules/LinkButton'
84
+ export { default as Modal, ModalHeader, ModalBody } from './molecules/Modal'
85
+ export type { ModalProps, ModalHeaderProps } from './molecules/Modal'
83
86
  export { default as RadioField } from './molecules/RadioField'
84
87
  export type { RadioFieldProps } from './molecules/RadioField'
85
- export {
86
- default as RadioGroup,
87
- RadioOption,
88
- } from './molecules/RadioGroup'
89
- export type {
90
- RadioGroupProps,
91
- RadioOptionProps,
92
- } from './molecules/RadioGroup'
88
+ export { default as RadioGroup, RadioOption } from './molecules/RadioGroup'
89
+ export type { RadioGroupProps, RadioOptionProps } from './molecules/RadioGroup'
93
90
  export { default as Rating } from './molecules/Rating'
94
91
  export type { RatingProps } from './molecules/Rating'
95
92
  export { default as SelectField } from './molecules/SelectField'
@@ -0,0 +1,106 @@
1
+ import type {
2
+ AriaAttributes,
3
+ KeyboardEvent,
4
+ MouseEvent,
5
+ ReactNode,
6
+ } from 'react'
7
+ import React from 'react'
8
+ import { createPortal } from 'react-dom'
9
+
10
+ import { Overlay } from '../..'
11
+ import { useFadeEffect, useUI } from '../../hooks'
12
+ import type { ModalContentProps } from './ModalContent'
13
+ import ModalContent from './ModalContent'
14
+
15
+ export type ModalChildrenProps = {
16
+ fade: 'in' | 'out'
17
+ fadeOut: () => void
18
+ fadeIn: () => void
19
+ }
20
+
21
+ type ModalChildrenFunction = (props: ModalChildrenProps) => ReactNode
22
+
23
+ export interface ModalProps extends Omit<ModalContentProps, 'children'> {
24
+ /**
25
+ * ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
26
+ */
27
+ testId?: string
28
+ /**
29
+ * Identifies the element (or elements) that labels the current element.
30
+ * @see aria-labelledby https://www.w3.org/TR/wai-aria-1.1/#aria-labelledby
31
+ */
32
+ 'aria-labelledby'?: AriaAttributes['aria-label']
33
+ /**
34
+ * A boolean value that represents the state of the Modal
35
+ */
36
+ isOpen?: boolean
37
+ /**
38
+ * Event emitted when the modal is closed
39
+ */
40
+ onDismiss?: () => void
41
+ /**
42
+ * Children or function as a children
43
+ */
44
+ children: ModalChildrenFunction | 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 Modal = ({
54
+ children,
55
+ testId = 'fs-modal',
56
+ isOpen = true,
57
+ onDismiss,
58
+ ...otherProps
59
+ }: ModalProps) => {
60
+ const { closeModal } = useUI()
61
+ const { fade, fadeOut, fadeIn } = useFadeEffect()
62
+
63
+ const handleBackdropClick = (event: MouseEvent) => {
64
+ if (event.defaultPrevented) {
65
+ return
66
+ }
67
+
68
+ event.stopPropagation()
69
+ fadeOut?.()
70
+ onDismiss?.()
71
+ }
72
+
73
+ const handleBackdropKeyDown = (event: KeyboardEvent) => {
74
+ if (event.key !== 'Escape' || event.defaultPrevented) {
75
+ return
76
+ }
77
+
78
+ event.stopPropagation()
79
+ fadeOut?.()
80
+ onDismiss?.()
81
+ }
82
+
83
+ return isOpen
84
+ ? createPortal(
85
+ <Overlay
86
+ onClick={handleBackdropClick}
87
+ onKeyDown={handleBackdropKeyDown}
88
+ >
89
+ <ModalContent
90
+ onTransitionEnd={() => fade === 'out' && closeModal()}
91
+ data-fs-modal
92
+ data-fs-modal-state={fade}
93
+ testId={testId}
94
+ {...otherProps}
95
+ >
96
+ {typeof children === 'function'
97
+ ? children({ fade, fadeOut, fadeIn })
98
+ : children}
99
+ </ModalContent>
100
+ </Overlay>,
101
+ document.body
102
+ )
103
+ : null
104
+ }
105
+
106
+ export default Modal
@@ -0,0 +1,13 @@
1
+ import React, { ReactNode, HTMLAttributes } from 'react'
2
+
3
+ export interface ModalBodyProps extends HTMLAttributes<HTMLDivElement> {
4
+ children: ReactNode
5
+ }
6
+
7
+ const ModalBody = ({ children, ...otherProps }: ModalBodyProps) => (
8
+ <div data-fs-modal-body {...otherProps}>
9
+ {children}
10
+ </div>
11
+ )
12
+
13
+ export default ModalBody
@@ -0,0 +1,90 @@
1
+ import type {
2
+ DetailedHTMLProps,
3
+ HTMLAttributes,
4
+ MouseEvent,
5
+ RefObject,
6
+ } from 'react'
7
+ import React, { useRef } from 'react'
8
+
9
+ import { useTrapFocus } from '../../hooks'
10
+
11
+ interface ModalContentPureProps
12
+ extends Omit<
13
+ DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
14
+ 'ref'
15
+ > {
16
+ beforeElementRef: RefObject<HTMLDivElement>
17
+ trapFocusRef: RefObject<HTMLDivElement>
18
+ afterElementRef: RefObject<HTMLDivElement>
19
+ testId?: string
20
+ }
21
+
22
+ const ModalContentPure = ({
23
+ beforeElementRef,
24
+ trapFocusRef,
25
+ afterElementRef,
26
+ testId = 'store-modal-content',
27
+ children,
28
+ ...otherProps
29
+ }: ModalContentPureProps) => {
30
+ return (
31
+ <>
32
+ <div
33
+ ref={beforeElementRef}
34
+ data-testid="beforeElement"
35
+ tabIndex={0}
36
+ aria-hidden="true"
37
+ />
38
+ <div
39
+ data-fs-modal-content
40
+ data-testid={testId}
41
+ ref={trapFocusRef}
42
+ aria-modal="true"
43
+ role="dialog"
44
+ tabIndex={-1}
45
+ {...otherProps}
46
+ >
47
+ {children}
48
+ </div>
49
+ <div
50
+ ref={afterElementRef}
51
+ data-testid="afterElement"
52
+ tabIndex={0}
53
+ aria-hidden="true"
54
+ />
55
+ </>
56
+ )
57
+ }
58
+
59
+ export type ModalContentProps = Omit<
60
+ ModalContentPureProps,
61
+ 'trapFocusRef' | 'onClick' | 'beforeElementRef' | 'afterElementRef'
62
+ >
63
+
64
+ const ModalContent = ({ children, ...otherProps }: ModalContentProps) => {
65
+ const trapFocusRef = useRef<HTMLDivElement>(null)
66
+ const beforeElementRef = useRef<HTMLDivElement>(null)
67
+ const afterElementRef = useRef<HTMLDivElement>(null)
68
+
69
+ useTrapFocus({
70
+ beforeElementRef,
71
+ trapFocusRef,
72
+ afterElementRef,
73
+ })
74
+
75
+ return (
76
+ <ModalContentPure
77
+ {...otherProps}
78
+ trapFocusRef={trapFocusRef}
79
+ beforeElementRef={beforeElementRef}
80
+ afterElementRef={afterElementRef}
81
+ onClick={(event: MouseEvent) => {
82
+ event.stopPropagation()
83
+ }}
84
+ >
85
+ {children}
86
+ </ModalContentPure>
87
+ )
88
+ }
89
+
90
+ export default ModalContent