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