From 8ab405721af0ecb435288ac9896ac5e789e5cac2 Mon Sep 17 00:00:00 2001 From: Nick De Villiers Date: Tue, 30 Apr 2024 11:18:55 +0100 Subject: [PATCH 1/3] feat: limited input component --- package-lock.json | 2 +- .../elements/LimitedInput/LimitedInput.scss | 15 +++++ .../LimitedInput/LimitedInput.stories.tsx | 20 +++++++ .../LimitedInput/LimitedInput.test.tsx | 38 ++++++++++++ .../elements/LimitedInput/LimitedInput.tsx | 59 +++++++++++++++++++ src/lib/elements/LimitedInput/index.ts | 1 + src/lib/elements/index.ts | 3 +- 7 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/lib/elements/LimitedInput/LimitedInput.scss create mode 100644 src/lib/elements/LimitedInput/LimitedInput.stories.tsx create mode 100644 src/lib/elements/LimitedInput/LimitedInput.test.tsx create mode 100644 src/lib/elements/LimitedInput/LimitedInput.tsx create mode 100644 src/lib/elements/LimitedInput/index.ts diff --git a/package-lock.json b/package-lock.json index b9e915d..cd7b35e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@storybook/addon-onboarding": "^1.0.10", "@storybook/blocks": "^7.6.7", "@storybook/react": "^7.6.7", - "@storybook/react-vite": "^8.0.9", + "@storybook/react-vite": "^8.0.0", "@storybook/testing-library": "^0.2.2", "@storybook/theming": "7.6.10", "@testing-library/jest-dom": "^6.1.3", diff --git a/src/lib/elements/LimitedInput/LimitedInput.scss b/src/lib/elements/LimitedInput/LimitedInput.scss new file mode 100644 index 0000000..48944b8 --- /dev/null +++ b/src/lib/elements/LimitedInput/LimitedInput.scss @@ -0,0 +1,15 @@ +@import "vanilla-framework"; + +.limited-input { + position: relative; + + &__wrapper::before { + position: absolute; + pointer-events: none; + padding-left: $spv--small; + padding-bottom: calc(0.4rem - 1px); + padding-top: calc(0.4rem - 1px); + content: var(--immutable, ""); + top: var(--top, "inherit"); + } +} diff --git a/src/lib/elements/LimitedInput/LimitedInput.stories.tsx b/src/lib/elements/LimitedInput/LimitedInput.stories.tsx new file mode 100644 index 0000000..28236a4 --- /dev/null +++ b/src/lib/elements/LimitedInput/LimitedInput.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { LimitedInput } from "@/lib/elements/LimitedInput/LimitedInput"; + +const meta: Meta = { + title: "elements/LimitedInput", + component: LimitedInput, + tags: ["autodocs"], +}; + +export default meta; + +export const Example: StoryObj = { + args: { + help: "The valid range for this subnet is 192.168.0.[1-254]", + label: "IP address", + immutableText: "192.168.0.", + placeholder: "[1-254]", + }, +}; diff --git a/src/lib/elements/LimitedInput/LimitedInput.test.tsx b/src/lib/elements/LimitedInput/LimitedInput.test.tsx new file mode 100644 index 0000000..5f383c6 --- /dev/null +++ b/src/lib/elements/LimitedInput/LimitedInput.test.tsx @@ -0,0 +1,38 @@ +import { render, screen } from "@testing-library/react"; + +import { LimitedInput } from "./LimitedInput"; + +const { getComputedStyle } = window; + +beforeAll(() => { + // getComputedStyle is not implemeneted in jsdom, so we need to do this. + window.getComputedStyle = (elt) => getComputedStyle(elt); +}); + +afterAll(() => { + // Reset to original implementation + window.getComputedStyle = getComputedStyle; +}); + +it("renders without crashing", async () => { + render(); + + expect( + screen.getByRole("textbox", { name: "Limited input" }), + ).toBeInTheDocument(); +}); + +it("sets the --immutable css variable to the provided immutable text", async () => { + const { rerender } = render( + , + ); + + rerender( + , + ); + + expect( + screen.getByRole("textbox", { name: "Limited input" }).parentElement + ?.parentElement, + ).toHaveStyle(`--immutable: "Some text";`); +}); diff --git a/src/lib/elements/LimitedInput/LimitedInput.tsx b/src/lib/elements/LimitedInput/LimitedInput.tsx new file mode 100644 index 0000000..85f242b --- /dev/null +++ b/src/lib/elements/LimitedInput/LimitedInput.tsx @@ -0,0 +1,59 @@ +import { RefObject, useEffect, useRef } from "react"; + +import { Input, InputProps } from "@canonical/react-components"; +import "./LimitedInput.scss"; +import classNames from "classnames"; + +export type LimitedInputProps = Omit & { + immutableText: string; +}; + +/** + * An input component with a fixed prefix that cannot be edited. + * + * @param immutableText The prefixed text that cannot be edited. + * @returns A LimitedInput component. + */ +export const LimitedInput = ({ + immutableText, + ...props +}: LimitedInputProps) => { + const limitedInputRef: RefObject = useRef(null); + + const inputWrapper = limitedInputRef.current?.firstElementChild; + + useEffect(() => { + if (inputWrapper) { + console.log("AHAHAHAHAHHA"); + if (props.label) { + inputWrapper.setAttribute( + "style", + `--top: 2.5rem; --immutable: "${immutableText}"`, + ); + } else { + inputWrapper.setAttribute("style", `--immutable: "${immutableText}"`); + } + + const width = window.getComputedStyle(inputWrapper, ":before").width; + + inputWrapper.lastElementChild?.firstElementChild?.setAttribute( + "style", + `padding-left: ${width}`, + ); + } + }, [inputWrapper]); + + return ( +
+ +
+ ); +}; diff --git a/src/lib/elements/LimitedInput/index.ts b/src/lib/elements/LimitedInput/index.ts new file mode 100644 index 0000000..55e5e65 --- /dev/null +++ b/src/lib/elements/LimitedInput/index.ts @@ -0,0 +1 @@ +export * from "./LimitedInput"; diff --git a/src/lib/elements/index.ts b/src/lib/elements/index.ts index d1a5d75..348c5c2 100644 --- a/src/lib/elements/index.ts +++ b/src/lib/elements/index.ts @@ -2,4 +2,5 @@ export * from "./Meter"; export * from "./ExternalLink"; export * from "./ProgressIndicator"; -export * from "./Placeholder"; \ No newline at end of file +export * from "./Placeholder"; +export * from "./LimitedInput"; \ No newline at end of file From c671a02a4a962279c9c3f66f9a222b4741ab04d9 Mon Sep 17 00:00:00 2001 From: Nick De Villiers Date: Tue, 30 Apr 2024 11:24:58 +0100 Subject: [PATCH 2/3] fix: remove console log debug --- src/lib/elements/LimitedInput/LimitedInput.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/elements/LimitedInput/LimitedInput.tsx b/src/lib/elements/LimitedInput/LimitedInput.tsx index 85f242b..c4179b3 100644 --- a/src/lib/elements/LimitedInput/LimitedInput.tsx +++ b/src/lib/elements/LimitedInput/LimitedInput.tsx @@ -24,7 +24,6 @@ export const LimitedInput = ({ useEffect(() => { if (inputWrapper) { - console.log("AHAHAHAHAHHA"); if (props.label) { inputWrapper.setAttribute( "style", From fb9167601ad9da9e6a14bb45a4b3e3ce6c3aa8cd Mon Sep 17 00:00:00 2001 From: Nick De Villiers Date: Tue, 30 Apr 2024 13:27:46 +0100 Subject: [PATCH 3/3] fix: immutable text not rendering in docs (and when used in maas-ui) --- src/lib/elements/LimitedInput/LimitedInput.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib/elements/LimitedInput/LimitedInput.tsx b/src/lib/elements/LimitedInput/LimitedInput.tsx index c4179b3..f499f48 100644 --- a/src/lib/elements/LimitedInput/LimitedInput.tsx +++ b/src/lib/elements/LimitedInput/LimitedInput.tsx @@ -20,11 +20,12 @@ export const LimitedInput = ({ }: LimitedInputProps) => { const limitedInputRef: RefObject = useRef(null); - const inputWrapper = limitedInputRef.current?.firstElementChild; - useEffect(() => { + const inputWrapper = limitedInputRef.current?.firstElementChild; if (inputWrapper) { if (props.label) { + // CSS variable "--immutable" is the content of the :before element, which shows the immutable octets + // "--top" is the `top` property of the :before element, which is adjusted if there is a label to prevent overlap inputWrapper.setAttribute( "style", `--top: 2.5rem; --immutable: "${immutableText}"`, @@ -35,12 +36,14 @@ export const LimitedInput = ({ const width = window.getComputedStyle(inputWrapper, ":before").width; + // Adjust the left padding of the input to be the same width as the immutable octets. + // This displays the user input and the unchangeable text together as one IP address. inputWrapper.lastElementChild?.firstElementChild?.setAttribute( "style", `padding-left: ${width}`, ); } - }, [inputWrapper]); + }, [limitedInputRef, immutableText, props.label]); return (