@fibery/ui-kit 1.40.3 → 1.41.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 (174) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/eslint.config.mjs +16 -0
  3. package/package.json +11 -13
  4. package/src/a11y-color.test.ts +162 -0
  5. package/src/a11y-color.ts +13 -20
  6. package/src/actions-menu/actions-menu-item.tsx +9 -12
  7. package/src/actions-menu/context-actions-menu.tsx +8 -3
  8. package/src/ai/model.tsx +0 -1
  9. package/src/ai/temperature.tsx +0 -1
  10. package/src/animated-height-container.tsx +3 -3
  11. package/src/antd/ant-modal.tsx +10 -5
  12. package/src/antd/index.tsx +3 -0
  13. package/src/antd/input.tsx +0 -1
  14. package/src/antd/styles.ts +15 -15
  15. package/src/app-icon-with-fallback.tsx +6 -6
  16. package/src/app-icon-wrapper.tsx +2 -3
  17. package/src/app-icon.tsx +4 -4
  18. package/src/avatar.tsx +3 -3
  19. package/src/button/base-button.tsx +0 -1
  20. package/src/button/button.tsx +1 -0
  21. package/src/button/make-button-colors.ts +5 -13
  22. package/src/checkbox.tsx +2 -2
  23. package/src/collapsible-section.tsx +11 -1
  24. package/src/color-picker/ColorPickerOrLoader.js +2 -2
  25. package/src/color-utils.test.ts +307 -0
  26. package/src/color-utils.ts +215 -0
  27. package/src/command-menu/index.tsx +1 -0
  28. package/src/comment.tsx +3 -2
  29. package/src/context-menu/index.tsx +12 -7
  30. package/src/date-picker/contexts.ts +2 -2
  31. package/src/date-picker/date-range-picker.tsx +3 -4
  32. package/src/date-picker/single-date-picker.tsx +147 -119
  33. package/src/date-picker/types.ts +4 -3
  34. package/src/day-select/iso-week-day-select.tsx +2 -2
  35. package/src/day-select/week-day-select.tsx +2 -2
  36. package/src/delayed.tsx +0 -1
  37. package/src/design-system/alpha.test.ts +59 -0
  38. package/src/design-system/alpha.ts +41 -0
  39. package/src/design-system/animation.ts +10 -0
  40. package/src/design-system/colors-css.test.ts +39 -0
  41. package/src/design-system/colors-css.ts +25 -0
  42. package/src/design-system/colors-js.test.ts +232 -0
  43. package/src/design-system/colors-js.ts +107 -0
  44. package/src/design-system/colors.test.ts +74 -0
  45. package/src/design-system/colors.ts +759 -0
  46. package/src/design-system/date.ts +3 -0
  47. package/src/design-system/layout.ts +106 -0
  48. package/src/design-system/theme.test.ts +94 -0
  49. package/src/design-system/theme.ts +93 -0
  50. package/src/design-system/typography.ts +179 -0
  51. package/src/design-system/vars.test.ts +1679 -0
  52. package/src/design-system/vars.ts +52 -0
  53. package/src/design-system.test.ts +45 -22
  54. package/src/design-system.ts +25 -1339
  55. package/src/dropdown-menu/index.tsx +21 -16
  56. package/src/emoji-picker/app-icon-picker.tsx +5 -5
  57. package/src/emoji-picker/emoji-picker-content-with-color.tsx +4 -4
  58. package/src/emoji-picker/icon-emoji-picker.tsx +3 -3
  59. package/src/favorites-icon.tsx +1 -1
  60. package/src/fibermoji-placeholder.tsx +3 -3
  61. package/src/field-container.tsx +11 -2
  62. package/src/file-item/file-icon.tsx +169 -0
  63. package/src/file-item/file-menu-items.tsx +68 -0
  64. package/src/file-item/file-preview-actions.tsx +38 -0
  65. package/src/file-item/file-title.tsx +48 -0
  66. package/src/file-item/types.ts +27 -0
  67. package/src/file-item/use-register-in-image-gallery.tsx +70 -0
  68. package/src/file-item-2.tsx +32 -348
  69. package/src/file-item.tsx +6 -3
  70. package/src/hue-shift.test.ts +91 -0
  71. package/src/icons/ast/ChatBubble.ts +8 -0
  72. package/src/icons/ast/ChatFloat.ts +8 -0
  73. package/src/icons/ast/ChatSidebar.ts +8 -0
  74. package/src/icons/ast/FileCounter.ts +8 -0
  75. package/src/icons/ast/FileMultiple.ts +8 -0
  76. package/src/icons/ast/FileOther.ts +8 -0
  77. package/src/icons/ast/NoBorder.ts +8 -0
  78. package/src/icons/ast/NoFill.ts +8 -0
  79. package/src/icons/ast/ValueEdit.ts +8 -0
  80. package/src/icons/ast/index.tsx +9 -0
  81. package/src/icons/react/ChatBubble.tsx +13 -0
  82. package/src/icons/react/ChatFloat.tsx +13 -0
  83. package/src/icons/react/ChatSidebar.tsx +13 -0
  84. package/src/icons/react/FileCounter.tsx +13 -0
  85. package/src/icons/react/FileMultiple.tsx +13 -0
  86. package/src/icons/react/FileOther.tsx +13 -0
  87. package/src/icons/react/NoBorder.tsx +13 -0
  88. package/src/icons/react/NoFill.tsx +13 -0
  89. package/src/icons/react/ValueEdit.tsx +13 -0
  90. package/src/icons/react/index.tsx +9 -0
  91. package/src/icons/svg/chat-bubble.svg +4 -0
  92. package/src/icons/svg/chat-float.svg +4 -0
  93. package/src/icons/svg/chat-sidebar.svg +4 -0
  94. package/src/icons/svg/file-counter.svg +3 -0
  95. package/src/icons/svg/file-multiple.svg +3 -0
  96. package/src/icons/svg/file-other.svg +3 -0
  97. package/src/icons/svg/no-border.svg +17 -0
  98. package/src/icons/svg/no-fill.svg +4 -0
  99. package/src/icons/svg/value-edit.svg +3 -0
  100. package/src/images-gallery/images-gallery.tsx +8 -6
  101. package/src/images-gallery/slide-buttons.tsx +4 -11
  102. package/src/is-iOS.ts +0 -1
  103. package/src/is-in-popup.ts +2 -1
  104. package/src/lists/actions-menu-row-surface.tsx +7 -7
  105. package/src/loading-sausage.tsx +2 -2
  106. package/src/media-query-utils.ts +1 -2
  107. package/src/mobile-keyboard-aware-popup.tsx +6 -5
  108. package/src/modal-menu/modal-menu-content.tsx +2 -2
  109. package/src/number-input/decimal.js +9 -7
  110. package/src/number-input/{number-inline-input-with-autosize.tsx → number-input-inline-with-autosize.tsx} +20 -41
  111. package/src/number-input/number-input-inline.tsx +118 -0
  112. package/src/number-input/{index.test.js → number-input.test.tsx} +38 -43
  113. package/src/number-input/number-input.tsx +63 -121
  114. package/src/number-input/types.ts +19 -0
  115. package/src/number-input/utils.ts +61 -0
  116. package/src/online-users.tsx +2 -2
  117. package/src/palette-generator.test.ts +309 -0
  118. package/src/palette-generator.ts +160 -0
  119. package/src/palettes/_.ts +72 -0
  120. package/src/palettes/slate-arch.colors-snapshot.test.ts +1689 -0
  121. package/src/palettes/slate-arch.ts +188 -0
  122. package/src/palettes/slate-user.colors-snapshot.test.ts +1689 -0
  123. package/src/palettes/slate-user.ts +187 -0
  124. package/src/palettes/warm-arch.colors-snapshot.test.ts +1689 -0
  125. package/src/palettes/warm-arch.ts +222 -0
  126. package/src/palettes/warm-user.colors-snapshot.test.ts +1689 -0
  127. package/src/palettes/warm-user.ts +222 -0
  128. package/src/platform.ts +0 -3
  129. package/src/popover/get-element-ref.ts +28 -0
  130. package/src/popover/index.tsx +236 -339
  131. package/src/popover/{mobile-popup-context.tsx → mobile-popover-context.tsx} +7 -7
  132. package/src/popover/mobile-popover.tsx +169 -0
  133. package/src/popover/modifiers.tsx +2 -2
  134. package/src/popover/popup-stack-context.tsx +8 -9
  135. package/src/progress.tsx +2 -2
  136. package/src/reactions/reaction-button.tsx +12 -6
  137. package/src/root-theme-provider.test.tsx +411 -0
  138. package/src/scale-generator.ts +356 -0
  139. package/src/select/components/menu-list-virtualized.tsx +12 -25
  140. package/src/select/components/menu.tsx +12 -2
  141. package/src/select/index.tsx +6 -5
  142. package/src/select/select.tsx +38 -39
  143. package/src/select/styles.ts +0 -1
  144. package/src/select/util.ts +1 -1
  145. package/src/static-palettes.ts +356 -0
  146. package/src/thematic-color-picker.tsx +266 -0
  147. package/src/thematic-constants.tsx +27 -0
  148. package/src/thematic-controls.tsx +144 -0
  149. package/src/thematic-scales.tsx +122 -0
  150. package/src/thematic-state.ts +370 -0
  151. package/src/thematic.tsx +386 -0
  152. package/src/theme-provider.test.tsx +820 -0
  153. package/src/theme-provider.tsx +158 -92
  154. package/src/theme-settings.ts +67 -12
  155. package/src/theme-styles.ts +58 -6
  156. package/src/toast/toast-action.tsx +1 -1
  157. package/src/toast/toast.tsx +1 -2
  158. package/src/toggle-on-off.tsx +2 -2
  159. package/src/toggle.tsx +7 -8
  160. package/src/tooltip.tsx +14 -10
  161. package/src/type-badge.tsx +7 -14
  162. package/src/unit/styles.ts +2 -25
  163. package/src/unit/unit-with-tooltip.tsx +3 -2
  164. package/src/use-is-phone.tsx +7 -2
  165. package/src/use-long-press.tsx +2 -2
  166. package/src/use-on-screen-keyboard-data.tsx +2 -2
  167. package/src/with-data.tsx +4 -3
  168. package/src/workflow-progress-icon.tsx +2 -2
  169. package/.eslintignore +0 -3
  170. package/.eslintrc +0 -14
  171. package/src/__mocks__/createInlineTheme.js +0 -3
  172. package/src/create-inline-theme.ts +0 -66
  173. package/src/number-input/index.js +0 -191
  174. package/src/palette.ts +0 -237
@@ -7,7 +7,8 @@ import {createPopper, Modifier, Placement, State} from "@popperjs/core";
7
7
  import {Options as FlipOptions} from "@popperjs/core/lib/modifiers/flip";
8
8
  import {Options as PreventOverflowOptions} from "@popperjs/core/lib/modifiers/preventOverflow";
9
9
  import cx from "classnames";
10
- import _ from "lodash";
10
+ import compact from "lodash/compact";
11
+ import noop from "lodash/noop";
11
12
  import {
12
13
  cloneElement,
13
14
  CSSProperties,
@@ -19,34 +20,141 @@ import {
19
20
  useId,
20
21
  useImperativeHandle,
21
22
  useMemo,
23
+ useRef,
22
24
  } from "react";
23
- import {
24
- safeAreaInsetBottomVar,
25
- safeAreaInsetLeftVar,
26
- safeAreaInsetRightVar,
27
- safeAreaInsetTopVar,
28
- } from "../mobile-styles";
29
25
  import {createPortal} from "react-dom";
30
26
  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";
27
+ import {space} from "../design-system";
28
+ import {ThemeProvider} from "../theme-provider";
36
29
  import {$TSFixMe} from "../tsfixme";
37
30
  import {useIsPhone} from "../use-is-phone";
38
31
  import {usePopupModifiers} from "./popup-modifiers-context";
39
32
  import {applyMaxSize, makeMaxSizeModifier, observeWidth} from "./modifiers";
40
33
  import {isElementReference, Reference} from "./reference";
41
34
  import {basicPopupClassName, basicPopupContainerClassName, hidePopupWhenTriggerIsHiddenClassName} from "./styles";
42
- import {MobilePopupProvider, useMobilePopup} from "./mobile-popup-context";
35
+ import {MobilePopoverProvider} from "./mobile-popover-context";
43
36
  import {usePopupStackContext} from "./popup-stack-context";
44
- import {useComposedRefs} from "@fibery/react/src/use-composed-refs";
37
+ import {composeRefs, useComposedRefs} from "@fibery/react/src/use-composed-refs";
45
38
  import {createContext} from "@fibery/react/src/create-context";
46
39
  import {Wrap} from "@fibery/react/src/wrap";
47
40
  import {composeEventHandlers} from "@fibery/react/src/compose-event-handlers";
41
+ import {getElementRef} from "./get-element-ref";
42
+ import {MobilePopover} from "./mobile-popover";
43
+ import {FocusScope, FocusScopeProps} from "@radix-ui/react-focus-scope";
44
+
45
+ type PopoverContextValue = {
46
+ open: boolean;
47
+ disabled?: boolean;
48
+ onOpen?: () => unknown;
49
+ onClose?: (event: Event) => unknown;
50
+ triggerElement: Reference;
51
+ setTriggerElement: (value: Reference) => void;
52
+ popupElement: HTMLDivElement | null;
53
+ setPopupElement: (value: HTMLDivElement) => void;
54
+ };
55
+
56
+ const [PopoverProvider, usePopoverContext] = createContext<PopoverContextValue>("PopoverContext");
57
+
58
+ type RootProps = {
59
+ children: ReactNode;
60
+ open: boolean;
61
+ disabled?: boolean;
62
+ onOpen?: () => unknown;
63
+ onClose?: (event: Event) => unknown;
64
+ // For legacy popup component support
65
+ triggerElement?: Reference;
66
+ setTriggerElement?: (value: Reference) => void;
67
+ popupElement?: HTMLDivElement | null;
68
+ setPopupElement?: (value: HTMLDivElement | null) => void;
69
+ };
70
+
71
+ const Root = ({children, open, disabled = false, onOpen, onClose, ...rest}: RootProps) => {
72
+ const id = useId();
73
+ const {registerEscapeCallback, registerPopup} = usePopupStackContext();
74
+
75
+ const [triggerElement, setTriggerElement] = useControllableState<Reference>({
76
+ value: rest.triggerElement,
77
+ onChange: rest.setTriggerElement,
78
+ });
79
+ const [popupElement, setPopupElement] = useControllableState<HTMLDivElement | null>({
80
+ value: rest.popupElement,
81
+ onChange: rest.setPopupElement,
82
+ });
48
83
 
49
- export const mobileZIndex = 1002;
84
+ const composedSetTriggerElement = useComposedRefs(setTriggerElement, rest.setTriggerElement);
85
+ const composedSetPopupElement = useComposedRefs(setPopupElement, rest.setPopupElement);
86
+
87
+ useEffect(() => {
88
+ let unregister: () => void;
89
+ if (registerPopup && open) {
90
+ unregister = registerPopup(id);
91
+ }
92
+ return () => {
93
+ unregister?.();
94
+ };
95
+ }, [registerPopup, id, open]);
96
+
97
+ useEffect(() => {
98
+ if (triggerElement && open) {
99
+ // Note: We don't check defaultPrevented because react-select and similar
100
+ // components call preventDefault() on all keydown events. We also don't
101
+ // expose onEscapeKeyDown prop to let consumers prevent default close behavior.
102
+ const onKeyDown = (evt: KeyboardEvent) => {
103
+ if (evt.key === "Escape") {
104
+ onClose?.(evt);
105
+ if (isElementReference(triggerElement)) {
106
+ triggerElement.focus();
107
+ }
108
+ evt.preventDefault();
109
+ evt.stopPropagation();
110
+ }
111
+ };
112
+
113
+ if (registerEscapeCallback) {
114
+ registerEscapeCallback(id, onKeyDown);
115
+ } else {
116
+ document.addEventListener("keydown", onKeyDown);
117
+ }
118
+
119
+ // if popup was opened but nothing was focused inside it we should catch 'Escape' key from trigger as well
120
+ // otherwise it can be caught by navigation panel
121
+ "addEventListener" in triggerElement && triggerElement.addEventListener("keydown", onKeyDown);
122
+
123
+ return () => {
124
+ if (!registerEscapeCallback) {
125
+ document.removeEventListener("keydown", onKeyDown);
126
+ }
127
+
128
+ "removeEventListener" in triggerElement && triggerElement.removeEventListener("keydown", onKeyDown);
129
+ };
130
+ }
131
+ return () => {};
132
+ }, [onClose, open, triggerElement, registerEscapeCallback, id]);
133
+
134
+ const contextValue = useMemo(() => {
135
+ return {
136
+ disabled,
137
+ triggerElement,
138
+ setTriggerElement: composedSetTriggerElement,
139
+ popupElement,
140
+ setPopupElement: composedSetPopupElement,
141
+ open,
142
+ onClose,
143
+ onOpen,
144
+ };
145
+ }, [
146
+ disabled,
147
+ triggerElement,
148
+ composedSetTriggerElement,
149
+ popupElement,
150
+ composedSetPopupElement,
151
+ open,
152
+ onClose,
153
+ onOpen,
154
+ ]);
155
+
156
+ return <PopoverProvider value={contextValue}>{children}</PopoverProvider>;
157
+ };
50
158
 
51
159
  const Backdrop = ({className}: {className?: string}) => {
52
160
  return (
@@ -65,187 +173,55 @@ const Backdrop = ({className}: {className?: string}) => {
65
173
  );
66
174
  };
67
175
 
68
- const mobileAnimationClassName = css`
69
- transition: transform ${transition}, opacity ${transition};
70
- transform: translateY(0);
71
- opacity: 1;
72
-
73
- @media (prefers-reduced-motion: no-preference) {
74
- @starting-style {
75
- transform: translateY(20%);
76
- opacity: 0;
77
- }
78
- }
79
- `;
80
-
81
- const popupContentClassName = css`
82
- display: grid;
83
- background: ${themeVars.actionMenuBg};
84
- grid-template-rows: 60px 1fr;
85
- `;
86
-
87
- export function MobilePopup({
88
- content,
89
- mobilePopupStyle,
90
- mobilePopupContentClassName,
91
- title,
92
- onClose,
93
- height,
94
- onNavigateBack,
95
- }: {
96
- content: ReactNode;
97
- mobilePopupStyle?: CSSProperties;
98
- mobilePopupContentClassName?: string;
99
- title: string;
100
- height?: string;
101
- onClose?: () => void;
102
- onNavigateBack?: () => void;
103
- }) {
104
- const theme = useTheme();
105
-
106
- const {title: contextTitle, headerSlot} = useMobilePopup();
107
-
108
- const headerContent = headerSlot ?? (
109
- <>
110
- {onNavigateBack && (
111
- <IconButton onClick={onNavigateBack} size="xLarge">
112
- <ArrowLeftIcon />
113
- </IconButton>
114
- )}
115
- <div
116
- className={css`
117
- ${textStyles.heading3}
118
- flex: 1;
119
- min-width: 0;
120
- overflow: hidden;
121
- text-overflow: ellipsis;
122
- white-space: nowrap;
123
- `}
124
- >
125
- {contextTitle ?? title}
126
- </div>
127
- {onClose && (
128
- <IconButton onClick={onClose} size="xLarge">
129
- <CloseIcon />
130
- </IconButton>
131
- )}
132
- </>
133
- );
134
-
135
- const popupContent = (
136
- <>
137
- <div
138
- className={cx(
139
- css`
140
- display: flex;
141
- align-items: center;
142
- gap: ${space.s8}px;
143
- padding: ${space.s8}px ${space.s8}px ${space.s8}px ${space.s20}px;
144
- min-width: 0;
145
- `,
146
- onNavigateBack &&
147
- css`
148
- padding-left: ${space.s4}px;
149
- `
150
- )}
151
- >
152
- {headerContent}
153
- </div>
154
- <div
155
- className={cx(
156
- css`
157
- border-top: ${themeVars.separators.opacity5};
158
- min-width: 0;
159
- `,
160
- mobilePopupContentClassName
161
- )}
162
- >
163
- {content}
164
- </div>
165
- </>
166
- );
167
-
168
- return (
169
- <div
170
- className={cx(
171
- css`
172
- position: absolute;
173
- left: var(${safeAreaInsetLeftVar});
174
- right: var(${safeAreaInsetRightVar});
175
- top: var(${safeAreaInsetTopVar});
176
- bottom: var(${safeAreaInsetBottomVar});
177
- z-index: ${mobileZIndex};
178
- `,
179
- !height && mobileAnimationClassName,
180
- !height && popupContentClassName,
181
- height &&
182
- css`
183
- display: grid;
184
- `
185
- )}
186
- style={{
187
- ...createInlineTheme(theme),
188
- ...mobilePopupStyle,
189
- ...(height ? {gridTemplateRows: `1fr ${height}`} : {}),
190
- }}
191
- onClick={stopPropagation}
192
- >
193
- {height ? (
194
- <>
195
- <div
196
- onClick={onClose}
197
- className={css`
198
- background-color: ${themeVars.modalBg};
199
- `}
200
- />
201
- <div
202
- className={cx(
203
- css`
204
- display: grid;
205
- grid-template-rows: 60px 1fr;
206
- `,
207
- mobileAnimationClassName,
208
- popupContentClassName
209
- )}
210
- >
211
- {popupContent}
212
- </div>
213
- </>
214
- ) : (
215
- popupContent
216
- )}
217
- </div>
218
- );
219
- }
220
-
221
176
  type PortalProps = {
222
177
  getPopupContainer?: () => HTMLElement;
223
178
  children: ReactNode;
224
179
  };
225
180
 
226
- const Portal = ({getPopupContainer: popupContainerOverride, children}: PortalProps) => {
181
+ const Portal = ({getPopupContainer, children}: PortalProps) => {
227
182
  const popupModifiers = usePopupModifiers();
228
- const getPopupContainer = popupContainerOverride ?? popupModifiers.getPopupContainer;
229
- return createPortal(children, getPopupContainer());
183
+ const getPopupContainerResult = getPopupContainer ?? popupModifiers.getPopupContainer;
184
+ return createPortal(children, getPopupContainerResult());
230
185
  };
231
186
 
232
187
  type TriggerProps = {
233
188
  asChild?: boolean;
234
- // Support case when popup trigger is virtual reference
189
+ /** Custom reference element for popover positioning. Use when the trigger element differs from the anchor point (e.g., virtual/detached elements). */
235
190
  reference?: Reference;
191
+ /** Event that opens the popover. "click" allows proper focus handling inside popover content. Default: "mousedown" */
192
+ triggerEvent?: "mousedown" | "click";
236
193
  } & React.ComponentPropsWithoutRef<"div">;
237
194
 
238
- const Trigger = ({asChild = false, reference, children, onMouseDown, ...props}: TriggerProps) => {
239
- const {setTriggerElement, disabled, open, onOpen, onClose} = useContext();
240
-
241
- const onMouseDownCb: React.MouseEventHandler<HTMLElement> = composeEventHandlers(onMouseDown, (e) => {
195
+ const Trigger = ({
196
+ asChild = false,
197
+ reference,
198
+ children,
199
+ triggerEvent = "mousedown",
200
+ onMouseDown,
201
+ onClick,
202
+ ...props
203
+ }: TriggerProps) => {
204
+ const {setTriggerElement, disabled, open, onOpen, onClose} = usePopoverContext();
205
+
206
+ const onToggle = (e: React.MouseEvent<HTMLElement>) => {
242
207
  if (disabled) {
243
208
  return;
244
209
  }
245
210
  if (e.button === 0) {
246
211
  open ? onClose?.(e as unknown as Event) : onOpen?.();
247
212
  }
248
- });
213
+ };
214
+
215
+ const triggerEventHandlers =
216
+ triggerEvent === "click"
217
+ ? {
218
+ onClick: composeEventHandlers(onClick, onToggle),
219
+ onMouseDown,
220
+ }
221
+ : {
222
+ onMouseDown: composeEventHandlers(onMouseDown, onToggle),
223
+ onClick,
224
+ };
249
225
 
250
226
  useEffect(() => {
251
227
  if (reference) {
@@ -259,17 +235,20 @@ const Trigger = ({asChild = false, reference, children, onMouseDown, ...props}:
259
235
  }
260
236
 
261
237
  if (asChild && isValidElement(children)) {
238
+ const childrenRef = getElementRef(children);
239
+
262
240
  return cloneElement(children, {
263
- ref: setTriggerElement,
264
241
  "aria-expanded": open,
265
242
  "aria-haspopup": true,
266
- onMouseDown: onMouseDownCb,
267
243
  ...props,
244
+ ref: composeRefs(setTriggerElement, childrenRef),
245
+ className: cx(children.props.className, props.className),
246
+ ...triggerEventHandlers,
268
247
  });
269
248
  }
270
249
 
271
250
  return (
272
- <div ref={setTriggerElement} onMouseDown={onMouseDownCb} {...props}>
251
+ <div ref={setTriggerElement} {...triggerEventHandlers} {...props}>
273
252
  {children}
274
253
  </div>
275
254
  );
@@ -282,7 +261,7 @@ type ContentProps = {
282
261
  popupClassName?: string;
283
262
  popupContainerClassName?: string;
284
263
  offset?: [number, number];
285
- onMouseLeave?: (e: MouseEvent<HTMLDivElement>) => void;
264
+ padding?: number;
286
265
  renderBackdrop?: boolean;
287
266
  backdropClassName?: string;
288
267
  preventOverflowOptions?: Partial<PreventOverflowOptions>;
@@ -304,17 +283,24 @@ type ContentProps = {
304
283
  * Marked as deprecated because it's better to avoid this prop
305
284
  */
306
285
  fixStackingContextAfterOpen?: boolean;
286
+ /** ran after Popup positioned the element the first time */
287
+ onFirstUpdate?: () => void;
288
+ onMouseLeave?: (e: MouseEvent<HTMLDivElement>) => void;
307
289
  /**
308
- * ran after Popup positioned the element the first time
290
+ * Event handler called when auto-focusing on open.
291
+ * Can be prevented.
309
292
  */
310
- onFirstUpdate?: () => void;
293
+ onOpenAutoFocus?: FocusScopeProps["onMountAutoFocus"];
294
+ /**
295
+ * Event handler called when auto-focusing on close.
296
+ * Can be prevented.
297
+ */
298
+ onCloseAutoFocus?: FocusScopeProps["onUnmountAutoFocus"];
311
299
  };
312
300
 
313
301
  export type ContentRef = {update: (() => Promise<unknown>) | null};
314
302
 
315
- const padding = space.s8;
316
-
317
- const Content = forwardRef<ContentRef, ContentProps>(function PopoverContentInner(
303
+ const Content = forwardRef<ContentRef, ContentProps>(function PopoverContent(
318
304
  {
319
305
  children,
320
306
  placement,
@@ -322,7 +308,7 @@ const Content = forwardRef<ContentRef, ContentProps>(function PopoverContentInne
322
308
  popupContainerClassName,
323
309
  popupClassName,
324
310
  offset = [0, 0],
325
- onMouseLeave,
311
+ padding = space.s8,
326
312
  renderBackdrop,
327
313
  backdropClassName,
328
314
  preventOverflowOptions = {},
@@ -335,15 +321,17 @@ const Content = forwardRef<ContentRef, ContentProps>(function PopoverContentInne
335
321
  mobilePopupStyle,
336
322
  mobilePopupContentClassName,
337
323
  fixStackingContextAfterOpen = false,
338
- onFirstUpdate,
339
324
  transform,
325
+ onFirstUpdate,
326
+ onMouseLeave,
327
+ onOpenAutoFocus,
328
+ onCloseAutoFocus,
340
329
  },
341
330
  ref
342
331
  ) {
343
- const {open, onClose, triggerElement, popupElement, setPopupElement} = useContext();
332
+ const {open, onClose, triggerElement, popupElement, setPopupElement} = usePopoverContext();
344
333
 
345
334
  const popupModifiers = usePopupModifiers();
346
- const theme = useTheme();
347
335
  const isPhone = useIsPhone();
348
336
 
349
337
  // We get infinite popup renders if this is not in a separate memo
@@ -357,10 +345,10 @@ const Content = forwardRef<ContentRef, ContentProps>(function PopoverContentInne
357
345
  applyMaxSize,
358
346
  ]
359
347
  : [];
360
- }, [preventOverflowOptions, shrinkable]);
348
+ }, [padding, preventOverflowOptions, shrinkable]);
361
349
 
362
350
  const modifiers = useMemo(() => {
363
- return _.compact([
351
+ return compact([
364
352
  {
365
353
  name: "flip",
366
354
  options: {
@@ -395,6 +383,7 @@ const Content = forwardRef<ContentRef, ContentProps>(function PopoverContentInne
395
383
  additionalModifiers,
396
384
  flipOptions,
397
385
  offset,
386
+ padding,
398
387
  popupModifiers,
399
388
  preventOverflowOptions,
400
389
  shrinkableModifiers,
@@ -406,17 +395,20 @@ const Content = forwardRef<ContentRef, ContentProps>(function PopoverContentInne
406
395
 
407
396
  useImperativeHandle(ref, () => ({update}), [update]);
408
397
 
398
+ const hasInteractedOutsideRef = useRef(false);
399
+
409
400
  const {onPointerDownCapture} = usePointerDownOutside((event) => {
410
401
  if (!open || (isPhone && supportMobile)) {
411
402
  return;
412
403
  }
413
404
  const target = event.target;
414
405
  if (target instanceof Node) {
415
- const ignoreElements = _.compact([isElementReference(triggerElement) && triggerElement]);
406
+ const ignoreElements = compact([isElementReference(triggerElement) && triggerElement]);
416
407
  if (ignoreElements.some((element) => element.contains(target))) {
417
408
  return;
418
409
  }
419
410
  }
411
+ hasInteractedOutsideRef.current = true;
420
412
  onClose?.(event);
421
413
  });
422
414
 
@@ -427,8 +419,8 @@ const Content = forwardRef<ContentRef, ContentProps>(function PopoverContentInne
427
419
  };
428
420
 
429
421
  const contentStyles = useMemo(() => {
430
- return {...styles.popper, ...createInlineTheme(theme), ...popupStyle};
431
- }, [popupStyle, styles.popper, theme]);
422
+ return {...styles.popper, ...popupStyle};
423
+ }, [popupStyle, styles.popper]);
432
424
 
433
425
  if (!open) {
434
426
  return null;
@@ -441,153 +433,66 @@ const Content = forwardRef<ContentRef, ContentProps>(function PopoverContentInne
441
433
 
442
434
  if (isPhone && supportMobile) {
443
435
  return (
444
- <MobilePopupProvider>
445
- <MobilePopup
436
+ <MobilePopoverProvider>
437
+ <MobilePopover
446
438
  onClose={onClose as $TSFixMe}
447
439
  title={title}
448
440
  mobilePopupContentClassName={mobilePopupContentClassName}
449
441
  mobilePopupStyle={mobilePopupStyle}
450
442
  content={children}
451
443
  />
452
- </MobilePopupProvider>
444
+ </MobilePopoverProvider>
453
445
  );
454
446
  }
447
+
455
448
  return (
456
- <>
449
+ <ThemeProvider portal>
457
450
  {renderBackdrop && <Backdrop className={backdropClassName} />}
458
- <div
459
- ref={setPopupElement}
460
- className={cx(
461
- basicPopupContainerClassName,
462
- hidePopupWhenTriggerIsHidden && hidePopupWhenTriggerIsHiddenClassName,
463
- popupContainerClassName
464
- )}
465
- onMouseLeave={onMouseLeave}
466
- onClick={stopPropagation}
467
- {...attributes.popper}
468
- onPointerDownCapture={onPointerDownCapture}
469
- style={contentStyles}
451
+ <FocusScope
452
+ asChild
453
+ loop
454
+ trapped={false}
455
+ onMountAutoFocus={onOpenAutoFocus}
456
+ onUnmountAutoFocus={(event) => {
457
+ onCloseAutoFocus?.(event);
458
+
459
+ if (!event.defaultPrevented) {
460
+ if (!hasInteractedOutsideRef.current) {
461
+ if (isElementReference(triggerElement)) {
462
+ triggerElement.focus();
463
+ }
464
+ }
465
+ // Always prevent auto focus because we either focus manually or want user agent focus
466
+ event.preventDefault();
467
+ }
468
+ }}
470
469
  >
471
- <div className={cx(basicPopupClassName, popupClassName)} style={popupStyle} onAnimationEnd={onAnimationEnd}>
472
- {children}
470
+ <div
471
+ role="dialog"
472
+ ref={setPopupElement}
473
+ className={cx(
474
+ basicPopupContainerClassName,
475
+ hidePopupWhenTriggerIsHidden && hidePopupWhenTriggerIsHiddenClassName,
476
+ popupContainerClassName
477
+ )}
478
+ onMouseLeave={onMouseLeave}
479
+ onClick={stopPropagation}
480
+ {...attributes.popper}
481
+ onPointerDownCapture={onPointerDownCapture}
482
+ style={contentStyles}
483
+ >
484
+ <div className={cx(basicPopupClassName, popupClassName)} style={popupStyle} onAnimationEnd={onAnimationEnd}>
485
+ {children}
486
+ </div>
473
487
  </div>
474
- </div>
475
- </>
488
+ </FocusScope>
489
+ </ThemeProvider>
476
490
  );
477
491
  });
478
492
 
479
- type ContextValue = {
480
- open: boolean;
481
- disabled?: boolean;
482
- onOpen?: () => unknown;
483
- onClose?: (event: Event) => unknown;
484
- triggerElement: Reference;
485
- setTriggerElement: (value: Reference) => void;
486
- popupElement: HTMLDivElement | null;
487
- setPopupElement: (value: HTMLDivElement) => void;
488
- };
489
-
490
- const [Provider, useContext] = createContext<ContextValue>("PopoverContext");
491
-
492
- type RootProps = {
493
- children: ReactNode;
494
- open: boolean;
495
- disabled?: boolean;
496
- closeOnEscape?: boolean;
497
- onOpen?: () => unknown;
498
- onClose?: (event: Event) => unknown;
499
- // For legacy popup component support
500
- triggerElement?: Reference;
501
- setTriggerElement?: (value: Reference) => void;
502
- popupElement?: HTMLDivElement | null;
503
- setPopupElement?: (value: HTMLDivElement | null) => void;
504
- };
505
-
506
- const Root = ({children, open, disabled = false, closeOnEscape = true, onOpen, onClose, ...rest}: RootProps) => {
507
- const id = useId();
508
- const {registerEscapeCallback, registerPopup} = usePopupStackContext();
509
-
510
- const [triggerElement, setTriggerElement] = useControllableState<Reference>({
511
- value: rest.triggerElement,
512
- onChange: rest.setTriggerElement,
513
- });
514
- const [popupElement, setPopupElement] = useControllableState<HTMLDivElement | null>({
515
- value: rest.popupElement,
516
- onChange: rest.setPopupElement,
517
- });
518
-
519
- const composedSetTriggerElement = useComposedRefs(setTriggerElement, rest.setTriggerElement);
520
- const composedSetPopupElement = useComposedRefs(setPopupElement, rest.setPopupElement);
521
-
522
- useEffect(() => {
523
- let unregister: () => void;
524
- if (registerPopup && open) {
525
- unregister = registerPopup(id);
526
- }
527
- return () => {
528
- unregister?.();
529
- };
530
- }, [registerPopup, id, open]);
531
-
532
- useEffect(() => {
533
- if (triggerElement && closeOnEscape && open) {
534
- const onKeyDown = (evt: KeyboardEvent) => {
535
- if (evt.defaultPrevented) {
536
- return;
537
- }
538
- if (evt.key === "Escape") {
539
- onClose?.(evt);
540
- evt.preventDefault();
541
- evt.stopPropagation();
542
- }
543
- };
544
-
545
- if (registerEscapeCallback) {
546
- registerEscapeCallback(id, onKeyDown);
547
- } else {
548
- document.addEventListener("keydown", onKeyDown);
549
- }
550
-
551
- // if popup was opened but nothing was focused inside it we should catch 'Escape' key from trigger as well
552
- // otherwise it can be caught by navigation panel
553
- "addEventListener" in triggerElement && triggerElement.addEventListener("keydown", onKeyDown);
554
-
555
- return () => {
556
- if (!registerEscapeCallback) {
557
- document.removeEventListener("keydown", onKeyDown);
558
- }
559
-
560
- "removeEventListener" in triggerElement && triggerElement.removeEventListener("keydown", onKeyDown);
561
- };
562
- }
563
- // eslint-disable-next-line @typescript-eslint/no-empty-function
564
- return () => {};
565
- }, [closeOnEscape, onClose, open, triggerElement, registerEscapeCallback, id]);
566
-
567
- const contextValue = useMemo(() => {
568
- return {
569
- disabled,
570
- triggerElement,
571
- setTriggerElement: composedSetTriggerElement,
572
- popupElement,
573
- setPopupElement: composedSetPopupElement,
574
- open,
575
- onClose,
576
- onOpen,
577
- };
578
- }, [
579
- disabled,
580
- triggerElement,
581
- composedSetTriggerElement,
582
- popupElement,
583
- composedSetPopupElement,
584
- open,
585
- onClose,
586
- onOpen,
587
- ]);
493
+ export {createPopper};
588
494
 
589
- return <Provider value={contextValue}>{children}</Provider>;
590
- };
495
+ export type {Placement, State, Reference, RootProps, TriggerProps, ContentProps};
591
496
 
592
497
  export const Popover = {
593
498
  Root,
@@ -596,14 +501,7 @@ export const Popover = {
596
501
  Content,
597
502
  };
598
503
 
599
- export {createPopper};
600
-
601
- export type {Placement, State, Reference, RootProps, TriggerProps, ContentProps};
602
-
603
- export type PopoverOldProps = Pick<
604
- RootProps,
605
- "children" | "open" | "onOpen" | "onClose" | "closeOnEscape" | "disabled"
606
- > &
504
+ export type PopoverOldProps = Pick<RootProps, "children" | "open" | "onOpen" | "onClose" | "disabled"> &
607
505
  ContentProps &
608
506
  Pick<PortalProps, "getPopupContainer"> & {
609
507
  trigger?: ReactNode;
@@ -635,7 +533,7 @@ export type PopoverOldProps = Pick<
635
533
  * onOpen={() => setIsOpen(true)}
636
534
  * onClose={() => setIsOpen(false)}
637
535
  * >
638
- * <Popover.Trigger>
536
+ * <Popover.Trigger asChild triggerEvent="click">
639
537
  * <button>Click me</button>
640
538
  * </Popover.Trigger>
641
539
  * <Popover.Portal>
@@ -661,14 +559,13 @@ export const PopoverOld = ({
661
559
  onMouseLeave,
662
560
  renderBackdrop,
663
561
  backdropClassName,
664
- closeOnEscape = true,
665
562
  preventOverflowOptions = {},
666
563
  flipOptions = {},
667
564
  additionalModifiers = [],
668
565
  hidePopupWhenTriggerIsHidden = true,
669
566
  shrinkable,
670
- onOpen = _.noop,
671
- onClose = _.noop,
567
+ onOpen = noop,
568
+ onClose = noop,
672
569
  supportMobile = false,
673
570
  title = "",
674
571
  mobilePopupStyle,
@@ -679,7 +576,7 @@ export const PopoverOld = ({
679
576
  transform,
680
577
  }: PopoverOldProps) => {
681
578
  return (
682
- <Popover.Root open={open} disabled={disabled} closeOnEscape={closeOnEscape} onOpen={onOpen} onClose={onClose}>
579
+ <Popover.Root open={open} disabled={disabled} onOpen={onOpen} onClose={onClose}>
683
580
  {triggerDomElement ? (
684
581
  <Popover.Trigger reference={triggerDomElement}></Popover.Trigger>
685
582
  ) : (