Patterns

Search Bar

AppSearchBar — the list-page filter strip: a keyword input, Popover-backed filter tags, and an Add-filters dropdown held in one flex shell with exactly one fixed gap.

Source in the MFEs
AppSearchBar.vuereference-fe pages/index.tsxPolicyManagement.vueGroupManagement.vue

AppSearchBar

Component

Search input + filter tags + Add-filter dropdown, with a fixed non-collapsing gap.

Download .zip
Install tosrc/components/common/Requiresantd
  • TSXAppSearchBar.tsx
  • CSSapp-search-bar.css
  • MDREADME.md

Live example

The filter strip that sits between the action bar and the table on every list page: a keyword Input with a SearchOutlined prefix (minWidth: 280 / maxWidth: 520) on the left, the active filters as closable Tags — each wrapped in a Popover holding its editor and an Apply button — and the Add filters Dropdown at the end.

Not the button toolbar
This is not the Create / Delete / Refresh row above it — that is the Action Bar pattern (AppActionBar). AppSearchBar only owns searching and filtering.
Preview
Status: ACTIVEType: CURRENCY

Click a tag to edit its value in a popover; close both tags and the input still keeps exactly one gap to the Add-filters button.

// React — reference-fe/src/pages/index.tsx. Order is fixed:
// keyword Input → active filter Tags (in Popovers) → Add-filters Dropdown.
<AppSearchBar gap={4}>
  <Input placeholder='Search code...' value={draftSearch.code}
    onChange={(e) => setDraftSearch({...draftSearch, code: e.target.value})}
    prefix={<SearchOutlined />} style={{minWidth: 280, maxWidth: 520}}
  />

  {/* Active filter tags with Popover UI inside */}
  {activeFilterKeys.map(key => (
    <Popover key={key} trigger='click' open={openPopovers[key]}
      content={<div><Input value={draftSearch[key]}/><Button type='primary' onClick={handleApplyFilters}>Apply</Button></div>}>
      <Tag closable onClose={() => handleCloseFilter(key)}>
        {filterLabelMap[key]}: {draftSearch[key]}
      </Tag>
    </Popover>
  ))}

  <Dropdown menu={{items: availableFilterOptions.map(opt => ({key: opt.key, label: opt.labelKey}))}}>
    <Button><i className='fa-filter'/> Add filters</Button>
  </Dropdown>
</AppSearchBar>

Slots & props

The Vue component (administrator-fe/src/components/common/AppSearchBar.vue) exposes three regions as slots; the React usage passes the same three regions as ordered children plus a gap prop. Whatever the stack, the order never changes: search field, filter tags, Add-filters button.

NameTypeDefaultDescription
#searchslot / first childThe keyword Input. Always prefix={<SearchOutlined />}, allowClear, and style={{ minWidth: 280, maxWidth: 520 }}. pressEnter applies the filters.
defaultslot / childrenThe active filter tags — each a closable Tag wrapped in a click-trigger Popover that holds the filter editor (Input/Select) and an Apply button.
#add-filterslot / last childThe Add-filters Dropdown button. Its menu lists availableFilterOptions — the filters not yet active; picking one adds a new tag.
gapnumber4Gap between the regions of the flex shell (React usage: <AppSearchBar gap={4}>).

Layout contract

Two visual invariants make every list page read the same, whether zero or five filters are applied:

Exactly one gap — even with zero tags
The shell is a single flexbox with one fixed gap, and the Add-filters button lives inside the filters region, not in a separate container. So with no tags the input sits exactly one gap from the button, and each added tag just slots into the same rhythm — nothing jumps or re-aligns.
Tags are locked to 13px
AppSearchBar pins the filter tag font-size to 13px via :deep(.ant-tag), so the label never changes size between the draft state (tag just added, no value yet) and the applied state (label + value). Don’t override it per page.

Rules

Every filter is a tag in a popover
Clicking a tag opens its Popover editor; Apply commits the draft value. Closing the tag removes the filter and returns that field to the Add-filters menu. Never render bare filter inputs in the bar.
The dropdown retires itself
PolicyManagement guards the button with v-if="availableFilterOptions.length > 0" — when every filter is already active, the Add-filters button disappears instead of opening an empty menu.
Position in the stack
AppSearchBar always comes right after AppActionBar and right before the table. See the List Screen pattern for the full four-band recipe.