reshaped 3.9.0 → 3.9.1-canary.3

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 (53) hide show
  1. package/dist/bundle.css +1 -1
  2. package/dist/bundle.js +2 -2
  3. package/dist/components/Accordion/AccordionControlled.js +0 -1
  4. package/dist/components/Actionable/Actionable.d.ts +8 -3
  5. package/dist/components/Actionable/Actionable.js +17 -70
  6. package/dist/components/Actionable/Actionable.types.d.ts +2 -36
  7. package/dist/components/Actionable/index.d.ts +2 -1
  8. package/dist/components/Badge/Badge.js +5 -4
  9. package/dist/components/Badge/Badge.module.css +1 -1
  10. package/dist/components/Calendar/Calendar.utils.js +6 -7
  11. package/dist/components/Card/Card.d.ts +1 -1
  12. package/dist/components/Carousel/Carousel.js +0 -1
  13. package/dist/components/Flyout/Flyout.module.css +1 -1
  14. package/dist/components/Flyout/Flyout.types.d.ts +7 -7
  15. package/dist/components/Flyout/FlyoutContent.js +3 -58
  16. package/dist/components/Flyout/FlyoutControlled.js +84 -83
  17. package/dist/components/Flyout/FlyoutTrigger.js +3 -3
  18. package/dist/components/Flyout/useFlyout.d.ts +3 -4
  19. package/dist/components/Flyout/useFlyout.js +70 -88
  20. package/dist/components/Flyout/utilities/safeArea.d.ts +10 -0
  21. package/dist/components/Flyout/utilities/safeArea.js +100 -0
  22. package/dist/components/Select/Select.js +1 -1
  23. package/dist/components/Select/SelectCustomControlled.js +0 -1
  24. package/dist/components/Tabs/TabsControlled.js +0 -1
  25. package/dist/components/Toast/ToastContainer.js +0 -1
  26. package/dist/components/_private/Expandable/Expandable.js +1 -3
  27. package/dist/components/_private/Portal/Portal.js +0 -3
  28. package/dist/core/Actionable/Actionable.d.ts +4 -0
  29. package/dist/core/Actionable/Actionable.js +73 -0
  30. package/dist/core/Actionable/Actionable.types.d.ts +34 -0
  31. package/dist/core/Actionable/Actionable.types.js +1 -0
  32. package/dist/core/Actionable/index.d.ts +2 -0
  33. package/dist/core/Actionable/index.js +1 -0
  34. package/dist/hooks/_private/usePrevious.js +0 -1
  35. package/dist/hooks/useOnClickOutside.js +8 -0
  36. package/dist/utilities/a11y/TrapFocus.js +9 -3
  37. package/dist/utilities/dom/index.d.ts +0 -1
  38. package/dist/utilities/dom/index.js +0 -1
  39. package/package.json +4 -2
  40. package/dist/components/Flyout/utilities/calculatePosition.d.ts +0 -31
  41. package/dist/components/Flyout/utilities/calculatePosition.js +0 -185
  42. package/dist/components/Flyout/utilities/constants.d.ts +0 -1
  43. package/dist/components/Flyout/utilities/constants.js +0 -1
  44. package/dist/components/Flyout/utilities/flyout.d.ts +0 -11
  45. package/dist/components/Flyout/utilities/flyout.js +0 -115
  46. package/dist/components/Flyout/utilities/getPositionFallbacks.d.ts +0 -3
  47. package/dist/components/Flyout/utilities/getPositionFallbacks.js +0 -39
  48. package/dist/components/Flyout/utilities/helpers.d.ts +0 -7
  49. package/dist/components/Flyout/utilities/helpers.js +0 -14
  50. package/dist/components/Flyout/utilities/isFullyVisible.d.ts +0 -13
  51. package/dist/components/Flyout/utilities/isFullyVisible.js +0 -23
  52. package/dist/utilities/dom/flyout.d.ts +0 -2
  53. package/dist/utilities/dom/flyout.js +0 -14
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Checks if a point is inside a triangle using barycentric coordinates
3
+ */
4
+ function isPointInTriangle(point, triangle) {
5
+ const [p1, p2, p3] = triangle;
6
+ const denominator = (p2.y - p3.y) * (p1.x - p3.x) + (p3.x - p2.x) * (p1.y - p3.y);
7
+ const a = ((p2.y - p3.y) * (point.x - p3.x) + (p3.x - p2.x) * (point.y - p3.y)) / denominator;
8
+ const b = ((p3.y - p1.y) * (point.x - p3.x) + (p1.x - p3.x) * (point.y - p3.y)) / denominator;
9
+ const c = 1 - a - b;
10
+ return a >= 0 && a <= 1 && b >= 0 && b <= 1 && c >= 0 && c <= 1;
11
+ }
12
+ /**
13
+ * Gets the two closest corners of the content element based on the flyout position
14
+ */
15
+ function getContentCorners(contentRect, position) {
16
+ const corners = {
17
+ topLeft: { x: contentRect.left, y: contentRect.top },
18
+ topRight: { x: contentRect.right, y: contentRect.top },
19
+ bottomLeft: { x: contentRect.left, y: contentRect.bottom },
20
+ bottomRight: { x: contentRect.right, y: contentRect.bottom },
21
+ };
22
+ if (position?.startsWith("bottom")) {
23
+ return [corners.topLeft, corners.topRight];
24
+ }
25
+ else if (position?.startsWith("top")) {
26
+ return [corners.bottomLeft, corners.bottomRight];
27
+ }
28
+ else if (position?.startsWith("start")) {
29
+ return [corners.topRight, corners.bottomRight];
30
+ }
31
+ else {
32
+ return [corners.topLeft, corners.bottomLeft];
33
+ }
34
+ }
35
+ /**
36
+ * Checks if the mouse is over the trigger or content elements
37
+ */
38
+ function isMouseOverElement(point, contentRef, triggerRef) {
39
+ const elements = document.elementsFromPoint(point.x, point.y);
40
+ return elements.some((el) => (contentRef.current && contentRef.current.contains(el)) ||
41
+ (triggerRef.current && triggerRef.current.contains(el)));
42
+ }
43
+ export function createSafeArea(options) {
44
+ const { contentRef, triggerRef, position, onClose, origin: passedOrigin } = options;
45
+ if (!contentRef.current) {
46
+ // If content doesn't exist, just close immediately
47
+ onClose();
48
+ return () => { };
49
+ }
50
+ const contentRect = contentRef.current.getBoundingClientRect();
51
+ const [corner1, corner2] = getContentCorners(contentRect, position);
52
+ // Add buffer to origin based on position to extend safe area
53
+ const buffer = 10;
54
+ const origin = { x: passedOrigin.x, y: passedOrigin.y };
55
+ if (position?.startsWith("bottom")) {
56
+ origin.y -= buffer;
57
+ }
58
+ else if (position?.startsWith("top")) {
59
+ origin.y += buffer;
60
+ }
61
+ else if (position?.startsWith("start")) {
62
+ origin.x += buffer;
63
+ }
64
+ else if (position?.startsWith("end")) {
65
+ origin.x -= buffer;
66
+ }
67
+ const triangle = [origin, corner1, corner2];
68
+ let timeoutId = null;
69
+ const cleanup = () => {
70
+ document.removeEventListener("mousemove", handleMouseMove);
71
+ if (timeoutId)
72
+ clearTimeout(timeoutId);
73
+ };
74
+ // Start timeout for 1 second
75
+ const startTimeout = () => {
76
+ if (timeoutId)
77
+ clearTimeout(timeoutId);
78
+ timeoutId = setTimeout(() => {
79
+ onClose();
80
+ cleanup();
81
+ }, 1000);
82
+ };
83
+ const handleMouseMove = (e) => {
84
+ const currentPoint = { x: e.clientX, y: e.clientY };
85
+ if (isMouseOverElement(currentPoint, contentRef, triggerRef)) {
86
+ cleanup();
87
+ return;
88
+ }
89
+ if (isPointInTriangle(currentPoint, triangle)) {
90
+ startTimeout();
91
+ }
92
+ else {
93
+ onClose();
94
+ cleanup();
95
+ }
96
+ };
97
+ startTimeout();
98
+ document.addEventListener("mousemove", handleMouseMove);
99
+ return cleanup;
100
+ }
@@ -9,7 +9,7 @@ const Select = (props) => {
9
9
  return (_jsx(SelectRoot, { ...props, children: (props) => {
10
10
  const { options } = props;
11
11
  const hasOptionChildren = React.Children.toArray(children).some((child) => {
12
- return React.isValidElement(child) && child.type === "option";
12
+ return (React.isValidElement(child) && (child.type === "option" || child.type === "optgroup"));
13
13
  });
14
14
  const hasOptions = Boolean(options || hasOptionChildren);
15
15
  if (!hasOptions) {
@@ -68,7 +68,6 @@ const SelectCustomControlled = (props) => {
68
68
  return child;
69
69
  });
70
70
  };
71
- // eslint-disable-next-line react-hooks/refs
72
71
  const resolvedChildren = traverseOptionList(children);
73
72
  const handleKeyDown = (e) => {
74
73
  const key = e.key;
@@ -7,7 +7,6 @@ const TabsControlled = (props) => {
7
7
  const { children, value, onChange, onSilentChange, itemWidth, variant, name, disableSelectionAnimation, direction = "row", size = "medium", } = props;
8
8
  const id = useElementId();
9
9
  const elActiveRef = React.useRef(null);
10
- // eslint-disable-next-line react-hooks/refs
11
10
  const elPrevActiveRef = React.useRef(elActiveRef.current);
12
11
  const elScrollableRef = React.useRef(null);
13
12
  const [selection, setSelection] = React.useState({
@@ -89,7 +89,6 @@ const ToastContainer = (props) => {
89
89
  // Height + padding + borders
90
90
  height: status === "entered" ? `calc(${toastHeight}px + var(--rs-unit-x2) + 2px)` : 0,
91
91
  // Disable transition when height of the toast can change
92
- // eslint-disable-next-line react-hooks/refs
93
92
  transitionDuration: resizingRef.current ? "0s" : undefined,
94
93
  }, onTransitionEnd: handleTransitionEnd, onFocus: stopTimer, onBlur: startTimer, children: _jsx("span", { className: s.wrapper, children: _jsx(Toast, { ...toastProps, collapsed: index > 0 && !inspected, attributes: { ...toastProps.attributes, ref: wrapperRef } }) }) }));
95
94
  };
@@ -10,9 +10,7 @@ const Expandable = (props) => {
10
10
  const rootRef = React.useRef(null);
11
11
  const mountedRef = React.useRef(false);
12
12
  const [animatedHeight, setAnimatedHeight] = React.useState(active ? "auto" : null);
13
- const contentClassNames = classNames(s.root,
14
- // eslint-disable-next-line react-hooks/refs
15
- mountedRef.current && animatedHeight !== "auto" && s["--animated"]);
13
+ const contentClassNames = classNames(s.root, mountedRef.current && animatedHeight !== "auto" && s["--animated"]);
16
14
  const handleTransitionEnd = (e) => {
17
15
  if (e.propertyName !== "height")
18
16
  return;
@@ -18,7 +18,6 @@ const Portal = (props) => {
18
18
  const { children, targetRef } = props;
19
19
  const mountedToggle = useToggle();
20
20
  const rootRef = React.useRef(null);
21
- // eslint-disable-next-line react-hooks/refs
22
21
  const rootNode = rootRef.current?.getRootNode();
23
22
  const isShadowDom = rootNode instanceof ShadowRoot;
24
23
  const defaultTargetEl = isShadowDom ? rootNode : document.body;
@@ -31,7 +30,6 @@ const Portal = (props) => {
31
30
  */
32
31
  const portal = usePortalScope();
33
32
  const nextScopeRef = targetRef || portal.scopeRef;
34
- // eslint-disable-next-line react-hooks/refs
35
33
  const targetEl = nextScopeRef?.current || defaultTargetEl;
36
34
  useIsomorphicLayoutEffect(() => {
37
35
  mountedToggle.activate();
@@ -39,7 +37,6 @@ const Portal = (props) => {
39
37
  }, []);
40
38
  /* Preserve the current theme when rendered in body */
41
39
  return [
42
- // eslint-disable-next-line react-hooks/refs
43
40
  ReactDOM.createPortal(_jsx(Theme, { children: children }), targetEl),
44
41
  // Make sure this element doesn't affect components using :last-child when their children use portals
45
42
  !mountedToggle.active && _jsx("div", { ref: rootRef, className: s.root }, "root"),
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import type * as T from "./Actionable.types";
3
+ declare const Actionable: React.ForwardRefExoticComponent<T.Props & React.RefAttributes<T.Ref>>;
4
+ export default Actionable;
@@ -0,0 +1,73 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { forwardRef } from "react";
4
+ import * as keys from "../../constants/keys.js";
5
+ import { classNames } from "../../utilities/props.js";
6
+ const Actionable = forwardRef((props, ref) => {
7
+ const { children, render, href, onClick, type, disabled, as, stopPropagation, className, attributes, } = props;
8
+ const rootAttributes = { ...attributes };
9
+ const hasClickHandler = onClick || attributes?.onClick;
10
+ const hasFocusHandler = attributes?.onFocus || attributes?.onBlur;
11
+ const isLink = Boolean(href || attributes?.href);
12
+ // Including attributes ref for the cases when event listeners are added through it
13
+ // To make sure it doesn't render a span
14
+ const isButton = Boolean(hasClickHandler || hasFocusHandler || type || attributes?.ref);
15
+ const renderedAsButton = !isLink && isButton && (!as || as === "button");
16
+ // Using any here to let TS save on type resolving, otherwise TS throws an error due to the type complexity
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ let TagName;
19
+ if (isLink) {
20
+ TagName = "a";
21
+ rootAttributes.href = disabled ? undefined : href || attributes?.href;
22
+ }
23
+ else if (renderedAsButton) {
24
+ TagName = "button";
25
+ rootAttributes.type = type || attributes?.type || "button";
26
+ rootAttributes.disabled = disabled || attributes?.disabled;
27
+ }
28
+ else if (isButton) {
29
+ const isFocusable = as === "label";
30
+ const simulateButton = !isFocusable || hasClickHandler || hasFocusHandler;
31
+ TagName = as || "span";
32
+ rootAttributes.role = simulateButton ? "button" : undefined;
33
+ rootAttributes.tabIndex = simulateButton ? 0 : undefined;
34
+ }
35
+ else {
36
+ TagName = as || "span";
37
+ }
38
+ const handlePress = (event) => {
39
+ if (disabled)
40
+ return;
41
+ if (stopPropagation)
42
+ event.stopPropagation();
43
+ onClick?.(event);
44
+ attributes?.onClick?.(event);
45
+ };
46
+ const handleKeyDown = (event) => {
47
+ const isSpace = event.key === keys.SPACE;
48
+ const isEnter = event.key === keys.ENTER;
49
+ if (!isSpace && !isEnter)
50
+ return;
51
+ if (rootAttributes.role !== "button")
52
+ return;
53
+ if (stopPropagation)
54
+ event.stopPropagation();
55
+ event.preventDefault();
56
+ handlePress(event);
57
+ };
58
+ const tagAttributes = {
59
+ ref: ref,
60
+ // rootAttributes can receive ref from Flyout
61
+ ...rootAttributes,
62
+ className: classNames(className),
63
+ onClick: handlePress,
64
+ onKeyDown: handleKeyDown,
65
+ "aria-disabled": disabled ? true : undefined,
66
+ children: children,
67
+ };
68
+ if (render)
69
+ return render(tagAttributes);
70
+ return _jsx(TagName, { ...tagAttributes });
71
+ });
72
+ Actionable.displayName = "Actionable";
73
+ export default Actionable;
@@ -0,0 +1,34 @@
1
+ import type React from "react";
2
+ import type * as G from "../../types/global";
3
+ export type AttributesRef = React.RefObject<HTMLButtonElement | null>;
4
+ type Attributes = G.Attributes<"button"> & Omit<React.JSX.IntrinsicElements["a"], keyof G.Attributes<"button">> & {
5
+ ref?: AttributesRef;
6
+ };
7
+ export type RenderAttributes = G.Attributes<"a"> & {
8
+ ref: React.RefObject<HTMLAnchorElement | null>;
9
+ children: React.ReactNode;
10
+ };
11
+ export type Props = {
12
+ /** Node for inserting the content */
13
+ children?: React.ReactNode;
14
+ /** Render a custom root element, useful for integrating with routers */
15
+ render?: (attributes: RenderAttributes) => React.ReactNode;
16
+ /** Callback when clicked, renders it as a button tag if href is not provided */
17
+ onClick?: (e: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => void;
18
+ /** URL, renders it as an anchor tag */
19
+ href?: string;
20
+ /** Type attribute, renders it as a button tag */
21
+ type?: React.ButtonHTMLAttributes<HTMLButtonElement>["type"];
22
+ /** Disable from user interaction */
23
+ disabled?: boolean;
24
+ /** Prevent the event from bubbling up to the parent */
25
+ stopPropagation?: boolean;
26
+ /** Render as a different element */
27
+ as?: keyof React.JSX.IntrinsicElements;
28
+ /** Additional classname for the root element */
29
+ className?: G.ClassName;
30
+ /** Additional attributes for the root element */
31
+ attributes?: Attributes;
32
+ };
33
+ export type Ref = HTMLButtonElement | HTMLAnchorElement;
34
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { default } from "./Actionable";
2
+ export type { Props as ActionableProps, Ref as ActionableRef } from "./Actionable.types";
@@ -0,0 +1 @@
1
+ export { default } from "./Actionable.js";
@@ -12,7 +12,6 @@ const usePrevious = (value, clean = false) => {
12
12
  React.useEffect(() => {
13
13
  ref.current = clean ? copy(value) : value;
14
14
  }, [value, clean]);
15
- // eslint-disable-next-line react-hooks/refs
16
15
  return ref.current;
17
16
  };
18
17
  export default usePrevious;
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import * as keys from "../constants/keys.js";
2
3
  import useHandlerRef from "./useHandlerRef.js";
3
4
  const useOnClickOutside = (refs, handler, options) => {
4
5
  const { disabled } = options || {};
@@ -25,11 +26,18 @@ const useOnClickOutside = (refs, handler, options) => {
25
26
  }
26
27
  });
27
28
  };
29
+ const handleKeyDown = (event) => {
30
+ if (![keys.ENTER, keys.SPACE].includes(event.key))
31
+ return;
32
+ handleMouseDown(event);
33
+ };
28
34
  document.addEventListener("mousedown", handleMouseDown, { passive: true });
29
35
  document.addEventListener("touchstart", handleMouseDown, { passive: true });
36
+ document.addEventListener("keydown", handleKeyDown, { passive: true });
30
37
  return () => {
31
38
  document.removeEventListener("mousedown", handleMouseDown);
32
39
  document.removeEventListener("touchstart", handleMouseDown);
40
+ document.removeEventListener("keydown", handleKeyDown);
33
41
  };
34
42
  // eslint-disable-next-line react-hooks/exhaustive-deps
35
43
  }, [...refs]);
@@ -80,6 +80,10 @@ class TrapFocus {
80
80
  const el = shadowRoot ?? document;
81
81
  el.removeEventListener("keydown", this.#handleKeyDown);
82
82
  };
83
+ #isLast = () => {
84
+ const tailItem = _a.chain.tailId && _a.chain.get(_a.chain.tailId);
85
+ return tailItem && tailItem.data.#root === this.#root;
86
+ };
83
87
  /**
84
88
  * Trap the focus, add observer and keyboard event listeners
85
89
  * and create a chain item
@@ -98,6 +102,8 @@ class TrapFocus {
98
102
  this.#mutationObserver = new MutationObserver(() => {
99
103
  if (!this.#root)
100
104
  return;
105
+ if (!this.#isLast())
106
+ return;
101
107
  const currentActiveElement = getActiveElement(this.#root);
102
108
  // Focus stayed inside the wrapper, no need to refocus
103
109
  if (this.#root.contains(currentActiveElement))
@@ -117,10 +123,10 @@ class TrapFocus {
117
123
  this.#addListeners();
118
124
  if (mode === "dialog")
119
125
  this.#screenReaderTrap.trap();
120
- // Don't add back to the chain if we're traversing back
121
- const tailItem = _a.chain.tailId && _a.chain.get(_a.chain.tailId);
122
126
  const currentActiveElement = getActiveElement(this.#root);
123
- if (!tailItem || this.#root !== tailItem.data.#root) {
127
+ const isLastInChain = this.#isLast();
128
+ // Don't add back to the chain if we're traversing back
129
+ if (!isLastInChain) {
124
130
  this.#chainId = _a.chain.add(this);
125
131
  // If the focus was moved manually (e.g. with autoFocus) - keep it there
126
132
  if (!this.#root.contains(currentActiveElement)) {
@@ -1,4 +1,3 @@
1
- export { getRectFromCoordinates } from "./flyout";
2
1
  export { getShadowRoot } from "./shadowDom";
3
2
  export { findParent, findClosestScrollableContainer, findClosestPositionContainer } from "./find";
4
3
  export { triggerChangeEvent } from "./event";
@@ -1,4 +1,3 @@
1
- export { getRectFromCoordinates } from "./flyout.js";
2
1
  export { getShadowRoot } from "./shadowDom.js";
3
2
  export { findParent, findClosestScrollableContainer, findClosestPositionContainer } from "./find.js";
4
3
  export { triggerChangeEvent } from "./event.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reshaped",
3
3
  "description": "Professionally crafted design system in React & Figma for building products of any scale and complexity",
4
- "version": "3.9.0",
4
+ "version": "3.9.1-canary.3",
5
5
  "license": "MIT",
6
6
  "email": "hello@reshaped.so",
7
7
  "homepage": "https://reshaped.so",
@@ -90,10 +90,12 @@
90
90
  "cssnano": "7.1.1",
91
91
  "csstype": "3.1.3",
92
92
  "culori": "4.0.2",
93
- "postcss-custom-media": "11.0.6"
93
+ "postcss-custom-media": "11.0.6",
94
+ "@reshaped/utilities": "3.9.1-canary.3"
94
95
  },
95
96
  "scripts": {
96
97
  "clean": "sh ./bin/clean.sh",
98
+ "dev": "storybook dev -p 3001 -c ../../.storybook --disable-telemetry",
97
99
  "build": "pnpm clean && pnpm build:esm && pnpm build:css && pnpm build:bundle",
98
100
  "build:themes": "node bin/cli.js theming --config dist/cli/theming/reshaped.config.js --output src/themes",
99
101
  "build:esm": "tsc -p tsconfig.esm.json && resolve-tspaths -p tsconfig.esm.json",
@@ -1,31 +0,0 @@
1
- import type * as T from "../Flyout.types";
2
- /**
3
- * Calculate styles for the current position
4
- */
5
- declare const calculatePosition: (args: {
6
- triggerBounds: DOMRect;
7
- flyoutBounds: {
8
- width: number;
9
- height: number;
10
- };
11
- passedContainer?: HTMLElement | null;
12
- containerBounds: DOMRect;
13
- } & Pick<T.Options, "position" | "rtl" | "width" | "contentGap" | "contentShift" | "fallbackAdjustLayout" | "fallbackMinWidth" | "fallbackMinHeight">) => {
14
- position: T.Position;
15
- styles: {
16
- left: string | null;
17
- right: string | null;
18
- top: string | null;
19
- bottom: string | null;
20
- transform: string;
21
- height: string | null;
22
- width: string | null;
23
- };
24
- boundaries: {
25
- left: number;
26
- top: number;
27
- height: number;
28
- width: number;
29
- };
30
- };
31
- export default calculatePosition;
@@ -1,185 +0,0 @@
1
- import { SCREEN_OFFSET } from "./constants.js";
2
- import { getRTLPosition, centerBySize } from "./helpers.js";
3
- /**
4
- * Calculate styles for the current position
5
- */
6
- const calculatePosition = (args) => {
7
- const { triggerBounds, flyoutBounds, containerBounds, position: passedPosition, rtl, width: passedWidth, contentGap = 0, contentShift = 0, passedContainer, fallbackAdjustLayout,
8
- // fallbackMinWidth,
9
- fallbackMinHeight, } = args;
10
- const isFullWidth = passedWidth === "full" || passedWidth === "100%";
11
- let left = 0;
12
- let top = 0;
13
- let bottom = null;
14
- let right = null;
15
- let height = undefined;
16
- let width = undefined;
17
- let position = passedPosition;
18
- if (rtl)
19
- position = getRTLPosition(position);
20
- if (isFullWidth || width === "trigger") {
21
- position = position.includes("top") ? "top" : "bottom";
22
- }
23
- const isHorizontalPosition = !!position.match(/^(start|end)/);
24
- // contentGap adds padding to the flyout to make sure it doesn't disapper while moving the mouse to the content
25
- // So its width/height is bigger than the visible part of the content
26
- const flyoutWidth = flyoutBounds.width + (isHorizontalPosition ? contentGap : 0);
27
- const flyoutHeight = flyoutBounds.height + (!isHorizontalPosition ? contentGap : 0);
28
- const triggerWidth = triggerBounds.width;
29
- const triggerHeight = triggerBounds.height;
30
- // Detect passed container scroll to sync the flyout position with it
31
- const containerX = passedContainer?.scrollLeft;
32
- const containerY = passedContainer?.scrollTop;
33
- const scrollX = containerX ?? window.scrollX;
34
- const scrollY = containerY ?? window.scrollY;
35
- const renderContainerHeight = passedContainer?.clientHeight ?? window.innerHeight;
36
- const renderContainerWidth = passedContainer?.clientWidth ?? window.innerWidth;
37
- // When rendering in the body, bottom bounds will be larrger than the viewport so we calculate it manually
38
- const containerBoundsBottom = passedContainer
39
- ? containerBounds.bottom
40
- : window.innerHeight - scrollY;
41
- // When inside a container, adjut position based on the container scroll since flyout is rendered outside the scroll area
42
- const relativeLeft = triggerBounds.left - containerBounds.left + (containerX || 0);
43
- const relativeRight = containerBounds.right - triggerBounds.right - (containerX || 0);
44
- const relativeTop = triggerBounds.top - containerBounds.top + (containerY || 0);
45
- const relativeBottom = containerBoundsBottom - triggerBounds.bottom - (containerY || 0);
46
- switch (position) {
47
- case "start":
48
- case "start-top":
49
- case "start-bottom":
50
- left = relativeLeft - flyoutWidth;
51
- right = relativeRight + triggerWidth;
52
- break;
53
- case "end":
54
- case "end-top":
55
- case "end-bottom":
56
- left = relativeLeft + triggerWidth;
57
- break;
58
- case "bottom":
59
- case "top":
60
- left = relativeLeft + centerBySize(triggerWidth, flyoutWidth) + contentShift;
61
- break;
62
- case "top-start":
63
- case "bottom-start":
64
- left = relativeLeft + contentShift;
65
- break;
66
- case "top-end":
67
- case "bottom-end":
68
- left = relativeLeft + triggerWidth - flyoutWidth + contentShift;
69
- right = relativeRight - contentShift;
70
- break;
71
- default:
72
- break;
73
- }
74
- switch (position) {
75
- case "top":
76
- case "top-start":
77
- case "top-end":
78
- top = relativeTop - flyoutHeight;
79
- bottom = relativeBottom + triggerHeight;
80
- break;
81
- case "bottom":
82
- case "bottom-start":
83
- case "bottom-end":
84
- top = relativeTop + triggerHeight;
85
- break;
86
- case "start":
87
- case "end":
88
- top = relativeTop + centerBySize(triggerHeight, flyoutHeight) + contentShift;
89
- break;
90
- case "start-top":
91
- case "end-top":
92
- top = relativeTop + contentShift;
93
- break;
94
- case "start-bottom":
95
- case "end-bottom":
96
- top = relativeTop + triggerHeight - flyoutHeight + contentShift;
97
- bottom = relativeBottom - contentShift;
98
- break;
99
- default:
100
- break;
101
- }
102
- if (fallbackAdjustLayout) {
103
- const getOverflow = () => {
104
- return {
105
- top: -top + scrollY + SCREEN_OFFSET,
106
- bottom: top + flyoutHeight + SCREEN_OFFSET - scrollY - renderContainerHeight,
107
- left: -left + scrollX + SCREEN_OFFSET,
108
- right: left + flyoutWidth + SCREEN_OFFSET - scrollX - renderContainerWidth,
109
- };
110
- };
111
- const overflow = getOverflow();
112
- if (isHorizontalPosition) {
113
- if (overflow.top > 0) {
114
- top = SCREEN_OFFSET + scrollY;
115
- if (bottom !== null)
116
- bottom = bottom - overflow.top;
117
- }
118
- else if (overflow.bottom > 0) {
119
- top = top - overflow.bottom;
120
- }
121
- }
122
- else {
123
- if (overflow.left > 0) {
124
- left = SCREEN_OFFSET + scrollX;
125
- if (right !== null)
126
- right = right - overflow.left;
127
- }
128
- else if (overflow.right > 0) {
129
- left = left - overflow.right;
130
- }
131
- }
132
- const updatedOverflow = getOverflow();
133
- if (updatedOverflow.top > 0) {
134
- height = Math.max(fallbackMinHeight ? parseInt(fallbackMinHeight) : 0, flyoutHeight - updatedOverflow.top);
135
- top = top + (flyoutHeight - height);
136
- }
137
- else if (updatedOverflow.bottom > 0) {
138
- height = Math.max(fallbackMinHeight ? parseInt(fallbackMinHeight) : 0, flyoutHeight - updatedOverflow.bottom);
139
- if (bottom !== null)
140
- bottom = bottom + (flyoutHeight - height);
141
- }
142
- // TODO: Decide if we need horizontal scrolling for the fallbacks, might be a bad practice anyways
143
- // if (updatedOverflow.left > 0) {
144
- // width = Math.max(
145
- // fallbackMinWidth ? parseInt(fallbackMinWidth) : 0,
146
- // flyoutWidth - updatedOverflow.left
147
- // );
148
- // left = left + (flyoutWidth - width);
149
- // } else if (updatedOverflow.right > 0) {
150
- // width = Math.max(
151
- // fallbackMinWidth ? parseInt(fallbackMinWidth) : 0,
152
- // flyoutWidth - updatedOverflow.right
153
- // );
154
- // if (right !== null) right = right + (flyoutWidth - width);
155
- // }
156
- }
157
- if (isFullWidth) {
158
- left = SCREEN_OFFSET;
159
- width = window.innerWidth - SCREEN_OFFSET * 2;
160
- }
161
- else if (passedWidth === "trigger") {
162
- width = triggerBounds.width;
163
- }
164
- const translateX = right !== null ? -right : left;
165
- const translateY = bottom !== null ? -bottom : top;
166
- return {
167
- position,
168
- styles: {
169
- left: right === null ? "0px" : null,
170
- right: right === null ? null : "0px",
171
- top: bottom === null ? "0px" : null,
172
- bottom: bottom === null ? null : "0px",
173
- transform: `translate(${translateX}px, ${translateY}px)`,
174
- height: height !== undefined ? `${height}px` : null,
175
- width: width !== undefined ? `${width}px` : (passedWidth ?? null),
176
- },
177
- boundaries: {
178
- left,
179
- top,
180
- height: height ?? Math.ceil(flyoutHeight),
181
- width: width ?? Math.ceil(flyoutWidth),
182
- },
183
- };
184
- };
185
- export default calculatePosition;
@@ -1 +0,0 @@
1
- export declare const SCREEN_OFFSET = 8;
@@ -1 +0,0 @@
1
- export const SCREEN_OFFSET = 8;
@@ -1,11 +0,0 @@
1
- import type * as T from "../Flyout.types";
2
- import type * as G from "../../../types/global";
3
- /**
4
- * Set position of the target element to fit on the screen
5
- */
6
- declare const flyout: (args: T.Options & {
7
- flyoutEl: HTMLElement;
8
- triggerEl: HTMLElement | null;
9
- triggerBounds?: DOMRect | G.Coordinates | null;
10
- }) => T.FlyoutData | undefined;
11
- export default flyout;