Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
"types": "./dist/components/FilterBar/index.d.ts",
"default": "./dist/components/FilterBar/index.js"
},
"./FilterBarSearchIcon": {
"types": "./dist/components/FilterBarSearchIcon/index.d.ts",
"default": "./dist/components/FilterBarSearchIcon/index.js"
},
"./FilterGroupItem": {
"types": "./dist/components/FilterGroupItem/index.d.ts",
"default": "./dist/components/FilterGroupItem/index.js"
Expand Down
16 changes: 15 additions & 1 deletion packages/main/src/components/FilterBar/FilterBar.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ArgTypesWithNote, ControlsWithNote, DocsHeader, Footer } from '@sb/components';
import SubcomponentsSection from '@sb/docs/SubcomponentsSection.md?raw';
import { Canvas, Description, Markdown, Meta } from '@storybook/addon-docs/blocks';
import { FilterBarSearchIcon } from '../FilterBarSearchIcon';
import { FilterGroupItem } from '../FilterGroupItem';
import * as ComponentStories from './FilterBar.stories';

<Meta of={ComponentStories} />

<DocsHeader of={ComponentStories} subComponents={['FilterGroupItem']} />
<DocsHeader of={ComponentStories} subComponents={['FilterGroupItem', 'FilterBarSearchIcon']} />

<br />

Expand Down Expand Up @@ -407,11 +408,24 @@ const handleInput = (event) => {

<Canvas of={ComponentStories.IdentifyInputEventOrigin} source />

## Trigger the search via the search icon

By default, the search icon of the `search` input is decorative. To let users trigger the search by activating the search icon (in addition to pressing <kbd>Enter</kbd> in the input), pass the `FilterBarSearchIcon` subcomponent to the `icon` prop of the search input and wire its `onClick` handler.

This is recommended when the "Go" button is displayed (`showGoOnFB`). The `FilterBarSearchIcon` is pre-controlled — you decide what "trigger search" means in its `onClick` handler.

<Canvas of={ComponentStories.WithInteractiveSearchIcon} />

<Markdown>{SubcomponentsSection}</Markdown>

## FilterGroupItem

<Description of={FilterGroupItem} />
<ArgTypesWithNote metaOf={ComponentStories} of={FilterGroupItem} exclude={['data-index']} />

## FilterBarSearchIcon

<Description of={FilterBarSearchIcon} />
<ArgTypesWithNote metaOf={ComponentStories} of={FilterBarSearchIcon} />

<Footer />
91 changes: 91 additions & 0 deletions packages/main/src/components/FilterBar/FilterBar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import TitleLevel from '@ui5/webcomponents/dist/types/TitleLevel.js';
import { useId, useReducer, useRef, useState } from 'react';
import { fn } from 'storybook/test';
import { FlexBoxDirection } from '../../enums/index.js';
import type { ComboBoxDomRef } from '../../webComponents/ComboBox/index.js';
import type { DatePickerDomRef } from '../../webComponents/DatePicker/index.js';
import {
ComboBox,
ComboBoxItem,
Expand All @@ -24,7 +26,9 @@ import {
Title,
Token,
} from '../../webComponents/index.js';
import type { InputDomRef } from '../../webComponents/Input/index.js';
import { Text } from '../../webComponents/Text/index.js';
import { FilterBarSearchIcon } from '../FilterBarSearchIcon/index.js';
import { FilterGroupItem } from '../FilterGroupItem/index.js';
import { FlexBox } from '../FlexBox/index.js';
import { VariantItem } from '../VariantItem/index.js';
Expand Down Expand Up @@ -623,3 +627,90 @@ export const IdentifyInputEventOrigin: Story = {
);
},
};

interface SearchSubmission {
searchTerm: string;
date: string;
category: string;
}

const initialSubmission: SearchSubmission = { searchTerm: '', date: '', category: '' };

function submissionReducer(state: SearchSubmission, action: { type: 'submit'; payload: SearchSubmission }) {
switch (action.type) {
case 'submit':
return action.payload;
default:
return state;
}
}

export const WithInteractiveSearchIcon: Story = {
name: 'with interactive search icon',
args: {
showGoOnFB: true,
},
render(args) {
const searchRef = useRef<InputDomRef>(null);
const dateRef = useRef<DatePickerDomRef>(null);
const categoryRef = useRef<ComboBoxDomRef>(null);
const [submission, dispatch] = useReducer(submissionReducer, initialSubmission);
// pre-controlled: the consumer decides what "trigger search" means
const triggerSearch = () => {
dispatch({
type: 'submit',
payload: {
searchTerm: searchRef.current?.value ?? '',
date: dateRef.current?.value ?? '',
category: categoryRef.current?.value ?? '',
},
});
};
return (
<>
<FilterBar
{...args}
onGo={triggerSearch}
search={
<Input
ref={searchRef}
// triggers on Enter / focusout
onChange={triggerSearch}
// triggers on click/tap or Enter/Space on the icon
icon={<FilterBarSearchIcon onClick={triggerSearch} />}
/>
}
>
<FilterGroupItem filterKey="input" label="Input">
<Input placeholder="Placeholder" />
</FilterGroupItem>
<FilterGroupItem filterKey="date" label="Date">
<DatePicker ref={dateRef} style={{ minWidth: 'auto' }} />
</FilterGroupItem>
<FilterGroupItem filterKey="category" label="Category">
<ComboBox ref={categoryRef}>
<ComboBoxItem text="Hardware" />
<ComboBoxItem text="Software" />
<ComboBoxItem text="Services" />
</ComboBox>
</FilterGroupItem>
</FilterBar>
<br />
<FlexBox direction={FlexBoxDirection.Column}>
<FlexBox>
<Label showColon>Submitted search term</Label>
<Text>{submission.searchTerm}</Text>
</FlexBox>
<FlexBox>
<Label showColon>Submitted date</Label>
<Text>{submission.date}</Text>
</FlexBox>
<FlexBox>
<Label showColon>Submitted category</Label>
<Text>{submission.category}</Text>
</FlexBox>
</FlexBox>
</>
);
},
};
2 changes: 1 addition & 1 deletion packages/main/src/components/FilterBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ const FilterBar = forwardRef<HTMLDivElement, FilterBarPropTypes>((props, ref) =>
noTypeahead: search.props.noTypeahead ?? true,
showClearIcon: true,
type: InputType.Search,
icon: <Icon name={searchIcon} />,
icon: search.props.icon ?? <Icon name={searchIcon} />,
})}
</div>
</FilterGroupItem>
Expand Down
4 changes: 3 additions & 1 deletion packages/main/src/components/FilterBar/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ export interface FilterBarPropTypes extends CommonProps {
/**
* Defines the search field rendered as first filter item.
*
* __Note:__ By default, `placeholder`, `icon`, `noTypeahead`, `showClearIcon`, and `type` are applied to the search input. Only `placeholder` and `noTypeahead` can be overridden.
* __Note:__
* - By default, `placeholder`, `icon`, `noTypeahead`, `showClearIcon`, and `type` are applied to the search input. Only `placeholder`, `noTypeahead`, and `icon` can be overridden.
* - By default, the search icon is decorative. If the "Go" button is displayed (`showGoOnFB`), pass the `FilterBarSearchIcon` subcomponent to the `icon` prop of the search input to render an interactive search icon that can trigger the search.
*
* __Note:__ The field is only available in the FilterBar not inside the filter configuration dialog.
*/
Expand Down
37 changes: 37 additions & 0 deletions packages/main/src/components/FilterBarSearchIcon/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';

import searchIcon from '@ui5/webcomponents-icons/dist/search.js';
import { useI18nBundle } from '@ui5/webcomponents-react-base';
import { forwardRef } from 'react';
import { SEARCH } from '../../i18n/i18n-defaults.js';
import type { InputIconDomRef, InputIconPropTypes } from '../../webComponents/InputIcon/index.js';
import { InputIcon } from '../../webComponents/InputIcon/index.js';

export interface FilterBarSearchIconPropTypes extends Omit<InputIconPropTypes, 'name' | 'accessibleName'> {
/**
* Defines the accessible name of the icon.
*
* **Note:** This property is used for accessibility purposes and will be announced by screen readers.
* When set, it is also rendered as a native `title` tooltip.
* @default the translated "Search" text
*/
accessibleName?: InputIconPropTypes['accessibleName'];
}

/**
* The `FilterBarSearchIcon` is an interactive search icon intended to be passed to the `icon` prop of the `FilterBar`'s `search` input.
*
* Unlike the default decorative search icon, it offers button-like behavior, which allows the search to be triggered by activating the icon. This is required when the "Go" button is displayed.
*/
const FilterBarSearchIcon = forwardRef<InputIconDomRef, FilterBarSearchIconPropTypes>((props, ref) => {
const { accessibleName, ...rest } = props;
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');

return (
<InputIcon ref={ref} accessibleName={accessibleName || i18nBundle.getText(SEARCH)} {...rest} name={searchIcon} />
);
});

FilterBarSearchIcon.displayName = 'FilterBarSearchIcon';

export { FilterBarSearchIcon };
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useRef, useState } from 'react';
import type { InputDomRef } from '../../../webComponents/Input/index.js';
import { Input } from '../../../webComponents/Input/index.js';
import { FilterBar } from '../../FilterBar/index.js';
import { FilterGroupItem } from '../../FilterGroupItem/index.js';
import { FilterBarSearchIcon } from '../index.js';

export const FilterBarSearchIconTestComp = () => {
const inputRef = useRef<InputDomRef>(null);
const [clickCount, setClickCount] = useState(0);
const [submittedTerm, setSubmittedTerm] = useState('');
// pre-controlled: the consumer decides what "trigger search" means
const triggerSearch = () => {
setClickCount((prev) => prev + 1);
setSubmittedTerm(inputRef.current?.value ?? '');
};
return (
<>
<FilterBar showGoOnFB search={<Input ref={inputRef} icon={<FilterBarSearchIcon onClick={triggerSearch} />} />}>
<FilterGroupItem filterKey="input" label="Input">
<Input placeholder="Placeholder" />
</FilterGroupItem>
</FilterBar>
<span data-testid="search-click-count">{clickCount}</span>
<span data-testid="submitted-term">{submittedTerm}</span>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expect, test } from '../../../../../../playwright/fixtures/gallery-fixtures.js';

test.describe('FilterBarSearchIcon', () => {
test('triggers the search handler when the icon is activated', async ({ mount, page, ui5wc }) => {
await mount('FilterBarSearchIcon/FilterBarSearchIconTestComp');

const searchInput = page.locator('[ui5-input]').first();
await expect(searchInput).toBeVisible();
await ui5wc.typeIntoInput(searchInput, 'quarterly report');

await expect(page.getByTestId('search-click-count')).toHaveText('0');

await page.locator('[ui5-input-icon][slot="icon"]').click();

await expect(page.getByTestId('search-click-count')).toHaveText('1');
await expect(page.getByTestId('submitted-term')).toHaveText('quarterly report');
});
});
1 change: 1 addition & 0 deletions packages/main/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type { ObjectPageHeaderPropTypes } from './components/ObjectPageHeader/in
export { ObjectPageTitle } from './components/ObjectPageTitle/index.js';
export type { ObjectPageTitlePropTypes } from './components/ObjectPageTitle/index.js';
export * from './components/FilterBar/index.js';
export * from './components/FilterBarSearchIcon/index.js';
export * from './components/FilterGroupItem/index.js';
export * from './components/FlexBox/index.js';
export * from './components/Grid/index.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/src/utils/component-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const REACT_COMPONENT_CATEGORIES = {
'ThemeProvider',
'FilterBar',
],
Inputs: ['FilterGroupItem', 'VariantManagement', 'VariantItem', 'MessageViewButton'],
Inputs: ['FilterGroupItem', 'FilterBarSearchIcon', 'VariantManagement', 'VariantItem', 'MessageViewButton'],
'Modals & Popovers': ['MessageBox', 'ActionSheet', 'SelectDialog'],
'User Feedback': ['MessageView', 'MessageItem', 'Modals'],
};
Expand Down
1 change: 1 addition & 0 deletions playwright/gallery/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default defineConfig({
withCoverage &&
IstanbulPlugin({
include: [
'packages/main/src/components/FilterBarSearchIcon/**/*',
'packages/main/src/components/SelectDialog/**/*',
'packages/main/src/components/Splitter*/**/*',
'packages/charts/src/**/*',
Expand Down
Loading