@artsy/palette-mobile 13.2.2 → 13.2.4

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.
@@ -2,6 +2,8 @@
2
2
  import { ButtonProps } from "../Button";
3
3
  type FollowButtonProps = Omit<ButtonProps, "variant" | "size" | "longestText" | "icon" | "children"> & {
4
4
  isFollowed: boolean;
5
+ followCount?: number;
6
+ longestText?: string;
5
7
  };
6
- export declare const FollowButton: ({ isFollowed, ...restProps }: FollowButtonProps) => JSX.Element;
8
+ export declare const FollowButton: ({ isFollowed, followCount, longestText, ...restProps }: FollowButtonProps) => JSX.Element;
7
9
  export {};
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FollowButton = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const CheckIcon_1 = require("../../svgs/CheckIcon");
6
+ const formatLargeNumber_1 = require("../../utils/formatLargeNumber");
6
7
  const Button_1 = require("../Button");
7
- const FollowButton = ({ isFollowed, ...restProps }) => {
8
- return ((0, jsx_runtime_1.jsx)(Button_1.Button, { variant: isFollowed ? "outline" : "outlineGray", size: "small", longestText: "Following", icon: isFollowed && (0, jsx_runtime_1.jsx)(CheckIcon_1.CheckIcon, { fill: "black60", width: "16px", height: "16px" }), ...restProps, children: isFollowed ? "Following" : "Follow" }));
8
+ const Text_1 = require("../Text");
9
+ const FollowButton = ({ isFollowed, followCount, longestText, ...restProps }) => {
10
+ return ((0, jsx_runtime_1.jsxs)(Button_1.Button, { variant: isFollowed ? "outline" : "outlineGray", size: "small", longestText: longestText ? longestText : "Following", icon: isFollowed && (0, jsx_runtime_1.jsx)(CheckIcon_1.CheckIcon, { fill: "black60", width: "16px", height: "16px" }), ...restProps, children: [(0, jsx_runtime_1.jsx)(Text_1.Text, { variant: "xs", children: isFollowed ? "Following" : "Follow" }), !!followCount && followCount > 1 && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsx)(Text_1.Text, { variant: "xs", color: "black60", children: " " + (0, formatLargeNumber_1.formatLargeNumber)(followCount, 1) }) }))] }));
9
11
  };
10
12
  exports.FollowButton = FollowButton;
@@ -46,7 +46,7 @@ const VariantsDisabled = () => ((0, jsx_runtime_1.jsx)(helpers_1.DataList, { dat
46
46
  exports.VariantsDisabled = VariantsDisabled;
47
47
  const TheFollowButton = () => {
48
48
  const [follow, setFollow] = (0, react_1.useState)(true);
49
- return ((0, jsx_runtime_1.jsx)(helpers_1.List, { children: (0, jsx_runtime_1.jsx)(FollowButton_1.FollowButton, { isFollowed: follow, onPress: () => setFollow((v) => !v) }) }));
49
+ return ((0, jsx_runtime_1.jsxs)(helpers_1.List, { children: [(0, jsx_runtime_1.jsx)(FollowButton_1.FollowButton, { isFollowed: follow, onPress: () => setFollow((v) => !v) }), (0, jsx_runtime_1.jsx)(FollowButton_1.FollowButton, { followCount: 4, isFollowed: follow, onPress: () => setFollow((v) => !v) }), (0, jsx_runtime_1.jsx)(FollowButton_1.FollowButton, { followCount: 40, isFollowed: follow, onPress: () => setFollow((v) => !v) }), (0, jsx_runtime_1.jsx)(FollowButton_1.FollowButton, { followCount: 4000, isFollowed: follow, onPress: () => setFollow((v) => !v) }), (0, jsx_runtime_1.jsx)(FollowButton_1.FollowButton, { followCount: 400000, isFollowed: follow, onPress: () => setFollow((v) => !v) }), (0, jsx_runtime_1.jsx)(FollowButton_1.FollowButton, { followCount: 40000000, isFollowed: follow, onPress: () => setFollow((v) => !v) }), (0, jsx_runtime_1.jsx)(FollowButton_1.FollowButton, { followCount: 4000000000, isFollowed: follow, onPress: () => setFollow((v) => !v) })] }));
50
50
  };
51
51
  exports.TheFollowButton = TheFollowButton;
52
52
  const TheCTAButton = () => ((0, jsx_runtime_1.jsx)(helpers_1.List, { children: (0, jsx_runtime_1.jsx)(CTAButton_1.CTAButton, { onPress: () => console.log("pressed"), children: "cta button" }) }));
@@ -1,7 +1,9 @@
1
1
  /// <reference types="react" />
2
- import { ButtonProps } from "./Button";
2
+ import { ButtonProps } from "../Button";
3
3
  type FollowButtonProps = Omit<ButtonProps, "variant" | "size" | "longestText" | "icon" | "children"> & {
4
4
  isFollowed: boolean;
5
+ followCount?: number;
6
+ longestText?: string;
5
7
  };
6
- export declare const FollowButton: ({ isFollowed, ...restProps }: FollowButtonProps) => JSX.Element;
8
+ export declare const FollowButton: ({ isFollowed, followCount, longestText, ...restProps }: FollowButtonProps) => JSX.Element;
7
9
  export {};
@@ -2,9 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FollowButton = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
- const Button_1 = require("./Button");
6
- const svgs_1 = require("../../svgs");
7
- const FollowButton = ({ isFollowed, ...restProps }) => {
8
- return ((0, jsx_runtime_1.jsx)(Button_1.Button, { variant: isFollowed ? "outline" : "outlineGray", size: "small", longestText: "Following", icon: isFollowed && (0, jsx_runtime_1.jsx)(svgs_1.CheckIcon, { fill: "black60", width: "16px", height: "16px" }), ...restProps, children: isFollowed ? "Following" : "Follow" }));
5
+ const CheckIcon_1 = require("../../svgs/CheckIcon");
6
+ const formatLargeNumber_1 = require("../../utils/formatLargeNumber");
7
+ const Button_1 = require("../Button");
8
+ const Text_1 = require("../Text");
9
+ const FollowButton = ({ isFollowed, followCount, longestText, ...restProps }) => {
10
+ return ((0, jsx_runtime_1.jsxs)(Button_1.Button, { variant: isFollowed ? "outline" : "outlineGray", size: "small", longestText: longestText ? longestText : "Following", icon: isFollowed && (0, jsx_runtime_1.jsx)(CheckIcon_1.CheckIcon, { fill: "black60", width: "16px", height: "16px" }), ...restProps, children: [(0, jsx_runtime_1.jsx)(Text_1.Text, { variant: "xs", children: isFollowed ? "Following" : "Follow" }), !!followCount && followCount > 1 && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsx)(Text_1.Text, { variant: "xs", color: "black60", children: " " + (0, formatLargeNumber_1.formatLargeNumber)(followCount, 1) }) }))] }));
9
11
  };
10
12
  exports.FollowButton = FollowButton;
@@ -4,7 +4,7 @@ import { EventEmitter } from "events";
4
4
  import { TextInput, TextInputProps } from "react-native";
5
5
  export declare const inputEvents: EventEmitter;
6
6
  export declare const emitInputClearEvent: () => void;
7
- export interface InputProps extends TextInputProps {
7
+ export interface InputProps extends Omit<TextInputProps, "placeholder"> {
8
8
  addClearListener?: boolean;
9
9
  /**
10
10
  * We are applying some optimisations to make sure the UX is smooth
@@ -22,6 +22,27 @@ export interface InputProps extends TextInputProps {
22
22
  onHintPress?: () => void;
23
23
  onSelectTap?: () => void;
24
24
  optional?: boolean;
25
+ /**
26
+ * The placeholder can be an array of string, specifically for android, because of a bug.
27
+ * On ios, the longest string will always be picked, as ios can add ellipsis.
28
+ * On android, the longest string **that fits** will be picked, as android doesn't use ellipsis.
29
+ * The way to use it is to put the longest string first, and the shortest string last.
30
+ *
31
+ * Check `HACKS.md` for more info.
32
+ *
33
+ * @example
34
+ * const placeholders = [
35
+ * "Wow this is a great and very long placeholder",
36
+ * "Wow this is a great and long placeholder",
37
+ * "Wow this is a great placeholder",
38
+ * "Wow",
39
+ * ]
40
+ * ...
41
+ * <Input
42
+ * placeholder={placeholders}
43
+ * />
44
+ */
45
+ placeholder?: string | string[];
25
46
  required?: boolean;
26
47
  selectComponentWidth?: number;
27
48
  selectDisplayLabel?: string | undefined | null;
@@ -31,6 +31,8 @@ const jsx_runtime_1 = require("react/jsx-runtime");
31
31
  const events_1 = require("events");
32
32
  const palette_tokens_1 = require("@artsy/palette-tokens");
33
33
  const theme_get_1 = __importDefault(require("@styled-system/theme-get"));
34
+ const isArray_1 = __importDefault(require("lodash/isArray"));
35
+ const isString_1 = __importDefault(require("lodash/isString"));
34
36
  const react_1 = require("react");
35
37
  const react_native_1 = require("react-native");
36
38
  const react_native_reanimated_1 = __importStar(require("react-native-reanimated"));
@@ -62,6 +64,8 @@ exports.Input = (0, react_1.forwardRef)(({ addClearListener = false, defaultValu
62
64
  const [delayedFocused, setDelayedFocused] = (0, react_1.useState)(false);
63
65
  const [value, setValue] = (0, react_1.useState)(propValue ?? defaultValue);
64
66
  const [showPassword, setShowPassword] = (0, react_1.useState)(!secureTextEntry);
67
+ const [inputWidth, setInputWidth] = (0, react_1.useState)(0);
68
+ const placeholderWidths = (0, react_1.useRef)([]);
65
69
  const rightComponentRef = (0, react_1.useRef)(null);
66
70
  const inputRef = (0, react_1.useRef)();
67
71
  const variant = (0, helpers_1.getInputVariant)({
@@ -136,17 +140,19 @@ exports.Input = (0, react_1.forwardRef)(({ addClearListener = false, defaultValu
136
140
  }
137
141
  return leftComponentWidth;
138
142
  }, [hasLeftComponent, leftComponentWidth, onSelectTap, selectComponentWidth]);
139
- const styles = {
140
- fontFamily: fontFamily,
141
- fontSize: parseInt(palette_tokens_1.THEME.textVariants["sm-display"].fontSize, 10),
142
- minHeight: props.multiline ? exports.MULTILINE_INPUT_MIN_HEIGHT : exports.INPUT_MIN_HEIGHT,
143
- maxHeight: props.multiline ? exports.MULTILINE_INPUT_MAX_HEIGHT : undefined,
144
- height: props.multiline ? exports.MULTILINE_INPUT_MIN_HEIGHT : undefined,
145
- borderWidth: 1,
146
- paddingRight: rightComponentWidth + exports.HORIZONTAL_PADDING,
147
- paddingLeft: textInputPaddingLeft,
148
- ...styleProp,
149
- };
143
+ const styles = (0, react_1.useMemo)(() => {
144
+ return {
145
+ fontFamily: fontFamily,
146
+ fontSize: parseInt(palette_tokens_1.THEME.textVariants["sm-display"].fontSize, 10),
147
+ minHeight: props.multiline ? exports.MULTILINE_INPUT_MIN_HEIGHT : exports.INPUT_MIN_HEIGHT,
148
+ maxHeight: props.multiline ? exports.MULTILINE_INPUT_MAX_HEIGHT : undefined,
149
+ height: props.multiline ? exports.MULTILINE_INPUT_MIN_HEIGHT : undefined,
150
+ borderWidth: 1,
151
+ paddingRight: rightComponentWidth + exports.HORIZONTAL_PADDING,
152
+ paddingLeft: textInputPaddingLeft,
153
+ ...styleProp,
154
+ };
155
+ }, [fontFamily, styleProp, props.multiline, rightComponentWidth, textInputPaddingLeft]);
150
156
  const labelStyles = (0, react_1.useMemo)(() => {
151
157
  return {
152
158
  // this is neeeded too make sure the label is on top of the input
@@ -210,9 +216,16 @@ exports.Input = (0, react_1.forwardRef)(({ addClearListener = false, defaultValu
210
216
  }, children: [unit && ((0, jsx_runtime_1.jsx)(Text_1.Text, { color: disabled ? "black30" : "black60", variant: "sm-display", children: unit })), icon] }));
211
217
  }
212
218
  if (onSelectTap) {
213
- return ((0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: onSelectTap, style: { position: "absolute" }, hitSlop: { top: 10, right: 10, bottom: 10, left: 10 }, children: (0, jsx_runtime_1.jsxs)(AnimatedFlex, { style: [
219
+ return ((0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: onSelectTap, style: [
220
+ leftComponentSharedStyles,
221
+ {
222
+ width: selectComponentWidth,
223
+ },
224
+ ], hitSlop: { top: 10, right: 10, bottom: 10, left: 10 }, children: (0, jsx_runtime_1.jsxs)(AnimatedFlex, { style: [
214
225
  {
215
- ...leftComponentSharedStyles,
226
+ paddingHorizontal: exports.HORIZONTAL_PADDING,
227
+ height: exports.INPUT_MIN_HEIGHT,
228
+ alignItems: "center",
216
229
  width: selectComponentWidth,
217
230
  flexDirection: "row",
218
231
  borderRightWidth: 1,
@@ -291,26 +304,68 @@ exports.Input = (0, react_1.forwardRef)(({ addClearListener = false, defaultValu
291
304
  left: 10,
292
305
  }, children: (0, jsx_runtime_1.jsx)(Text_1.Text, { underline: true, variant: "xs", color: "black60", children: hintText }) }) }));
293
306
  }, [hintText, props.onHintPress, space]);
307
+ const getPlatformSpecificPlaceholder = (0, react_1.useCallback)(() => {
308
+ if (!placeholder) {
309
+ return "";
310
+ }
311
+ if (react_native_1.Platform.OS === "ios") {
312
+ return (0, isArray_1.default)(placeholder) ? placeholder[0] : placeholder;
313
+ }
314
+ // if it's android and we only have one string, return that string
315
+ if ((0, isString_1.default)(placeholder)) {
316
+ return placeholder;
317
+ }
318
+ // otherwise, find a placeholder that has longest width that fits in the inputtext
319
+ const longestFittingStringIndex = placeholderWidths.current.findIndex((placeholderWidth) => {
320
+ return placeholderWidth <= inputWidth;
321
+ });
322
+ if (longestFittingStringIndex > -1) {
323
+ return placeholder[longestFittingStringIndex];
324
+ }
325
+ // otherwise just return the shortest placeholder
326
+ return placeholder[placeholder.length - 1];
327
+ }, [inputWidth, placeholder]);
294
328
  const getPlaceholder = (0, react_1.useCallback)(() => {
295
329
  // Show placeholder always if there is no title
296
330
  // This is because we won't have a title animation
297
331
  if (!props.title) {
298
- return placeholder;
332
+ return getPlatformSpecificPlaceholder();
299
333
  }
300
334
  // On blur, we want to show the placeholder immediately
301
335
  if (delayedFocused) {
302
- return placeholder;
336
+ return getPlatformSpecificPlaceholder();
303
337
  }
304
338
  // On focus, we want to show the placeholder after the title animation has finished
305
339
  return "";
306
- }, [delayedFocused, props.title, placeholder]);
340
+ }, [delayedFocused, getPlatformSpecificPlaceholder, props.title]);
307
341
  const renderAnimatedTitle = (0, react_1.useCallback)(() => {
308
342
  if (!props.title) {
309
343
  return null;
310
344
  }
311
345
  return ((0, jsx_runtime_1.jsx)(Flex_1.Flex, { flexDirection: "row", zIndex: 100, pointerEvents: "none", height: exports.LABEL_HEIGHT, children: (0, jsx_runtime_1.jsxs)(AnimatedText, { style: [labelStyles, labelAnimatedStyles], numberOfLines: 1, children: [" ", props.title, " "] }) }));
312
346
  }, [labelStyles, labelAnimatedStyles, props.title]);
313
- return ((0, jsx_runtime_1.jsxs)(Flex_1.Flex, { flexGrow: 1, children: [renderHint(), renderAnimatedTitle(), (0, jsx_runtime_1.jsx)(AnimatedStyledInput, { value: value, onChangeText: handleChangeText, style: [styles, textInputAnimatedStyles], onFocus: handleFocus, onBlur: handleBlur, scrollEnabled: false, editable: !disabled, textAlignVertical: props.multiline ? "top" : "center", ref: inputRef, placeholderTextColor: color("black60"), placeholder: getPlaceholder(), secureTextEntry: !showPassword, ...props }), renderRightComponent(), renderLeftComponent(), renderBottomComponent()] }));
347
+ const renderAndroidPlaceholderMeasuringHack = (0, react_1.useCallback)(() => {
348
+ if (react_native_1.Platform.OS === "ios" || !(0, isArray_1.default)(placeholder)) {
349
+ return null;
350
+ }
351
+ // Do not render the hack if we have already measured the placeholder
352
+ if (placeholderWidths.current.length > 0) {
353
+ return null;
354
+ }
355
+ return ((0, jsx_runtime_1.jsx)(Flex_1.Flex, { style: {
356
+ position: "absolute",
357
+ top: -10000,
358
+ width: 10000,
359
+ alignItems: "baseline", // this is to make Texts get the smallest width they can get to fit the text
360
+ }, children: placeholder.map((placeholderString, index) => ((0, jsx_runtime_1.jsx)(Text_1.Text, { onLayout: (event) => {
361
+ placeholderWidths.current[index] = event.nativeEvent.layout.width;
362
+ }, numberOfLines: 1, style: {
363
+ ...styles,
364
+ }, children: placeholderString }))) }));
365
+ }, [placeholder, styles]);
366
+ return ((0, jsx_runtime_1.jsxs)(Flex_1.Flex, { flexGrow: 1, children: [renderAndroidPlaceholderMeasuringHack(), renderHint(), renderAnimatedTitle(), (0, jsx_runtime_1.jsx)(AnimatedStyledInput, { value: value, onChangeText: handleChangeText, style: [styles, textInputAnimatedStyles], onFocus: handleFocus, onBlur: handleBlur, onLayout: (event) => {
367
+ setInputWidth(event.nativeEvent.layout.width);
368
+ }, scrollEnabled: false, editable: !disabled, textAlignVertical: props.multiline ? "top" : "center", ref: inputRef, placeholderTextColor: color("black60"), placeholder: getPlaceholder(), secureTextEntry: !showPassword, ...props }), renderRightComponent(), renderLeftComponent(), renderBottomComponent()] }));
314
369
  });
315
370
  const StyledInput = (0, styled_components_1.default)(react_native_1.TextInput) `
316
371
  padding: ${exports.HORIZONTAL_PADDING}px;
@@ -0,0 +1 @@
1
+ export declare function formatLargeNumber(number: number, decimalPlaces?: number): string;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatLargeNumber = void 0;
4
+ function formatLargeNumber(number, decimalPlaces = 0) {
5
+ if (number < 1000) {
6
+ return number.toString();
7
+ }
8
+ else if (number < 1000000) {
9
+ return `${(number / 1000).toFixed(decimalPlaces)}K`;
10
+ }
11
+ else if (number < 1000000000) {
12
+ return `${(number / 1000000).toFixed(decimalPlaces)}M`;
13
+ }
14
+ else {
15
+ return `${(number / 1000000000).toFixed(decimalPlaces)}B`;
16
+ }
17
+ }
18
+ exports.formatLargeNumber = formatLargeNumber;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const formatLargeNumber_1 = require("./formatLargeNumber");
4
+ describe("formatLargeNumber", () => {
5
+ it("should return the same number for numbers less than 1000", () => {
6
+ expect((0, formatLargeNumber_1.formatLargeNumber)(500)).toBe("500");
7
+ });
8
+ it("should format numbers in thousands correctly", () => {
9
+ expect((0, formatLargeNumber_1.formatLargeNumber)(1500, 1)).toBe("1.5K");
10
+ expect((0, formatLargeNumber_1.formatLargeNumber)(1500, 2)).toBe("1.50K");
11
+ });
12
+ it("should format numbers in millions correctly", () => {
13
+ expect((0, formatLargeNumber_1.formatLargeNumber)(1500000, 1)).toBe("1.5M");
14
+ expect((0, formatLargeNumber_1.formatLargeNumber)(1500000, 2)).toBe("1.50M");
15
+ });
16
+ it("should format numbers in billions correctly", () => {
17
+ expect((0, formatLargeNumber_1.formatLargeNumber)(1500000000, 1)).toBe("1.5B");
18
+ expect((0, formatLargeNumber_1.formatLargeNumber)(1500000000, 2)).toBe("1.50B");
19
+ });
20
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artsy/palette-mobile",
3
- "version": "13.2.2",
3
+ "version": "13.2.4",
4
4
  "description": "Artsy's design system for React Native",
5
5
  "scripts": {
6
6
  "android": "RCT_METRO_PORT=8082 react-native run-android --port 8082",