-
Notifications
You must be signed in to change notification settings - Fork 4
feat(BBBSearch): add search component #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AtilaU19
wants to merge
1
commit into
bigbluebutton:develop
Choose a base branch
from
AtilaU19:task-35
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| # BBBSearch | ||
|
|
||
| A search input component with built-in debounce, clear button, and optional loading state. Designed to replace ad-hoc search implementations across the application with a consistent, accessible component. | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Uncontrolled (simplest) | ||
|
|
||
| ```tsx | ||
| import { BBBSearch } from 'bbb-ui-components-react'; | ||
|
|
||
| <BBBSearch | ||
| placeholder="Search participants..." | ||
| onSearch={(term) => filterParticipants(term)} | ||
| /> | ||
| ``` | ||
|
|
||
| ### With custom debounce | ||
|
|
||
| ```tsx | ||
| <BBBSearch | ||
| placeholder="Search messages..." | ||
| debounce={300} | ||
| onSearch={(term) => searchMessages(term)} | ||
| /> | ||
| ``` | ||
|
|
||
| ### Controlled | ||
|
|
||
| ```tsx | ||
| const [query, setQuery] = useState(''); | ||
|
|
||
| <BBBSearch | ||
| value={query} | ||
| onChange={setQuery} | ||
| onClear={() => setQuery('')} | ||
| placeholder="Filter..." | ||
| /> | ||
| ``` | ||
|
|
||
| ### With loading state | ||
|
|
||
| ```tsx | ||
| <BBBSearch | ||
| placeholder="Search..." | ||
| isLoading={isSearching} | ||
| onSearch={handleSearch} | ||
| /> | ||
| ``` | ||
|
|
||
| ## Props | ||
|
|
||
| | Prop | Type | Default | Description | | ||
| |---|---|---|---| | ||
| | `placeholder` | `string` | — | Placeholder text inside the input | | ||
| | `value` | `string` | — | Controlled value (enables controlled mode) | | ||
| | `onChange` | `(value: string) => void` | — | Called on every keystroke (required in controlled mode) | | ||
| | `onSearch` | `(term: string) => void` | — | Debounced callback fired after the user stops typing | | ||
| | `onClear` | `() => void` | — | Called when the clear button is clicked (optional override) | | ||
| | `debounce` | `number` | `500` | Debounce delay in ms applied to `onSearch` | | ||
| | `isLoading` | `boolean` | `false` | Shows a spinner and hides the clear button | | ||
| | `disabled` | `boolean` | `false` | Disables the input | | ||
| | `autoFocus` | `boolean` | `false` | Focuses the input on mount | | ||
| | `inputRef` | `React.Ref<HTMLInputElement>` | — | Ref forwarded to the `<input>` element | | ||
| | `aria-label` | `string` | `placeholder` | Accessible label (falls back to `placeholder`) | | ||
|
|
||
| ## Behavior Notes | ||
|
|
||
| - **Uncontrolled mode**: when `value` is not provided, the component manages its own state and fires `onSearch` after the debounce delay. | ||
| - **Controlled mode**: when `value` + `onChange` are both provided, the parent controls the input value entirely. | ||
| - **Clear button**: bypasses debounce — calls `onSearch('')` immediately (or `onClear` if provided). | ||
| - **Loading state**: `isLoading` hides the clear button and shows a spinner instead. Useful while an async search is in progress. | ||
| - The underlying `<input>` uses `type="search"` for better browser and accessibility integration. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import React, { useState } from 'react'; | ||
| import type { Meta, StoryObj } from '@storybook/react'; | ||
| import BBBSearch from './component'; | ||
|
|
||
| const meta = { | ||
| title: 'BBBSearch', | ||
| component: BBBSearch, | ||
| tags: ['autodocs'], | ||
| argTypes: { | ||
| placeholder: { | ||
| control: 'text', | ||
| description: 'Placeholder text displayed inside the input.', | ||
| }, | ||
| debounce: { | ||
| control: { type: 'number', min: 0, max: 2000, step: 100 }, | ||
| description: 'Debounce delay in milliseconds applied to `onSearch`.', | ||
| }, | ||
| isLoading: { | ||
| control: 'boolean', | ||
| description: 'Shows a spinner and hides the clear button when true.', | ||
| }, | ||
| disabled: { | ||
| control: 'boolean', | ||
| description: 'Disables the input.', | ||
| }, | ||
| autoFocus: { | ||
| control: 'boolean', | ||
| description: 'Focuses the input on mount.', | ||
| }, | ||
| }, | ||
| } satisfies Meta<typeof BBBSearch>; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| /** Basic uncontrolled usage — simplest way to use BBBSearch. */ | ||
| export const Default: Story = { | ||
| args: { | ||
| placeholder: 'Search participants...', | ||
| debounce: 500, | ||
| onSearch: (term: string) => console.log('onSearch:', term), | ||
| }, | ||
| }; | ||
|
|
||
| /** Shows the loading spinner while a search is in progress. */ | ||
| export const WithLoading: Story = { | ||
| args: { | ||
| placeholder: 'Search messages...', | ||
| isLoading: true, | ||
| value: 'hello', | ||
| }, | ||
| }; | ||
|
|
||
| const ControlledSearch = (args: React.ComponentProps<typeof BBBSearch>) => { | ||
| const [value, setValue] = useState(''); | ||
| return ( | ||
| <BBBSearch | ||
| {...args} | ||
| value={value} | ||
| onChange={setValue} | ||
| onClear={() => setValue('')} | ||
| placeholder="Filter results..." | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| /** Controlled mode — parent manages the value. */ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| export const Controlled: Story = { | ||
| render: (args) => <ControlledSearch {...args} />, | ||
| }; | ||
|
|
||
| /** Disabled state. */ | ||
| export const Disabled: Story = { | ||
| args: { | ||
| placeholder: 'Search disabled...', | ||
| disabled: true, | ||
| }, | ||
| }; | ||
|
|
||
| /** Fast debounce for instant-search scenarios. */ | ||
| export const WithFastDebounce: Story = { | ||
| args: { | ||
| placeholder: 'Search (100ms debounce)...', | ||
| debounce: 100, | ||
| onSearch: (term: string) => console.log('onSearch:', term), | ||
| }, | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| import React, { JSX, useState, useEffect, useCallback, useRef } from 'react'; | ||
| import BBBSpinner from '../Spinner/component'; | ||
| import * as Styled from './styles'; | ||
| import { BBBSearchProps } from './types'; | ||
|
|
||
| /** | ||
| * Search input with debounce, clear action, and optional loading state. | ||
| * It supports both uncontrolled usage and controlled mode via `value` + `onChange`. | ||
| */ | ||
| function BBBSearch({ | ||
| placeholder, | ||
| value, | ||
| onChange, | ||
| onSearch, | ||
| onClear, | ||
| debounce = 500, | ||
| isLoading = false, | ||
| disabled = false, | ||
| autoFocus = false, | ||
| inputRef, | ||
| 'aria-label': ariaLabel, | ||
| }: BBBSearchProps): JSX.Element { | ||
| const isControlled = value !== undefined; | ||
|
|
||
| const [internalValue, setInternalValue] = useState(''); | ||
| const localInputRef = useRef<HTMLInputElement>(null); | ||
| const resolvedRef = (inputRef as React.RefObject<HTMLInputElement>) ?? localInputRef; | ||
|
|
||
| const currentValue = isControlled ? (value ?? '') : internalValue; | ||
| const hasValue = currentValue.length > 0; | ||
|
|
||
| // Debounced onSearch (uncontrolled mode) | ||
| useEffect(() => { | ||
| if (isControlled || !onSearch) return; | ||
|
|
||
| const timeoutId = setTimeout(() => { | ||
| onSearch(internalValue); | ||
| }, debounce); | ||
|
|
||
| return () => clearTimeout(timeoutId); | ||
| }, [internalValue, debounce, onSearch, isControlled]); | ||
|
|
||
| // Handle autoFocus on mount | ||
| useEffect(() => { | ||
| if (autoFocus && resolvedRef.current) { | ||
| resolvedRef.current.focus(); | ||
| } | ||
| }, [autoFocus, resolvedRef]); | ||
|
|
||
| const handleChange = useCallback( | ||
| (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| const next = e.target.value; | ||
| if (isControlled) { | ||
| onChange?.(next); | ||
| } else { | ||
| setInternalValue(next); | ||
| } | ||
| }, | ||
| [isControlled, onChange], | ||
| ); | ||
|
|
||
| const handleClear = useCallback(() => { | ||
| if (onClear) { | ||
| onClear(); | ||
| } else if (isControlled) { | ||
| onChange?.(''); | ||
| } else { | ||
| // Clear is immune to debounce — fire onSearch immediately | ||
| setInternalValue(''); | ||
| onSearch?.(''); | ||
| } | ||
|
|
||
| resolvedRef.current?.focus(); | ||
| }, [onClear, isControlled, onChange, onSearch, resolvedRef]); | ||
|
|
||
| const handleContainerClick = useCallback( | ||
| (e: React.MouseEvent<HTMLDivElement>) => { | ||
| if (e.target === e.currentTarget) { | ||
| resolvedRef.current?.focus(); | ||
| } | ||
| }, | ||
| [resolvedRef], | ||
| ); | ||
|
|
||
| return ( | ||
| <Styled.Container $disabled={disabled} onClick={handleContainerClick}> | ||
| <Styled.SearchIconWrapper aria-hidden="true"> | ||
| <svg | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| width="16" | ||
| height="16" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| strokeWidth="2" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| focusable="false" | ||
| > | ||
| <circle cx="11" cy="11" r="8" /> | ||
| <line x1="21" y1="21" x2="16.65" y2="16.65" /> | ||
| </svg> | ||
| </Styled.SearchIconWrapper> | ||
|
|
||
| <Styled.StyledInput | ||
| ref={resolvedRef} | ||
| type="search" | ||
| role="searchbox" | ||
| placeholder={placeholder} | ||
| value={currentValue} | ||
| onChange={handleChange} | ||
| disabled={disabled} | ||
| aria-label={ariaLabel ?? placeholder} | ||
| /> | ||
|
|
||
| {isLoading && hasValue && ( | ||
| <Styled.ActionWrapper> | ||
| <BBBSpinner size={16} strokeWidth={3} /> | ||
| </Styled.ActionWrapper> | ||
| )} | ||
|
|
||
| {!isLoading && hasValue && ( | ||
| <Styled.ClearButton | ||
| type="button" | ||
| onClick={handleClear} | ||
| aria-label="Clear search" | ||
| tabIndex={0} | ||
| > | ||
| <svg | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| width="16" | ||
| height="16" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| strokeWidth="2.5" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| focusable="false" | ||
| aria-hidden="true" | ||
| > | ||
| <line x1="18" y1="6" x2="6" y2="18" /> | ||
| <line x1="6" y1="6" x2="18" y2="18" /> | ||
| </svg> | ||
| </Styled.ClearButton> | ||
| )} | ||
| </Styled.Container> | ||
| ); | ||
| } | ||
|
|
||
| export default BBBSearch; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default as BBBSearch } from './component'; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a mention to this component in the project README - under available components.