Repository navigation
v0.7.6
Astryx 0.7.6 — all @astryxdesign/* packages ship at this version.
OLD_VERSION=0.7.5 # replace with the Core version your project uses today
npm install @astryxdesign/core@0.7.6 @astryxdesign/cli@0.7.6 # plus every other @astryxdesign/* package you use, at 0.7.6
npx @astryxdesign/cli upgrade --from "$OLD_VERSION" --apply --path .@astryxdesign/core
New Features
-
useAnnounce takes an optional
{hasReplace: true}. An instance created with it states one current state, such as a search's result count: each message it writes withdraws that instance's previous message while it has not been spoken yet, whatever their politeness, and is spoken at its own politeness, so input faster than a frame (a barcode scanner, dictation, automation) says only the latest. Messages from other instances, and from instances without it, still join. MultiSelector sends its result count, its create row and its empty-search message through such an instance, so fast typing no longer says1 result Create "Urg" Create "Urge" …; its selection changes keep their own instance and still join. This follows spec:AST-086 FR9 and FR13 (#9070). (#8640) -
Adds three
@astryx.diffViewer.*keys to theencatalog for the labDiffViewer's large-change fold: its "Large change: N lines" label and its Show full diff / Hide full diff button. No existing string changes; locales without a translation fall back to English. (#9462) -
FieldLabel: a tap on the label's info icon (
labelTooltip) opens its help without also toggling or focusing the control (component:FieldLabelFR9). Before, the tap that opened the help also activated the label's control, as a tap anywhere on a native label does: on a touch screen it focused the field, raising the keyboard, or toggled the switch or checkbox the label names, an unintended second action. Now a tap by finger or pen opens the help alone; a mouse click on the icon still activates the control like the rest of the label, and hover still shows the help. (#9089) -
FieldStatus: the
tooltipvariant is deprecated and is removed in the next minor release. FieldStatus has no tooltip presentation: a direct<FieldStatus variant="tooltip">draws a plain message box with no tooltip, and your editor now marks it deprecated. To show a status message from a field's on-field icon, setstatusVariant="tooltip"on the field control instead; that stays supported. (#9196)
Before:<FieldStatus type="error" message="Required" variant="tooltip" />
After:
<TextInput label="Name" value={name} onChange={setName} status={{type: 'error', message: 'Required'}} statusVariant="tooltip" />
-
Adapt NumberInput's compact steppers to primary-pointer precision: hide them on coarse pointers and restore the field's themed end padding. Fine-pointer steppers are unchanged. (#8015)
Compatibility: this is a non-breaking presentation adaptation. The public props and exports, numeric value and callback contracts, validation, direct entry, and keyboard stepping remain unchanged; existing NumberInput calls require no source or configuration migration. Coarse-pointer users enter the value directly instead of using the compact visual stepper aids. -
TextInput and TextArea tell a touch keyboard what the field holds. New props
enterKeyHint(what the Return key says: Search, Send, Next, Go, Done),inputMode(which keyboard: number pad, decimal pad, phone pad, web search, email, URL),autoCapitalizeandhasAutoCorrect, plushasSpellCheckon TextInput (TextArea already had it). TextInput'stypealso acceptssearch,telandurl. The value types are the new exported closed unionsEnterKeyHint,InputModeandAutoCapitalize, so TypeScript rejects any other value. Each prop renders the native attribute of the same name and nothing when unset, so existing fields keep the browser's defaults. (#8166) -
A theme's
motiontakes two knobs beside the duration scale:tempo(0.75–1.5) scales every motion duration, andcharacter('crisp','standard'or'expressive') sets the new--ease-entranceand--ease-movetokens;--ease-exitstays the same at every character. Their types,ThemeMotionConfig,ThemeMotionKnobsandMotionCharacter, are exported. A theme without them renders exactly as before; a duration scale and explicit tokens win over the knobs, Spinner's and Skeleton's loops keep their timing under every tempo, and the press release fade follows tempo up to the 200 ms Reduce Motion cap under the person's setting. Springs don't read the knobs yet. The motion, token and theme-authoring docs and the theme template describe the knobs. (#8661)Also released in
@astryxdesign/cli. -
TimeInput, DateTimeInput: the
hourFormatdefault follows the locale. This is a default change. The old default was always'12h'; the new default, whenhourFormatis unset, is theInternationalizationProviderlocale's hour cycle, so ade-DE,en-GB, orvi-VNapp shows "14:30" likeTimestampbeside it, whileen-USand an app with no provider keep "2:30 PM". To keep the old default, passhourFormat="12h". To honor a person's own 12/24-hour preference everywhere, put it on the provider locale (en-US-u-hc-h23). Twelve-hour text now uses the locale's own day period ("오후 2:30", "午後2:30", "2:30 p. m.") instead of a fixed English AM/PM, along with its digits and separator, and the typed field reads that text back. Fixes #8992. (#9022) -
A
SearchSourcecan show results before its search settles.search(query, context)may callcontext.update(items)(new exported typeSearchContext) to show matches it already has, such as people the page loaded earlier, and then return the server's answer.BaseTypeahead,TypeaheadandTokenizershow each list for the current query, change the list at most once per keystroke, and add later rows only after the ones already shown, so the highlighted row never moves; the field stays busy and the result count is announced once, when the search settles. A later list that moves or removes shown rows is shown as given with a development warning. Sources that never callupdateshow one list per query as before, with two changes every field sees: option element ids now follow the item'sidinstead of its row index (tests that select options by an index-based id need updating), and a groupedTokenizerwithhasCreateshows its Create row after the groups instead of first. To show local matches at once, passdebounceMs={0}and delay the server request inside the source (spec:AST-092). (#7962)
Fixes
-
A bottom-sheet DropdownMenu whose sheet preload fails (a dropped connection) no longer reports an unhandled promise rejection, and no longer keeps the failed load, so a module loader that retries loads the sheet's actions on the next open. (#8478)
-
DropdownMenuno longer rewrites its trigger's style on every render. Its trigger refs, and the trigger refusePopoverreturns, now stay the same function when the popover'sonShoworonHidechanges, so React stops detaching and reattaching the trigger, and a tooltip anchored to the same trigger keeps its place. Triggers that takeusePopover's ref directly (ComplexSelector's built-in trigger, DateTimeInput's date field and Breadcrumbs) get the stable ref too. (#9130) -
A
Popoverin a Document Picture-in-Picture window now scrolls once that window gets too short for it or its content grows past it, instead of being cut off: it measures its overflow from the window it renders in, not the page. (#9098) -
In Safari, content already gliding after a layout change no longer jumps ahead when a second change retargets it, such as a row that moves again while it is still moving, or a row inside a card that is itself gliding: the new glide started up to about 3 px ahead of where the row was drawn, more the later in its frame the change was made. A gliding element's place before a change is now read on the clock of every glide that carries it, its own and its gliding containers', so the new glide starts where the old one is drawn. (#9327)
-
AppShellis a layout root: the layout values its ancestors pass down stop at its root, so it lays out the same inside a paddedCard, aSectionor aLayoutas at the page root. Before, its internal Layout escaped a padded host's padding on all four sides, so in a paddedCardthat clips its overflow the shell's logo was cut at the inline start and its navigation and content at the other edges. A caller's own props and styles onAppShellstill apply; theme, color mode, direction and translations keep inheriting. (#9418) -
With
height="fill", AppShell's main region can be scrolled from the keyboard when its content overflows and has nothing focusable (WCAG 2.1.1 Keyboard). Before, main was only the skip link's target (tabIndex={-1}): Tab passed it by, so long text, a report or a read-only page could not be scrolled without a pointer. Now, while main scrolls overflowing content, it is a stop in the tab order and keyboard focus on it draws the standard focus ring, inset; in an elevated shell with a top nav and an inline side nav, main then takes the surface's rounded top-start corner so the ring follows it. Tab entry moves on when the first focusable element inside main is a plain link or button; otherwise main is one stop before it. A main region that fits, andheight="auto"(where the page scrolls), are unchanged. (#8111) -
DateInput ignores selections from an already-open disabled picker and blocks Reset's value change and month navigation while disabled, without dismissing the picker or moving focus. (#8813)
-
DateRangeInput ignores calendar and preset commits after its open picker becomes disabled, without dismissing the picker or moving focus. Presets reflect the field's disabled state and reason. Enabled selection, clear behavior, and loading-only dismissal remain unchanged. (#9155)
-
MultiSelector blocks selection and creation callbacks and Actions when it becomes disabled while its panel is open, without removing the focused control. (#9152)
-
Selector ignores option selections after it becomes disabled, including selections from an already-open popup and pointer gestures started while enabled. Its trigger remains focusable and disabled values remain excluded from forms. (#9137)
-
In forced colors (Windows High Contrast), a lone
Avatar's initials and default person icon now take the color around them, as the browser forces it, instead of always showing CanvasText: inside a control that paints itself disabled they show GrayText. Faces inside anAvatarGroupkeep CanvasText on the group's ring. Outside forced colors nothing changes. (#8819) -
Badge: a badge with a label is now never shorter than one line of that label. With a larger text size, a badge's label grew while the badge kept its fixed height, so the label spilled out of the pill and white text on a colored badge became unreadable. The badge now grows with its label. At the default size nothing changes, a theme that sets the badge's height keeps that height there while its badges still grow with larger text, a color-only badge keeps its size at every text size, and a badge still shrinks and truncates in a container too narrow for it. (#8236)
-
Dragging a
BottomSheetwith snap points up from a shorter detent, or back down past it, no longer flashes the sheet away from the finger. For one frame the sheet jumped by the height its scrolling area gives up at that detent (about half the screen at a half-height stop) and then snapped back. The height and position now change in the same frame. (#9193) -
A BottomSheet opened from inside another sheet's content drags down to dismiss on its own. The covered sheet underneath no longer takes the gesture, so dragging the top sheet's grabber no longer moves or dismisses the sheet beneath it. (#8820)
-
Calendar, DateInput, DateTimeInput: a month label that stands alone starts with a capital where the locale capitalizes one. In Spanish, French, Italian, Dutch, Polish, Russian and Ukrainian, Calendar's heading read "octubre de 2026"; it now reads "Octubre de 2026", following CLDR's stand-alone capitalization, as the weekday headers already use CLDR's stand-alone names. A two-month view capitalizes both months, and each month grid's name, the touch date fields' month headings and
useCalendarNavigation'smonthYearLabeldo the same. Dates in running text, such as a field's value, are unchanged, and so is every locale CLDR does not capitalize, Vietnamese included. (#9371) -
Carousel's scroll area is a group named like the carousel, so a screen-reader user who tabs onto it hears where they are. It is a tab stop only while its content overflows; a carousel that fits no longer adds an empty stop. (#8155)
-
ChatComposerInput: a tap on a pasted-text token opens its preview without raising the keyboard. Before, the tap that opened the preview also focused the composer around the token and placed the caret in it, which on a phone raises the on-screen keyboard over the preview. Now a tap by finger or pen stops at the token, so touch reaches the preview and its Expand action without the keyboard covering them, the operable path
architecture:interaction-modalityINV4 asks of every modality; a mouse click on the token still places the caret in the composer, and hover still opens the preview. (#9189) -
A click inside an open Tooltip or HoverCard in a ChatComposer's body no longer moves focus to the composer's input. A click on the body's empty space still focuses it. (#9375)
-
A press in a ChatComposerInput's own editor no longer closes its open trigger menu (the
@mention or/command list). Clicking inside the query keeps the menu showing; a click that moves the caret out of the query, a press anywhere else on the page and Escape still close it. (#9503) -
Safari no longer slows down as a page fills a container with many children because of ChatMessage's avatar-spacing rule. The avatar's top spacing beside a named bubble is now judged from the message itself instead of from its siblings. It looks exactly as before. (#9438)
-
CheckboxList: a group disabled with a
disabledMessagegives that reason to each checkbox as its description (aria-describedby), as well as to the group, so a screen reader announces it on the focused checkbox. A row's own description comes first. (#9168) -
ClickableCard: a disabled link card no longer passes an undefined
hrefto the router link component, and a focused link card keeps focus when its disabled state changes. (#8659) -
CodeBlock: in right-to-left layouts the collapsed disclosure chevron now points toward the inline end (left), as other disclosure chevrons do. Expanded, it still points down. (#9294)
-
When a
Collapsiblecloses while keyboard focus is inside its content, for example from an owner-setisOpenor a group's value, focus moves to its trigger. Before, focus fell to the page, so a keyboard or screen-reader user lost their place (WCAG 2.2 SC 2.4.3). Focus anywhere else is not moved. (#9250) -
TypeaheadandTokenizerexpose their state on the combobox (WCAG 2.2 SC 4.1.2;component:FormLayoutFR5 and AR2). Before, the combobox had noaria-required, so underFormLayout defaultOptionality="required", where the visible "Required" marker is hidden, a screen reader announced a required field as optional; and an error status showed its message with noaria-invalid. Now the combobox setsaria-requiredfor the resolved required state andaria-invalidfor an error status. A caller's ownaria-requiredoraria-invalidstill applies when the field's own state is off. (#9034) -
CommandPalette no longer shows its "Type to search" text after you have typed. While the first search is still loading, the list stays empty and the field's loading spinner shows; the no-results text appears once that search finishes with nothing. Clearing the field brings "Type to search" back. (#9420)
-
Comments: the failure announcement spoken for a Comments error banner now comes from the
@astryx.comments.errorAnnouncementlocale entry, so it can be translated like the rest of the component's messages. (#7902) -
ComplexSelector's popup content keeps its last rows clear of a bottom bar the app declares through
LayerProvider'sinset, instead of scrolling them underneath it, and its chevron turns at once when the person prefers reduced motion instead of animating its rotation. (#8551) -
ComplexSelector: with
statusVariant="tooltip"the status message is no longer lost. The trigger row now shows the status icon as a focusable info-tip button that reveals the message on hover, keyboard focus, or tap, and the trigger is described by that message instead of pointing at an element that did not exist. Pressing the info-tip does not open the popup. (#9026) -
A context menu that was never opened no longer mounts its menu surface; it mounts at the first open and shows as it attaches. A Table whose cells carry context menus (row expansion gives every body cell one) showed its first rows after a minute or more at 2,000 rows in Safari; it now shows them in a few seconds. (#9278)
-
An app bundled into one esbuild file (no code splitting, as no-build pages are) no longer crashes on first render with a TypeError such as "Cannot read properties of undefined (reading 'useId')" or "(reading 'useEffect')", for example when it uses
ListorButton. Code-split apps are unchanged and still load Core's lazy modules on demand. (#9055) -
DateInput, DateRangeInput and DateTimeInput no longer build their calendar while the popover is closed. Each closed field used to mount a full month grid at page load and keep it updated, so a form with many date fields paid that cost per field. The calendar now mounts when the popover opens and opens on the field's current value. Opening, focus and dismissal are unchanged. (#8902)
-
A press in a DateInput's own text input, on its clear button or on its label no longer closes its open calendar. Clicking into the date text to fix a digit keeps the calendar showing; a press anywhere else on the page and Escape still close it, and moving focus from anywhere in the field (its text, clear button or calendar button) to another field now closes it too. (#9433)
-
A press in a DateTimeInput's own time input no longer closes its open list of preset times. Clicking into the time text to fix a digit keeps the list showing; a press anywhere else on the page, moving focus away and Escape still close it. (#9459)
-
A
Dialogrendered into a same-origin iframe through a portal now returns focus, when it closes, to the control in the iframe that opened it, as it does in the page. (#8381) -
A disabled field that keeps its tab stop (TextInput, TextArea, NumberInput, Typeahead, Tokenizer, Selector, MultiSelector, and DateInput, TimeInput, DateRangeInput and DateTimeInput on every surface: popover, native and bottom sheet) now shows keyboard focus under forced colors, such as Windows High Contrast. Its focus border takes the system Highlight color there, as enabled fields' does. Nothing changes outside forced colors. (#9410)
-
A disabled
Item,ListItemorTreeListrow with anhrefno longer navigates from a screen reader, Enter, or a link menu. Its link now has nohref,targetorrel, as a disabledLinkdoes, and it keeps the link role. (#9056) -
Under a finger in Chrome, the page beside an open non-modal
Drawer(hasScrim={false}) scrolls again. The panel clipped its content withoverflow: hidden, which made it a scroll container, and Chrome held a drag on the page beside it in that container's overscroll containment, so the page did not move. It now clips withoverflow: clip, which clips the same way without a scroll container. The panel's content area still scrolls on its own, and a modal drawer is unchanged. (#8636) -
The development warning for passing both
buttonandrenderTriggertoDropdownMenunow namesrenderTrigger; it named atriggerprop that does not exist. The docs match the component too:buttonis mutually exclusive withrenderTrigger, the private menu radius defaults to--radius-container, and a disabledDropdownMenuCheckboxItemorDropdownMenuRadioItemtakes no focus — the arrow keys, typeahead, and the pointer highlight skip it — while it stays in the accessibility tree as disabled. (#7517) -
A keyboard-focused
DropdownMenurow now shows the standard focus ring, drawn inside the row, in every color mode. Rows showed focus only with a translucent background, which forced colors (Windows High Contrast and similar modes) replace with the page color, so a keyboard user there could not tell which row Enter would activate. The ring follows the last input: after a key press it marks the focused row, including a row the mouse then moves focus to; after a mouse or touch press it does not show. Action, destructive, checkbox, radio and sub-menu rows all show it, including inContextMenuand breadcrumb menus. AContextMenuopened by right-click or long-press focuses its first row without the ring; one opened from the keyboard shows it. (#7517) -
The anchored
DropdownMenunow reflectsdata-presentation="popover"on itsdropdown-menutarget, as the bottom sheet already reflectedbottom-sheet. A theme rule written forpresentation:popover, which the docs list as a visual prop of that target, now applies. (#7517) -
DropdownMenuandMoreMenunow forward the caller's ref to the trigger button in the bottom-sheet presentation (architecture:public-component-apiINV8). Before,presentation="bottom-sheet", orpresentation="adaptive"on a compact touch screen, replaced thebutton.ref(and therefthatMoreMenupasses) with an internal ref, so a caller's ref stayed empty. The popover presentation already forwarded it. Apps need no code change. (#8898) -
DropdownMenu,MoreMenuandContextMenurender a submenu for a data item with nesteditemsinside a{type: 'section'}, as they do at the top level. Before, such an item rendered as a plain row with no flyout and noaria-haspopup, although its type allows nesteditemsthere; the bottom sheet on a phone already showed it as a submenu. (#8858) -
With an
animatedLayouthandle, a Collapsible or a TreeList row closed while it opens, or opened again while it closes, continues from what it shows: the content's edge stays on the content after it on every frame, instead of jumping back to fully shown or hidden. Under Reduce Motion with a touch pointer, where the content cross-fades instead, a reversal still starts its fade over. (#8721) -
Field:
FieldStatusPlacement, the type of every field control'sstatusVariant('attached' | 'detached' | 'tooltip'), is exported from@astryxdesign/coreand@astryxdesign/core/Field, so a wrapper component can type astatusVariantprop it forwards. (#9368) -
Enabled Field inputs (TextInput, TextArea, NumberInput, Selector, MultiSelector, Typeahead, Tokenizer, and the date and time inputs) now show keyboard focus under forced colors, such as Windows High Contrast. Forced colors drop the inset focus shadow and repaint every border in one system color, so a focused field looked the same as one at rest. A focused field's border now takes the system Highlight color there, with or without a status. Disabled fields, which stay focusable, don't get it yet. Nothing changes outside forced colors. (#9392)
-
FieldStatus wraps a long unbroken word in a validation message, such as an email address or URL echoed from the field, inside the message box instead of painting it past the field's edge where its end could not be read. (#9126)
-
TextArea, Typeahead and Tokenizer: a caller's
aria-labelledbyno longer replaces the field's own label. Before, it was the only name the control had, solabel="Notes" aria-labelledby="billing-heading"named the textarea "Billing". Now the caller's ids follow the field's own label ("Notes Billing"), as TextInput, NumberInput, Selector and MultiSelector already do. A Typeahead inside an InputGroup, which ignored the caller's ids altogether, is now named by the group, its label, then the caller's ids. The visible label stays in the accessible name (WCAG 2.2 SC 2.5.3 Label in Name;family:input-fieldsFR10.1). Fields without a calleraria-labelledbyare unchanged. No API changes. (#8862) -
FileInput with
statusVariant="tooltip"keeps its status button, and the message it reveals, in dropzone mode and while the field is busy (loading or uploading), instead of dropping it and leaving the field's description pointing at nothing. (#9134) -
FileInput: a caller's
aria-describedbyand otheraria-*props now reach the trigger button, the field's focusable control. Before they were set only on the hidden native input, which isaria-hiddenand never focused, soaria-describedby="hint"was never read ("Report" announced without "PDF only, up to 10 MB"). The caller's ids are now joined to the trigger's own description ids, first. The props the trigger composes itself (aria-label,aria-labelledby,aria-invalid,aria-busy,aria-disabled,aria-hidden) are not copied. Seefamily:input-fieldsFR10.1. No API changes. (#9031) -
When several picked or dropped files break
acceptormaxSize,FileInputnames every rejected file and why in its status message, not only the first (WCAG 2.2 SC 3.3.1). The messages are listed in the active locale. Going overmaxFilesstill reports the limit, "Maximum N files allowed". (#9043) -
Inside
<FormLayout defaultOptionality="required">, FileInput and Slider now tell screen readers they are required even without their ownisRequired, like the other fields.isOptionalstill opts a field out. (#9053) -
ButtonGroup and InputGroup: the rule that rounds a group's last member no longer slows down filling a container in WebKit (as measured), on any page, groups or not. In WebKit, inserting 10,000 children into one element took about 1.15 s with the old rule on the page and about 10 ms with the new one. Corners look exactly as before: only the last member, skipping trailing popovers, templates and dialogs, rounds its trailing corners. (#9394)
-
Include the visible heading in SideNavHeading and TopNavHeading menu button names, with or without heading links. Keep the translated generic name when TopNavHeading has no heading. (#9329)
-
Tooltip and HoverCard: a tap on a disabled
Button,IconButtonorToggleButton, or a disabled menu item, now opens the tooltip that explains why it is disabled, and nothing activates. Before, withtouchTrigger="auto"(the default), the tap counted as the control's own action, so on a touch screen the disabled reason could not be reached (spec:AST-085FR5,architecture:interaction-modalityINV4). Anaria-disabledcontrol now performs no action, and a tap on a label for a disabled control opens its reason too. A tap on a disabled control inside another acting trigger, such as an option row or a link-role card, now opens that trigger's tooltip or hover card. Taps on enabled controls,touchTrigger="tap"and"none", and mouse hover are unchanged. (#8207) -
Touch press feedback now hears scrolls and page hides on the window's capture phase, where its pointer listeners already were. Under an app's window-capture
stopPropagation, or a document-capturestopImmediatePropagationregistered before Core, a finger's pressed look now ends when the content under it scrolls, and every pressed look ends when the page is hidden. (#8360) -
TypeScript users applying
interactionOverlayStylesthrough an Astryx component'sxstyleno longer get a type error. The touch-press properties of each variant (animationName,animationDuration,animationTimingFunction,animationFillMode, and the colour variants'backgroundImage) now carry their CSS value types instead ofunknown. The generated CSS is unchanged. (#9440) -
A disabled swipe action on an
Itemrow now stays focusable while the row is swiped open. Before, it rendered a<button>with the nativedisabledattribute, so Tab skipped it and a keyboard or screen-reader user could not reach it to learn it exists. Now it exposesaria-disabled="true"and keeps its Tab stop, while a click, Enter or Space still runs noonActivate, leaves the row open, and does not reach the row's ownonClick(spec:AST-085FR1, FR2). No API changes. (#9211) -
The arrow-key hint of TabList, Toolbar and SegmentedControl no longer appears, or gets used up, when focus lands in content a tab or toolbar button opens through a portal, such as a sheet, dialog or popover. It still shows the first time keyboard focus enters the row itself. (#9362)
-
A tall tooltip, or any layer that keeps clear of its trigger, no longer covers the trigger for an extra frame when the page scrolls on a busy device. The layer re-checks its place in the frame after the scroll, before that frame paints, so the trigger stays visible from the second frame on. (#9123)
-
useLayer: a show that a browser-run popover hide refuses (Chromium 156 throws an
InvalidStateError) is replayed once that hide has ended, so the layer ends shown. For example, when the browser's own Escape closes a light-dismissible layer, the tooltip on the button that opened it now shows instead of failing. (#8543) -
A press inside an open layer (a popover, tooltip, hover card, menu, dialog, drawer, lightbox, mobile navigation, bottom sheet or toast) no longer runs click, pointer, mouse or touch handlers on the elements around where the layer is written. Before, a click inside a menu written inside a clickable card or row could also activate that card or row. Handlers inside the layer, capture-phase handlers, and document and window listeners still receive the press. A handler that needs presses inside a layer belongs on the layer's content. (#9497)
SelectableCard: a click inside an open layer within the card (such as a popover's content) no longer toggles it. Until the next minor, the caller'sonClickstill receives that click, now during the capture phase, so before the layer's own handlers. -
Link:
maxLinesnow truncates when the link sits in a line narrower than its text. The link, as an anchor or a button, stops at its container and its label draws an ellipsis, instead of growing to its whole text and being clipped with no ellipsis. (#9059) -
A caller's
data-colorno longer replaces the oneLinkreflects for theme targeting (architecture:public-component-apiINV5,LinkFR11). Before, caller props were spread after the theme attributes, so passingdata-colorchanged what theme rules saw on the root even though the realcolorprop was unchanged. This holds for the anchor, button and disabled forms. Other callerdata-*attributes still reach the root. (#9157) -
A finger held on a DropdownMenu trigger may drift up to 13px on each axis and still open the menu under every primary pointer, a phone's, a tablet's and a touchscreen laptop's alike, as AST-094 FR29 allows; it cancelled past 10px. No menu, row or trigger reads the pointer query. (#8360)
-
MobileNav's header row grows with its content. It was a fixed one-bar height, so a header taller than one row (a title with a search field under it, or a title that wraps at a larger text size) overflowed the row: the bottom border ran through it and the content ran into the first navigation item. The row is now at least one bar high, so a one-line header looks as before.SideNavin drawer mode, which passes itsheadertoMobileNav, gets the same fix. (#8865) -
Components rendered into a same-origin iframe or a Document Picture-in-Picture window now read the last input in their own document for their keyboard focus rings and keyboard highlights (Slider, MultiSelector, DropdownMenu, ChatComposer, CommandPalette, panel search fields, menus), and for the ring on focus they move themselves. On the page, behavior is unchanged. Where an overlay returns focus on close is unchanged. vjeux wrote the original per-document fix, for Dialog (#8381); this extends it to every reader. (#9148)
-
In the DateInput and DateTimeInput bottom-sheet calendars, a month arrow clicked or tapped as the previous arrow's scroll is ending now brings the calendar to the month its title names in Safari, instead of back to the previous arrow's month. (#8513)
-
In the DateInput and DateTimeInput bottom-sheet calendars, a month arrow no longer sets the calendar paging on by itself in Safari when the page stalls for a moment right after the tap; the calendar moves one month and stays there. (#8513)
-
MultiSelector: the default count text on the trigger (
triggerDisplay="count", for example "2 selected") now comes from the message catalog (@astryx.multiSelector.selectedCount), so it is translated and can use plural forms under any locale.formatValuestill replaces it. (#9020) -
In a SideNavHeading or TopNavHeading menu built with NavHeadingMenu, one ArrowDown or ArrowUp now moves focus one item. It used to move two, skipping every other item, and in a two-item menu ArrowDown never moved focus. (#9019)
-
Fix FileInput to prevent file picker activation and file selection while busy. Clear drag-over highlighting when disabled or busy, and expose the busy trigger as disabled to assistive technologies. (#9167)
-
NumberInput: with
formatValue, tabbing into the field keeps its text selected, so typing replaces the value. Before, the editable text replaced the formatted text after focus and collapsed Tab's selection to the end, so with1,500,000shown, Tab then7gave15000007and committed it on blur. Now1,500,000becomes a fully selected1500000and7replaces it. A click still places the caret where it landed. Fields withoutformatValueare unchanged. No API changes. (#9033) -
Following an
Outlinelink moves focus to the chosen section, as a native in-page link moves the focus starting point, so the next Tab continues from that section instead of jumping back to the top of the page. The section is focused without scrolling, so Outline's own scroll is unchanged (WCAG 2.2 SC 2.4.3 Focus Order). (#9045) -
Dismissing a file picker (Escape, or Cancel in the chooser) inside a
Drawer,MobileNavorLightboxno longer closes it and loses what was in it. Only a close request fired at the surface itself closes it, asDialogandBottomSheetalready do. (#8855) -
A
Dialog,DrawerorBottomSheetthat is open on the server now renders closed until hydration, and opens right after it where it rests, with no entry motion, as anything present on page load does; its later opens and closes animate as before. Before, it painted open over a live page witharia-modal="true"but no modal state: the page under it stayed interactive, a sheet's invisible host could swallow clicks, and assistive technology was told a modality that didn't exist (WCAG 2.2 SC 4.1.2). ABottomSheetSwitcherwhose active sheet is set on the server is covered too. Client-rendered overlays are unchanged. (#9360) -
A determinate ProgressBar keeps moving to its new value while a script runs. The fill animated its
width, which lays out and repaints on the main thread every frame, so a value change stalled for as long as the page ran JavaScript and then jumped. The fill now spans the track and slides into place with a transform that the browser animates on its compositor, in Chromium and in Safari, laying out and painting nothing per frame. Under reduced motion a new value now lands at once, on every pointer. Otherwise duration, easing, colors, marks and right-to-left drawing are unchanged (the fill mirrors with the bar's own direction, so a bar in a left-to-right region of a right-to-left page still fills from the left); the track now clips its fill to the rounded rail in both modes, so below about one track height of fill the leading end follows the track's curve instead of drawing a narrower full-height capsule, and a runtime change of the bar's direction slides the fill across to the other side instead of jumping there. (#8196) -
A disabled
RadioListItemno longer runs a caller'sonClick(spec:AST-085FR2). Before, a radio in a group disabled with adisabledMessagestays focusable througharia-disabledrather than nativedisabled, so the browser did not silence it and the handler still ran on a click on the radio, on Space, and on an arrow key onto it. It still refused to select. An enabled radio is unchanged: itsonClickstill runs. (#9120) -
RadioList: a group disabled with a
disabledMessagegives that reason to each radio as its description (aria-describedby), as well as to the radio group, so a screen reader announces it on the focused radio. A radio's own description comes first. (#9169) -
RadioList and Switch: a required field no longer reads as "invalid entry" to screen readers before the user has answered. Each required radio and a required switch take their invalid state from
status. (#9042) -
useResizable(and every component that uses it, such asSideNav) no longer reads the window size again after each render it commits. That read forced a layout of the whole page inside every commit, so a tab switch under anAppShellwith aSideNavpaid for it on slow devices. The window is still read during render, and every size, bound and reported maximum is unchanged. (#8857) -
A
Tablecolumn-resize handle is named by its column's visible text when the header is a node, such as<span>Amount (EUR)</span>, instead of by the column key. The key remains the fallback only for a header with no text. (#9057) -
useScrollOverflowmeasures again when the container's content changes with the container keeping its size: a child added, removed or resized, or a new gap or padding. Carousel's scroll area, a tab stop only while its content overflows, and its edge buttons and fades follow slides added or removed and a slide that grows; TabList's fades follow its tabs the same way. (#8155) -
SegmentedControl: a control disabled with a
disabledMessagegives that reason to each segment as its description (aria-describedby), as well as to the group, so a screen reader announces it on the segment that has focus. The control also always keeps one Tab stop: the selected segment, even when it is itself disabled, else the first segment not disabled on its own, else the first. (#9176) -
SegmentedControl: a disabled control, or a disabled selected segment, keeps the control's Tab stop. Before, a control disabled without a
disabledMessagehad no Tab stop and ignored the pointer. A selected segment that was itself disabled lost the stop to another segment, and once the stop had moved there, selecting a third segment left it behind, so Tab re-entered on an unchecked segment. Now the selected segment always holds the one Tab stop, announced as disabled (aria-disabled) when it is, and nothing can be selected while disabled. (#9314) -
A disabled
SegmentedControlItemno longer runs a caller'sonClick(spec:AST-085FR2). Before, the segment refused to select but still called the handler on a click, and on Enter or Space for the selected segment of a group disabled with a reason, which stays focusable. This covers a segment disabled by its ownisDisabledand one disabled by its group. An enabled segment is unchanged: itsonClickstill runs first and can still cancel selection withpreventDefault(). (#8989) -
Selector, MultiSelector: pressing another field while the list is open now leaves focus in that field. Before, the list's close handler moved focus back to the Selector's trigger, so the next key went to the closed Selector (a letter could change its value through type-to-select). Escape, picking an option, or a press on nothing focusable still return focus to the trigger, and a MultiSelector with
renderTriggerstill returns focus to the caller's control on every close. (#9018) -
A disabled
SideNavItemstays focusable. Before, it rendered a<button>with the nativedisabledattribute, so Tab skipped it and keyboard and screen-reader users could not reach it, and an item that became disabled while focused dropped focus to the page. Now it exposesaria-disabled="true"and keeps its Tab stop, while a click, Enter or Space still runs noonClick, does not navigate, and does not close the mobile drawer. In a collapsed rail, a disabled item's label tooltip now opens when Tab focuses it and describes the focusable control rather than its row. This meets the focusable-disabled requirement in spec:AST-085 (FR1, FR2, FR4, FR5). No API changes. (#9102) -
When a collapsible
SideNavcollapses and takes the keyboard focus with it, focus now moves instead of falling to the page, where a keyboard or screen-reader user lost their place (WCAG 2.2 SC 2.4.3). From a row the collapsed rail does not render, such as an item without an icon, focus moves to the collapse toggle, or, with no toggle in the SideNav, to the nearest row still in the rail. On a row the rail keeps, focus stays on that row, and a header or footer control the collapse keeps or re-renders under the same name keeps it. From a header or footer control the collapse removes or replaces under another name, or the resize handle of aresizableSideNav, focus moves to the collapse toggle, or, with none, to the first row. Focus the collapse does not take is not moved. (#9249) -
A
resizableSideNavkeeps its nav and items mounted when it collapses or expands, as a plain one does. Before, every collapse remounted them, so an open nested group closed and item state reset. (#9290) -
SideNav (
footerIcons,topContent), SideNavHeading (icon,headerEndContent), SideNavItem (endContent) and SideNavSection (endContent): a slot whose value is0showed a stray "0" beside its slot, or nothing at all. Now each slot follows the shared presence check. Empty values (null,false,'') render no slot, as an omitted one does.0renders inside its slot as content, including a collapsed rail's icon-only heading and the footer and top areas around it. (#9498) -
Slider: a press on the mark label of two coincident thumbs' shared value now goes to the upper thumb, as a press on the thumbs does, so a press that never moves leaves focus on the same thumb wherever it lands. Before, a press on that label went to the lower thumb, which the upper one covers. (#9184)
-
Slider: a range slider whose two thumbs sit on the same value can be dragged apart again. Only one of the two thumbs can travel toward where the pointer goes: a press on the track beside them picks the thumb on its side, and a press on the thumbs themselves waits for the pointer to move, so the direction of that first movement picks the thumb, which takes the drag's focus, pressed paint and value tooltip. Such a drag always follows the pointer: once the pointer passes the other thumb, the dragged thumb stops against it and the other one takes the drag, so a first step that drifts the wrong way never sticks it. Before, two thumbs at the maximum could not be dragged down, and a press on the track above two coincident thumbs moved neither. (#8151)
-
Slider: a range slider's thumb names ("Minimum value" and "Maximum value", announced to screen readers) now come from the message catalog (
@astryx.slider.minimumThumb,@astryx.slider.maximumThumb), so they follow the app's locale. (#9023) -
Published packages no longer include repository test and benchmark files, reducing install size without changing exports. (#9153)
Also released in
@astryxdesign/richtext,@astryxdesign/cli,@astryxdesign/build,@astryxdesign/theme-butter,@astryxdesign/theme-chocolate,@astryxdesign/theme-gothic,@astryxdesign/theme-matcha,@astryxdesign/theme-neutral,@astryxdesign/theme-stone,@astryxdesign/theme-y2k. -
More slots treat
'',falseandtrueas empty, the way React renders them, instead of drawing an empty wrapper: ChatSystemMessage'sicon, Item'sstartContent,endContentanddescription(so a ListItem or menu row gains no blank lane), and MetadataList'stitleand MetadataListItem'sicon.0still counts as content and shows "0". (#9204) -
DropdownMenuGroup'stitleandBadge'slabelnow use the sharedisRenderablecheck, sonull,undefined,true,falseand''count as rendering nothing, and0still counts as content. ADropdownMenuGroupwithtitle={true}no longer renders an empty heading that names the group.Badgeis unchanged (thanks @vjeux, #8236). Other slots are unchanged. (#9160) -
More slots treat
'',falseandtrueas empty, the way React renders them, instead of drawing an empty wrapper: TreeList'sheader(a header that renders nothing no longer names the tree, so it keeps itsaria-label), TreeList items'startContentandendContent, TopNavMegaMenu'sitemsandfeatured, Table headeraftercontent, and OverflowList's measured overflow indicator.0still counts as content and shows "0". (#9206) -
Many components that show built-in text are smaller. Each one now carries only the English for its own strings instead of every Core string, so a component such as Button loads about 7 KB less (gzip) and a new string elsewhere in Core no longer makes it bigger. Table (and Markdown, which renders it), Chat, PowerSearch, CommandPalette and the date and time inputs still carry every Core string for now. Nothing you see changes: every built-in string still shows the same English, translations and overrides resolve as before, and
useTranslator,InternationalizationContextand@astryxdesign/core/locales/en.generated.jsstill reach every Astryx string. (#9429) -
Table, Markdown, Chat, CommandPalette and the date and time inputs are smaller too. Each now carries only the English for its own strings instead of every Core string, about 7 KB less (gzip) each. PowerSearch still carries every Core string, because a custom operator's
i18nKeycan name any Astryx key. Nothing you see changes: every built-in string shows the same English, and translations and overrides resolve as before. (#9452) -
TextArea: a
startIconof0showed a stray "0" beside the start-icon slot, with no padding reserved for it. The slot and its padding now follow one presence check: empty values (null,false,'') render no slot, and0renders inside the slot like any other content. TextInput uses the same check, with no visible change. (#9443) -
In a clickable
Stepper, a disabledStepnow keeps its button: it stays in the tab order, is announced as unavailable (aria-disabled), and keeps focus if it becomes disabled while focused, but still does not activate. Its description (the reason it is unavailable) describes the button (spec:AST-085FR1, FR4, FR5). Before, it rendered as plain text, so keyboard and screen-reader users could not find it or tell it was unavailable. (#8867) -
A disabled
Switchno longer runs a caller'sonKeyDownoronKeyUp(spec:AST-085FR2). Before, the switch stayed focusable througharia-disabledinstead of nativedisabled, so a key pressed on it still reached those handlers, while itsonClickwas already withheld. It still refused to toggle. An enabled switch is unchanged: its key handlers still run. (#9122) -
Tablenames say what they belong to (WCAG 2.2 SC 4.1.2 and 2.4.6; the APG sortable table pattern underspec:AST-020FR15;spec:AST-025FR12). (#9047)- A sort button was named "Sort by Name", or "Sort by Name, sorted ascending" with "priority 2 of 3" in multi-sort, and that phrase replaced the column header's name. It is now named by its column's text, so the header keeps that name; the direction comes from the header's
aria-sort, and while several columns sort, each button is described by its priority ("Priority 2 of 3"). - The scroll region was a group named "Table" on every table. It now takes the table's own name (its
aria-labelledby, else itsaria-label), and "Table" only for an unnamed table.
- A sort button was named "Sort by Name", or "Sort by Name, sorted ascending" with "priority 2 of 3" in multi-sort, and that phrase replaced the column header's name. It is now named by its column's text, so the header keeps that name; the direction comes from the header's
-
TabList: pressing or arrowing to a tab in a scrolling strip no longer measures the strip inside the press. Focusing the tab already scrolls it into view, so the check that keeps the selected tab whole now runs in the next frame instead of forcing the new selection's style and layout before the press can paint. (#8856)
-
Text and Heading: a long unbroken word, URL or ID in text that isn't truncated now wraps instead of running past its container, so in ordinary flow a phone-width page (320 px) no longer scrolls sideways. Ordinary text lays out exactly as before, and truncation (
maxLines) is unchanged. Inside a flex row, the flex child still needsmin-width: 0. Text and Heading now setoverflow-wrapthemselves, so a container you style withoverflow-wrap: anywhereno longer reaches their text; Astryx's own containers (CommandPaletteEmpty, Toast, FieldStatus, Banner) pass theiranywherewrap through. (#9351) -
TextArea: the
maxLengthcharacter counter now comes from one catalog message (@astryx.textArea.counter), so its numbers use the locale's number format and translators can change the order or separator. In English a limit of 1,000 or more now shows grouped digits (62/2,000instead of62/2000). (#9025) -
TextInput: the clear button no longer shifts inward when the field turns busy. The busy Spinner now sits inward of the clear button, as in Typeahead and Tokenizer, so a user reaching for the button while a search runs does not miss it. No API changes. (#9027)
-
ProgressBar target marks expose their label as an accessible name before the tooltip loads, while keeping keyboard focus and hover/focus tooltips. (#9276)
-
Timestamp keeps a caller's
aria-labelon relative output and a caller'stabIndexwhile its hover card is attached. Before, the full-date name and the card's tab stop overwrote them, so a timestamp named for its context (for example "Posted 5 minutes ago") lost that name, and one taken out of the tab order came back into it. Without a caller value, nothing changes. (#8726) -
Toast: a
Themeinside toast content shown by the fallback viewport (noLayerProvider) now themes only its own subtree. It no longer rewrites the page's<html>theme as if it were the app's rootTheme. First fixed in #5617. (#9253) -
A toast that is closing when a new toast takes its visible place under
maxVisible, or that the same handler pushes out and then dismisses, leaves the stack at once. Before, it came back later as an invisible, empty row that kept the notifications landmark, took one of the visible places, and let F6 move focus to its hidden dismiss button. A secondcollisionBehavior: 'ignore'toast with the sameuniqueID, raised in the same handler as the first, is now dropped unannounced, as it already was once the first had rendered. When a control inside a toast raises toasts and then dismisses its own toast, focus moves to a toast that shows, the new one included, instead of to the notifications region. (#8051) -
Toast: a toast's auto-hide timer stays paused while any pause condition still holds. Before, the pointer leaving the card, focus leaving it, or the window regaining focus resumed the timer even if another condition was still active, so a toast could hide while focus was on its action or the pointer was over it. Focus moving between the toast's own controls also keeps it paused. (#9016)
-
Toast: dismissing the last toast after reaching it with Tab or a click returns focus to where it was before, instead of dropping it to the page. (#9039)
-
A caller's
aria-pressedordata-is-pressedno longer replaces the pressed stateToggleButtonreflects (architecture:public-component-apiINV5,ToggleButtonFR12). Before, caller props were spread last, so passing either attribute made assistive technology and theme rules see a state that did not matchisPressed.ToggleButtonnow ownsaria-pressedand the reflecteddata-is-pressedanddata-elevation. Other callerdata-*attributes still reach the root. (#9159) -
ToggleButtonGroupacceptsclassName,style,id,aria-*,data-*and event handlers, and passes them to its group element. The group keeps its ownrole,aria-labeland theme target. (#9040) -
A disabled
Tokenreflects its state on thetokentheme target (data-disabled) in every structure, so a theme can style it. A disabled link token keeps a focusable link announced as disabled (aria-disabled="true", nohref), instead of becoming plain text with no tab stop. (#9037) -
A
Tokenwith anhrefnow calls a caller'sonClick(architecture:public-component-apiINV5,TokenFR3). Before,onClickwas dropped wheneverhrefmade the token a link, so the handler never ran on a click. This covers the link token and the link token with a remove button, where a plain click on the token surface runs the handler once, a Cmd/Ctrl-click or middle click on the surface opens the link in a new tab without running it, and a click on the Remove button does not run it. Navigation is unchanged, and callingpreventDefaultin the handler still stops it. (#9205) -
The inline
TokenizerClear all button announces "All items removed", once, as the touch sheet does, so a screen reader user learns the selection was emptied.PowerSearch's Clear all announces its own words, "All filters removed". (#9038) -
Tokenizer: the inline create row now takes its
Create "…"label from the@astryx.tokenizer.createmessage, the same one the bottom sheet uses, so it follows the provider's locale instead of always reading in English. Fixes #8931. (#9029) -
A link in a Toolbar (such as a Button with
href) now shares the toolbar's single tab stop. Arrow keys reach it like any other control, and it no longer adds a second Tab stop to the row. (#8921) -
Tooltip: under the default
focusTrigger="auto", keyboard focus opens the tooltip of a trigger that was out of the tab order when the tooltip attached. A Button or IconButton in a Toolbar or ButtonGroup whosetooltipwas set after mount, or any roving-tabindex member rendered withtabindex="-1", now shows its tooltip when the arrow keys reach it. Whether focus counts is decided when it arrives, so a focusable trigger the old check missed, such as a<summary>, opens too. Focus inside a trigger that is not focusable still does not open it. (#9366) -
A click inside an open Tooltip or HoverCard no longer activates the card, row or field around its trigger: a FileInput no longer opens its file picker, and a ClickableCard, SelectableCard, Token, or Item, TreeList or Table row no longer activates. Links and buttons inside a HoverCard keep working. (#9252)
-
TopNavMenuandTopNavMegaMenunow forward theirrefto the trigger button in drawer mode (architecture:public-component-apiINV8). Before, only the desktop popover trigger received the ref, so a caller's ref stayed empty whenever the menu rendered inside the mobile drawer. Apps need no code change; the desktop trigger and every other prop are unchanged. (#8897) -
TopNavMegaMenuFeaturedCard: in right-to-left layouts the link arrow now points toward the inline end (left) instead of backwards. The link's text and accessible name are unchanged. (#9317)
-
TopNav (
heading,startContent,endContent), TopNavHeading (logo,headerEndContent), TopNavMegaMenuItem (icon), TopNavMenu drawer items'iconand NavHeadingMenuItem'sdescription: a slot whose value is0showed a stray "0" beside its slot. Now each slot follows the shared presence check. Empty values (null,false,'') render no slot, as an omitted one does: an emptycenterContentorstartContentno longer switches the bar to its center layout, shows the mobile-bar menu toggle or opens an empty drawer section.0renders inside its slot as content. (#9500) -
Where the primary pointer is fine (a mouse, a trackpad, a touchscreen laptop, a desktop pen tablet), the press feedback controller no longer registers a touch listener: it reads the primary pointer at the first finger or pen press and keeps its
touchcancellistener only while that pointer is coarse. (#8360) -
When a
TreeListrow collapses while keyboard focus is inside its children, for example from anitemschange, focus moves to that row (the outermost one when nested rows collapse together), which then holds the tree's Tab stop unless it is disabled (a disabled row takes focus, and the stop goes to the tree's first enabled row if the collapse removed the row that held it). Before, the focused row disappeared and nothing held focus, so a keyboard or screen-reader user lost their place (WCAG 2.2 SC 2.4.3). Focus anywhere else is not moved. (#9251) -
ChatComposerInput's trigger menu announces its "Searching…" row through Core's shared busy announcement, like other busy controls: once per search, not repeated while typing continues, and not at all when results arrive first. Inside a native modal dialog an app renders without Astryx, it is now heard inside that dialog. (#9154)
-
TypeaheadandTokenizer: after the search source is replaced, pressing ArrowDown on the closed field loads the new source (its focus entries, or the typed query against it) instead of opening the old source's list. (#9166) -
A
TypeaheadorTokenizerdisabled with a reason (isDisabledwithdisabledMessage) no longer opens its results from ArrowDown or a click. ATypeaheaddisabled this way also no longer accepts a pick by Enter or a press, even from a list that was open before it became disabled (aTokenizeralready refused those). Escape or Tab still closes such a list. (#9036) -
Typeahead, Tokenizer, PowerSearch: a disabled field keeps its tab stop. Before, a field disabled without a
disabledMessagerendered a natively disabled input, so Tab skipped it and screen-reader users moving by focus never reached it. Now the input is announced as disabled (aria-disabled), stays read-only, opens and picks nothing, and shows a focus ring under keyboard focus. A search still in flight when the field becomes disabled is dropped: it opens, announces, and spins nothing. A Tokenizer or PowerSearch withtokenOverflowBehavior="unfocusedLayer"and nodisabledMessagekeeps its natively disabled input for now. (#9275) -
Type-to-select ignores accents in every locale. Pressing
dnow reaches "Đào",areaches "Ánh" and "Åsa",nreaches "Ñandú",lreaches "Łódź", andınow reaches "Isparta" and "İzmir". This applies to a closed Selector and to open DropdownMenu, ContextMenu, MultiSelector and TreeList lists, and to the publicuseTypeaheadanduseTreeFocushooks. Before, a key typed without the mark skipped every item that starts with the marked letter. Matching ignores case, the combining accents U+0300–U+036F, the stroked letters đ ł ø ħ ŧ ƀ, and Turkish dotted and dotless i. It works the same way in every locale, because type-to-select is navigation, not sorting. When you type the accented letter itself, an exact match comes first. For Turkish, ı pairs with I and İ with i. A combining mark typed first is ignored; typed after a letter, it is part of that letter. Sorting and search filtering are unchanged. This meetsarchitecture:internationalizationINV17. No API changes. (#8864) -
A press in a Typeahead's or Tokenizer's own text input, or on its buttons, no longer closes its open list. Clicking between two letters to fix a typo moves the caret and keeps the matches showing; a press anywhere else on the page still closes the list, and Escape still does. (#9367)
-
Tokenizer: an open list reloads when the tokens change, so it shows current choices instead of the old ones — a token removed with its button is offered again, and a value set from outside is no longer listed. (#9416) -
With a value, a
Typeahead's Tab stop is the value's token button (WCAG 2.2 SC 2.4.6 and 4.1.2). It was named by the value alone ("Apple"), so screen reader users could not tell which field it belonged to. It is now named by the field label and the value ("Fruit Apple"), and described by the field's description and status. (#9049)
A clickableTokenpassedaria-labelledbyandaria-describedbyto its root, a generic element where they named nothing. It now passes them to its inner button, the element that owns its action. Every other structure keeps them on its root. -
useTheme:
modeis always'light'or'dark'. A mode outsidelight | dark | system, such as a stale persisted preference, now resolves through the OS preference like'system'instead of being returned as is. First fixed in #5617. (#9254) -
Dragging a picker wheel with a mouse now commits the value when the button is released, as a finger's drag does. Before, the wheel committed every row it passed while the button was still down, so in DateInput the calendar behind the month and year wheels changed under the hand. This applies to the wheels in the TimeInput, DateTimeInput and DateInput bottom sheets. Touch, keys and a click on a row are unchanged. (#8483)
Performance
-
Calendar, DateInput and the other date components now reuse one date formatter per locale and format instead of building one for every label they show, so rendering a month (42 day labels) no longer builds 42 formatters. (#8335)
-
useResizablewithautoSaveId(andSideNavwithresizable={{autoSaveId}}) now saves a dragged size tolocalStorageonce, when the drag ends, instead of on every pointer move. Every write reached each other open tab of the site as astorageevent, so one drag sent dozens of them. The storage key, the stored format and the size saved at the end are unchanged, and keyboard steps,resize(),collapse()andexpand()still save at once. (#7782) -
Table: when nothing in a table depends on a row's position, re-sorting it no longer re-renders every row; the rows only move. On a 1,000-row table this is about a tenth of the work a sort click does. This applies to a table that has an
idKey, sets neitherrowCountnorrowIndexStart, and uses neitheruseTableRowIndexnor a plugin that transforms body rows (selection, row expansion, grouped rows, tree data). Those tables still re-render their rows on a sort. (#9234) -
Pages that mount many truncated
TextandHeadingelements at once mount faster. Each one used to measure whether its text overflows while React was still committing the page, so every measurement forced its own style and layout pass; they now measure right after the commit, all of them together, so the page reads layout once. The ellipsis, the line clamp and the full-text tooltip are unchanged. (#9013)
Documentation
-
Source comments in the BottomSheet gesture and mobile keyboard code are shorter and explain only non-obvious reasons. Nothing a consumer imports or renders changes. (#9524)
-
Source comments in BottomSheet and BottomSheetSwitcher are shorter and explain only non-obvious reasons. Nothing a consumer imports or renders changes. (#9523)
-
Component specs are no longer shipped inside the npm package; they live in the repo at
docs/specs/components/, one folder per component, beside each platform's spec.astryx swizzleno longer copies the component's spec file into your project. Nothing a consumer imports or renders changes. (#9007) -
FileInput's
statusVariantdocs say that dropzone mode does not supportattached; usedetachedortooltipthere. Rendering is unchanged in this release. From the next minor release, a dropzone resolvesattachedtodetachedand defaults to it. (#9258) -
The gesture arena's and the gesture host's internal doc comments sit on the declarations they describe. (#9114)
-
Grid's
alignandjustifydocs no longer claim a'stretch'default. Unset, they leave the browser default,normal, which stretches items that have no natural size or aspect ratio. Rendering is unchanged. (#9347) -
GridSpan: document the
xstyle,className,style, andrefprops, including that a setcolumnsorrowsreplaces the same grid placement instyle. (#9312) -
Heading: document the
xstyle,className,style, andrefprops, including that amaxLinesabove 1 replaces anyWebkitLineClamppassed instyle. (#9328) -
More source comments in the core hooks are shorter and explain only non-obvious reasons. Nothing a consumer imports or renders changes. (#9520)
-
Source comments in the core hooks are shorter and explain only non-obvious reasons. A few hook doc comments are reworded. Nothing a consumer imports or renders changes. (#9514)
-
HoverCard: document the
xstyle,className,style, andisOpenprops in every doc language, say that a text trigger is wrapped for you and an element trigger needs no ref, and say thathasHoverIndicationapplies to text triggers only. (#9536) -
HStack: say that items follow the writing direction (right to left in RTL), that an explicit
hAlignorvAlignwins over itsjustifyoralignalias, and document theclassName,style, andrefprops. (#9540) -
Recorded the component contract updates completed in this release.
Covered records: #8783, #8822, #8878, #9050, #9058, #9062, #9081, #9083, #9090, #9096, #9100, #9101, #9103, #9105, #9108, #9111, #9112, #9113, #9117, #9118, #9124, #9125, #9127, #9128, #9129, #9131, #9132, #9133, #9135, #9158, #9175. -
Stack, VStack, and StackItem: say that a horizontal stack follows the writing direction (right to left in RTL), and document the
className,style, andrefprops, plusxstyleon VStack and StackItem. (#9583) -
More source comments in the core utilities are shorter and explain only non-obvious reasons. Nothing a consumer imports or renders changes. (#9527)
-
Source comments in ten core utility files are shorter and explain only non-obvious reasons. Nothing a consumer imports or renders changes. (#9522)
Other Changes
-
Internal: the touch presentations' animator rechecks a finished animation's end in frame callbacks, starting a frame before it and looking again while up to two frames remain, instead of only on timers that a loaded page can run a frame or two late, so the frame that reaches the curve's end drops the animation before it paints, unless the timer that starts those rechecks itself runs more than a frame late. The timers remain for a page that draws no frames. Not exported from the package entry point. (#9285)
-
Internal: the touch presentations' animator keeps each animation's frame-callback recheck of its curve's end its own, so a recheck still waiting for a replaced animation no longer stands in for the next animation's, which then fell back to its timer. Not exported from the package entry point. (#9325)
-
Internal: the touch presentations' animator starts a call from rest, made in a task, at the first frame after it when the browser has not yet told the page that start (Chromium reports a compositor's start a frame or more later), so a retarget or a rebase in that window continues from the value on screen instead of replaying the curve from its start. A call made in a frame callback, or one whose first frame comes after a block, keeps the browser's start. Not exported from the package entry point. (#9255)
-
AppShell drops an unmount Effect that repeated the release main's ref already performs; main's overflow observation and keyboard delegation are still released on unmount. No behavior changes. (#9262)
-
Internal: the gesture recognizers and the menu press model share one guard for the browser's click after a gesture that already acted, swallowing it for 400ms or until the next press. Keyboard and assistive-technology clicks (
detail0) always pass. (#8360) -
Internal: the touch motion tables register the three context-menu springs spec:AST-094 FR4 names (
springs.contextMenuLift,springs.contextMenuDismissandsprings.contextMenuMorph, a new row at response 0.3 s and damping ratio 0.8) as internal motion roles, so the registered roles are FR4's again. Nothing public changes: themes do not set motion roles, and no shipped component reads these springs yet. (#9432) -
FileInput's source is formatted with the repository's Prettier settings. No behavior changes. (#9263)
-
Internal: a gesture that cancels itself on a finger's move (a long press dragged past its tolerance) keeps that finger claimed until it lifts, so the page never takes the drag over midway; a resting finger is still given back when its gesture is cancelled. Not exported from the package entry point. (#8076)
-
Internal: touch-action derivation, the native scroll probe and haptics for the gesture system. Not exported from the package entry point; no component uses them yet. (#8071)
-
Internal: the gesture hooks call nothing back after unmount;
usePressTrackerbrakes with the host by default and keeps one tracker, so an option change or a new ref never drops a press in progress; the recognizer's arena plumbing isprotected. Not exported from the package entry point. (#8360) -
Internal: gesture host fixes from review. A claimed pen holds only its own stylus touches, and a finger is matched to its scroller, its target and a moving element along the flat tree, through slots and shadow roots. Not exported from the package entry point. (#8360)
-
Internal: the gesture host becomes the one document-level touch owner: the finger count, touch gates, and a kept press for continuous-tracking controls, whose element gets
touch-action: manipulation. A scroll lock's page hold and the finger-scroll watch keep their own listeners and ask the host only about the touches it owns. A second finger fails a one-finger pan that has not claimed, so the page can pinch. A scroll-edge pan holds a lone finger from the first move that pulls away from an edge every scroller under it is at, so those moves are cancelled before it begins; with a second finger anywhere on the screen the page keeps its pinch. A press tracker whose hold acts (a stepper's hold-repeat) can bring a selection lock for its presses, as a recognizer can. Internal, for the touch components that adopt them; not exported from the package entry point. (#8360) -
Internal: the gesture system's DOM host (claims, click suppression, the browser's takeover, the pointer behind a contact), serving a mouse, a pen and a finger alike, with no page listener while idle where the primary pointer is fine. Not exported from the package entry point; no component uses it yet. (#8071)
-
Internal: UIControl press tracking with control and cell profiles and the gesture hooks. Not exported from the package entry point. (#8076)
-
Internal: the gesture system's README for component authors, with its browser proof in Chromium and WebKit. (#8076)
-
Internal: gesture recognizer and arena fixes from review; a pan claims nothing before it begins at 10 pt; one velocity tracker for every gesture (
spec:AST-100: UIKit's measured blend, a scrolling surface's release through the pan'sscrollVelocity(), and FR2's release rules). Not exported from the package entry point. (#8360) -
Internal: swipe, pinch and screen-edge pan gesture recognizers. Not exported from the package entry point; no component uses them yet. (#8061)
-
Internal: a recognizer created with
claimsTouches: falseis a watcher: passive listeners only, simultaneous with every recognizer, and it follows a touch the browser takes for a scroll to the lift. Not exported from the package entry point. (#8360) -
Internal: with the gesture host serving every pointer, a press tracker takes a mouse press as a press, painting at once with no scroll brake (AST-094 FR11); a finger's or a pen's hold that does something keeps text unselected under any primary pointer, a touchscreen laptop's included, while a mouse keeps its released behavior (FR41); a finger is read once where the browser sends Touch Events without exposing
ontouchstart; andperEventPointer, from before FR43, is accepted and does nothing. Not exported from the package entry point. (#8360) -
Internal: the gesture recognizers hear page-wide input through the same shared stream on the window's capture phase, so an app's window-capture
stopPropagation, or a document-capturestopImmediatePropagationregistered before Core, no longer hides a touch from them; a recognizer that must claim a touch mid-gesture still needs the touch to reach its element. Each event type's listener is passive unless a subscriber cancels it (the gesture host'stouchendandcontextmenuwhile a claiming recognizer or press tracker is attached, the page hold'stouchmovewhile it holds, and the click guard'sclickwhile it is armed, and from the menu press model's first arm on), so a page of pressables registers no listener that can hold a scroll. (#8360) -
Internal: ChatLayout drops a scrollbar style rule whose condition nothing ever set, and Popover, Table and the press feedback controller each read an internal marker through one shared name instead of a second hand-typed copy. No visible or behavioral change. (#9256)
-
Item, Token and ChatComposer no longer run their own check for presses inside an open Tooltip or HoverCard; the layer root already keeps those presses from them. No behavior changes. (#9531)
-
Internal:
useLongPresstakes its default duration and tolerance, 500ms and 10px on either axis, from the same source as the long-press gesture recognizer, so ContextMenu's long press behaves as before. A finger held on a DropdownMenu trigger passes its own 13px allowance instead (see the trigger drift fix). (#8360) -
A bottom-sheet DropdownMenu's internal sheet preload reports a failed load in its result, with the loader's error, instead of catching it with an empty handler. Nothing changes for callers: a failed preload still raises no unhandled rejection, and the next open loads the sheet's actions. (#9069)
-
Restore a Tall BottomSheet's internal position after keyboard dismissal. (#8742)
-
Internal: the announcement routing inside open modals watches each dialog's
open,inertandaria-hiddenthrough Core's shared MutationObserver, so any number of open modals use one observer instead of one each. Routing is unchanged. (#9151) -
The date picker's month scroller attaches and removes its four input listeners directly, each with one shared options object, instead of looping over an array of event names and creating an options object on every attach. Behavior is unchanged. The scroller and its listeners come from @vjeux's month scroller fix. (#9146)
-
Internal: the motion roles test's header and the motion README's files table name the internal motion roles, exactly the ones
spec:AST-094FR4 lists. Documentation only; no behavior change. (#8502) -
MultiSelector's source is formatted with the repository's Prettier settings. No behavior changes. (#9247)
-
Internal: when Tab moves focus into a NumberInput with
formatValue, the focus handler itself puts the editable text in the input and selects it, instead of a layout effect that waited for the next render. Behavior is unchanged: typing still replaces the whole number. (#9150) -
An internal
usePresentation/readPresentationresolves a component'spresentationprop into the surface to render and whether its touch input modes apply, keeping an omitted value distinct from an explicit'popover'so a caller's choice always wins over a touch default. Not exported from the package entry point. (#8298) -
On iOS, a touch on an element the compositor animator is moving reaches what is drawn: the animator reports each moving element's offset from its layout box, and the gesture host sends the touch, press bounds and the tap's click there instead of to the layout position iOS WebKit hit-tests. (#8360)
-
Internal: under Reduce Motion, a call that names the opacity a waiting dip returns to, with no travel, joins that dip instead of fading the element out a second time, and
reducedMotionFade()resolves the cross-fade's curve as a registered motion role where it plays. Not exported from the package entry point. (#8076) -
useResizable's size parsing, bound resolution, clamping and snapping, saved-size storage and content-box measurement move unchanged into one internal module, so the factory that follows resolves, clamps, snaps and saves exactly as the hook does. No behavior changes. (#9197) -
Tokenizerhands focus between its controls from callback refs and the Clear all confirmation's close handler, not from effects, asarchitecture:react-component-runtimeINV2 and INV6 require. Where focus lands is unchanged. The presentation swap and the sheet are internal (not yet public). Thanks @imdreamrunner (#7977). (#9162) -
Internal: the gesture host applies a recognizer's own
selectionLockto that recognizer's contact and imports no lock itself; a long press brings one only when it has a hold (aminimumPressDurationabove 0), andselectionLock: nullopts out. Not exported from the package entry point. (#8360) -
Internal: core's touch style keys stay literal, and a test pins every one to the touch presentation resolver's constants (
touchKeys, internal to core). An importeddefineConstskey compiles to a different StyleX priority than the same literal. No public export and no style changes. (#8331) -
Internal: the touch presentations' animator drives a pseudo-element such as a dialog's ::backdrop: a static filling animation holds its value after a curve ends and after
set, a custom property on its host promotes it, and a Reduce Motion dip borrows and hands back the pseudo-element's own opacity. Not exported from the package entry point. (#8522) -
Internal: the gesture hooks' attach-and-release lifecycle,
useAttachment, moves tohooks/(not exported), where other hooks can use it, anduseFingerScrollkeeps its finger-scroll watch through it instead of its own copy of that lifecycle. The gesture-sync helpers stay ingestures/gestureSync.ts. No behavior changes. (#9414) -
Internal:
useFingerScroll(scroller)tells a finger's scroll from the program's, and the gestures README gains a guide to migrating a raw-touch drag onto the recognizers. Internal to the touch components: not exported from the package entry point or the gestures barrel. (#8360)
@astryxdesign/richtext
Fixes
- RichTextEditor's
toolbarnow treats''as empty, as it already treatednull,undefinedand booleans, sotoolbar=""no longer adds the toolbar's top border to the editing surface. RichTextEditorToolbar'sendContenttreats'',falseandtrueas empty, so it no longer adds a trailing separator.0still counts as content and shows "0". (#9265)
Other Changes
- RichTextEditor:
statusVariantnames'tooltip'in its own type, so it keeps acceptingstatusVariant="tooltip"after FieldStatus drops its deprecatedtooltipvariant in the next minor release. No behavior change. (#9196)
@astryxdesign/cli
New Features
- When a template imports Astryx's docs-only demo content,
astryx template <name> <path>,astryx template <name>,astryx template <name> --skeleton, andastryx component <Name> --showcasereplace each demo-content element with a marked<Text>Card content</Text>placeholder and remove the demo-content import, so the code they hand out never imports a module that only Astryx's own previews have. The placeholder uses the file's own AstryxTextimport when that import is in scope, and otherwise importsText, under an alias when the file already uses that name.template.copyandtemplate.showgaindemoContentReplaced(0 when the template carried none), their text output says how many elements became placeholders, and thecomponentsthattemplate.showandtemplate.skeletonlist are the components of the returned source. No shipped template uses demo content yet, so every current template is copied, shown, and outlined exactly as before. The docsite's shadcn install of a block or page applies the same replacement and also replaces demo media, each with a marked placeholder: a placeholder image for an image and adata:,placeholder-video-add-your-ownsource for a video. The item's description says how many references and elements were replaced. The adjacent.astryx/<item>.jsonreceipt keeps its current shape, so every releasedastryx upgradestill reads it. (#9430)
Fixes
-
BottomSheet checklist examples use CheckboxList so the whole row toggles, not just the label (#9533)
-
The motion guide now says that under Reduce Motion, components cross-fade under a finger and change at once under a mouse, and that a value changing in place, such as a progress fill, takes its new value at once under any pointer (design:reduced-motion DR1–DR3). (#8615)
Performance
-
Calling
integrationComponentConflicts()orintegrationCheck()from@astryxdesign/cli/apimore than once in one process now reads each TypeScript file a package'stsconfig.jsonincludes once while the file is unchanged, instead of on every call. A singleastryx doctor integration componentsorastryx integration checkrun is unchanged. (#9422) -
search()shares a catalog gather that is still running: searches that start together in one long-running process, such as a docs site's search right after it starts, now gather once instead of once each (#9460)
Documentation
-
A new
useCollator — Locale-aware Name Sortexample block sorts a list of names with the compare function fromuseCollator: accented and lowercase names land where a reader expects, and switching the provider locale to Swedish moves Å and Ö after Z. Copy it withastryx template useCollatorHookUsage <path>. (#9510) -
A new
usePressFeedback — Pressable Cardexample block shows the hook on an Astryx Card: the hook's marker and theinteractionOverlayStyles.backgroundImageoverlay go straight on the Card, so pressing the card darkens it the way every Astryx pressable does, under a finger or a mouse (Enter or Space flips it), andfocusOutlineStyles.focusVisiblegives keyboard users the shared Astryx focus ring on it. Copy it withastryx template usePressFeedbackHookUsage <path>; the docs-only playing card it shows becomes a<Text>placeholder for your own content. (#9431) -
A new
useToast — Settings Savedexample block calls the function fromuseToaston a settings form: Save shows a "Settings saved" toast with an Undo action, itsuniqueIDmakes a second Save replace the toast instead of stacking another, and a failed save shows an error toast instead. Copy it withastryx template useToastHookUsage <path>. (#9512) -
A new
useTranslator — Load More Footerexample block gets a list footer's "Load more" label and its "Showing 3 of 9" count from the functionuseTranslatorreturns. The app's own message keys are supplied per locale throughInternationalizationProvider, so switching the provider to French changes both strings. Copy it withastryx template useTranslatorHookUsage <path>. (#9573)
Other Changes
- Preserved the v0.7.5 upgrade codemod registry when the release outputs returned to main (#9086), so the v0.7.6 CLI continues to offer that migration.
@astryxdesign/build
New Features
- A production build with
astryxStylexwarns when its stylesheet reads a StyleX-generated custom property that nothing declares, naming the variable and the rule that reads it. It checks the class-name prefixes the build uses,stylexOverrides.classNamePrefixincluded, and never reports a custom property an app declares in its own CSS. (#8075)
Fixes
astryxStylexresolvesdefineConstsvalues read from another file. With the{stylexOptions}form (the one Storybook uses), a file that read only constants from a.stylex.tsmodule lost that import once StyleX compiled it, so the constants never reached the CSS: a constant used as a media-query key failed the build ("Invalid empty selector") and one used as a value shipped as a custom property nothing declares. In either form, a constant defined on the other side of the layer split, or in an Astryx package (compiled with the library class-name prefix) and read from an app file (compiled with its own), stayed unresolved. (#8075)
Documentation
- The Next.js setup in the README says that its Babel config turns off SWC, which
next/fontneeds, so a freshcreate-next-applayout must drop itsnext/fontimport before the webpack source build will compile. (#9439)
Contributors
Thanks to everyone who contributed to this release:
@AKnassa @cixzhang @czarandy @divyanshudhruv @duytbp @ernestt @harjothkhara @HelloOjasMutreja @humbertovirtudes @imdreamrunner @korkt-kim @kyu-rong @MannXo @nynexman4464 @parinith-web @potatowagon @rubyycheung @vjeux
Full Changelog: v0.7.5...v0.7.6