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;
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,
.where-builder,
.sort-complex,
.group-by-builder {
margin-top: base(1);
margin-top: calc(var(--base) / 2);
}
@include small-break {
&__wrap {
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%;
.search-bar__actions {
.pill {
padding: base(0.2) base(0.2) base(0.2) base(0.4);
justify-content: space-between;
}
}
.pill-selector,
.where-builder,
.sort-complex {
margin-top: calc(var(--base) / 2);
}
&__toggle-columns,
&__toggle-where,
&__toggle-sort,

View File

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

View File

@@ -2,18 +2,41 @@
@layer payload-default {
.search-bar {
--icon-width: 40px;
--search-bg: var(--theme-elevation-50);
display: grid;
grid-template-columns: auto 1fr;
width: 100%;
display: flex;
align-items: center;
background-color: var(--theme-elevation-50);
background-color: var(--search-bg);
border-radius: var(--style-radius-m);
padding: calc(var(--base) * 0.6);
gap: calc(var(--base) * 0.6);
position: relative;
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 {
flex-grow: 1;
grid-column: 1/3;
grid-row: 1/2;
background-color: transparent;
border-radius: inherit;
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;
align-items: center;
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 = {
Actions?: React.ReactNode[]
className?: string
filterKey?: string
label?: string
onSearchChange?: (search: string) => void
onSearchChange: (search: string) => void
searchQueryParam?: string
}
export function SearchBar({
Actions,
className,
filterKey,
label,
label = 'Search...',
onSearchChange,
searchQueryParam,
}: SearchBarProps) {
@@ -25,7 +23,6 @@ export function SearchBar({
<SearchIcon />
<SearchFilter
handleChange={onSearchChange}
key={filterKey || 'search'}
label={label}
searchQueryParam={searchQueryParam}
/>

View File

@@ -1,51 +1,17 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
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
}
import type { ParsedQs } from 'qs-esm'
import { usePathname } from 'next/navigation.js'
import type { SearchFilterProps } from './types.js'
import { useDebounce } from '../../hooks/useDebounce.js'
import './index.scss'
const baseClass = 'search-filter'
export const SearchFilter: React.FC<SearchFilterProps> = (props) => {
export function SearchFilter(props: SearchFilterProps) {
const { handleChange, initialParams, label, searchQueryParam } = props
const searchParam = initialParams?.search || searchQueryParam
const pathname = usePathname()
const [search, setSearch] = useState(typeof searchParam === 'string' ? searchParam : undefined)
/**
@@ -67,7 +33,12 @@ export const SearchFilter: React.FC<SearchFilterProps> = (props) => {
setSearch(searchParam as string)
previousSearch.current = searchParam as string
}
}, [searchParam, pathname])
return () => {
shouldUpdateState.current = true
previousSearch.current = undefined
}
}, [searchParam])
useEffect(() => {
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
}