Skip to content

Commit 6fedb6c

Browse files
committed
feat(text-input): add new next component
1 parent 391a6c1 commit 6fedb6c

29 files changed

+2860
-67
lines changed

docs/validations.mdx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ These states can indicate whether the input is valid or not, and can also preven
2323
### Validation Redesign
2424

2525
Carbon is in the process of implementing a new validation pattern to our input components. To opt into this new pattern, you can set the
26-
`validationRedesignOptIn` flag to true on the [CarbonProvider](../?path=/docs/carbon-provider--docs).
26+
`validationRedesignOptIn` flag to true on the [CarbonProvider](../?path=/docs/carbon-provider--docs).
2727

2828
The new validation pattern is designed to be more consistent and user-friendly, providing a clearer indication of the input's state and we encourage its use over the legacy pattern using tooltips.
2929

@@ -64,6 +64,9 @@ Component props that are not supported if the opt-in flag is set to true are lab
6464
The legacy validation pattern is still available for use, but we recommend using the new validation pattern for a more consistent and user-friendly experience.
6565
This pattern uses tooltips to provide feedback to users about the state of their input.
6666

67+
The `Textbox` component is used below for documentation purposes, but it is recommend to use the new `TextInput` component instead which only uses the new validation pattern
68+
by default.
69+
6770
#### States
6871

6972
Each input component that supports validations accepts the following props - `error`, `warning` and `info`.
@@ -117,7 +120,7 @@ For more information on how to use React Hook Form, please refer to the [React H
117120
import { useForm, SubmitHandler, Controller } from 'react-hook-form';
118121
import Button from 'carbon-react/lib/components/button';
119122
import Form from 'carbon-react/lib/components/form';
120-
import Textbox from 'carbon-react/lib/components/textbox';
123+
import TextInput from 'carbon-react/lib/components/textbox/__next__';
121124

122125
interface FormValues {
123126
name: string;
@@ -146,7 +149,7 @@ const MyForm = () => {
146149
required: 'Name is required',
147150
}}
148151
render={({ field: { onChange, onBlur, value }, fieldState }) => (
149-
<Textbox
152+
<TextInput
150153
label="Name"
151154
required
152155
value={value}

docs/validations.stories.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from "react";
22
import { Meta, StoryObj } from "@storybook/react";
33

44
import Textbox from "../src/components/textbox";
5+
import TextInput from "../src/components/textbox/__next__";
56
import { RadioButton, RadioButtonGroup } from "../src/components/radio-button";
67
import { Checkbox, CheckboxGroup } from "../src/components/checkbox";
78
import CarbonProvider from "../src/components/carbon-provider";
@@ -134,15 +135,15 @@ export const ValidationRedesign: StoryObj = () => {
134135
<RequiredFieldsIndicator mb={2}>
135136
Fill in all fields marked with
136137
</RequiredFieldsIndicator>
137-
<Textbox
138+
<TextInput
138139
label="Textbox"
139140
inputHint="Hint text"
140141
value={state}
141142
onChange={(e) => setState(e.target.value)}
142143
required
143144
error="Error Message (Fix is required)"
144145
/>
145-
<Textbox
146+
<TextInput
146147
label="Textbox"
147148
inputHint="Hint text"
148149
value={state}

playwright-ct.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export default defineConfig({
4949
...devices["Desktop Chrome"],
5050
testIdAttribute: "data-role",
5151
viewport: { width: 1366, height: 768 },
52+
trace: "on-first-retry",
5253
},
5354
},
5455
],

src/__internal__/form-field/form-field.component.tsx

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import { MarginProps } from "styled-system";
1010
import invariant from "invariant";
1111

1212
import { ValidationProps } from "../validations";
13-
import FormFieldStyle, { FieldLineStyle } from "./form-field.style";
13+
import FormFieldStyle, { FieldLineStyle, LabelField } from "./form-field.style";
1414
import Label, { LabelProps } from "../label";
15+
import HintText from "../../__internal__/hint-text";
1516
import FieldHelp from "../field-help";
1617
import tagComponent, { TagProps } from "../utils/helpers/tags/tags";
1718
import useIsAboveBreakpoint from "../../hooks/__internal__/useIsAboveBreakpoint";
@@ -40,6 +41,10 @@ interface CommonFormFieldProps extends MarginProps, ValidationProps {
4041
fieldHelpId?: string;
4142
/** Label content */
4243
label?: React.ReactNode;
44+
/** A hint string rendered before the input but after the label. Intended to describe the purpose or content of the input. */
45+
inputHint?: string;
46+
/** The unique identifier for the hint element. Useful for accessibility (e.g., connecting via aria-describedby). */
47+
inputHintId?: string;
4348
/** Text alignment of the label */
4449
labelAlign?: "left" | "right";
4550
/** A message that the Help component will display */
@@ -86,6 +91,11 @@ export interface FormFieldProps extends CommonFormFieldProps, TagProps {
8691
maxWidth?: string;
8792
/** @private @internal @ignore */
8893
"data-component"?: string;
94+
size?: "small" | "medium" | "large";
95+
/** The width of the component container as a percentage */
96+
containerWidth?: number;
97+
/** @private @internal @ignore */
98+
isInsideTextInput?: boolean;
8999
}
90100

91101
const FormField = ({
@@ -102,6 +112,8 @@ const FormField = ({
102112
tooltipId,
103113
fieldHelpId,
104114
label,
115+
inputHint,
116+
inputHintId,
105117
labelId,
106118
labelAlign,
107119
labelHelp,
@@ -117,6 +129,9 @@ const FormField = ({
117129
isRequired,
118130
validationIconId,
119131
validationRedesignOptIn,
132+
size,
133+
containerWidth,
134+
isInsideTextInput,
120135
...rest
121136
}: FormFieldProps) => {
122137
const invalidValidationProp: string | undefined = useMemo(() => {
@@ -204,40 +219,69 @@ const FormField = ({
204219
</FieldHelp>
205220
) : null;
206221

222+
const labelComponent = label && (
223+
<Label
224+
labelId={labelId}
225+
align={labelAlign}
226+
disabled={disabled}
227+
error={!validationRedesignOptIn && error}
228+
warning={!validationRedesignOptIn && warning}
229+
info={!validationRedesignOptIn && info}
230+
help={labelHelp}
231+
tooltipId={tooltipId}
232+
htmlFor={id}
233+
helpIcon={labelHelpIcon}
234+
inline={inlineLabel}
235+
width={labelWidth}
236+
useValidationIcon={useValidationIcon}
237+
pr={!reverse ? labelSpacing : undefined}
238+
pl={reverse ? labelSpacing : undefined}
239+
isRequired={isRequired}
240+
validationIconId={validationIconId}
241+
as={labelAs}
242+
>
243+
{label}
244+
</Label>
245+
);
246+
207247
return (
208-
<FormFieldStyle {...tagComponent(dataComponent, rest)} {...marginProps}>
248+
<FormFieldStyle
249+
containerWidth={containerWidth}
250+
{...tagComponent(dataComponent, rest)}
251+
{...marginProps}
252+
>
209253
<FieldLineStyle
210254
data-role="field-line"
211255
inline={inlineLabel}
212256
maxWidth={maxWidth}
257+
labelInline={labelInline}
258+
size={size}
259+
isInsideTextInput={isInsideTextInput}
213260
>
214261
{reverse && children}
215262

216-
{label && (
217-
<Label
218-
labelId={labelId}
219-
align={labelAlign}
220-
disabled={disabled}
221-
error={!validationRedesignOptIn && error}
222-
warning={!validationRedesignOptIn && warning}
223-
info={!validationRedesignOptIn && info}
224-
help={labelHelp}
225-
tooltipId={tooltipId}
226-
htmlFor={id}
227-
helpIcon={labelHelpIcon}
228-
inline={inlineLabel}
263+
{(label || inputHint) && isInsideTextInput && (
264+
<LabelField
265+
data-role="label-field"
266+
labelInline={labelInline}
229267
width={labelWidth}
230-
useValidationIcon={useValidationIcon}
231-
pr={!reverse ? labelSpacing : undefined}
232-
pl={reverse ? labelSpacing : undefined}
233-
isRequired={isRequired}
234-
validationIconId={validationIconId}
235-
as={labelAs}
268+
isLarge={size === "large"}
236269
>
237-
{label}
238-
</Label>
270+
{labelComponent}
271+
{inputHint && (
272+
<HintText
273+
marginBottom="0"
274+
data-element="input-hint"
275+
id={inputHintId}
276+
>
277+
{inputHint}
278+
</HintText>
279+
)}
280+
</LabelField>
239281
)}
240282

283+
{!isInsideTextInput && labelComponent}
284+
241285
{fieldHelpInline && fieldHelp}
242286

243287
{!reverse && children}

src/__internal__/form-field/form-field.style.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import styled, { css } from "styled-components";
22
import { space } from "styled-system";
33
import applyBaseTheme from "../../style/themes/apply-base-theme";
4+
import labelConfig from "../../components/textbox/__next__/config";
45

5-
const FormFieldStyle = styled.div.attrs(applyBaseTheme)`
6+
const FormFieldStyle = styled.div.attrs(applyBaseTheme)<{
7+
containerWidth?: number;
8+
}>`
9+
${({ containerWidth }) => containerWidth && `width: ${containerWidth}%;`}
610
position: relative;
711
margin-bottom: var(--fieldSpacing);
812
& + & {
@@ -17,13 +21,51 @@ const FormFieldStyle = styled.div.attrs(applyBaseTheme)`
1721
export interface FieldLineStyleProps {
1822
inline?: boolean;
1923
maxWidth?: string;
24+
labelInline?: boolean;
25+
size?: "small" | "medium" | "large";
26+
isInsideTextInput?: boolean;
2027
}
2128
const FieldLineStyle = styled.div<FieldLineStyleProps>`
22-
${({ inline, maxWidth }) => css`
23-
display: ${inline ? "flex" : "block"};
29+
${({
30+
inline,
31+
maxWidth,
32+
labelInline,
33+
size = "medium",
34+
isInsideTextInput,
35+
}) => css`
36+
display: flex;
37+
flex-direction: ${inline ? "row" : "column"};
2438
${maxWidth && `max-width: ${maxWidth};`}
39+
40+
${isInsideTextInput &&
41+
`
42+
gap: ${
43+
labelInline
44+
? labelConfig[size].gap.inline
45+
: labelConfig[size].gap.nonInline
46+
};
47+
`}
2548
`}
2649
`;
2750

28-
export { FieldLineStyle };
51+
interface LabelFieldProps {
52+
width?: number;
53+
labelInline?: boolean;
54+
isLarge?: boolean;
55+
}
56+
57+
const LabelField = styled.div<LabelFieldProps>`
58+
${({ width }) => width && `width: ${width}%;`}
59+
display: flex;
60+
flex-direction: column;
61+
${({ labelInline }) => `align-items: ${labelInline ? "end" : "start"};`}
62+
${({ isLarge }) =>
63+
`font-size: ${isLarge ? "var(--fontSizes200)" : "var(--fontSizes100)"};`}
64+
65+
[data-role="label-container"] {
66+
all: revert;
67+
}
68+
`;
69+
70+
export { FieldLineStyle, LabelField };
2971
export default FormFieldStyle;

src/__internal__/form-field/form-field.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,9 @@ test("should not render with `labelInline` when `adaptiveLabelBreakpoint` set an
147147
/>,
148148
);
149149

150-
expect(screen.getByTestId("field-line")).toHaveStyle("display: block");
150+
expect(screen.getByTestId("field-line")).toHaveStyle(
151+
"flex-direction: column",
152+
);
151153
});
152154

153155
test("should render with `labelInline` when `adaptiveLabelBreakpoint` set and screen is bigger than the breakpoint", () => {

src/__internal__/hint-text/hint-text.style.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const StyledHintText = styled.div<HintTextProps>`
1616
display: flex;
1717
align-items: center;
1818
font-size: 14px;
19+
line-height: 150%;
1920
2021
${({ isLarge }) =>
2122
isLarge &&

src/__internal__/input/input-presentation.component.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export interface CommonInputPresentationProps extends ValidationProps {
3434
borderRadius?: BorderRadiusType | BorderRadiusType[];
3535
/** Renders with transparent borders. This will not effect focus styling or validation borders */
3636
hideBorders?: boolean;
37+
/** @private @internal @ignore */
38+
isInsideTextInput?: boolean;
3739
}
3840

3941
export interface InputPresentationProps extends CommonInputPresentationProps {
@@ -56,6 +58,7 @@ const InputPresentation = ({
5658
prefix,
5759
readOnly,
5860
size = "medium",
61+
isInsideTextInput,
5962
warning,
6063
}: InputPresentationProps): JSX.Element => {
6164
const { hasFocus, onMouseDown, onMouseEnter, onMouseLeave } =
@@ -79,6 +82,7 @@ const InputPresentation = ({
7982
<StyledInputPresentationContainer
8083
inputWidth={inputWidth}
8184
maxWidth={maxWidth}
85+
isInsideTextInput={isInsideTextInput}
8286
data-role="input-presentation-container"
8387
>
8488
{positionedChildren}

src/__internal__/input/input-presentation.style.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@ import { CarbonProviderProps } from "../../components/carbon-provider";
77
import addFocusStyling from "../../style/utils/add-focus-styling";
88

99
export const StyledInputPresentationContainer = styled.div<
10-
Pick<CommonInputPresentationProps, "inputWidth" | "maxWidth">
10+
Pick<
11+
CommonInputPresentationProps,
12+
"inputWidth" | "maxWidth" | "isInsideTextInput"
13+
>
1114
>`
12-
flex: 0 0 ${({ inputWidth }) => inputWidth}%;
15+
${({ inputWidth, isInsideTextInput }) => {
16+
if (!inputWidth) return "";
17+
if (isInsideTextInput) return `width: ${inputWidth}%;`;
18+
return `flex: 0 0 ${inputWidth}%;`;
19+
}}
1320
display: flex;
1421
position: relative;
1522
max-width: ${({ maxWidth }) => (maxWidth ? `${maxWidth}` : "100%")};

src/__internal__/validation-message/validation-message.style.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const StyledValidationMessage = styled.p<StyledValidationMessageProps>`
1818
? "var(--colorsSemanticNegative450)"
1919
: "var(--colorsSemanticNegative500)";
2020
return css`
21+
line-height: 150%;
2122
color: ${isWarning ? "var(--tempColorsSemanticCaution600)" : darkBgColour};
2223
font-weight: ${isWarning ? "normal" : "500"};
2324
font-size: ${isLarge ? "var(--fontSizes200)" : "var(--fontSizes100)"};

0 commit comments

Comments
 (0)