ChemPal Documentation - v1.7.0
    Preparing search index...

    Variable SearchFormConst

    SearchForm: FC<SearchFormProps> = ...

    Renders the primary search bar: a text input bound to the shared searchFilters.titleQuery, a submit button, and an advanced-options button that opens the settings drawer. The input hydrates from and persists to session storage so the query survives popup re-opens.

    The SearchFormProps controlling submit handling, placeholder text, and the drawer-toggle override.

    The search form element.

    <SearchForm onSearch={(q) => runSearch(q)} placeholder="Search chemicals..." />
    // Renders a search box; submitting "acetone" calls onSearch("acetone").
    export const SearchForm: FC<SearchFormProps> = ({
    onSearch,
    onDrawerToggle,
    placeholder = i18n('search_placeholder'),
    }) => {
    const appContext = useAppContext();
    const { searchFilters, setSearchFilters } = appContext;
    const query = searchFilters.titleQuery;

    // Blue-highlight the advanced-search button (and note the count on hover) when
    // any drawer filter is set to a non-default value.
    const activeFilterCount = countActiveSearchFilters({
    selectedSuppliers: appContext.selectedSuppliers,
    searchFilters,
    userSettings: appContext.userSettings,
    });

    // Set when the typed query is an invalid advanced query; blocks submit until fixed.
    const [searchError, setSearchError] = useState<string | undefined>(undefined);
    // Debounced copy for display only — the message appears after the user pauses typing.
    const hintMessage = useDelayedError(searchError, query, 200);

    // Hydrate the shared title query from the persisted draft in session storage
    // so this field reflects whatever was last typed in any search input — even
    // across popup re-opens. Both this component and DrawerSearchPanel bind to
    // searchFilters.titleQuery, so once context is populated they stay in sync
    // automatically without re-reading the cache.
    useEffect(() => {
    const loadSearchInput = async () => {
    try {
    const data = await cstorage.session.get([CACHE.SEARCH_INPUT]);
    const stored = data[CACHE.SEARCH_INPUT];
    if (typeof stored === 'string' && stored && stored !== searchFilters.titleQuery) {
    setSearchFilters({ ...searchFilters, titleQuery: stored });
    }
    } catch (error) {
    console.warn('Failed to load search input from session storage:', { error });
    }
    };
    loadSearchInput();
    // Run once on mount; intentionally don't depend on searchFilters to avoid loops.
    }, []);

    const handleChange = async (value: string) => {
    setSearchFilters({ ...searchFilters, titleQuery: value });
    try {
    await cstorage.session.set({ [CACHE.SEARCH_INPUT]: value });
    } catch (error) {
    console.warn('Failed to persist search input to session storage:', { error });
    }
    };

    const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    if (searchError) {
    return;
    }
    const trimmed = query.trim();
    if (trimmed) {
    // Clear the live draft so re-opening the drawer doesn't show stale text
    // from the just-submitted query. The parent's onSearch handler is also
    // responsible for clearing CACHE.SEARCH_INPUT in session storage.
    setSearchFilters({ ...searchFilters, titleQuery: '' });
    onSearch(trimmed);
    }
    };

    const handleDrawerToggle = () => {
    if (onDrawerToggle) {
    onDrawerToggle();
    } else {
    appContext.toggleDrawer();
    }
    };

    return (
    <Box className={styles['search-form-container']} data-testid="search-form-container">
    <SearchFormPaper onSubmit={handleSubmit}>
    <HighlightedSearchInput
    value={query}
    onChange={handleChange}
    placeholder={placeholder}
    ariaLabel={i18n('search_form_aria')}
    onValidityChange={(blocked, message) =>
    setSearchError(blocked ? (message ?? i18n('search_invalid_query')) : undefined)
    }
    style={{ marginLeft: 16, flex: 1, fontSize: '1.15rem' }}
    />

    <IconButton
    className={styles['search-form-icon-button']}
    type="submit"
    aria-label={i18n('search_submit')}
    disabled={!query.trim() || Boolean(searchError)}
    >
    <SearchIcon />
    </IconButton>

    <SearchFormDivider />
    <Tooltip
    title={
    activeFilterCount > 0
    ? i18n('search_active_filters', [String(activeFilterCount)])
    : i18n('search_advanced_options')
    }
    >
    <IconButton
    className={styles['search-form-icon-button']}
    type="button"
    aria-label={i18n('search_advanced_options')}
    onClick={handleDrawerToggle}
    sx={activeFilterCount > 0 ? { color: '#4e73af' } : undefined}
    >
    <ScienceIcon />
    </IconButton>
    </Tooltip>
    </SearchFormPaper>

    {hintMessage && (
    <div role="alert" className={styles['search-error-hint']}>
    {hintMessage}
    </div>
    )}
    </Box>
    );
    };