@artsy/palette-mobile 13.2.2 → 13.2.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.
@@ -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;
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.3",
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",