diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index faad6677b5..16d0046279 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1585,6 +1585,9 @@ importers:
'@semcore/input':
specifier: ^17.2.0
version: link:../input
+ '@semcore/spin':
+ specifier: ^17.2.0
+ version: link:../spin
'@semcore/typography':
specifier: ^17.2.0
version: link:../typography
diff --git a/semcore/base-components/src/components/flex-box/screen-reader-only-box/ScreenReaderOnlyBox.tsx b/semcore/base-components/src/components/flex-box/screen-reader-only-box/ScreenReaderOnlyBox.tsx
index 603df2d322..1ff2358845 100644
--- a/semcore/base-components/src/components/flex-box/screen-reader-only-box/ScreenReaderOnlyBox.tsx
+++ b/semcore/base-components/src/components/flex-box/screen-reader-only-box/ScreenReaderOnlyBox.tsx
@@ -4,13 +4,44 @@ import React from 'react';
import style from './screenReaderOnlyBox.shadow.css';
import Box from '../Box';
-function ScreenReaderOnlyComponent() {
+type SROnlyType = {
+ ariaLive?: boolean;
+ children?: React.ReactNode;
+};
+
+function ScreenReaderOnlyComponent(props: SROnlyType) {
const SScreenReaderOnly = Root;
+ const { ariaLive, children } = props;
+ const [content, setContent] = React.useState(ariaLive ? null : children);
+
+ React.useEffect(() => {
+ if (!ariaLive) {
+ setContent(children);
+ return;
+ }
+
+ const timer = setTimeout(() => {
+ setContent(children);
+ }, 100);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [children]);
- return sstyled(style)();
+ return sstyled(style)(
+
+ {content}
+ ,
+ );
};
-type ScreenReaderOnlyType = Intergalactic.Component<'span'>;
+type ScreenReaderOnlyType = Intergalactic.Component<'span', SROnlyType>;
export const ScreenReaderOnly = createComponent<
ScreenReaderOnlyType,
diff --git a/semcore/date-picker/__tests__/date-range-comparator.browser-test.tsx b/semcore/date-picker/__tests__/date-range-comparator.browser-test.tsx
index 488b4331e7..352c7ae510 100644
--- a/semcore/date-picker/__tests__/date-range-comparator.browser-test.tsx
+++ b/semcore/date-picker/__tests__/date-range-comparator.browser-test.tsx
@@ -116,6 +116,7 @@ test.describe(`${TAG.VISUAL}`, () => {
TAG.MOUSE,
'@date-picker'],
}, async ({ page }) => {
+ await page.clock.setFixedTime(new Date('2024-01-15T12:00:00'));
await loadPage(page, 'stories/components/date-picker/docs/examples/date_range_comparator_advanced_use.tsx', 'en');
const from = page.locator('[data-ui-name="DateRangeComparator.ValueDateRange"]').first();
diff --git a/semcore/dropdown-menu/__tests__/dropdown-menu.browser-test.tsx b/semcore/dropdown-menu/__tests__/dropdown-menu.browser-test.tsx
index e07e218924..28489627ec 100644
--- a/semcore/dropdown-menu/__tests__/dropdown-menu.browser-test.tsx
+++ b/semcore/dropdown-menu/__tests__/dropdown-menu.browser-test.tsx
@@ -703,7 +703,6 @@ test.describe(`${TAG.VISUAL} `, () => {
await expect(search).toBeFocused();
await page.keyboard.press('Tab');
- if (browserName == 'firefox') await page.keyboard.press('Tab'); // because in ff one additional focus on the list (bug)
await expect(locators.item(page).nth(30)).toHaveClass(/highlighted/);
});
@@ -1746,9 +1745,9 @@ test.describe(`${TAG.FUNCTIONAL}`, () => {
});
await test.step('Verify result count is exposed to screen readers only', async () => {
- const status = page.locator('#search-result');
+ const status = page.getByRole('status');
await expect(status).toContainText('2 results found');
- await expect(status).toHaveAttribute('aria-hidden', 'true');
+ await expect(status).toHaveAttribute('aria-live', 'polite');
await expect(page.locator('text="Nothing found"')).toHaveCount(0);
});
});
diff --git a/semcore/dropdown-menu/src/DropdownMenu.jsx b/semcore/dropdown-menu/src/DropdownMenu.jsx
index a2f19d8898..0b2775aa5a 100644
--- a/semcore/dropdown-menu/src/DropdownMenu.jsx
+++ b/semcore/dropdown-menu/src/DropdownMenu.jsx
@@ -331,10 +331,14 @@ function List({ styles, Children }) {
const SBar = ScrollAreaComponent.Bar;
const SScrollContainer = ScrollAreaComponent.Container;
+ const preventFocusByClick = React.useCallback((e) => {
+ e.preventDefault();
+ }, []);
+
return sstyled(styles)(
-
+
diff --git a/semcore/dropdown/__tests__/index.test.tsx b/semcore/dropdown/__tests__/index.test.tsx
index 7e6af5bea0..ccdcb68c61 100644
--- a/semcore/dropdown/__tests__/index.test.tsx
+++ b/semcore/dropdown/__tests__/index.test.tsx
@@ -4,6 +4,7 @@ import {
render,
userEvent,
screen,
+ waitFor,
} from '@semcore/testing-utils/testing-library';
import { expect, test, describe, beforeEach, vi } from '@semcore/testing-utils/vitest';
import React from 'react';
@@ -30,16 +31,19 @@ describe('Dropdown.StatusItem', () => {
test('Verify renders screen reader result count', () => {
render();
- const status = screen.getByText('2 results found');
- expect(status).toBeInTheDocument();
- expect(status).toHaveAttribute('id', 'search-result');
- expect(status).toHaveAttribute('aria-hidden', 'true');
+ waitFor(() => {
+ const status = screen.getByRole('status', { name: '2 results found' });
+ expect(status).toBeInTheDocument();
+ expect(status).toHaveAttribute('aria-live', 'polite');
+ });
});
test('Verify renders singular result count', () => {
render();
- expect(screen.getByText('1 result found')).toBeInTheDocument();
+ waitFor(() => {
+ expect(screen.getByRole('status', { name: '1 result found' })).toBeInTheDocument();
+ });
});
test('Verify renders loading and error states', () => {
diff --git a/semcore/dropdown/src/AbstractDropdown.tsx b/semcore/dropdown/src/AbstractDropdown.tsx
index 439e53025e..b3320f0575 100644
--- a/semcore/dropdown/src/AbstractDropdown.tsx
+++ b/semcore/dropdown/src/AbstractDropdown.tsx
@@ -312,10 +312,10 @@ export abstract class AbstractDropdown extends Component
- {children ?? text}
-
+ <>
+
+ {children ?? text}
+
+
+ {children ?? text}
+
+ >
);
}
@@ -61,7 +66,7 @@ class StatusItemRoot extends Component<
return this.renderText(getI18nText('StatusItem.defaultState.nothingFound'), children);
} else {
return (
-
+
{children ?? getI18nText('StatusItem.defaultState.ScreenReaderOnlyText', { count: itemsCount })}
);
diff --git a/semcore/select/__tests__/filters-with-select/serp-features.browser-test.tsx b/semcore/select/__tests__/filters-with-select/serp-features.browser-test.tsx
index 5ee156d440..cd12802c2b 100644
--- a/semcore/select/__tests__/filters-with-select/serp-features.browser-test.tsx
+++ b/semcore/select/__tests__/filters-with-select/serp-features.browser-test.tsx
@@ -13,7 +13,7 @@ const locators = {
ellipsisHint: (page: Page) => page.locator('[data-ui-name="Hint"]'),
clear: (page: Page) => page.getByRole('button', { name: 'Clear' }),
loadingText: (page: Page) => page.getByText('Loading...'),
- errorText: (page: Page) => page.getByText('Something went wrong.'),
+ errorText: (page: Page) => page.getByText('Something went wrong.').first(),
clearSearchHint: (page: Page) => page.getByText('Clear search field'),
optionByName: (page: Page, name: string) => page.getByRole('option', { name }),
textByContent: (page: Page, text: string) => page.getByText(text),
diff --git a/semcore/select/__tests__/index.test.tsx b/semcore/select/__tests__/index.test.tsx
index 8d59c8b632..f706f72328 100644
--- a/semcore/select/__tests__/index.test.tsx
+++ b/semcore/select/__tests__/index.test.tsx
@@ -1,9 +1,9 @@
import { runDependencyCheckTests } from '@semcore/testing-utils/shared-tests';
-import { cleanup, render, userEvent, waitFor } from '@semcore/testing-utils/testing-library';
+import { cleanup, render, screen, userEvent, waitFor } from '@semcore/testing-utils/testing-library';
import { expect, test, describe, beforeEach, vi } from '@semcore/testing-utils/vitest';
import React from 'react';
-import Select, { InputSearch } from '../src';
+import Select, { AutoSuggest, InputSearch } from '../src';
describe('select Dependency imports', () => {
runDependencyCheckTests('select');
@@ -275,3 +275,73 @@ describe('InputSearch', () => {
unmount();
});
});
+
+describe('AutoSuggest', () => {
+ const BREEDS = ['Persian', 'British Shorthair', 'Sphynx', 'Bengal'];
+
+ beforeEach(() => {
+ cleanup();
+
+ const mockIntersectionObserver = vi.fn();
+ mockIntersectionObserver.mockReturnValue({
+ observe: () => null,
+ unobserve: () => null,
+ disconnect: () => null,
+ });
+ window.IntersectionObserver = mockIntersectionObserver;
+ });
+
+ test('Verify forwards className, data-* to root and ref to a DOM node', () => {
+ const ref = React.createRef();
+ render(
+ ,
+ );
+
+ const root = screen.getByTestId('as-root');
+ expect(root).toBeInTheDocument();
+ expect(root.className).toContain('my-autosuggest');
+ expect(ref.current).toBeInstanceOf(HTMLElement);
+ });
+
+ test('Verify uses defaultValue in uncontrolled mode', () => {
+ render();
+ expect((screen.getByRole('combobox') as HTMLInputElement).value).toBe('p');
+ });
+
+ test('Verify calls onChange with (value, event) while typing', async () => {
+ const spy = vi.fn();
+ const Wrapper = () => {
+ const [value, setValue] = React.useState('');
+ return (
+ {
+ setValue(next);
+ spy(next, event);
+ }}
+ suggestions={BREEDS}
+ />
+ );
+ };
+ render();
+
+ const input = screen.getByRole('combobox');
+ await userEvent.click(input);
+ await userEvent.type(input, 'p');
+
+ expect(spy).toHaveBeenLastCalledWith('p', expect.anything());
+ });
+
+ test('Verify has no default "Select option" placeholder', () => {
+ render();
+
+ const input = screen.getByRole('combobox');
+ expect(input.getAttribute('placeholder') ?? '').not.toBe('Select option');
+ });
+});
diff --git a/semcore/select/__tests__/select.browser-test.tsx b/semcore/select/__tests__/select.browser-test.tsx
index 48bd9bdc45..3be770af4d 100644
--- a/semcore/select/__tests__/select.browser-test.tsx
+++ b/semcore/select/__tests__/select.browser-test.tsx
@@ -813,9 +813,9 @@ test.describe(`${TAG.FUNCTIONAL} `, () => {
});
await test.step('Verify result count is exposed to screen readers only', async () => {
- const status = page.locator('#search-result');
+ const status = page.getByRole('status');
await expect(status).toContainText('1 result found');
- await expect(status).toHaveAttribute('aria-hidden', 'true');
+ await expect(status).toHaveAttribute('aria-live', 'polite');
await expect(page.locator('text="Nothing found"')).toHaveCount(0);
});
});
@@ -832,9 +832,8 @@ test.describe(`${TAG.FUNCTIONAL} `, () => {
});
await test.step('Verify "Nothing found" is visible', async () => {
- const status = page.locator('#search-result');
+ const status = page.getByRole('listbox').getByText('Nothing found').first();
await expect(status).toBeVisible();
- await expect(status).toContainText('Nothing found');
});
});
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--8cbf8-om-label-right-icon-addon-with-trigger-text-2-chromium-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--8cbf8-om-label-right-icon-addon-with-trigger-text-2-chromium-linux.png
index df38691b5b..916dfbf792 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--8cbf8-om-label-right-icon-addon-with-trigger-text-2-chromium-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--8cbf8-om-label-right-icon-addon-with-trigger-text-2-chromium-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--8cbf8-om-label-right-icon-addon-with-trigger-text-2-firefox-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--8cbf8-om-label-right-icon-addon-with-trigger-text-2-firefox-linux.png
index e3143f00c1..5c71e2fb6c 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--8cbf8-om-label-right-icon-addon-with-trigger-text-2-firefox-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--8cbf8-om-label-right-icon-addon-with-trigger-text-2-firefox-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--8cbf8-om-label-right-icon-addon-with-trigger-text-2-webkit-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--8cbf8-om-label-right-icon-addon-with-trigger-text-2-webkit-linux.png
index dbf5f79145..03ba9c9b34 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--8cbf8-om-label-right-icon-addon-with-trigger-text-2-webkit-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--8cbf8-om-label-right-icon-addon-with-trigger-text-2-webkit-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--9526a--invalid-state-with-label-right-badge-addon-2-chromium-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--9526a--invalid-state-with-label-right-badge-addon-2-chromium-linux.png
index 3b09397edb..e818026703 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--9526a--invalid-state-with-label-right-badge-addon-2-chromium-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--9526a--invalid-state-with-label-right-badge-addon-2-chromium-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--9526a--invalid-state-with-label-right-badge-addon-2-firefox-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--9526a--invalid-state-with-label-right-badge-addon-2-firefox-linux.png
index 78bc697006..c817fdcfee 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--9526a--invalid-state-with-label-right-badge-addon-2-firefox-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--9526a--invalid-state-with-label-right-badge-addon-2-firefox-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--9526a--invalid-state-with-label-right-badge-addon-2-webkit-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--9526a--invalid-state-with-label-right-badge-addon-2-webkit-linux.png
index ab3c6424ab..2f313475b1 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--9526a--invalid-state-with-label-right-badge-addon-2-webkit-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--9526a--invalid-state-with-label-right-badge-addon-2-webkit-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--96589-om-label-right-icon-addon-with-trigger-text-1-chromium-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--96589-om-label-right-icon-addon-with-trigger-text-1-chromium-linux.png
index b3e9a13fe4..0edb096d3c 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--96589-om-label-right-icon-addon-with-trigger-text-1-chromium-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--96589-om-label-right-icon-addon-with-trigger-text-1-chromium-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--96589-om-label-right-icon-addon-with-trigger-text-1-firefox-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--96589-om-label-right-icon-addon-with-trigger-text-1-firefox-linux.png
index 944328056e..0c62cf8b58 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--96589-om-label-right-icon-addon-with-trigger-text-1-firefox-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--96589-om-label-right-icon-addon-with-trigger-text-1-firefox-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--96589-om-label-right-icon-addon-with-trigger-text-1-webkit-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--96589-om-label-right-icon-addon-with-trigger-text-1-webkit-linux.png
index 96b3691eaa..06c7d1c6bd 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--96589-om-label-right-icon-addon-with-trigger-text-1-webkit-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--96589-om-label-right-icon-addon-with-trigger-text-1-webkit-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--c06de--invalid-state-with-label-right-badge-addon-1-chromium-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--c06de--invalid-state-with-label-right-badge-addon-1-chromium-linux.png
index d96da81ee2..d41184f30c 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--c06de--invalid-state-with-label-right-badge-addon-1-chromium-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--c06de--invalid-state-with-label-right-badge-addon-1-chromium-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--c06de--invalid-state-with-label-right-badge-addon-1-firefox-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--c06de--invalid-state-with-label-right-badge-addon-1-firefox-linux.png
index 88cea19f0d..18b07625c6 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--c06de--invalid-state-with-label-right-badge-addon-1-firefox-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--c06de--invalid-state-with-label-right-badge-addon-1-firefox-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--c06de--invalid-state-with-label-right-badge-addon-1-webkit-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--c06de--invalid-state-with-label-right-badge-addon-1-webkit-linux.png
index 03b77c3702..655ec8ca38 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--c06de--invalid-state-with-label-right-badge-addon-1-webkit-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons--c06de--invalid-state-with-label-right-badge-addon-1-webkit-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-disabled-left-badge-addon-1-chromium-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-disabled-left-badge-addon-1-chromium-linux.png
index 4ad8a10e5f..95d284862f 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-disabled-left-badge-addon-1-chromium-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-disabled-left-badge-addon-1-chromium-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-disabled-left-badge-addon-1-firefox-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-disabled-left-badge-addon-1-firefox-linux.png
index a86964f727..7d595e9740 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-disabled-left-badge-addon-1-firefox-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-disabled-left-badge-addon-1-firefox-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-disabled-left-badge-addon-1-webkit-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-disabled-left-badge-addon-1-webkit-linux.png
index ac39d63468..b53d7f2ef2 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-disabled-left-badge-addon-1-webkit-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-disabled-left-badge-addon-1-webkit-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-1-chromium-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-1-chromium-linux.png
index 6c374aa512..853524fc3b 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-1-chromium-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-1-chromium-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-1-firefox-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-1-firefox-linux.png
index 294db22f33..0af53fcf43 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-1-firefox-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-1-firefox-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-1-webkit-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-1-webkit-linux.png
index fb11757dc5..502e95b231 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-1-webkit-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-1-webkit-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-2-chromium-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-2-chromium-linux.png
index c2d146ba5c..21eb8d126a 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-2-chromium-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-2-chromium-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-2-firefox-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-2-firefox-linux.png
index 311c30a8ba..6d8b36b99b 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-2-firefox-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-2-firefox-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-2-webkit-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-2-webkit-linux.png
index 6d09ba9c6b..8f09ae14ac 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-2-webkit-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-addon-2-webkit-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-1-chromium-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-1-chromium-linux.png
index 8e951ba40a..3d0c39d8d8 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-1-chromium-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-1-chromium-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-1-firefox-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-1-firefox-linux.png
index b64bcee77d..cad0ed22ab 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-1-firefox-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-1-firefox-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-1-webkit-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-1-webkit-linux.png
index 7277f7b8bf..5a7ba87b65 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-1-webkit-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-1-webkit-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-2-chromium-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-2-chromium-linux.png
index 437504ad86..f45ddf0f2e 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-2-chromium-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-2-chromium-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-2-firefox-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-2-firefox-linux.png
index da07107b0f..38c8eef1df 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-2-firefox-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-2-firefox-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-2-webkit-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-2-webkit-linux.png
index 7a0a7edc24..b9e6ed6534 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-2-webkit-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-L-valid-state-left-icon-right-text-addons-2-webkit-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-1-chromium-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-1-chromium-linux.png
index a6a84e3cae..e2b0c95d30 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-1-chromium-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-1-chromium-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-1-firefox-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-1-firefox-linux.png
index b9783e3824..4e475ab21c 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-1-firefox-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-1-firefox-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-1-webkit-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-1-webkit-linux.png
index 6a48f5a410..5f202bad4e 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-1-webkit-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-1-webkit-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-2-chromium-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-2-chromium-linux.png
index 6910f888e7..c240588faa 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-2-chromium-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-2-chromium-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-2-firefox-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-2-firefox-linux.png
index 49171f3920..b041b7e4c3 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-2-firefox-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-2-firefox-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-2-webkit-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-2-webkit-linux.png
index a95a35fa53..3d0d2cebe9 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-2-webkit-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-left-badge-addon-with-trigger-text-2-webkit-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-1-chromium-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-1-chromium-linux.png
index 5cd65f5057..308357c82b 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-1-chromium-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-1-chromium-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-1-firefox-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-1-firefox-linux.png
index 3e365e8f18..a48cb221f5 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-1-firefox-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-1-firefox-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-1-webkit-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-1-webkit-linux.png
index 1402db9e5a..96749f1456 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-1-webkit-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-1-webkit-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-2-chromium-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-2-chromium-linux.png
index f6850ca4b3..a95509a193 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-2-chromium-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-2-chromium-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-2-firefox-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-2-firefox-linux.png
index 92ee9dfe49..225b987570 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-2-firefox-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-2-firefox-linux.png differ
diff --git a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-2-webkit-linux.png b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-2-webkit-linux.png
index df5f897c44..63ece7949a 100644
Binary files a/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-2-webkit-linux.png and b/semcore/select/__tests__/select.browser-test.tsx-snapshots/-visual-Verify-select-basic-props-and-addons-size-M-with-label-left-text-right-icon-addons-2-webkit-linux.png differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx
index 4302d199e7..6a7f60fe0b 100644
--- a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx
+++ b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx
@@ -3,11 +3,80 @@ import { loadPage } from '@semcore/testing-utils/shared/helpers';
import { TAG } from '@semcore/testing-utils/shared/tags';
const locators = {
- menu: (page: Page) => page.locator('[data-ui-name="Select.Menu"]'),
- options: (page: Page) => page.locator('[data-ui-name="Select.Option"]'),
- trigger: (page: Page) => page.locator('[data-ui-name="Select.Trigger"]'),
- input: (page: Page) => page.locator('input'),
- optionByText: (page: Page, text: string) => page.locator(`text=${text}`),
+ options: (page: Page) => page.getByRole('option'),
+ input: (page: Page) => page.getByRole('combobox'),
+ optionByText: (page: Page, text: string) => page.getByRole('option', { name: new RegExp(text, 'i') }),
+ startTypingStatus: (page: Page) => page.getByText('Start typing to see options'),
+ loadingStatus: (page: Page) => page.getByText('Loading...'),
+ inputWrapper: (page: Page) => page.locator('[data-ui-name="AutoSuggest.Trigger"]'),
+ outline: (page: Page) =>
+ page.locator('[data-ui-name="AutoSuggest.Trigger"] div:not([data-ui-name])').first(),
+ addon: (page: Page) => page.locator('[data-ui-name="Input.Addon"]'),
+ loadingSpin: (page: Page) =>
+ page.locator('[data-ui-name="AutoSuggest.Trigger"] [data-ui-name="Spin"]'),
+ loadingAddon: (page: Page) =>
+ page.locator('[data-ui-name="AutoSuggest.Trigger"] [data-ui-name="Input.Addon"]').last(),
+};
+
+const examplePath = 'stories/patterns/ux-patterns/auto-suggest/tests/examples/autosuggest_test.tsx';
+
+type AutoSuggestExampleProps = {
+ suggestionsSource?: 'sync' | 'async';
+ initialValue?: string;
+ asyncDelay?: number;
+ autoFocus?: boolean;
+ size?: 'm' | 'l';
+ readOnly?: boolean;
+ statusItemPlaceholder?: string;
+ addonLeft?: 'none' | 'icon' | 'badge' | 'tag';
+ addonRight?: 'none' | 'icon' | 'badge' | 'tag';
+ button?: 'none' | 'left' | 'right' | 'both';
+};
+
+const loadAutoSuggest = async (page: Page, props: AutoSuggestExampleProps = {}) => {
+ await loadPage(page, examplePath, 'en', props);
+};
+
+const compositionPath =
+ 'stories/patterns/ux-patterns/auto-suggest/tests/examples/autosuggest_composition.tsx';
+
+type CompositionExampleProps = {
+ suggestionsSource?: 'sync' | 'async';
+ asyncDelay?: number;
+ size?: 'm' | 'l';
+ width?: number;
+ popperWidth?: number;
+ popperMaxHeight?: number;
+ statusItemPlaceholder?: string;
+ addonLeft?: 'none' | 'icon' | 'badge' | 'tag';
+ addonRight?: 'none' | 'icon' | 'badge' | 'tag';
+ customStartTyping?: boolean;
+ customLoadingState?: boolean;
+ customSuggestionItem?: boolean;
+};
+
+const loadComposition = async (page: Page, props: CompositionExampleProps = {}) => {
+ await loadPage(page, compositionPath, 'en', props);
+};
+
+const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
+const expectOptionsToMatch = async (page: Page, query: string) => {
+ const queryPattern = new RegExp(escapeRegExp(query), 'i');
+
+ await expect.poll(async () => {
+ const texts = await locators.options(page).allTextContents();
+ return texts.length > 0 && texts.every((text) => queryPattern.test(text));
+ }).toBeTruthy();
+
+ await expect(locators.options(page).first()).toBeVisible();
+ const count = await locators.options(page).count();
+ expect(count).toBeGreaterThan(0);
+
+ for (let i = 0; i < count; i++) {
+ await expect(locators.options(page).nth(i)).toHaveAttribute('aria-selected', 'false');
+ await expect(locators.options(page).nth(i)).not.toHaveClass(/selected/);
+ }
};
/* =====================================================
@@ -18,64 +87,41 @@ test.describe(TAG.VISUAL, () => {
test('Verify AutoSuggest keyboard navigation states', {
tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@select', '@input'],
}, async ({ page }) => {
- await loadPage(page, 'stories/patterns/ux-patterns/auto-suggest/docs/examples/autosuggest_example.tsx', 'en');
-
- await test.step('Verify navigation between options visual state', async () => {
- await page.keyboard.press('Tab');
- await page.keyboard.press('Enter');
- await page.keyboard.type('a');
- await locators.options(page).first().waitFor({ state: 'visible' });
-
- await page.keyboard.press('ArrowDown');
- await page.keyboard.press('ArrowDown');
- await page.keyboard.press('ArrowDown');
+ await loadAutoSuggest(page);
- await expect(page).toHaveScreenshot();
- });
+ await page.keyboard.press('Tab');
+ await page.keyboard.type('a');
+ await locators.options(page).first().waitFor({ state: 'visible' });
- await test.step('Verify selected state', async () => {
- await page.keyboard.press('Enter');
- await locators.options(page).first().waitFor({ state: 'hidden' });
+ await page.keyboard.press('ArrowDown');
+ await page.keyboard.press('ArrowDown');
+ await page.keyboard.press('ArrowDown');
- await expect(page).toHaveScreenshot();
- });
+ await expect(page).toHaveScreenshot();
});
test('Verify AutoSuggest mouse navigation states', {
tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@select', '@input'],
}, async ({ page }) => {
- await loadPage(page, 'stories/patterns/ux-patterns/auto-suggest/docs/examples/autosuggest_example.tsx', 'en');
-
- const input = locators.input(page);
- const inputRect = (await input.boundingBox())!;
- const inputCoords = [inputRect.x + inputRect.width / 2, inputRect.y + inputRect.height / 2];
+ await loadAutoSuggest(page);
- await test.step('Verify menu with options visual state', async () => {
- await page.mouse.click(inputCoords[0], inputCoords[1]);
- await page.keyboard.type('a');
- await locators.options(page).first().waitFor({ state: 'visible' });
+ await locators.input(page).click();
+ await page.keyboard.type('a');
+ await locators.options(page).first().waitFor({ state: 'visible' });
- await expect(page).toHaveScreenshot();
- });
-
- await test.step('Verify selected state', async () => {
- await test.step('Verify selected option highlighted visual state', async () => {
- const persianOption = locators.optionByText(page, 'persian');
- const persianOptionRect = (await persianOption.boundingBox())!;
- const persianOptionCoords = [
- persianOptionRect.x + persianOptionRect.width / 2,
- persianOptionRect.y + persianOptionRect.height / 2,
- ];
+ await expect(page).toHaveScreenshot();
+ });
- await page.mouse.click(persianOptionCoords[0], persianOptionCoords[1]);
- await locators.options(page).first().waitFor({ state: 'hidden' });
+ test('Verify AutoSuggest size l visual state', {
+ tag: [TAG.PRIORITY_MEDIUM, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page, { size: 'l' });
- await page.mouse.click(inputCoords[0], inputCoords[1]);
- await locators.options(page).first().waitFor({ state: 'visible' });
+ await locators.input(page).click();
+ await page.keyboard.type('a');
+ await locators.options(page).first().waitFor({ state: 'visible' });
- await expect(page).toHaveScreenshot();
- });
- });
+ await expect(page).toHaveScreenshot();
});
});
@@ -85,135 +131,473 @@ test.describe(TAG.VISUAL, () => {
We verify states, visibility, and attributes.
===================================================== */
test.describe(TAG.FUNCTIONAL, () => {
- test('Verify AutoSuggest keyboard navigation', {
- tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@select', '@input'],
- }, async ({ page }) => {
- await loadPage(page, 'stories/patterns/ux-patterns/auto-suggest/docs/examples/autosuggest_example.tsx', 'en');
+ test.describe('behaviour', () => {
+ test('Verify AutoSuggest keyboard navigation', {
+ tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page);
+
+ await test.step('Verify combobox a11y attributes', async () => {
+ const input = locators.input(page);
+ await expect(input).toHaveAttribute('role', 'combobox');
+ await expect(input).toHaveAttribute('autocomplete', 'off');
+ await expect(input).toHaveAttribute('aria-autocomplete', 'list');
+ await expect(input).toHaveAttribute('aria-haspopup', 'listbox');
+ await expect(input).toHaveAttribute('aria-expanded', 'false');
+ });
- await test.step('Verify menu not expanded when nothing entered', async () => {
- await page.keyboard.press('Tab');
- await page.keyboard.press('Enter');
- await page.keyboard.press('ArrowDown');
- await expect(locators.menu(page)).not.toBeVisible();
+ await test.step('Verify initial dropdown is shown when empty input is focused', async () => {
+ await page.keyboard.press('Tab');
+ await expect(locators.startTypingStatus(page).nth(1)).toBeVisible();
+ await expect(locators.options(page)).toHaveCount(0);
+ // The init "Start typing" state is not an expanded listbox of options
+ await expect(locators.input(page)).toHaveAttribute('aria-expanded', 'false');
+ });
+
+ await test.step('Verify suggestions are filtered with each entered character', async () => {
+ await page.keyboard.type('p');
+ await expectOptionsToMatch(page, 'p');
+ // With real options shown, the combobox is expanded
+ await expect(locators.input(page)).toHaveAttribute('aria-expanded', 'true');
+ const optionsForP = await locators.options(page).count();
+
+ await page.keyboard.type('e');
+ await expectOptionsToMatch(page, 'pe');
+ const optionsForPe = await locators.options(page).count();
+ expect(optionsForPe).toBeLessThanOrEqual(optionsForP);
+
+ await page.keyboard.press('Backspace');
+ await expectOptionsToMatch(page, 'p');
+ });
+
+ await test.step('Verify Enter selection closes menu until the value is edited', async () => {
+ await page.keyboard.type('er');
+ await expect(locators.optionByText(page, 'persian')).toBeVisible();
+ // Let the debounced filter fully settle so the pending timer doesn't
+ // reopen the menu right after selection.
+ await page.waitForTimeout(400);
+
+ await page.keyboard.press('ArrowDown');
+ await page.keyboard.press('Enter');
+
+ await expect(locators.input(page)).toHaveValue('Persian');
+ await locators.options(page).first().waitFor({ state: 'hidden' });
+ await expect(locators.options(page)).toHaveCount(0);
+
+ await page.keyboard.press('Enter');
+ await expect(locators.options(page)).toHaveCount(0);
+
+ for (let i = 0; i < 'ersian'.length; i++) {
+ await page.keyboard.press('Backspace');
+ }
+ await expect(locators.input(page)).toHaveValue('P');
+ await expectOptionsToMatch(page, 'p');
+ });
});
- await test.step('Verify menu appears when character entered but nothing is selected', async () => {
- await page.keyboard.type('a');
- await locators.options(page).first().waitFor({ state: 'visible' });
+ test('Verify AutoSuggest autoFocus focuses the input and opens matches options when some value pre defined', {
+ tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page, { autoFocus: true, initialValue: 'p' });
- const count = await locators.options(page).count();
- for (let i = 0; i < count; i++) {
- await expect(locators.options(page).nth(i)).toHaveAttribute('aria-selected', 'false');
- await expect(locators.options(page).nth(i)).not.toHaveClass(/selected/);
- }
+ // On render the input is focused and, since there are matches, the list opens
+ await expect(locators.input(page)).toBeFocused();
+ await expectOptionsToMatch(page, 'p');
});
- await test.step('Verify option not selected and menu closed by Escape', async () => {
- await page.waitForTimeout(200);
+ test('Verify AutoSuggest focus states', {
+ tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await test.step('Verify matching prefilled value opens options on focus', async () => {
+ await loadAutoSuggest(page, { initialValue: 'p' });
+ await locators.input(page).click();
+ await expectOptionsToMatch(page, 'p');
+ });
+
+ await test.step('Verify non-matching prefilled value does not open anything on focus', async () => {
+ await loadAutoSuggest(page, { initialValue: 'zzzz' });
+ await locators.input(page).click();
+ await expect(locators.options(page)).toHaveCount(0);
+ await expect(locators.startTypingStatus(page)).not.toBeVisible();
+ });
+ });
+
+ test('Verify AutoSuggest mouse navigation', {
+ tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page);
+
+ await test.step('Verify initial dropdown is shown when empty input is focused', async () => {
+ await locators.input(page).click();
+ await expect(locators.startTypingStatus(page)).toBeVisible();
+ });
+
+ await test.step('Verify menu expanded when character entered', async () => {
+ await page.keyboard.type('per');
+ await expectOptionsToMatch(page, 'per');
+ });
+
+ await test.step('Verify mouse selection closes menu until the value is edited', async () => {
+ const persianOption = locators.optionByText(page, 'persian');
+ await persianOption.click();
+ await locators.options(page).first().waitFor({ state: 'hidden' });
+
+ await expect(locators.input(page)).toHaveValue('Persian');
+ await expect(locators.options(page)).toHaveCount(0);
+ });
+
+ await test.step('Verify editing selected value reopens filtered suggestions', async () => {
+ await locators.input(page).click();
+ await page.keyboard.press('Backspace');
+ await expectOptionsToMatch(page, 'persia');
+ });
+ });
+
+ // Leaving the field and re-focusing always re-triggers the menu, regardless of
+ // what the user did before (selection or Escape). So after a selection, a fresh
+ // focus reopens the list with the matches for the current value.
+ test('Verify AutoSuggest reopens suggestions on focus after selection', {
+ tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page);
+
+ await locators.input(page).click();
+ await page.keyboard.type('per');
+ await expectOptionsToMatch(page, 'per');
+ await page.waitForTimeout(400);
+
+ await locators.optionByText(page, 'persian').click();
+ await locators.options(page).first().waitFor({ state: 'hidden' });
+ await expect(locators.input(page)).toHaveValue('Persian');
+
+ await locators.input(page).evaluate((node: HTMLInputElement) => node.blur());
+ await locators.input(page).click();
+
+ // Re-focusing re-triggers the menu with the matches for the current value
+ await expectOptionsToMatch(page, 'persian');
+ });
+
+ test('Verify AutoSuggest Escape closes suggestions until blur and focus', {
+ tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page);
+
+ await locators.input(page).click();
+ await page.keyboard.type('a');
+ await expectOptionsToMatch(page, 'a');
- await page.keyboard.press('ArrowDown');
- await page.keyboard.press('ArrowDown');
- await page.keyboard.press('ArrowDown');
await page.keyboard.press('Escape');
+ await expect(locators.options(page)).toHaveCount(0);
+
+ await page.keyboard.type('b');
+ await page.waitForTimeout(400);
+ await expect(locators.options(page)).toHaveCount(0);
+
+ await locators.input(page).evaluate((node: HTMLInputElement) => node.blur());
+ await locators.input(page).click();
+ await expectOptionsToMatch(page, 'ab');
+ });
+
+ test('Verify AutoSuggest async loading state', {
+ tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page, { suggestionsSource: 'async', asyncDelay: 500 });
+
+ await locators.input(page).click();
+ await page.keyboard.type('per');
+
+ await expect(locators.loadingStatus(page)).toBeVisible();
+ await expect(locators.optionByText(page, 'persian')).toBeVisible();
+ await expect(locators.loadingStatus(page)).not.toBeVisible();
+ await expectOptionsToMatch(page, 'per');
+ });
+
+ test('Verify AutoSuggest accepts regexp-like characters in query', {
+ tag: [TAG.PRIORITY_MEDIUM, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page);
+
+ await locators.input(page).click();
+ await page.keyboard.type('*');
+ await page.waitForTimeout(400);
+
+ await expect(locators.input(page)).toHaveValue('*');
+ await expect(locators.options(page)).toHaveCount(0);
+ await expect(locators.startTypingStatus(page)).not.toBeVisible();
+ });
+
+ test('Verify AutoSuggest ignores stale async results', {
+ tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page, { suggestionsSource: 'async', asyncDelay: 700 });
+
+ await locators.input(page).click();
+ await page.keyboard.type('p');
+ await expect(locators.loadingStatus(page)).toBeVisible();
+
+ await page.keyboard.type('er');
+
+ await expect(locators.optionByText(page, 'persian')).toBeVisible();
+ await expectOptionsToMatch(page, 'per');
+ await expect(locators.optionByText(page, 'sphynx')).toHaveCount(0);
+ });
+
+ test('Verify AutoSuggest highlight preserves the original case of the option', {
+ tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page);
+
+ await locators.input(page).click();
+ await page.keyboard.type('p');
- await expect(locators.menu(page)).not.toBeVisible();
- await expect(locators.trigger(page)).toHaveAttribute('value', 'a');
+ const persianOption = locators.optionByText(page, 'persian');
+ await expect(persianOption).toBeVisible();
+ // Case is preserved (not lowercased to the typed query)
+ await expect(persianOption).toHaveText('Persian');
+ // The matched fragment is wrapped in and keeps the original capital
+ await expect(persianOption.locator('strong')).toHaveText('P');
+ });
+
+ test('Verify AutoSuggest renders HTML in option text as plain text', {
+ tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ const dialogs: string[] = [];
+ page.on('dialog', async (dialog) => {
+ dialogs.push(dialog.message());
+ await dialog.dismiss();
+ });
+
+ await loadAutoSuggest(page);
+
+ await test.step('HTML tags in option text are not interpreted', async () => {
+ await locators.input(page).click();
+ await page.keyboard.type('cat');
+ const option = locators.optionByText(page, 'cat');
+ await expect(option).toBeVisible();
+ await expect(option).toHaveText('cat');
+ expect(await page.locator('[role="option"] b').count()).toBe(0);
+ });
+
+ await test.step('Image payload does not inject an element or fire onerror', async () => {
+ await locators.input(page).fill('');
+ await page.keyboard.type('img');
+ await page.waitForTimeout(400);
+
+ expect(await page.locator('img[src="x"]').count()).toBe(0);
+ expect(dialogs).toHaveLength(0);
+ });
+ });
+
+ test('Verify AutoSuggest accepts a matching regexp-like option without crashing', {
+ tag: [TAG.PRIORITY_MEDIUM, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page);
+
+ await locators.input(page).click();
+ await page.keyboard.type('[');
+
+ // "[Siamese" matches "[" and renders without throwing
+ await expect(page.getByRole('option', { name: '[Siamese' })).toBeVisible();
+ });
+
+ test('Verify AutoSuggest arrow navigation moves sequentially in an open list', {
+ tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page);
+
+ await locators.input(page).click();
+ await page.keyboard.type('sh');
+ await expectOptionsToMatch(page, 'sh');
+
+ // Filtered order: British Shorthair, Oriental Shorthair, American Shorthair, Exotic Shorthair
+ await page.keyboard.press('ArrowDown'); // 1st option
+ await page.keyboard.press('ArrowDown'); // 2nd option (no reset to the first)
await page.keyboard.press('Enter');
- await locators.options(page).first().waitFor({ state: 'visible' });
+
+ await expect(locators.input(page)).toHaveValue('Oriental Shorthair');
+ });
+
+ test('Verify AutoSuggest arrow keys reopen the list and highlight first/last after Escape', {
+ tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page);
+
+ await test.step('ArrowDown reopens and highlights the first option', async () => {
+ await locators.input(page).click();
+ await page.keyboard.type('sh');
+ await expectOptionsToMatch(page, 'sh');
+ await page.waitForTimeout(400);
+
+ await page.keyboard.press('Escape');
+ await expect(locators.options(page)).toHaveCount(0);
+
+ await page.keyboard.press('ArrowDown');
+ await expectOptionsToMatch(page, 'sh');
+ await expect(locators.options(page).first()).toHaveClass(/highlighted/);
+ await expect(locators.options(page).nth(1)).not.toHaveClass(/highlighted/);
+ });
+ });
+
+ test('Verify AutoSuggest ArrowUp reopens the list and highlights the last option after Escape', {
+ tag: [TAG.PRIORITY_MEDIUM, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page);
+
+ await locators.input(page).click();
+ await page.keyboard.type('sh');
+ await expectOptionsToMatch(page, 'sh');
+ await page.waitForTimeout(400);
+
+ await page.keyboard.press('Escape');
+ await expect(locators.options(page)).toHaveCount(0);
+
+ await page.keyboard.press('ArrowUp');
+ await expectOptionsToMatch(page, 'sh');
+
const count = await locators.options(page).count();
- for (let i = 0; i < count; i++) {
- await expect(locators.options(page).nth(i)).toHaveAttribute('aria-selected', 'false');
- await expect(locators.options(page).nth(i)).not.toHaveClass(/selected/);
- }
+ await expect(locators.options(page).nth(count - 1)).toHaveClass(/highlighted/);
+ await expect(locators.options(page).first()).not.toHaveClass(/highlighted/);
});
- await test.step('Verify option selected and menu closed by Enter', async () => {
- await page.waitForTimeout(200);
- await page.keyboard.press('ArrowDown');
- await page.keyboard.press('ArrowDown');
+ test('Verify AutoSuggest ArrowDown reopens the list with a highlight after selection', {
+ tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page);
+
+ await locators.input(page).click();
+ await page.keyboard.type('sh');
+ await expectOptionsToMatch(page, 'sh');
+ await page.waitForTimeout(400);
+
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
+ await expect(locators.input(page)).toHaveValue('British Shorthair');
await locators.options(page).first().waitFor({ state: 'hidden' });
- await expect(locators.trigger(page)).toHaveAttribute('value', 'ragdoll');
+
+ // Arrow keys reopen the closed list and highlight the (only) matching option
+ await page.keyboard.press('ArrowDown');
+ await expect(locators.options(page).first()).toBeVisible();
+ await expect(locators.options(page).first()).toHaveClass(/highlighted/);
});
- await test.step('Verify selected value is shown without selected option styling', async () => {
- await page.keyboard.press('Enter');
- await locators.options(page).first().waitFor({ state: 'visible' });
- await expect(locators.options(page).first()).toHaveText(/ragdoll/);
- await expect(locators.options(page).first()).toHaveAttribute('aria-selected', 'false');
- await expect(locators.options(page).first()).not.toHaveClass(/selected/);
- await expect(locators.options(page).first()).not.toHaveClass(/highlighted/);
+ test('Verify AutoSuggest readOnly prevents typing and opening suggestions', {
+ tag: [TAG.PRIORITY_MEDIUM, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page, { readOnly: true });
+
+ await locators.input(page).click();
+ await page.keyboard.type('per');
+ await page.waitForTimeout(400);
+
+ await expect(locators.input(page)).toHaveValue('');
+ await expect(locators.options(page)).toHaveCount(0);
});
- await test.step('Verify exact match keeps menu opened without selected option styling', async () => {
- for (let i = 0; i < 'ragdoll'.length; i++) {
- await page.keyboard.press('Backspace');
- }
- await page.keyboard.type('persian');
- await locators.options(page).first().waitFor({ state: 'visible' });
- await expect(locators.options(page).first()).toHaveText(/persian/);
- await expect(locators.options(page).first()).toHaveAttribute('aria-selected', 'false');
- await expect(locators.options(page).first()).not.toHaveClass(/selected/);
- await page.keyboard.press('Enter');
- await expect(locators.menu(page)).toBeVisible();
- await expect(locators.options(page).first()).toHaveText(/persian/);
- await expect(locators.options(page).first()).toHaveAttribute('aria-selected', 'false');
- await expect(locators.options(page).first()).not.toHaveClass(/selected/);
+ test('Verify AutoSuggest statusItemPlaceholder empty string hides the init state', {
+ tag: [TAG.PRIORITY_MEDIUM, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page, { statusItemPlaceholder: '' });
+ await locators.input(page).click();
+ await page.waitForTimeout(300);
+ await expect(locators.startTypingStatus(page)).not.toBeVisible();
+ await expect(locators.options(page)).toHaveCount(0);
+ });
+
+ test('Verify AutoSuggest statusItemPlaceholder custom text is shown in the init state', {
+ tag: [TAG.PRIORITY_MEDIUM, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page, { statusItemPlaceholder: 'Pick a breed' });
+ await locators.input(page).click();
+ await expect(page.getByText('Pick a breed')).toBeVisible();
+ });
+
+ test('Verify AutoSuggest loading coexists with addonRight without breaking the flow', {
+ tag: [TAG.PRIORITY_MEDIUM, TAG.KEYBOARD, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadAutoSuggest(page, { suggestionsSource: 'async', asyncDelay: 800, addonRight: 'icon' });
+
+ const input = locators.input(page);
+ await input.click();
+ await page.keyboard.type('per');
+
+ // With addonRight set, the loading spinner still shows and results still resolve
+ await expect(locators.loadingStatus(page)).toBeVisible();
+ await expect(locators.optionByText(page, 'persian')).toBeVisible();
+ await expect(locators.loadingStatus(page)).not.toBeVisible();
+ await expectOptionsToMatch(page, 'per');
});
});
- test('Verify AutoSuggest mouse navigation', {
- tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@select', '@input'],
- }, async ({ page }) => {
- await loadPage(page, 'stories/patterns/ux-patterns/auto-suggest/docs/examples/autosuggest_example.tsx', 'en');
+ /* Composition — compound subcomponents API */
+ test.describe('composition', () => {
+ const compositionInput = (page: Page) => page.getByLabel('Your pet breed');
- const input = locators.input(page);
- const inputRect = (await input.boundingBox())!;
- const inputCoords = [inputRect.x + inputRect.width / 2, inputRect.y + inputRect.height / 2];
+ test('Verify AutoSuggest renders via explicit compound subcomponents', {
+ tag: [TAG.PRIORITY_HIGH, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadComposition(page);
- await test.step('Verify menu not expanded when nothing entered', async () => {
- await page.mouse.click(inputCoords[0], inputCoords[1]);
- await expect(locators.menu(page)).not.toBeVisible();
+ await compositionInput(page).click();
+ await compositionInput(page).fill('a');
+ await expectOptionsToMatch(page, 'a');
});
- await test.step('Verify menu expanded when character entered', async () => {
- await page.keyboard.type('a');
- await locators.options(page).first().waitFor({ state: 'visible' });
+ test('Verify AutoSuggest custom StartTypingState renders its own content', {
+ tag: [TAG.PRIORITY_HIGH, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadComposition(page, { customStartTyping: true });
- await expect(locators.menu(page)).toBeVisible();
- const count = await locators.options(page).count();
- for (let i = 0; i < count; i++) {
- await expect(locators.options(page).nth(i)).toHaveAttribute('aria-selected', 'false');
- await expect(locators.options(page).nth(i)).not.toHaveClass(/selected/);
- }
+ await compositionInput(page).click();
+ await expect(page.getByTestId('custom-start-typing')).toBeVisible();
+ await expect(page.getByText('Search for your favourite breed')).toBeVisible();
});
- await test.step('Verify menu closed when option clicked', async () => {
- const persianOption = locators.optionByText(page, 'persian');
- const persianOptionRect = (await persianOption.boundingBox())!;
- const persianOptionCoords = [
- persianOptionRect.x + persianOptionRect.width / 2,
- persianOptionRect.y + persianOptionRect.height / 2,
- ];
+ test('Verify AutoSuggest custom SuggestionItem overrides the option rendering', {
+ tag: [TAG.PRIORITY_HIGH, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadComposition(page, { customSuggestionItem: true });
- await page.mouse.click(persianOptionCoords[0], persianOptionCoords[1]);
- await locators.options(page).first().waitFor({ state: 'hidden' });
+ await compositionInput(page).click();
+ await compositionInput(page).fill('a');
+ await expect(locators.options(page).first()).toBeVisible();
- await expect(persianOption).toHaveCount(0);
- await expect(locators.trigger(page)).toHaveAttribute('value', 'persian');
+ const optionsCount = await locators.options(page).count();
+ expect(optionsCount).toBeGreaterThan(0);
+ await expect(page.getByTestId('custom-suggestion-item')).toHaveCount(optionsCount);
});
- await test.step('Verify menu opened without selected option styling', async () => {
- await page.mouse.click(inputCoords[0], inputCoords[1]);
- await locators.options(page).first().waitFor({ state: 'visible' });
- await expect(locators.trigger(page)).toHaveAttribute('value', 'persian');
+ test('Verify AutoSuggest popper width and max-height are configurable', {
+ tag: [TAG.PRIORITY_MEDIUM, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadComposition(page, { width: 320, popperWidth: 420, popperMaxHeight: 120 });
+
+ await compositionInput(page).click();
+ await compositionInput(page).fill('a');
+ await expect(locators.options(page).first()).toBeVisible();
+
+ const box = await page.locator('[data-ui-name="AutoSuggest.Popper"]').first().boundingBox();
+ // popperWidth widens the popper beyond the trigger
+ expect(Math.round(box?.width ?? 0)).toBe(420);
+ // popperMaxHeight caps the height (a few px of padding above the cap is ok)
+ expect(box?.height ?? 0).toBeLessThanOrEqual(140);
+ expect(box?.height ?? 0).toBeGreaterThan(80);
+ });
- await expect(locators.options(page).first()).toHaveText(/persian/);
- await expect(locators.options(page).first()).toHaveAttribute('aria-selected', 'false');
- await expect(locators.options(page).first()).not.toHaveClass(/selected/);
- await expect(locators.options(page).first()).not.toHaveClass(/highlighted/);
+ test('Verify AutoSuggest custom LoadingState renders its own content', {
+ tag: [TAG.PRIORITY_MEDIUM, '@select', '@input'],
+ }, async ({ page }) => {
+ await loadComposition(page, {
+ suggestionsSource: 'async',
+ asyncDelay: 1500,
+ customLoadingState: true,
+ });
+
+ await compositionInput(page).click();
+ await compositionInput(page).fill('per');
+
+ await expect(page.getByTestId('custom-loading')).toBeVisible();
+ await expect(page.getByText('Fetching breeds…')).toBeVisible();
});
});
});
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-1-chromium-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-1-chromium-linux.png
index 981ff6e6e5..f4548ed9a1 100644
Binary files a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-1-chromium-linux.png and b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-1-chromium-linux.png differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-1-firefox-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-1-firefox-linux.png
index 9530bb1204..6567465484 100644
Binary files a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-1-firefox-linux.png and b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-1-firefox-linux.png differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-1-webkit-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-1-webkit-linux.png
index ba2e9ce063..732d452ce9 100644
Binary files a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-1-webkit-linux.png and b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-1-webkit-linux.png differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-2-chromium-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-2-chromium-linux.png
deleted file mode 100644
index eea0df1f31..0000000000
Binary files a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-2-chromium-linux.png and /dev/null differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-2-firefox-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-2-firefox-linux.png
deleted file mode 100644
index 7c743f3faa..0000000000
Binary files a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-2-firefox-linux.png and /dev/null differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-2-webkit-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-2-webkit-linux.png
deleted file mode 100644
index 955d7394d2..0000000000
Binary files a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-keyboard-navigation-states-2-webkit-linux.png and /dev/null differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-1-chromium-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-1-chromium-linux.png
index f2400d3266..e32731711e 100644
Binary files a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-1-chromium-linux.png and b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-1-chromium-linux.png differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-1-firefox-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-1-firefox-linux.png
index 5eb953b0e9..3edbb725ab 100644
Binary files a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-1-firefox-linux.png and b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-1-firefox-linux.png differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-1-webkit-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-1-webkit-linux.png
index 20248a6d90..bd2b3a55cb 100644
Binary files a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-1-webkit-linux.png and b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-1-webkit-linux.png differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-2-chromium-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-2-chromium-linux.png
deleted file mode 100644
index 2c4b922450..0000000000
Binary files a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-2-chromium-linux.png and /dev/null differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-2-firefox-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-2-firefox-linux.png
deleted file mode 100644
index 535a89857f..0000000000
Binary files a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-2-firefox-linux.png and /dev/null differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-2-webkit-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-2-webkit-linux.png
deleted file mode 100644
index d239a60848..0000000000
Binary files a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-mouse-navigation-states-2-webkit-linux.png and /dev/null differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-size-l-visual-state-1-chromium-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-size-l-visual-state-1-chromium-linux.png
new file mode 100644
index 0000000000..39087fe523
Binary files /dev/null and b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-size-l-visual-state-1-chromium-linux.png differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-size-l-visual-state-1-firefox-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-size-l-visual-state-1-firefox-linux.png
new file mode 100644
index 0000000000..728f173ef7
Binary files /dev/null and b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-size-l-visual-state-1-firefox-linux.png differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-size-l-visual-state-1-webkit-linux.png b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-size-l-visual-state-1-webkit-linux.png
new file mode 100644
index 0000000000..91df5ce6e6
Binary files /dev/null and b/semcore/select/__tests__/ux-patterns-with-select/auto-suggest.browser-test.tsx-snapshots/-visual-Verify-AutoSuggest-size-l-visual-state-1-webkit-linux.png differ
diff --git a/semcore/select/__tests__/ux-patterns-with-select/ux-patterns.axe-test.tsx b/semcore/select/__tests__/ux-patterns-with-select/ux-patterns.axe-test.tsx
index d3ca714334..623ba3915f 100644
--- a/semcore/select/__tests__/ux-patterns-with-select/ux-patterns.axe-test.tsx
+++ b/semcore/select/__tests__/ux-patterns-with-select/ux-patterns.axe-test.tsx
@@ -21,21 +21,53 @@ test.describe(`@select ${TAG.ACCESSIBILITY}`, () => {
});
test('AutoSuggest', async ({ page }) => {
- await loadPage(page, 'stories/patterns/ux-patterns/auto-suggest/docs/examples/autosuggest_example.tsx', 'en');
+ const autoSuggestExample = 'stories/patterns/ux-patterns/auto-suggest/tests/examples/autosuggest_test.tsx';
+
+ await loadPage(page, autoSuggestExample, 'en');
await test.step('Default state', async () => {
const violations = await getAccessibilityViolations({ page });
expect(violations).toEqual([]);
});
- await test.step('Opened autosuggest with typed input', async () => {
+ await test.step('Initial dropdown', async () => {
await page.keyboard.press('Tab');
+ await page.getByText('Start typing to see options').waitFor({ state: 'visible' });
+
+ const violations = await getAccessibilityViolations({ page });
+ expect(violations).toEqual([]);
+ });
+
+ await test.step('Opened autosuggest with typed input', async () => {
await page.keyboard.type('a');
await page.getByText('persian').waitFor({ state: 'visible' });
const violations = await getAccessibilityViolations({ page });
expect(violations).toEqual([]);
});
+
+ await test.step('Loading autosuggest', async () => {
+ await loadPage(page, autoSuggestExample, 'en', { suggestionsSource: 'async', asyncDelay: 500 });
+ await page.keyboard.press('Tab');
+ await page.keyboard.type('per');
+ await page.getByText('Loading...').waitFor({ state: 'visible' });
+
+ const violations = await getAccessibilityViolations({ page });
+ expect(violations).toEqual([]);
+ });
+
+ await test.step('Closed state after selecting an option', async () => {
+ await loadPage(page, autoSuggestExample, 'en');
+ await page.keyboard.press('Tab');
+ await page.keyboard.type('per');
+ await page.getByRole('option').first().waitFor({ state: 'visible' });
+ await page.keyboard.press('ArrowDown');
+ await page.keyboard.press('Enter');
+ await page.getByRole('option').first().waitFor({ state: 'hidden' });
+
+ const violations = await getAccessibilityViolations({ page });
+ expect(violations).toEqual([]);
+ });
});
test('InputPhone', async ({ page }) => {
diff --git a/semcore/select/package.json b/semcore/select/package.json
index 2323bd5e4d..fbcd24cce6 100644
--- a/semcore/select/package.json
+++ b/semcore/select/package.json
@@ -8,7 +8,7 @@
"author": "UI-kit team ",
"license": "MIT",
"scripts": {
- "build": "pnpm semcore-builder --source=js && pnpm vite build"
+ "build": "pnpm semcore-builder --source=js,ts && pnpm vite build"
},
"exports": {
"types": "./lib/types/index.d.ts",
@@ -23,6 +23,7 @@
"@semcore/dropdown": "^17.2.0",
"@semcore/dropdown-menu": "^17.2.0",
"@semcore/input": "^17.2.0",
+ "@semcore/spin": "^17.2.0",
"@semcore/typography": "^17.2.0",
"classnames": "2.2.6"
},
diff --git a/semcore/select/src/components/AutoSuggest/AutoSuggest.tsx b/semcore/select/src/components/AutoSuggest/AutoSuggest.tsx
new file mode 100644
index 0000000000..5d5f8da034
--- /dev/null
+++ b/semcore/select/src/components/AutoSuggest/AutoSuggest.tsx
@@ -0,0 +1,496 @@
+import type { NeighborItemProps } from '@semcore/base-components';
+import type { Intergalactic } from '@semcore/core';
+import { Component, createComponent, Root } from '@semcore/core';
+import i18nEnhance from '@semcore/core/lib/utils/enhances/i18nEnhance';
+import { getAccessibleName } from '@semcore/core/lib/utils/getAccessibleName';
+import { forkRef } from '@semcore/core/lib/utils/ref';
+import uniqueIDEnhancement from '@semcore/core/lib/utils/uniqueID';
+import { isFocusInside } from '@semcore/core/lib/utils/use/useFocusLock';
+import { cssVariableEnhance } from '@semcore/core/lib/utils/useCssVariable';
+import Input from '@semcore/input';
+import Spin from '@semcore/spin';
+import React from 'react';
+
+import type { NSAutoSuggest } from './AutoSuggest.type';
+import { Highlight } from './Highlight';
+// todo Brauer Ilia: change to ../../Select after rewriting to ts
+import Select from '../../index';
+import { localizedMessages } from '../../translations/__intergalactic-dynamic-locales';
+
+class AutoSuggestRoot extends Component<
+ Intergalactic.InternalTypings.InferComponentProps,
+ typeof AutoSuggestRoot.enhance,
+ NSAutoSuggest.Handlers,
+ {},
+ NSAutoSuggest.State,
+ NSAutoSuggest.DefaultProps
+> {
+ static displayName = 'AutoSuggest';
+
+ static defaultProps = (): NSAutoSuggest.DefaultProps => {
+ return {
+ defaultValue: '',
+ placeholder: '',
+ children: (
+ <>
+
+
+ >
+ ),
+ };
+ };
+
+ static enhance = [
+ uniqueIDEnhancement(),
+ i18nEnhance(localizedMessages),
+ cssVariableEnhance({
+ variable: '--intergalactic-duration-popper',
+ fallback: '200',
+ map: (v: string) => Number.parseInt(v, 10).toString(),
+ prop: 'duration',
+ }),
+ ] as const;
+
+ private abortController: AbortController | undefined;
+ private changeDebounce = 0;
+ private triggerRef = React.createRef();
+ private popperRef = React.createRef();
+
+ state: NSAutoSuggest.State = {
+ isVisible: false,
+ highlightedIndex: -1,
+ suggestions: Array.isArray(this.props.suggestions) ? this.props.suggestions : [],
+ openOnChanges: true,
+ isLoading: false,
+ };
+
+ protected uncontrolledProps() {
+ return {
+ value: null,
+ };
+ }
+
+ get id() {
+ const { uid, id } = this.asProps;
+
+ return id ?? `${uid}_autosuggest-trigger`;
+ }
+
+ get isStartTypingState() {
+ const { statusItemPlaceholder } = this.asProps;
+ const { suggestions, isVisible } = this.state;
+
+ return isVisible && suggestions.length === 0 && statusItemPlaceholder !== '';
+ }
+
+ get isAriaExpanded() {
+ const { isVisible, isLoading, suggestions } = this.state;
+
+ return isVisible && suggestions.length > 0 && !isLoading && !this.isStartTypingState;
+ }
+
+ get isVisiblePopper() {
+ const { value, statusItemPlaceholder } = this.asProps;
+ const { isVisible, isLoading, suggestions } = this.state;
+
+ return isVisible &&
+ (value === '' || suggestions.length > 0 || isLoading || (this.changeDebounce && statusItemPlaceholder !== '')) &&
+ !(value === '' && suggestions.length === 0 && statusItemPlaceholder === '');
+ }
+
+ get neighborLocation(): NeighborItemProps['neighborLocation'] {
+ const {
+ addonLeft: AddonLeft,
+ addonRight: AddonRight,
+ } = this.asProps;
+
+ let neighborLocation: NeighborItemProps['neighborLocation'] = undefined;
+
+ if (AddonLeft && AddonRight) {
+ neighborLocation = 'both';
+ } else if (AddonLeft) {
+ neighborLocation = 'left';
+ } else if (AddonRight) {
+ neighborLocation = 'right';
+ }
+
+ return neighborLocation;
+ }
+
+ getTriggerProps(): NSAutoSuggest.Trigger.InnerProps {
+ const { size, getI18nText, neighborLocation, addonLeft, addonRight, uid } = this.asProps;
+ const { isLoading } = this.state;
+
+ return {
+ 'tag': Input,
+ 'onFocus': this.handleFocus,
+ 'onBlur': this.handleBlur,
+ 'aria-haspopup': 'listbox',
+ 'aria-expanded': this.isAriaExpanded ? 'true' : 'false',
+ 'aria-controls': `igc-${uid}-list`,
+ isLoading,
+ size,
+ getI18nText,
+ neighborLocation,
+ addonLeft,
+ addonRight,
+ };
+ }
+
+ getTriggerValueProps(): NSAutoSuggest.Trigger.Value.InnerProps {
+ const { value, forwardRef, autoComplete, role, onChange, ref, id, ...props } = this.asProps;
+
+ return {
+ autoComplete: 'off',
+ onChange: this.handleChange,
+ onKeyDown: this.handleKeyDown,
+ role: 'combobox',
+ value,
+ ref: forwardRef ? forkRef(forwardRef, this.triggerRef) : this.triggerRef,
+ id: this.id,
+ ...props,
+ neighborLocation: this.neighborLocation,
+ };
+ }
+
+ getPopperProps(): NSAutoSuggest.Popper.InnerProps {
+ return {
+ 'aria-labelledby': this.id,
+ 'ref': this.popperRef,
+ };
+ }
+
+ getPopperLoadingStateProps(): NSAutoSuggest.Popper.LoadingState.InnerProps {
+ const { isLoading } = this.state;
+
+ return {
+ isLoading,
+ };
+ }
+
+ getPopperStartTypingStateProps(): NSAutoSuggest.Popper.StartTypingState.InnerProps {
+ const { getI18nText, statusItemPlaceholder } = this.asProps;
+ const { isLoading } = this.state;
+
+ return {
+ isLoading,
+ isStartTypingState: this.isStartTypingState,
+ children: statusItemPlaceholder ?? getI18nText('AutoSuggest.Popper.placeholderText'),
+ };
+ }
+
+ getPopperListProps(): NSAutoSuggest.Popper.List.InnerProps {
+ const { value } = this.asProps;
+ const { isLoading, suggestions } = this.state;
+
+ const triggerElement = this.triggerRef.current;
+
+ return {
+ value,
+ isLoading,
+ suggestions,
+ 'isStartTypingState': this.isStartTypingState,
+ 'aria-label': getAccessibleName(triggerElement),
+ };
+ }
+
+ getPopperSuggestionItemProps(_: never, index: number): NSAutoSuggest.Popper.SuggestionItem.InnerProps {
+ const { suggestions } = this.state;
+ const { value } = this.asProps;
+
+ const option = suggestions[index];
+
+ return {
+ value: option,
+ selected: false,
+ onClick: (e: React.SyntheticEvent) => this.handleChangeSelect(option, e),
+ children: (
+ {option}
+ ),
+ };
+ }
+
+ handleChange = (value: string, event: React.SyntheticEvent) => {
+ this.handlers.value(value, event);
+
+ if (this.changeDebounce) {
+ clearTimeout(this.changeDebounce);
+ }
+ if (this.abortController) {
+ this.abortController.abort();
+ }
+
+ if (value !== this.asProps.value) {
+ const { suggestions, duration } = this.asProps;
+
+ if (value === '') {
+ this.setState({ isVisible: false });
+
+ setTimeout(() => {
+ this.setState({ suggestions: [] });
+ }, Number(duration)); // wait for closing and then clear suggestions.
+ return;
+ }
+
+ if (!Array.isArray(suggestions)) {
+ this.setState({ isLoading: true, isVisible: true });
+ }
+
+ this.changeDebounce = window.setTimeout(async () => {
+ if (Array.isArray(suggestions)) {
+ const filteredSuggestions = value === '' ? [] : suggestions.filter((s) => s.toLowerCase().includes(value.toLowerCase()));
+
+ this.setState({ suggestions: filteredSuggestions });
+ } else {
+ this.abortController = new AbortController();
+ const abortSignal = this.abortController.signal;
+
+ const filteredSuggestions = await suggestions(value, abortSignal);
+
+ if (!this.abortController.signal.aborted) {
+ this.setState({ suggestions: filteredSuggestions, isLoading: false, isVisible: filteredSuggestions.length > 0 });
+ }
+ }
+
+ if (this.state.openOnChanges) {
+ this.handleChangeVisible(true);
+ }
+ }, 300);
+ }
+ };
+
+ handleChangeVisible = (isVisible: boolean) => {
+ this.setState({ isVisible });
+ };
+
+ handleChangeHighlightedIndex = (index: number | null) => {
+ this.setState({ highlightedIndex: index ?? -1 });
+ };
+
+ handleKeyDown = (e: React.KeyboardEvent) => {
+ if (!e.key.startsWith('Arrow')) {
+ this.setState({ highlightedIndex: -1 });
+ }
+
+ const { value } = this.asProps;
+ const { isVisible, suggestions } = this.state;
+
+ if (isVisible) {
+ if (e.key === 'Escape') {
+ this.setState({ openOnChanges: false });
+ }
+ } else {
+ const filteredSuggestions = suggestions.filter((s) => {
+ return value !== '' && s.toLowerCase().includes(value.toLowerCase());
+ });
+
+ if (e.key === 'ArrowDown') {
+ this.setState({
+ suggestions: filteredSuggestions,
+ highlightedIndex: 0,
+ });
+ }
+ if (e.key === 'ArrowUp') {
+ this.setState({
+ suggestions: filteredSuggestions,
+ highlightedIndex: filteredSuggestions.length - 1,
+ });
+ }
+ }
+ };
+
+ handleChangeSelect = (value: string, e: React.SyntheticEvent) => {
+ this.handlers.value(value, e);
+ };
+
+ handleFocus = () => {
+ const { value, statusItemPlaceholder } = this.asProps;
+ const { suggestions } = this.state;
+ this.setState({
+ openOnChanges: true,
+ isVisible: statusItemPlaceholder === '' ? value !== '' : true,
+ suggestions: suggestions.filter((s) => {
+ return value !== '' && s.toLowerCase().includes(value.toLowerCase());
+ }),
+ highlightedIndex: -1,
+ });
+ };
+
+ handleBlur = () => {
+ setTimeout(() => {
+ const popperElement = this.popperRef.current;
+
+ if (!popperElement || !isFocusInside(popperElement)) {
+ this.handleChangeVisible(false);
+ }
+ });
+ };
+
+ render() {
+ const { highlightedIndex } = this.state;
+
+ return (
+
+ );
+ }
+}
+
+class TriggerRoot extends Component<
+ Intergalactic.InternalTypings.InferChildComponentProps
+> {
+ static displayName = 'Trigger';
+
+ static defaultProps = () => {
+ return {
+ children: (
+
+ ),
+ };
+ };
+
+ render() {
+ const {
+ isLoading,
+ size,
+ addonLeft: AddonLeft,
+ addonRight: AddonRight,
+ Children,
+ } = this.asProps;
+
+ return (
+
+ {AddonLeft
+ ? (
+
+
+
+ )
+ : null}
+
+ {AddonRight
+ ? (
+
+
+
+ )
+ : null}
+ {isLoading && (
+
+
+
+ )}
+
+ );
+ }
+}
+
+class ValueRoot extends Component<
+ Intergalactic.InternalTypings.InferChildComponentProps
+> {
+ static displayName = 'Value';
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+class PopperRoot extends Component<
+ Intergalactic.InternalTypings.InferChildComponentProps
+> {
+ static defaultProps = () => {
+ return {
+ children: (
+ <>
+
+
+
+ >
+ ),
+ };
+ };
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+function LoadingStateRoot(
+ props: Intergalactic.InternalTypings.InferChildComponentProps,
+) {
+ const { Children, isLoading, children } = props;
+
+ if (!isLoading) return null;
+
+ return children === undefined
+ ? ()
+ : (
+
+
+
+ );
+}
+
+function StartTypingStateRoot(
+ props: Intergalactic.InternalTypings.InferChildComponentProps,
+) {
+ const { Children, isLoading, isStartTypingState } = props;
+
+ if (isLoading || !isStartTypingState) return null;
+
+ return (
+
+
+
+ );
+}
+
+class ListRoot extends Component<
+ Intergalactic.InternalTypings.InferChildComponentProps
+> {
+ static defaultProps = () => {
+ return {
+ children: (),
+ };
+ };
+
+ render() {
+ const { suggestions, isLoading, isStartTypingState, Children } = this.asProps;
+
+ if (isLoading || isStartTypingState) return null;
+
+ return (
+
+ {suggestions.map((option) => (
+
+ ))}
+
+ );
+ }
+}
+
+function SuggestionItemRoot() {
+ return (
+
+ );
+}
+
+export const AutoSuggest = createComponent(AutoSuggestRoot, {
+ Trigger: [TriggerRoot, { Value: ValueRoot }],
+ Popper: [PopperRoot, {
+ LoadingState: LoadingStateRoot,
+ StartTypingState: StartTypingStateRoot,
+ List: ListRoot,
+ SuggestionItem: SuggestionItemRoot,
+ }],
+});
diff --git a/semcore/select/src/components/AutoSuggest/AutoSuggest.type.ts b/semcore/select/src/components/AutoSuggest/AutoSuggest.type.ts
new file mode 100644
index 0000000000..5a492832f4
--- /dev/null
+++ b/semcore/select/src/components/AutoSuggest/AutoSuggest.type.ts
@@ -0,0 +1,149 @@
+import type { NeighborItemProps } from '@semcore/base-components';
+import type { Intergalactic } from '@semcore/core';
+import type Dropdown from '@semcore/dropdown';
+import type { DropdownTriggerProps } from '@semcore/dropdown';
+import type { InputValueProps, InputProps } from '@semcore/input';
+import type Input from '@semcore/input';
+import type React from 'react';
+
+import type Select from '../../index';
+
+declare namespace NSAutoSuggest {
+ type Suggestion = string;
+
+ type Props = InputValueProps & {
+ /**
+ * List of suggestions or async function to load suggestions.
+ */
+ suggestions: Suggestion[] | ((value: string, signal: AbortSignal) => Promise);
+ /**
+ * Placeholder in popper for init state.
+ * Set an empty string to hide init state.
+ */
+ statusItemPlaceholder?: string;
+ /** Tag for the left Addon */
+ addonLeft?: React.ElementType;
+ /** Tag for the right Addon */
+ addonRight?: React.ElementType;
+ };
+
+ type State = {
+ isVisible: boolean;
+ highlightedIndex: number;
+ suggestions: Suggestion[];
+ openOnChanges: boolean;
+ isLoading: boolean;
+ };
+
+ type DefaultProps = {
+ defaultValue: string;
+ placeholder: string;
+ children: React.ReactNode;
+ };
+
+ type Handlers = {
+ value: null;
+ };
+
+ namespace Trigger {
+ type Props = {};
+
+ type InnerProps = {
+ 'tag': typeof Input;
+ 'onFocus': () => void;
+ 'onBlur': () => void;
+ 'aria-haspopup': 'listbox';
+ 'aria-expanded': 'true' | 'false';
+ 'aria-controls': string;
+ 'addonLeft'?: React.ElementType;
+ 'addonRight'?: React.ElementType;
+ 'isLoading': NSAutoSuggest.State['isLoading'];
+ 'size': NSAutoSuggest.Props['size'];
+ 'getI18nText': (str: string) => string;
+ 'neighborLocation': NeighborItemProps['neighborLocation'];
+ };
+
+ namespace Value {
+ type Props = {};
+
+ type InnerProps = {
+ id: string;
+ neighborLocation: NeighborItemProps['neighborLocation'];
+ value: string;
+ role: 'combobox';
+ onChange: (value: string, e: React.SyntheticEvent) => void;
+ onKeyDown: (e: React.KeyboardEvent) => void;
+ autoComplete: 'off';
+ ref?: React.Ref;
+ };
+
+ type Component = Intergalactic.Component;
+ }
+
+ type Component = Intergalactic.Component & {
+ Value: Value.Component;
+ };
+ }
+ namespace Popper {
+ type Props = {};
+
+ type InnerProps = {
+ 'aria-labelledby': string;
+ 'ref': React.RefObject;
+ };
+
+ namespace LoadingState {
+ type InnerProps = {
+ isLoading: boolean;
+ };
+
+ type Component = Intergalactic.Component;
+ }
+ namespace StartTypingState {
+ type InnerProps = {
+ isLoading: boolean;
+ isStartTypingState: boolean;
+ children: string;
+ };
+
+ type Component = Intergalactic.Component;
+ }
+ namespace List {
+ type InnerProps = {
+ 'value': string;
+ 'isLoading': boolean;
+ 'suggestions': Suggestion[];
+ 'isStartTypingState': boolean;
+ 'aria-label': string;
+ };
+
+ type Component = Intergalactic.Component;
+ }
+ namespace SuggestionItem {
+ type InnerProps = {
+ value: Suggestion;
+ selected: false;
+ onClick: (e: React.SyntheticEvent) => void;
+ children: React.ReactNode;
+ };
+
+ type Component = Intergalactic.Component;
+ }
+
+ type Component = Intergalactic.Component & {
+ LoadingState: LoadingState.Component;
+ StartTypingState: StartTypingState.Component;
+ List: List.Component;
+ SuggestionItem: SuggestionItem.Component;
+ };
+ }
+
+ type Component = Intergalactic.Component & {
+ Trigger: Trigger.Component;
+ Popper: Popper.Component;
+ };
+}
+
+export {
+ NSAutoSuggest,
+};
diff --git a/semcore/select/src/components/AutoSuggest/Highlight.tsx b/semcore/select/src/components/AutoSuggest/Highlight.tsx
new file mode 100644
index 0000000000..d010b3e5b0
--- /dev/null
+++ b/semcore/select/src/components/AutoSuggest/Highlight.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+
+type HighlightProps = {
+ highlight: string;
+ children: string;
+};
+
+export function Highlight({ highlight, children }: HighlightProps) {
+ const ref = React.useRef(null);
+
+ React.useEffect(() => {
+ const child = document.createElement('span');
+
+ if (highlight) {
+ const regexp = new RegExp(RegExp.escape(highlight.toLowerCase()), 'ig');
+ const results = [...children.matchAll(regexp)];
+
+ let cursor = 0;
+
+ for (const result of results) {
+ const foundStr = result[0];
+ const index = result.index;
+
+ const before = children.slice(cursor, index);
+ const bold = children.slice(index, index + foundStr.length);
+
+ cursor = index + foundStr.length;
+
+ const beforeNode = document.createTextNode(before);
+ const boldNode = document.createElement('strong');
+ boldNode.textContent = bold;
+
+ child.appendChild(beforeNode);
+ child.appendChild(boldNode);
+ }
+
+ if (cursor < children.length) {
+ const after = children.slice(cursor);
+ const afterNode = document.createTextNode(after);
+ child.appendChild(afterNode);
+ }
+ } else {
+ child.textContent = children;
+ }
+
+ ref.current?.appendChild(child);
+
+ return () => {
+ child.remove();
+ };
+ }, [highlight, children]);
+
+ return ;
+}
diff --git a/semcore/select/src/index.d.ts b/semcore/select/src/index.d.ts
index 9003c3a6dc..a2e244500b 100644
--- a/semcore/select/src/index.d.ts
+++ b/semcore/select/src/index.d.ts
@@ -17,6 +17,8 @@ import type Input from '@semcore/input';
import type { Text } from '@semcore/typography';
import type React from 'react';
+import type { NSAutoSuggest } from './components/AutoSuggest/AutoSuggest.type.ts';
+
export type SelectInputSearch = InputValueProps & {};
export type OptionValue = string | number;
@@ -172,5 +174,7 @@ declare const wrapSelect: (
) => React.ReactNode,
) => IntergalacticSelectComponent;
-export { InputSearch, wrapSelect };
+declare const AutoSuggest = NSAutoSuggest.Component;
+
+export { InputSearch, wrapSelect, AutoSuggest, NSAutoSuggest };
export default Select;
diff --git a/semcore/select/src/index.js b/semcore/select/src/index.js
index 96e699c8ca..843f03a702 100644
--- a/semcore/select/src/index.js
+++ b/semcore/select/src/index.js
@@ -1,3 +1,4 @@
export { default as InputSearch } from './InputSearch';
export { default } from './Select';
export * from './Select';
+export { AutoSuggest } from './components/AutoSuggest/AutoSuggest';
diff --git a/semcore/select/src/translations/en.json b/semcore/select/src/translations/en.json
index a8a32c02d4..30c237891b 100644
--- a/semcore/select/src/translations/en.json
+++ b/semcore/select/src/translations/en.json
@@ -2,5 +2,6 @@
"clearSearch": "Clear search field",
"selectPlaceholder": "Select option",
"Select.InputSearch.Value:placeholder": "Search",
- "Select.InputSearch.Value:aria-label": "Search"
+ "Select.InputSearch.Value:aria-label": "Search",
+ "AutoSuggest.Popper.placeholderText": "Start typing to see options"
}
diff --git a/semcore/time-picker/__tests__/time-picker.browser-test.tsx b/semcore/time-picker/__tests__/time-picker.browser-test.tsx
index cfd9d07ec3..b41bcaead7 100644
--- a/semcore/time-picker/__tests__/time-picker.browser-test.tsx
+++ b/semcore/time-picker/__tests__/time-picker.browser-test.tsx
@@ -529,6 +529,8 @@ test.describe(`${TAG.FUNCTIONAL} `, () => {
await loadPage(page, 'stories/components/time-picker/docs/examples/expanded_access_to_all_the_components.tsx', 'en');
const formatButton = page.locator('[data-ui-name="TimePicker.Format"] span');
+ const formatValueInit = (await formatButton.textContent())?.trim();
+ expect(formatValueInit).toBe('AM');
await test.step('Verify Format can be changed by keyboard when nothing entered', async () => {
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
@@ -539,10 +541,6 @@ test.describe(`${TAG.FUNCTIONAL} `, () => {
});
await test.step('Verify Format can be changed by mouse', async () => {
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Space');
await formatButton.click();
const formatValue = (await formatButton.textContent())?.trim();
expect(formatValue).toBe('AM');
diff --git a/semcore/time-picker/__tests__/ux-patterns-with-date-picker/time-date-picker.browser-test.tsx-snapshots/-visual-Verify-pattern-with-Time-Picker-and-Date-picker-1-chromium-linux.png b/semcore/time-picker/__tests__/ux-patterns-with-date-picker/time-date-picker.browser-test.tsx-snapshots/-visual-Verify-pattern-with-Time-Picker-and-Date-picker-1-chromium-linux.png
index a13a22a917..e1c1693001 100644
Binary files a/semcore/time-picker/__tests__/ux-patterns-with-date-picker/time-date-picker.browser-test.tsx-snapshots/-visual-Verify-pattern-with-Time-Picker-and-Date-picker-1-chromium-linux.png and b/semcore/time-picker/__tests__/ux-patterns-with-date-picker/time-date-picker.browser-test.tsx-snapshots/-visual-Verify-pattern-with-Time-Picker-and-Date-picker-1-chromium-linux.png differ
diff --git a/semcore/time-picker/__tests__/ux-patterns-with-date-picker/time-date-picker.browser-test.tsx-snapshots/-visual-Verify-pattern-with-Time-Picker-and-Date-picker-1-firefox-linux.png b/semcore/time-picker/__tests__/ux-patterns-with-date-picker/time-date-picker.browser-test.tsx-snapshots/-visual-Verify-pattern-with-Time-Picker-and-Date-picker-1-firefox-linux.png
index d8d433c707..b22498f138 100644
Binary files a/semcore/time-picker/__tests__/ux-patterns-with-date-picker/time-date-picker.browser-test.tsx-snapshots/-visual-Verify-pattern-with-Time-Picker-and-Date-picker-1-firefox-linux.png and b/semcore/time-picker/__tests__/ux-patterns-with-date-picker/time-date-picker.browser-test.tsx-snapshots/-visual-Verify-pattern-with-Time-Picker-and-Date-picker-1-firefox-linux.png differ
diff --git a/semcore/time-picker/__tests__/ux-patterns-with-date-picker/time-date-picker.browser-test.tsx-snapshots/-visual-Verify-pattern-with-Time-Picker-and-Date-picker-1-webkit-linux.png b/semcore/time-picker/__tests__/ux-patterns-with-date-picker/time-date-picker.browser-test.tsx-snapshots/-visual-Verify-pattern-with-Time-Picker-and-Date-picker-1-webkit-linux.png
index f8c52c0a5d..3c67be2042 100644
Binary files a/semcore/time-picker/__tests__/ux-patterns-with-date-picker/time-date-picker.browser-test.tsx-snapshots/-visual-Verify-pattern-with-Time-Picker-and-Date-picker-1-webkit-linux.png and b/semcore/time-picker/__tests__/ux-patterns-with-date-picker/time-date-picker.browser-test.tsx-snapshots/-visual-Verify-pattern-with-Time-Picker-and-Date-picker-1-webkit-linux.png differ
diff --git a/stories/components/dropdown-menu/advanced/examples/project-selector.tsx b/stories/components/dropdown-menu/advanced/examples/project-selector.tsx
index 3ca01ff2e3..23ba98cdf8 100644
--- a/stories/components/dropdown-menu/advanced/examples/project-selector.tsx
+++ b/stories/components/dropdown-menu/advanced/examples/project-selector.tsx
@@ -111,7 +111,6 @@ const Demo = (props: ProjectSelectorProps) => {
onChange={setSearchValue}
m={1}
autoFocus={false}
- aria-describedby={searchValue ? 'search-result' : undefined}
/>
{filteredProjects.length > 0 && (
@@ -129,7 +128,7 @@ const Demo = (props: ProjectSelectorProps) => {
}}
/>
)}
-
+
{
-
+
{filteredMenuData.length > 0 && (
@@ -132,7 +132,7 @@ const Demo = () => {
})}
)}
-
+
{
autoFocus={isVisible}
value={search}
onChange={setSearch}
- aria-describedby={search ? 'search-result' : undefined}
/>
{props.state !== 'loading' && props.state !== 'error' && (
@@ -70,7 +69,6 @@ const Demo = (props: DropDownPropsExample) => {
{props.customChildren || undefined}
diff --git a/stories/components/dropdown-menu/tests/examples/selectable-props.tsx b/stories/components/dropdown-menu/tests/examples/selectable-props.tsx
index 7fed945b61..4b5e60b521 100644
--- a/stories/components/dropdown-menu/tests/examples/selectable-props.tsx
+++ b/stories/components/dropdown-menu/tests/examples/selectable-props.tsx
@@ -51,7 +51,6 @@ const Demo = (props: DropDownPropsExample) => {
autoFocus={isVisible}
value={search}
onChange={setSearch}
- aria-describedby={search ? 'search-result' : undefined}
/>
{props.state !== 'loading' && props.state !== 'error' && (
@@ -75,7 +74,6 @@ const Demo = (props: DropDownPropsExample) => {
{props.customChildren || undefined}
diff --git a/stories/components/notice-bubble/docs/examples/replace_last_notice.tsx b/stories/components/notice-bubble/docs/examples/replace_last_notice.tsx
index 6de31f130e..e786d71365 100644
--- a/stories/components/notice-bubble/docs/examples/replace_last_notice.tsx
+++ b/stories/components/notice-bubble/docs/examples/replace_last_notice.tsx
@@ -1,4 +1,5 @@
import Button from '@semcore/ui/button';
+import { lastInteraction } from '@semcore/ui/core';
import { NoticeBubbleContainer, NoticeBubbleManager } from '@semcore/ui/notice-bubble';
import React from 'react';
@@ -8,6 +9,8 @@ let counter = 0;
const manager = new NoticeBubbleManager();
const Demo = (props: ReplaceLastNoticeBubbleProps) => {
+ const openButtonRef = React.useRef(null);
+
const handleClick = () => {
counter++;
@@ -17,12 +20,21 @@ const Demo = (props: ReplaceLastNoticeBubbleProps) => {
duration: props.duration,
type: props.type,
focusLock: props.focusLock,
+ onClose: () => {
+ if (lastInteraction.isKeyboard()) {
+ setTimeout(() => {
+ openButtonRef.current?.focus();
+ }, 300);
+ }
+ },
});
};
return (
<>
-
+
>
);
diff --git a/stories/components/select/docs/examples/advanced_filtering_control.tsx b/stories/components/select/docs/examples/advanced_filtering_control.tsx
index fe1300d6aa..8007ea7c9c 100644
--- a/stories/components/select/docs/examples/advanced_filtering_control.tsx
+++ b/stories/components/select/docs/examples/advanced_filtering_control.tsx
@@ -23,7 +23,7 @@ const Demo = () => {
-
+
{
alert('Clicked on the Clear button');
@@ -40,7 +40,7 @@ const Demo = () => {
))}
)}
-
+
diff --git a/stories/components/select/docs/examples/options_filtering.tsx b/stories/components/select/docs/examples/options_filtering.tsx
index ebcf6ff16a..8535d4f571 100644
--- a/stories/components/select/docs/examples/options_filtering.tsx
+++ b/stories/components/select/docs/examples/options_filtering.tsx
@@ -24,7 +24,6 @@ const Demo = () => {
{options.length > 0 && (
@@ -35,7 +34,7 @@ const Demo = () => {
))}
)}
-
+ {filter !== '' && }
diff --git a/stories/components/select/tests/examples/basic_props_and_trigger_addons.tsx b/stories/components/select/tests/examples/basic_props_and_trigger_addons.tsx
index 5d4537640e..37fed7c4c5 100644
--- a/stories/components/select/tests/examples/basic_props_and_trigger_addons.tsx
+++ b/stories/components/select/tests/examples/basic_props_and_trigger_addons.tsx
@@ -6,7 +6,12 @@ import type { SelectProps } from '@semcore/ui/select';
import { Text } from '@semcore/ui/typography';
import React from 'react';
-export type SelectBasicProps = SelectProps & {
+type SelectBasicValue = number | number[] | null;
+
+export type SelectBasicProps = Omit<
+ SelectProps,
+ 'value' | 'defaultValue' | 'onChange' | 'options' | 'tag'
+> & {
labelText?: string;
showLabel?: boolean;
optionCount?: number;
@@ -70,6 +75,16 @@ const Demo = (props: SelectBasicProps) => {
}));
const hasCustomTrigger = showLeftAddon || showRightAddon || showTriggerText;
+ const [value, setValue] = React.useState(multiselect ? [] : null);
+
+ React.useEffect(() => {
+ setValue(multiselect ? [] : null);
+ }, [multiselect]);
+
+ const selectedTriggerText = options
+ .filter((option) => Array.isArray(value) ? value.includes(option.value) : value === option.value)
+ .map((option) => option.label)
+ .join(', ');
return (
@@ -78,56 +93,73 @@ const Demo = (props: SelectBasicProps) => {
{labelText}
)}
-
+ {hasCustomTrigger
+ ? (
+
+ )
+ : (
+
+ )}
);
};
diff --git a/stories/components/select/tests/examples/on_change_input_search.tsx b/stories/components/select/tests/examples/on_change_input_search.tsx
index 6a1f0a5aac..88600c69d2 100644
--- a/stories/components/select/tests/examples/on_change_input_search.tsx
+++ b/stories/components/select/tests/examples/on_change_input_search.tsx
@@ -39,7 +39,6 @@ const Demo = ({ state = 'default', customChildren, size = 'm' }: OnChangeInputSe
{state === 'default' &&
@@ -49,7 +48,7 @@ const Demo = ({ state = 'default', customChildren, size = 'm' }: OnChangeInputSe
))}
-
+
{customChildren || undefined}
diff --git a/stories/patterns/filters/serp-features/docs/examples/serp-filter.tsx b/stories/patterns/filters/serp-features/docs/examples/serp-filter.tsx
index 24868a59f1..55e5d96c6f 100644
--- a/stories/patterns/filters/serp-features/docs/examples/serp-filter.tsx
+++ b/stories/patterns/filters/serp-features/docs/examples/serp-filter.tsx
@@ -1,9 +1,8 @@
import ReloadIcon from '@semcore/icon/Reload/m';
-import { Flex, Box, ScreenReaderOnly, ScrollArea, hideScrollBarsFromScreenReadersContext } from '@semcore/ui/base-components';
+import { Flex, Box, ScrollArea, hideScrollBarsFromScreenReadersContext } from '@semcore/ui/base-components';
import { FilterTrigger } from '@semcore/ui/base-trigger';
import Button, { ButtonLink } from '@semcore/ui/button';
import Select, { InputSearch } from '@semcore/ui/select';
-import { Text } from '@semcore/ui/typography';
import React from 'react';
const serpFeatures = [
@@ -195,18 +194,17 @@ const Demo = () => {
{loading && ()}
{!loading && error && (
-
-
+
+
{message}
-
-
+
+
Reload
-
+
)}
{!loading && !error && (
<>
diff --git a/stories/patterns/ux-patterns/auto-suggest/docs/__tests__/autosuggest_example.test.tsx b/stories/patterns/ux-patterns/auto-suggest/docs/__tests__/autosuggest_example.test.tsx
index 1e6345a6a7..6ce589ce1c 100644
--- a/stories/patterns/ux-patterns/auto-suggest/docs/__tests__/autosuggest_example.test.tsx
+++ b/stories/patterns/ux-patterns/auto-suggest/docs/__tests__/autosuggest_example.test.tsx
@@ -3,11 +3,15 @@ import { expect, userEvent, within } from 'storybook/test';
export async function AutoSuggestTest({ canvasElement }: { canvasElement: HTMLElement }) {
const canvas = within(canvasElement);
- const inputTrigger = within(document.body).getByPlaceholderText('Start typing for options');
+ // The example sets no `placeholder` (defaults to ''); inputs are associated with
+ // their labels via htmlFor/id. Use the sync input for a deterministic assertion.
+ const input = canvas.getByLabelText('SYNC Your pet breed');
- if (!inputTrigger) {
- throw new Error('Section 1 not found');
- }
- await userEvent.click(inputTrigger);
- await userEvent.type(inputTrigger, 'a');
+ await userEvent.click(input);
+ await userEvent.type(input, 'a');
+
+ // Suggestions render in a portal on document.body, not inside canvasElement.
+ // Typing "a" matches several breeds, so expect multiple options.
+ const options = await within(document.body).findAllByRole('option', {}, { timeout: 3000 });
+ expect(options.length).toBeGreaterThan(0);
}
diff --git a/stories/patterns/ux-patterns/auto-suggest/docs/examples/autosuggest_example.tsx b/stories/patterns/ux-patterns/auto-suggest/docs/examples/autosuggest_example.tsx
index e89356c96e..ea83fc972c 100644
--- a/stories/patterns/ux-patterns/auto-suggest/docs/examples/autosuggest_example.tsx
+++ b/stories/patterns/ux-patterns/auto-suggest/docs/examples/autosuggest_example.tsx
@@ -1,142 +1,84 @@
import { Box } from '@semcore/ui/base-components';
-import Input from '@semcore/ui/input';
-import Select from '@semcore/ui/select';
+import { AutoSuggest } from '@semcore/ui/select';
import { Text } from '@semcore/ui/typography';
import React from 'react';
-const Highlight = ({ highlight, children }: { highlight: string; children: string }) => {
- let html = children;
- if (highlight) {
- try {
- const re = new RegExp(highlight.toLowerCase(), 'g');
- html = html.replace(
- re,
- `${highlight}`,
- );
- } catch (e) {}
- }
- return ;
-};
-
-const debounce = (func: Function, timeout: number) => {
- let timer: number;
- return (...args: any[]) => {
- window.clearTimeout(timer);
- timer = window.setTimeout(() => {
- func(...args);
- }, timeout);
- };
-};
+const suggestions = [
+ 'persian',
+ 'maine coon',
+ 'ragdoll',
+ 'sphynx',
+ 'siamese',
+ 'bengal',
+ 'british shorthair',
+ 'abyssinian',
+ 'birman',
+ 'oriental shorthair',
+ 'scottish fold',
+ 'devon rex',
+ 'norwegian forest',
+ 'siberian',
+ 'russian blue',
+ 'savannah',
+ 'american shorthair',
+ 'exotic shorthair',
+ 'ragamuffin',
+ 'balinese',
+];
-type Suggestion = {
- value: string;
- title: string;
-};
-
-const fakeFetch = async (query: string): Promise => {
+const fakeFetch = async (query: string, signal: AbortSignal): Promise => {
if (!query) return [];
- return [
- 'persian',
- 'maine coon',
- 'ragdoll',
- 'sphynx',
- 'siamese',
- 'bengal',
- 'british shorthair',
- 'abyssinian',
- 'birman',
- 'oriental shorthair',
- 'scottish fold',
- 'devon rex',
- 'norwegian forest',
- 'siberian',
- 'russian blue',
- 'savannah',
- 'american shorthair',
- 'exotic shorthair',
- 'ragamuffin',
- 'balinese',
- ]
- .filter((breed) => breed.toLowerCase().includes(query.toLowerCase()))
- .map((value) => ({ value, title: value }));
-};
+ if (signal.aborted) {
+ return [];
+ }
-const Demo = () => {
- const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
- const [visible, setVisible] = React.useState(false);
- const [query, setQuery] = React.useState('');
- const [suggestions, setSuggestions] = React.useState([]);
- const loadSuggestions = React.useCallback(
- debounce(
- (query: string) => fakeFetch(query).then((suggestions) => setSuggestions(suggestions)),
- 300,
- ),
- [],
- );
- React.useEffect(() => {
- loadSuggestions(query);
- }, [query]);
- const handleSelect = React.useCallback((x: string) => {
- setQuery(x);
- setVisible(false);
- }, []);
+ return new Promise((resolve) => {
+ const onAbort = () => {
+ signal.removeEventListener('abort', onAbort);
+ resolve([]);
+ };
+ signal.addEventListener('abort', onAbort);
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (!e.key.startsWith('Array')) {
- setHighlightedIndex(-1);
- }
- };
+ setTimeout(() => {
+ signal.removeEventListener('abort', onAbort);
- const handleHighlightedIndexChange = (index: number | null) => {
- setHighlightedIndex(index);
- };
+ resolve(suggestions.filter((breed) => breed.toLowerCase().includes(query.toLowerCase())));
+ }, 2000);
+ });
+};
- const handleChangeVisible = (visible: boolean) => {
- setVisible(visible);
- };
+const Demo = () => {
+ const [query1, setQuery1] = React.useState('');
+ const [query2, setQuery2] = React.useState('');
return (
- <>
-
- Your pet breed
+
+
+ ASYNC Your pet breed
+
+
+
+
+
+
+ SYNC Your pet breed
-
-
+
+
- >
+
);
};
diff --git a/stories/patterns/ux-patterns/auto-suggest/tests/AutoSuggest.stories.tsx b/stories/patterns/ux-patterns/auto-suggest/tests/AutoSuggest.stories.tsx
new file mode 100644
index 0000000000..b20b642b8b
--- /dev/null
+++ b/stories/patterns/ux-patterns/auto-suggest/tests/AutoSuggest.stories.tsx
@@ -0,0 +1,119 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
+import AutosuggestCompositionExample, {
+ autosuggestCompositionDefaultProps,
+} from './examples/autosuggest_composition';
+import type { AutosuggestCompositionProps } from './examples/autosuggest_composition';
+import AutosuggestTestExample, { autosuggestTestDefaultProps } from './examples/autosuggest_test';
+import type { AutosuggestTestProps } from './examples/autosuggest_test';
+
+const meta: Meta = {
+ title: 'Patterns/UX Patterns/AutoSuggest/Tests',
+};
+export default meta;
+
+const commonArgs = {
+ suggestionsSource: 'sync',
+ asyncDelay: 1000,
+ size: 'm',
+ statusItemPlaceholder: 'Start typing to see options',
+ addonLeft: 'none',
+ addonRight: 'none',
+} satisfies Pick<
+ AutosuggestTestProps & AutosuggestCompositionProps,
+ 'suggestionsSource' | 'asyncDelay' | 'size' | 'statusItemPlaceholder' | 'addonLeft' | 'addonRight'
+>;
+
+const commonArgTypes = {
+ suggestionsSource: {
+ control: { type: 'radio' },
+ options: ['sync', 'async'],
+ },
+ asyncDelay: {
+ control: { type: 'number', min: 0, step: 100 },
+ },
+ width: {
+ control: { type: 'number', min: 160, step: 20 },
+ },
+ placeholder: {
+ control: 'text',
+ },
+ size: {
+ control: { type: 'radio' },
+ options: ['m', 'l'],
+ },
+ statusItemPlaceholder: {
+ control: 'text',
+ },
+ addonLeft: {
+ control: { type: 'select' },
+ options: ['none', 'icon', 'badge', 'tag'],
+ },
+ addonRight: {
+ control: { type: 'select' },
+ options: ['none', 'icon', 'badge', 'tag'],
+ },
+} as const;
+
+export const Autosuggest: StoryObj = {
+ render: AutosuggestTestExample,
+ args: {
+ ...autosuggestTestDefaultProps,
+ ...commonArgs,
+ },
+ argTypes: {
+ ...commonArgTypes,
+ initialValue: {
+ control: 'text',
+ },
+ autoFocus: {
+ control: 'boolean',
+ },
+ withPlaceholder: {
+ control: 'boolean',
+ },
+ readOnly: {
+ control: 'boolean',
+ },
+ disabled: {
+ control: 'boolean',
+ },
+ button: {
+ control: { type: 'radio' },
+ options: ['none', 'left', 'right', 'both'],
+ },
+ onChangeLog: {
+ control: 'boolean',
+ },
+ },
+};
+
+export const Composition: StoryObj = {
+ render: AutosuggestCompositionExample,
+ args: {
+ ...autosuggestCompositionDefaultProps,
+ ...commonArgs,
+ },
+ argTypes: {
+ ...commonArgTypes,
+ popperWidth: {
+ control: { type: 'number', min: 0, step: 20 },
+ },
+ popperMaxHeight: {
+ control: { type: 'number', min: 0, step: 20 },
+ },
+ neighborLocation: {
+ control: { type: 'radio' },
+ options: ['none', 'left', 'right', 'both'],
+ },
+ customStartTyping: {
+ control: 'boolean',
+ },
+ customLoadingState: {
+ control: 'boolean',
+ },
+ customSuggestionItem: {
+ control: 'boolean',
+ },
+ },
+};
diff --git a/stories/patterns/ux-patterns/auto-suggest/tests/examples/autosuggest_composition.tsx b/stories/patterns/ux-patterns/auto-suggest/tests/examples/autosuggest_composition.tsx
new file mode 100644
index 0000000000..c769f56db3
--- /dev/null
+++ b/stories/patterns/ux-patterns/auto-suggest/tests/examples/autosuggest_composition.tsx
@@ -0,0 +1,237 @@
+import SearchL from '@semcore/icon/Search/l';
+import SearchM from '@semcore/icon/Search/m';
+import Badge from '@semcore/ui/badge';
+import { Box, Flex } from '@semcore/ui/base-components';
+import Button from '@semcore/ui/button';
+import { AutoSuggest } from '@semcore/ui/select';
+import Spin from '@semcore/ui/spin';
+import Tag from '@semcore/ui/tag';
+import { Text } from '@semcore/ui/typography';
+import React from 'react';
+
+type AddonType = 'none' | 'icon' | 'badge' | 'tag';
+
+const buildAddon = (type: AddonType, size: 'm' | 'l'): React.ElementType | undefined => {
+ if (type === 'icon') {
+ return size === 'l' ? SearchL : SearchM;
+ }
+ if (type === 'badge') {
+ return () => ;
+ }
+ if (type === 'tag') {
+ return () => Tag;
+ }
+ return undefined;
+};
+
+const suggestions = [
+ 'Persian',
+ 'Maine Coon',
+ 'British Shorthair',
+ 'Sphynx',
+ 'Siamese',
+ 'Bengal',
+ 'Abyssinian',
+ 'Birman',
+ 'Oriental Shorthair',
+ 'Scottish Fold',
+ 'Devon Rex',
+ 'Norwegian Forest',
+];
+
+const fakeFetch = async (query: string, signal: AbortSignal, delay: number): Promise => {
+ if (!query) return [];
+ if (signal.aborted) return [];
+
+ return new Promise((resolve) => {
+ const onAbort = () => {
+ signal.removeEventListener('abort', onAbort);
+ resolve([]);
+ };
+ signal.addEventListener('abort', onAbort);
+
+ setTimeout(() => {
+ signal.removeEventListener('abort', onAbort);
+ resolve(suggestions.filter((breed) => breed.toLowerCase().includes(query.toLowerCase())));
+ }, delay);
+ });
+};
+
+export type AutosuggestCompositionProps = {
+ suggestionsSource?: 'sync' | 'async';
+ asyncDelay?: number;
+ size?: 'm' | 'l';
+ width?: number;
+ popperWidth?: number;
+ popperMaxHeight?: number;
+ placeholder?: string;
+ statusItemPlaceholder?: string;
+ addonLeft?: AddonType;
+ addonRight?: AddonType;
+ neighborLocation?: 'none' | 'left' | 'right' | 'both';
+ customStartTyping?: boolean;
+ customLoadingState?: boolean;
+ customSuggestionItem?: boolean;
+};
+
+export const autosuggestCompositionDefaultProps: Required = {
+ suggestionsSource: 'sync',
+ asyncDelay: 1000,
+ size: 'm',
+ width: 320,
+ popperWidth: 0,
+ popperMaxHeight: 0,
+ placeholder: 'Search...',
+ statusItemPlaceholder: 'Start typing to see options',
+ addonLeft: 'none',
+ addonRight: 'none',
+ neighborLocation: 'none',
+ customStartTyping: true,
+ customLoadingState: false,
+ customSuggestionItem: false,
+};
+
+const Demo = (props: AutosuggestCompositionProps) => {
+ const {
+ suggestionsSource,
+ asyncDelay,
+ size,
+ width,
+ popperWidth,
+ popperMaxHeight,
+ placeholder,
+ statusItemPlaceholder,
+ addonLeft,
+ addonRight,
+ neighborLocation,
+ customStartTyping,
+ customLoadingState,
+ customSuggestionItem,
+ } = {
+ ...autosuggestCompositionDefaultProps,
+ ...props,
+ };
+ const [query, setQuery] = React.useState('');
+
+ // neighborLocation squares the AutoSuggest corners adjacent to the button(s).
+ let autoSuggestNeighbor: 'left' | 'right' | 'both' | undefined;
+ if (neighborLocation !== 'none') {
+ autoSuggestNeighbor = neighborLocation;
+ }
+ const hasLeftButton = neighborLocation === 'left' || neighborLocation === 'both';
+ const hasRightButton = neighborLocation === 'right' || neighborLocation === 'both';
+
+ const getSuggestions = React.useCallback(
+ (q: string, signal: AbortSignal) => fakeFetch(q, signal, asyncDelay),
+ [asyncDelay],
+ );
+
+ const addonLeftComponent = React.useMemo(() => buildAddon(addonLeft, size), [addonLeft, size]);
+ const addonRightComponent = React.useMemo(() => buildAddon(addonRight, size), [addonRight, size]);
+
+ // Popper sizing is optional; only pass when configured (0 = use defaults).
+ // Note: stretch='min' (Dropdown default) keeps the popper at least as wide as
+ // the trigger, so popperWidth can widen it but cannot narrow below the input.
+ const popperProps: Record = {};
+ if (popperWidth) popperProps.w = popperWidth;
+ if (popperMaxHeight) popperProps.hMax = popperMaxHeight;
+
+ // Render subcomponents self-closing when not customized — passing children
+ // (even `undefined`) overrides the built-in content, pass children
+ // when we actually want a custom render.
+ let loadingStateEl = ;
+ if (customLoadingState) {
+ loadingStateEl = (
+
+
+
+ Fetching breeds…
+
+
+ );
+ }
+
+ let startTypingStateEl = ;
+ if (customStartTyping) {
+ startTypingStateEl = (
+
+
+
+ Search for your favourite breed
+
+
+ );
+ }
+
+ let suggestionItemEl = ;
+ if (customSuggestionItem) {
+ suggestionItemEl = (
+
+
+
+ Custom option
+
+
+ );
+ }
+
+ const autoSuggestEl = (
+
+
+
+
+
+ {loadingStateEl}
+ {startTypingStateEl}
+
+ {suggestionItemEl}
+
+
+
+ );
+
+ return (
+
+
+ Your pet breed
+
+ {neighborLocation === 'none'
+ ? (
+
+ {autoSuggestEl}
+
+ )
+ : (
+
+ {hasLeftButton && (
+
+ )}
+
+ {autoSuggestEl}
+
+ {hasRightButton && (
+
+ )}
+
+ )}
+
+ );
+};
+
+export default Demo;
diff --git a/stories/patterns/ux-patterns/auto-suggest/tests/examples/autosuggest_test.tsx b/stories/patterns/ux-patterns/auto-suggest/tests/examples/autosuggest_test.tsx
new file mode 100644
index 0000000000..7d7215ce0b
--- /dev/null
+++ b/stories/patterns/ux-patterns/auto-suggest/tests/examples/autosuggest_test.tsx
@@ -0,0 +1,211 @@
+import SearchL from '@semcore/icon/Search/l';
+import SearchM from '@semcore/icon/Search/m';
+import Badge from '@semcore/ui/badge';
+import { Box, Flex } from '@semcore/ui/base-components';
+import Button from '@semcore/ui/button';
+import { AutoSuggest } from '@semcore/ui/select';
+import Tag from '@semcore/ui/tag';
+import { Text } from '@semcore/ui/typography';
+import React from 'react';
+
+type AddonType = 'none' | 'icon' | 'badge' | 'tag';
+
+const buildAddon = (type: AddonType, size: 'm' | 'l'): React.ElementType | undefined => {
+ if (type === 'icon') {
+ return size === 'l' ? SearchL : SearchM;
+ }
+ if (type === 'badge') {
+ return () => ;
+ }
+ if (type === 'tag') {
+ return () => Tag;
+ }
+ return undefined;
+};
+
+const suggestions = [
+ 'Persian',
+ '
',
+ 'cat',
+ 'Sphynx/',
+ '[Siamese',
+ 'Bengal]',
+ 'British Shorthair',
+ 'Abyssinian',
+ 'Birman',
+ 'Oriental Shorthair',
+ 'Scottish Fold',
+ 'Devon Rex',
+ 'Norwegian Forest',
+ 'Siberian',
+ 'Russian Blue',
+ 'Savannah',
+ 'American Shorthair',
+ 'Exotic Shorthair',
+ 'Ragamuffin',
+ 'Balinese',
+];
+
+export type AutosuggestTestProps = {
+ suggestionsSource?: 'sync' | 'async';
+ initialValue?: string;
+ asyncDelay?: number;
+ autoFocus?: boolean;
+ disabled?: boolean;
+ width?: number;
+ withPlaceholder?: boolean;
+ placeholder?: string;
+ size?: 'm' | 'l';
+ readOnly?: boolean;
+ statusItemPlaceholder?: string;
+ addonLeft?: AddonType;
+ addonRight?: AddonType;
+ button?: 'none' | 'left' | 'right' | 'both';
+ onChangeLog?: boolean;
+};
+
+export const autosuggestTestDefaultProps: Required = {
+ suggestionsSource: 'sync',
+ initialValue: '',
+ asyncDelay: 1000,
+ autoFocus: false,
+ width: 250,
+ withPlaceholder: true,
+ placeholder: 'Start typing to see options',
+ size: 'm',
+ readOnly: false,
+ statusItemPlaceholder: 'Start typing to see options',
+ addonLeft: 'none',
+ addonRight: 'none',
+ button: 'none',
+ onChangeLog: false,
+ disabled: false,
+};
+
+const fakeFetch = async (query: string, signal: AbortSignal, delay: number): Promise => {
+ if (!query) return [];
+
+ if (signal.aborted) {
+ return [];
+ }
+
+ return new Promise((resolve) => {
+ const onAbort = () => {
+ signal.removeEventListener('abort', onAbort);
+ resolve([]);
+ };
+ signal.addEventListener('abort', onAbort);
+
+ setTimeout(() => {
+ signal.removeEventListener('abort', onAbort);
+
+ resolve(suggestions.filter((breed) => breed.toLowerCase().includes(query.toLowerCase())));
+ }, delay);
+ });
+};
+
+const Demo = (props: AutosuggestTestProps) => {
+ const {
+ suggestionsSource,
+ initialValue,
+ asyncDelay,
+ autoFocus,
+ width,
+ withPlaceholder,
+ placeholder,
+ size,
+ disabled,
+ readOnly,
+ statusItemPlaceholder,
+ addonLeft,
+ addonRight,
+ button,
+ onChangeLog,
+ } = {
+ ...autosuggestTestDefaultProps,
+ ...props,
+ };
+ const [query, setQuery] = React.useState(initialValue);
+
+ React.useEffect(() => {
+ setQuery(initialValue);
+ }, [initialValue]);
+
+ const handleChange = (value: string, event?: unknown) => {
+ setQuery(value);
+ if (onChangeLog) {
+ console.log('AutoSuggest onChange → value:', value, '| event:', event);
+ }
+ };
+
+ const getSuggestions = React.useCallback(
+ (query: string, signal: AbortSignal) => fakeFetch(query, signal, asyncDelay),
+ [asyncDelay],
+ );
+
+ const addonLeftComponent = React.useMemo(() => buildAddon(addonLeft, size), [addonLeft, size]);
+ const addonRightComponent = React.useMemo(() => buildAddon(addonRight, size), [addonRight, size]);
+
+ const placeholderProp = withPlaceholder ? { placeholder } : {};
+
+ let autoSuggestNeighbor: 'left' | 'right' | 'both' | undefined;
+ if (button === 'left') {
+ autoSuggestNeighbor = 'left';
+ } else if (button === 'right') {
+ autoSuggestNeighbor = 'right';
+ } else if (button === 'both') {
+ autoSuggestNeighbor = 'both';
+ }
+
+ const autoSuggestEl = (
+
+ );
+
+ return (
+ <>
+
+ Your pet breed
+
+ {button === 'none'
+ ? (
+
+ {autoSuggestEl}
+
+ )
+ : (
+
+ {(button === 'left' || button === 'both') && (
+
+ )}
+
+ {autoSuggestEl}
+
+ {(button === 'right' || button === 'both') && (
+
+ )}
+
+ )}
+ >
+ );
+};
+
+export default Demo;
diff --git a/website/docs/patterns/auto-suggest/static/start.png b/website/docs/patterns/auto-suggest/static/start.png
index f0b96a1fa2..586fa1def3 100644
Binary files a/website/docs/patterns/auto-suggest/static/start.png and b/website/docs/patterns/auto-suggest/static/start.png differ