From c3ab4920878a363660b31e5018b04c07420f2f8e Mon Sep 17 00:00:00 2001 From: hodanoori <107242553+hodanoori@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:42:55 +0200 Subject: [PATCH 1/2] fix(heureka): fixes embedded prop handling (#530) * fix(heureka): corrects embedded prop handling * chore(heureka): adds changeset * chore(heureka): sets embedded default value within the global store slice * chore(heureka): removes endpoints check from useQueryClientFn --- .changeset/eighty-steaks-wink.md | 6 ++++++ apps/heureka/src/App.jsx | 10 ++-------- apps/heureka/src/components/CustomAppShell.jsx | 5 +++-- apps/heureka/src/hooks/useQueryClientFn.js | 4 ++-- apps/heureka/src/lib/slices/createGlobalsSlice.js | 2 +- 5 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 .changeset/eighty-steaks-wink.md diff --git a/.changeset/eighty-steaks-wink.md b/.changeset/eighty-steaks-wink.md new file mode 100644 index 000000000..dc62cd2c9 --- /dev/null +++ b/.changeset/eighty-steaks-wink.md @@ -0,0 +1,6 @@ +--- +"@cloudoperators/juno-app-heureka": patch +"@cloudoperators/juno-app-greenhouse": patch +--- + +This fixes the embedded prop handling, ensuring it is passed correctly to AppShell. diff --git a/apps/heureka/src/App.jsx b/apps/heureka/src/App.jsx index 7c2fa38db..60e94249f 100644 --- a/apps/heureka/src/App.jsx +++ b/apps/heureka/src/App.jsx @@ -3,19 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useLayoutEffect } from "react" +import React from "react" import styles from "./styles.scss?inline" import { AppShellProvider, CodeBlock } from "@cloudoperators/juno-ui-components" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { MessagesProvider } from "@cloudoperators/juno-messages-provider" import AsyncWorker from "./components/AsyncWorker" import { ErrorBoundary } from "react-error-boundary" -import { useGlobalsActions, StoreProvider } from "./components/StoreProvider" +import { StoreProvider } from "./components/StoreProvider" import PanelManager from "./components/shared/PanelManager" import CustomAppShell from "./components/CustomAppShell" function App(props = {}) { - const { setEmbedded, setApiEndpoint } = useGlobalsActions() const preErrorClasses = ` custom-error-pre border-theme-error @@ -24,11 +23,6 @@ function App(props = {}) { w-full ` - useLayoutEffect(() => { - setApiEndpoint(props.endpoint) - if (props.embedded === "true" || props.embedded === true) setEmbedded(true) - }, []) - const fallbackRender = ({ error }) => { return (
diff --git a/apps/heureka/src/components/CustomAppShell.jsx b/apps/heureka/src/components/CustomAppShell.jsx index 490ecf6eb..7ac694b70 100644 --- a/apps/heureka/src/components/CustomAppShell.jsx +++ b/apps/heureka/src/components/CustomAppShell.jsx @@ -5,7 +5,7 @@ import React from "react" import { AppShell, PageHeader, TopNavigation, TopNavigationItem } from "@cloudoperators/juno-ui-components" -import { useGlobalsActions, useGlobalsActiveView } from "./StoreProvider" +import { useGlobalsActions, useGlobalsActiveView, useGlobalsEmbedded } from "./StoreProvider" import ServicesView from "./services/ServicesView" import IssueMatchesView from "./issueMatches/IssueMatchesView" import ComponentsView from "./components/ComponentsView" @@ -21,6 +21,7 @@ const VIEW_CONFIG = { const CustomAppShell = ({ children }) => { const { setActiveView, setShowPanel } = useGlobalsActions() const activeView = useGlobalsActiveView() + const embedded = useGlobalsEmbedded() const handleNavItemChange = (item) => { setActiveView(item) @@ -40,7 +41,7 @@ const CustomAppShell = ({ children }) => { const ActiveComponent = VIEW_CONFIG[activeView]?.component return ( - } topNavigation={topNavigation}> + } topNavigation={topNavigation} embedded={embedded}> {ActiveComponent && } {children} diff --git a/apps/heureka/src/hooks/useQueryClientFn.js b/apps/heureka/src/hooks/useQueryClientFn.js index 49361d265..b569950b4 100644 --- a/apps/heureka/src/hooks/useQueryClientFn.js +++ b/apps/heureka/src/hooks/useQueryClientFn.js @@ -24,7 +24,7 @@ const useQueryClientFn = () => { As stated in getQueryDefaults, the order of registration of query defaults does matter. Since the first matching defaults are returned by getQueryDefaults, the registration should be made in the following order: from the least generic key to the most generic one. This way, in case of specific key, the first matching one would be the expected one. */ useEffect(() => { - if (!queryClient || !endpoint) return + if (!queryClient) return // Services main query queryClient.setQueryDefaults(["ServicesMain"], { @@ -135,7 +135,7 @@ const useQueryClientFn = () => { // Set queryClientFnReady to true once setQueryClientFnReady(true) - }, [queryClient, endpoint]) + }, [queryClient]) } export default useQueryClientFn diff --git a/apps/heureka/src/lib/slices/createGlobalsSlice.js b/apps/heureka/src/lib/slices/createGlobalsSlice.js index 8aedd6100..7dac0dca5 100644 --- a/apps/heureka/src/lib/slices/createGlobalsSlice.js +++ b/apps/heureka/src/lib/slices/createGlobalsSlice.js @@ -8,7 +8,7 @@ import constants from "../../components/shared/constants" const createGlobalsSlice = (set, get, options) => ({ globals: { - embedded: false, //Set to true if app is to be embedded in another existing app or page. + embedded: options?.embedded === true || options?.embedded === "true", //Set to true if app is to be embedded in another existing app or page. apiEndpoint: options?.apiEndpoint, //The API endpoint to use for fetching data. isUrlStateSetup: false, //Set to true when the URL state has been set up. queryClientFnReady: false, //Set to true when the queryClient function is ready to be used. From 436f35a28330097f38e7c985a9f6a5ceff1d143f Mon Sep 17 00:00:00 2001 From: Guoda <121792659+guoda-puidokaite@users.noreply.github.com> Date: Fri, 18 Oct 2024 09:09:17 +0200 Subject: [PATCH 2/2] chore(ui): migrate Search Input component to Typescript (#528) * chore(ui): convert SearchInput to Typescript and add types * chore(ui): remove deprecated code and change tests to vitest * chore(ui): update docs, tests and other small changes * chore(ui): add deprecated code for Filter component tests * chore(ui): generate changeset and fix test requiring .js component * chore(ui): remove deprecated .js story * chore(ui): refactored existing code * chore(ui): improve tests * chore(ui): improve conditional rendering * fix(ui): add missing newline * chore(ui): fix conditional rendering and update docs with removing outdated comment * chore(ui): fix conditional check --------- Co-authored-by: Esther Schmitz --- .changeset/perfect-socks-beam.md | 5 + .../src/components/Filters/Filters.test.js | 2 +- .../JsonViewer/JsonViewer.component.js | 2 +- .../SearchInput/SearchInput.component.tsx | 225 ++++++++++++++++++ ...put.stories.js => SearchInput.stories.tsx} | 2 +- .../SearchInput/SearchInput.test.tsx | 160 +++++++++++++ .../SearchInput/{index.js => index.ts} | 0 .../components/SearchInput/searchinput.scss | 10 +- .../SearchInput/SearchInput.component.js | 0 .../SearchInput/SearchInput.test.js | 0 .../src/deprecated_js/SearchInput/index.js | 6 + .../SearchInput/searchinput.scss | 6 + 12 files changed, 411 insertions(+), 7 deletions(-) create mode 100644 .changeset/perfect-socks-beam.md create mode 100644 packages/ui-components/src/components/SearchInput/SearchInput.component.tsx rename packages/ui-components/src/components/SearchInput/{SearchInput.stories.js => SearchInput.stories.tsx} (91%) create mode 100644 packages/ui-components/src/components/SearchInput/SearchInput.test.tsx rename packages/ui-components/src/components/SearchInput/{index.js => index.ts} (100%) rename packages/ui-components/src/{components => deprecated_js}/SearchInput/SearchInput.component.js (100%) rename packages/ui-components/src/{components => deprecated_js}/SearchInput/SearchInput.test.js (100%) create mode 100644 packages/ui-components/src/deprecated_js/SearchInput/index.js create mode 100644 packages/ui-components/src/deprecated_js/SearchInput/searchinput.scss diff --git a/.changeset/perfect-socks-beam.md b/.changeset/perfect-socks-beam.md new file mode 100644 index 000000000..14e8d53ad --- /dev/null +++ b/.changeset/perfect-socks-beam.md @@ -0,0 +1,5 @@ +--- +"@cloudoperators/juno-ui-components": minor +--- + +Migrate the Search Input component to TypeScript diff --git a/packages/ui-components/src/components/Filters/Filters.test.js b/packages/ui-components/src/components/Filters/Filters.test.js index 0565bc20a..9620854f9 100644 --- a/packages/ui-components/src/components/Filters/Filters.test.js +++ b/packages/ui-components/src/components/Filters/Filters.test.js @@ -7,7 +7,7 @@ import * as React from "react" import { render, screen } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { Filters } from "./index" -import { SearchInput } from "../SearchInput/index" +import { SearchInput } from "../../deprecated_js/SearchInput" describe("Filters", () => { test("renders Filters", async () => { diff --git a/packages/ui-components/src/components/JsonViewer/JsonViewer.component.js b/packages/ui-components/src/components/JsonViewer/JsonViewer.component.js index 3abdd0a22..fa7da3576 100644 --- a/packages/ui-components/src/components/JsonViewer/JsonViewer.component.js +++ b/packages/ui-components/src/components/JsonViewer/JsonViewer.component.js @@ -6,7 +6,7 @@ import PropTypes from "prop-types" import React, { useContext, useLayoutEffect } from "react" import * as themes from "./themes" -import { SearchInput } from "../SearchInput/SearchInput.component" +import { SearchInput } from "../../deprecated_js/SearchInput" // DEFAULT THEME (DARK) const DEFAULT_THEME = { diff --git a/packages/ui-components/src/components/SearchInput/SearchInput.component.tsx b/packages/ui-components/src/components/SearchInput/SearchInput.component.tsx new file mode 100644 index 000000000..9794c38ec --- /dev/null +++ b/packages/ui-components/src/components/SearchInput/SearchInput.component.tsx @@ -0,0 +1,225 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect, ChangeEvent, KeyboardEvent, MouseEvent, useCallback } from "react" + +import { Icon } from "../Icon" +import { Stack } from "../Stack" + +import "./searchinput.scss" + +export interface SearchInputProps { + /** + * Specifies the name attribute for the input element. + */ + name?: string + /** + * Determines the visual styling variant of the SearchInput component. + * - "default": Standard search input styling. + * - "hero": A larger search input intended for standalone use on a dedicated search page, akin to the initial Google search page. + * - "rounded": A search input with rounded edges. + */ + variant?: "rounded" | "hero" | "default" + /** + * Disables the search input when set to true. + */ + disabled?: boolean + /** + * Custom placeholder text displayed in the search input. + */ + placeholder?: string + /** + * Initial value for the search input. + */ + value?: string + /** + * Controls the autocomplete attribute of the input element. + * Pass a valid autocomplete value. + * We do not enforce validity. + */ + autoComplete?: string + /** + * Determines whether to show the 'Clear' button. + */ + clear?: boolean + /** + * Pass an optional CSS class to apply to the search input. + */ + className?: string + /** + * Callback function invoked when a search is triggered, either by pressing the 'Enter' key or by clicking the search icon. + */ + onSearch?: (_value: string) => void + /** + * Click handler for the search icon. + */ + onClick?: (_event: MouseEvent) => void + /** + * Change handler for the search input. + */ + onChange?: (_event: ChangeEvent) => void + /** + * KeyPress handler for the search input. By default, triggers the onSearch function when the 'Enter' key is pressed. + */ + onKeyPress?: (_event: KeyboardEvent) => void + /** + * Click handler for the 'Clear' button. + */ + onClear?: (_event: MouseEvent) => void +} + +const getWrapperStyles = (variant: "rounded" | "hero" | "default"): string => { + const baseStyles = "jn-relative jn-inline-block jn-win-max" + switch (variant) { + case "rounded": + return `${baseStyles} jn-w-auto` + case "hero": + return `${baseStyles} jn-w-full` + default: + return `${baseStyles} jn-w-auto` + } +} + +const getSearchStyles = (variant: "rounded" | "hero" | "default"): string => { + const baseStyles = ` + jn-bg-theme-textinput + jn-text-theme-high + jn-shadow + jn-w-full + focus:jn-outline-none + focus:jn-ring-2 + focus:jn-ring-theme-focus + disabled:jn-cursor-not-allowed + disabled:jn-opacity-50 + ` + + const roundedStyles = "jn-rounded-full focus:jn-rounded-full" + switch (variant) { + case "rounded": + return `${baseStyles} ${roundedStyles} jn-text-base jn-w-auto jn-pl-3 jn-pr-16 jn-py-1` + case "hero": + return `${baseStyles} ${roundedStyles} jn-text-lg jn-w-full jn-pl-6 jn-pr-20 jn-py-2.5` + default: + return `${baseStyles} jn-rounded jn-text-base jn-leading-4 jn-pl-4 jn-pr-16 jn-py-2.5` + } +} + +const getIconWrapperStyles = (variant: "rounded" | "hero" | "default"): string => { + switch (variant) { + case "rounded": + return "jn-absolute jn-inline-flex jn-right-3 jn-top-1" + case "hero": + return "jn-absolute jn-inline-flex jn-right-5" + default: + return "jn-absolute jn-inline-flex jn-right-3 jn-top-2" + } +} + +const getClearIconStyles = (variant: "rounded" | "hero" | "default"): string => { + switch (variant) { + case "hero": + return "jn-mr-2.5" + default: + return "jn-mr-2" + } +} + +const getClearIconSize = (variant: "rounded" | "hero" | "default"): string => { + return variant === "hero" ? "24" : "18" +} + +/** + * A SearchInput is a controlled input component for searching. + * It provides a text field to enter a search query and optional clear and search icons. + * Three styling variants are supported: "rounded", "hero", and "default". + */ +export const SearchInput: React.FC = ({ + value = "", + name = "search", + variant = "default", + disabled = false, + clear = true, + onSearch, + onChange, + onClick, + onKeyPress, + onClear, + autoComplete = "off", + placeholder = "Search…", + className = "", + ...props +}) => { + const [val, setValue] = useState(value) + + useEffect(() => { + setValue(value) + }, [value]) + + const handleInputChange = useCallback( + (event: ChangeEvent): void => { + setValue(event.target.value) + onChange?.(event) + }, + [onChange] + ) + + const handleKeyPress = useCallback( + (event: KeyboardEvent): void => { + if (event.key === "Enter" && onSearch) { + onSearch(val) + } + onKeyPress?.(event) + }, + [onSearch, onKeyPress, val] + ) + + const handleSearchClick = useCallback( + (event: MouseEvent): void => { + onSearch?.(val) + onClick?.(event) + }, + [onSearch, onClick, val] + ) + + const handleClearClick = useCallback( + (event: MouseEvent): void => { + setValue("") + onClear?.(event) + }, + [onClear] + ) + + return ( +
+ + +
+ {clear && val?.length > 0 && ( + + )} + +
+
+
+ ) +} diff --git a/packages/ui-components/src/components/SearchInput/SearchInput.stories.js b/packages/ui-components/src/components/SearchInput/SearchInput.stories.tsx similarity index 91% rename from packages/ui-components/src/components/SearchInput/SearchInput.stories.js rename to packages/ui-components/src/components/SearchInput/SearchInput.stories.tsx index ebeaf199f..ddf1b7dba 100644 --- a/packages/ui-components/src/components/SearchInput/SearchInput.stories.js +++ b/packages/ui-components/src/components/SearchInput/SearchInput.stories.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SearchInput } from "./index.js" +import { SearchInput } from "./" export default { title: "Components/SearchInput", diff --git a/packages/ui-components/src/components/SearchInput/SearchInput.test.tsx b/packages/ui-components/src/components/SearchInput/SearchInput.test.tsx new file mode 100644 index 000000000..966fe55e0 --- /dev/null +++ b/packages/ui-components/src/components/SearchInput/SearchInput.test.tsx @@ -0,0 +1,160 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as React from "react" +import { render, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { describe, expect, test, vi } from "vitest" +import { SearchInput } from "./" + +describe("SearchInput Component", () => { + describe("Basic Rendering", () => { + test("should render a valid HTML input type 'search'", () => { + render() + expect(screen.getByRole("searchbox")).toBeInTheDocument() + expect(screen.getByRole("searchbox")).toHaveAttribute("type", "search") + }) + }) + + describe("Placeholder Handling", () => { + test("should render a default placeholder 'Search…'", () => { + render() + expect(screen.getByRole("searchbox")).toHaveAttribute("placeholder", "Search…") + }) + + test("should render a specific placeholder when provided", () => { + render() + expect(screen.getByRole("searchbox")).toHaveAttribute("placeholder", "My custom placeholder") + }) + }) + + describe("Value Handling", () => { + test("should render a specific value when provided", () => { + render() + expect(screen.getByRole("searchbox")).toHaveValue("blah") + }) + + test("should render a Clear icon if the field has a value", () => { + render() + expect(screen.getByTitle("Clear")).toBeInTheDocument() + }) + + test("should update the input's value when typing", async () => { + render() + const input = screen.getByRole("searchbox") + await userEvent.type(input, "abc") + expect(input).toHaveValue("abc") + }) + }) + + describe("Attributes and ClassNames", () => { + test("should render with the default name 'search'", () => { + render() + expect(screen.getByRole("searchbox")).toHaveAttribute("name", "search") + }) + + test("should render with a specific name when provided", () => { + render() + expect(screen.getByRole("searchbox")).toHaveAttribute("name", "searchbox") + }) + + test("should apply all provided props", () => { + render() + expect(screen.getByRole("searchbox")).toHaveAttribute("name", "My shiny little Message") + }) + + test("should apply custom classNames when provided", () => { + render() + expect(screen.getByRole("search")).toHaveClass("my-custom-class") + }) + }) + + describe("Disabled State", () => { + test("should disable the search input when 'disabled' prop is passed", () => { + render() + expect(screen.getByRole("searchbox")).toBeDisabled() + }) + }) + + describe("Event Handlers", () => { + test("should trigger 'onSearch' handler when search button is clicked", async () => { + const handleSearch = vi.fn() + render() + await userEvent.click(screen.getByRole("button", { name: /search/i })) + expect(handleSearch).toHaveBeenCalledTimes(1) + }) + + test("should trigger 'onClick' handler when search button is clicked", async () => { + const handleClick = vi.fn() + render() + await userEvent.click(screen.getByRole("button", { name: /search/i })) + expect(handleClick).toHaveBeenCalledTimes(1) + }) + + test("should trigger both 'onClick' and 'onSearch' handlers when search button is clicked if both are provided", async () => { + const handleClick = vi.fn() + const handleSearch = vi.fn() + render() + await userEvent.click(screen.getByRole("button", { name: /search/i })) + expect(handleClick).toHaveBeenCalledTimes(1) + expect(handleSearch).toHaveBeenCalledTimes(1) + }) + + test("should trigger 'onSearch' handler when Enter key is pressed", async () => { + const handleSearch = vi.fn() + render() + await userEvent.type(screen.getByRole("searchbox"), "{enter}") + expect(handleSearch).toHaveBeenCalledTimes(1) + }) + + test("should not trigger 'onSearch' handler when keys other than Enter are pressed", async () => { + const handleSearch = vi.fn() + render() + await userEvent.type(screen.getByRole("searchbox"), "{shift}") + expect(handleSearch).toHaveBeenCalledTimes(0) + }) + + test("should trigger 'onKeyPress' handler when any key is pressed, including Enter", async () => { + const handleKeyPress = vi.fn() + render() + await userEvent.type(screen.getByRole("searchbox"), "{enter}abc") + expect(handleKeyPress).toHaveBeenCalledTimes(4) + }) + + test("should trigger 'onChange' handler as text is typed", async () => { + const handleChange = vi.fn() + render() + await userEvent.type(screen.getByRole("searchbox"), "abc") + expect(handleChange).toHaveBeenCalledTimes(3) + }) + + test("should trigger 'onClear' handler when Clear icon is clicked", async () => { + const handleClear = vi.fn() + render() + await userEvent.click(screen.getByTitle("Clear")) + expect(handleClear).toHaveBeenCalledTimes(1) + }) + }) + + describe("Clear Button Logic", () => { + test("should render a Clear icon if the field has a value", () => { + render() + expect(screen.getByTitle("Clear")).toBeInTheDocument() + }) + + test("should clear the input when the Clear icon is clicked", async () => { + render() + await userEvent.click(screen.getByTitle("Clear")) + expect(screen.getByRole("searchbox")).toHaveValue("") + }) + + test("should trigger 'onClear' handler when the Clear icon is clicked", async () => { + const handleClear = vi.fn() + render() + await userEvent.click(screen.getByTitle("Clear")) + expect(handleClear).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/ui-components/src/components/SearchInput/index.js b/packages/ui-components/src/components/SearchInput/index.ts similarity index 100% rename from packages/ui-components/src/components/SearchInput/index.js rename to packages/ui-components/src/components/SearchInput/index.ts diff --git a/packages/ui-components/src/components/SearchInput/searchinput.scss b/packages/ui-components/src/components/SearchInput/searchinput.scss index a8bfe21cc..e08f1f05b 100644 --- a/packages/ui-components/src/components/SearchInput/searchinput.scss +++ b/packages/ui-components/src/components/SearchInput/searchinput.scss @@ -1,6 +1,8 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors -// SPDX-License-Identifier: Apache-2.0 +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ .juno-search-input::-webkit-search-cancel-button { - -webkit-appearance: none; -} \ No newline at end of file + -webkit-appearance: none; +} diff --git a/packages/ui-components/src/components/SearchInput/SearchInput.component.js b/packages/ui-components/src/deprecated_js/SearchInput/SearchInput.component.js similarity index 100% rename from packages/ui-components/src/components/SearchInput/SearchInput.component.js rename to packages/ui-components/src/deprecated_js/SearchInput/SearchInput.component.js diff --git a/packages/ui-components/src/components/SearchInput/SearchInput.test.js b/packages/ui-components/src/deprecated_js/SearchInput/SearchInput.test.js similarity index 100% rename from packages/ui-components/src/components/SearchInput/SearchInput.test.js rename to packages/ui-components/src/deprecated_js/SearchInput/SearchInput.test.js diff --git a/packages/ui-components/src/deprecated_js/SearchInput/index.js b/packages/ui-components/src/deprecated_js/SearchInput/index.js new file mode 100644 index 000000000..3c1d79629 --- /dev/null +++ b/packages/ui-components/src/deprecated_js/SearchInput/index.js @@ -0,0 +1,6 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { SearchInput } from "./SearchInput.component" diff --git a/packages/ui-components/src/deprecated_js/SearchInput/searchinput.scss b/packages/ui-components/src/deprecated_js/SearchInput/searchinput.scss new file mode 100644 index 000000000..ec67207fd --- /dev/null +++ b/packages/ui-components/src/deprecated_js/SearchInput/searchinput.scss @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors +// SPDX-License-Identifier: Apache-2.0 + +.juno-search-input::-webkit-search-cancel-button { + -webkit-appearance: none; +}