react-native-ui-lib 8.3.4-snapshot.7814 → 8.3.4-snapshot.7824

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": "react-native-ui-lib",
3
- "version": "8.3.4-snapshot.7814",
3
+ "version": "8.3.4-snapshot.7824",
4
4
  "main": "src/index.js",
5
5
  "types": "src/index.d.ts",
6
6
  "author": "Ethan Sharabi <ethan.shar@gmail.com>",
@@ -1,12 +1,8 @@
1
1
  {
2
2
  "name": "FloatingButton",
3
3
  "category": "overlays",
4
- "description": "Hovering button with gradient background",
5
- "modifiers": [
6
- "margin",
7
- "background",
8
- "color"
9
- ],
4
+ "description": "Hovering button with gradient background, backed by ScreenFooter",
5
+ "modifiers": ["margin", "background", "color"],
10
6
  "example": "https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/FloatingButtonScreen.tsx",
11
7
  "images": [
12
8
  "https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/FloatingButton/FloatingButton.gif?raw=true"
@@ -58,6 +54,12 @@
58
54
  "type": "boolean",
59
55
  "description": "Whether to show background overlay"
60
56
  },
57
+ {
58
+ "name": "hoisted",
59
+ "type": "boolean",
60
+ "description": "Whether the footer should be hoisted above the keyboard. When true, uses KeyboardAccessoryView for keyboard-aware positioning. When false, uses sticky positioning.",
61
+ "default": "true"
62
+ },
61
63
  {
62
64
  "name": "testID",
63
65
  "type": "string",
@@ -96,11 +98,7 @@
96
98
  },
97
99
  {
98
100
  "type": "table",
99
- "columns": [
100
- "Property",
101
- "Default",
102
- "Full Width"
103
- ],
101
+ "columns": ["Property", "Default", "Full Width"],
104
102
  "items": [
105
103
  {
106
104
  "title": "Main button",
@@ -130,10 +128,7 @@
130
128
  },
131
129
  {
132
130
  "type": "table",
133
- "columns": [
134
- "Layout",
135
- "Component"
136
- ],
131
+ "columns": ["Layout", "Component"],
137
132
  "items": [
138
133
  {
139
134
  "title": "Horizontal",
@@ -1,5 +1,4 @@
1
- import React, { PropsWithChildren, PureComponent } from 'react';
2
- import { Animated } from 'react-native';
1
+ import React, { PropsWithChildren } from 'react';
3
2
  import { ButtonProps } from '../button';
4
3
  export declare enum FloatingButtonLayouts {
5
4
  VERTICAL = "Vertical",
@@ -23,7 +22,7 @@ export interface FloatingButtonProps {
23
22
  */
24
23
  bottomMargin?: number;
25
24
  /**
26
- * Whether the buttons get the container's full with (vertical layout only)
25
+ * Whether the buttons get the container's full width (vertical layout only)
27
26
  */
28
27
  fullWidth?: boolean;
29
28
  /**
@@ -42,6 +41,12 @@ export interface FloatingButtonProps {
42
41
  * Whether to show background overlay
43
42
  */
44
43
  hideBackgroundOverlay?: boolean;
44
+ /**
45
+ * Whether the footer should be hoisted above the keyboard.
46
+ * When true (default), uses KeyboardAccessoryView for keyboard-aware positioning.
47
+ * When false, uses sticky positioning.
48
+ */
49
+ hoisted?: boolean;
45
50
  /**
46
51
  * Used as testing identifier
47
52
  * <TestID> - the floatingButton container
@@ -50,40 +55,9 @@ export interface FloatingButtonProps {
50
55
  */
51
56
  testID?: string;
52
57
  }
53
- /**
54
- * @description: Hovering button with gradient background
55
- * @modifiers: margin, background, color
56
- * @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/FloatingButtonScreen.tsx
57
- * @gif: https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/FloatingButton/FloatingButton.gif?raw=true
58
- */
59
- declare class FloatingButton extends PureComponent<FloatingButtonProps> {
60
- static displayName: string;
61
- static floatingButtonLayouts: typeof FloatingButtonLayouts;
62
- static defaultProps: {
63
- duration: number;
64
- buttonLayout: FloatingButtonLayouts;
65
- };
66
- initialVisibility?: boolean;
67
- firstLoad: boolean;
68
- visibleAnimated: Animated.Value;
69
- constructor(props: FloatingButtonProps);
70
- componentDidUpdate(prevProps: FloatingButtonProps): void;
71
- getAnimatedStyle: () => {
72
- opacity: Animated.Value;
73
- transform: {
74
- translateY: Animated.AnimatedInterpolation<string | number>;
75
- }[];
76
- };
77
- get isSecondaryOnly(): boolean;
78
- get isHorizontalLayout(): boolean;
79
- get isSecondaryHorizontal(): boolean | undefined;
80
- get isSecondaryVertical(): boolean | undefined;
81
- renderButton(): React.JSX.Element | undefined;
82
- renderOverlay: () => React.JSX.Element | undefined;
83
- renderSecondaryButton(): React.JSX.Element | undefined;
84
- renderHorizontalLayout(): React.JSX.Element;
85
- renderVerticalLayout(): React.JSX.Element;
86
- render(): false | React.JSX.Element | undefined;
87
- }
88
- declare const _default: React.ForwardRefExoticComponent<FloatingButtonProps & React.RefAttributes<any>> & typeof FloatingButton;
58
+ declare const _default: React.ForwardRefExoticComponent<FloatingButtonProps & React.RefAttributes<any>> & {
59
+ (props: FloatingButtonProps): React.JSX.Element | null;
60
+ displayName: string;
61
+ floatingButtonLayouts: typeof FloatingButtonLayouts;
62
+ };
89
63
  export default _default;
@@ -1,190 +1,80 @@
1
- import React, { PureComponent } from 'react';
2
- import { StyleSheet, Animated } from 'react-native';
3
- import { Constants, asBaseComponent } from "../../commons/new";
4
- import { Colors, Shadows, Spacings } from "../../style";
5
- import View from "../view";
6
- import Image from "../image";
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import { asBaseComponent } from "../../commons/new";
4
+ import { LogService } from "../../services";
5
+ import { Colors, Shadows } from "../../style";
7
6
  import Button from "../button";
7
+ import ScreenFooter, { ScreenFooterLayouts, ScreenFooterBackgrounds, KeyboardBehavior, ItemsFit } from "../screenFooter";
8
8
  export let FloatingButtonLayouts = /*#__PURE__*/function (FloatingButtonLayouts) {
9
9
  FloatingButtonLayouts["VERTICAL"] = "Vertical";
10
10
  FloatingButtonLayouts["HORIZONTAL"] = "Horizontal";
11
11
  return FloatingButtonLayouts;
12
12
  }({});
13
- const gradientImage = () => require("./gradient.png");
14
-
15
13
  /**
16
- * @description: Hovering button with gradient background
14
+ * @description: Hovering button with gradient background, backed by ScreenFooter
17
15
  * @modifiers: margin, background, color
18
16
  * @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/FloatingButtonScreen.tsx
19
17
  * @gif: https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/FloatingButton/FloatingButton.gif?raw=true
20
18
  */
21
- class FloatingButton extends PureComponent {
22
- static displayName = 'FloatingButton';
23
- static floatingButtonLayouts = FloatingButtonLayouts;
24
- static defaultProps = {
25
- duration: 300,
26
- buttonLayout: FloatingButtonLayouts.VERTICAL
27
- };
28
- constructor(props) {
29
- super(props);
30
- this.initialVisibility = props.visible;
31
- this.firstLoad = true;
32
- this.visibleAnimated = new Animated.Value(Number(!!props.visible));
33
- }
34
- componentDidUpdate(prevProps) {
35
- const {
36
- visible,
37
- duration
38
- } = this.props;
39
- if (prevProps.visible !== visible) {
40
- Animated.timing(this.visibleAnimated, {
41
- toValue: Number(!!visible),
42
- duration,
43
- useNativeDriver: true
44
- }).start();
45
- }
46
- }
47
- getAnimatedStyle = () => {
48
- return {
49
- opacity: this.visibleAnimated,
50
- transform: [{
51
- translateY: this.visibleAnimated.interpolate({
52
- inputRange: [0, 1],
53
- outputRange: [Constants.screenHeight / 2, 0]
54
- })
55
- }]
56
- };
57
- };
58
- get isSecondaryOnly() {
59
- const {
60
- secondaryButton,
61
- button
62
- } = this.props;
63
- return !!secondaryButton && !button;
64
- }
65
- get isHorizontalLayout() {
66
- const {
67
- buttonLayout
68
- } = this.props;
69
- return buttonLayout === FloatingButtonLayouts.HORIZONTAL || this.isSecondaryOnly;
70
- }
71
- get isSecondaryHorizontal() {
72
- const {
73
- secondaryButton
74
- } = this.props;
75
- return secondaryButton && this.isHorizontalLayout;
76
- }
77
- get isSecondaryVertical() {
78
- const {
79
- secondaryButton
80
- } = this.props;
81
- return secondaryButton && !this.isHorizontalLayout;
82
- }
83
- renderButton() {
84
- const {
85
- bottomMargin,
86
- button,
87
- fullWidth,
88
- testID
89
- } = this.props;
90
- if (button) {
91
- const shadowStyle = button && !button.outline && !button.link ? styles.shadow : undefined;
92
- const marginStyle = {
93
- marginTop: Spacings.s4,
94
- marginBottom: this.isSecondaryVertical ? Spacings.s4 : bottomMargin || Spacings.s8,
95
- marginLeft: this.isSecondaryHorizontal || fullWidth ? Spacings.s4 : undefined,
96
- marginRight: this.isSecondaryHorizontal ? Spacings.s5 : fullWidth ? Spacings.s4 : undefined
19
+ const FloatingButton = props => {
20
+ const {
21
+ visible = false,
22
+ button,
23
+ secondaryButton,
24
+ bottomMargin,
25
+ fullWidth,
26
+ buttonLayout = FloatingButtonLayouts.VERTICAL,
27
+ duration = 300,
28
+ withoutAnimation,
29
+ hideBackgroundOverlay,
30
+ hoisted = true,
31
+ testID
32
+ } = props;
33
+ useEffect(() => {
34
+ // eslint-disable-next-line max-len
35
+ LogService.warn('RNUILib FloatingButton now uses ScreenFooter internally, which requires a SafeAreaProvider. If you experience safe area issues, please wrap your app (or the relevant screen) with <SafeAreaProvider>.');
36
+ }, []);
37
+ const footerContentContainerStyle = useMemo(() => {
38
+ if (bottomMargin !== undefined) {
39
+ return {
40
+ paddingBottom: bottomMargin
97
41
  };
98
- const shouldFlex = this.isSecondaryHorizontal || fullWidth && this.isHorizontalLayout;
99
- return <Button size={Button.sizes.large} flex={!!shouldFlex} style={[shadowStyle, marginStyle]} testID={`${testID}.button`} {...button} />;
100
42
  }
43
+ return undefined;
44
+ }, [bottomMargin]);
45
+ const isSecondaryOnly = !!secondaryButton && !button;
46
+ const isHorizontal = buttonLayout === FloatingButtonLayouts.HORIZONTAL || isSecondaryOnly;
47
+ if (!button && !secondaryButton) {
48
+ return null;
101
49
  }
102
- renderOverlay = () => {
103
- if (!this.props.hideBackgroundOverlay) {
104
- return <View pointerEvents={'none'} style={styles.image}>
105
- <Image style={styles.image} source={gradientImage()} resizeMode={'stretch'} tintColor={Colors.$backgroundDefault} />
106
- </View>;
50
+ const renderPrimaryButton = () => {
51
+ if (!button) {
52
+ return null;
107
53
  }
54
+ const shadowStyle = !button.outline && !button.link ? styles.shadow : undefined;
55
+ const shouldFlex = isHorizontal && !!secondaryButton || fullWidth && isHorizontal;
56
+ return <Button key="primary" size={Button.sizes.large} flex={!!shouldFlex} style={shadowStyle} testID={testID ? `${testID}.button` : undefined} {...button} />;
108
57
  };
109
- renderSecondaryButton() {
110
- const {
111
- secondaryButton,
112
- bottomMargin,
113
- testID,
114
- fullWidth,
115
- button
116
- } = this.props;
117
- if (secondaryButton) {
118
- const bgColor = secondaryButton.backgroundColor || Colors.$backgroundDefault;
119
- const shouldFlex = this.isHorizontalLayout && !!button || fullWidth && this.isSecondaryOnly;
120
- const buttonStyle = this.isHorizontalLayout ? [styles.shadow, styles.horizontalSecondaryMargin, {
121
- backgroundColor: bgColor
122
- }] : {
123
- marginBottom: bottomMargin || Spacings.s7
124
- };
125
- return <Button outline={this.isHorizontalLayout} flex={shouldFlex} link={!this.isHorizontalLayout} size={Button.sizes.large} testID={`${testID}.secondaryButton`} {...secondaryButton} style={buttonStyle} enableShadow={false} />;
126
- }
127
- }
128
- renderHorizontalLayout() {
129
- return <>
130
- {this.renderOverlay()}
131
- {this.renderSecondaryButton()}
132
- {this.renderButton()}
133
- </>;
134
- }
135
- renderVerticalLayout() {
136
- return <>
137
- {this.renderOverlay()}
138
- {this.renderButton()}
139
- {this.renderSecondaryButton()}
140
- </>;
141
- }
142
- render() {
143
- // NOTE: keep this.firstLoad as true as long as the visibility changed to true
144
- const {
145
- withoutAnimation,
146
- visible,
147
- fullWidth,
148
- testID,
149
- button,
150
- secondaryButton
151
- } = this.props;
152
- this.firstLoad && !visible ? this.firstLoad = true : this.firstLoad = false;
153
-
154
- // NOTE: On first load, don't show if it should not be visible
155
- if (this.firstLoad === true && !this.initialVisibility) {
156
- return false;
58
+ const renderSecondaryButton = () => {
59
+ if (!secondaryButton) {
60
+ return null;
157
61
  }
158
- if (!visible && withoutAnimation) {
159
- return false;
160
- }
161
- if (button || secondaryButton) {
162
- const hasBothButtons = !!(button && secondaryButton);
163
- const shouldCenter = !fullWidth || this.isHorizontalLayout && hasBothButtons;
164
- return <View row={this.isHorizontalLayout} center={shouldCenter} pointerEvents="box-none" animated style={[styles.container, this.getAnimatedStyle()]} testID={testID}>
165
- {this.isHorizontalLayout ? this.renderHorizontalLayout() : this.renderVerticalLayout()}
166
- </View>;
167
- }
168
- }
169
- }
62
+ const shouldFlex = isHorizontal && !!button || fullWidth && isSecondaryOnly;
63
+ const bgColor = secondaryButton.backgroundColor || Colors.$backgroundDefault;
64
+ return <Button key="secondary" outline={isHorizontal} link={!isHorizontal} flex={shouldFlex} size={Button.sizes.large} testID={testID ? `${testID}.secondaryButton` : undefined} {...secondaryButton} style={isHorizontal ? [styles.shadow, {
65
+ backgroundColor: bgColor
66
+ }] : undefined} enableShadow={false} />;
67
+ };
68
+ const children = isHorizontal ? [renderSecondaryButton(), renderPrimaryButton()] : [renderPrimaryButton(), renderSecondaryButton()];
69
+ return <ScreenFooter visible={visible} layout={isHorizontal ? ScreenFooterLayouts.HORIZONTAL : ScreenFooterLayouts.VERTICAL} backgroundType={hideBackgroundOverlay ? ScreenFooterBackgrounds.TRANSPARENT : ScreenFooterBackgrounds.FADING} keyboardBehavior={hoisted ? KeyboardBehavior.HOISTED : KeyboardBehavior.STICKY} animationDuration={withoutAnimation ? 0 : duration} itemsFit={fullWidth ? ItemsFit.STRETCH : undefined} contentContainerStyle={footerContentContainerStyle} testID={testID}>
70
+ {children}
71
+ </ScreenFooter>;
72
+ };
73
+ FloatingButton.displayName = 'FloatingButton';
74
+ FloatingButton.floatingButtonLayouts = FloatingButtonLayouts;
170
75
  const styles = StyleSheet.create({
171
- container: {
172
- // ...StyleSheet.absoluteFillObject, // TODO: this is breaking scenarios where the FloatingButton is inside a KeyboardTrackingView
173
- top: undefined,
174
- zIndex: 99
175
- },
176
- image: {
177
- ...StyleSheet.absoluteFillObject,
178
- width: '100%',
179
- height: '100%'
180
- },
181
76
  shadow: {
182
77
  ...Shadows.sh20.bottom
183
- },
184
- horizontalSecondaryMargin: {
185
- marginTop: Spacings.s4,
186
- marginBottom: Spacings.s7,
187
- marginLeft: Spacings.s5
188
78
  }
189
79
  });
190
80
  export default asBaseComponent(FloatingButton);
@@ -26,7 +26,8 @@ const ScreenFooter = props => {
26
26
  visible = true,
27
27
  animationDuration = 200,
28
28
  shadow = ScreenFooterShadow.SH20,
29
- hideDivider = false
29
+ hideDivider = false,
30
+ contentContainerStyle: contentContainerStyleOverride
30
31
  } = props;
31
32
  const keyboard = useAnimatedKeyboard();
32
33
  const [height, setHeight] = useState(0);
@@ -112,13 +113,15 @@ const ScreenFooter = props => {
112
113
  if (isSolid) {
113
114
  const shadowStyle = Shadows[shadow]?.top;
114
115
  const backgroundElevation = shadowStyle?.elevation || 0;
115
- // When the background has a shadow (elevation on Android), it might render on top of the content
116
116
  style.push({
117
117
  elevation: backgroundElevation + 1
118
118
  });
119
119
  }
120
+ if (contentContainerStyleOverride) {
121
+ style.push(contentContainerStyleOverride);
122
+ }
120
123
  return style;
121
- }, [layout, alignItems, justifyContent, insets.bottom, isSolid, shadow, isKeyboardVisible]);
124
+ }, [layout, alignItems, justifyContent, insets.bottom, isSolid, shadow, isKeyboardVisible, contentContainerStyleOverride]);
122
125
  const solidBackgroundStyle = useMemo(() => {
123
126
  if (!isSolid) {
124
127
  return undefined;
@@ -154,13 +157,13 @@ const ScreenFooter = props => {
154
157
  maxWidth: '100%'
155
158
  };
156
159
  return <View key={index} style={fixedStyle}>
157
- {child}
158
- </View>;
160
+ {child}
161
+ </View>;
159
162
  }
160
163
  if (isHorizontal && React.isValidElement(child) && itemsFit === ItemsFit.STRETCH) {
161
164
  return <View flex row centerH key={index}>
162
- {child}
163
- </View>;
165
+ {child}
166
+ </View>;
164
167
  }
165
168
  return child;
166
169
  }, [itemsFit, itemWidth, isHorizontal]);
@@ -81,6 +81,11 @@
81
81
  "type": "boolean",
82
82
  "description": "If true, hides the top divider for solid background. Only applies when backgroundType is 'solid'",
83
83
  "default": "false"
84
+ },
85
+ {
86
+ "name": "contentContainerStyle",
87
+ "type": "StyleProp<ViewStyle>",
88
+ "description": "Custom style for the content container that wraps the footer's children. Can be used to override default padding, gap, or other layout properties."
84
89
  }
85
90
  ],
86
91
  "snippet": [
@@ -118,11 +123,7 @@
118
123
  },
119
124
  {
120
125
  "type": "table",
121
- "columns": [
122
- "Layout Type",
123
- "Use Case",
124
- "Example"
125
- ],
126
+ "columns": ["Layout Type", "Use Case", "Example"],
126
127
  "items": [
127
128
  {
128
129
  "title": "Horizontal - Spread",
@@ -157,11 +158,7 @@
157
158
  },
158
159
  {
159
160
  "type": "table",
160
- "columns": [
161
- "Layout Type",
162
- "Use Case",
163
- "Example"
164
- ],
161
+ "columns": ["Layout Type", "Use Case", "Example"],
165
162
  "items": [
166
163
  {
167
164
  "title": "Vertical - Fit",
@@ -187,11 +184,7 @@
187
184
  },
188
185
  {
189
186
  "type": "table",
190
- "columns": [
191
- "Background Type",
192
- "Visual",
193
- "When to Use"
194
- ],
187
+ "columns": ["Background Type", "Visual", "When to Use"],
195
188
  "items": [
196
189
  {
197
190
  "title": "Solid",
@@ -226,11 +219,7 @@
226
219
  },
227
220
  {
228
221
  "type": "table",
229
- "columns": [
230
- "Behavior",
231
- "Description",
232
- "Example"
233
- ],
222
+ "columns": ["Behavior", "Description", "Example"],
234
223
  "items": [
235
224
  {
236
225
  "title": "Sticky",
@@ -259,4 +248,3 @@
259
248
  ]
260
249
  }
261
250
  }
262
-
@@ -1,5 +1,5 @@
1
1
  import { PropsWithChildren } from 'react';
2
- import { DimensionValue } from 'react-native';
2
+ import { DimensionValue, StyleProp, ViewStyle } from 'react-native';
3
3
  export declare enum ScreenFooterLayouts {
4
4
  HORIZONTAL = "horizontal",
5
5
  VERTICAL = "vertical"
@@ -97,4 +97,9 @@ export interface ScreenFooterProps extends PropsWithChildren<{}> {
97
97
  * Only applies when backgroundType is 'solid'
98
98
  */
99
99
  hideDivider?: boolean;
100
+ /**
101
+ * Custom style for the content container that wraps the footer's children.
102
+ * Can be used to override default padding, gap, or other layout properties.
103
+ */
104
+ contentContainerStyle?: StyleProp<ViewStyle>;
100
105
  }
@@ -9,6 +9,10 @@ import { ButtonDriver } from "../button/Button.driver.new";
9
9
  import { ViewDriver } from "../view/View.driver.new";
10
10
  export const TextFieldDriver = (props, options) => {
11
11
  const driver = usePressableDriver(useComponentDriver(props, options));
12
+ const inputDriver = useComponentDriver({
13
+ renderTree: props.renderTree,
14
+ testID: `${props.testID}.input`
15
+ }, options);
12
16
  const floatingPlaceholderDriver = TextDriver({
13
17
  renderTree: props.renderTree,
14
18
  testID: `${props.testID}.floatingPlaceholder`
@@ -41,30 +45,31 @@ export const TextFieldDriver = (props, options) => {
41
45
  renderTree: props.renderTree,
42
46
  testID: `${props.testID}.clearButton.container`
43
47
  });
48
+ const getInputElement = () => inputDriver.queryElement() ?? driver.getElement();
44
49
  const getValue = () => {
45
- return driver.getElement().props.value ?? driver.getElement().props.defaultValue;
50
+ return getInputElement().props.value ?? getInputElement().props.defaultValue;
46
51
  };
47
52
  const changeText = text => {
48
- fireEvent.changeText(driver.getElement(), text);
53
+ fireEvent.changeText(getInputElement(), text);
49
54
  };
50
55
  const focus = () => {
51
- fireEvent(driver.getElement(), 'focus');
56
+ fireEvent(getInputElement(), 'focus');
52
57
  };
53
58
  const blur = () => {
54
- fireEvent(driver.getElement(), 'blur');
59
+ fireEvent(getInputElement(), 'blur');
55
60
  };
56
61
  const isEnabled = () => {
57
- return !driver.getElement().props.accessibilityState?.disabled;
62
+ return !getInputElement().props.accessibilityState?.disabled;
58
63
  };
59
64
  const getPlaceholder = () => {
60
65
  const exists = () => {
61
- const hasPlaceholder = !!driver.getElement().props.placeholder;
66
+ const hasPlaceholder = !!getInputElement().props.placeholder;
62
67
  const hasText = !!getValue();
63
68
  return hasPlaceholder && (!hasText || hasText && floatingPlaceholderDriver.exists());
64
69
  };
65
70
  const getText = () => {
66
71
  if (exists()) {
67
- return driver.getElement().props.placeholder;
72
+ return getInputElement().props.placeholder;
68
73
  }
69
74
  };
70
75
  return {
@@ -78,6 +78,8 @@ const TextField = props => {
78
78
  readonly = false,
79
79
  showMandatoryIndication,
80
80
  clearButtonStyle,
81
+ testID,
82
+ accessibilityLabel: accessibilityLabelProp,
81
83
  ...others
82
84
  } = usePreset(props);
83
85
  const {
@@ -135,11 +137,31 @@ const TextField = props => {
135
137
  const hasValue = fieldState.value !== undefined; // NOTE: not pressable if centered without a value (so can't center placeholder)
136
138
  const inputStyle = useMemo(() => [typographyStyle, colorStyle, others.style, hasValue && centeredTextStyle], [typographyStyle, colorStyle, others.style, centeredTextStyle, hasValue]);
137
139
  const dummyPlaceholderStyle = useMemo(() => [inputStyle, styles.dummyPlaceholder], [inputStyle]);
140
+ const defaultAccessibilityLabel = useMemo(() => {
141
+ const parts = [];
142
+ if (label) {
143
+ parts.push(label);
144
+ }
145
+ if (context.isMandatory) {
146
+ parts.push('required');
147
+ }
148
+ parts.push('textField');
149
+ if (helperText) {
150
+ parts.push(helperText);
151
+ } else if (placeholder) {
152
+ parts.push(placeholder);
153
+ }
154
+ if (showCharCounter && others.maxLength) {
155
+ parts.push(`you can enter up to ${others.maxLength} characters`);
156
+ }
157
+ return parts.join(', ');
158
+ }, [label, context.isMandatory, helperText, placeholder, showCharCounter, others.maxLength]);
159
+ const accessibilityLabel = accessibilityLabelProp ?? defaultAccessibilityLabel;
138
160
  return <FieldContext.Provider value={context}>
139
- <View {...containerProps} style={[margins, positionStyle, containerStyle, centeredContainerStyle]}>
161
+ <View {...containerProps} testID={testID} accessible accessibilityLabel={accessibilityLabel} style={[margins, positionStyle, containerStyle, centeredContainerStyle]}>
140
162
  <View row spread style={centeredContainerStyle}>
141
- <Label label={label} labelColor={labelColor} labelStyle={_labelStyle} labelProps={labelProps} floatingPlaceholder={floatingPlaceholder} validationMessagePosition={validationMessagePosition} testID={`${props.testID}.label`} showMandatoryIndication={showMandatoryIndication} enableErrors={enableErrors} />
142
- {validationMessagePosition === ValidationMessagePosition.TOP && <ValidationMessage enableErrors={enableErrors} validate={others.validate} validationMessage={others.validationMessage} validationMessageStyle={_validationMessageStyle} retainValidationSpace={retainValidationSpace && retainTopMessageSpace} testID={`${props.testID}.validationMessage`} />}
163
+ <Label label={label} labelColor={labelColor} labelStyle={_labelStyle} labelProps={labelProps} floatingPlaceholder={floatingPlaceholder} validationMessagePosition={validationMessagePosition} testID={`${testID}.label`} showMandatoryIndication={showMandatoryIndication} enableErrors={enableErrors} />
164
+ {validationMessagePosition === ValidationMessagePosition.TOP && <ValidationMessage enableErrors={enableErrors} validate={others.validate} validationMessage={others.validationMessage} validationMessageStyle={_validationMessageStyle} retainValidationSpace={retainValidationSpace && retainTopMessageSpace} testID={`${testID}.validationMessage`} />}
143
165
  {topTrailingAccessory && <View>{topTrailingAccessory}</View>}
144
166
  </View>
145
167
  <View style={[paddings, fieldStyle]} row centerV centerH={centered}>
@@ -158,26 +180,26 @@ const TextField = props => {
158
180
  {Constants.isAndroid && centered && <Text marginR-s1 style={dummyPlaceholderStyle}>
159
181
  {placeholder}
160
182
  </Text>}
161
- {floatingPlaceholder && <FloatingPlaceholder defaultValue={others.defaultValue} placeholder={placeholder} floatingPlaceholderStyle={_floatingPlaceholderStyle} floatingPlaceholderColor={floatingPlaceholderColor} floatOnFocus={floatOnFocus} validationMessagePosition={validationMessagePosition} extraOffset={leadingAccessoryMeasurements?.width} testID={`${props.testID}.floatingPlaceholder`} showMandatoryIndication={showMandatoryIndication} />}
183
+ {floatingPlaceholder && <FloatingPlaceholder defaultValue={others.defaultValue} placeholder={placeholder} floatingPlaceholderStyle={_floatingPlaceholderStyle} floatingPlaceholderColor={floatingPlaceholderColor} floatOnFocus={floatOnFocus} validationMessagePosition={validationMessagePosition} extraOffset={leadingAccessoryMeasurements?.width} testID={`${testID}.floatingPlaceholder`} showMandatoryIndication={showMandatoryIndication} />}
162
184
  <Input hitSlop={{
163
185
  top: 20,
164
186
  bottom: 20
165
- }} placeholderTextColor={hidePlaceholder ? 'transparent' : placeholderTextColor} value={fieldState.value} {...others} readonly={readonly} style={inputStyle} onFocus={onFocus} onBlur={onBlur} onChangeText={onChangeText} placeholder={placeholder} hint={hint} showMandatoryIndication={showMandatoryIndication && !label} />
187
+ }} placeholderTextColor={hidePlaceholder ? 'transparent' : placeholderTextColor} value={fieldState.value} {...others} testID={`${testID}.input`} readonly={readonly} style={inputStyle} onFocus={onFocus} onBlur={onBlur} onChangeText={onChangeText} placeholder={placeholder} hint={hint} showMandatoryIndication={showMandatoryIndication && !label} />
166
188
  </View>}
167
- {showClearButton && <ClearButton onClear={onClear} testID={`${props.testID}.clearButton`} onChangeText={onChangeText} clearButtonStyle={clearButtonStyle} />}
189
+ {showClearButton && <ClearButton onClear={onClear} testID={`${testID}.clearButton`} onChangeText={onChangeText} clearButtonStyle={clearButtonStyle} />}
168
190
  {trailingAccessory}
169
191
  {/* </View> */}
170
192
  </View>
171
193
  <View row spread center={centered}>
172
194
  <View flex={!centered} flexG={centered} marginR-s4={showCharCounter}>
173
- {validationMessagePosition === ValidationMessagePosition.BOTTOM && <ValidationMessage enableErrors={enableErrors} validate={others.validate} validationMessage={others.validationMessage} validationIcon={validationIcon} validationMessageStyle={_validationMessageStyle} retainValidationSpace={retainValidationSpace} testID={`${props.testID}.validationMessage`} />}
174
- {helperText && <Text $textNeutralHeavy subtext marginT-s1 testID={`${props.testID}.helperText`}>
195
+ {validationMessagePosition === ValidationMessagePosition.BOTTOM && <ValidationMessage enableErrors={enableErrors} validate={others.validate} validationMessage={others.validationMessage} validationIcon={validationIcon} validationMessageStyle={_validationMessageStyle} retainValidationSpace={retainValidationSpace} testID={`${testID}.validationMessage`} />}
196
+ {helperText && <Text $textNeutralHeavy subtext marginT-s1 testID={`${testID}.helperText`}>
175
197
  {helperText}
176
198
  </Text>}
177
199
  {bottomAccessory}
178
200
  </View>
179
201
  <View>
180
- {showCharCounter && <CharCounter maxLength={others.maxLength} charCounterStyle={charCounterStyle} testID={`${props.testID}.charCounter`} />}
202
+ {showCharCounter && <CharCounter maxLength={others.maxLength} charCounterStyle={charCounterStyle} testID={`${testID}.charCounter`} />}
181
203
  </View>
182
204
  </View>
183
205
  </View>