Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
b293652
fix(popover): a list opened by focus opens for a field focused as it …
davidnich Oct 7, 2026
cdaba6c
fix(rich-text-editor): a value is compared with the document as it is…
davidnich Oct 7, 2026
9e025c6
fix(popover): the focus check is made once per field, so Escape close…
davidnich Oct 7, 2026
c633b89
Merge develop (#696, 0.78.0) into bugfix/focus-popover-opens-when-foc…
davidnich Oct 7, 2026
27dad97
docs(components): the focus popover's attach-time check and the rich-…
davidnich Oct 7, 2026
eaff15b
fix(table): a table is scrolled sideways from its header too; dragToS…
davidnich Oct 7, 2026
58e8620
Merge #708 (bugfix/focus-popover-opens-when-focused) into bugfix/tabl…
davidnich Oct 7, 2026
c432b42
fix(table): Shift+wheel over the header scrolls the table sideways; a…
davidnich Oct 7, 2026
3d5c4a3
fix(styled): a component rendered `as` another receives none of reqor…
davidnich Oct 7, 2026
e0d3bb2
fix(description): a description takes block content
davidnich Oct 7, 2026
0669d75
perf(Tier): the description's style is one object, not a fresh one pe…
davidnich Oct 7, 2026
30a6e81
fix(table): a finger's swipe on the header is not ended by the cell i…
davidnich Oct 7, 2026
9180338
fix(Panel, Tier): the panel's media props stay back; a modern tier's …
davidnich Oct 7, 2026
c701745
fix(panel): floating actions stay while the pointer crosses to them —…
davidnich Oct 7, 2026
7f6bd6c
fix(styled): a styling prop that is also an HTML attribute name reach…
davidnich Oct 7, 2026
21ee8c2
Merge #710 and #713 (table header scroll, panel floating actions) int…
davidnich Oct 7, 2026
ae99313
fix: plain-text descriptions stay paragraphs; no group flag reaches t…
davidnich Oct 7, 2026
7c9c127
chore(release): one version for the consolidated PR: 0.78.5
davidnich Oct 7, 2026
94edea0
fix(popover): its controls answer for it as it is now, and Escape clo…
davidnich Oct 8, 2026
d877213
fix(menu): an item brought into view scrolls its menu, and nothing ar…
davidnich Oct 8, 2026
ba2c40f
fix(textarea): a field that offers templates lists them when it is co…
davidnich Oct 8, 2026
b827156
docs(components): Escape and the popover's controls, a menu's own scr…
davidnich Oct 8, 2026
be112f0
fix(table): a drag on a column's resize handle resizes the column and…
davidnich Oct 8, 2026
97f9adb
fix(textarea): text written without a key puts the template list away…
davidnich Oct 9, 2026
432b59a
fix(rich-text-editor): a chip in a text is not a tab stop of its own,…
davidnich Oct 9, 2026
f9512c6
fix(textarea): a click on a chip in a template field neither lists th…
davidnich Oct 12, 2026
f74b521
Merge develop (0.78.1, #714) into bugfix/focus-popover-opens-when-foc…
davidnich Oct 12, 2026
79d92e0
fix(gestures): a finger's drag keeps going when the element it landed…
davidnich Oct 12, 2026
6fa4909
fix(rows, panel): narrow, a row's actions stack inside it and a table…
davidnich Oct 11, 2026
4bde968
fix(panel): a narrow bar's title keeps its words whole while the acti…
davidnich Oct 11, 2026
a7790c8
fix(table, rows): narrow, a row's actions are one menu button that na…
davidnich Oct 12, 2026
2a70729
fix(breadcrumbs): what fits is measured again when a crumb changes wi…
davidnich Oct 12, 2026
4d3c7c3
fix(breadcrumbs): a crumb that grows before its first size report is …
davidnich Oct 12, 2026
e653852
docs(stories): the template-field UX stories say what they show (qoru…
davidnich Oct 12, 2026
5cc6405
fix(statistic): a label keeps its words whole, and a tile is as wide …
davidnich Oct 12, 2026
d60c131
fix(rows): the narrow-row stories draw the containers their rows fill…
davidnich Oct 12, 2026
99e2be1
test(stories): a simulated finger drags and focuses as a real one doe…
davidnich Oct 12, 2026
a4391bb
test(table): every table story loads without a console error (qorus#646)
davidnich Oct 12, 2026
99efcc9
fix(rows): a narrow severity row keeps its actions in sight, and a st…
davidnich Oct 12, 2026
99888c9
fix(table): a narrow table's search is a button until it is used, so …
davidnich Oct 12, 2026
086f921
test(table): Title Word Wider Than Its Space waits for the folded sea…
davidnich Oct 12, 2026
a60ab46
fix(breadcrumbs): the folded current page is named in full, however i…
davidnich Oct 12, 2026
d1c403a
test(stories): story files run one at a time, so a touch story's emul…
davidnich Oct 12, 2026
fe16b52
test(stories): a touch story runs in a page of its own, because Chrom…
davidnich Oct 12, 2026
1209c96
fix(breadcrumbs): what does not fit is folded before the browser draw…
davidnich Oct 12, 2026
7b0ee16
fix(breadcrumbs): a trail holding tabs folds crumbs before it squeeze…
davidnich Oct 12, 2026
95d0510
fix(popover): it repositions at the next frame, not inside its resize…
davidnich Oct 12, 2026
00504c5
fix(dropdown, stories): a dropdown keys the custom elements it is giv…
davidnich Oct 12, 2026
3f87c25
fix(table): a cell action's label is a string or a number, as a panel…
davidnich Oct 13, 2026
bb7157e
test(stories): the touch stories' browser is a phone from the start, …
davidnich Oct 13, 2026
142b42d
fix(menu): a plain DOM element in a menu is given none of the menu's …
davidnich Oct 13, 2026
b5ed2bb
fix(popover, textarea): Escape goes on unless it was the popover's; t…
Foxhoundn Oct 13, 2026
c5825e5
fix(dropdown, control-group): a custom element keeps its key and its …
Foxhoundn Oct 13, 2026
4bd51d3
fix(menu): overflow overlay counts as a scroller when an item is brou…
Foxhoundn Oct 13, 2026
34b6b08
fix(panel): a wide bar lays its title and actions out as before; a ti…
Foxhoundn Oct 13, 2026
3adab1f
fix(table, rows): a cell action's label is drawn again; the row actio…
Foxhoundn Oct 13, 2026
976d72a
fix(statistic): tiles narrow to their share of a row again, and a wor…
Foxhoundn Oct 13, 2026
5704a4b
fix(breadcrumbs): crumbs unfold when the bar itself makes the trail w…
Foxhoundn Oct 13, 2026
72c369c
test(stories): a story tagged touch can no longer run in neither project
Foxhoundn Oct 13, 2026
98ba321
docs(components): the review fixes of #708
Foxhoundn Oct 13, 2026
95ce3cc
fix(row-actions, stories): one style object for the filled group and …
Foxhoundn Oct 13, 2026
8abf239
docs(stories): the breadcrumbs stories say nothing of the IDE's pages
Foxhoundn Oct 13, 2026
026395c
fix(statistic): a label that wraps is centred, as its tile is
Foxhoundn Oct 13, 2026
f659e23
fix(modal): Escape closes a modal from the key or its confirmation, n…
davidnich Oct 13, 2026
acdbe7f
fix(data-view, timeline): a section or item tells the host of a click…
davidnich Oct 13, 2026
fcb181c
fix(tree): a new item's key and value fields hold text from the start…
davidnich Oct 13, 2026
1f3892f
test(stories): the control group stories' inputs can be typed into, a…
davidnich Oct 13, 2026
37ef79b
test(stories): no story makes React warn - every React warning fails …
davidnich Oct 13, 2026
8faff91
fix(dropdown): every row of the list has a key of its own (qorus#646)
davidnich Oct 13, 2026
47fde14
fix(control-group, menu): a key given to two children is made unique,…
Foxhoundn Oct 14, 2026
34553ea
test(stories): the narrow-row stories hand their memoized rows one ac…
davidnich Oct 14, 2026
9ef15cc
fix(drawer): a responsiveLayout drawer opened on a phone is a sheet t…
davidnich Oct 14, 2026
87239b9
fix(panel, popover): floating actions and popovers follow what they a…
davidnich Oct 14, 2026
d45a15b
fix(popover, panel, notification, rating, nav-rail, marquee): a tap o…
davidnich Oct 14, 2026
03ddb5d
fix(paging): an infinite collection never loads past its last page an…
Oct 14, 2026
d11a941
fix(control-group): a group that re-renders by itself keeps its first…
Oct 14, 2026
85f6099
feat(colors): text and icon colors are read against what they are dra…
Oct 14, 2026
3edf31d
fix: Reqore's text and icons stand out from what they are drawn on (q…
Oct 14, 2026
76de251
fix(stories): stories draw text only on colors it can be read on (qor…
Oct 14, 2026
98c46c3
test(stories): the story run checks the contrast of text and icons (q…
Oct 14, 2026
6e203e6
fix(hover): a pointer the event does not name counts as one that hovers
davidnich Oct 15, 2026
5960623
fix(button): an icon's color given in its props stands out at the opa…
Oct 15, 2026
b5f7a66
fix(deepscan): readable-color props are one object while what they ho…
davidnich Oct 15, 2026
de9b9cd
feat(motion): Reqore honours prefers-reduced-motion; the story run as…
Oct 15, 2026
2dcd352
fix(icons): an icon's color given in its props stands out where an in…
Oct 15, 2026
bd20c18
fix(link): a link's intent reads on the surface it is drawn on (qorus…
Oct 15, 2026
2b84953
fix(button): an active button draws what it holds on its fill (qorus#…
Oct 15, 2026
a7194b5
fix(entity-row): the description and metadata fade their text, not wh…
Oct 15, 2026
0fba3d8
fix(effect): a translucent gradient is read with the ring that shows …
Oct 15, 2026
7279d2e
chore: the contrast work moves to its own PR, so this PR's review sho…
Oct 15, 2026
26327a4
fix(popover, panel): what is a way in opens at a tap; hydration keeps…
Foxhoundn Oct 15, 2026
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
14 changes: 14 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,20 @@ release on npm.
- **Location:** `src/stories/` (e.g. `Collection.stories.tsx`).
- **Pattern:** ArgTypes for props, canvas controls, visual testing via Chromatic.
- **Command:** `yarn build-storybook` builds the static site.
- **Touch stories:** a story whose play calls `withTouchScreen` (`src/stories/utils/touchScreen.ts`)
carries `tags: ['touch']`. `yarn test:stories` runs those stories in their own Vitest project
(`storybook-touch`), whose browser context is a phone from the start (Playwright's `isMobile` and
`hasTouch`, 390×844), and before the others; an untagged one fails with the reason. Never turn
Chromium's touch emulation on or off over the DevTools protocol: turning it off leaves the page
with the machine's hover and pointer, not the mouse Playwright launched it with, and a Linux CI
runner has none, so every later story in the page finds `(hover: none)` (it never shows on a Mac,
which always reports a mouse). `.storybook/vitest.setup.ts` checks every story's browser is its
project's: `(hover: hover) and (pointer: fine)`, or on the phone `(hover: none) and (pointer: coarse)`.
- **No layout change inside a ResizeObserver callback:** a story fails when the browser reports
"ResizeObserver loop completed with undelivered notifications" (`.storybook/vitest.setup.ts`). It
means a resize callback changed the layout it observes in the same frame, often by setting state
synchronously (react-popper's `forceUpdate` renders with `flushSync`). Defer that change to the next
frame (`requestAnimationFrame`, once per frame), or measure without changing the layout.

### Verifying a visual fix locally with Qlip — reqore mechanics

Expand Down
19 changes: 19 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ module.exports = {
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', 'plugin:react/jsx-runtime', 'plugin:storybook/recommended'],
overrides: [
{
// Stories are not shipped; the helper wraps styled-components' own `styled`.
files: ['src/stories/**', 'src/helpers/styled.ts'],
rules: { 'no-restricted-imports': 'off' },
},
{
env: {
node: true,
Expand Down Expand Up @@ -37,6 +42,20 @@ module.exports = {
// Allow the "omit a key" idiom — `const { dropped, ...rest } = obj` — where
// the destructured sibling is intentionally discarded.
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }],
// Reqore's `styled` (src/helpers/styled.ts) keeps styling props that are also HTML attribute
// names (`size`, `disabled`, `checked`, ...) off the elements they are not attributes of.
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'styled-components',
importNames: ['default'],
message: "Import `styled` from 'src/helpers/styled' (it filters DOM props).",
},
],
},
],
},
// Ignore storybook files
ignorePatterns: ['**/stories/*', '**/mock/*'],
Expand Down
9 changes: 9 additions & 0 deletions .storybook/reactWarnings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright 2026 Qore Technologies, s.r.o.
import { watchReactWarnings } from '../src/stories/utils/reactWarningGate';

/**
* The console's React warnings, watched from before any of reqore's modules load: `vitest.setup.ts` imports this
* module first, so a warning a module gives as it loads (a library calling a deprecated React API at import) is kept
* too, and fails the story file's first story (`src/stories/utils/reactWarningGate.ts`).
*/
export const reactWarnings = watchReactWarnings();
102 changes: 101 additions & 1 deletion .storybook/vitest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
// first: it watches the console for React's warnings before any other module loads (`reactWarnings.ts`)
import { reactWarnings } from './reactWarnings';
import { setProjectAnnotations } from '@storybook/react-vite';
import { createElement } from 'react';
import { beforeAll } from 'vitest';
import { afterAll, afterEach, beforeAll, beforeEach, expect } from 'vitest';
import { server } from 'vitest/browser';
import * as previewAnnotations from './preview';
import { reactWarningsFailure, type IReactWarning } from '../src/stories/utils/reactWarningGate';
import { TOUCH_STORY_PROJECT } from '../src/stories/utils/touchScreen';

/** Whether this run's browser is the phone the stories tagged `touch` run on (vitest.config.ts). */
const onThePhone = (): boolean => {
// a project's browser runs as a project of its own, named after both:
// "storybook-touch (chromium)"
const project = server.config.name ?? '';

return project === TOUCH_STORY_PROJECT || project.startsWith(`${TOUCH_STORY_PROJECT} (`);
};

// Registers the React renderer (renderToCanvas) plus the project's global
// decorators/parameters from .storybook/preview for the Storybook Vitest project.
Expand All @@ -17,6 +31,21 @@ const annotations = setProjectAnnotations([
// the story test times out. A 100vh frame resolves regardless of parent.
(Story: any) => createElement('div', { style: { height: '100vh' } }, createElement(Story)),
],
/* A story tagged `touch` runs on the phone. The two story projects tell those stories from the
others by the tags Storybook reads from the story file before it runs it (vitest.config.ts);
a tag the story only has once it runs - spread into it from a shared object - is not one of
them, and the story would run here, on the desktop, with a mouse. It fails instead, saying
how to tag it. */
beforeEach: ({ tags, title, name }) => {
if (tags.includes('touch') && !onThePhone()) {
throw new Error(
`"${title} / ${name}" is tagged 'touch' only once it runs, so it ran in the ` +
`"${server.config.name}" project, whose browser has a mouse. Write the tag in the ` +
`story file, as a string in the story's or its meta's own \`tags\` ` +
`(tags: ['touch']), so that it runs on the phone.`
);
}
},
},
]);

Expand All @@ -31,3 +60,74 @@ style.textContent = `
}
`;
document.head.appendChild(style);

/* A story fails when a ResizeObserver callback changes the layout it observes in the same frame: the
browser leaves the change unreported until the next one and says so with an error event whose message
is "ResizeObserver loop completed with undelivered notifications" (qorus#646). Vitest only logs that
event (it carries no error object), so it is caught here. The listener goes on through EventTarget, not
`window.addEventListener`: Vitest counts the error listeners a page adds that way and stops reporting
any unhandled error once there is one. */
const resizeLoops: string[] = [];

EventTarget.prototype.addEventListener.call(window, 'error', (event: Event) => {
const { message } = event as ErrorEvent;

if (typeof message === 'string' && message.includes('ResizeObserver loop')) {
resizeLoops.push(message);
}
});

beforeEach(() => {
resizeLoops.length = 0;
});

afterEach(async () => {
// the browser reports it after the frame's layout: let the frames after the story's last change pass
await new Promise<void>((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
);
expect(
resizeLoops,
'a ResizeObserver callback changed the layout it observes in the same frame - defer that change to the next frame'
).toEqual([]);
});

/* Each story's browser is the one its project promises (vitest.config.ts): a desktop with a mouse, or, for
the stories tagged `touch`, a phone. The phone's stories run first, so every desktop story checks, after
them, that it still has its mouse: a touch screen that left a desktop page without one (qorus#646) fails
the next story here. */
beforeEach(() => {
const project = server.config.name ?? '';
const promised = onThePhone()
? '(hover: none) and (pointer: coarse)'
: '(hover: hover) and (pointer: fine)';

expect(matchMedia(promised).matches, `the "${project}" project's browser matches ${promised}`).toBe(true);
});

// No story makes React warn (`src/stories/utils/reactWarningGate.ts`): each warning fails the story it was given in.
// One given before the story started - as its file loaded, or as the story before it unmounted - fails it too, saying
// so; one given after a file's last story fails the file.
let reactWarningsBeforeStory: IReactWarning[] = [];

beforeEach(() => {
reactWarningsBeforeStory = reactWarnings.take();
});

afterEach(() => {
const failure = reactWarningsFailure({
before: reactWarningsBeforeStory,
during: reactWarnings.take(),
});
reactWarningsBeforeStory = [];
if (failure) {
throw failure;
}
});

afterAll(() => {
const failure = reactWarningsFailure({ after: reactWarnings.take() });
if (failure) {
throw failure;
}
});
102 changes: 101 additions & 1 deletion __tests__/EntityRow.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fireEvent, render } from '@testing-library/react';
import { cleanup, fireEvent, render } from '@testing-library/react';
import {
ReqoreContent,
ReqoreEntityRow,
Expand Down Expand Up @@ -609,3 +609,103 @@ test('Renders without a ResizeObserver implementation', () => {
globalThis.ResizeObserver = original;
}
});

/*
* Narrow, two or more actions are one menu button (qorus#646): the label keeps the row's width.
*/

const menuButtons = () => document.querySelectorAll('.reqore-row-actions-menu-button');

test('Folds two or more actions into one menu button in a narrow container', () => {
withResizeObserver(NarrowResizeObserver, () => {
NarrowResizeObserver.width = 400;
renderRowWithActions({ actions: [{ label: 'Run' }, { icon: 'DeleteBinLine', tooltip: 'Remove' }] });

const row = document.querySelector('.reqore-entity-row')!;
expect(row.getAttribute('data-actions')).toBe('menu');
expect(menuButtons()).toHaveLength(1);
expect(menuButtons()[0].getAttribute('aria-label')).toBe('Actions');
expect(row.querySelectorAll('.reqore-entity-row-actions .reqore-button')).toHaveLength(1);
});
});

test('Names the menu button with actionsMenuLabel', () => {
withResizeObserver(NarrowResizeObserver, () => {
NarrowResizeObserver.width = 400;
renderRowWithActions({
actionsMenuLabel: 'Akce',
actions: [{ label: 'Run' }, { label: 'Stop' }],
});

expect(menuButtons()[0].getAttribute('aria-label')).toBe('Akce');
});
});

test('Keeps the actions in sight, stacked, with narrowActions="stack"', () => {
withResizeObserver(NarrowResizeObserver, () => {
NarrowResizeObserver.width = 400;
renderRowWithActions({ narrowActions: 'stack', actions: [{ label: 'Run' }, { label: 'Stop' }] });

const row = document.querySelector('.reqore-entity-row')!;
expect(row.getAttribute('data-narrow')).toBe('true');
expect(row.getAttribute('data-actions')).toBeNull();
expect(menuButtons()).toHaveLength(0);
expect(row.querySelectorAll('.reqore-entity-row-actions .reqore-button')).toHaveLength(2);
});
});

test('Stacked, labels an icon-only action that is a menu with its tooltip', () => {
withResizeObserver(NarrowResizeObserver, () => {
NarrowResizeObserver.width = 400;
renderRowWithActions({
narrowActions: 'stack',
actions: [
{ label: 'Run' },
{ icon: 'More2Line', tooltip: 'More actions', actions: [{ label: 'Show in chat' }] },
],
});

const [, more] = Array.from(
document.querySelector('.reqore-entity-row-actions')!.children
) as HTMLElement[];
expect(more.textContent).toContain('More actions');
});
});

test('Keeps a single action a button when narrow, and every action inline when wide', () => {
withResizeObserver(NarrowResizeObserver, () => {
NarrowResizeObserver.width = 400;
renderRowWithActions({ actions: [{ label: 'Run' }] });
expect(menuButtons()).toHaveLength(0);
cleanup();

NarrowResizeObserver.width = 900;
renderRowWithActions({ actions: [{ label: 'Run' }, { label: 'Stop' }] });
expect(menuButtons()).toHaveLength(0);
expect(document.querySelectorAll('.reqore-entity-row-actions .reqore-button')).toHaveLength(2);
});
});

test('Stacked, labels an action whose label is empty with its tooltip, a menu too', () => {
withResizeObserver(NarrowResizeObserver, () => {
NarrowResizeObserver.width = 400;
renderRowWithActions({
narrowActions: 'stack',
actions: [
{ label: '', icon: 'DeleteBinLine', tooltip: 'Remove' },
{
label: '',
icon: 'More2Line',
tooltip: 'More actions',
actions: [{ label: 'Show in chat' }],
},
],
});

const [remove, more] = Array.from(
document.querySelector('.reqore-entity-row-actions')!.children
) as HTMLElement[];
expect(remove.textContent).toContain('Remove');
expect(more.textContent).toContain('More actions');
});
});
28 changes: 28 additions & 0 deletions __tests__/NavRail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,34 @@ describe("showLabels='hover'", () => {
expect(dashboard().textContent).toBe('');
});

test("a finger's tap dwells no labels open: the mouseenter emulated after it is no hover", () => {
renderRail(<ReqoreNavRail items={ITEMS} defaultActiveId='dashboard' showLabels='hover' />);
const nav = screen.getByRole('navigation');

fireEvent.pointerEnter(nav, { pointerType: 'touch', buttons: 1 });
fireEvent.pointerDown(nav, { pointerType: 'touch', buttons: 1 });
fireEvent.pointerUp(nav, { pointerType: 'touch', buttons: 0 });
fireEvent.mouseEnter(nav);
act(() => {
vi.advanceTimersByTime(5000);
});

expect(dashboard().textContent).toBe('');
});

test('a mouse dwells them open, its pointer event before its mouseenter', () => {
renderRail(<ReqoreNavRail items={ITEMS} defaultActiveId='dashboard' showLabels='hover' />);
const nav = screen.getByRole('navigation');

fireEvent.pointerEnter(nav, { pointerType: 'mouse', buttons: 0 });
fireEvent.mouseEnter(nav);
act(() => {
vi.advanceTimersByTime(1500);
});

expect(dashboard()).toHaveTextContent('Dashboard');
});

test('showLabelsDelay sets the dwell', () => {
renderRail(
<ReqoreNavRail
Expand Down
41 changes: 40 additions & 1 deletion __tests__/Rating.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fireEvent, render } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
import { useState } from 'react';
import {
ReqoreContent,
ReqoreLayoutContent,
Expand Down Expand Up @@ -232,3 +233,41 @@ test('Renders with custom icons', () => {
expect(document.querySelectorAll('.reqore-rating-item').length).toBe(5);
expect(document.querySelectorAll('.reqore-icon').length).toBe(5);
});

/** A rating that holds the value it is given, and says what it shows. */
const HeldRating = () => {
const [value, setValue] = useState(3);
return (
<ReqoreUIProvider>
<ReqoreLayoutContent>
<ReqoreContent>
<ReqoreRating value={value} onChange={setValue} allowClear showRatingValue />
</ReqoreContent>
</ReqoreLayoutContent>
</ReqoreUIProvider>
);
};

test("A finger's tap previews nothing: a value the tap cleared reads as cleared", () => {
render(<HeldRating />);
const third = document.querySelectorAll('.reqore-rating-item')[2];

// a tap, as a browser sends it: the finger's pointer events, then the mouse events it emulates
fireEvent.pointerOver(third, { pointerType: 'touch', buttons: 1 });
fireEvent.pointerDown(third, { pointerType: 'touch', buttons: 1 });
fireEvent.pointerUp(third, { pointerType: 'touch', buttons: 0 });
fireEvent.mouseMove(third);
fireEvent.click(third);

expect(screen.getByText('0 / 5')).toBeTruthy();
});

test('A mouse resting on a star previews it', () => {
render(<HeldRating />);
const second = document.querySelectorAll('.reqore-rating-item')[1];

fireEvent.pointerMove(second, { pointerType: 'mouse', buttons: 0 });
fireEvent.mouseMove(second);

expect(screen.getByText('2 / 5')).toBeTruthy();
});
Loading
Loading