diff --git a/package-lock.json b/package-lock.json index d9bdf3da59afc1f3de0178bdd0df0a359307b4a7..8f0b98286b83fb19ae4487eb4de8bff2cb6a9ea6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31106,6 +31106,7 @@ "version": "1.0.1", "license": "UNLICENSED", "dependencies": { + "@sikt/sds-button": "^2.0.0", "@sikt/sds-core": "^1.0.1", "@sikt/sds-icons": "^1.0.0" }, diff --git a/packages/input/Input.test.tsx b/packages/input/Input.test.tsx index 14b4b900499509ec8d031913e4e71dd697532f33..2b0713b1f279ea8ec5d776893e6978af48cf5cbd 100644 --- a/packages/input/Input.test.tsx +++ b/packages/input/Input.test.tsx @@ -102,15 +102,8 @@ describe("Input", () => { expect(errorEl).toHaveClass("sds-input__help-text"); }); - it("should have left icon element", async () => { - render( - <TextInput - label="Foo" - data-testid="test" - icon="icon" - iconPosition="start" - /> - ); + it("should have icon element", async () => { + render(<TextInput label="Foo" data-testid="test" icon="icon" />); const container = screen.getByTestId("test"); const icon = container.getElementsByClassName("sds-input__icon")[0]; @@ -121,23 +114,23 @@ describe("Input", () => { expect(screen.getByText("Foo")).toBeInTheDocument(); }); - it("should have right icon element", async () => { + it("should have action element", async () => { render( - <TextInput + <SearchInput label="Foo" data-testid="test" - icon="icon" - iconPosition="end" + actionProps={{ label: "Test label" }} /> ); const container = screen.getByTestId("test"); - const icon = container.getElementsByClassName("sds-input__icon")[0]; + const action = container.getElementsByClassName("sds-input__action")[0]; const input = container.getElementsByClassName("sds-input__input")[0]; - expect(icon.compareDocumentPosition(input)).toBe( + expect(action.compareDocumentPosition(input)).toBe( Node.DOCUMENT_POSITION_PRECEDING ); expect(screen.getByText("Foo")).toBeInTheDocument(); + expect(action).toHaveAccessibleName("Test label"); }); }); }); diff --git a/packages/input/Input.tsx b/packages/input/Input.tsx index aebf782415ddafff5ffad13ee07963201f1d5169..40d33aa7b03cff47addcc38832690020359b5fc3 100644 --- a/packages/input/Input.tsx +++ b/packages/input/Input.tsx @@ -16,6 +16,7 @@ import { PhoneIcon, WarningIcon, } from "@sikt/sds-icons"; +import { InputActionButton, InputActionButtonProps } from "./InputActionButton"; import "./input.pcss"; type InputTypes = @@ -43,7 +44,7 @@ export interface InputProps ) => void; value?: string; icon?: ReactNode; - iconPosition?: "start" | "end"; + actionProps?: Omit<InputActionButtonProps, "icon">; errorText?: string; helpText?: string; inputProps?: InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement>; @@ -62,7 +63,7 @@ const Input = forwardRef< onChange, value, icon, - iconPosition = "start", + actionProps, type, errorText, helpText, @@ -97,9 +98,7 @@ const Input = forwardRef< {label} </div> <div className="sds-input__wrapper"> - {iconPosition === "start" && icon && ( - <div className="sds-input__icon">{icon}</div> - )} + {icon && <div className="sds-input__icon">{icon}</div>} {type === "textarea" ? ( <textarea ref={ref as ForwardedRef<HTMLTextAreaElement>} @@ -129,8 +128,14 @@ const Input = forwardRef< {...inputProps} /> )} - {iconPosition === "end" && icon && ( - <div className="sds-input__icon">{icon}</div> + {type === "search" && ( + <InputActionButton + type="submit" + className="sds-input__action" + label="Søk" + icon={<MagnifyingGlassIcon />} + {...actionProps} + /> )} </div> </label> @@ -175,14 +180,6 @@ export const TelInput = forwardRef<HTMLInputElement, InputProps>( ); TelInput.displayName = "TelInput"; export const SearchInput = forwardRef<HTMLInputElement, InputProps>( - (props, ref) => ( - <Input - type="search" - icon={<MagnifyingGlassIcon />} - iconPosition="end" - ref={ref} - {...props} - /> - ) + (props, ref) => <Input type="search" ref={ref} {...props} /> ); SearchInput.displayName = "SearchInput"; diff --git a/packages/input/InputActionButton.test.tsx b/packages/input/InputActionButton.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..54bd21633ce892b73cb9573149d5b44dc469797f --- /dev/null +++ b/packages/input/InputActionButton.test.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { axe } from "jest-axe"; +import { InputActionButton } from "./InputActionButton"; +import { MagnifyingGlassIcon } from "@sikt/sds-icons"; + +describe("InputActionButton", () => { + describe("a11y", () => { + it("should be accessible", async () => { + const { container } = render( + <InputActionButton label="Foo" icon={<MagnifyingGlassIcon />} /> + ); + + expect(await axe(container)).toHaveNoViolations(); + }); + }); + + describe("api", () => { + it("should render", async () => { + render( + <InputActionButton + label="Foo" + icon={<MagnifyingGlassIcon />} + data-testid="test" + /> + ); + + expect(screen.getByTestId("test")).toHaveClass("sds-input-action"); + expect(screen.getByTestId("test")).toHaveAccessibleName("Foo"); + }); + + it("calls click handler", async () => { + const user = userEvent.setup(); + const clickHandler = jest.fn(); + render( + <InputActionButton + label="Foo" + icon={<MagnifyingGlassIcon />} + data-testid="test" + onClick={clickHandler} + /> + ); + + await user.click(screen.getByTestId("test")); + + expect(clickHandler).toHaveBeenCalled(); + }); + + it("should have class name", async () => { + render( + <InputActionButton + label="Foo" + icon={<MagnifyingGlassIcon />} + data-testid="test" + className="test-class-name" + /> + ); + + expect(screen.getByTestId("test")).toHaveClass( + "sds-input-action test-class-name" + ); + }); + }); +}); diff --git a/packages/input/InputActionButton.tsx b/packages/input/InputActionButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..394c2deb101f07200c939c8e3f8a4fe5d4e9802e --- /dev/null +++ b/packages/input/InputActionButton.tsx @@ -0,0 +1,27 @@ +import React, { ButtonHTMLAttributes, ReactNode } from "react"; +import clsx from "clsx"; +import { TertiaryButton } from "@sikt/sds-button"; +import "./input-action-button.pcss"; + +export interface InputActionButtonProps + extends ButtonHTMLAttributes<HTMLButtonElement> { + label: string; + icon: ReactNode; +} + +export const InputActionButton = ({ + className, + label, + icon, + ...rest +}: InputActionButtonProps) => ( + <TertiaryButton + type="button" + className={clsx("sds-input-action", className)} + aria-label={label} + title={label} + icon={icon} + iconType="only" + {...rest} + /> +); diff --git a/packages/input/input-action-button.pcss b/packages/input/input-action-button.pcss new file mode 100644 index 0000000000000000000000000000000000000000..96d161cb57d3626e70cd76e256843d0f6c3d0a96 --- /dev/null +++ b/packages/input/input-action-button.pcss @@ -0,0 +1,3 @@ +.sds-input-action .sds-button__icon { + font-size: var(--sds-size-text-m); +} diff --git a/packages/input/input.pcss b/packages/input/input.pcss index 3ed84acfa1c76dc9cb2b20f764b27bf446d9786b..d2a13c8cfeceeefd8dcc01c767761f4530f3914f 100644 --- a/packages/input/input.pcss +++ b/packages/input/input.pcss @@ -52,6 +52,11 @@ &:focus-visible { outline: none; } + + /* Hide native clear search button in Chrome and Safari */ + &::-webkit-search-cancel-button { + appearance: none; + } } &:hover { @@ -78,15 +83,23 @@ } } - &__icon { + &__icon, + &__action { align-items: center; display: inline-flex; font-size: var(--sds-size-text-m); justify-content: center; - width: var(--sds-size-text-m); height: 100%; } + &__icon { + width: var(--sds-size-text-m); + } + + &__action { + margin-right: calc(-1 * var(--sds-size-base-s)); + } + &--error { --sds-input-border-color: var(--sds-color-surface-danger); --sds-input-label-color: var(--sds-color-text-danger); diff --git a/packages/input/package.json b/packages/input/package.json index 94a914992e2d974dec643487c5da8ee76e5f8f6e..fa5756641abc72905c86abec10c528f0ad06afa6 100644 --- a/packages/input/package.json +++ b/packages/input/package.json @@ -13,6 +13,7 @@ "build": "rollup -c ../../rollup.config.mjs" }, "dependencies": { + "@sikt/sds-button": "^2.0.0", "@sikt/sds-core": "^1.0.1", "@sikt/sds-icons": "^1.0.0" }, diff --git a/packages/input/stories/EmailInput.stories.tsx b/packages/input/stories/EmailInput.stories.tsx index 66bd9704c2dec7cde5c0298af272239fae5d3a77..fa883b84490c0bda6aed0c535209a71771e0640e 100644 --- a/packages/input/stories/EmailInput.stories.tsx +++ b/packages/input/stories/EmailInput.stories.tsx @@ -23,7 +23,6 @@ export const WithCustomIcon: Story = { args: { ...Input.args, icon: <GearIcon />, - iconPosition: "end", }, }; diff --git a/packages/input/stories/PasswordInput.stories.tsx b/packages/input/stories/PasswordInput.stories.tsx index 5951dd0e051f9abbc3475b9793e3ac66c804a2fe..4d77d95418cf53187d6c93bd7c0fd610747dd113 100644 --- a/packages/input/stories/PasswordInput.stories.tsx +++ b/packages/input/stories/PasswordInput.stories.tsx @@ -23,7 +23,6 @@ export const WithCustomIcon: Story = { args: { ...Default.args, icon: <UserCircleIcon />, - iconPosition: "end", }, }; diff --git a/packages/input/stories/SearchInput.stories.tsx b/packages/input/stories/SearchInput.stories.tsx index 6b16ad0f52bbb6333dd36c3b0e2ecaf3f06d567e..c474290532c830535e8e9090b2036c09c36da2d1 100644 --- a/packages/input/stories/SearchInput.stories.tsx +++ b/packages/input/stories/SearchInput.stories.tsx @@ -23,7 +23,6 @@ export const WithCustomIcon: Story = { args: { ...Default.args, icon: <MapPinIcon />, - iconPosition: "start", }, }; @@ -34,3 +33,7 @@ export const WithHelpText: Story = { export const WithError: Story = { args: { ...Default.args, errorText: "Error!" }, }; + +export const WithCustomActionLabel: Story = { + args: { ...Default.args, actionProps: { label: "Finn resultater" } }, +}; diff --git a/packages/input/stories/TelInput.stories.tsx b/packages/input/stories/TelInput.stories.tsx index 1a9f31366ff239ae3544cc895b70261b36ac0501..b8cfc392837283c041c8e0bb4bc177573804fb40 100644 --- a/packages/input/stories/TelInput.stories.tsx +++ b/packages/input/stories/TelInput.stories.tsx @@ -23,7 +23,6 @@ export const WithCustomIcon: Story = { args: { ...Default.args, icon: <SlidersIcon />, - iconPosition: "end", }, };