@duro-app/ui 5.12.0 → 5.13.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.
- package/dist/Table-Bms2ftt3.js +1694 -0
- package/dist/Table-Bms2ftt3.js.map +1 -0
- package/dist/components/AppShell/native.css.d.ts +53 -0
- package/dist/components/AppShell/native.css.d.ts.map +1 -0
- package/dist/components/Drawer/Drawer.d.ts +7 -35
- package/dist/components/Drawer/Drawer.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -1
- package/dist/components/Drawer/DrawerScroll.stories.d.ts.map +1 -1
- package/dist/components/Drawer/DrawerSnap.stories.d.ts +20 -0
- package/dist/components/Drawer/DrawerSnap.stories.d.ts.map +1 -0
- package/dist/components/Drawer/native.css.d.ts +55 -0
- package/dist/components/Drawer/native.css.d.ts.map +1 -0
- package/dist/components/Drawer/parts.css.d.ts +96 -0
- package/dist/components/Drawer/parts.css.d.ts.map +1 -0
- package/dist/components/Drawer/parts.d.ts +56 -0
- package/dist/components/Drawer/parts.d.ts.map +1 -0
- package/dist/components/Drawer/sheetPhysics.d.ts +56 -0
- package/dist/components/Drawer/sheetPhysics.d.ts.map +1 -0
- package/dist/components/Drawer/styles.css.d.ts +13 -58
- package/dist/components/Drawer/styles.css.d.ts.map +1 -1
- package/dist/components/Drawer/touchInput.d.ts +35 -0
- package/dist/components/Drawer/touchInput.d.ts.map +1 -0
- package/dist/components/Drawer/useDrawerRoot.d.ts +33 -0
- package/dist/components/Drawer/useDrawerRoot.d.ts.map +1 -0
- package/dist/components/Drawer/useSheetHeights.d.ts +7 -0
- package/dist/components/Drawer/useSheetHeights.d.ts.map +1 -0
- package/dist/components/Drawer/useSwipeDismiss.d.ts +13 -2
- package/dist/components/Drawer/useSwipeDismiss.d.ts.map +1 -1
- package/dist/components/Menu/Menu.d.ts +7 -1
- package/dist/components/Menu/Menu.d.ts.map +1 -1
- package/dist/components/Menu/native.css.d.ts +14 -0
- package/dist/components/Menu/native.css.d.ts.map +1 -0
- package/dist/components/Menu/useMenuOpen.d.ts +16 -0
- package/dist/components/Menu/useMenuOpen.d.ts.map +1 -0
- package/dist/components/Menu/useMenuRoot.d.ts +3 -6
- package/dist/components/Menu/useMenuRoot.d.ts.map +1 -1
- package/dist/components/Popover/native.css.d.ts +11 -0
- package/dist/components/Popover/native.css.d.ts.map +1 -0
- package/dist/components/SideNav/Chevron.d.ts +4 -0
- package/dist/components/SideNav/Chevron.d.ts.map +1 -0
- package/dist/components/SideNav/SideNav.d.ts.map +1 -1
- package/dist/components/SideNav/styles.css.d.ts +6 -0
- package/dist/components/SideNav/styles.css.d.ts.map +1 -1
- package/dist/components/SideNav/types.d.ts +6 -0
- package/dist/components/SideNav/types.d.ts.map +1 -0
- package/dist/components/SplitPane/SplitPane.d.ts +38 -0
- package/dist/components/SplitPane/SplitPane.d.ts.map +1 -0
- package/dist/components/SplitPane/SplitPane.meta.d.ts +3 -0
- package/dist/components/SplitPane/SplitPane.meta.d.ts.map +1 -0
- package/dist/components/SplitPane/SplitPane.stories.d.ts +17 -0
- package/dist/components/SplitPane/SplitPane.stories.d.ts.map +1 -0
- package/dist/components/SplitPane/styles.css.d.ts +78 -0
- package/dist/components/SplitPane/styles.css.d.ts.map +1 -0
- package/dist/components/Tabs/TabLayout.d.ts +7 -0
- package/dist/components/Tabs/TabLayout.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.d.ts.map +1 -1
- package/dist/components/Tabs/indicator.d.ts +28 -0
- package/dist/components/Tabs/indicator.d.ts.map +1 -0
- package/dist/components/Tabs/styles.css.d.ts +4 -0
- package/dist/components/Tabs/styles.css.d.ts.map +1 -1
- package/dist/components/Tabs/useTabIndicator.d.ts +4 -0
- package/dist/components/Tabs/useTabIndicator.d.ts.map +1 -0
- package/dist/components/Tabs/useTabKeys.d.ts +13 -0
- package/dist/components/Tabs/useTabKeys.d.ts.map +1 -0
- package/dist/components/Tag/RemoveGlyph.d.ts +4 -0
- package/dist/components/Tag/RemoveGlyph.d.ts.map +1 -0
- package/dist/components/Tag/Tag.d.ts +2 -24
- package/dist/components/Tag/Tag.d.ts.map +1 -1
- package/dist/components/Tag/styles.css.d.ts +8 -0
- package/dist/components/Tag/styles.css.d.ts.map +1 -1
- package/dist/components/Tag/types.d.ts +39 -0
- package/dist/components/Tag/types.d.ts.map +1 -0
- package/dist/components/Toast/ToastProvider.d.ts +2 -21
- package/dist/components/Toast/ToastProvider.d.ts.map +1 -1
- package/dist/components/Toast/native.css.d.ts +81 -0
- package/dist/components/Toast/native.css.d.ts.map +1 -0
- package/dist/components/Toast/store.d.ts +47 -0
- package/dist/components/Toast/store.d.ts.map +1 -0
- package/dist/components/Tooltip/TooltipHint.d.ts +9 -0
- package/dist/components/Tooltip/TooltipHint.d.ts.map +1 -0
- package/dist/components/Tree/Tree.d.ts.map +1 -1
- package/dist/components/Tree/styles.css.d.ts +6 -0
- package/dist/components/Tree/styles.css.d.ts.map +1 -1
- package/dist/components/Tree/useTreeKeyboard.d.ts +19 -0
- package/dist/components/Tree/useTreeKeyboard.d.ts.map +1 -0
- package/dist/components/VirtualList/VirtualList.d.ts +33 -0
- package/dist/components/VirtualList/VirtualList.d.ts.map +1 -0
- package/dist/components/VirtualList/VirtualList.meta.d.ts +3 -0
- package/dist/components/VirtualList/VirtualList.meta.d.ts.map +1 -0
- package/dist/components/VirtualList/VirtualList.stories.d.ts +15 -0
- package/dist/components/VirtualList/VirtualList.stories.d.ts.map +1 -0
- package/dist/components/VirtualList/styles.css.d.ts +28 -0
- package/dist/components/VirtualList/styles.css.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4935 -4260
- package/dist/index.js.map +1 -1
- package/dist/shared/ButtonHost.d.ts +11 -0
- package/dist/shared/ButtonHost.d.ts.map +1 -0
- package/dist/shared/pressableChild.d.ts +14 -0
- package/dist/shared/pressableChild.d.ts.map +1 -0
- package/dist/table.js +1 -1
- package/package.json +2 -2
- package/src/components/AppShell/AppShell.meta.ts +1 -1
- package/src/components/AppShell/AppShell.native.tsx +174 -0
- package/src/components/AppShell/native.css.ts +56 -0
- package/src/components/Button/Button.tsx +3 -3
- package/src/components/Drawer/Drawer.meta.ts +2 -1
- package/src/components/Drawer/Drawer.native.tsx +469 -0
- package/src/components/Drawer/Drawer.tsx +58 -145
- package/src/components/Drawer/DrawerScroll.stories.tsx +4 -54
- package/src/components/Drawer/DrawerSnap.stories.tsx +205 -0
- package/src/components/Drawer/native.css.ts +58 -0
- package/src/components/Drawer/parts.css.ts +125 -0
- package/src/components/Drawer/parts.tsx +110 -0
- package/src/components/Drawer/sheetPhysics.ts +103 -0
- package/src/components/Drawer/styles.css.ts +15 -74
- package/src/components/Drawer/touchInput.ts +81 -0
- package/src/components/Drawer/useDrawerRoot.ts +90 -0
- package/src/components/Drawer/useSheetHeights.ts +42 -0
- package/src/components/Drawer/useSwipeDismiss.ts +146 -23
- package/src/components/Menu/Menu.meta.ts +1 -1
- package/src/components/Menu/Menu.native.tsx +268 -0
- package/src/components/Menu/Menu.tsx +11 -2
- package/src/components/Menu/native.css.ts +18 -0
- package/src/components/Menu/useMenuOpen.ts +33 -0
- package/src/components/Menu/useMenuRoot.ts +7 -26
- package/src/components/Popover/Popover.meta.ts +1 -1
- package/src/components/Popover/Popover.native.tsx +205 -0
- package/src/components/Popover/native.css.ts +13 -0
- package/src/components/SideNav/Chevron.native.tsx +20 -0
- package/src/components/SideNav/Chevron.tsx +20 -0
- package/src/components/SideNav/SideNav.meta.ts +1 -1
- package/src/components/SideNav/SideNav.tsx +57 -41
- package/src/components/SideNav/styles.css.ts +8 -0
- package/src/components/SideNav/types.ts +6 -0
- package/src/components/SplitPane/SplitPane.meta.ts +40 -0
- package/src/components/SplitPane/SplitPane.native.tsx +47 -0
- package/src/components/SplitPane/SplitPane.stories.tsx +148 -0
- package/src/components/SplitPane/SplitPane.tsx +218 -0
- package/src/components/SplitPane/styles.css.ts +81 -0
- package/src/components/Tabs/TabLayout.native.tsx +9 -0
- package/src/components/Tabs/TabLayout.tsx +6 -0
- package/src/components/Tabs/Tabs.meta.ts +1 -1
- package/src/components/Tabs/Tabs.tsx +38 -136
- package/src/components/Tabs/indicator.ts +36 -0
- package/src/components/Tabs/styles.css.ts +7 -0
- package/src/components/Tabs/useTabIndicator.native.ts +35 -0
- package/src/components/Tabs/useTabIndicator.ts +49 -0
- package/src/components/Tabs/useTabKeys.native.ts +2 -0
- package/src/components/Tabs/useTabKeys.ts +99 -0
- package/src/components/Tag/RemoveGlyph.native.tsx +17 -0
- package/src/components/Tag/RemoveGlyph.tsx +10 -0
- package/src/components/Tag/Tag.meta.ts +1 -1
- package/src/components/Tag/Tag.native.tsx +70 -0
- package/src/components/Tag/Tag.tsx +9 -37
- package/src/components/Tag/styles.css.ts +9 -0
- package/src/components/Tag/types.ts +43 -0
- package/src/components/Toast/Toast.meta.ts +1 -1
- package/src/components/Toast/ToastProvider.native.tsx +226 -0
- package/src/components/Toast/ToastProvider.tsx +22 -84
- package/src/components/Toast/native.css.ts +78 -0
- package/src/components/Toast/store.ts +96 -0
- package/src/components/Toggle/Toggle.tsx +3 -3
- package/src/components/Tooltip/Tooltip.meta.ts +1 -1
- package/src/components/Tooltip/Tooltip.native.tsx +36 -0
- package/src/components/Tooltip/TooltipHint.ts +13 -0
- package/src/components/Tree/Tree.meta.ts +1 -1
- package/src/components/Tree/Tree.tsx +23 -135
- package/src/components/Tree/styles.css.ts +6 -0
- package/src/components/Tree/useTreeKeyboard.native.ts +5 -0
- package/src/components/Tree/useTreeKeyboard.ts +152 -0
- package/src/components/VirtualList/VirtualList.meta.ts +39 -0
- package/src/components/VirtualList/VirtualList.stories.tsx +95 -0
- package/src/components/VirtualList/VirtualList.tsx +103 -0
- package/src/components/VirtualList/styles.css.ts +31 -0
- package/src/hooks/useContainerBelow.native.ts +30 -0
- package/src/index.native.ts +33 -0
- package/src/index.ts +8 -1
- package/src/shared/ButtonHost.native.tsx +70 -0
- package/src/shared/ButtonHost.tsx +12 -0
- package/src/shared/portal/AnchoredOverlay.native.tsx +117 -0
- package/src/shared/pressableChild.ts +33 -0
- package/src/shared/useKeyboardHeight.native.ts +18 -0
- package/src/shared/useScreenReaderEnabled.native.ts +19 -0
- package/dist/Table-ColfpgKV.js +0 -1552
- package/dist/Table-ColfpgKV.js.map +0 -1
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
import {createPortal} from 'react-dom'
|
|
2
|
-
import {
|
|
3
|
-
type ReactNode,
|
|
4
|
-
type RefObject,
|
|
5
|
-
createContext,
|
|
6
|
-
useContext,
|
|
7
|
-
useState,
|
|
8
|
-
useEffect,
|
|
9
|
-
useCallback,
|
|
10
|
-
useId,
|
|
11
|
-
useRef,
|
|
12
|
-
} from 'react'
|
|
2
|
+
import {type ReactNode, useState, useEffect, useCallback} from 'react'
|
|
13
3
|
import {html} from 'react-strict-dom'
|
|
14
4
|
import type {DurationToken} from '@duro-app/tokens/keys'
|
|
15
5
|
import {styles} from './styles.css'
|
|
16
|
-
import {useOverlayOpenState} from '../../shared/useOverlayOpenState'
|
|
17
6
|
import {usePortalMount} from '../ThemeProvider/ThemeProvider'
|
|
18
7
|
import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
|
|
19
8
|
import {ModalContext} from '../../shared/ModalContext'
|
|
20
9
|
import {useSwipeDismiss} from './useSwipeDismiss'
|
|
10
|
+
import {useSheetHeights} from './useSheetHeights'
|
|
11
|
+
import {cycleDetent} from './sheetPhysics'
|
|
12
|
+
import {partStyles} from './parts.css'
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_GRABBER_LABELS,
|
|
15
|
+
Description,
|
|
16
|
+
DrawerContext,
|
|
17
|
+
Footer,
|
|
18
|
+
Header,
|
|
19
|
+
Title,
|
|
20
|
+
useDrawer,
|
|
21
|
+
type DrawerSize,
|
|
22
|
+
type GrabberLabels,
|
|
23
|
+
} from './parts'
|
|
24
|
+
import {useDrawerRoot, type DrawerRootProps} from './useDrawerRoot'
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export type DrawerAnchor = 'right' | 'left' | 'bottom'
|
|
25
|
-
export type DrawerSize = 'sm' | 'md' | 'lg'
|
|
26
|
+
export type {DrawerAnchor, DrawerSize, SnapPoint} from './parts'
|
|
26
27
|
|
|
27
28
|
const closeDurationMap = {
|
|
28
29
|
instant: styles.closeDurationInstant,
|
|
@@ -34,79 +35,11 @@ const closeDurationMap = {
|
|
|
34
35
|
slow: styles.closeDurationSlow,
|
|
35
36
|
} as const satisfies Record<DurationToken, unknown>
|
|
36
37
|
|
|
37
|
-
// --- Context ---
|
|
38
|
-
|
|
39
|
-
interface DrawerContextValue {
|
|
40
|
-
open: boolean
|
|
41
|
-
closing: boolean
|
|
42
|
-
closeDuration: DurationToken
|
|
43
|
-
anchor: DrawerAnchor
|
|
44
|
-
dismissable: boolean
|
|
45
|
-
swipeDismiss: boolean
|
|
46
|
-
requestOpen: () => void
|
|
47
|
-
requestClose: () => void
|
|
48
|
-
/** Close immediately without CSS animation (used by swipe dismiss which handles its own exit animation) */
|
|
49
|
-
requestCloseImmediate: () => void
|
|
50
|
-
titleId: string
|
|
51
|
-
descriptionId: string
|
|
52
|
-
panelRef: RefObject<HTMLDivElement | null>
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const DrawerContext = createContext<DrawerContextValue | null>(null)
|
|
56
|
-
|
|
57
|
-
function useDrawer() {
|
|
58
|
-
const ctx = useContext(DrawerContext)
|
|
59
|
-
if (!ctx) throw new Error('Drawer compound components must be used within Drawer.Root')
|
|
60
|
-
return ctx
|
|
61
|
-
}
|
|
62
|
-
|
|
63
38
|
// --- Root ---
|
|
64
39
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
open?: boolean
|
|
69
|
-
/** Default open state (uncontrolled) */
|
|
70
|
-
defaultOpen?: boolean
|
|
71
|
-
/** Called when drawer open state changes */
|
|
72
|
-
onOpenChange?: (open: boolean) => void
|
|
73
|
-
/** Which edge the drawer slides from. Default: 'right' */
|
|
74
|
-
anchor?: DrawerAnchor
|
|
75
|
-
/** Whether clicking backdrop or pressing Escape closes the drawer. Default: true */
|
|
76
|
-
dismissable?: boolean
|
|
77
|
-
/** Whether swipe gesture dismisses the drawer. Default: true */
|
|
78
|
-
swipeDismiss?: boolean
|
|
79
|
-
/** Motion token for the close animation. Default: 'fast' (150ms) */
|
|
80
|
-
closeAnimationDuration?: DurationToken
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function Root({
|
|
84
|
-
children,
|
|
85
|
-
open: controlledOpen,
|
|
86
|
-
defaultOpen = false,
|
|
87
|
-
onOpenChange,
|
|
88
|
-
anchor = 'right',
|
|
89
|
-
dismissable = true,
|
|
90
|
-
swipeDismiss = true,
|
|
91
|
-
closeAnimationDuration = 'fast',
|
|
92
|
-
}: RootProps) {
|
|
93
|
-
const {
|
|
94
|
-
present,
|
|
95
|
-
closing,
|
|
96
|
-
requestOpen,
|
|
97
|
-
requestClose,
|
|
98
|
-
closeNow: requestCloseImmediate,
|
|
99
|
-
} = useOverlayOpenState({
|
|
100
|
-
open: controlledOpen,
|
|
101
|
-
defaultOpen,
|
|
102
|
-
onOpenChange,
|
|
103
|
-
exitDuration: closeAnimationDuration,
|
|
104
|
-
closeNotice: 'after-exit',
|
|
105
|
-
})
|
|
106
|
-
const panelRef = useRef<HTMLDivElement>(null)
|
|
107
|
-
|
|
108
|
-
const titleId = useId()
|
|
109
|
-
const descriptionId = useId()
|
|
40
|
+
function Root({children, ...props}: DrawerRootProps) {
|
|
41
|
+
const ctx = useDrawerRoot(props)
|
|
42
|
+
const {open: present, dismissable, requestClose} = ctx
|
|
110
43
|
|
|
111
44
|
// Handle Escape key
|
|
112
45
|
useEffect(() => {
|
|
@@ -134,26 +67,7 @@ function Root({
|
|
|
134
67
|
}
|
|
135
68
|
}, [present])
|
|
136
69
|
|
|
137
|
-
return
|
|
138
|
-
<DrawerContext.Provider
|
|
139
|
-
value={{
|
|
140
|
-
open: present,
|
|
141
|
-
closing,
|
|
142
|
-
closeDuration: closeAnimationDuration,
|
|
143
|
-
anchor,
|
|
144
|
-
dismissable,
|
|
145
|
-
swipeDismiss,
|
|
146
|
-
requestOpen,
|
|
147
|
-
requestClose,
|
|
148
|
-
requestCloseImmediate,
|
|
149
|
-
titleId,
|
|
150
|
-
descriptionId,
|
|
151
|
-
panelRef,
|
|
152
|
-
}}
|
|
153
|
-
>
|
|
154
|
-
{children}
|
|
155
|
-
</DrawerContext.Provider>
|
|
156
|
-
)
|
|
70
|
+
return <DrawerContext.Provider value={ctx}>{children}</DrawerContext.Provider>
|
|
157
71
|
}
|
|
158
72
|
|
|
159
73
|
// --- Trigger ---
|
|
@@ -162,7 +76,7 @@ function Trigger({children}: {children: ReactNode}) {
|
|
|
162
76
|
const {requestOpen} = useDrawer()
|
|
163
77
|
|
|
164
78
|
return (
|
|
165
|
-
<html.div onClick={requestOpen} style={
|
|
79
|
+
<html.div onClick={requestOpen} style={partStyles.inlineWrapper}>
|
|
166
80
|
{children}
|
|
167
81
|
</html.div>
|
|
168
82
|
)
|
|
@@ -173,6 +87,8 @@ function Trigger({children}: {children: ReactNode}) {
|
|
|
173
87
|
interface PortalProps {
|
|
174
88
|
children: ReactNode
|
|
175
89
|
size?: DrawerSize
|
|
90
|
+
/** The grabber's accessible names, by what a press does next. Snap sheets only */
|
|
91
|
+
grabberLabels?: GrabberLabels
|
|
176
92
|
}
|
|
177
93
|
|
|
178
94
|
// Slide animation maps
|
|
@@ -194,7 +110,7 @@ const viewportAnchorMap = {
|
|
|
194
110
|
bottom: styles.viewportBottom,
|
|
195
111
|
} as const
|
|
196
112
|
|
|
197
|
-
function Portal({children, size = 'md'}: PortalProps) {
|
|
113
|
+
function Portal({children, size = 'md', grabberLabels = DEFAULT_GRABBER_LABELS}: PortalProps) {
|
|
198
114
|
const {
|
|
199
115
|
open,
|
|
200
116
|
closing,
|
|
@@ -207,8 +123,14 @@ function Portal({children, size = 'md'}: PortalProps) {
|
|
|
207
123
|
titleId,
|
|
208
124
|
descriptionId,
|
|
209
125
|
panelRef,
|
|
126
|
+
snapPoints,
|
|
127
|
+
snapIndex,
|
|
128
|
+
setSnapIndex,
|
|
210
129
|
} = useDrawer()
|
|
211
130
|
const mount = usePortalMount()
|
|
131
|
+
const snapping = snapPoints.length > 0
|
|
132
|
+
const heights = useSheetHeights(snapPoints, snapping && open)
|
|
133
|
+
const sized = snapping && heights.length === snapPoints.length
|
|
212
134
|
// The shown panel element, tracked as state so swipe listeners follow it
|
|
213
135
|
// when the portal mount arrives and the panel is remounted into it.
|
|
214
136
|
const [panel, setPanel] = useState<HTMLDivElement | null>(null)
|
|
@@ -232,6 +154,7 @@ function Portal({children, size = 'md'}: PortalProps) {
|
|
|
232
154
|
enabled: swipeEnabled && open && !closing,
|
|
233
155
|
onDismiss: requestCloseImmediate,
|
|
234
156
|
panel,
|
|
157
|
+
snap: sized ? {heights, index: snapIndex, onSnap: setSnapIndex} : null,
|
|
235
158
|
})
|
|
236
159
|
|
|
237
160
|
if (!open) return null
|
|
@@ -265,12 +188,29 @@ function Portal({children, size = 'md'}: PortalProps) {
|
|
|
265
188
|
isHorizontal && styles.panelHorizontal,
|
|
266
189
|
isHorizontal && styles[size],
|
|
267
190
|
!isHorizontal && styles.panelVertical,
|
|
191
|
+
sized && styles.panelSnap,
|
|
192
|
+
sized && styles.sheetHeight(heights[snapIndex]),
|
|
268
193
|
anchor === 'left' && styles.panelLeftBorder,
|
|
269
194
|
closing && slideOutMap[anchor],
|
|
270
195
|
closing && closeDurationMap[closeDuration],
|
|
271
196
|
!closing && slideInMap[anchor],
|
|
272
197
|
]}
|
|
273
198
|
>
|
|
199
|
+
{snapping && (
|
|
200
|
+
<html.button
|
|
201
|
+
type="button"
|
|
202
|
+
data-drawer-grabber=""
|
|
203
|
+
aria-label={
|
|
204
|
+
cycleDetent(snapIndex, snapPoints.length) > snapIndex
|
|
205
|
+
? grabberLabels.expand
|
|
206
|
+
: grabberLabels.collapse
|
|
207
|
+
}
|
|
208
|
+
onClick={() => setSnapIndex(cycleDetent(snapIndex, snapPoints.length))}
|
|
209
|
+
style={[partStyles.grabberHit, styles.grabberHitWeb]}
|
|
210
|
+
>
|
|
211
|
+
<html.span style={partStyles.grabber} />
|
|
212
|
+
</html.button>
|
|
213
|
+
)}
|
|
274
214
|
<ModalContext.Provider value={true}>
|
|
275
215
|
<ControlContextBoundary>{children}</ControlContextBoundary>
|
|
276
216
|
</ModalContext.Provider>
|
|
@@ -287,46 +227,19 @@ function Portal({children, size = 'md'}: PortalProps) {
|
|
|
287
227
|
return mount ? createPortal(node, mount) : node
|
|
288
228
|
}
|
|
289
229
|
|
|
290
|
-
// ---
|
|
291
|
-
|
|
292
|
-
function Header({children}: {children: ReactNode}) {
|
|
293
|
-
return <html.div style={styles.header}>{children}</html.div>
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// --- Title ---
|
|
297
|
-
|
|
298
|
-
function Title({children}: {children: ReactNode}) {
|
|
299
|
-
const {titleId} = useDrawer()
|
|
300
|
-
return (
|
|
301
|
-
<html.h2 id={titleId} style={styles.title}>
|
|
302
|
-
{children}
|
|
303
|
-
</html.h2>
|
|
304
|
-
)
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// --- Description ---
|
|
230
|
+
// --- Body ---
|
|
308
231
|
|
|
309
|
-
function
|
|
310
|
-
const {
|
|
232
|
+
function Body({children}: {children: ReactNode}) {
|
|
233
|
+
const {snapPoints} = useDrawer()
|
|
234
|
+
// A snap sheet's Body is its scroll viewport: a drag there scrolls it, and
|
|
235
|
+
// moves the sheet only from the top edge (or upward, below the largest detent).
|
|
311
236
|
return (
|
|
312
|
-
<html.
|
|
237
|
+
<html.div style={styles.body} data-duro-scroll={snapPoints.length > 0 ? '' : undefined}>
|
|
313
238
|
{children}
|
|
314
|
-
</html.
|
|
239
|
+
</html.div>
|
|
315
240
|
)
|
|
316
241
|
}
|
|
317
242
|
|
|
318
|
-
// --- Body ---
|
|
319
|
-
|
|
320
|
-
function Body({children}: {children: ReactNode}) {
|
|
321
|
-
return <html.div style={styles.body}>{children}</html.div>
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// --- Footer ---
|
|
325
|
-
|
|
326
|
-
function Footer({children}: {children: ReactNode}) {
|
|
327
|
-
return <html.div style={styles.footer}>{children}</html.div>
|
|
328
|
-
}
|
|
329
|
-
|
|
330
243
|
// --- Close ---
|
|
331
244
|
|
|
332
245
|
interface CloseProps {
|
|
@@ -339,14 +252,14 @@ function Close({children, 'aria-label': ariaLabel = 'Close'}: CloseProps) {
|
|
|
339
252
|
|
|
340
253
|
if (children) {
|
|
341
254
|
return (
|
|
342
|
-
<html.div onClick={requestClose} style={
|
|
255
|
+
<html.div onClick={requestClose} style={partStyles.inlineWrapper}>
|
|
343
256
|
{children}
|
|
344
257
|
</html.div>
|
|
345
258
|
)
|
|
346
259
|
}
|
|
347
260
|
|
|
348
261
|
return (
|
|
349
|
-
<html.button onClick={requestClose} aria-label={ariaLabel} style={
|
|
262
|
+
<html.button onClick={requestClose} aria-label={ariaLabel} style={partStyles.closeButton}>
|
|
350
263
|
<svg width={16} height={16} viewBox="0 0 16 16" fill="none">
|
|
351
264
|
<path
|
|
352
265
|
d="M4 4l8 8M12 4l-8 8"
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
2
|
import {expect, waitFor} from 'storybook/test'
|
|
3
|
-
import {cdp, page} from 'vitest/browser'
|
|
4
3
|
import {Drawer} from './Drawer'
|
|
5
4
|
import {ToggleGroup} from '../ToggleGroup/ToggleGroup'
|
|
6
5
|
import {Toggle} from '../Toggle/Toggle'
|
|
7
6
|
import {Icon, type IconName} from '../Icon'
|
|
8
7
|
import {SIZES_PX} from '@duro-app/tokens/keys'
|
|
8
|
+
import {cdpSend, pause, setViewport, toPage, touchDrag, type Point} from './touchInput'
|
|
9
9
|
|
|
10
10
|
const meta: Meta = {
|
|
11
11
|
title: 'Components/Drawer/Scroll',
|
|
@@ -87,56 +87,6 @@ function Scene({pressed}: {pressed?: string} = {}) {
|
|
|
87
87
|
)
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
type Point = {x: number; y: number}
|
|
91
|
-
|
|
92
|
-
/** `CDPSession` is empty until the provider's typings load; `send` exists at runtime. */
|
|
93
|
-
const cdpSend = (method: string, params: object) =>
|
|
94
|
-
(cdp() as unknown as {send: (method: string, params: object) => Promise<unknown>}).send(
|
|
95
|
-
method,
|
|
96
|
-
params,
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
const pause = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* CDP input lands in top-level page coordinates; the story runs in an iframe
|
|
103
|
-
* the test runner offsets and may scale to fit. Map a point up every frame.
|
|
104
|
-
*/
|
|
105
|
-
function toPage(p: Point): Point {
|
|
106
|
-
let {x, y} = p
|
|
107
|
-
let win: Window = window
|
|
108
|
-
while (win.frameElement && win.parent !== win) {
|
|
109
|
-
const rect = win.frameElement.getBoundingClientRect()
|
|
110
|
-
const scale = rect.width / win.innerWidth
|
|
111
|
-
x = rect.left + x * scale
|
|
112
|
-
y = rect.top + y * scale
|
|
113
|
-
win = win.parent
|
|
114
|
-
}
|
|
115
|
-
return {x, y}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* A page-coordinate touch point with a 1px radius: Chrome's touch adjustment
|
|
120
|
-
* snaps a fat default touch onto the nearest control, which would turn every
|
|
121
|
-
* drag in the list into a drag on a toggle.
|
|
122
|
-
*/
|
|
123
|
-
const touchAt = (p: Point) => ({...toPage(p), radiusX: 1, radiusY: 1})
|
|
124
|
-
|
|
125
|
-
/** Real touch drag through CDP; synthetic pointer events don't scroll. */
|
|
126
|
-
async function touchDrag(from: Point, to: Point) {
|
|
127
|
-
const steps = 12
|
|
128
|
-
const send = (type: string, touchPoints: Point[]) =>
|
|
129
|
-
cdpSend('Input.dispatchTouchEvent', {type, touchPoints: touchPoints.map(touchAt)})
|
|
130
|
-
await send('touchStart', [from])
|
|
131
|
-
for (let i = 1; i <= steps; i++) {
|
|
132
|
-
await send('touchMove', [
|
|
133
|
-
{x: from.x + ((to.x - from.x) * i) / steps, y: from.y + ((to.y - from.y) * i) / steps},
|
|
134
|
-
])
|
|
135
|
-
await pause(16)
|
|
136
|
-
}
|
|
137
|
-
await send('touchEnd', [])
|
|
138
|
-
}
|
|
139
|
-
|
|
140
90
|
const getViewport = () => document.querySelector<HTMLElement>('[data-duro-scroll]')!
|
|
141
91
|
const getDialog = () => document.querySelector('[role="dialog"]')
|
|
142
92
|
|
|
@@ -158,7 +108,7 @@ function freePoint(el: HTMLElement, fy: number): Point {
|
|
|
158
108
|
}
|
|
159
109
|
|
|
160
110
|
async function setup() {
|
|
161
|
-
await
|
|
111
|
+
await setViewport(390, 844)
|
|
162
112
|
await cdpSend('Emulation.setTouchEmulationEnabled', {enabled: true, maxTouchPoints: 1})
|
|
163
113
|
await waitFor(() => expect(getViewport()).toBeTruthy())
|
|
164
114
|
const viewport = getViewport()
|
|
@@ -242,7 +192,7 @@ export const TapOnToggleStillPresses: Story = {
|
|
|
242
192
|
export const MountKeepsTheDrawerStill: Story = {
|
|
243
193
|
// size the page before the render: a drawer mounts at its final width
|
|
244
194
|
beforeEach: async () => {
|
|
245
|
-
await
|
|
195
|
+
await setViewport(390, 844)
|
|
246
196
|
},
|
|
247
197
|
render: () => <Scene pressed={ICON_NAMES[39]} />,
|
|
248
198
|
play: async () => {
|
|
@@ -278,7 +228,7 @@ export const CoarsePointerRows: Story = {
|
|
|
278
228
|
),
|
|
279
229
|
play: async () => {
|
|
280
230
|
// phone width, so 42 icons overflow three rows
|
|
281
|
-
await
|
|
231
|
+
await setViewport(390, 844)
|
|
282
232
|
await cdpSend('Emulation.setTouchEmulationEnabled', {enabled: true, maxTouchPoints: 1})
|
|
283
233
|
await waitFor(() => expect(matchMedia('(pointer: coarse)').matches).toBe(true))
|
|
284
234
|
const viewport = getViewport()
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
|
+
import {expect, fn, userEvent, waitFor} from 'storybook/test'
|
|
3
|
+
import {Drawer, type SnapPoint} from './Drawer'
|
|
4
|
+
import {Button} from '../Button/Button'
|
|
5
|
+
import {List} from '../List/List'
|
|
6
|
+
import {cdpSend, centre, pause, setViewport, touchDrag, type Point} from './touchInput'
|
|
7
|
+
|
|
8
|
+
const meta: Meta = {
|
|
9
|
+
title: 'Components/Drawer/Snap points',
|
|
10
|
+
parameters: {layout: 'fullscreen'},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default meta
|
|
14
|
+
type Story = StoryObj
|
|
15
|
+
|
|
16
|
+
const SESSIONS = Array.from({length: 40}, (_, i) => `Session ${i + 1}`)
|
|
17
|
+
|
|
18
|
+
interface SheetArgs {
|
|
19
|
+
snapPoints: SnapPoint[]
|
|
20
|
+
defaultSnapPoint: SnapPoint
|
|
21
|
+
titleAlign: 'start' | 'center'
|
|
22
|
+
onOpenChange?: (open: boolean) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEFAULT_ARGS: SheetArgs = {
|
|
26
|
+
snapPoints: ['medium', 'large'],
|
|
27
|
+
defaultSnapPoint: 'medium',
|
|
28
|
+
titleAlign: 'center',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The picked B0 direction (docs/mockups/native-sheet/Done.dc.html): grabber, centred title, a text Done. */
|
|
32
|
+
function Sheet(props: Partial<SheetArgs>) {
|
|
33
|
+
const {snapPoints, defaultSnapPoint, titleAlign, onOpenChange} = {...DEFAULT_ARGS, ...props}
|
|
34
|
+
return (
|
|
35
|
+
<Drawer.Root
|
|
36
|
+
anchor="bottom"
|
|
37
|
+
snapPoints={snapPoints}
|
|
38
|
+
defaultSnapPoint={defaultSnapPoint}
|
|
39
|
+
onOpenChange={onOpenChange}
|
|
40
|
+
defaultOpen
|
|
41
|
+
>
|
|
42
|
+
<Drawer.Portal>
|
|
43
|
+
<Drawer.Header titleAlign={titleAlign}>
|
|
44
|
+
<Drawer.Title>Move to project</Drawer.Title>
|
|
45
|
+
<Drawer.Close>
|
|
46
|
+
<Button variant="link">Done</Button>
|
|
47
|
+
</Drawer.Close>
|
|
48
|
+
</Drawer.Header>
|
|
49
|
+
<Drawer.Body>
|
|
50
|
+
<List.Root aria-label="Sessions">
|
|
51
|
+
{SESSIONS.map((s) => (
|
|
52
|
+
<List.Item key={s}>
|
|
53
|
+
<List.Text>{s}</List.Text>
|
|
54
|
+
</List.Item>
|
|
55
|
+
))}
|
|
56
|
+
</List.Root>
|
|
57
|
+
</Drawer.Body>
|
|
58
|
+
</Drawer.Portal>
|
|
59
|
+
</Drawer.Root>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const SnapSheet: StoryObj<SheetArgs> = {
|
|
64
|
+
args: {...DEFAULT_ARGS, onOpenChange: fn()},
|
|
65
|
+
argTypes: {
|
|
66
|
+
snapPoints: {
|
|
67
|
+
control: 'check',
|
|
68
|
+
options: ['medium', 'large'],
|
|
69
|
+
description: "The detents: 'medium' half the window, 'large' the window less its top inset",
|
|
70
|
+
},
|
|
71
|
+
defaultSnapPoint: {
|
|
72
|
+
control: 'inline-radio',
|
|
73
|
+
options: ['medium', 'large'],
|
|
74
|
+
description: 'The detent the sheet opens at',
|
|
75
|
+
},
|
|
76
|
+
titleAlign: {
|
|
77
|
+
control: 'inline-radio',
|
|
78
|
+
options: ['start', 'center'],
|
|
79
|
+
description: 'Drawer.Header: the title at the start, or centred with the close at the end',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
// Keyed by the opening detent: it seeds state on mount, so a change remounts the sheet.
|
|
83
|
+
render: (args) => (
|
|
84
|
+
<Sheet
|
|
85
|
+
key={args.defaultSnapPoint}
|
|
86
|
+
{...args}
|
|
87
|
+
snapPoints={args.snapPoints.length > 0 ? args.snapPoints : ['medium']}
|
|
88
|
+
/>
|
|
89
|
+
),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const getDialog = () => document.querySelector<HTMLElement>('[role="dialog"]')
|
|
93
|
+
const getGrabber = () => document.querySelector<HTMLElement>('[data-drawer-grabber]')!
|
|
94
|
+
const getBody = () => document.querySelector<HTMLElement>('[role="dialog"] [data-duro-scroll]')!
|
|
95
|
+
const sheetHeight = () => getDialog()!.getBoundingClientRect().height
|
|
96
|
+
|
|
97
|
+
const WINDOW_H = 844
|
|
98
|
+
const MEDIUM = WINDOW_H / 2
|
|
99
|
+
const LARGE = WINDOW_H // no top safe-area inset in a desktop browser
|
|
100
|
+
|
|
101
|
+
async function setup() {
|
|
102
|
+
await setViewport(390, WINDOW_H)
|
|
103
|
+
await cdpSend('Emulation.setTouchEmulationEnabled', {enabled: true, maxTouchPoints: 1})
|
|
104
|
+
await waitFor(() => expect(getDialog()).not.toBeNull())
|
|
105
|
+
// let the slide-in finish
|
|
106
|
+
await pause(500)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const near = (actual: number, expected: number) =>
|
|
110
|
+
expect(Math.abs(actual - expected)).toBeLessThanOrEqual(2)
|
|
111
|
+
|
|
112
|
+
async function dragGrabber(dy: number) {
|
|
113
|
+
const from: Point = centre(getGrabber())
|
|
114
|
+
await touchDrag(from, {x: from.x, y: from.y + dy})
|
|
115
|
+
await pause(400)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// One drag per story: a test that drags three times can outlast the runner's
|
|
119
|
+
// timeout on a loaded machine.
|
|
120
|
+
export const DragUpGrowsToLarge: Story = {
|
|
121
|
+
render: () => <Sheet />,
|
|
122
|
+
play: async () => {
|
|
123
|
+
await setup()
|
|
124
|
+
await waitFor(() => near(sheetHeight(), MEDIUM))
|
|
125
|
+
await dragGrabber(-300)
|
|
126
|
+
await waitFor(() => near(sheetHeight(), LARGE))
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const DragDownShrinksToMedium: Story = {
|
|
131
|
+
render: () => <Sheet defaultSnapPoint="large" />,
|
|
132
|
+
play: async () => {
|
|
133
|
+
await setup()
|
|
134
|
+
await waitFor(() => near(sheetHeight(), LARGE))
|
|
135
|
+
await dragGrabber(300)
|
|
136
|
+
await waitFor(() => near(sheetHeight(), MEDIUM))
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const DragDownBelowMediumDismisses: Story = {
|
|
141
|
+
render: () => <Sheet />,
|
|
142
|
+
play: async () => {
|
|
143
|
+
await setup()
|
|
144
|
+
await waitFor(() => near(sheetHeight(), MEDIUM))
|
|
145
|
+
await dragGrabber(300)
|
|
146
|
+
await waitFor(() => expect(getDialog()).toBeNull())
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const SlowShortDragSnapsBack: Story = {
|
|
151
|
+
render: () => <Sheet />,
|
|
152
|
+
play: async () => {
|
|
153
|
+
await setup()
|
|
154
|
+
const from = centre(getGrabber())
|
|
155
|
+
// 60px over 20 moves (about 0.2 px/ms): well under the fling speed, well
|
|
156
|
+
// short of the next detent
|
|
157
|
+
await touchDrag(from, {x: from.x, y: from.y - 60}, 20)
|
|
158
|
+
await pause(400)
|
|
159
|
+
await waitFor(() => near(sheetHeight(), MEDIUM))
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const GrabberTapCyclesDetents: Story = {
|
|
164
|
+
render: () => <Sheet />,
|
|
165
|
+
play: async () => {
|
|
166
|
+
await setup()
|
|
167
|
+
const grabber = getGrabber()
|
|
168
|
+
await expect(grabber).toHaveAccessibleName('Expand sheet')
|
|
169
|
+
await userEvent.click(grabber)
|
|
170
|
+
await waitFor(() => near(sheetHeight(), LARGE))
|
|
171
|
+
await expect(grabber).toHaveAccessibleName('Shrink sheet')
|
|
172
|
+
await userEvent.click(grabber)
|
|
173
|
+
await waitFor(() => near(sheetHeight(), MEDIUM))
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const DragUpOnContentGrowsThenScrolls: Story = {
|
|
178
|
+
render: () => <Sheet />,
|
|
179
|
+
play: async () => {
|
|
180
|
+
await setup()
|
|
181
|
+
const from = centre(getBody())
|
|
182
|
+
// At medium, the content's upward drag grows the sheet …
|
|
183
|
+
await touchDrag(from, {x: from.x, y: from.y - 250})
|
|
184
|
+
await pause(400)
|
|
185
|
+
await waitFor(() => near(sheetHeight(), LARGE))
|
|
186
|
+
await expect(getBody().scrollTop).toBe(0)
|
|
187
|
+
// … at the largest detent it scrolls the list.
|
|
188
|
+
const again = centre(getBody())
|
|
189
|
+
await touchDrag(again, {x: again.x, y: again.y - 120})
|
|
190
|
+
await pause(300)
|
|
191
|
+
await expect(getBody().scrollTop).toBeGreaterThan(0)
|
|
192
|
+
near(sheetHeight(), LARGE)
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const DoneCloses: Story = {
|
|
197
|
+
render: () => <Sheet />,
|
|
198
|
+
play: async () => {
|
|
199
|
+
await setup()
|
|
200
|
+
await userEvent.click(
|
|
201
|
+
document.querySelector('[role="dialog"] button:not([data-drawer-grabber])')!,
|
|
202
|
+
)
|
|
203
|
+
await waitFor(() => expect(getDialog()).toBeNull())
|
|
204
|
+
},
|
|
205
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
5
|
+
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Native Drawer surfaces. Motion is Reanimated's (plan N0, Decision 4): these
|
|
9
|
+
* styles draw the panel and backdrop, the animated views around them move them.
|
|
10
|
+
*/
|
|
11
|
+
export const nativeStyles = css.create({
|
|
12
|
+
fill: {
|
|
13
|
+
position: 'absolute',
|
|
14
|
+
top: 0,
|
|
15
|
+
left: 0,
|
|
16
|
+
right: 0,
|
|
17
|
+
bottom: 0,
|
|
18
|
+
},
|
|
19
|
+
scrim: {
|
|
20
|
+
backgroundColor: colors.scrim,
|
|
21
|
+
},
|
|
22
|
+
panel: {
|
|
23
|
+
flexGrow: 1,
|
|
24
|
+
backgroundColor: colors.bgCard,
|
|
25
|
+
boxShadow: shadows.lg,
|
|
26
|
+
display: 'flex',
|
|
27
|
+
flexDirection: 'column',
|
|
28
|
+
overflow: 'hidden',
|
|
29
|
+
},
|
|
30
|
+
panelBottom: {
|
|
31
|
+
borderTopWidth: borders.hairline,
|
|
32
|
+
borderTopStyle: 'solid',
|
|
33
|
+
borderTopColor: colors.border,
|
|
34
|
+
borderTopLeftRadius: radii.lg,
|
|
35
|
+
borderTopRightRadius: radii.lg,
|
|
36
|
+
},
|
|
37
|
+
panelRight: {
|
|
38
|
+
borderLeftWidth: borders.hairline,
|
|
39
|
+
borderLeftStyle: 'solid',
|
|
40
|
+
borderLeftColor: colors.border,
|
|
41
|
+
},
|
|
42
|
+
panelLeft: {
|
|
43
|
+
borderRightWidth: borders.hairline,
|
|
44
|
+
borderRightStyle: 'solid',
|
|
45
|
+
borderRightColor: colors.border,
|
|
46
|
+
},
|
|
47
|
+
// The safe-area insets (and a snap sheet's overshoot skirt), as padding.
|
|
48
|
+
insets: (top: number, bottom: number) => ({paddingTop: top, paddingBottom: bottom}),
|
|
49
|
+
// The Body's padding inside its ScrollView (no flex: it sizes to content).
|
|
50
|
+
bodyContent: {
|
|
51
|
+
paddingLeft: spacing.lg,
|
|
52
|
+
paddingRight: spacing.lg,
|
|
53
|
+
paddingBottom: spacing.lg,
|
|
54
|
+
},
|
|
55
|
+
// A snap sheet resting below its largest detent: its lower part is off
|
|
56
|
+
// screen, so the list scrolls that far further to show its end.
|
|
57
|
+
hiddenBelow: (height: number) => ({paddingBottom: height}),
|
|
58
|
+
})
|