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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions packages/lab/src/MobilePowerSearch/MobilePowerSearch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2632,6 +2632,89 @@ describe('MobilePowerSearch Core parity', () => {
).toHaveLength(9);
});

it('does not announce "No results found" when Add filter is left after a query that listed suggestions', async () => {
setup({config: contentSearchConfig, filters: [openFilter]});
openSheet();
tapRow('Add filter');
const input = within(sheet()).getByRole('searchbox', {name: 'Search'});
fireEvent.change(input, {target: {value: 'status'}});
expect(
within(sheet()).getAllByRole('button', {name: /Status/}).length,
).toBeGreaterThan(0);
// Back to Filters: the Add filter sheet's body stays mounted, its
// results list emptied by the step change — no empty-result message is
// spoken, not on the page and not in the Filters dialog.
fireEvent.click(within(sheet()).getByRole('button', {name: 'Back'}));
expect(
within(sheet()).getByRole('heading', {name: 'Filters'}),
).toBeTruthy();
await new Promise(resolve => setTimeout(resolve, 40));
expect(pageRegionText()).toBe('');
expect(
sheet().querySelector(
':scope > [data-astryx-modal-live-region="polite"]',
)?.textContent ?? '',
).not.toContain('No results found');
// Escape out of the flow after another query: nothing on the page.
tapRow('Add filter');
fireEvent.change(
within(sheet()).getByRole('searchbox', {name: 'Search'}),
{target: {value: 'status'}},
);
fireEvent.keyDown(sheet(), {key: 'Escape'});
settleSheets();
await new Promise(resolve => setTimeout(resolve, 40));
expect(pageRegionText()).toBe('');
});

it('announces the unavailable state only while its step is open, and again when the same stale filter reopens', async () => {
const stale: PowerSearchConfig = {
...config,
fields: config.fields.map(f =>
f.key === 'status'
? {...f, operators: f.operators.filter(o => o.key !== 'is')}
: f,
),
};
const {rerender} = setup({filters: [openFilter, authorFilter]});
openEditFlow('Status is Open');
fireEvent.click(within(sheet()).getByRole('button', {name: 'Cancel'}));
expect(
within(sheet()).getByRole('heading', {name: 'Filters'}),
).toBeTruthy();
// The value step is closed but its body stays mounted with the draft; a
// config update that drops the edited operator must not be spoken — not
// now, and not held for the next dialog that opens.
rerender(
<MobilePowerSearch
config={stale}
filters={[openFilter, authorFilter]}
onChange={vi.fn()}
/>,
);
await new Promise(resolve => setTimeout(resolve, 40));
expect(pageRegionText()).toBe('');
// Opening another filter's step: words held for the closed step would
// surface in the value step's dialog here.
tapRow('Author is Ada');
await new Promise(resolve => setTimeout(resolve, 40));
for (const region of document.querySelectorAll('[aria-live]')) {
expect(region.textContent ?? '').not.toContain(
'This filter is no longer available',
);
}
fireEvent.click(within(sheet()).getByRole('button', {name: 'Cancel'}));
// Reopening the stale filter — its row now reads the field's label and
// the raw operator key — speaks it, in its own dialog, as a new
// occasion even though the filter is the same.
tapRow('Status is');
await waitFor(() =>
expect(sheetRegion().textContent).toBe(
'This filter is no longer available',
),
);
});

it('shows no "no match" message for a config with no field', () => {
setup({config: {name: 'Empty', fields: []}});
openSheet();
Expand Down
17 changes: 16 additions & 1 deletion packages/lab/src/MobilePowerSearch/MobilePowerSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -503,15 +503,27 @@ function SheetEmptyMessage({
id,
message,
occasion,
isShown,
}: {
id?: string;
message: string;
occasion: string;
/**
* Whether the message's step is the open one. A closed sheet keeps its body
* mounted, so the message can exist — and its occasion change — while no
* one is in the sheet: nothing is spoken then, and the occasion is
* forgotten so the same one is spoken again when the step reopens on it.
*/
isShown: boolean;
}) {
const ref = useRef<HTMLDivElement>(null);
const announce = useAnnounce();
const spokenOccasionRef = useRef<string | null>(null);
useEffect(() => {
if (!isShown) {
spokenOccasionRef.current = null;
return;
}
if (spokenOccasionRef.current === occasion) {
return;
}
Expand All @@ -520,7 +532,7 @@ function SheetEmptyMessage({
if (text) {
announce(text);
}
}, [announce, occasion]);
}, [announce, isShown, occasion]);
return (
<div ref={ref} id={id} {...stylex.props(styles.empty)}>
<Text type="supporting" color="secondary">
Expand Down Expand Up @@ -1994,6 +2006,7 @@ export function MobilePowerSearch({
id={contentResultsId}
message="No results found"
occasion={contentQuery}
isShown={step === 'fields'}
/>
) : (
// An empty query with no bootstrap suggestion: the results
Expand All @@ -2008,6 +2021,7 @@ export function MobilePowerSearch({
<SheetEmptyMessage
message="No filters match your search"
occasion={fieldQuery}
isShown={step === 'fields'}
/>
)
) : (
Expand Down Expand Up @@ -2144,6 +2158,7 @@ export function MobilePowerSearch({
<SheetEmptyMessage
message="This filter is no longer available"
occasion={`${draft.field}-${draft.operator ?? ''}`}
isShown={step === 'value'}
/>
)
)}
Expand Down
Loading