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 (
+
+ );
+};
+
+
+```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.
## 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.
+
+
+
+
+
### 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 (
-