diff --git a/src/app/components/client/InputField.module.scss b/src/app/components/client/InputField.module.scss
index 8f95ee8820d..5f4f5e941b7 100644
--- a/src/app/components/client/InputField.module.scss
+++ b/src/app/components/client/InputField.module.scss
@@ -7,14 +7,36 @@
flex-direction: column;
position: relative;
+ .inputFieldWrapper {
+ position: relative;
+ }
+
+ .floatingLabel {
+ background: $color-white;
+ border-radius: $border-radius-sm;
+ color: $color-black;
+ display: inline-block;
+ // Add a bit more space: The next spacing step is too much.
+ left: calc($spacing-sm * 1.5);
+ line-height: 1em;
+ padding: 0 calc($spacing-xs * 0.5);
+ pointer-events: none;
+ position: absolute;
+ top: 50%;
+ transform-origin: top left;
+ transform: translate(-0.05em, -50%) scale(1);
+ transition: transform 0.2s ease-in-out;
+ user-select: none;
+ }
+
.inputField {
border: 1px solid $color-grey-30;
border-radius: $border-radius-sm;
color: $color-black;
- // Add a bit more vertical space: The next spacing step is too much.
- padding: calc($spacing-sm * 1.5) $spacing-md;
+ // Add a bit more space: The next spacing step is too much.
+ padding: calc($spacing-sm * 1.5);
+ width: 100%;
- &::placeholder,
&.noValue {
color: $color-grey-40;
}
@@ -39,6 +61,25 @@
}
}
+ &:has(.floatingLabel) {
+ ::placeholder {
+ @include visually-hidden;
+ }
+ .inputField {
+ // Move the value string off-center.
+ padding: calc($spacing-md * 1.25) calc($spacing-sm * 1.5) $spacing-xs;
+ }
+ }
+
+ &:focus-within,
+ &:not(:has(:placeholder-shown)) {
+ .floatingLabel {
+ color: $color-grey-40;
+ // Make the floating label visually align with the input value.
+ transform: translate(-0.05em, -115%) scale(0.75);
+ }
+ }
+
.inputLabel {
font-weight: 600;
margin-bottom: $spacing-sm;
diff --git a/src/app/components/client/InputField.test.tsx b/src/app/components/client/InputField.test.tsx
new file mode 100644
index 00000000000..87cdd76fa6a
--- /dev/null
+++ b/src/app/components/client/InputField.test.tsx
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { expect } from "@jest/globals";
+import { render } from "@testing-library/react";
+import { composeStory } from "@storybook/react";
+import { axe } from "jest-axe";
+import Meta, {
+ TextInputFieldEmpty,
+ TextInputFieldEmptyFloatingLabel,
+ TextInputFieldFilled,
+ TextInputFieldFilledFloatingLabel,
+} from "./stories/InputField.stories";
+
+describe("InputField", () => {
+ test.each([
+ TextInputFieldEmpty,
+ TextInputFieldFilled,
+ TextInputFieldEmptyFloatingLabel,
+ TextInputFieldFilledFloatingLabel,
+ ])("passes the axe accessibility test suite for %s", async (component) => {
+ const ComposedInput = composeStory(component, Meta);
+ const { container } = render(