@fibery/ui-kit 1.37.0 → 1.38.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 (139) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +8 -5
  3. package/src/actions-menu/actions-menu-item.tsx +18 -6
  4. package/src/actions-menu/actions-menu-props.tsx +4 -0
  5. package/src/actions-menu/actions-menu-sub-command-menu.tsx +7 -4
  6. package/src/actions-menu/actions-menu-sub-menu.tsx +9 -3
  7. package/src/actions-menu/actions-menu.tsx +24 -8
  8. package/src/actions-panel.tsx +12 -2
  9. package/src/axis-header.tsx +136 -76
  10. package/src/box.tsx +1 -1
  11. package/src/breadcrumb.tsx +78 -0
  12. package/src/button/actions-button-compact.tsx +4 -1
  13. package/src/button/base-button.tsx +5 -3
  14. package/src/button/button-group.tsx +1 -1
  15. package/src/button/button.tsx +31 -0
  16. package/src/button/icon-button.tsx +9 -0
  17. package/src/button/select-button.tsx +4 -0
  18. package/src/card-container.tsx +7 -4
  19. package/src/color-picker/swatch.tsx +1 -0
  20. package/src/command-menu/index.tsx +3 -3
  21. package/src/context-menu/index.tsx +10 -1
  22. package/src/date-picker/date-range-picker.tsx +6 -3
  23. package/src/date-picker/relative-date-picker.tsx +6 -2
  24. package/src/date-picker/single-date-picker.tsx +8 -5
  25. package/src/date-picker/styles.ts +32 -26
  26. package/src/date-picker/types.ts +3 -1
  27. package/src/day-select/day-select.tsx +21 -75
  28. package/src/day-select/iso-week-day-select.tsx +1 -1
  29. package/src/day-select/mount-day-select.tsx +2 -2
  30. package/src/day-select/week-day-select.tsx +1 -5
  31. package/src/design-system.ts +21 -12
  32. package/src/dropdown-menu/index.tsx +8 -1
  33. package/src/emoji-picker/app-icon-picker.tsx +6 -4
  34. package/src/emoji-picker/emoji-picker-preview-footer.tsx +1 -1
  35. package/src/emoji-picker/emoji-picker.tsx +3 -0
  36. package/src/emoji-picker/emoji.tsx +2 -1
  37. package/src/emoji-picker/icon-emoji-picker.tsx +10 -1
  38. package/src/emoji-picker/primitives/footer.tsx +0 -1
  39. package/src/emoji-picker/primitives/skin-tone.tsx +1 -0
  40. package/src/emoji-picker/use-responsive-emoji-size.ts +11 -0
  41. package/src/emoji-picker/utils/emoji-set.ts +2 -1
  42. package/src/empty-state/empty-state.tsx +6 -0
  43. package/src/field-icon.tsx +4 -1
  44. package/src/file-item.tsx +164 -47
  45. package/src/html-styles.ts +8 -0
  46. package/src/icons/ast/ActivityFilled.ts +8 -0
  47. package/src/icons/ast/AiAssistantFilled.ts +8 -0
  48. package/src/icons/ast/CollapseAllHorizontal.ts +8 -0
  49. package/src/icons/ast/DateRange.ts +1 -1
  50. package/src/icons/ast/EntityCreateFilled.ts +8 -0
  51. package/src/icons/ast/ExpandAllHorizontal.ts +8 -0
  52. package/src/icons/ast/GlobeUnpublish.ts +8 -0
  53. package/src/icons/ast/HomeFilled.ts +8 -0
  54. package/src/icons/ast/KeyboardHideDown.ts +8 -0
  55. package/src/icons/ast/LayoutMultiple.ts +8 -0
  56. package/src/icons/ast/RicheditorHistoryRedo.ts +1 -1
  57. package/src/icons/ast/RicheditorHistoryUndo.ts +1 -1
  58. package/src/icons/ast/SearchFilled.ts +8 -0
  59. package/src/icons/ast/SettingsFilled.ts +8 -0
  60. package/src/icons/ast/SlideMenuFilled.ts +8 -0
  61. package/src/icons/ast/TypeDate.ts +1 -1
  62. package/src/icons/ast/index.tsx +12 -0
  63. package/src/icons/react/ActivityFilled.tsx +13 -0
  64. package/src/icons/react/AiAssistantFilled.tsx +13 -0
  65. package/src/icons/react/CollapseAllHorizontal.tsx +13 -0
  66. package/src/icons/react/EntityCreateFilled.tsx +13 -0
  67. package/src/icons/react/ExpandAllHorizontal.tsx +13 -0
  68. package/src/icons/react/GlobeUnpublish.tsx +13 -0
  69. package/src/icons/react/HomeFilled.tsx +13 -0
  70. package/src/icons/react/KeyboardHideDown.tsx +13 -0
  71. package/src/icons/react/LayoutMultiple.tsx +13 -0
  72. package/src/icons/react/SearchFilled.tsx +13 -0
  73. package/src/icons/react/SettingsFilled.tsx +13 -0
  74. package/src/icons/react/SlideMenuFilled.tsx +13 -0
  75. package/src/icons/react/index.tsx +12 -0
  76. package/src/icons/svg/activity-filled.svg +4 -0
  77. package/src/icons/svg/ai-assistant-filled.svg +3 -0
  78. package/src/icons/svg/collapse-all-horizontal.svg +3 -0
  79. package/src/icons/svg/date-range.svg +3 -3
  80. package/src/icons/svg/entity-create-filled.svg +4 -0
  81. package/src/icons/svg/expand-all-horizontal.svg +3 -0
  82. package/src/icons/svg/globe-unpublish.svg +4 -0
  83. package/src/icons/svg/home-filled.svg +3 -0
  84. package/src/icons/svg/keyboard-hide-down.svg +1 -0
  85. package/src/icons/svg/layout-multiple.svg +3 -0
  86. package/src/icons/svg/richeditor/history/redo.svg +3 -2
  87. package/src/icons/svg/richeditor/history/undo.svg +3 -2
  88. package/src/icons/svg/search-filled.svg +3 -0
  89. package/src/icons/svg/settings-filled.svg +3 -0
  90. package/src/icons/svg/slide-menu-filled.svg +1 -0
  91. package/src/icons/svg/type/date.svg +3 -2
  92. package/src/images-gallery/images-gallery.tsx +254 -0
  93. package/src/images-gallery/zoom.tsx +112 -0
  94. package/src/is-in-popup.ts +1 -1
  95. package/src/layout-styles.ts +21 -1
  96. package/src/lists/actions-menu-row-surface.tsx +3 -1
  97. package/src/lists/list-row-surface.tsx +7 -1
  98. package/src/mobile-keyboard-aware-popup.tsx +87 -0
  99. package/src/mobile-styles.ts +8 -0
  100. package/src/modal-menu/contexts/modal-menu-context.tsx +22 -0
  101. package/src/modal-menu/contexts/modal-submenu-context.tsx +16 -0
  102. package/src/modal-menu/index.tsx +14 -0
  103. package/src/modal-menu/modal-menu-content.tsx +68 -0
  104. package/src/modal-menu/modal-menu-group.tsx +10 -0
  105. package/src/modal-menu/modal-menu-item.tsx +37 -0
  106. package/src/modal-menu/modal-menu-label.tsx +24 -0
  107. package/src/modal-menu/modal-menu-root.tsx +57 -0
  108. package/src/modal-menu/modal-menu-separator.tsx +14 -0
  109. package/src/modal-menu/modal-menu-trigger.tsx +37 -0
  110. package/src/modal-menu/modal-submenu-content.tsx +49 -0
  111. package/src/modal-menu/modal-submenu-trigger.tsx +34 -0
  112. package/src/modal-menu/modal-submenu.tsx +17 -0
  113. package/src/online-users.tsx +3 -3
  114. package/src/palette.ts +1 -1
  115. package/src/popover/index.tsx +687 -0
  116. package/src/popover/mobile-popup-context.tsx +29 -0
  117. package/src/{popup → popover}/modifiers.tsx +0 -7
  118. package/src/{popup → popover}/popup-modifiers-context.ts +3 -3
  119. package/src/reactions/reaction-button.tsx +30 -45
  120. package/src/reactions/reaction-picker.tsx +39 -45
  121. package/src/select/index.tsx +8 -4
  122. package/src/select/select-in-popover.tsx +21 -33
  123. package/src/tab-nav/tab-nav.tsx +35 -9
  124. package/src/toast/primitives.tsx +8 -1
  125. package/src/toggle-button/round-toggle-button.tsx +23 -0
  126. package/src/toggle-button/toggle-button-group.tsx +34 -0
  127. package/src/toggle-button/toggle-button.tsx +164 -0
  128. package/src/toggle.tsx +3 -3
  129. package/src/tooltip.tsx +2 -2
  130. package/src/unit/styles.ts +2 -2
  131. package/src/use-is-phone.tsx +28 -3
  132. package/src/use-is-support-hover.ts +1 -1
  133. package/src/use-on-screen-keyboard-data.tsx +66 -0
  134. package/src/workflow-progress-icon.tsx +16 -22
  135. package/src/popup/index.tsx +0 -383
  136. /package/src/{popup → popover}/popup-stack-context.tsx +0 -0
  137. /package/src/{popup → popover}/reference.ts +0 -0
  138. /package/src/{popup → popover}/styles.ts +0 -0
  139. /package/src/{popup → popover}/use-click-outside.ts +0 -0
@@ -0,0 +1,687 @@
1
+ /* eslint-disable max-lines */
2
+ import {stopPropagation} from "@fibery/react/src/stop-propagation";
3
+ import {useControllableState} from "@fibery/react/src/use-controllable-state";
4
+ import {usePointerDownOutside} from "@fibery/react/src/use-pointer-down-outside";
5
+ import {css} from "@linaria/core";
6
+ import {createPopper, Modifier, Placement, State} from "@popperjs/core";
7
+ import {Options as FlipOptions} from "@popperjs/core/lib/modifiers/flip";
8
+ import {Options as PreventOverflowOptions} from "@popperjs/core/lib/modifiers/preventOverflow";
9
+ import cx from "classnames";
10
+ import _ from "lodash";
11
+ import {
12
+ cloneElement,
13
+ CSSProperties,
14
+ forwardRef,
15
+ isValidElement,
16
+ MouseEvent,
17
+ ReactNode,
18
+ useEffect,
19
+ useId,
20
+ useImperativeHandle,
21
+ useMemo,
22
+ } from "react";
23
+ import {
24
+ safeAreaInsetBottomVar,
25
+ safeAreaInsetLeftVar,
26
+ safeAreaInsetRightVar,
27
+ safeAreaInsetTopVar,
28
+ } from "../mobile-styles";
29
+ import {createPortal} from "react-dom";
30
+ import {usePopper} from "react-popper";
31
+ import {createInlineTheme, space, textStyles, themeVars, transition} from "../design-system";
32
+ import {IconButton} from "../button/icon-button";
33
+ import CloseIcon from "../icons/react/Close";
34
+ import ArrowLeftIcon from "../icons/react/ArrowLeft";
35
+ import {useTheme} from "../theme-provider";
36
+ import {$TSFixMe} from "../tsfixme";
37
+ import {useIsPhone} from "../use-is-phone";
38
+ import {usePopupModifiers} from "./popup-modifiers-context";
39
+ import {getMaxSize, observeWidth} from "./modifiers";
40
+ import {isElementReference, Reference} from "./reference";
41
+ import {basicPopupClassName, basicPopupContainerClassName, hidePopupWhenTriggerIsHiddenClassName} from "./styles";
42
+ import {MobilePopupProvider, useMobilePopup} from "./mobile-popup-context";
43
+ import {usePopupStackContext} from "./popup-stack-context";
44
+ import {useComposedRefs} from "@fibery/react/src/use-composed-refs";
45
+ import {createContext} from "@fibery/react/src/create-context";
46
+ import {Wrap} from "@fibery/react/src/wrap";
47
+
48
+ const Backdrop = ({className}: {className?: string}) => {
49
+ return (
50
+ <div
51
+ id={"popup-backdrop"}
52
+ data-testid={"popup-backdrop"}
53
+ className={cx(
54
+ className,
55
+ css`
56
+ position: absolute;
57
+ inset: 0;
58
+ z-index: 1000;
59
+ `
60
+ )}
61
+ />
62
+ );
63
+ };
64
+
65
+ const mobileAnimationClassName = css`
66
+ transition: transform ${transition}, opacity ${transition};
67
+ transform: translateY(0);
68
+ opacity: 1;
69
+
70
+ @media (prefers-reduced-motion: no-preference) {
71
+ @starting-style {
72
+ transform: translateY(20%);
73
+ opacity: 0;
74
+ }
75
+ }
76
+ `;
77
+
78
+ const popupContentClassName = css`
79
+ display: grid;
80
+ background: ${themeVars.actionMenuBg};
81
+ grid-template-rows: 60px 1fr;
82
+ `;
83
+
84
+ export function MobilePopup({
85
+ content,
86
+ mobilePopupStyle,
87
+ mobilePopupContentClassName,
88
+ title,
89
+ onClose,
90
+ height,
91
+ onNavigateBack,
92
+ }: {
93
+ content: ReactNode;
94
+ mobilePopupStyle?: CSSProperties;
95
+ mobilePopupContentClassName?: string;
96
+ title: string;
97
+ height?: string;
98
+ onClose?: () => void;
99
+ onNavigateBack?: () => void;
100
+ }) {
101
+ const theme = useTheme();
102
+
103
+ const {title: contextTitle, headerSlot} = useMobilePopup();
104
+
105
+ const headerContent = headerSlot ?? (
106
+ <>
107
+ {onNavigateBack && (
108
+ <IconButton onClick={onNavigateBack} size="xLarge">
109
+ <ArrowLeftIcon />
110
+ </IconButton>
111
+ )}
112
+ <div
113
+ className={css`
114
+ ${textStyles.heading3}
115
+ flex: 1;
116
+ min-width: 0;
117
+ overflow: hidden;
118
+ text-overflow: ellipsis;
119
+ white-space: nowrap;
120
+ `}
121
+ >
122
+ {contextTitle ?? title}
123
+ </div>
124
+ <IconButton onClick={onClose} size="xLarge">
125
+ <CloseIcon />
126
+ </IconButton>
127
+ </>
128
+ );
129
+
130
+ const popupContent = (
131
+ <>
132
+ <div
133
+ className={cx(
134
+ css`
135
+ display: flex;
136
+ align-items: center;
137
+ gap: ${space.s8}px;
138
+ padding: ${space.s8}px ${space.s8}px ${space.s8}px ${space.s20}px;
139
+ min-width: 0;
140
+ `,
141
+ onNavigateBack &&
142
+ css`
143
+ padding-left: ${space.s4}px;
144
+ `
145
+ )}
146
+ >
147
+ {headerContent}
148
+ </div>
149
+ <div
150
+ className={cx(
151
+ css`
152
+ border-top: ${themeVars.separators.opacity5};
153
+ min-width: 0;
154
+ `,
155
+ mobilePopupContentClassName
156
+ )}
157
+ >
158
+ {content}
159
+ </div>
160
+ </>
161
+ );
162
+
163
+ return (
164
+ <div
165
+ className={cx(
166
+ css`
167
+ position: absolute;
168
+ left: var(${safeAreaInsetLeftVar});
169
+ right: var(${safeAreaInsetRightVar});
170
+ top: var(${safeAreaInsetTopVar});
171
+ bottom: var(${safeAreaInsetBottomVar});
172
+ z-index: 1002;
173
+ `,
174
+ !height && mobileAnimationClassName,
175
+ !height && popupContentClassName,
176
+ height &&
177
+ css`
178
+ display: grid;
179
+ `
180
+ )}
181
+ style={{
182
+ ...createInlineTheme(theme),
183
+ ...mobilePopupStyle,
184
+ ...(height ? {gridTemplateRows: `1fr ${height}`} : {}),
185
+ }}
186
+ onClick={stopPropagation}
187
+ >
188
+ {height ? (
189
+ <>
190
+ <div
191
+ onClick={onClose}
192
+ className={css`
193
+ background-color: ${themeVars.modalBg};
194
+ `}
195
+ />
196
+ <div
197
+ className={cx(
198
+ css`
199
+ display: grid;
200
+ grid-template-rows: 60px 1fr;
201
+ `,
202
+ mobileAnimationClassName,
203
+ popupContentClassName
204
+ )}
205
+ >
206
+ {popupContent}
207
+ </div>
208
+ </>
209
+ ) : (
210
+ popupContent
211
+ )}
212
+ </div>
213
+ );
214
+ }
215
+
216
+ type PortalProps = {
217
+ getPopupContainer?: () => HTMLElement;
218
+ children: ReactNode;
219
+ };
220
+
221
+ const Portal = ({getPopupContainer: popupContainerOverride, children}: PortalProps) => {
222
+ const popupModifiers = usePopupModifiers();
223
+ const getPopupContainer = popupContainerOverride ?? popupModifiers.getPopupContainer;
224
+ return createPortal(children, getPopupContainer());
225
+ };
226
+
227
+ type TriggerProps = {
228
+ asChild?: boolean;
229
+ // Support case when popup trigger is virtual reference
230
+ reference?: Reference;
231
+ children?: ReactNode;
232
+ };
233
+
234
+ const Trigger = ({asChild = false, reference, children}: TriggerProps) => {
235
+ const {setTriggerElement, disabled, open, onOpen, onClose} = useContext();
236
+
237
+ const onMouseDown: React.MouseEventHandler<HTMLElement> = (e) => {
238
+ if (disabled) {
239
+ return;
240
+ }
241
+ if (e.button === 0) {
242
+ open ? onClose?.(e as unknown as Event) : onOpen?.();
243
+ }
244
+ };
245
+
246
+ useEffect(() => {
247
+ if (reference) {
248
+ setTriggerElement(reference);
249
+ }
250
+ }, [reference, setTriggerElement]);
251
+
252
+ if (reference) {
253
+ return null;
254
+ }
255
+
256
+ if (asChild && isValidElement(children)) {
257
+ return cloneElement(children, {
258
+ // @ts-expect-error: React.cloneElement typing does not include `ref`, but it's valid at runtime
259
+ ref: setTriggerElement,
260
+ "aria-expanded": open,
261
+ "aria-haspopup": true,
262
+ onMouseDown,
263
+ });
264
+ }
265
+
266
+ return (
267
+ <div ref={setTriggerElement} onMouseDown={onMouseDown}>
268
+ {children}
269
+ </div>
270
+ );
271
+ };
272
+
273
+ type ContentProps = {
274
+ children: ReactNode;
275
+ placement?: Placement;
276
+ popupStyle?: $TSFixMe;
277
+ popupClassName?: string;
278
+ popupContainerClassName?: string;
279
+ offset?: [number, number];
280
+ onMouseLeave?: (e: MouseEvent<HTMLDivElement>) => void;
281
+ renderBackdrop?: boolean;
282
+ backdropClassName?: string;
283
+ preventOverflowOptions?: Partial<PreventOverflowOptions>;
284
+ flipOptions?: Partial<FlipOptions>;
285
+ additionalModifiers?: Array<Partial<Modifier<$TSFixMe, $TSFixMe>>>;
286
+ hidePopupWhenTriggerIsHidden?: boolean;
287
+ shrinkable?: boolean;
288
+ supportMobile?: boolean;
289
+ title?: string;
290
+ mobilePopupStyle?: CSSProperties;
291
+ mobilePopupContentClassName?: string;
292
+ transform?: boolean;
293
+ /**
294
+ * @deprecated
295
+ * Because of openning animation popup creates new stacking context https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context
296
+ * this might break any components that use position: fixed inside the popup (i.e. formula editor -> monaco editor)
297
+ * Pass true to restore stacking context after animation is finished.
298
+ *
299
+ * Marked as deprecated because it's better to avoid this prop
300
+ */
301
+ fixStackingContextAfterOpen?: boolean;
302
+ /**
303
+ * ran after Popup positioned the element the first time
304
+ */
305
+ onFirstUpdate?: () => void;
306
+ };
307
+
308
+ export type ContentRef = {update: (() => Promise<unknown>) | null};
309
+
310
+ const Content = forwardRef<ContentRef, ContentProps>(function PopoverContentInner(
311
+ {
312
+ children,
313
+ placement,
314
+ popupStyle,
315
+ popupContainerClassName,
316
+ popupClassName,
317
+ offset = [0, 0],
318
+ onMouseLeave,
319
+ renderBackdrop,
320
+ backdropClassName,
321
+ preventOverflowOptions = {},
322
+ flipOptions = {},
323
+ additionalModifiers = [],
324
+ hidePopupWhenTriggerIsHidden = true,
325
+ shrinkable,
326
+ supportMobile = false,
327
+ title = "",
328
+ mobilePopupStyle,
329
+ mobilePopupContentClassName,
330
+ fixStackingContextAfterOpen = false,
331
+ onFirstUpdate,
332
+ transform,
333
+ },
334
+ ref
335
+ ) {
336
+ const {open, onClose, triggerElement, popupElement, setPopupElement} = useContext();
337
+
338
+ const popupModifiers = usePopupModifiers();
339
+ const theme = useTheme();
340
+ const isPhone = useIsPhone();
341
+
342
+ // We get infinite popup renders if this is not in a separate memo
343
+ const shrinkableModifiers = useMemo(() => {
344
+ return shrinkable ? getMaxSize(preventOverflowOptions) : [];
345
+ }, [preventOverflowOptions, shrinkable]);
346
+
347
+ const modifiers = useMemo(() => {
348
+ return _.compact([
349
+ {name: "flip", options: {...popupModifiers.flipOptions, ...flipOptions}},
350
+ {
351
+ name: "preventOverflow",
352
+ options: {
353
+ tether: false,
354
+ padding: space.s6,
355
+ ...popupModifiers.preventOverflowOptions,
356
+ ...preventOverflowOptions,
357
+ },
358
+ },
359
+ {name: "offset", options: {offset: offset}},
360
+ !transform && {name: "computeStyles", options: {gpuAcceleration: false}},
361
+ observeWidth,
362
+ ...shrinkableModifiers,
363
+ ...additionalModifiers,
364
+ ...(popupModifiers.additionalModifiers ?? []),
365
+ ]);
366
+ }, [
367
+ additionalModifiers,
368
+ flipOptions,
369
+ offset,
370
+ popupModifiers,
371
+ preventOverflowOptions,
372
+ shrinkableModifiers,
373
+ transform,
374
+ ]);
375
+
376
+ // migrate to floating-ui https://floating-ui.com/docs/migration
377
+ const {styles, attributes, update} = usePopper(triggerElement, popupElement, {placement, onFirstUpdate, modifiers});
378
+
379
+ useImperativeHandle(ref, () => ({update}), [update]);
380
+
381
+ const {onPointerDownCapture} = usePointerDownOutside((event) => {
382
+ if (!open || (isPhone && supportMobile)) {
383
+ return;
384
+ }
385
+ const target = event.target;
386
+ if (target instanceof Node) {
387
+ const ignoreElements = _.compact([isElementReference(triggerElement) && triggerElement]);
388
+ if (ignoreElements.some((element) => element.contains(target))) {
389
+ return;
390
+ }
391
+ }
392
+ onClose?.(event);
393
+ });
394
+
395
+ const onAnimationEnd: React.AnimationEventHandler = (e) => {
396
+ if (fixStackingContextAfterOpen) {
397
+ (e.target as HTMLElement).setAttribute("data-fix-stacking-context", "");
398
+ }
399
+ };
400
+
401
+ const contentStyles = useMemo(() => {
402
+ return {...styles.popper, ...createInlineTheme(theme), ...popupStyle};
403
+ }, [popupStyle, styles.popper, theme]);
404
+
405
+ if (!open) {
406
+ return null;
407
+ }
408
+
409
+ // When transform is disabled there is a noticeable blink on popup open (it's not positioned correctly)
410
+ if (!transform && !triggerElement) {
411
+ return null;
412
+ }
413
+
414
+ if (isPhone && supportMobile) {
415
+ return (
416
+ <MobilePopupProvider>
417
+ <MobilePopup
418
+ onClose={onClose as $TSFixMe}
419
+ title={title}
420
+ mobilePopupContentClassName={mobilePopupContentClassName}
421
+ mobilePopupStyle={mobilePopupStyle}
422
+ content={children}
423
+ />
424
+ </MobilePopupProvider>
425
+ );
426
+ }
427
+ return (
428
+ <>
429
+ {renderBackdrop && <Backdrop className={backdropClassName} />}
430
+ <div
431
+ ref={setPopupElement}
432
+ className={cx(
433
+ basicPopupContainerClassName,
434
+ hidePopupWhenTriggerIsHidden && hidePopupWhenTriggerIsHiddenClassName,
435
+ popupContainerClassName
436
+ )}
437
+ onMouseLeave={onMouseLeave}
438
+ onClick={stopPropagation}
439
+ {...attributes.popper}
440
+ onPointerDownCapture={onPointerDownCapture}
441
+ style={contentStyles}
442
+ >
443
+ <div className={cx(basicPopupClassName, popupClassName)} style={popupStyle} onAnimationEnd={onAnimationEnd}>
444
+ {children}
445
+ </div>
446
+ </div>
447
+ </>
448
+ );
449
+ });
450
+
451
+ type ContextValue = {
452
+ open: boolean;
453
+ disabled?: boolean;
454
+ onOpen?: () => unknown;
455
+ onClose?: (event: Event) => unknown;
456
+ triggerElement: Reference;
457
+ setTriggerElement: (value: Reference) => void;
458
+ popupElement: HTMLDivElement | null;
459
+ setPopupElement: (value: HTMLDivElement) => void;
460
+ };
461
+
462
+ const [Provider, useContext] = createContext<ContextValue>("PopoverContext");
463
+
464
+ type RootProps = {
465
+ children: ReactNode;
466
+ open: boolean;
467
+ disabled?: boolean;
468
+ closeOnEscape?: boolean;
469
+ onOpen?: () => unknown;
470
+ onClose?: (event: Event) => unknown;
471
+ // For legacy popup component support
472
+ triggerElement?: Reference;
473
+ setTriggerElement?: (value: Reference) => void;
474
+ popupElement?: HTMLDivElement | null;
475
+ setPopupElement?: (value: HTMLDivElement | null) => void;
476
+ };
477
+
478
+ const Root = ({children, open, disabled = false, closeOnEscape = true, onOpen, onClose, ...rest}: RootProps) => {
479
+ const id = useId();
480
+ const {registerEscapeCallback, registerPopup} = usePopupStackContext();
481
+
482
+ const [triggerElement, setTriggerElement] = useControllableState<Reference>({
483
+ value: rest.triggerElement,
484
+ onChange: rest.setTriggerElement,
485
+ });
486
+ const [popupElement, setPopupElement] = useControllableState<HTMLDivElement | null>({
487
+ value: rest.popupElement,
488
+ onChange: rest.setPopupElement,
489
+ });
490
+
491
+ const composedSetTriggerElement = useComposedRefs(setTriggerElement, rest.setTriggerElement);
492
+ const composedSetPopupElement = useComposedRefs(setPopupElement, rest.setPopupElement);
493
+
494
+ useEffect(() => {
495
+ let unregister: () => void;
496
+ if (registerPopup && open) {
497
+ unregister = registerPopup(id);
498
+ }
499
+ return () => {
500
+ unregister?.();
501
+ };
502
+ }, [registerPopup, id, open]);
503
+
504
+ useEffect(() => {
505
+ if (triggerElement && closeOnEscape && open) {
506
+ const onKeyDown = (evt: KeyboardEvent) => {
507
+ if (evt.defaultPrevented) {
508
+ return;
509
+ }
510
+ if (evt.key === "Escape") {
511
+ onClose?.(evt);
512
+ evt.preventDefault();
513
+ evt.stopPropagation();
514
+ }
515
+ };
516
+
517
+ if (registerEscapeCallback) {
518
+ registerEscapeCallback(id, onKeyDown);
519
+ } else {
520
+ document.addEventListener("keydown", onKeyDown);
521
+ }
522
+
523
+ // if popup was opened but nothing was focused inside it we should catch 'Escape' key from trigger as well
524
+ // otherwise it can be caught by navigation panel
525
+ "addEventListener" in triggerElement && triggerElement.addEventListener("keydown", onKeyDown);
526
+
527
+ return () => {
528
+ if (!registerEscapeCallback) {
529
+ document.removeEventListener("keydown", onKeyDown);
530
+ }
531
+
532
+ "removeEventListener" in triggerElement && triggerElement.removeEventListener("keydown", onKeyDown);
533
+ };
534
+ }
535
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
536
+ return () => {};
537
+ }, [closeOnEscape, onClose, open, triggerElement, registerEscapeCallback, id]);
538
+
539
+ const contextValue = useMemo(() => {
540
+ return {
541
+ disabled,
542
+ triggerElement,
543
+ setTriggerElement: composedSetTriggerElement,
544
+ popupElement,
545
+ setPopupElement: composedSetPopupElement,
546
+ open,
547
+ onClose,
548
+ onOpen,
549
+ };
550
+ }, [
551
+ disabled,
552
+ triggerElement,
553
+ composedSetTriggerElement,
554
+ popupElement,
555
+ composedSetPopupElement,
556
+ open,
557
+ onClose,
558
+ onOpen,
559
+ ]);
560
+
561
+ return <Provider value={contextValue}>{children}</Provider>;
562
+ };
563
+
564
+ export const Popover = {
565
+ Root,
566
+ Trigger,
567
+ Portal,
568
+ Content,
569
+ };
570
+
571
+ export {createPopper};
572
+
573
+ export type {Placement, State, Reference, RootProps, TriggerProps, ContentProps};
574
+
575
+ export type PopoverOldProps = Pick<
576
+ RootProps,
577
+ "children" | "open" | "onOpen" | "onClose" | "closeOnEscape" | "disabled"
578
+ > &
579
+ ContentProps &
580
+ Pick<PortalProps, "getPopupContainer"> & {
581
+ trigger?: ReactNode;
582
+ triggerDomElement?: Reference;
583
+ renderInPortal?: boolean;
584
+ };
585
+
586
+ /**
587
+ * @deprecated Use the new `Popover` compound component instead.
588
+ *
589
+ * The `PopoverOld` component is deprecated and will be removed in a future version.
590
+ * Please migrate to the new `Popover` API which provides better composition and flexibility:
591
+ *
592
+ * @example
593
+ * ```tsx
594
+ * // Instead of PopoverOld:
595
+ * <PopoverOld
596
+ * trigger={<button>Click me</button>}
597
+ * open={isOpen}
598
+ * onOpen={() => setIsOpen(true)}
599
+ * onClose={() => setIsOpen(false)}
600
+ * >
601
+ * <div>Popover content</div>
602
+ * </PopoverOld>
603
+ *
604
+ * // Use Popover compound components:
605
+ * <Popover.Root
606
+ * open={isOpen}
607
+ * onOpen={() => setIsOpen(true)}
608
+ * onClose={() => setIsOpen(false)}
609
+ * >
610
+ * <Popover.Trigger>
611
+ * <button>Click me</button>
612
+ * </Popover.Trigger>
613
+ * <Popover.Portal>
614
+ * <Popover.Content>
615
+ * <div>Popover content</div>
616
+ * </Popover.Content>
617
+ * </Popover.Portal>
618
+ * </Popover.Root>
619
+ * ```
620
+ */
621
+ export const PopoverOld = ({
622
+ trigger,
623
+ triggerDomElement,
624
+ children,
625
+ placement,
626
+ open,
627
+ popupStyle,
628
+ popupContainerClassName,
629
+ popupClassName,
630
+ renderInPortal,
631
+ offset = [0, 0],
632
+ disabled = false,
633
+ onMouseLeave,
634
+ renderBackdrop,
635
+ backdropClassName,
636
+ closeOnEscape = true,
637
+ preventOverflowOptions = {},
638
+ flipOptions = {},
639
+ additionalModifiers = [],
640
+ hidePopupWhenTriggerIsHidden = true,
641
+ shrinkable,
642
+ onOpen = _.noop,
643
+ onClose = _.noop,
644
+ supportMobile = false,
645
+ title = "",
646
+ mobilePopupStyle,
647
+ mobilePopupContentClassName,
648
+ getPopupContainer,
649
+ fixStackingContextAfterOpen = false,
650
+ onFirstUpdate,
651
+ transform,
652
+ }: PopoverOldProps) => {
653
+ return (
654
+ <Popover.Root open={open} disabled={disabled} closeOnEscape={closeOnEscape} onOpen={onOpen} onClose={onClose}>
655
+ {triggerDomElement ? (
656
+ <Popover.Trigger reference={triggerDomElement}></Popover.Trigger>
657
+ ) : (
658
+ <Popover.Trigger>{trigger}</Popover.Trigger>
659
+ )}
660
+ <Wrap with={Popover.Portal} if={renderInPortal} getPopupContainer={getPopupContainer}>
661
+ <Popover.Content
662
+ children={children}
663
+ placement={placement}
664
+ popupStyle={popupStyle}
665
+ popupContainerClassName={popupContainerClassName}
666
+ popupClassName={popupClassName}
667
+ offset={offset}
668
+ onMouseLeave={onMouseLeave}
669
+ renderBackdrop={renderBackdrop}
670
+ backdropClassName={backdropClassName}
671
+ preventOverflowOptions={preventOverflowOptions}
672
+ flipOptions={flipOptions}
673
+ additionalModifiers={additionalModifiers}
674
+ hidePopupWhenTriggerIsHidden={hidePopupWhenTriggerIsHidden}
675
+ shrinkable={shrinkable}
676
+ supportMobile={supportMobile}
677
+ title={title}
678
+ mobilePopupStyle={mobilePopupStyle}
679
+ mobilePopupContentClassName={mobilePopupContentClassName}
680
+ fixStackingContextAfterOpen={fixStackingContextAfterOpen}
681
+ onFirstUpdate={onFirstUpdate}
682
+ transform={transform}
683
+ />
684
+ </Wrap>
685
+ </Popover.Root>
686
+ );
687
+ };
@@ -0,0 +1,29 @@
1
+ import _ from "lodash";
2
+ import {FC, ReactNode, useState, useMemo} from "react";
3
+ import {createContext} from "@fibery/react/src/create-context";
4
+
5
+ type MobilePopupContext = {
6
+ title?: string;
7
+ headerSlot?: ReactNode;
8
+ setTitle: (title: string) => void;
9
+ setHeader: (header?: ReactNode) => void;
10
+ };
11
+
12
+ const [Provider, useMobilePopup] = createContext<MobilePopupContext>("MobilePopupContext", {
13
+ setTitle: _.noop,
14
+ setHeader: _.noop,
15
+ });
16
+
17
+ export const MobilePopupProvider: FC<{children?: ReactNode}> = ({children}) => {
18
+ const [title, setTitle] = useState<string | undefined>();
19
+ const [headerSlot, setHeader] = useState<ReactNode | undefined>();
20
+
21
+ const contextValue = useMemo(
22
+ () => ({title, headerSlot, setTitle, setHeader}),
23
+ [title, headerSlot, setTitle, setHeader]
24
+ );
25
+
26
+ return <Provider value={contextValue}>{children}</Provider>;
27
+ };
28
+
29
+ export {useMobilePopup};