Skip to content

Commit

Permalink
refactor: PrefixedInput input set text method MAASENG-3116 (#5425)
Browse files Browse the repository at this point in the history
  • Loading branch information
petermakowski authored May 7, 2024
1 parent 518968f commit 8e595e1
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 79 deletions.
37 changes: 22 additions & 15 deletions src/app/base/components/PrefixedInput/PrefixedInput.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
/* eslint-disable testing-library/no-node-access */
import { render, screen } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";

import PrefixedInput from "./PrefixedInput";

const { getComputedStyle } = window;

beforeAll(() => {
// getComputedStyle is not implemeneted in jsdom, so we need to do this.
window.getComputedStyle = (elt) => getComputedStyle(elt);
Element.prototype.getBoundingClientRect = vi.fn(() => ({
width: 100,
height: 0,
x: 0,
y: 0,
top: 0,
right: 0,
bottom: 0,
left: 0,
toJSON: vi.fn(),
}));
});

afterAll(() => {
// Reset to original implementation
window.getComputedStyle = getComputedStyle;
vi.restoreAllMocks();
});

it("renders without crashing", async () => {
Expand All @@ -25,18 +31,19 @@ it("renders without crashing", async () => {
).toBeInTheDocument();
});

it("sets the --immutable css variable to the provided immutable text", async () => {
const { rerender } = render(
it("displays the immutable text", async () => {
render(
<PrefixedInput aria-label="Limited input" immutableText="Some text" />
);

rerender(
expect(screen.getByText("Some text")).toBeInTheDocument();
});

it("adjusts input padding", async () => {
render(
<PrefixedInput aria-label="Limited input" immutableText="Some text" />
);
const inputElement = screen.getByRole("textbox", { name: "Limited input" });

// Direct node access is needed here to check the CSS variable
expect(
screen.getByRole("textbox", { name: "Limited input" }).parentElement
?.parentElement
).toHaveStyle(`--immutable: "Some text";`);
await waitFor(() => expect(inputElement).toHaveStyle("padding-left: 100px"));
});
62 changes: 29 additions & 33 deletions src/app/base/components/PrefixedInput/PrefixedInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { RefObject } from "react";
import { useEffect, useRef } from "react";
import { useLayoutEffect, useRef } from "react";

import type { InputProps } from "@canonical/react-components";
import { Input } from "@canonical/react-components";
Expand All @@ -9,45 +8,42 @@ export type PrefixedInputProps = Omit<InputProps, "type"> & {
immutableText: string;
};

// TODO: Upstream to maas-react-components https://warthogs.atlassian.net/browse/MAASENG-3113
const PrefixedInput = ({ immutableText, ...props }: PrefixedInputProps) => {
const prefixedInputRef: RefObject<HTMLDivElement> = useRef(null);
const prefixTextRef = useRef<HTMLDivElement>(null);
const inputWrapperRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const inputWrapper = prefixedInputRef.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}"`
);
} else {
inputWrapper.setAttribute("style", `--immutable: "${immutableText}"`);
}

const width = window.getComputedStyle(inputWrapper, ":before").width;
useLayoutEffect(() => {
const prefixElement = prefixTextRef.current;
const inputElement = inputWrapperRef.current?.querySelector("input");

if (prefixElement && inputElement) {
// 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
.querySelector("input")
?.setAttribute("style", `padding-left: ${width}`);
const prefixWidth = prefixElement.getBoundingClientRect().width;
inputElement.style.paddingLeft = `${prefixWidth}px`;
}
}, [prefixedInputRef, immutableText, props.label]);
}, [immutableText, props.label]);

return (
<div className="prefixed-input" ref={prefixedInputRef}>
<Input
className={classNames("prefixed-input__input", props.className)}
type="text"
wrapperClassName={classNames(
"prefixed-input__wrapper",
props.wrapperClassName
)}
{...props}
/>
<div
className={classNames("prefixed-input", {
"prefixed-input--with-label": !!props.label,
})}
>
<div className="prefixed-input__text" ref={prefixTextRef}>
{immutableText}
</div>
<div ref={inputWrapperRef}>
<Input
className={classNames("prefixed-input__input", props.className)}
type="text"
wrapperClassName={classNames(
"prefixed-input__wrapper",
props.wrapperClassName
)}
{...props}
/>
</div>
</div>
);
};
Expand Down
13 changes: 8 additions & 5 deletions src/app/base/components/PrefixedInput/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@
.prefixed-input {
position: relative;

&__wrapper::before {
.prefixed-input__text {
position: absolute;
pointer-events: none;
padding-left: $spv--small;
padding-bottom: calc(0.4rem - 1px);
padding-top: calc(0.4rem - 1px);
// TODO: Investigate replacement for using these variables https://warthogs.atlassian.net/browse/MAASENG-3116
content: var(--immutable, "");
top: var(--top, "inherit");
}

&--with-label {
.prefixed-input__text {
top: 2.5rem;
}
}
}
}
}
26 changes: 0 additions & 26 deletions src/app/base/components/PrefixedIpInput/PrefixedIpInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,6 @@ import PrefixedIpInput from "./PrefixedIpInput";

import { renderWithBrowserRouter } from "@/testing/utils";

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("displays the correct range help text for a subnet", () => {
render(
<Formik initialValues={{}} onSubmit={vi.fn()}>
Expand All @@ -31,20 +19,6 @@ it("displays the correct range help text for a subnet", () => {
expect(screen.getByText("10.0.0.[1-254]")).toBeInTheDocument();
});

it("sets the --immutable css variable to the immutable octets of the subnet", () => {
render(
<Formik initialValues={{}} onSubmit={vi.fn()}>
<PrefixedIpInput aria-label="IP address" cidr="10.0.0.0/24" name="ip" />
</Formik>
);

// Direct node access is needed here to check the CSS variable
expect(
screen.getByRole("textbox", { name: "IP address" }).parentElement
?.parentElement
).toHaveStyle(`--immutable: "10.0.0."`);
});

it("displays the correct placeholder for a subnet", () => {
render(
<Formik initialValues={{}} onSubmit={vi.fn()}>
Expand Down

0 comments on commit 8e595e1

Please sign in to comment.