fix(ui): consistent searchbar across folders and lists (#13712)

Search bar styling was inconsistent across folder and list views. This
re-uses the SearchBar component in the ListHeader component.
This commit is contained in:
Jarrod Flesch
2025-09-05 16:40:11 -04:00
committed by GitHub
parent a4a0298435
commit 09e3174834
6 changed files with 129 additions and 158 deletions

View File

@@ -6,101 +6,21 @@
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
&__wrap {
display: flex;
align-items: center;
justify-content: space-between;
gap: base(0.5);
background-color: var(--theme-elevation-50);
border-radius: var(--style-radius-m);
}
&__search {
display: flex;
background-color: var(--theme-elevation-50);
border-radius: var(--style-radius-m);
gap: base(0.4);
flex-grow: 1;
position: relative;
.icon {
flex-shrink: 0;
}
}
.icon--search {
position: absolute;
left: 0;
top: 50%;
transform: translate3d(0, -50%, 0);
inset-inline-start: base(0.4);
z-index: 1;
pointer-events: none;
}
.search-filter {
flex-grow: 1;
input {
height: 46px;
padding-left: 36px;
margin: 0;
}
}
&__custom-control {
padding: 0;
border-radius: 0;
}
&__buttons {
display: flex;
align-items: center;
gap: calc(var(--base) / 4);
padding-right: 10px;
}
.pill-selector, .pill-selector,
.where-builder, .where-builder,
.sort-complex, .sort-complex,
.group-by-builder { .group-by-builder {
margin-top: base(1); margin-top: calc(var(--base) / 2);
} }
@include small-break { @include small-break {
&__wrap { .search-bar__actions {
flex-direction: column;
align-items: stretch;
background-color: transparent;
border-radius: 0;
}
.search-filter {
width: 100%;
input {
height: 40px;
padding: 0 base(1.5);
}
}
&__buttons {
padding-right: 0;
margin: 0;
width: 100%;
.pill { .pill {
padding: base(0.2) base(0.2) base(0.2) base(0.4); padding: base(0.2) base(0.2) base(0.2) base(0.4);
justify-content: space-between; justify-content: space-between;
} }
} }
.pill-selector,
.where-builder,
.sort-complex {
margin-top: calc(var(--base) / 2);
}
&__toggle-columns, &__toggle-columns,
&__toggle-where, &__toggle-where,
&__toggle-sort, &__toggle-sort,

View File

@@ -11,7 +11,6 @@ import { Popup } from '../../elements/Popup/index.js'
import { useUseTitleField } from '../../hooks/useUseAsTitle.js' import { useUseTitleField } from '../../hooks/useUseAsTitle.js'
import { ChevronIcon } from '../../icons/Chevron/index.js' import { ChevronIcon } from '../../icons/Chevron/index.js'
import { Dots } from '../../icons/Dots/index.js' import { Dots } from '../../icons/Dots/index.js'
import { SearchIcon } from '../../icons/Search/index.js'
import { useListQuery } from '../../providers/ListQuery/index.js' import { useListQuery } from '../../providers/ListQuery/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { AnimateHeight } from '../AnimateHeight/index.js' import { AnimateHeight } from '../AnimateHeight/index.js'
@@ -19,7 +18,7 @@ import { ColumnSelector } from '../ColumnSelector/index.js'
import { GroupByBuilder } from '../GroupByBuilder/index.js' import { GroupByBuilder } from '../GroupByBuilder/index.js'
import { Pill } from '../Pill/index.js' import { Pill } from '../Pill/index.js'
import { QueryPresetBar } from '../QueryPresets/QueryPresetBar/index.js' import { QueryPresetBar } from '../QueryPresets/QueryPresetBar/index.js'
import { SearchFilter } from '../SearchFilter/index.js' import { SearchBar } from '../SearchBar/index.js'
import { WhereBuilder } from '../WhereBuilder/index.js' import { WhereBuilder } from '../WhereBuilder/index.js'
import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched.js' import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched.js'
import './index.scss' import './index.scss'
@@ -132,67 +131,64 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
queryPresetPermissions={queryPresetPermissions} queryPresetPermissions={queryPresetPermissions}
/> />
)} )}
<div className={`${baseClass}__wrap`}> <SearchBar
<div className={`${baseClass}__search`}> Actions={[
<SearchIcon /> !smallBreak && (
<SearchFilter <React.Fragment key="before-actions">{beforeActions && beforeActions}</React.Fragment>
handleChange={handleSearchChange} ),
key={collectionSlug} enableColumns && (
label={searchLabelTranslated.current}
searchQueryParam={query?.search}
/>
</div>
<div className={`${baseClass}__buttons`}>
{!smallBreak && <React.Fragment>{beforeActions && beforeActions}</React.Fragment>}
{enableColumns && (
<Pill <Pill
aria-controls={`${baseClass}-columns`} aria-controls={`${baseClass}-columns`}
aria-expanded={visibleDrawer === 'columns'} aria-expanded={visibleDrawer === 'columns'}
className={`${baseClass}__toggle-columns`} className={`${baseClass}__toggle-columns`}
icon={<ChevronIcon direction={visibleDrawer === 'columns' ? 'up' : 'down'} />} icon={<ChevronIcon direction={visibleDrawer === 'columns' ? 'up' : 'down'} />}
id="toggle-list-columns" id="toggle-list-columns"
key="toggle-list-columns"
onClick={() => setVisibleDrawer(visibleDrawer !== 'columns' ? 'columns' : undefined)} onClick={() => setVisibleDrawer(visibleDrawer !== 'columns' ? 'columns' : undefined)}
pillStyle="light" pillStyle="light"
size="small" size="small"
> >
{t('general:columns')} {t('general:columns')}
</Pill> </Pill>
)} ),
{enableFilters && ( enableFilters && (
<Pill <Pill
aria-controls={`${baseClass}-where`} aria-controls={`${baseClass}-where`}
aria-expanded={visibleDrawer === 'where'} aria-expanded={visibleDrawer === 'where'}
className={`${baseClass}__toggle-where`} className={`${baseClass}__toggle-where`}
icon={<ChevronIcon direction={visibleDrawer === 'where' ? 'up' : 'down'} />} icon={<ChevronIcon direction={visibleDrawer === 'where' ? 'up' : 'down'} />}
id="toggle-list-filters" id="toggle-list-filters"
key="toggle-list-filters"
onClick={() => setVisibleDrawer(visibleDrawer !== 'where' ? 'where' : undefined)} onClick={() => setVisibleDrawer(visibleDrawer !== 'where' ? 'where' : undefined)}
pillStyle="light" pillStyle="light"
size="small" size="small"
> >
{t('general:filters')} {t('general:filters')}
</Pill> </Pill>
)} ),
{enableSort && ( enableSort && (
<Pill <Pill
aria-controls={`${baseClass}-sort`} aria-controls={`${baseClass}-sort`}
aria-expanded={visibleDrawer === 'sort'} aria-expanded={visibleDrawer === 'sort'}
className={`${baseClass}__toggle-sort`} className={`${baseClass}__toggle-sort`}
icon={<ChevronIcon />} icon={<ChevronIcon />}
id="toggle-list-sort" id="toggle-list-sort"
key="toggle-list-sort"
onClick={() => setVisibleDrawer(visibleDrawer !== 'sort' ? 'sort' : undefined)} onClick={() => setVisibleDrawer(visibleDrawer !== 'sort' ? 'sort' : undefined)}
pillStyle="light" pillStyle="light"
size="small" size="small"
> >
{t('general:sort')} {t('general:sort')}
</Pill> </Pill>
)} ),
{collectionConfig.admin.groupBy && ( collectionConfig.admin.groupBy && (
<Pill <Pill
aria-controls={`${baseClass}-group-by`} aria-controls={`${baseClass}-group-by`}
aria-expanded={visibleDrawer === 'group-by'} aria-expanded={visibleDrawer === 'group-by'}
className={`${baseClass}__toggle-group-by`} className={`${baseClass}__toggle-group-by`}
icon={<ChevronIcon direction={visibleDrawer === 'group-by' ? 'up' : 'down'} />} icon={<ChevronIcon direction={visibleDrawer === 'group-by' ? 'up' : 'down'} />}
id="toggle-group-by" id="toggle-group-by"
key="toggle-group-by"
onClick={() => onClick={() =>
setVisibleDrawer(visibleDrawer !== 'group-by' ? 'group-by' : undefined) setVisibleDrawer(visibleDrawer !== 'group-by' ? 'group-by' : undefined)
} }
@@ -203,13 +199,14 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
label: '', label: '',
})} })}
</Pill> </Pill>
)} ),
{listMenuItems && Array.isArray(listMenuItems) && listMenuItems.length > 0 && ( listMenuItems && Array.isArray(listMenuItems) && listMenuItems.length > 0 && (
<Popup <Popup
button={<Dots ariaLabel={t('general:moreOptions')} />} button={<Dots ariaLabel={t('general:moreOptions')} />}
className={`${baseClass}__popup`} className={`${baseClass}__popup`}
horizontalAlign="right" horizontalAlign="right"
id="list-menu" id="list-menu"
key="list-menu"
size="small" size="small"
verticalAlign="bottom" verticalAlign="bottom"
> >
@@ -217,9 +214,13 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
<Fragment key={`list-menu-item-${i}`}>{item}</Fragment> <Fragment key={`list-menu-item-${i}`}>{item}</Fragment>
))} ))}
</Popup> </Popup>
)} ),
</div> ].filter(Boolean)}
</div> key={collectionSlug}
label={searchLabelTranslated.current}
onSearchChange={handleSearchChange}
searchQueryParam={query?.search}
/>
{enableColumns && ( {enableColumns && (
<AnimateHeight <AnimateHeight
className={`${baseClass}__columns`} className={`${baseClass}__columns`}

View File

@@ -2,18 +2,41 @@
@layer payload-default { @layer payload-default {
.search-bar { .search-bar {
--icon-width: 40px;
--search-bg: var(--theme-elevation-50);
display: grid;
grid-template-columns: auto 1fr;
width: 100%; width: 100%;
display: flex; background-color: var(--search-bg);
align-items: center;
background-color: var(--theme-elevation-50);
border-radius: var(--style-radius-m); border-radius: var(--style-radius-m);
padding: calc(var(--base) * 0.6); position: relative;
gap: calc(var(--base) * 0.6); min-height: 46px;
isolation: isolate;
&:has(.search-bar__actions) {
grid-template-columns: auto 1fr auto;
}
.icon--search {
grid-column: 1/2;
grid-row: 1/2;
z-index: 1;
align-self: center;
justify-self: center;
pointer-events: none;
width: 40px;
}
.search-filter { .search-filter {
flex-grow: 1; grid-column: 1/3;
grid-row: 1/2;
background-color: transparent;
border-radius: inherit;
input { input {
flex-grow: 1; height: 100%;
padding: calc(var(--base) * 0.5) var(--base) calc(var(--base) * 0.5) var(--icon-width);
background-color: transparent;
} }
} }
@@ -21,6 +44,32 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: calc(var(--base) / 4); gap: calc(var(--base) / 4);
padding: calc(var(--base) * 0.5);
grid-column: 3/4;
}
@include small-break {
min-height: 40px;
background-color: transparent;
&:has(.search-bar__actions) {
row-gap: calc(var(--base) / 2);
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
}
.search-filter {
background-color: var(--search-bg);
}
&__actions {
grid-row: 2/3;
grid-column: 1/3;
display: flex;
align-items: center;
gap: calc(var(--base) / 4);
padding: 0;
}
} }
} }
} }

View File

@@ -7,16 +7,14 @@ const baseClass = 'search-bar'
type SearchBarProps = { type SearchBarProps = {
Actions?: React.ReactNode[] Actions?: React.ReactNode[]
className?: string className?: string
filterKey?: string
label?: string label?: string
onSearchChange?: (search: string) => void onSearchChange: (search: string) => void
searchQueryParam?: string searchQueryParam?: string
} }
export function SearchBar({ export function SearchBar({
Actions, Actions,
className, className,
filterKey, label = 'Search...',
label,
onSearchChange, onSearchChange,
searchQueryParam, searchQueryParam,
}: SearchBarProps) { }: SearchBarProps) {
@@ -25,7 +23,6 @@ export function SearchBar({
<SearchIcon /> <SearchIcon />
<SearchFilter <SearchFilter
handleChange={onSearchChange} handleChange={onSearchChange}
key={filterKey || 'search'}
label={label} label={label}
searchQueryParam={searchQueryParam} searchQueryParam={searchQueryParam}
/> />

View File

@@ -1,51 +1,17 @@
'use client' 'use client'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
export type SearchFilterProps = { import type { SearchFilterProps } from './types.js'
/**
* This prop is deprecated and will be removed in the next major version.
*
* @deprecated
*/
fieldName?: string
handleChange?: (search: string) => void
/**
* This prop is deprecated and will be removed in the next major version.
*
* Prefer passing in `searchString` instead.
*
* @deprecated
*/
initialParams?: ParsedQs
label: string
searchQueryParam?: string
/**
* This prop is deprecated and will be removed in the next major version.
*
* @deprecated
*/
setValue?: (arg: string) => void
/**
* This prop is deprecated and will be removed in the next major version.
*
* @deprecated
*/
value?: string
}
import type { ParsedQs } from 'qs-esm'
import { usePathname } from 'next/navigation.js'
import { useDebounce } from '../../hooks/useDebounce.js' import { useDebounce } from '../../hooks/useDebounce.js'
import './index.scss' import './index.scss'
const baseClass = 'search-filter' const baseClass = 'search-filter'
export const SearchFilter: React.FC<SearchFilterProps> = (props) => { export function SearchFilter(props: SearchFilterProps) {
const { handleChange, initialParams, label, searchQueryParam } = props const { handleChange, initialParams, label, searchQueryParam } = props
const searchParam = initialParams?.search || searchQueryParam const searchParam = initialParams?.search || searchQueryParam
const pathname = usePathname()
const [search, setSearch] = useState(typeof searchParam === 'string' ? searchParam : undefined) const [search, setSearch] = useState(typeof searchParam === 'string' ? searchParam : undefined)
/** /**
@@ -67,7 +33,12 @@ export const SearchFilter: React.FC<SearchFilterProps> = (props) => {
setSearch(searchParam as string) setSearch(searchParam as string)
previousSearch.current = searchParam as string previousSearch.current = searchParam as string
} }
}, [searchParam, pathname])
return () => {
shouldUpdateState.current = true
previousSearch.current = undefined
}
}, [searchParam])
useEffect(() => { useEffect(() => {
if (debouncedSearch !== previousSearch.current && shouldUpdateState.current) { if (debouncedSearch !== previousSearch.current && shouldUpdateState.current) {

View File

@@ -0,0 +1,33 @@
import type { ParsedQs } from 'qs-esm'
export type SearchFilterProps = {
/**
* This prop is deprecated and will be removed in the next major version.
*
* @deprecated
*/
fieldName?: string
handleChange?: (search: string) => void
/**
* This prop is deprecated and will be removed in the next major version.
*
* Prefer passing in `searchString` instead.
*
* @deprecated
*/
initialParams?: ParsedQs
label: string
searchQueryParam?: string
/**
* This prop is deprecated and will be removed in the next major version.
*
* @deprecated
*/
setValue?: (arg: string) => void
/**
* This prop is deprecated and will be removed in the next major version.
*
* @deprecated
*/
value?: string
}