diff --git a/CHANGELOG.md b/CHANGELOG.md index f8c0d784..6c2cc8c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.0.32 + +- Updated SearchInput component with debounce functionality + ## 3.0.31 - Fix linting error and warnings diff --git a/docs/storybook/SearchInput.md b/docs/storybook/SearchInput.md new file mode 100644 index 00000000..6926daa4 --- /dev/null +++ b/docs/storybook/SearchInput.md @@ -0,0 +1,42 @@ +### Overview + +- The SearchInput component is a versatile input field that supports both controlled and uncontrolled modes with debounce functionality. + +### Interaction + +- Users can enter search terms into the input field. If the `type` prop is not specified, it defaults to a search field, which browsers style differently to indicate its purpose. +- When the input is having search term, a clear icon appears to help users reset their search. + +### Context and Usage + +The SearchInput component supports both controlled and uncontrolled modes: +- It provides flexible input functionality with debounce support and can be used in both controlled and uncontrolled modes. +- Use the `onSearch` handler to manage API calls, which will be triggered based on the specified debounce timeout. + +#### Common Behavior + +- The `onChange` handler triggers on every character change. +- The debounce feature, with a default delay of 0 milliseconds, can be customized using the `debounceTimeout` prop. +- The `onSearch` function is called after the debounce delay, allowing for API calls. + +#### Controlled Mode + +- Use the `value` prop to control the input. + +#### Uncontrolled Mode + +- Use the `defaultValue` prop to set the initial input value. + +### Guidance + +- The `onChange` handler functions the same as in a standard input field. +- All props related to the MUI TextField component are supported. +- New props added for managing the debounce feature: `debounceTimeout` and `onSearch`. + +## NOTICE + +This work is licensed under the [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/legalcode). + +- SPDX-License-Identifier: CC-BY-4.0 +- SPDX-FileCopyrightText: Copyright (c) 2024 Contributors to the Eclipse Foundation +- Source URL: https://github.com/eclipse-tractusx/portal-shared-components diff --git a/jest.config.ts b/jest.config.ts index dae05643..2b4d17bb 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -24,4 +24,13 @@ module.exports = { transform: { '^.+\\.tsx?$': 'ts-jest', }, + moduleNameMapper: { + '\\.(css|scss)$': '/src/test/__mocks__/styleMock.ts', + '\\.svg$': '/src/test/__mocks__/fileMock.ts', + '@mui/material/styles/createPalette': + '/src/test/__mocks__/createPalette.ts', + '@mui/material/styles/createTypography': + '/src/test/__mocks__/createTypography.ts', + }, + setupFiles: ['/src/test/setupTests.ts'], } diff --git a/package.json b/package.json index bd987e68..4fa42a03 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@catena-x/portal-shared-components", - "version": "3.0.31", + "version": "3.0.32", "description": "Catena-X Portal Shared Components", "author": "Catena-X Contributors", "license": "Apache-2.0", diff --git a/src/components/basic/SearchInput/SearchInput.stories.tsx b/src/components/basic/SearchInput/SearchInput.stories.tsx index 08a30188..375591c7 100644 --- a/src/components/basic/SearchInput/SearchInput.stories.tsx +++ b/src/components/basic/SearchInput/SearchInput.stories.tsx @@ -18,15 +18,22 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ +import { useState } from 'react' import { type ComponentStory } from '@storybook/react' import { SearchInput as Component } from '.' +import searchInputDoc from '../../../../docs/storybook/SearchInput.md?raw' export default { - title: 'Form', + title: 'Form/SearchInput', component: Component, tags: ['autodocs'], argTypes: {}, + parameters: { + docs: { + description: { component: searchInputDoc }, + }, + }, } const Template: ComponentStory = ( @@ -35,3 +42,20 @@ const Template: ComponentStory = ( export const SearchInput = Template.bind({}) SearchInput.args = {} + +export const SearchInputWithDebounce = () => { + const [term, setTerm] = useState('') + + return ( + { + console.log('onSearch', v) + }} + onChange={(e) => { + setTerm(e.target.value) + }} + /> + ) +} diff --git a/src/components/basic/SearchInput/SearchInput.test.tsx b/src/components/basic/SearchInput/SearchInput.test.tsx new file mode 100644 index 00000000..6a420b2f --- /dev/null +++ b/src/components/basic/SearchInput/SearchInput.test.tsx @@ -0,0 +1,125 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +import { useState } from 'react' +import { fireEvent, screen, waitFor } from '@testing-library/react' +import { SearchInput } from '.' +import { render } from '../../../test/testUtils' + +jest.useFakeTimers() + +describe('SearchInput', () => { + const debounceTimeout = 300 + const onSearch = jest.fn() + const onChange = jest.fn() + + const ControlledSearchInput = () => { + const [value, setValue] = useState('') + return ( + setValue(e.target.value)} + placeholder="Search" + /> + ) + } + const UncontrolledSearchInput = () => ( + + ) + + beforeEach(() => { + jest.clearAllMocks() + }) + + test('renders the Search Input component', () => { + render() + }) + + test('should call search function after specified debounce timeout', async () => { + render() + const input = screen.getByPlaceholderText('Search') + fireEvent.change(input, { target: { value: 'te' } }) + fireEvent.change(input, { target: { value: 'testing...' } }) + expect(onSearch).not.toHaveBeenCalled() + expect(onChange).toHaveBeenCalledTimes(2) + + jest.advanceTimersByTime(debounceTimeout) + await waitFor(() => expect(onSearch).toHaveBeenCalledWith('testing...')) + expect(onSearch).toHaveBeenCalledTimes(1) + }) + + test('should call onChange handler when input value changes', () => { + render() + + const input = screen.getByPlaceholderText('Search') + fireEvent.change(input, { target: { value: 't' } }) + expect(onChange).toHaveBeenCalledTimes(1) + fireEvent.change(input, { target: { value: 'te' } }) + expect(onChange).toHaveBeenCalledTimes(2) + fireEvent.change(input, { target: { value: 'test' } }) + expect(onChange).toHaveBeenCalledTimes(3) + }) + + test('should call debounced search function with new value after reset', async () => { + render() + + const input = screen.getByPlaceholderText('Search') + fireEvent.change(input, { target: { value: 'test' } }) + + jest.advanceTimersByTime(debounceTimeout) + await waitFor(() => expect(onSearch).toHaveBeenCalledWith('test')) + + fireEvent.change(input, { target: { value: '' } }) + + jest.advanceTimersByTime(debounceTimeout) + await waitFor(() => expect(onSearch).toHaveBeenCalledWith('')) + }) + + test('should not call search function on initial render when value is provided', () => { + render() + + jest.advanceTimersByTime(debounceTimeout) + expect(onChange).not.toHaveBeenCalled() + }) + + test('should call search function when controlled value changes', async () => { + const { rerender } = render( + + ) + + rerender( + + ) + + jest.advanceTimersByTime(debounceTimeout) + await waitFor(() => expect(onSearch).toHaveBeenCalledWith('updated')) + }) +}) diff --git a/src/components/basic/SearchInput/index.tsx b/src/components/basic/SearchInput/index.tsx index 1f05bff5..31b03c2c 100644 --- a/src/components/basic/SearchInput/index.tsx +++ b/src/components/basic/SearchInput/index.tsx @@ -18,10 +18,11 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ +import { useCallback, useEffect, useState } from 'react' import SearchIcon from '@mui/icons-material/Search' import { Box, - type IconProps, + debounce, TextField, type TextFieldProps, useTheme, @@ -29,16 +30,52 @@ import { interface SearchProps extends Omit { variant?: 'outlined' - endAdornment?: IconProps + endAdornment?: React.ReactNode + debounceTimeout?: number + onSearch?: (value: string) => void } export const SearchInput = ({ + debounceTimeout = 0, + onSearch, variant, endAdornment, ...props }: SearchProps) => { const theme = useTheme() const { icon01 } = theme.palette.icon + const { value, onChange } = props + + const [isMounted, setIsMounted] = useState(false) + + useEffect(() => { + setIsMounted(true) + }, []) + + // Memoize the debounced function + const debouncedSearch = useCallback( + debounce((query: string) => { + onSearch?.(query) + }, debounceTimeout), + [] + ) + + // Handle debounce when input is controlled + useEffect(() => { + if (isMounted && value !== undefined) { + debouncedSearch(value as string) + } + }, [value]) + + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value + + // Handle debounce when input is uncontrolled + if (value === undefined) { + debouncedSearch(newValue) + } + onChange?.(e) + } return ( @@ -52,9 +89,10 @@ export const SearchInput = ({ type={props.type ?? 'search'} InputProps={{ startAdornment: , - endAdornment: endAdornment === undefined ? endAdornment : null, + endAdornment: endAdornment ?? null, }} {...props} + onChange={handleChange} /> ) diff --git a/src/test/__mocks__/createPalette.ts b/src/test/__mocks__/createPalette.ts new file mode 100644 index 00000000..9a4a5094 --- /dev/null +++ b/src/test/__mocks__/createPalette.ts @@ -0,0 +1,22 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +import { type Palette } from '@mui/material' +const createPalette: (palette: Palette) => Palette = (palette) => palette +export default createPalette diff --git a/src/test/__mocks__/createTypography.ts b/src/test/__mocks__/createTypography.ts new file mode 100644 index 00000000..2c9b5628 --- /dev/null +++ b/src/test/__mocks__/createTypography.ts @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +import { type Palette } from '@mui/material' +import { type TypographyOptions } from '@mui/material/styles/createTypography' + +const createTypography: ( + palette: Palette, + typography: TypographyOptions +) => TypographyOptions = (palette, typography) => ({ + ...palette, + ...typography, +}) +export default createTypography diff --git a/src/test/__mocks__/fileMock.ts b/src/test/__mocks__/fileMock.ts new file mode 100644 index 00000000..bd7ebc5c --- /dev/null +++ b/src/test/__mocks__/fileMock.ts @@ -0,0 +1,22 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +module.exports = { + default: 'default-mocking-for-file', +} diff --git a/src/test/__mocks__/styleMock.ts b/src/test/__mocks__/styleMock.ts new file mode 100644 index 00000000..3864d597 --- /dev/null +++ b/src/test/__mocks__/styleMock.ts @@ -0,0 +1,20 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +module.exports = {} diff --git a/src/test/setupTests.ts b/src/test/setupTests.ts new file mode 100644 index 00000000..878f4bb2 --- /dev/null +++ b/src/test/setupTests.ts @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +window.matchMedia = + window.matchMedia || + function () { + return { + matches: false, + addListener: function () {}, + removeListener: function () {}, + } + } diff --git a/src/test/testUtils.tsx b/src/test/testUtils.tsx new file mode 100644 index 00000000..5e7bceb2 --- /dev/null +++ b/src/test/testUtils.tsx @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +import React, { type ReactElement } from 'react' +import { SharedThemeProvider } from '../components' +import { render as rtlRender, type RenderOptions } from '@testing-library/react' + +const AllTheProviders = ({ children }: { children: React.ReactNode }) => { + return {children} +} + +export const render = ( + ui: ReactElement, + options?: Omit +) => rtlRender(ui, { wrapper: AllTheProviders, ...options })