@swan-io/lake 15.5.0 → 15.5.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swan-io/lake",
3
- "version": "15.5.0",
3
+ "version": "15.5.2",
4
4
  "engines": {
5
5
  "node": ">22.12.0"
6
6
  },
@@ -7,7 +7,8 @@ type Props = {
7
7
  step: number;
8
8
  disabled?: boolean;
9
9
  style?: ViewProps["style"];
10
+ labelledBy?: string;
10
11
  onChange: (value: number) => void;
11
12
  };
12
- export declare const LakeSlider: ({ id, min, max, value, step, disabled, style, onChange, }: Props) => import("react").JSX.Element;
13
+ export declare const LakeSlider: ({ id, min, max, value, step, disabled, style, labelledBy, onChange, }: Props) => import("react").JSX.Element;
13
14
  export {};
@@ -8,7 +8,7 @@ const styles = StyleSheet.create({
8
8
  zIndex: 1,
9
9
  },
10
10
  });
11
- export const LakeSlider = ({ id, min, max, value, step, disabled = false, style, onChange, }) => (_jsx(View, { style: [styles.container, style], children: _jsx("input", { id: id, type: "range", min: min, max: max, value: value, step: step, disabled: disabled, onChange: useCallback((event) => onChange(Number(event.target.value)), [onChange]), style: {
11
+ export const LakeSlider = ({ id, min, max, value, step, disabled = false, style, labelledBy, onChange, }) => (_jsx(View, { style: [styles.container, style], children: _jsx("input", { id: id, "aria-labelledby": labelledBy, type: "range", min: min, max: max, value: value, step: step, disabled: disabled, onChange: useCallback((event) => onChange(Number(event.target.value)), [onChange]), style: {
12
12
  backgroundSize: `${((value - min) / (max - min)) * 100}% 100%`,
13
13
  ...(disabled ? commonStyles.disabled : {}),
14
14
  } }) }));
@@ -18,4 +18,4 @@ export type LakeTagInputProps = Merge<TextInputProps, {
18
18
  onValuesChanged: (values: string[]) => void;
19
19
  placeholder?: string;
20
20
  }>;
21
- export declare const LakeTagInput: ({ ref, validator, onFocus: originalOnFocus, onBlur: originalOnBlur, validateOnBlur, values, onValuesChanged, readOnly, disabled, valid, hideErrors, placeholder, help, error, }: LakeTagInputProps) => import("react").JSX.Element;
21
+ export declare const LakeTagInput: ({ ref, id, validator, onFocus: originalOnFocus, onBlur: originalOnBlur, validateOnBlur, values, onValuesChanged, readOnly, disabled, valid, hideErrors, placeholder, help, error, }: LakeTagInputProps) => import("react").JSX.Element;
@@ -77,7 +77,7 @@ const styles = StyleSheet.create({
77
77
  },
78
78
  });
79
79
  const SEPARATORS_REGEX = /,| /;
80
- export const LakeTagInput = ({ ref, validator = () => true, onFocus: originalOnFocus, onBlur: originalOnBlur, validateOnBlur = true, values, onValuesChanged, readOnly = false, disabled = false, valid = false, hideErrors = false, placeholder, help, error, }) => {
80
+ export const LakeTagInput = ({ ref, id, validator = () => true, onFocus: originalOnFocus, onBlur: originalOnBlur, validateOnBlur = true, values, onValuesChanged, readOnly = false, disabled = false, valid = false, hideErrors = false, placeholder, help, error, }) => {
81
81
  const inputRef = useRef(null);
82
82
  const containerRef = useRef(null);
83
83
  const [isFocused, setIsFocused] = useState(false);
@@ -152,5 +152,5 @@ export const LakeTagInput = ({ ref, validator = () => true, onFocus: originalOnF
152
152
  isHovered && styles.hovered,
153
153
  ], "aria-errormessage": error, onPress: autoFocus, ref: containerRef, children: [values.map((value, i) => (_jsx(Tag, { onPressRemove: !readOnly && !disabled
154
154
  ? () => onValuesChanged(values.filter(current => current !== value))
155
- : undefined, style: styles.tag, color: validator(value) ? "gray" : "negative", children: value }, i))), _jsx(TextInput, { ref: mergedRef, style: [styles.input, disabled && styles.disabled], onFocus: onFocus, onBlur: onBlur, "aria-disabled": disabled, onChangeText: onTextInputChange, onKeyPress: onTextInputKeyPress, readOnly: readOnly, placeholder: placeholder })] }), !hideErrors && (_jsx(Box, { direction: "row", style: styles.errorContainer, children: isNotNullish(error) ? (_jsx(LakeText, { variant: "smallRegular", color: colors.negative[500], children: error })) : (_jsx(LakeText, { variant: "smallRegular", color: colors.gray[500], children: help !== null && help !== void 0 ? help : " " })) }))] }));
155
+ : undefined, style: styles.tag, color: validator(value) ? "gray" : "negative", children: value }, i))), _jsx(TextInput, { ref: mergedRef, id: id, style: [styles.input, disabled && styles.disabled], onFocus: onFocus, onBlur: onBlur, "aria-disabled": disabled, onChangeText: onTextInputChange, onKeyPress: onTextInputKeyPress, readOnly: readOnly, placeholder: placeholder })] }), !hideErrors && (_jsx(Box, { direction: "row", style: styles.errorContainer, children: isNotNullish(error) ? (_jsx(LakeText, { variant: "smallRegular", color: colors.negative[500], children: error })) : (_jsx(LakeText, { variant: "smallRegular", color: colors.gray[500], children: help !== null && help !== void 0 ? help : " " })) }))] }));
156
156
  };
@@ -1,7 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { memo, useEffect, useImperativeHandle, useRef, useState } from "react";
3
3
  import { StyleSheet, View } from "react-native";
4
- import { match } from "ts-pattern";
4
+ import { match, P } from "ts-pattern";
5
5
  import { colors, shadows, spacings } from "../constants/design";
6
6
  import { useContextualLayer } from "../hooks/useContextualLayer";
7
7
  import { useHover } from "../hooks/useHover";
@@ -57,6 +57,7 @@ const Tooltip = memo(({ ref, children, content, describedBy, matchReferenceWidth
57
57
  placement,
58
58
  visible,
59
59
  matchReferenceWidth,
60
+ withArrow: !hideArrow,
60
61
  });
61
62
  const rootElement = getRootElement(referenceRef.current);
62
63
  useImperativeHandle(ref, () => ({
@@ -113,14 +114,17 @@ const Tooltip = memo(({ ref, children, content, describedBy, matchReferenceWidth
113
114
  useEffect(() => {
114
115
  return () => clearTimeout(timeoutRef.current);
115
116
  }, []);
116
- return (_jsxs(_Fragment, { children: [_jsx(View, { ref: referenceRef, style: containerStyle, children: children }), isNotNullish(rootElement) && position.isSome() && !disabled && (_jsx(Portal, { container: rootElement, children: _jsx(View, { style: [position.get().rootStyle, styles.root], children: _jsxs(View, { ref: contentRef, role: "tooltip", "aria-describedby": describedBy, style: [styles.base, position.get().style], children: [position.get().verticalPosition === "bottom" ? (_jsx(Box, { direction: "row", justifyContent: match(position.get().horizontalPosition)
117
- .with("left", () => "start")
118
- .with("center", () => "center")
119
- .with("right", () => "end")
120
- .exhaustive(), style: [styles.arrowBar, styles.arrowBarTop], children: _jsx(Svg, { width: 16, height: 8, viewBox: "0 0 16 8", children: _jsx(Polygon, { points: "8 0 16 8 0 8", fill: hideArrow ? "transparent" : colors.gray[900] }) }) })) : null, _jsx(View, { style: [styles.content, { width, maxWidth }], children: typeof content === "string" || typeof content === "number" ? (_jsx(LakeText, { align: "center", color: colors.gray.contrast, children: content })) : (content) }), position.get().verticalPosition === "top" ? (_jsx(Box, { direction: "row", justifyContent: match(position.get().horizontalPosition)
121
- .with("left", () => "start")
122
- .with("center", () => "center")
123
- .with("right", () => "end")
124
- .exhaustive(), style: [styles.arrowBar, styles.arrowBarBottom], children: _jsx(Svg, { width: 16, height: 8, viewBox: "0 0 16 8", children: _jsx(Polygon, { points: "8 8 16 0 0 0", fill: hideArrow ? "transparent" : colors.gray[900] }) }) })) : null] }) }) }))] }));
117
+ return (_jsxs(_Fragment, { children: [_jsx(View, { ref: referenceRef, style: containerStyle, children: children }), isNotNullish(rootElement) && position.isSome() && !disabled && (_jsx(Portal, { container: rootElement, children: _jsx(View, { style: [position.get().rootStyle, styles.root], children: _jsxs(View, { ref: contentRef, role: "tooltip", "aria-describedby": describedBy, style: [styles.base, position.get().style], children: [position.get().verticalPosition === "bottom" ? (_jsx(TooltipArrow, { verticalPosition: "bottom", position: position.get().arrowPosition, hideArrow: hideArrow })) : null, _jsx(View, { style: [styles.content, { width, maxWidth }], children: typeof content === "string" || typeof content === "number" ? (_jsx(LakeText, { align: "center", color: colors.gray.contrast, children: content })) : (content) }), position.get().verticalPosition === "top" ? (_jsx(TooltipArrow, { verticalPosition: "top", position: position.get().arrowPosition, hideArrow: hideArrow })) : null] }) }) }))] }));
125
118
  });
119
+ const TooltipArrow = ({ verticalPosition, position, hideArrow }) => {
120
+ const { polygonPoints, top, bottom } = verticalPosition === "top"
121
+ ? { polygonPoints: "8 8 16 0 0 0", top: undefined, bottom: 1 }
122
+ : { polygonPoints: "8 0 16 8 0 8", top: 1, bottom: undefined };
123
+ const { justifyContent, left } = match(position)
124
+ .with("left", () => ({ justifyContent: "start", left: undefined }))
125
+ .with("right", () => ({ justifyContent: "end", left: undefined }))
126
+ .with(P.number, value => ({ justifyContent: "center", left: value }))
127
+ .exhaustive();
128
+ return (_jsx(Box, { direction: "row", justifyContent: justifyContent, style: [styles.arrowBar, styles.arrowBarBottom, { top, bottom, left }], children: _jsx(Svg, { width: 16, height: 8, viewBox: "0 0 16 8", children: _jsx(Polygon, { points: polygonPoints, fill: hideArrow ? "transparent" : colors.gray[900] }) }) }));
129
+ };
126
130
  export const InformationTooltip = ({ ref, text, placement = "center", icon = "info-regular", iconSize = 24, }) => (_jsx(LakeTooltip, { ref: ref, describedBy: "copy", placement: placement, togglableOnFocus: true, width: 300, content: text, children: _jsx(View, { style: styles.info, children: _jsx(Icon, { name: icon, size: iconSize, color: colors.gray[900] }) }) }));
@@ -24,17 +24,19 @@ type Config = {
24
24
  matchReferenceWidth?: boolean;
25
25
  matchReferenceMinWidth?: boolean;
26
26
  referenceRef?: RefObject<View | Text | null>;
27
+ withArrow?: boolean;
27
28
  };
28
29
  export type ContextualLayerPosition = {
29
30
  rootStyle: ViewStyle;
30
31
  style: ViewStyle;
31
32
  verticalPosition: "top" | "bottom";
32
33
  horizontalPosition: "left" | "center" | "right";
34
+ arrowPosition: "left" | "right" | number;
33
35
  };
34
36
  type ContextualLayerConfig = {
35
37
  referenceRef: RefObject<View | Text | null>;
36
38
  contentRef: RefObject<View | Text | null>;
37
39
  position: Option<ContextualLayerPosition>;
38
40
  };
39
- export declare const useContextualLayer: ({ placement, verticalPlacement, visible, matchReferenceWidth, matchReferenceMinWidth, referenceRef: externalReferenceRef, }: Config) => ContextualLayerConfig;
41
+ export declare const useContextualLayer: ({ placement, verticalPlacement, visible, matchReferenceWidth, matchReferenceMinWidth, withArrow, referenceRef: externalReferenceRef, }: Config) => ContextualLayerConfig;
40
42
  export {};
@@ -1,8 +1,21 @@
1
1
  import { Option } from "@swan-io/boxed";
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
- import { match } from "ts-pattern";
3
+ import { match, P } from "ts-pattern";
4
+ const MAX_OFFSET_FOR_CENTER_PLACEMENT = 100;
4
5
  const HORIZONTAL_SAFETY_MARGIN = 16;
5
- export const useContextualLayer = ({ placement = "left", verticalPlacement, visible, matchReferenceWidth = false, matchReferenceMinWidth = false, referenceRef: externalReferenceRef, }) => {
6
+ const getArrowPosition = (placement, referenceWidth, contentWidth) => {
7
+ if (placement === "center") {
8
+ return 0;
9
+ }
10
+ if (contentWidth <= referenceWidth) {
11
+ return placement;
12
+ }
13
+ const maxOffset = contentWidth / 2 - 12; // avoid arrow to be too close to the edge of the tooltip
14
+ const offset = (contentWidth - referenceWidth) / 2;
15
+ const arrowOffset = Math.min(offset, maxOffset);
16
+ return placement === "left" ? -arrowOffset : arrowOffset;
17
+ };
18
+ export const useContextualLayer = ({ placement, verticalPlacement, visible, matchReferenceWidth = false, matchReferenceMinWidth = false, withArrow = false, referenceRef: externalReferenceRef, }) => {
6
19
  const referenceRef = useRef(null);
7
20
  const contentRef = useRef(null);
8
21
  const usedRef = externalReferenceRef !== null && externalReferenceRef !== void 0 ? externalReferenceRef : referenceRef;
@@ -25,14 +38,13 @@ export const useContextualLayer = ({ placement = "left", verticalPlacement, visi
25
38
  // if contentRef isn't used in the component, we don't infer the placement depending on content size
26
39
  const contentRect = contentElement != null ? contentElement.getBoundingClientRect() : { width: 0, height: 0 };
27
40
  const contentWidth = contentRect.width;
28
- const contentMidWidth = contentWidth / 2;
29
- const isCenteredEnough = availableSpaceBefore > contentMidWidth && availableSpaceAfter > contentMidWidth;
41
+ const isCenteredEnough = Math.abs(availableSpaceBefore - availableSpaceAfter) < MAX_OFFSET_FOR_CENTER_PLACEMENT;
30
42
  const bestPlacement = availableSpaceBefore > availableSpaceAfter ? "right" : "left";
31
43
  const inferedPlacement = match(placement)
32
44
  .returnType()
33
- .with("center", () => (isCenteredEnough ? "center" : bestPlacement))
34
- .with("left", () => (availableSpaceBefore > contentWidth ? "left" : bestPlacement))
35
- .with("right", () => (availableSpaceAfter > contentWidth ? "right" : bestPlacement))
45
+ .with("center", P.nullish, () => (isCenteredEnough ? "center" : bestPlacement))
46
+ .with("left", () => (availableSpaceAfter > contentWidth ? "left" : bestPlacement))
47
+ .with("right", () => (availableSpaceBefore > contentWidth ? "right" : bestPlacement))
36
48
  .exhaustive();
37
49
  const openAbove = verticalPlacement === "above"
38
50
  ? true
@@ -53,6 +65,7 @@ export const useContextualLayer = ({ placement = "left", verticalPlacement, visi
53
65
  .with("right", () => ({ right: window.innerWidth - rect.right }))
54
66
  .with("center", () => ({ left: rect.left + width / 2, transform: "translateX(-50%)" }))
55
67
  .exhaustive();
68
+ const arrowPosition = withArrow ? getArrowPosition(inferedPlacement, width, contentWidth) : 0;
56
69
  const maxHeight = openAbove ? availableSpaceAbove : window.innerHeight - rect.top - height;
57
70
  const rootStyle = {
58
71
  position: "absolute",
@@ -80,10 +93,18 @@ export const useContextualLayer = ({ placement = "left", verticalPlacement, visi
80
93
  return Option.Some({
81
94
  rootStyle,
82
95
  horizontalPosition: inferedPlacement,
96
+ arrowPosition,
83
97
  verticalPosition: openAbove ? "top" : "bottom",
84
98
  style,
85
99
  });
86
- }, [placement, verticalPlacement, matchReferenceWidth, matchReferenceMinWidth, usedRef]);
100
+ }, [
101
+ placement,
102
+ verticalPlacement,
103
+ matchReferenceWidth,
104
+ matchReferenceMinWidth,
105
+ usedRef,
106
+ withArrow,
107
+ ]);
87
108
  useEffect(() => {
88
109
  if (visible) {
89
110
  setPosition(getPosition());