mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix(studio): show keyboard focus on filter bar (#50827)
## Problem - Filter fields could not be re-entered with Tab, and remove buttons were previously skipped. - Earlier focus rings flashed or crowded controls. Segmented filters and design system examples also had uneven layout. ## Solution - Tab now reaches each filter’s property, operator, value and remove button. Editable fields use the caret; read-only values and remove buttons keep a visible focus cue. - Refined spacing and sizing. The segmented highlight is inset and clears when focus enters a control. Both variants and the focus guidance are documented on the [design system page](https://design-system-git-dnywh-fix-filter-bar-focus-supabase.vercel.app/design-system/docs/fragments/filter-bar). |After | | --- | | <img width="462" height="126" alt="CleanShot 2026-09-24 at 15 04 23@2x" src="https://github.com/user-attachments/assets/2ee8bee2-f4fa-40f4-ada5-67bfe32384e7" /> | | <img width="362" height="96" alt="CleanShot 2026-09-24 at 15 04 47@2x" src="https://github.com/user-attachments/assets/9f42054a-f432-493d-b417-f10892676c96" /> | ## Review instructions The easiest way to test this is to open Table Editor on both production/staging and on this PR’s [deploy preview](https://studio-staging-git-dnywh-fix-filter-bar-focus-supabase.vercel.app/). Try navigating by keyboard (tab, left/right arrow) within the Filter Bar. Compare the two. - In Studio’s Table Editor or the [design system example](https://design-system-git-dnywh-fix-filter-bar-focus-supabase.vercel.app/design-system/docs/fragments/filter-bar), add a filter. Tab through its property, operator, value and remove button, then Shift+Tab back into editing. - Check that the segmented and pill examples fill their preview width and that focus cues do not collide with neighbouring controls. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary * **New Features** * Added a pill appearance for the Filter Bar, with usage guidance and interactive examples for pill and segmented presentations. * **Accessibility** * Improved keyboard navigation through filter controls, including removing a filter with the keyboard. * Clarified that an editable text field’s insertion caret can serve as its visible focus indicator; read-only fields and controls without a caret still need another visible indicator. * **Style** * Refined Filter Bar spacing and focus styling across appearances. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
36371de151
commit
9a03f3cd9c
13 files changed
+255
-36
No files matched your search
@@ -28,6 +28,8 @@ Chromium-based browsers and Firefox handle this automatically via the Tab key. S
|
||||
|
||||
Interactive page elements should also provide visual feedback upon selection via a `focus-visible` state. We use one shared focus ring so users recognize this state instantly. Its color follows the theme's brighter primary hue in both themes. See [Primary and brand colors](../docs/color-usage#primary-and-brand-colors).
|
||||
|
||||
An editable text field can use its visible insertion caret to show focus. This can be useful for inputs embedded within a compact control, where another ring would obscure nearby elements. Check that the caret clearly identifies the active field. Read-only fields and controls without a caret still need a visible focus indicator.
|
||||
|
||||
### Focus ring recipe
|
||||
|
||||
Prefer the shared utilities over inventing local styles:
|
||||
@@ -75,7 +77,7 @@ transition-property: color, background-color, border-color, ...
|
||||
Rules:
|
||||
|
||||
- Prefer `:focus-visible` over `:focus` so click/tap does not show a focus indicator
|
||||
- Never use `outline-none` / `outline-hidden` without a ring or outline replacement
|
||||
- Never use `outline-none` / `outline-hidden` without a visible focus indicator, such as a ring, outline, or insertion caret in an editable text field
|
||||
- Always use the shared color (`ring-ring` / `outline-ring`). Variants (primary, danger, warning) do not change focus color. The shared color derives from `--primary` with enough lightness to remain visible in light mode.
|
||||
- Do not animate the focus indicator; avoid `transition-all` / `transition` on controls that show one (prefer `transition-colors`)
|
||||
- Prefer `focus-ring` / `focus-inset` over copy-pasting the class stack
|
||||
|
||||
@@ -50,6 +50,34 @@ export function FilterDemo() {
|
||||
}
|
||||
```
|
||||
|
||||
## Variants
|
||||
|
||||
The default variant joins filters into a segmented bar. Use the pill variant to show each filter as a separate rounded chip:
|
||||
|
||||
### Segmented
|
||||
|
||||
<ComponentPreview
|
||||
name="filter-bar-segmented-demo"
|
||||
description="Filters joined into a segmented bar."
|
||||
peekCode
|
||||
showDottedGrid
|
||||
wide
|
||||
/>
|
||||
|
||||
### Pill
|
||||
|
||||
<ComponentPreview
|
||||
name="filter-bar-pill-demo"
|
||||
description="Rounded chips for each filter, as used in Studio."
|
||||
peekCode
|
||||
showDottedGrid
|
||||
wide
|
||||
/>
|
||||
|
||||
```tsx
|
||||
<FilterBar variant="pill" className="border-0 bg-transparent overflow-visible" {...props} />
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### FilterProperty
|
||||
@@ -95,6 +123,7 @@ interface FilterCondition {
|
||||
| onFreeformTextChange | (text: string) => void | Callback when free-form text changes |
|
||||
| actions | FilterBarAction[]? | Optional custom actions to show in the menu |
|
||||
| isLoading | boolean? | If true, dims the bar while work is in progress |
|
||||
| variant | 'default' \| 'pill'? | Filter appearance. Defaults to 'default' |
|
||||
|
||||
## Custom actions (e.g. AI)
|
||||
|
||||
|
||||
@@ -118,19 +118,28 @@ const initialFilters: FilterGroup = {
|
||||
conditions: [],
|
||||
}
|
||||
|
||||
export default function FilterBarDemo() {
|
||||
function FilterBarExample({ variant }: { variant: 'default' | 'pill' }) {
|
||||
const [filters, setFilters] = useState<FilterGroup>(initialFilters)
|
||||
const [freeformText, setFreeformText] = useState('')
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<FilterBar
|
||||
filterProperties={filterProperties}
|
||||
freeformText={freeformText}
|
||||
onFreeformTextChange={setFreeformText}
|
||||
filters={filters}
|
||||
onFilterChange={setFilters}
|
||||
/>
|
||||
<FilterBar
|
||||
variant={variant}
|
||||
className={variant === 'pill' ? 'border-0 bg-transparent overflow-visible' : undefined}
|
||||
filterProperties={filterProperties}
|
||||
freeformText={freeformText}
|
||||
onFreeformTextChange={setFreeformText}
|
||||
filters={filters}
|
||||
onFilterChange={setFilters}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FilterBarDemo() {
|
||||
return (
|
||||
<div className="w-full space-y-6">
|
||||
<FilterBarExample variant="default" />
|
||||
<FilterBarExample variant="pill" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useState } from 'react'
|
||||
import { FilterBar, type FilterGroup } from 'ui-patterns/FilterBar'
|
||||
|
||||
const filterProperties = [
|
||||
{ label: 'Name', name: 'name', type: 'string' as const, operators: ['=', '!='] },
|
||||
{
|
||||
label: 'Status',
|
||||
name: 'status',
|
||||
type: 'string' as const,
|
||||
options: ['active', 'inactive', 'pending'],
|
||||
operators: ['=', '!='],
|
||||
},
|
||||
]
|
||||
|
||||
export default function FilterBarPillDemo() {
|
||||
const [filters, setFilters] = useState<FilterGroup>({ logicalOperator: 'AND', conditions: [] })
|
||||
const [freeformText, setFreeformText] = useState('')
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<FilterBar
|
||||
variant="pill"
|
||||
className="border-0 bg-transparent overflow-visible"
|
||||
filterProperties={filterProperties}
|
||||
filters={filters}
|
||||
onFilterChange={setFilters}
|
||||
freeformText={freeformText}
|
||||
onFreeformTextChange={setFreeformText}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useState } from 'react'
|
||||
import { FilterBar, type FilterGroup } from 'ui-patterns/FilterBar'
|
||||
|
||||
const filterProperties = [
|
||||
{ label: 'Name', name: 'name', type: 'string' as const, operators: ['=', '!='] },
|
||||
{
|
||||
label: 'Status',
|
||||
name: 'status',
|
||||
type: 'string' as const,
|
||||
options: ['active', 'inactive', 'pending'],
|
||||
operators: ['=', '!='],
|
||||
},
|
||||
]
|
||||
|
||||
export default function FilterBarSegmentedDemo() {
|
||||
const [filters, setFilters] = useState<FilterGroup>({ logicalOperator: 'AND', conditions: [] })
|
||||
const [freeformText, setFreeformText] = useState('')
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<FilterBar
|
||||
filterProperties={filterProperties}
|
||||
filters={filters}
|
||||
onFilterChange={setFilters}
|
||||
freeformText={freeformText}
|
||||
onFreeformTextChange={setFreeformText}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -605,6 +605,18 @@ export const examples: Registry = [
|
||||
registryDependencies: ['filter-bar'],
|
||||
files: ['example/filter-bar-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'filter-bar-pill-demo',
|
||||
type: 'components:example',
|
||||
registryDependencies: ['filter-bar'],
|
||||
files: ['example/filter-bar-pill-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'filter-bar-segmented-demo',
|
||||
type: 'components:example',
|
||||
registryDependencies: ['filter-bar'],
|
||||
files: ['example/filter-bar-segmented-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'hover-card-demo',
|
||||
type: 'components:example',
|
||||
|
||||
@@ -1159,7 +1159,9 @@ test.describe('Filter Bar', () => {
|
||||
await addFilter(page, ref, columnName, '=', 'Alice')
|
||||
|
||||
const conditionEl = page.getByTestId(`filter-condition-${columnName}`)
|
||||
await conditionEl.locator('span', { hasText: columnName }).first().click()
|
||||
await conditionEl
|
||||
.getByRole('button', { name: `Change property from ${columnName}` })
|
||||
.click()
|
||||
|
||||
const searchInput = page.getByTestId(`filter-property-search-${columnName}`)
|
||||
await expect(searchInput).toBeVisible()
|
||||
|
||||
@@ -68,8 +68,7 @@ export async function addFilterWithDropdownValue(
|
||||
export async function switchProperty(page: Page, currentColumnName: string, newColumnName: string) {
|
||||
const conditionEl = page.getByTestId(`filter-condition-${currentColumnName}`)
|
||||
await conditionEl
|
||||
.locator(`span`, { hasText: new RegExp(`^${currentColumnName}$`, 'i') })
|
||||
.first()
|
||||
.getByRole('button', { name: `Change property from ${currentColumnName}` })
|
||||
.click()
|
||||
|
||||
const searchInput = page.getByTestId(`filter-property-search-${currentColumnName}`)
|
||||
|
||||
@@ -171,6 +171,84 @@ describe('FilterBar', () => {
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('lets keyboard users reenter and edit a selected filter', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<FilterBar
|
||||
filterProperties={mockFilterProperties}
|
||||
filters={{
|
||||
logicalOperator: 'AND',
|
||||
conditions: [{ propertyName: 'name', operator: '=', value: 'test' }],
|
||||
}}
|
||||
onFilterChange={mockOnFilterChange}
|
||||
freeformText=""
|
||||
onFreeformTextChange={mockOnFreeformTextChange}
|
||||
/>
|
||||
)
|
||||
|
||||
await user.tab()
|
||||
expect(screen.getByLabelText('Change property from Name')).toHaveFocus()
|
||||
await user.tab()
|
||||
expect(screen.getByLabelText('Operator for Name')).toHaveFocus()
|
||||
await user.tab()
|
||||
expect(screen.getByLabelText('Value for Name')).toHaveFocus()
|
||||
await user.tab()
|
||||
expect(screen.getByRole('button', { name: 'Remove Name filter' })).toHaveFocus()
|
||||
await user.tab({ shift: true })
|
||||
expect(screen.getByLabelText('Value for Name')).toHaveFocus()
|
||||
})
|
||||
|
||||
it('clears the chip highlight when focus moves to its remove button', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<FilterBar
|
||||
filterProperties={mockFilterProperties}
|
||||
filters={{
|
||||
logicalOperator: 'AND',
|
||||
conditions: [{ propertyName: 'name', operator: '=', value: 'test' }],
|
||||
}}
|
||||
onFilterChange={mockOnFilterChange}
|
||||
freeformText=""
|
||||
onFreeformTextChange={mockOnFreeformTextChange}
|
||||
/>
|
||||
)
|
||||
|
||||
await user.click(screen.getByTestId('filter-bar-freeform-input'))
|
||||
await user.keyboard('{ArrowLeft}')
|
||||
const condition = screen.getByTestId('filter-condition-name')
|
||||
expect(condition).toHaveAttribute('data-highlighted', 'true')
|
||||
|
||||
await user.tab({ shift: true })
|
||||
expect(screen.getByRole('button', { name: 'Remove Name filter' })).toHaveFocus()
|
||||
expect(condition).toHaveAttribute('data-highlighted', 'false')
|
||||
})
|
||||
|
||||
it('lets keyboard users remove a selected filter', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onFilterChange = vi.fn()
|
||||
render(
|
||||
<FilterBar
|
||||
filterProperties={mockFilterProperties}
|
||||
filters={{
|
||||
logicalOperator: 'AND',
|
||||
conditions: [{ propertyName: 'name', operator: '=', value: 'test' }],
|
||||
}}
|
||||
onFilterChange={onFilterChange}
|
||||
freeformText=""
|
||||
onFreeformTextChange={mockOnFreeformTextChange}
|
||||
/>
|
||||
)
|
||||
|
||||
await user.tab()
|
||||
await user.tab()
|
||||
await user.tab()
|
||||
await user.tab()
|
||||
expect(screen.getByRole('button', { name: 'Remove Name filter' })).toHaveFocus()
|
||||
await user.keyboard('{Enter}')
|
||||
expect(onFilterChange).toHaveBeenCalledWith(initialFilters)
|
||||
await waitFor(() => expect(screen.getByTestId('filter-bar-freeform-input')).toHaveFocus())
|
||||
})
|
||||
|
||||
it('opens group popover and allows selecting a property', async () => {
|
||||
const user = userEvent.setup()
|
||||
let currentFilters = initialFilters
|
||||
|
||||
@@ -59,7 +59,7 @@ function FilterBarContent({ className }: { className?: string }) {
|
||||
<div className="w-full space-y-2 relative">
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex items-stretch gap-0 w-full border rounded-md h-full bg-foreground/[.026] cursor-text p-0 pr-2 overflow-auto',
|
||||
'relative flex items-stretch gap-0 w-full border rounded-md h-full bg-foreground/[.026] cursor-text p-0 overflow-auto',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -40,6 +40,7 @@ export type FilterBarContextValue = {
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
highlightedConditionPath: number[] | null
|
||||
setHighlightedConditionPath: (path: number[] | null) => void
|
||||
|
||||
onFilterChange: (filters: FilterGroup) => void
|
||||
commitFilters: (filters: FilterGroup) => void
|
||||
@@ -370,6 +371,7 @@ export const FilterBarRoot = forwardRef<FilterBarHandle, FilterBarRootProps>(fun
|
||||
isLoading: loading,
|
||||
error,
|
||||
highlightedConditionPath,
|
||||
setHighlightedConditionPath,
|
||||
|
||||
onFilterChange,
|
||||
commitFilters,
|
||||
|
||||
@@ -41,8 +41,10 @@ export function FilterCondition({
|
||||
handlePropertyChange,
|
||||
handleKeyDown,
|
||||
handleRemoveCondition,
|
||||
rootRef,
|
||||
handleSelectMenuItem,
|
||||
setActiveInput,
|
||||
setHighlightedConditionPath,
|
||||
variant,
|
||||
} = useFilterBar()
|
||||
|
||||
@@ -253,9 +255,19 @@ export function FilterCondition({
|
||||
[handleInputChange, path]
|
||||
)
|
||||
|
||||
const onRemove = useCallback(() => {
|
||||
handleRemoveCondition(path)
|
||||
}, [handleRemoveCondition, path])
|
||||
const onRemove = useCallback(
|
||||
(event?: React.MouseEvent<HTMLButtonElement>) => {
|
||||
handleRemoveCondition(path)
|
||||
if (event?.detail === 0) {
|
||||
window.setTimeout(() => {
|
||||
rootRef.current
|
||||
?.querySelector<HTMLInputElement>('[data-testid="filter-bar-freeform-input"]')
|
||||
?.focus()
|
||||
}, 0)
|
||||
}
|
||||
},
|
||||
[handleRemoveCondition, path, rootRef]
|
||||
)
|
||||
|
||||
if (!property) return null
|
||||
|
||||
@@ -263,10 +275,12 @@ export function FilterCondition({
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className={cn(
|
||||
'flex items-stretch px-0 h-[26px] bg-muted group shrink-0',
|
||||
variant === 'pill' ? 'rounded-sm border' : 'border-r',
|
||||
isHighlighted && 'ring-2 ring-primary-bright'
|
||||
'relative flex items-stretch px-0 bg-muted group shrink-0',
|
||||
variant === 'pill' ? 'h-[26px] rounded-sm border' : 'self-stretch border-r',
|
||||
isHighlighted &&
|
||||
(variant === 'pill' ? 'ring-2 ring-ring' : 'rounded-sm ring-2 ring-inset ring-ring')
|
||||
)}
|
||||
onFocusCapture={() => setHighlightedConditionPath(null)}
|
||||
data-testid={`filter-condition-${property.name}`}
|
||||
data-highlighted={isHighlighted}
|
||||
>
|
||||
@@ -280,7 +294,7 @@ export function FilterCondition({
|
||||
onChange={(e) => setPropertySearchText(e.target.value)}
|
||||
onBlur={handlePropertyBlur}
|
||||
onKeyDown={handlePropertyKeyDown}
|
||||
className="h-full border-none bg-transparent py-0 pl-2 pr-1 text-xs hover:border-transparent focus:border-transparent focus-visible:border-transparent focus:outline-hidden focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 text-foreground-light w-full absolute left-0 top-0"
|
||||
className="h-full border-none bg-transparent py-0 pl-2 pr-1 text-xs md:text-xs leading-4 hover:border-transparent focus:border-transparent focus-visible:border-transparent focus:outline-hidden focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 text-foreground-light w-full absolute left-0 top-0"
|
||||
placeholder={property.label}
|
||||
autoFocus
|
||||
aria-label={`Change property from ${property.label}`}
|
||||
@@ -292,15 +306,22 @@ export function FilterCondition({
|
||||
data-form-type="other"
|
||||
/>
|
||||
) : null}
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs pl-2 pr-1 shrink-0 whitespace-nowrap text-foreground-light h-full flex items-center cursor-pointer hover:text-foreground transition-colors',
|
||||
isPropertyActive && 'invisible'
|
||||
)}
|
||||
onClick={() => handleLabelClick(path)}
|
||||
>
|
||||
{property.label}
|
||||
</span>
|
||||
{isPropertyActive ? (
|
||||
<span className="invisible text-xs leading-4 pl-2 pr-1 shrink-0 whitespace-nowrap h-full flex items-center">
|
||||
{property.label}
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className="text-xs leading-4 pl-2 pr-1 shrink-0 whitespace-nowrap text-foreground-light h-full flex items-center cursor-pointer hover:text-foreground transition-colors"
|
||||
aria-label={`Change property from ${property.label}`}
|
||||
onFocus={() => handleLabelClick(path)}
|
||||
onClick={() => handleLabelClick(path)}
|
||||
>
|
||||
{property.label}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</PopoverAnchor>
|
||||
<PopoverContent
|
||||
@@ -341,7 +362,7 @@ export function FilterCondition({
|
||||
className="h-full border-none bg-transparent py-0 px-1 text-center text-xs md:text-xs hover:border-transparent focus:border-transparent focus-visible:border-transparent focus:outline-hidden focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 text-foreground w-full absolute left-0 top-0"
|
||||
aria-label={`Operator for ${property.label}`}
|
||||
data-testid={`filter-operator-${property.name}`}
|
||||
tabIndex={-1}
|
||||
tabIndex={0}
|
||||
autoComplete="off"
|
||||
data-1p-ignore
|
||||
data-lpignore="true"
|
||||
@@ -386,10 +407,14 @@ export function FilterCondition({
|
||||
onFocus={() => handleInputFocus(path)}
|
||||
onBlur={handleValueBlur}
|
||||
onKeyDown={handleValueKeyDown}
|
||||
className="h-full border-none bg-transparent py-0 px-1 text-xs md:text-xs hover:border-transparent focus:border-transparent focus-visible:border-transparent focus:outline-hidden focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 w-full absolute left-0 top-0"
|
||||
className={cn(
|
||||
'h-full border-none bg-transparent py-0 px-1 text-xs md:text-xs hover:border-transparent focus:border-transparent focus-visible:border-transparent focus:outline-hidden focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 w-full absolute left-0 top-0',
|
||||
hasFormattedCustomValue &&
|
||||
'focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring'
|
||||
)}
|
||||
aria-label={`Value for ${property.label}`}
|
||||
data-testid={`filter-value-${property.name}`}
|
||||
tabIndex={-1}
|
||||
tabIndex={0}
|
||||
autoComplete="off"
|
||||
data-1p-ignore
|
||||
data-lpignore="true"
|
||||
@@ -453,9 +478,8 @@ export function FilterCondition({
|
||||
/>
|
||||
}
|
||||
onClick={onRemove}
|
||||
className="group hover:text-foreground hover:!bg-surface-600 rounded-none px-1 h-auto py-0"
|
||||
className="group relative !h-[18px] !w-[18px] shrink-0 self-center m-[3px] rounded-sm !p-0 hover:text-foreground hover:!bg-surface-600 focus-visible:z-20 focus-visible:!bg-transparent focus-visible:ring-2 focus-visible:ring-offset-0"
|
||||
aria-label={`Remove ${property.label} filter`}
|
||||
tabIndex={-1}
|
||||
data-testid={`filter-remove-${property.name}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -206,7 +206,7 @@ export function FilterGroup({ group, path }: FilterGroupProps) {
|
||||
onFocus={() => handleGroupFreeformFocus(path)}
|
||||
onBlur={handleFreeformBlur}
|
||||
onKeyDown={handleFreeformKeyDown}
|
||||
className="border-none bg-transparent text-xs hover:border-transparent focus:border-transparent focus-visible:border-transparent focus:outline-hidden focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 w-full flex-1 h-auto min-w-0 px-2 py-1"
|
||||
className="relative m-[3px] border-none bg-transparent text-xs hover:border-transparent focus:border-transparent focus-visible:border-transparent focus:outline-hidden focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 w-auto flex-1 h-auto min-w-0 px-2 py-1"
|
||||
placeholder={
|
||||
group.conditions.length === 0 ? emptyPlaceholder : 'Add more filters...'
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user