Repository navigation
Conversation
…wn on (qorus#646) David's review found black text on a dark gray background: the muted intent (#f2f2f230) picked its text from the near-white it is made of, but is drawn at 19% over a dark page. Text and icon colors are now picked against what is actually drawn, to WCAG AA: 4.5:1 for text, 3:1 for icons and large text. - getReadableColorFrom / getReadableColor choose the candidate with the higher contrast (not a luminance threshold), draw a translucent background over what is under it (the theme it was made from), and give up the dimmed #f2f2f2 / #0d0d0d for pure white / black where the dimmed one is under 4.5:1 (#f2f2f2 on the info blue is 4.2:1). - New: getDrawnColor, getContrastRatio (unrounded: polished rounds 4.495 up to 4.5), getReadableColorOn (one color for every color of a gradient), getReadableAlpha / getReadableFadedColor (fade only as far as it reads), getReadableAccentColor(On) (keep the hue, move toward the side that can read on every background), getReadableIntentColor, getReadableThemeColor, getReadableIconLook, getReadableTextOn, getReadableGroupOpacity (how far a group drawn as a whole can fade and every mark in it still read), getThemeSurface(s), fadeColor (a translucent color at a panel's opacity stays translucent; rgba() replaced its alpha). - A one-color gradient (buildGradientColorsFromString) keeps its text readable at both ends, and a step lighter as it is drawn hovered or pressed: info's light end was 3.25:1 with white. - ReqoreSurfaceProvider / useReqoreSurface (exported, with TReqoreSurface): what a component paints - one color, or each color of its gradient - is handed to what it holds, which reads against it rather than the page. An app can tell Reqore about a background of its own the same way. Tests that fail before: __tests__/helpers/readableColors.test.tsx (the helpers missing, black picked for the muted intent, #f2f2f2 kept on the info blue, info's gradient 3.86:1 a step lighter) and __tests__/surfaceContext.test.tsx.
…orus#646) Each component picks its text and icon colors against the background it actually draws (a minimal wash, a translucent intent, a gradient, a lightened header), hands that background to what it holds, and fades secondary text and icons only as far as they stay readable (4.5:1 text, 3:1 icons and large text; WCAG 1.4.3 / 1.4.11). - Button: text, description and icons against its drawn fill at rest, hovered and active; a minimal or transparent button keeps its color in a readable shade; badges read on the button, every color of it. - Tag: solid, minimal, soft and text tags, against every color they are drawn on; the key is darkened as far as its text needs (over a light color 30% black left it in the mid-tones, where nothing reads); icons and actions; label and key effects read on the tag. - Input / Textarea / RichTextEditor: text, placeholder, icons and the clear button against the field's own wash. - Span / Paragraph / Heading / Icon: an intent, or a theme color named by name, in a shade of it that reads where it is drawn; text drawn in a gradient is drawn in colors that read, hovered and pressed too; a text glow's letters sit in a halo of the glow and are drawn in the color that reads on it (white letters in a white halo inside a red glow were 3.85:1). - An effect with several gradients is read as the layers they are drawn as, each over the ones under it. - Panel (content, the title bar over the content's gradient, description, title icon, a title rendered as a link), Message, notifications, Callout, EntityRow, Statistic, Testimonial, FeatureCard, Accordion, Timeline, Table cells, NavRail, Breadcrumbs (the separator too), SegmentedControl, CollapsibleContent's reveal button, Rating, Tree, StackedBar, DescriptionList, Bubble timestamps: each reads against its own surface; one with a gradient hands every color of it to what it holds. A ControlGroup or a Menu drawn in a gradient does too, and a menu divider's label reads on the menu. - Checkbox: the box against every color under it; a switch's word on the knob reads on the knob and the other on the track (one color for both was white on the light info knob, 2.56:1), a gradient track and a knob in the effect's colors are read as drawn, and an unset knob is drawn faint without fading its icon, which still stands out by 3:1. - Notifications: the text, icon, close button and pending spinner read on the notification as drawn over every color under it; content of the caller's own (a tag) is no longer faded with the text under a title; an iconColor is drawn in a shade of it that stands out from its tile. - NavRail: an idle rail rests at idleOpacity, or as little less faded as its icons need to stand out by 3:1 - the marks' and the active mark's on its fill, faded into the page with the rail (it rested at 0.34: 2.3:1). - A translucent theme drawn at a panel's opacity stays translucent (the muted EmptyState was white on white). - Breadcrumbs no longer force the bar's text color onto a crumb with its own intent (dark text on the warning crumb). - StackedBar writes each segment in what reads on it (white on a light segment was 1.9:1). - Disabled components that are not form controls say so (aria-disabled): Tag, Callout, Panel, Accordion item, EntityRow, Statistic, CollapsibleContent, the Checkbox row, Progress, SeverityRow, StackedBar, Testimonial, the Textarea wrapper, a disabled row's table cells. WCAG exempts disabled controls from contrast; assistive technology is told. Tests that fail before: __tests__/componentSurfaces.test.tsx (a group or a menu drawn in a gradient hands its colors on), __tests__/tagColors.test.tsx (a tag's key darkened as far as it takes, a tag reads on every color it is drawn on). The story run's contrast gate (the last commit) failed 524 of 1186 story tests before these and the story colors, and none after.
…us#646) Some stories put text or icons on colors no text color reads on: a cyan-to-pink badge gradient, a purple-to-teal item, purple-to-pink groups and menus, a pink message, an orange badge under a darkened key, a light green backdrop blob, a magenta sphere, dark icon colors on a dark panel, a red spinner and a red bold leaf on dark gray, a muted wash on a mid-blue picker. Each keeps the same kind of color in a shade the text reads on, so the demos show readable choices. The notification stories' busy backdrop tells the notifications on it what it is drawn in (ReqoreSurfaceProvider), as an app would for a background of its own, and its row labels are a shade less faint.
…orus#646) After each story settles (its transitions and finite animations finished), in its theme: - text by axe-core's color-contrast rule (4.5:1, 3:1 when large); - text on a gradient, which axe cannot decide, against the gradient's colors where the text is drawn on it, and text drawn in a gradient (background-clip: text) in each of its colors; - icons (an svg in .reqore-icon) at 3:1 against what they are drawn on (WCAG 1.4.11), which axe does not check. What a mark is drawn on is composed as the browser draws it: each ancestor's paint over what is under it (a translucent color blended, a gradient as its colors along the part the mark covers, a fill over a ring counting alone), an element at an opacity drawn as a whole - its paint and everything in it - then faded into what is under it, and a floating layer (a popover, a modal) over the app layout's surface. Disabled content is exempt (WCAG), as is anything not drawn and decoration the page marks as such (data-decorative). Fail-first: the first gated run failed 524 of 1186 story tests in 62 of 75 story files (Button 22/25, Tag 18/22, Table 49/71, Panel 32/59, ...); with the previous commits every story passes. Unit tests: __tests__/contrastGate.test.ts (the gradient position, faded group and settling cases fail on the gate before them). axe-core 4.10.2 is a dev dependency. src/components/COMPONENTS.md has a section on how Reqore picks text and icon colors now: against what is drawn, the surface handed down (ReqoreSurfaceProvider), the floors on fades, what stays the caller's.
…city the button draws it at (qorus#646)
A button draws its icons at CONTROL_ICON_OPACITY and picks an icon's own
color (leftIconColor, iconColor) in the shade and at the opacity that
stand out by 3:1. A color given in the icon's own props instead -
leftIconProps={{ intent: 'warning' }}, as the Qorus IDE's readiness rows
give it - was spread over that look: the icon made it readable at full
opacity and the button drew it at 70%, a warning icon at 2.4:1 (814
findings in the IDE's contrast gate).
The button now takes the intent or color in leftIconProps /
rightIconProps as the icon's own color (they still win over leftIconColor,
as they did when spread last) and picks it as it does leftIconColor; the
rest of the props are passed on as before.
Test (fails before): an icon colored through its props is drawn exactly
like one colored through leftIconColor, left and right; that look stands
out by 3:1 at rest and hovered, on the dark theme and a light one; the
other icon props (its size) still reach it.
…ld holds (qorus#646) The contrast work (85f6099..98c46c3) computes each faded description, label and icon opacity in the render and hands it to a memo component (ReqoreIcon, ReqoreP, ReqoreSpan, ReqoreButton) as a new object every time, and DeepScan reported 21 REACT_INEFFICIENT_PURE_COMPONENT_PROP and one INSUFFICIENT_NULL_CHECK as new on #708 (FAILURE at 98c46c3, 6e203e6 and 5960623). Each such effect, style and handler is memoized on what it is made of: Button, Input and Tag's icon effects; Checkbox's knob and box icons; Callout, EntityRow and FeatureCard's descriptions (and EntityRow's metadata, Callout's close handler); RichTextEditor's placeholder and paragraph styles; SegmentedControl's item refs (one callback per item); Statistic's label; Timeline's timestamp and content styles. Panel reads the description effect's color and opacity null-safely, as it reads its textSize below.
…ks for it (qorus#646) A user who asks for reduced motion (WCAG 2.3.3) still got every spring Reqore runs - an input's clear button fading in, a drawer sliding, a reveal - and every CSS transition. A contrast check measured them on their way: the clear button, which rests at 4.8:1, at 1.4-2.6:1 in the IDE's gate (a react-spring fade, which no gate can wait on). - ReqoreUIProvider turns react-spring's skipAnimation on while the user prefers reduced motion (set as it renders, so the first render's springs settle too, and again when the preference changes; it turns off only what it turned on): every spring settles at once, at its end. - Its global style takes every CSS transition inside the layout and the portal to 0.01ms under the preference (not none: a transition of no time sends no transitionend, which some components wait for). Keyframe animations (a spinner, a pulse) are as they were. - The story run's contrast gate asks for reduced motion while it measures (a browser command, emulateReducedMotion), so it measures what stays on screen, never a frame of a fade. A story tagged `reduced-motion` runs under it; the others run with the motion a user sees, as their checks of a marquee, a count-up or a sheet expect. Tests (fail before): under reduced motion an input's clear button is at its settled opacity on first paint and Reqore's springs skip; the story Form/Input "Transitions Under Reduced Motion" finds the clear button's transition at 0.01ms (0.1s without the preference).
…put, a tag or a panel draws it faded (qorus#646) As with the button (5960623): an input's icons, a tag's and a panel's title icon are drawn at CONTROL_ICON_OPACITY, and the component picks an icon's own color and that opacity together so that it stands out by 3:1. A color given in the icon's props - leftIconProps={{ intent }}, iconProps={{ color }} - was spread over the pick: the icon made it readable at full opacity, and drawn at 70% it fell short (a collection's filter icon given leftIconProps={{ intent: 'muted' }} at 2.2:1, the IDE's gate). Each now takes it as the icon's own color (it still wins over the component's own, as it did when spread last) and passes the rest on. The split is one helper, splitIconColor (helpers/iconProps), the button uses it too. Test (fails before): an input's left and right icon, a tag's and a panel's title icon colored through their props are drawn exactly like ones colored through the component's own prop.
…#646) A link with an intent was written in the shade of it that reads on the theme's page, wherever it was: on a table cell tinted with the warning intent 3.5:1, over an entity row's gradient 2.8:1 (the IDE's gate). It now reads the surface a component around it hands down, as a span does. Test (fails before): an info link reads 4.5:1 on a warning-tinted cell and on every color of a gradient, in a shade of blue.
…646) An active button (a selected menu item, a sidebar section on the active path) is filled with its color, but its description, its badges and its icons were picked for the button at rest - its color's wash over the page: a light blue description 2.0:1 and badge 2.7:1 on the info blue of the IDE's account menu, a grey description 4.4:1 on a sidebar's section toggle. The description is now picked on the active fill, the button hands that fill to what it holds, and its icons are picked on it. The nav rail reads its active mark as the button now draws it (its icon on the fill, in the rail's activeEffect), so it still rests only as faded as that icon stands out by 3:1. Tests (fail before): an active button with a color writes its description and draws its badge on its fill; one without a color writes its description on its fill; at rest the description reads as before. The NavRail idle stories fail with the old rail and this button.
…at they hold (qorus#646) An entity row faded its description and metadata as a whole, so what they hold with a color of its own was faded a second time: a minimal warning tag in the metadata at 4.0:1, a link in the description at 2.8:1 (the IDE's gate). Their text is now written in the faded color (the readable floor as before; an opacity a caller gives is still that floor, a color a caller gives is the caller's), and a tag or a link in them is drawn as it is. Test (fails before): both slots are drawn whole in a faded color, a tag and a link in them are not faded, and a caller's opacity is kept.
…through it (qorus#646) A gradient effect paints its fill over a lighter ring of the same gradient (border-box). With an opaque fill the ring is hidden; with a translucent one - a glow of 'info:lighten:1:0.5' over the page - it shows through, but the colors a component read its text against left it out: a statistic's label faded for the blue over the page read 3.6:1 on what was painted (the IDE's support hero). The effect's own readable colors and getEffectBackgrounds now draw the fill over the ring (one fill layer stop by stop, as they share the gradient's geometry). Test (fails before): a statistic's label stands out by 4.5:1 from the corner of a translucent radial glow as it is painted, ring included.
|
CI note: this PR's base is #708's branch ( Checked locally at aeb3d8c, a tree byte-identical to #708's 0fba3d8, which CI built as
DeepScan has run here: 0 new issues. |
…ch child once (qorus#646) A responsive group folds its children into a `…` menu one at a time while its row overflows. Three things were wrong with how it counted: - Nothing stopped the count once every child was in the menu. A group whose overflow button alone is wider than its room (a labelled or large button, a narrow container) kept counting. Each step re-rendered the same row and measured it again, forever. In a browser that is a render loop at frame rate; under act it never returns. The Qorus IDE's agent found the unbounded counter while chasing a frozen story. - The row sliced the raw `children` array, but the menu took from React.Children.toArray(children), which leaves out the holes a caller leaves (null, false: an action it decided not to render). With a hole in front, a child was drawn in both the row and the menu. - A group with one child, which is not an array, threw at its first fold (`children.slice` is not a function). The row, the menu and the count now read one list, `foldable`: the children as React renders them, holes left out. The count stops when the row fits or when nothing is left in the row to fold. That bound is the end of the list, not an iteration cap. The old comment explained why the count could not be bounded by the number of children: the row and the count read different lists. They no longer do. Tests: controlGroupFoldIsBounded. Three cases fail before the fix: the group measures itself past the test's limit, `children.slice is not a function`, and a child is in both the row and the menu. A fourth case, the row still stops as soon as it fits, passes both before and after.
|
Heads-up @davidnich: I'm fixing the contrast findings from my review of #708 here. Please hold pushes until I comment that it's in. First I'll merge #708's new commit (26327a4, the touch fixes) into this branch. Then:
|
…focused' into bugfix/contrast-readable-text-and-icons
…rt hex as CSS reads it, and results are cached (qorus#646) Review of the contrast pass found these in the color helpers: - A color the helpers cannot parse (a CSS variable, `currentColor`, a name, a typo, `#f00a` before this fix) threw. It is now returned as it is given: `getContrastRatio` is NaN for it, a surface it names is left out, and a wash of it is mixed by the browser (`getColorAtAlpha` writes `color-mix(...)` for it). `isReadableColor` tells whether the helpers can read a color. - A short hex was repeated instead of doubled (`#fdc` read as `#fdcfdc`). polished writes colors short where it can (`#bbaaff` as `#baf`), so a minimal button's text came out green instead of the lavender it was picked as. Every helper now reads and writes the long form. - `getContrastRatio` is unrounded (polished rounds luminance to three places: `#595959` on black was 3.0, it is 2.998). - An explicit hex icon color is drawn as given, as a standalone ReqoreIcon draws it and as COMPONENTS.md says; only a theme color (an intent, `main`, a shade of one) is moved to a readable shade (`getReadableIconLook` -> `getReadableThemeColor`). - A caller's own `ifLight` / `ifDark` pair stays theirs when neither reaches 4.5:1: the better of the two is returned. - Each pure helper keeps its last 2000 results (`createColorCache`): every Button, Tag, Effect and Input re-render ran the color maths again. - `ReqoreSurfaceProvider` keeps its value while its colors are the same, so an inline effect does not re-render everything below it. Tests: helpers/colorParsing, helpers/explicitColors, surfaceProviderIdentity. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, colors are worked out once per render, and reduced motion stays inside Reqore (qorus#646)
The rest of the review of the contrast pass:
- Crashes: `descriptionEffect.color: 'red'`, a CSS variable or an intent
named where a hex was expected no longer throw (entity row, callout,
feature card, panel, statistic).
- Explicit values are the caller's: a nav rail's `idleOpacity`, an
explicit `descriptionEffect` / `labelEffect` / `metadataEffect`
`opacity`, a hex icon color on a button, a tag or an input. The
floors apply to the defaults only.
- Own fills and surfaces:
- `effect.color` on a button or a tag is read on its own fill, at
rest and hovered; a soft or text tag reads on every stop of a
gradient it sits on;
- a transparent or minimal element with its own `customTheme` reads
on that color; a callout's own text, a transparent statistic or
testimonial, and an entity row's actions read on their own surface;
- a popover's content and a drawer read on their own fill, not on
whatever opened them;
- a nav rail's active group hands its gradient (or tint) to its marks:
a resting section mark picked its text for the rail around the
group, and only read on the Effects story because `#baf` was misread
as a light green;
- `theme.breadcrumbs.item.color` reaches the crumbs; description list,
rich-text placeholder, stacked bar, timeline, message, tree and the
segmented control's More button read on what they are drawn on.
- `aria-disabled`: a component that is not disabled keeps the one its
caller gives.
- Render cost: Button and Tag work their colors out once per render
(`getButtonPaint`, `getTagPaint`) instead of in each style rule, Tag
is a `memo` component, Effect remembers its gradient pass, Span, P and
Link memoise their color, Input and Textarea use `getInputLook`. A
hovered button's text color declaration lost its semicolon and was
dropped with button animations on.
- Reduced motion reaches Reqore's own springs and `reqore-` elements
only; the app's own react-spring animations and transitions are left
alone (no `Globals.assign`).
COMPONENTS.md describes each. Tests: ariaDisabledPassThrough,
breadcrumbsColors, colorTolerance, controlOwnColors,
descriptionColorTolerance, explicitOpacityAndAria, fieldColors,
messageColors, navRailActiveGroupSurface, notificationColors,
portalSurfaces, reducedMotionSprings, stackedBarColors, timelineColors,
unpaintedSurfaceText, and updates to DescriptionList, reducedMotion and
segmentedControl.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ere they are drawn (qorus#646) A hex icon color and an explicit opacity are now drawn as given, so the stories that passed values which only read after Reqore changed them fail the contrast gate. Each keeps the same kind of value in one that reads: - Button: the minimal button's left and right icons in a light purple and blue (they were near-black on the dark washes); the described button's icon in `success:lighten:2`, a theme color, since one hex cannot read on both the white and the dark fills the template is drawn in. - Tag and Input: a lighter pink and purple. - EntityRow and Testimonial With Effects: metadata and role at 0.7 opacity (0.6 read at 4.18:1 and 4.13:1). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d-icons' into bugfix/contrast-readable-text-and-icons Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
It's in, at 473ada3. @davidnich, you can push again. The fixes listed in my last comment are in three commits, on top of a merge of #708 (26327a4, the touch fixes) and of your 2f2bbcf:
Two things you didn't see in my last comment
Checks at 473ada3: lint and Left open: the version. These are changed defaults (explicit values drawn as given), so it's your call, as on #708. |
…w holds (qorus#646) DeepScan's REACT_INEFFICIENT_PURE_COMPONENT_PROP, 13 new on 473ada3: - ReqoreTag is a memo component now, so every new object it is handed is flagged: DataView's bold and type-label effects are module constants, and the dropdown's trail of levels takes its click handlers from a `useCallback` and a `useMemo`'d list instead of new arrows. - The segmented control's More button had the same three issues before (its props, its menu and the menu items' click handlers); its tap and surface changes moved their lines, so DeepScan counted them as new. They are memoised now. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
qlip visual regression🔍 183 changed, 33 new of 1224 snapshots. |
…s named for its row (qorus#646)
When a narrow EntityRow or SeverityRow folds its actions into one menu, the
action's own aria-label was not passed on to its menu item. reqraft's
experiment card has "Show analytics for Short copy" on a button; folded, the
item was called only "Show analytics", the same as the item in every other
row's menu. Every menu button was also called "Actions", so a list of
versions read as a list of identical buttons.
- An item now keeps its action's own aria-label as its accessible name. Its
words still come from its label, tooltip or aria-label, as before.
- The menu button, and the menu it opens, are named for their row when the
row's label is text: "<actionsMenuLabel>: <row label>", for example
"Actions: Short copy" (rowLabel on ReqoreRowActionsMenu, rowLabelText).
A row whose label is an element keeps the plain "Actions". A colon rather
than "for", so a host's own word ("Akce") reads right in its language.
- Applies to ReqoreEntityRow and ReqoreSeverityRow. A table's row menu is
unchanged, since its rows have no label of their own.
Tests: rowActionsMenu, EntityRow and SeverityRow. Eight cases fail before
the fix: items keep their names, the button and menu are named for the row
(also with a host word), a blank row label names nothing, rowLabelText, and
the rows' folded menus. The EntityRow and SeverityRow narrow-menu stories
check the row-named button. COMPONENTS.md documents both.
…over takes a click at once (qorus#646) useCombinedRefs handed a component's element to the refs it was given in an effect, after the browser had painted it. A popover around a Reqore button or a dropdown learnt of its trigger only then, and armed its click listener in the render after that. So a trigger on screen opened nothing when it was clicked straight away, and nothing on the page said when it would. The Qorus IDE's story harness kept sleeps for it (a dropdown's .template-selector, among others). The effect also ran after every render, since its list of refs is a new array each time. useCombinedRefs now returns setRef, a ref callback. React calls it in the commit that attaches the element, so every ref holds the element before the paint. It changes only when a ref given changes, and then React hands the element over as it does for any new ref callback. Button, Input, Textarea, Tabs item, FadeScroller, Table body and header, Panel, Menu and Menu item draw their element with it. The four that draw through a ref callback of their own (Popover, Panel, Menu, Menu item) call it from a callback that is one function for the component's life: a new callback each render would detach and attach the element every time. The unused `_innerRef` state, set again on every render, is gone. Tests: popoverArmedWhenDrawn. The page is drawn and committed with flushSync and nothing more, which is the state of a page as it is first painted. The trigger is then clicked at once. A dropdown's popover opens; it stayed shut before the fix. A popover around a Reqore button opens too, which already held before the fix.
|
Pushed
|
…e end of the label's line (qorus#646)
In the opt-in 'menu' mode, two or more actions fold into one menu button at
the end of the label's line when the row is narrow. A row of that mode with
one action had no menu to fold into, so it wrapped the action under the
label: a line of its own for one button, where a row with more had none.
Now that one action stays a button at the end of the label's line, where the
menu would be. The row says so: `data-actions='end'`. Only 'menu' rows
change. The default ('row', visible buttons under the label) and 'stack' are
as they were (David, qorus#646).
Stories, both failing before (the action was under the label):
- Entity Row > Narrow Container Lone Action In Menu Mode (a 400px container);
- Entity Row > Narrow Container Lone Action In Menu Mode On A Phone: a
finger's size, and a tap runs the action without opening the row.
Narrow Container Actions Menu (two or more) is unchanged. The prop's docs,
TReqoreNarrowRowActions and COMPONENTS.md say it.
…om its render (qorus#646)
The date picker read its value, and its minValue and maxValue, with
parseAbsoluteToLocal, which throws for anything but an ISO date-time with a
zone. Any other string threw from the render, and so did an invalid Date
(through toISOString). The Qorus IDE's Workflow Orders page went to its
router's error page for "2026-06-02T08:00:00".
The rule, in one reader (readDate) that never throws:
- A date-time with a zone or an offset is that instant, in the user's
zone, as before.
- A date-time without one is that wall-clock time in the user's zone: ISO
8601 reads a date-time with no offset as local time.
- A date alone is that day at midnight, local.
- Anything else, text that is no date or an invalid Date, is no date. The
field is empty, and says it was given a value it cannot read the way it
says a value out of its bounds: it is invalid (aria-invalid,
data-invalid), drawn in the danger intent.
- A bound it cannot read is no bound. The year and month dropdowns fall
back to their default range.
Other inputs that read a value while rendering, checked:
- DataView's scalar threw for an invalid Date (toISOString); it now shows
it as it is ("Invalid Date").
- TimeAgo already handled an unreadable time.
- There is no other date, time or date-range input, and the number inputs
parse nothing while rendering.
Test: datePickerUnreadableValues, 7 cases, all failing before:
- a date-time with no zone, and a date alone, shown as local time;
- text and an invalid Date: empty and invalid;
- unreadable bounds, and an invalid Date as a bound;
- DataView with an invalid Date.
…ot read (qorus#646) Form/DatePicker > Values Without A Zone Or Unreadable renders three pickers: - "2026-06-02T08:00:00" shows 08:00, local; - "2026-10-17" shows that day at midnight; - text that is no date is an empty field, drawn as invalid. Before a6def18 the story threw from its render: "Invalid ISO 8601 date time string: 2026-06-02T08:00:00".
…, and a pinned column's header hides what scrolls under it (qorus#646)
Scrolled, a table's pinned columns were drawn over its sticky header: the
label was cut off and the menu button covered, and only the middle showed
the header. The panel's sticky header and a pinned cell were both at
z-index 2 in one stacking context, and the cells come later in the
document, so they won. A panel with a sticky header now draws its body in
a stacking context of its own (isolation: isolate), so nothing in the body
is drawn over the header, whatever its z-index. It is not a containing
block: nothing in the body is placed differently, and popovers and menus
are drawn outside the panel.
A pinned column's header cell painted nothing, so the column headers that
scroll under it showed through ("ORDER DA" under "ACTIONS"). It now paints
what the header row is drawn on (the first color of a gradient), opaque as
a pinned body cell is, and tells what it holds that it is drawn on that.
Stories: Layout/Panel "Sticky Header Above Its Body" and Collections/Table
"Sticky Header Above Pinned Columns" scroll each layer under the header and
read every point across it, and require pinned column headers to be opaque
and on top. Unit: __tests__/stickyLayers.test.tsx.
…the end of the label's line (qorus#646) As on ReqoreEntityRow (da508bf), at David's direction: in the opt-in 'menu' mode, two or more actions fold into one menu button at the end of the label's line when the row is narrow. A severity row of that mode with one action had no menu to fold into, so it stacked the action under the label: a line of its own for one button, where a row with more had none. Now that one action stays a button at the end of the label's line, where the menu would be, and the row says so: data-actions='end'. It is not stacked, so an icon-only action stays its icon with its tooltip, as at full width. Only 'menu' rows change: the default ('stack') and 'row' are as they were. Stories, both failing before (data-actions was 'stack', the action under the label): - Severity Row > Narrow Container Lone Action In Menu Mode: a 400px container, beside the same row with the default, still stacked; - Severity Row > Narrow Container Lone Action In Menu Mode On A Phone: a finger's size, and a tap runs the action without opening the row. Unit (SeverityRow.test.tsx): 'end' for 'menu' with one action, labeled or icon-only (fails before); one action unchanged by default, with 'stack', with 'row', and wide with 'menu'. The prop's docs, RowActionsMenu's TReqoreNarrowRowActions and COMPONENTS.md say it.
… label (qorus#646)
David, on the EntityRow phone story ("the button takes up too much space on
a narrow screen; on a narrow screen it should read 'Test'"), as a general
rule: a row-end action on a narrow screen uses a short label.
Every row action type takes an optional `shortLabel`
(IReqoreRowActionShortLabel: IReqoreEntityRowAction, IReqoreSeverityRowAction,
TReqoreTableCellAction). Where reqore draws an action compactly, on a line
it shares with the row's label, the action says it:
- the one action of a narrow narrowActions='menu' ReqoreEntityRow or
ReqoreSeverityRow, at the end of the label's line (data-actions='end');
- a narrow table's folded column, for an action with no icon (one with an
icon is its icon alone there, as before: the column is the menu button's
width).
Its label is then its tooltip (unless it has one) and its accessible name
(unless it has one), so a short label is written as words of its label.
With no short label (getCompactRowAction), an action with an icon and a
tooltip is its icon alone there, named by its label: its author gave it the
words it reads by without its label. Any other says its label, as before.
So no row drawn today changes unless it gives an action a short label or a
tooltip. Wide, and on a line of its own (stacked, or in a row under the
label), every action says its label. Menus name each action by its label.
Stories:
- Entity Row > Narrow Container Lone Action In Menu Mode On A Phone: the
action reads "Test", and is still called "Test the connection" (fails
before: it read "Test the connection");
- Entity Row > Narrow Container Lone Action Short Label and Severity Row >
Narrow Container Lone Action Short Label: three rows in a 720px box and a
400px one; wide each says its label, narrow its short label, its icon
alone (an icon and a tooltip), or its label (neither). Both fail before.
Unit: __tests__/rowActionShortLabel.test.tsx (getCompactRowAction, both
rows, the table's folded column; 10 of 16 fail before, the 6 that pass are
the cases that do not change).
COMPONENTS.md documents shortLabel on EntityRow, SeverityRow and Table; the
type is exported from the index.
|
Pushed
|
…ose does, as prettier writes it (qorus#646) 90f64b0 escaped the double quotes around "Test" and "Test the connection" in the description, which prettier writes as a single-quoted string: two more deviations in a file whose count must not go up. Typographic quotes need no escape. EntityRow.stories: 36 deviations, as before 90f64b0.
|
Pushed
|
…roup (qorus#646)
A wrapped ReqoreControlGroup gave its children no cap. A child as wide as
content that does not wrap stayed that wide, past the group and the card it
was in: the Qorus IDE's data provider path (a `fluid wrap fill` group whose
steps are `fluid={false}` groups holding a select that says its value and
description) ran 487-712px wide in a 346px card on a phone, and the
favorites dialog's Apply was cut off.
Now each child of a wrapped group is capped at the row (max-width: 100%)
and may shrink below its content to fit it (min-width: 0). A capped button
says its description on as many lines as it takes, as a fluid button in a
wrapped group already did (fluid buttons always had max-width: 100%).
The rule is reqore's global style (UIProvider,
WRAPPED_CONTROL_GROUP_CHILD_SELECTOR, `:where(.reqore-control-group-wrapped)
> *`), and weighs nothing: a child that sets its own width limits (a
button's maxWidth) keeps them. A group's own styles could not weigh less
than the child's: styled-components scopes them to the group's class. The
group marks itself `reqore-control-group-wrapped` when it wraps.
Stories (Form/Control Group):
- Wrapped Group Keeps A Long Button Inside: a picked path in a 346px card;
fails before (children 488, 351 and 382px wide in 346px);
- Wrapped Group Keeps A Long Button Inside On A Phone: the same in a card
on a phone, with the card's Apply; fails before (488px in 332px).
Unit: __tests__/controlGroupWrapCap.test.tsx (which children are capped,
the rule's weight; without the class, the two that cap fail).
|
Pushed
|
…s its own maxWidth (qorus#646) A button had max-width: 100% only when it was fluid and not fixed (`maxWidth || (fluid && !fixed ? '100%' : undefined)`). A fixed button had no cap at all, and fixed reaches a select's button directly or through a group (a group hands its children `fixed`). So in the Qorus IDE's data provider path, a step's select saying its value and description stayed as wide as both on one line, 487-712px in a 346px card on a phone, even inside the group round 9 capped at the row. Now every button is capped at its container (max-width: 100%) unless it sets its own maxWidth, which stays its limit; a square button keeps its side. fixed keeps meaning that the button does not stretch. A capped button's label is clipped as any button's is, and its description takes as many lines as it needs, as in round 9. What else this changes (every story file captured before and after, 1188 captures): a panel's single action that the title bar's responsive group folded into a menu of one, because its labelled button overflowed the group, now stays its icon, as the panel's own stories describe (Panel · Compact Title When Narrow, Fit Label Comparison); and a drawer sheet's header icons sit half a pixel lower (82.64 to 83.14px; the buttons' boxes are unchanged), which shows in Drawer · Responsive Sheet Mobile. Stories, failing before: Button · Fixed Button Stays Inside Its Container (476px in a 346px card) and its phone story. Unit: __tests__/buttonNeverWiderThanItsContainer.test.tsx (fixed, fixed and fluid, and neither fail before; fluid, maxWidth and square are the cases that do not change).
…rus#646) ReqoreColumns, and ReqoreCollection with it, laid its columns out at `minmax(<minColumnWidth>, <maxColumnWidth>)`. A collection's minimum is its zoom width, 400px at 100%, so in a container narrower than that the column stayed 400px and its cards ran past it: in the Qorus IDE's favorites dialog on a phone, a card was laid out at 63-433px in a dialog whose content ends at 342px, and its Apply was out of reach. Now the minimum is `min(<minColumnWidth>, 100%)` (getColumnMinimum): the width is the narrowest a column is where the container has room for it, not a width it keeps where there is less. A keyword minimum (auto, min-content, max-content, fit-content(...)) is kept as it is. Callers that already wrote min(<width>, 100%) themselves are unaffected. What else this changes: nothing else drawn (every story file captured before and after, 1188 captures). Stories, failing before: Collection · Narrow Container Keeps Its Items Inside (cards 420px in a 342px box, Apply and Remove past it) and its phone story (the favorites in a dialog on a phone). Unit: __tests__/columnNeverWiderThanItsContainer.test.tsx (the minimum and the rule fail before; the keywords are the case that does not change).
|
Pushed
|
|
@Foxhoundn this PR is ready for your review, merge and release as 0.78.7. David approved every round's stories, and it is green at reqraft (toolkit-react) #137 depends on it. This PR's |
…e field out of view (qorus#646) Root-caused by the Qorus IDE agent (David's round-7 review): in a short window - a 1280x720 Storybook window's ~380px canvas, a phone in landscape - a click into an editor opened no list at all. A popover held below its target (keepPlacement) makes room by scrolling the target's scroller by what the list lacks. A list taller than the window scrolled the field from y=209 to y=-59: out of view, so Popper marked the reference hidden, and the popover closed as it opened. The field is now scrolled up at most to the top of what shows it (its scroller, or the window where the scroller starts above it). What is still missing is taken from the list's height (it is clamped to the room under the field), or the list flips where that room is under a hundred pixels, as before. Stories (Form/RichTextEditor), both failing before (the list never opens): - The List Opens In A Short Window: a field 200px down a scrolling page in a 980x380 window, a list of twenty templates; - The List Opens On A Phone In Landscape: the same on a touch screen at 844x390, opened with a tap.
…pened level is the same list (qorus#646)
Found building the Qorus IDE's field picker, and in released reqore with
plain nested `items`.
The list of an opened level is drawn by the list of the level above it,
which kept its document key listener and its `passKeyHandler`. With the
focus outside the list - a text field or an editor it is opened from,
which keeps the focus - every key was handled by both levels: ↓ moved the
highlight in both, and Enter in the opened level also selected the row the
level above had highlighted ("Inner 1 | Plain" for one Enter). Only the
level the list shows now listens, and hands its controls to
`passKeyHandler`.
An opened level was also handed only some of the list's options, so it
dropped `filterRanking`, `filterPlaceholderBuilder`, `noItemsFoundText`,
`listId` and `passKeyHandler`: a list in German said "Search 2 items..."
and "No items found" one level down, ranked nothing, and its rows had no
ids for `aria-activedescendant`. It is handed them all now.
Stories (Form/Dropdown), both failing before:
- Only The Open Level Hears The Keys: a group opened from a text field
that keeps the focus; ↓ Enter picks "Inner 1" only;
- An Opened Level Says And Ranks As The List Does: a German list ranked by
relevance with a `listId`, opened at its group.
…as it (qorus#646) Root-caused by the Qorus IDE agent from David's IDE round-1 review: a checkbox with `wrapLabel` in less room than its box and its label on one line - the IDE's "Skip totals rows" in a 38px row - kept its label on one line, past the row. `wrapLabel` only stopped the label's text from being kept on one line. The label sits in two control groups (on either side of the box), and a group that is not fluid is `flex: 0 0 auto`: it kept the width of the label's one line, so there was never less room for the label to wrap in. The checkbox was also exactly one line high, so a label that did wrap would have run out of it. With `wrapLabel` now: - the groups the label is in shrink (`flex-shrink: 1; min-width: 0`); - the checkbox is never wider than its container and shrinks in a row (`max-width: 100%`, `min-width: 0`, `flex: 0 1 auto` unless fluid or fixed); - it is as high as its label, at least one line (`min-height`). A checkbox whose label does not wrap is drawn as before. Stories (Form/Checkbox), each failing before (the label runs to 338px or 632px in a 160px box, or past a 390px screen): - A Long Label Wraps In A Narrow Box (beside one that does not wrap); - A Long Label On The Left Wraps In A Narrow Box; - A Three Line Label Makes The Checkbox As High (a checkbox and a switch); - Long Labels Wrap On A Phone (touch, 390px; a tap on a wrapped label checks its box). Unit: __tests__/checkboxWrapLabel.test.tsx; its three wrapping tests fail before, and a checkbox that does not wrap keeps its height and width.
… saying what is missing (qorus#646) DeepScan on #717 (NULL_POINTER, __tests__/stickyLayers.test.tsx:61): the header row was asserted with `expect(row).toBeTruthy()` and then read through `row!`, and each header cell's probe through `!` too. An assertion that does not narrow the type leaves the next line reading a property of what may be nothing. `requiredElement(selector, what, parent)` and `requiredAttribute(element, name)` (src/stories/utils/required.ts) return what they find, or throw with what is missing. stickyLayers uses them for every element and attribute it reads, and its header cells are told from their row's children by `instanceof HTMLElement`, not a cast. The same in round 11's own tests and stories, where a `!` or a cast read an element that might not be there: checkboxWrapLabel.test, the Checkbox stories' boxes and phone story, the Dropdown stories' search field, and the RichTextEditor short-window stories' page, list and window. Nothing drawn changes: the Checkbox, Dropdown and RichTextEditor TemplateFieldUX story files pass as before (61 stories) with captures identical to the last ones.
…s the same (qorus#646) Found with DeepScan's REACT_INEFFICIENT_PURE_COMPONENT_PROP on #717 (a table header cell's inline click handler for the memo ReqoreButton): handing the button a stable handler did not stop it being drawn again, because the stacked control group it is in handed it a new style object on every render. A stacked group squares the corners that meet and tucks each border under the one before, in a style it makes anew each time; it was a prop that always changed, so a memo child - a button, a dropdown - was drawn again with its group whatever else it was handed. useCloneThroughFragments (the control group's, the button rail's and the menu's) now keeps the style each child was last handed, by its place, and hands it again while it has the same properties with the same values. What is drawn does not change. Unit: __tests__/controlGroupKeepsChildStyle.test.tsx - a memo child is not drawn again when its group is (fails before), and is when its place in the group changes its style (its corners rounded at the new end).
…s and objects while they are the same (qorus#646) DeepScan on #717 (REACT_INEFFICIENT_PURE_COMPONENT_PROP, src/components/Table/headerCell.tsx:430): the cell handed the memo ReqoreButton a click handler made anew on every render, so the button was drawn again each time the cell was - when another column was sorted, or the cell moved - for nothing. Its effect object, and the menus' (ReqoreDropdown is a memo too) handlers, input props, effect and button style were made anew the same way. Now: - the click (⌘-click sorts, then the column's onClick) and the filter handler are useCallbacks of what they use; - the menus' input props, the menu's effect (the filter's glow, the sorted column's gradient) and the button's effect are memos of what they are made of; - whether a menu opens (not on a ⌘-click) and the ⋯'s style are module constants. A ⌘ check of an event that is not there no longer throws. What is drawn does not change. Unit: __tests__/tableHeaderCellKeepsItsControls.test.tsx - the header cell's button, and its menu's, are not drawn again when another column is sorted or the cell moves (both fail before, with the cell's handlers made inline), and are when the cell's own column is sorted or it is handed another click handler.
01ad747 read the sort direction in the menu effect's dependencies, whatever the column: a table that does not sort hands its header cells no sort data, and every such table threw "Cannot read properties of undefined (reading 'direction')" (42 table unit tests). The cell now reads what it is told of the sort only when it is told it: whether it is the column sorted by, and its direction, both of sort data that may be absent. Unit: tableHeaderCellKeepsItsControls - a cell with no sort data is drawn, and drawn again (fails on 01ad747).
|
Pushed
|
|
@Foxhoundn this PR is ready for your review, merge and release as 0.78.7, now with round 11 and the two DeepScan fixes. David approved every round's stories. It is green at What round 11 adds:
reqraft (toolkit-react) #137 is waiting on this release. It will pin 0.78.7 next. |
This PR would introduce changes in every single product we have, making it impossible to properly review the current branches / PRs. This PR will be merged after every open IDE / toolkit-react branch is merged. |
|
Closed in favour of two PRs, at the front-end team's request to keep the contrast work apart from the fixes reqraft and the Qorus IDE depend on:
Each of #717's 62 commits went to one side, with 3 split in two; its 4 merges were not picked. The classification, one line per commit with its reason and the commit it is now, is in #722's description. Nothing is lost: the contrast branch's final tree is #717's head, plus one story fix made in review and one popover fix that reqraft's tests found on #722's first prerelease, on develop's current dependencies (#718). Every story was approved by David again on both PRs. |
Text and icons read on what they are drawn on: the qorus#646 contrast work, with the reqore fixes found since then collected here (David's choice), and Foxhoundn's review of it.
Base:
develop. #708 merged (0183173, released as 0.78.5) and was merged into this branch (52add6e), so the diff is only what is listed here.Links: #708 · qoretechnologies/qorus#646
Version: none set here. This PR takes the next free version when it merges (#715 claims 0.78.6).
What is in it
prefers-reduced-motion; the story run asks for it while it checks contrast.It comes from David's review of qorus#646: "black text on a dark grey background — extremely difficult to read". It changes how Reqore picks text and icon colours everywhere, so qlip shows broad changes. Below is what changed and why each kind of diff is expected.
Commits
qlip: on build #384 every changed and new snapshot (179 + 2) is flagged with its cause for review; none is accepted by us.
1. Colours are picked against what is actually drawn, not against the colour a surface is made from (
helpers/colors.ts)getReadableColorFromandgetReadableColorchoose by WCAG contrast instead of a luminance threshold.#f2f2f230. Its opaque hue is near-white, so it used to pick black text even though it is drawn at 19% over a dark page.#f2f2f2/#0d0d0ddoes not reach 4.5:1, they fall back to pure white or black. For example,#f2f2f2on the info blue is 4.2:1 and white is 4.7:1.getDrawnColorgetContrastRatio: unrounded, because polished rounds 4.495 up to 4.5.getReadableColorOn: one colour that reads on every colour of a gradient.getReadableAlphaandgetReadableFadedColor: fade only as far as the result stays readable.getReadableAccentColor(On): keeps the hue and moves the lightness toward the side that can read on every background.getReadableIntentColor,getReadableThemeColor,getReadableIconLook,getReadableTextOngetReadableGroupOpacity: how far something drawn as a whole can fade before a mark in it stops reading.getThemeSurface(s),fadeColorgradient: 'info') keep their text readable at both ends, including one step lighter when hovered or pressed. qlip: such gradients span a little less on the light side.2. Surface context (
ReqoreSurfaceProvider/useReqoreSurface, exported)3. Faded or secondary text and icons have a floor: 4.5:1 for text and 3:1 for icons (WCAG 1.4.3 / 1.4.11).
CONTROL_ICON_OPACITY), the clear-input button, checkbox and switch words, breadcrumb separators, and the knob icon of an unset switch.4. Intent-coloured text and icons use a readable shade of the intent
intenton Span, P, Heading and IconiconColor5. Behaviour changes worth a look
idleRevealrail rests atidleOpacity(0.34) or less faded, as far as its icons need to still stand out by 3:1. In the default theme it now rests at about 0.64. The idle stories now assert "dimmed but readable" instead of exactly 0.34.6. Bugs found along the way
rgba(color, opacity)replaced the alpha. EmptyState "Muted" was white on white.aria-disabled: Tag, Callout, Panel, the Accordion item, EntityRow, Statistic, CollapsibleContent, the Checkbox row, Progress, SeverityRow, StackedBar, Testimonial, the Textarea and RichTextEditor wrappers, and the table cells of a disabled row.7. The story run's contrast gate (
src/stories/utils/contrastGate.ts, an afterEach in.storybook/vitest.setup.ts, and axe-core 4.10.2 as a dev dependency). It runs after transitions finish.color-contrast.data-decorative.Some stories put colours together that no text colour can read on. Examples:
Those stories now use the same kind of colour in a shade the text reads on, so the demos model readable choices.
8. An icon's colour given in its props (d3c8108, 8ad3d8c)
CONTROL_ICON_OPACITYand pick its colour and opacity together so that it stands out by 3:1. A colour given in the icon's own props (leftIconProps={{ intent }}, a panel'siconProps) was spread over that pick: the icon made it readable at full opacity, and drawn at 70% a warning icon was 2.4:1 (814 findings in the Qorus IDE's contrast gate).leftIconColor; the rest of the props pass on. One helper,splitIconColor(helpers/iconProps).9. A link's intent reads on the surface it is drawn on (e8b4ec2): on a warning-tinted table cell it was 3.5:1, over an entity row's gradient 2.8:1.
10. An active button draws what it holds on its fill (2a15c0f): a selected menu item's or an active sidebar toggle's description, badges and icons were picked for the resting wash (2.0–2.7:1 on the info blue). The NavRail's idle floor reads its active mark the same way. qlip: the NavRail idle stories rest brighter.
11. An entity row fades its description's and metadata's text, not what they hold (0fda83b): a tag or a link inside was faded twice (4.0:1, 2.8:1).
12. A translucent gradient is read with the lighter ring that shows through it (aeb3d8c): a statistic's label on a translucent glow read 3.6:1.
13. Reduced motion (79b6fe6)
prefers-reduced-motion: reduce(WCAG 2.3.3), Reqore's springs settle at once (react-spring'sskipAnimation, set byReqoreUIProvideras it renders and kept current), and CSS transitions inside the layout and the portal take 0.01ms (not none: a transition of no time sends notransitionend). Keyframe animations are as they were.emulateReducedMotion), so it measures what stays on screen, never a frame of a fade. A story taggedreduced-motionruns under it; the others run with the motion a user sees. qlip captures before the gate, and each story resets to normal motion first, so this changes no existing snapshot.14. DeepScan: readable-colour props are one object while what they hold holds (e563e64)
REACT_INEFFICIENT_PURE_COMPONENT_PROP(a faded description, label or icon effect, a style or a handler built inline and handed to a memo component) and 1INSUFFICIENT_NULL_CHECKin Panel.15. ControlGroup: a responsive group folds only what it has, and each child once (2f2bbcf)
16. Foxhoundn's review of the contrast work (b824566, 11fdbfa, 951549c, cbcd1ad)
currentColor, a name) is drawn as given instead of throwing; a short hex is read as CSS reads it (#bafis#bbaaff, not a green); results are cached.idleOpacity, an explicit effect opacity, a hex icon colour) are the caller's, and the floors apply to defaults only; own fills and surfaces are honoured (a button's or tag'seffect.color, acustomTheme, a popover's and a drawer's own fill); colours are worked out once per render; reduced motion stays inside Reqore.17. Row-menu names (e970c1f)
aria-labelas its accessible name ("Show analytics for Short copy", not only "Show analytics").rowLabelonReqoreRowActionsMenu,rowLabelText). A row whose label is an element keeps the plain "Actions". A table's row menu is unchanged.18. A popover takes a click as soon as it is drawn (dceb77d)
useCombinedRefshanded a component's element to its refs in an effect, after the paint. A popover around a Reqore button or a dropdown learnt of its trigger only then, so a trigger clicked straight away opened nothing.useCombinedRefsnow returnssetRef, a ref callback React calls in the commit that attaches the element. Button, Input, Textarea, Tabs item, FadeScroller, Table body and header, Panel, Menu and Menu item use it.19. The breadcrumbs' folded menus open at a tap and say they are menus (4193ebc)
aria-haspopup="menu"andaria-expanded; the "…" dropdown has the classreqore-breadcrumbs-overflow-menu.Checks (head 4193ebc)
🤖 Generated with Claude Code