Skip to content

Commit

Permalink
feat(components): Allow for external label [JOB-113378] (#2333)
Browse files Browse the repository at this point in the history
* Add InputLabel

* Use pre-existing label

* Remove InputLabel

* Fix snapshots

* Clean things up and add docs

* Add tests for hiding miniLabel

* Changes from code review

* Improve logic code readability
  • Loading branch information
Aiden-Brine authored Feb 3, 2025
1 parent b0ad63b commit 1435e97
Show file tree
Hide file tree
Showing 15 changed files with 192 additions and 71 deletions.
1 change: 0 additions & 1 deletion docs/components/InputNumber/InputNumber.stories.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs";
import { useState } from "react";
import { InputNumber } from "@jobber/components/InputNumber";
import { Button } from "@jobber/components/Button";
import { Content } from "@jobber/components/Content";
Expand Down
121 changes: 75 additions & 46 deletions docs/components/InputText/InputText.stories.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs";
import { Canvas, Meta } from "@storybook/addon-docs";
import { useState } from "react";
import { InputText } from "@jobber/components/InputText";
import { Content } from "@jobber/components/Content";
import { Disclosure } from "@jobber/components/Disclosure";
import { FormFieldLabel } from "@jobber/components/FormField";

<Meta title="Components/Forms and Inputs/InputText" component={InputText} />

Expand All @@ -15,22 +17,38 @@ Input text is used in forms that accept short or long answers from users.

Use this to allow users to provide short answers.

export const ControlledExample = () => {
const [age, setAge] = useState("Veintisiete");
return (
<InputText
value={age}
onChange={setAge}
name="age"
placeholder="Age in words"
/>
);
};

<Canvas>
<Story name="Controlled">
{() => {
const [age, setAge] = useState("Veintisiete");
return (
<InputText
value={age}
onChange={setAge}
name="age"
placeholder="Age in words"
/>
);
}}
</Story>
<ControlledExample />
</Canvas>

<Disclosure title="Show code">
```tsx
const ControlledExample = () => {
const [age, setAge] = useState("Veintisiete");
return (
<InputText
value={age}
onChange={setAge}
name="age"
placeholder="Age in words"
/>
);
};
```
</Disclosure>

### Multiline

Use this to allow users to provide long answers. The default number of rows is
Expand All @@ -40,13 +58,7 @@ For web, you can set a minimum and maximum number of rows. See:
[Web/rows example](../?path=/story/components-forms-and-inputs-inputtext-web--multiline).

<Canvas>
<Story name="Multiline">
<InputText
multiline={true}
placeholder="Describe your age"
name="describeAge"
/>
</Story>
<InputText multiline={true} placeholder="Describe your age" />
</Canvas>

### Prefix/suffix
Expand Down Expand Up @@ -110,46 +122,36 @@ This includes, but is not limited to:
you can pass an object of callback functions to validate all of them.

<Canvas>
<Story name="Validation">
{() => {
return (
<InputText
placeholder="What's your age"
name="age"
validations={{
required: {
value: true,
message: "You have to tell us your age",
},
validate: validations,
}}
/>
);
function validations(val) {
<InputText
placeholder="What's your age"
validations={{
required: {
value: true,
message: "You have to tell us your age",
},
validate: val => {
if (val.length > 0 && !isNaN(val)) {
return "Type your age in words please.";
}
if (val.length >= 10) {
return "That seems too old.";
}
return true;
}
},
}}
</Story>
/>
</Canvas>

## States

### Disabled

<Canvas>
<Story name="disabled">
<InputText
placeholder="Credit card"
value="**** **** **** 1234"
disabled={true}
/>
</Story>
<InputText
placeholder="Credit card"
value="**** **** **** 1234"
disabled={true}
/>
</Canvas>

### Invalid
Expand All @@ -162,6 +164,33 @@ See:
<InputText placeholder="Email" value="atlantis" invalid={true} />
</Canvas>

### External label

You can use `FormFieldLabel` to provide a label outside of the input. The
`showMiniLabel` prop on `InputText` can be used to hide the mini label that
appears when a value is provided.

<style>
{`
.fullWidth {
width: 100%;
}
`}
</style>

<Canvas>
<div className="fullWidth">
<FormFieldLabel external={true} htmlFor="ext-input">
External label
</FormFieldLabel>
<InputText
showMiniLabel={false}
placeholder="You can still have a placeholder"
id="ext-input"
/>
</div>
</Canvas>

### Keyboard

Determine what default keyboard appears on mobile.
Expand Down
19 changes: 19 additions & 0 deletions docs/components/InputText/Web.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Button } from "@jobber/components/Button";
import { Content } from "@jobber/components/Content";
import { Grid } from "@jobber/components/Grid";
import { Box } from "@jobber/components/Box";
import { FormFieldLabel } from "@jobber/components/FormField";

export default {
title: "Components/Forms and Inputs/InputText/Web",
Expand All @@ -19,6 +20,17 @@ const BasicTemplate: ComponentStory<typeof InputText> = args => {
return <InputText {...args} />;
};

const ExternalLabelTemplate: ComponentStory<typeof InputText> = args => {
return (
<div>
<FormFieldLabel external={true} htmlFor="ext-input">
External label
</FormFieldLabel>
<InputText id="ext-input" {...args} />
</div>
);
};

export const Basic = BasicTemplate.bind({});
Basic.args = {
name: "age",
Expand Down Expand Up @@ -75,6 +87,13 @@ Clearable.args = {
clearable: "always",
};

export const ExternalLabel = ExternalLabelTemplate.bind({});
ExternalLabel.args = {
name: "name",
clearable: "always",
showMiniLabel: false,
};

export const VersionComparison = () => {
const [values, setValues] = React.useState({
basic: "",
Expand Down
7 changes: 7 additions & 0 deletions packages/components/src/FormField/FormField.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,13 @@
transition: all var(--timing-quick);
}

.externalLabel {
display: block;
margin-bottom: var(--space-smaller);
color: var(--field--placeholder-color);
line-height: var(--typography--lineHeight-base);
}

.select select,
.right select,
.select .label {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ declare const styles: {
readonly "input": string;
readonly "label": string;
readonly "select": string;
readonly "externalLabel": string;
readonly "postfix": string;
readonly "affixIcon": string;
readonly "suffix": string;
Expand Down
26 changes: 26 additions & 0 deletions packages/components/src/FormField/FormField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,32 @@ describe("FormField", () => {
);
});
});
describe("with showMiniLabel set to false", () => {
it("should still render placeholder if there is no value", () => {
const FORM_FIELD_TEST_ID = "Form-Field-Wrapper";
const placeholder = "The best placeholder!";
render(<FormField placeholder={placeholder} showMiniLabel={false} />);
expect(screen.getByLabelText(placeholder)).toBeInTheDocument();
expect(screen.getByTestId(FORM_FIELD_TEST_ID)).not.toHaveClass(
"miniLabel",
);
});
it("should hide the mini label", () => {
const FORM_FIELD_TEST_ID = "Form-Field-Wrapper";
const placeholder = "The best placeholder!";
render(
<FormField
placeholder={placeholder}
showMiniLabel={false}
value="Foo"
/>,
);
expect(screen.queryByLabelText(placeholder)).not.toBeInTheDocument();
expect(screen.getByTestId(FORM_FIELD_TEST_ID)).not.toHaveClass(
"miniLabel",
);
});
});
});

describe("when small", () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/FormField/FormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export function FormField(props: FormFieldProps) {
// Warning: do not move useId into FormFieldInternal. This must be here to avoid
// a problem where useId isn't stable across multiple StrictMode renders.
// https://github.com/facebook/react/issues/27103
const id = useId();
const generatedId = useId();
const id = props.id || generatedId;

return <FormFieldInternal {...props} id={id} />;
}
Expand Down
19 changes: 18 additions & 1 deletion packages/components/src/FormField/FormFieldTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export interface Suffix extends BaseSuffix {
* interfaces.
*/
export interface CommonFormFieldProps {
/**
* A unique identifier for the input.
*/
readonly id?: string;

/**
* Determines the alignment of the text inside the input.
*/
Expand All @@ -68,6 +73,15 @@ export interface CommonFormFieldProps {
*/
readonly disabled?: boolean;

/**
* Controls the visibility of the mini label that appears inside the input
* when a value is entered. By default, the placeholder text moves up to
* become a mini label. Set to false to disable this behavior.
*
* @default true
*/
readonly showMiniLabel?: boolean;

/**
* Highlights the field red to indicate an error.
*/
Expand Down Expand Up @@ -110,7 +124,10 @@ export interface CommonFormFieldProps {
onValidation?(message: string): void;

/**
* Hint text that goes above the value once the form is filled out.
* Text that appears inside the input when empty and floats above the value
* as a mini label once the user enters a value.
* When showMiniLabel is false, this text only serves as a standard placeholder and
* disappears when the user types.
*/
readonly placeholder?: string;

Expand Down
Loading

0 comments on commit 1435e97

Please sign in to comment.