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:
@@ -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,
|
||||||
|
|||||||
@@ -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`}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
33
packages/ui/src/elements/SearchFilter/types.ts
Normal file
33
packages/ui/src/elements/SearchFilter/types.ts
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user