@startlibs/components 1.0.4 → 1.1.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 (115) hide show
  1. package/lib/BasicBulletList.mjs +9 -0
  2. package/lib/BasicBulletList.mjs.map +1 -0
  3. package/lib/Button.mjs +95 -0
  4. package/lib/Button.mjs.map +1 -0
  5. package/lib/ContextMenu.mjs +452 -0
  6. package/lib/ContextMenu.mjs.map +1 -0
  7. package/lib/Dialog.mjs +232 -0
  8. package/lib/Dialog.mjs.map +1 -0
  9. package/lib/Draggable.mjs +880 -0
  10. package/lib/Draggable.mjs.map +1 -0
  11. package/lib/ErrorBoundary.mjs +108 -0
  12. package/lib/ErrorBoundary.mjs.map +1 -0
  13. package/lib/FillLastLineResizer.mjs +238 -0
  14. package/lib/FillLastLineResizer.mjs.map +1 -0
  15. package/lib/Flip.mjs +604 -0
  16. package/lib/Flip.mjs.map +1 -0
  17. package/lib/FlipList.mjs +186 -0
  18. package/lib/FlipList.mjs.map +1 -0
  19. package/lib/FormatDate.mjs +25 -0
  20. package/lib/FormatDate.mjs.map +1 -0
  21. package/lib/Icon.mjs +50 -0
  22. package/lib/Icon.mjs.map +1 -0
  23. package/lib/Image.mjs +41 -0
  24. package/lib/Image.mjs.map +1 -0
  25. package/lib/Link.mjs +116 -0
  26. package/lib/Link.mjs.map +1 -0
  27. package/lib/Loading.mjs +10 -0
  28. package/lib/Loading.mjs.map +1 -0
  29. package/lib/LoadingDiv.mjs +83 -0
  30. package/lib/LoadingDiv.mjs.map +1 -0
  31. package/lib/Notifications.mjs +63 -0
  32. package/lib/Notifications.mjs.map +1 -0
  33. package/lib/PageLoader.mjs +42 -0
  34. package/lib/PageLoader.mjs.map +1 -0
  35. package/lib/PersistentNotifications.mjs +130 -0
  36. package/lib/PersistentNotifications.mjs.map +1 -0
  37. package/lib/Popup.mjs +49 -0
  38. package/lib/Popup.mjs.map +1 -0
  39. package/lib/SplitColumnsContainer.mjs +43 -0
  40. package/lib/SplitColumnsContainer.mjs.map +1 -0
  41. package/lib/SwitchDiv.mjs +61 -0
  42. package/lib/SwitchDiv.mjs.map +1 -0
  43. package/lib/TextButton.mjs +41 -0
  44. package/lib/TextButton.mjs.map +1 -0
  45. package/lib/ToastNotifications.mjs +215 -0
  46. package/lib/ToastNotifications.mjs.map +1 -0
  47. package/lib/Tooltip.mjs +160 -0
  48. package/lib/Tooltip.mjs.map +1 -0
  49. package/lib/TransitionDiv.mjs +56 -0
  50. package/lib/TransitionDiv.mjs.map +1 -0
  51. package/lib/WarningDialog.mjs +84 -0
  52. package/lib/WarningDialog.mjs.map +1 -0
  53. package/lib/alt/buttons/OnClickLoadingButton.mjs +36 -0
  54. package/lib/alt/buttons/OnClickLoadingButton.mjs.map +1 -0
  55. package/lib/alt/dialogs/DialogWithImageHeader.mjs +14 -0
  56. package/lib/alt/dialogs/DialogWithImageHeader.mjs.map +1 -0
  57. package/lib/alt/dialogs/DialogWithSidebar.mjs +106 -0
  58. package/lib/alt/dialogs/DialogWithSidebar.mjs.map +1 -0
  59. package/lib/alt/dialogs/MobileBubbleDialog.mjs +49 -0
  60. package/lib/alt/dialogs/MobileBubbleDialog.mjs.map +1 -0
  61. package/lib/containers/DialogContainer.mjs +413 -0
  62. package/lib/containers/DialogContainer.mjs.map +1 -0
  63. package/lib/containers/Reposition.mjs +35 -0
  64. package/lib/containers/Reposition.mjs.map +1 -0
  65. package/lib/containers/usePositioning.mjs +294 -0
  66. package/lib/containers/usePositioning.mjs.map +1 -0
  67. package/lib/index.cjs +5000 -0
  68. package/lib/index.cjs.map +1 -0
  69. package/lib/index.mjs +37 -0
  70. package/lib/index.mjs.map +1 -0
  71. package/lib/useDialogWithToggle.mjs +18 -0
  72. package/lib/useDialogWithToggle.mjs.map +1 -0
  73. package/lib/willUseIsSticky.mjs +123 -0
  74. package/lib/willUseIsSticky.mjs.map +1 -0
  75. package/package.json +44 -29
  76. package/src/BasicBulletList.jsx +21 -0
  77. package/src/Button.jsx +288 -0
  78. package/src/ContextMenu.jsx +621 -0
  79. package/src/Dialog.jsx +413 -0
  80. package/src/Draggable.jsx +801 -0
  81. package/src/ErrorBoundary.jsx +75 -0
  82. package/src/FillLastLineResizer.jsx +226 -0
  83. package/src/Flip.jsx +560 -0
  84. package/src/FlipList.jsx +136 -0
  85. package/src/FormatDate.jsx +9 -0
  86. package/src/Icon.jsx +49 -0
  87. package/src/Image.jsx +23 -0
  88. package/src/Link.jsx +68 -0
  89. package/src/Loading.jsx +29 -0
  90. package/src/LoadingDiv.jsx +75 -0
  91. package/src/Notifications.jsx +89 -0
  92. package/src/PageLoader.jsx +48 -0
  93. package/src/PersistentNotifications.jsx +182 -0
  94. package/src/Popup.jsx +49 -0
  95. package/src/SplitColumnsContainer.jsx +63 -0
  96. package/src/SwitchDiv.jsx +30 -0
  97. package/src/TextButton.jsx +34 -0
  98. package/src/ToastNotifications.jsx +298 -0
  99. package/src/Tooltip.jsx +158 -0
  100. package/src/TransitionDiv.jsx +34 -0
  101. package/src/WarningDialog.jsx +70 -0
  102. package/src/alt/buttons/OnClickLoadingButton.jsx +16 -0
  103. package/src/alt/dialogs/DialogWithImageHeader.jsx +42 -0
  104. package/src/alt/dialogs/DialogWithSidebar.jsx +135 -0
  105. package/src/alt/dialogs/MobileBubbleDialog.jsx +56 -0
  106. package/src/containers/DialogContainer.jsx +423 -0
  107. package/src/containers/Reposition.jsx +24 -0
  108. package/src/containers/usePositioning.jsx +305 -0
  109. package/src/index.js +35 -0
  110. package/src/useDialogWithToggle.jsx +12 -0
  111. package/src/willUseIsSticky.jsx +113 -0
  112. package/.babelrc +0 -27
  113. package/lib/index.js +0 -5429
  114. package/rollup.config.js +0 -52
  115. package/yarn-error.log +0 -3724
@@ -0,0 +1,621 @@
1
+ import React, {useState, useMemo, useEffect, useRef, forwardRef} from 'react';
2
+ import ReactDOM from 'react-dom'
3
+ import styled, {css, isStyledComponent} from 'styled-components'
4
+
5
+ import {
6
+ Fill,
7
+ useEnsureRef,
8
+ useRefState,
9
+ useToggle,
10
+ willUseLayers,
11
+ willUseSharedState
12
+ } from '@startlibs/core';
13
+ import {darken, transparentize} from 'polished'
14
+ import {isMemo} from 'react-is'
15
+ import {
16
+ stopPropagation,
17
+ getColor,
18
+ ifProp,
19
+ callIfFunction,
20
+ customTheme,
21
+ oneWayAnimation,
22
+ animationTiming,
23
+ styledExtend,
24
+ media
25
+ } from '@startlibs/utils'
26
+ import mitt from 'mitt'
27
+
28
+ import {Button} from './Button'
29
+ import {useFlipEnter, useFlipLeave} from './Flip'
30
+ import {Icon} from './Icon'
31
+ import {Link} from './Link'
32
+ import {getDialogLayersSize} from './containers/DialogContainer'
33
+ import {usePositioning} from './containers/usePositioning'
34
+ import {createPressAndMoveEvent, isMobile} from '@startlibs/utils'
35
+
36
+
37
+ const fadeIn = oneWayAnimation('opacity: 0', 'opacity: 1')
38
+ const slideDown = oneWayAnimation('transform: translateY(-10px); pointer-events: none', 'transform: translateY(0); pointer-events: none')
39
+ const slideUpOnTop = oneWayAnimation('transform: translateY(10px); pointer-events: none', 'transform: translateY(0); pointer-events: none')
40
+ const slideRight = oneWayAnimation('transform: translateX(-10px)', 'transform: none;')
41
+ const slideLeftOnLeft = oneWayAnimation('transform: translateX(10px); pointer-events: none', 'transform: translateX(0px); pointer-events: none')
42
+
43
+ const popupAnimation = animationTiming('0.2s ease-out')
44
+
45
+ const areComponentsEqual = (a,b) => a === b
46
+
47
+ export const ListItem = styled.li`
48
+ position: relative;
49
+ font-size: 13px;
50
+ & > * {
51
+ padding: 11px 1.5rem 11px 3.5rem;
52
+ display: block;
53
+ }
54
+ ${props => props.hasSubmenu && `
55
+ & > * {
56
+ padding-right: 3rem;
57
+ }
58
+ `}
59
+ ${Icon}, .icon {
60
+ font-size: ${props => props.iconSize || 23}px;
61
+ position: absolute;
62
+ padding-left: 0.25rem;
63
+ left: 0;
64
+ top: 50%;
65
+ transform: translateY(-50%);
66
+ width: 3.5rem;
67
+ text-align: center;
68
+ }
69
+ ${media.mobile`
70
+ font-size: 15px;
71
+ & > * {
72
+ padding-top: 15px;
73
+ padding-bottom: 15px;
74
+ }
75
+ ${Icon}, .icon {
76
+ font-size: ${props => (props.iconSize || 23) + 2}px;
77
+ }
78
+ `}
79
+ & > a {
80
+ color: inherit;
81
+ font-weight: inherit;
82
+ text-decoration: inherit;
83
+ }
84
+ & > a, & > label {
85
+ cursor: pointer;
86
+ }
87
+ ${ifProp('hover', css`
88
+ & > a, & > label {
89
+ // background: ${props => lighten(0.53, getColor("main")(props))};
90
+ background: ${props => transparentize(0.88, getColor("main")(props))};
91
+ color: ${props => darken(0.03, getColor("main")(props))};
92
+ }`)}
93
+ ${ifProp('highlight', css`
94
+ color: ${getColor('main')};
95
+ `)}
96
+ input[type="radio"] {
97
+ display: none;
98
+ }
99
+ ${ifProp('noActions', `
100
+ max-width: 17rem;
101
+ color: rgba(0,0,0,0.4);
102
+ pointer-events: none;
103
+ text-align: center;
104
+ white-space: normal;
105
+ `)}
106
+ ${ifProp('withButton', css`
107
+ padding: 0.25rem 1rem;
108
+ ${Button} {
109
+ width: 100%;
110
+ color: white;
111
+ font-weight: 500;
112
+ padding: 10px 1.25rem;
113
+ ${media.mobile`
114
+ padding: 13px 1.25rem;
115
+ `}
116
+ }
117
+ `)}
118
+ ${ifProp('smallPaddingRight', css`
119
+ & > * {
120
+ padding-right: 1.5rem;
121
+ }
122
+ `)}
123
+ ${customTheme('ListItem')}
124
+ `
125
+
126
+ const ListItemDescText = styled.div`
127
+ font-size: 11px;
128
+ margin-top: 0.25rem;
129
+ opacity: 0.6;
130
+ ${media.mobile `
131
+ font-size: 12px;
132
+ `}
133
+ `
134
+ const ListItemHelpText = styled.span`
135
+ opacity: 0.5;
136
+ `
137
+
138
+ export const ListMenu = styled.ul`
139
+ padding: 0.5rem 0;
140
+ cursor: default;
141
+ overflowY: auto;
142
+ text-align: left;
143
+ color: inherit;
144
+ user-select: none;
145
+ `
146
+
147
+ export const ContextListMenu = styled(ListMenu)`
148
+ position: absolute;
149
+ z-index: 999;
150
+ background-color: white;
151
+ backdrop-filter: blur(20px);
152
+ background-color: rgb(255 255 255 / 80%);
153
+ box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 10px -2px rgba(0,0,0,0.15), 0 0px 2px 0px rgba(0,0,0,0.2);
154
+ border-radius: 12px;
155
+ margin-bottom: 0.5rem;
156
+ overflow: auto;
157
+ max-height: 100vh;
158
+ max-height: 100dvh;
159
+ max-width: 100vw;
160
+ max-width: 100dvw;
161
+ ${props => props.withoutIcons && `
162
+ ${ListItem} > * {
163
+ padding-left: 1.5rem;
164
+ }
165
+ `}
166
+ ${ifProp('sub', css`
167
+ ${props => props.fullHeight ? '' : (props.atTop ? 'margin-top: -0.5rem;' : 'margin-top: 0.5rem;')}
168
+
169
+ ${ifProp('atRight', `
170
+ margin-left: -4px;
171
+ `, `
172
+ margin-left: 4px;
173
+ `)}
174
+ `)}
175
+ ${customTheme('ContextMenu')}
176
+ `
177
+
178
+ export const StickyContainer = styled.div`
179
+ position: sticky;
180
+ background: white;
181
+ ${props => props.atBottom && `
182
+ bottom: -0.5rem;
183
+ padding-bottom: 0.5rem;
184
+ margin-bottom: -0.5rem;
185
+ `}
186
+ ${props => props.atTop && `
187
+ top: -0.5rem;
188
+ padding-top: 0.5rem;
189
+ margin-top: -0.5rem;
190
+ `}
191
+ `
192
+
193
+ export const ListSeparator = styled.li`
194
+ border-bottom: 1px solid rgba(0,0,0,0.1);
195
+ margin-top: 0.5rem;
196
+ margin-bottom: 0.5rem;
197
+ cursor: default;
198
+ height: 0;
199
+ ${ifProp('lighten', css`
200
+ border-color: rgba(255,255,255,0.15);
201
+ `)}
202
+ `
203
+
204
+ export const ListHeading = styled.li`
205
+ display: flex;
206
+ align-items: center;
207
+ padding: .5rem 1rem;
208
+ color: rgba(0,0,0,0.4);
209
+ font-size: 12px;
210
+ text-transform: uppercase;
211
+ :after {
212
+ content: '';
213
+ flex-grow: 1;
214
+ margin-left: 0.75rem;
215
+ height: 1px;
216
+ background: rgba(0,0,0,0.15);
217
+ }
218
+ `
219
+
220
+ const SubmenuArrowIcon = styledExtend(Icon)`
221
+ font-size: 13px;
222
+ position: absolute;
223
+ right: 0;
224
+ top: 50%;
225
+ transform: translateY(-50%);
226
+ width: 3rem;
227
+ text-align: center;
228
+ `
229
+
230
+
231
+ let layerCursor = 0
232
+
233
+ export class SimpleLi extends React.Component {
234
+
235
+ static isKeyboardNavigable = true
236
+ static defaultProps = {
237
+ emitter: mitt()
238
+ }
239
+
240
+ componentWillUnmount() {
241
+ this.props.emitter.off('confirm', this.confirm)
242
+ }
243
+
244
+ componentDidUpdate() {
245
+ if (this.props.hover) {
246
+ this.scrollToVisibility()
247
+ }
248
+ }
249
+
250
+ componentDidMount() {
251
+ this.props.emitter.on('confirm', this.confirm)
252
+ if (this.props.selected && this.props.setCursorOnHover) {
253
+ this.props.setCursorOnHover(this.props.index)
254
+ const el = ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.findDOMNode(this)
255
+ const list = el.parentNode
256
+ list.scrollTop = el.offsetTop + (el.offsetHeight / 2) - (list.offsetHeight / 2)
257
+ }
258
+ }
259
+
260
+ scrollToVisibility = () => {
261
+ const el = ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.findDOMNode(this)
262
+ const list = el.parentNode
263
+ if (el.offsetTop < list.scrollTop) {
264
+ list.scrollTop = el.offsetTop
265
+ } else if (el.offsetTop + el.offsetHeight > list.scrollTop + list.offsetHeight) {
266
+ list.scrollTop = el.offsetTop + el.offsetHeight - list.offsetHeight
267
+ }
268
+ }
269
+
270
+ confirm = () => {
271
+ if (this.props.hover) {
272
+ const node = ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.findDOMNode(this)
273
+ node.click()
274
+ }
275
+ }
276
+ onMouseEnter = () => this.props.setCursorOnHover && this.props.setCursorOnHover(this.props.index)
277
+ onMouseOut = () => this.props.setCursorOnHover && this.props.setCursorOnHover(-1)
278
+
279
+ touchEnd = (e) => {
280
+ if (this.props.children) {
281
+ e.preventDefault()
282
+ if (!this.props.hover)
283
+ this.onMouseEnter()
284
+ }
285
+ }
286
+
287
+ onClick = (e) => {
288
+ this.props.onClick && this.props.onClick()
289
+ }
290
+
291
+ render() {
292
+ const {label, layerIndex, href, linkTo, prefixWith, subMenu, icon, children, ...rest} = this.props
293
+ const Tag = (linkTo) ? Link : 'a'
294
+ return <ListItem
295
+ {...rest}
296
+ onClick={this.onClick}
297
+ onMouseLeave={this.onMouseOut}
298
+ onMouseEnter={this.onMouseEnter}
299
+ onTouchEnd={this.touchEnd}
300
+ >
301
+ <Tag href={href} to={linkTo}>
302
+ {prefixWith}
303
+ {icon && <Icon icon={icon}/>}
304
+ {label}
305
+ </Tag>
306
+ </ListItem>
307
+ }
308
+ }
309
+
310
+ export const Li = React.forwardRef(({emitter = mitt(), index, menuSlot, keepOpen, hover, layerIndex, hoverCursor, passTouchThrough, children, setCursorOnHover, onClick, prefixWith, linkTo, icon, label,descText,helpText, target, href, ...rest},ref) => {
311
+ const nodeRef = useEnsureRef(ref)
312
+ const tagRef = useRef()
313
+ const subMenu = useToggle()
314
+ const subPrevented = useRefState(false)
315
+
316
+ const onOpenSub = () => {
317
+ subPrevented.set(false)
318
+ if (hover && children) {
319
+ subMenu.open()
320
+ }
321
+ }
322
+
323
+ const onPreventSub = (subIndex) => {
324
+ if (index !== subIndex)
325
+ subMenu.close()
326
+ subPrevented.set(true)
327
+ }
328
+
329
+ const confirm = () => {
330
+ if (hover) {
331
+ nodeRef.current.click()
332
+ }
333
+ }
334
+
335
+ useEffect(() => {
336
+ if (hover) {
337
+ try {
338
+ const el = nodeRef.current
339
+ const parent = el.parentElement
340
+ const stickyContainer = parent.lastChild
341
+ const stickyContainerHeight =
342
+ (stickyContainer && stickyContainer.tagName === "DIV" && (stickyContainer.offsetTop - parent.scrollTop) < parent.offsetHeight)
343
+ ? stickyContainer.offsetHeight
344
+ : 0
345
+ if (el.offsetTop > parent.offsetHeight + parent.scrollTop - stickyContainerHeight - el.offsetHeight) {
346
+ parent.scroll(0, el.offsetTop - parent.offsetHeight + el.offsetHeight + stickyContainerHeight)
347
+ } else if (el.offsetTop < parent.scrollTop) {
348
+ parent.scroll(0, el.offsetTop)
349
+ }
350
+ } catch (e) {}
351
+ }
352
+ emitter.on('confirm', confirm)
353
+ emitter.on('preventSub', onPreventSub)
354
+ emitter.on('openSub', onOpenSub)
355
+ return () => {
356
+ emitter.off('confirm', confirm)
357
+ emitter.off('preventSub', onPreventSub)
358
+ emitter.off('openSub', onOpenSub)
359
+ }
360
+ }, [hover])
361
+
362
+ useEffect(() => () => subMenu.close(), [])
363
+
364
+ useEffect(() => {
365
+ if (children && !isMobile()) {
366
+ if (hover) {
367
+ if (!subMenu.isOpen || subMenu.closing || !subPrevented.get()) {
368
+ subMenu.open()
369
+ }
370
+ } else {
371
+ if (subMenu.isOpen) {
372
+ if (hoverCursor === -1) {
373
+ // Mouse saiu de todos os itens — provavelmente indo até o submenu (que
374
+ // abre ao lado, com um pequeno gap). Janela generosa; ao alcançá-lo, o
375
+ // onMouseEnter/onMouseMove do submenu chama subMenu.open e cancela.
376
+ subMenu.closeAfter(450)
377
+ } else {
378
+ // Mouse foi para outro item do menu — mantém os 200ms originais.
379
+ subMenu.closeAfter(200)
380
+ }
381
+ emitter.emit('preventSub', index)
382
+ }
383
+ }
384
+ }
385
+ }, [hoverCursor])
386
+
387
+ const onMouseMove = (e) => {
388
+ if (!hover) {
389
+ callIfFunction(setCursorOnHover, index)
390
+ }
391
+ e?.stopPropagation()
392
+ }
393
+
394
+ const onMouseOut = (e) => {
395
+ callIfFunction(setCursorOnHover, -1)
396
+ e?.stopPropagation()
397
+ }
398
+
399
+ const onClickHandler = (e) => {
400
+ if (children || keepOpen) {
401
+ e.stopPropagation()
402
+ }
403
+ if (children) {
404
+ if (hover) {
405
+ subMenu.toggle()
406
+ } else {
407
+ onMouseMove()
408
+ }
409
+ } else {
410
+ callIfFunction(onClick, e)
411
+ }
412
+ }
413
+
414
+ const onCloseSub = () => emitter.emit('openSub')
415
+
416
+ const addProps = (children) => React.Children.map(children, c => React.cloneElement(c, {
417
+ fixed: rest.fixedPosition,
418
+ closing: subMenu.closing,
419
+ onMouseMove: subMenu.open,
420
+ // onMouseEnter não borbulha, então não é barrado pelo stopPropagation dos itens
421
+ // do submenu (linha onMouseMove do Li). Garante que alcançar o submenu sempre
422
+ // cancele o fechamento agendado por closeAfter(200), mantendo-o aberto.
423
+ onMouseEnter: subMenu.open,
424
+ onClose: onCloseSub,
425
+ sub: true,
426
+ slot: c.props.slot || menuSlot
427
+ }))
428
+
429
+ const content = <>
430
+ {prefixWith}
431
+ {icon && <Icon icon={icon}/>}
432
+ {label} {helpText && <ListItemHelpText>({helpText})</ListItemHelpText>}
433
+ {descText && <ListItemDescText>{descText}</ListItemDescText>}
434
+ {children && <SubmenuArrowIcon icon="arrow-right"/>}
435
+ </>
436
+
437
+ const Tag = linkTo ? Link : 'a'
438
+ return <ListItem
439
+ ref={nodeRef}
440
+ {...rest}
441
+ hover={hover}
442
+ onClick={onClickHandler}
443
+ onMouseLeave={onMouseOut}
444
+ onMouseMove={onMouseMove}
445
+ hasSubmenu={children}
446
+ >
447
+ {
448
+ (rest.withButton || rest.noActions)
449
+ ? content
450
+ : <Tag href={href} target={target} to={linkTo} ref={tagRef} onMouseEnter={onMouseMove}>{content}</Tag>
451
+ }
452
+ {
453
+ subMenu.isOpen && addProps(children)
454
+ }
455
+ </ListItem>
456
+ })
457
+ Li.isKeyboardNavigable
458
+ Li.Simple = SimpleLi
459
+
460
+ const [useContextMenuLayers, contextMenuLayersSize] = willUseLayers()
461
+ export const contextMenuLayersQuantity = contextMenuLayersSize
462
+ const [useContextMenuSharedState] = willUseSharedState()
463
+
464
+ export const ContextMenuComponent = forwardRef((
465
+ {
466
+ children, sub, hoverIndex, autoHover, menuSlot, withoutIcons, onClose, onHover = () => 0, fill, onClick, ...props
467
+ },
468
+ unesuredRef
469
+ ) => {
470
+ const ref = useEnsureRef(unesuredRef)
471
+ const [hoverCursor, setHoverCursor] = useState(autoHover || sub ? 0 : -1)
472
+ const [layerCursor, setLayerCursor] = useContextMenuSharedState(0)
473
+
474
+ const [layerIndex, {getSize}] = useContextMenuLayers()
475
+
476
+ const itemsEmitter = useMemo(() => mitt(), [])
477
+ useEffect(() => {
478
+ window.addEventListener('keydown', keydown)
479
+ return () => {
480
+ callIfFunction(onClose)
481
+ window.removeEventListener('keydown', keydown)
482
+ }
483
+ }, [layerIndex, layerCursor, hoverCursor])
484
+
485
+ useEffect(() => {
486
+ if (hoverIndex !== undefined) {
487
+ setHoverCursor(Math.min(Math.max(hoverIndex, -1), getTotalKeyboardItems(children) + 1))
488
+ }
489
+ }, [hoverIndex !== hoverCursor])
490
+
491
+ const keydown = (e) => {
492
+ if (e.keyCode === 37 || e.keyCode === 39) {
493
+ if (!e.defaultPrevented) {
494
+ const layers = getSize()
495
+ const direction = e.keyCode - 38
496
+ setLayerCursor((layerCursor + direction + layers) % layers)
497
+ }
498
+ e.preventDefault()
499
+ }
500
+ if (layerIndex !== layerCursor) return
501
+ if (e.keyCode === 27 && ref.current) {
502
+ ref.current.click()
503
+ }
504
+ if (e.keyCode === 13 || e.keyCode === 32 && hoverCursor >= 0) {// enter
505
+ e.preventDefault()
506
+ itemsEmitter.emit('confirm')
507
+ }
508
+ if (e.keyCode === 38 || e.keyCode === 40) { // up down
509
+ e.preventDefault()
510
+ const direction = e.keyCode - 39
511
+ const max = getTotalKeyboardItems(children)
512
+ const newIndex = (hoverCursor + direction + max) % max
513
+ setHoverCursor(newIndex)
514
+ onHover(newIndex)
515
+ }
516
+ }
517
+
518
+ const getTotalKeyboardItems = (children) => {
519
+ let sum = 0
520
+ React.Children.forEach(children, child => {
521
+ if (child && !child.props.disabled && !child.props.readOnly) {
522
+ if ((!isStyledComponent(child.type) && child.type === Li) || child.type.isKeyboardNavigable) {
523
+ sum += 1
524
+ }
525
+ if (areComponentsEqual(child.type, StickyContainer) || child.props.nested) {
526
+ sum += getTotalKeyboardItems(child.props.children)
527
+ }
528
+ }
529
+ }
530
+ )
531
+ return sum
532
+ }
533
+
534
+ const setCursorOnHover = (index) => {
535
+ setLayerCursor(layerIndex)
536
+ setHoverCursor(index)
537
+ onHover(index)
538
+ }
539
+
540
+ const formatChildren = (children, indexRef = {current: 0}) => {
541
+ return React.Children.map(children, (child, j) =>
542
+ child && !child.props.disabled && !child.props.readOnly
543
+ ? (
544
+ child.props.nested
545
+ ? React.cloneElement(child, child.props, formatChildren(child.props.children, indexRef))
546
+ : ((!isStyledComponent(child.type) && !isMemo(child.type) && areComponentsEqual(child.type, Li)) || child.type.isKeyboardNavigable)
547
+ ? React.cloneElement(child, {
548
+ fixedPosition:props.fixed,
549
+ hover: hoverCursor === indexRef.current && layerIndex === layerCursor,
550
+ hoverCursor: hoverCursor,
551
+ index: indexRef.current++,
552
+ setCursorOnHover,
553
+ emitter: itemsEmitter,
554
+ layerIndex: layerIndex,
555
+ menuSlot
556
+ })
557
+ : (
558
+ areComponentsEqual(child.type, StickyContainer)
559
+ ? React.cloneElement(child, {
560
+ ...child.props,
561
+ children: formatChildren(child.props.children, indexRef)
562
+ })
563
+ : child
564
+ )
565
+ )
566
+ : child
567
+ )
568
+ }
569
+
570
+ const touchStart = createPressAndMoveEvent((e,prevPos) => {
571
+ // e.preventDefault()
572
+ e.stopPropagation()
573
+ e.target.dispatchEvent(new MouseEvent('mousemove', {bubbles: true}))
574
+ let passedResistance = false
575
+ return (e,nextPos) => {
576
+ const delta = prevPos.clientY - nextPos.clientY
577
+ if (Math.abs(delta) > 10) {
578
+ passedResistance = true
579
+ setCursorOnHover(-1)
580
+ }
581
+ }
582
+ })
583
+
584
+ return (
585
+ <ContextListMenu
586
+ data-drag={false}
587
+ ref={ref}
588
+ sub={sub}
589
+ onWheel={e => e.stopPropagation()}
590
+ onTouchStart={touchStart}
591
+ withoutIcons={withoutIcons}
592
+ onMouseDown={stopPropagation()}
593
+ {...props}
594
+ >
595
+ {formatChildren(children)}
596
+ </ContextListMenu>
597
+ )
598
+ })
599
+
600
+
601
+ export const ContextMenu = forwardRef(({slot = 'Popup', sub, withoutIcons, children, ...rest}, ref) => {
602
+ const contextMenuRef = useEnsureRef(ref)
603
+ const [positionInfo, placeholder, arrow] = usePositioning(contextMenuRef, rest)
604
+ const {atRight, atBottom} = {...positionInfo}
605
+ const slotName = useMemo(() => getDialogLayersSize() === 0 ? slot : (slot + '-' + (getDialogLayersSize() - 1)), [])
606
+
607
+ const enterAnimation = useMemo(() => [fadeIn, sub ? (atRight ? slideRight : slideLeftOnLeft) : (atBottom ? slideDown : slideUpOnTop)], [atRight, atBottom])
608
+ useFlipEnter(contextMenuRef, !!positionInfo && enterAnimation)
609
+ useFlipLeave(contextMenuRef, !!positionInfo && enterAnimation, sub && {events: {mouseover: rest.onMouseMove}})
610
+ return <>
611
+ {placeholder}
612
+ <Fill name={slotName}>
613
+ <ContextMenuComponent {...rest} menuSlot={slot} sub={sub} {...positionInfo} ref={contextMenuRef} withoutIcons={withoutIcons}>
614
+ {arrow}
615
+ {children}
616
+ </ContextMenuComponent>
617
+ </Fill>
618
+ </>
619
+ })
620
+
621
+ ContextMenu.ListItem = ListItem