From 1435e973a5b5865af3b903bdd05c7f6ee458f4be Mon Sep 17 00:00:00 2001 From: Aiden Brine Date: Mon, 3 Feb 2025 10:32:56 -0800 Subject: [PATCH] feat(components): Allow for external label [JOB-113378] (#2333) * 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 --- .../InputNumber/InputNumber.stories.mdx | 1 - .../InputText/InputText.stories.mdx | 121 +++++++++++------- docs/components/InputText/Web.stories.tsx | 19 +++ .../src/FormField/FormField.module.css | 7 + .../src/FormField/FormField.module.css.d.ts | 1 + .../src/FormField/FormField.test.tsx | 26 ++++ .../components/src/FormField/FormField.tsx | 3 +- .../src/FormField/FormFieldTypes.ts | 19 ++- .../src/FormField/FormFieldWrapper.tsx | 44 ++++--- .../__snapshots__/FormField.test.tsx.snap | 2 +- .../hooks/useFormFieldWrapperStyles.ts | 4 +- packages/components/src/FormField/index.ts | 2 +- .../src/InputText/InputText.rebuilt.tsx | 10 +- .../src/InputTime/InputTimeProps.tsx | 1 + packages/components/src/utils/meta/meta.json | 3 +- 15 files changed, 192 insertions(+), 71 deletions(-) diff --git a/docs/components/InputNumber/InputNumber.stories.mdx b/docs/components/InputNumber/InputNumber.stories.mdx index cfa7c6163b..e6129a0603 100644 --- a/docs/components/InputNumber/InputNumber.stories.mdx +++ b/docs/components/InputNumber/InputNumber.stories.mdx @@ -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"; diff --git a/docs/components/InputText/InputText.stories.mdx b/docs/components/InputText/InputText.stories.mdx index 3a527da262..62513eb499 100644 --- a/docs/components/InputText/InputText.stories.mdx +++ b/docs/components/InputText/InputText.stories.mdx @@ -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"; @@ -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 ( + + ); +}; + - - {() => { - const [age, setAge] = useState("Veintisiete"); - return ( - - ); - }} - + + +```tsx +const ControlledExample = () => { + const [age, setAge] = useState("Veintisiete"); + return ( + + ); +}; +``` + + ### Multiline Use this to allow users to provide long answers. The default number of rows is @@ -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). - - - + ### Prefix/suffix @@ -110,22 +122,14 @@ This includes, but is not limited to: you can pass an object of callback functions to validate all of them. - - {() => { - return ( - - ); - function validations(val) { + { if (val.length > 0 && !isNaN(val)) { return "Type your age in words please."; } @@ -133,9 +137,9 @@ This includes, but is not limited to: return "That seems too old."; } return true; - } + }, }} - + /> ## States @@ -143,13 +147,11 @@ This includes, but is not limited to: ### Disabled - - - + ### Invalid @@ -162,6 +164,33 @@ See: +### 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. + + + + +
+ + External label + + +
+
+ ### Keyboard Determine what default keyboard appears on mobile. diff --git a/docs/components/InputText/Web.stories.tsx b/docs/components/InputText/Web.stories.tsx index 5f458db4e6..653a023ef5 100644 --- a/docs/components/InputText/Web.stories.tsx +++ b/docs/components/InputText/Web.stories.tsx @@ -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", @@ -19,6 +20,17 @@ const BasicTemplate: ComponentStory = args => { return ; }; +const ExternalLabelTemplate: ComponentStory = args => { + return ( +
+ + External label + + +
+ ); +}; + export const Basic = BasicTemplate.bind({}); Basic.args = { name: "age", @@ -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: "", diff --git a/packages/components/src/FormField/FormField.module.css b/packages/components/src/FormField/FormField.module.css index b0414d4e4c..a0ad40784d 100644 --- a/packages/components/src/FormField/FormField.module.css +++ b/packages/components/src/FormField/FormField.module.css @@ -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 { diff --git a/packages/components/src/FormField/FormField.module.css.d.ts b/packages/components/src/FormField/FormField.module.css.d.ts index ebf1d4c935..25e3206e10 100644 --- a/packages/components/src/FormField/FormField.module.css.d.ts +++ b/packages/components/src/FormField/FormField.module.css.d.ts @@ -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; diff --git a/packages/components/src/FormField/FormField.test.tsx b/packages/components/src/FormField/FormField.test.tsx index b5084f50fd..5089bc0a1c 100644 --- a/packages/components/src/FormField/FormField.test.tsx +++ b/packages/components/src/FormField/FormField.test.tsx @@ -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(); + 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( + , + ); + expect(screen.queryByLabelText(placeholder)).not.toBeInTheDocument(); + expect(screen.getByTestId(FORM_FIELD_TEST_ID)).not.toHaveClass( + "miniLabel", + ); + }); + }); }); describe("when small", () => { diff --git a/packages/components/src/FormField/FormField.tsx b/packages/components/src/FormField/FormField.tsx index 8e325890b2..7b97d24b9e 100644 --- a/packages/components/src/FormField/FormField.tsx +++ b/packages/components/src/FormField/FormField.tsx @@ -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 ; } diff --git a/packages/components/src/FormField/FormFieldTypes.ts b/packages/components/src/FormField/FormFieldTypes.ts index 22758a6a1f..4a10d98a5e 100644 --- a/packages/components/src/FormField/FormFieldTypes.ts +++ b/packages/components/src/FormField/FormFieldTypes.ts @@ -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. */ @@ -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. */ @@ -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; diff --git a/packages/components/src/FormField/FormFieldWrapper.tsx b/packages/components/src/FormField/FormFieldWrapper.tsx index 60e9fc6d17..a6c2e107dd 100644 --- a/packages/components/src/FormField/FormFieldWrapper.tsx +++ b/packages/components/src/FormField/FormFieldWrapper.tsx @@ -18,6 +18,7 @@ export interface FormFieldWrapperProps extends FormFieldProps { readonly descriptionIdentifier: string; readonly clearable: Clearable; readonly onClear: () => void; + readonly showMiniLabel?: boolean; } export function FormFieldWrapper({ @@ -42,6 +43,7 @@ export function FormFieldWrapper({ onClear, toolbar, toolbarVisibility = "while-editing", + showMiniLabel = true, wrapperRef, }: PropsWithChildren) { const prefixRef = useRef() as RefObject; @@ -62,6 +64,7 @@ export function FormFieldWrapper({ disabled, inline, size, + showMiniLabel, }); const { focused } = useFormFieldFocus({ wrapperRef }); @@ -92,15 +95,18 @@ export function FormFieldWrapper({ - + {(showMiniLabel || !value) && ( + + {placeholder} + + )} {children} @@ -157,19 +163,25 @@ export function FormFieldWrapperMain({ } export function FormFieldLabel({ - placeholder, - identifier, + children, + htmlFor, style, + external = false, }: { - readonly placeholder?: string; - readonly identifier?: string; + readonly children?: ReactNode; + readonly htmlFor?: string; readonly style?: React.CSSProperties; + readonly external?: boolean; }) { - if (!placeholder) return null; + if (!children) return null; return ( -