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(); + expect(await axe(container)).toHaveNoViolations(); + }); +}); diff --git a/src/app/components/client/InputField.tsx b/src/app/components/client/InputField.tsx index abc2bb447df..e0d566ce508 100644 --- a/src/app/components/client/InputField.tsx +++ b/src/app/components/client/InputField.tsx @@ -13,6 +13,7 @@ import { useL10n } from "../../hooks/l10n"; export const InputField = ( props: AriaTextFieldProps & { iconButton?: ReactNode; + hasFloatingLabel?: boolean; }, ) => { const { isRequired, label, isInvalid, value, description } = props; @@ -29,13 +30,14 @@ export const InputField = ( return (
{label && ( -