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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

93 changes: 93 additions & 0 deletions packages/atomic-elements/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,98 @@
# @atomicjolt/atomic-elements

## 3.8.0

### Minor Changes

- Pin `@react-aria/collections` to `3.0.0-beta.1`, the only version `Table`
actually works with.

Released as a minor rather than a patch: narrowing the declared range can force
a consumer already resolving 3.1.x back down to `3.0.0-beta.1`, and can conflict
with another dependency that pulls `@react-aria/collections` in through
`react-aria-components`. No public API changed, but the resolved tree does, so
this should not arrive via an auto-merged patch bump.

The range was `^3.0.0-alpha.6`. A caret on a prerelease permits everything below
4.0.0, so consumers resolved 3.1.x, where the collections internals were
restructured into a re-export of `react-aria/dist/private/collections`. Under
those versions `Table`'s head/body nodes reach the collection renderer without
the `render` it calls, and any `Table` crashes on mount with
`TypeError: node.render is not a function` — including this package's own
`Table.spec.tsx`, which passes here only because the lockfile holds
3.0.0-beta.1.

Verified against a consumer app across every published version:

| `@react-aria/collections` | `Table` |
| -------------------------- | ------------------------------- |
| 3.0.0-beta.1 | works |
| 3.0.0, 3.0.3, 3.1.0, 3.1.1 | `node.render is not a function` |

An exact pin is a stopgap: it freezes the package on a prerelease. The real fix
is migrating `Table` to the post-3.0 collections API, which should be tracked
separately. Until then the declared range should at least match what the code
supports, so consumers don't silently resolve a broken tree.

### Patch Changes

- Mark the `ComboBox` listbox popover non-modal, so its input stays focusable on
current `react-aria`.

`ComboBoxField` provided `PopoverContext` without `isNonModal`, so `usePopover`
took its modal branch. That branch used to hide the rest of the page with
`aria-hidden`, which is inert-in-name-only — the trigger's own text input stayed
focusable, so the omission was invisible. Current `@react-aria/overlays` changed
it to `ariaHideOutside([popover], { shouldUseInert: true })`, which applies the
real `inert` attribute to everything outside the popover. That includes the
input that opened the listbox, so the ComboBox becomes impossible to focus or
type into as soon as the menu can open. React Aria Components sets `isNonModal`
on its own ComboBox popover for this reason.

Verified in headless Chromium against a consumer app with no version overrides
(`@react-aria/combobox` 3.16.1, `react-stately` 3.49.0), clicking the input and
typing:

| `menuTrigger` | before | after |
| --------------------------- | ------------------------ | ---------------------- |
| `manual` (menu never opens) | focuses, accepts input | focuses, accepts input |
| `input` (the default) | dead — no focus, no text | focuses, accepts input |
| `focus` | dead — no focus, no text | focuses, accepts input |

Unaffected by the selection props used: `selectedKey`/`onSelectionChange`,
`value`/`onChange`, and passing neither all behaved identically.

This is what forced consumers to pin the whole `@react-aria/*` tree via
`overrides` to keep `ComboBox` working. `Menu`, `DatePicker` and `Modal` were
already fine on current versions; `Select`/`MultiSelect` are intentionally left
modal, since their trigger is a button and focus moves into the listbox.

- Declare eight react-aria/react-stately packages the library imports but never
listed as dependencies. They resolved only because this repo's lockfile happened
to hoist them via `react-stately@3.36.1`; consumers whose tree resolves
differently get unresolvable imports.

Imported at runtime by `dist/esm`:

- `@react-aria/selection`
- `@react-stately/form`
- `@react-stately/list`
- `@react-stately/overlays`

Referenced by the emitted `dist/types` (so consumers need them to typecheck —
`@react-types/shared` appears in 25 declaration files):

- `@react-types/button`
- `@react-types/combobox`
- `@react-types/grid`
- `@react-types/shared`

`react-stately` dropped its `@react-stately/form` dependency after 3.36.1, so on
a current resolve `MultiSelectField/useMultiSelectState` fails outright with
`Failed to resolve import "@react-stately/form"`. In a downstream app this also
manifested as `CustomSelect` rendering its trigger but never opening; declaring
these fixes it with no version pinning required.

## 3.7.3

### Patch Changes
Expand Down
12 changes: 10 additions & 2 deletions packages/atomic-elements/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@atomicjolt/atomic-elements",
"version": "3.7.3",
"version": "3.8.0",
"sideEffects": false,
"module": "dist/esm/index.js",
"main": "dist/cjs/index.js",
Expand All @@ -21,7 +21,7 @@
"@react-aria/button": "^3.9.5",
"@react-aria/calendar": "^3.5.8",
"@react-aria/checkbox": "^3.14.3",
"@react-aria/collections": "^3.0.0-alpha.6",
"@react-aria/collections": "3.0.0-beta.1",
"@react-aria/combobox": "^3.9.1",
"@react-aria/datepicker": "^3.10.1",
"@react-aria/dialog": "^3.5.14",
Expand All @@ -42,6 +42,7 @@
"@react-aria/radio": "^3.10.4",
"@react-aria/searchfield": "^3.7.5",
"@react-aria/select": "^3.14.5",
"@react-aria/selection": "^3.23.1",
"@react-aria/separator": "^3.3.13",
"@react-aria/switch": "^3.6.4",
"@react-aria/table": "^3.14.1",
Expand All @@ -52,8 +53,15 @@
"@react-aria/utils": "^3.24.1",
"@react-aria/visually-hidden": "^3.8.12",
"@react-stately/flags": "^3.0.3",
"@react-stately/form": "^3.1.2",
"@react-stately/list": "^3.12.0",
"@react-stately/overlays": "^3.6.14",
"@react-stately/table": "^3.13.0",
"@react-stately/utils": "^3.10.1",
"@react-types/button": "^3.11.0",
"@react-types/combobox": "^3.13.3",
"@react-types/grid": "^3.3.0",
"@react-types/shared": "^3.28.0",
"classnames": "^2.3.1",
"react-stately": "^3.34.0",
"react-transition-group": "^4.4.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { describe, test, expect } from "vitest";
import { render } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ComboBox } from ".";
import { Item } from "../../Collection";

Expand All @@ -24,3 +25,35 @@ describe("matches snapshots", () => {
expect(result.asFragment()).toMatchSnapshot();
});
});

describe("the open listbox", () => {
// A ComboBox keeps focus in its own input while the listbox is open, so its
// popover has to be non-modal. `usePopover`'s modal branch calls
// `ariaHideOutside(..., { shouldUseInert: true })`, which marks everything
// outside the popover `inert` — including the input that opened it, leaving
// it impossible to focus or type in.
test("leaves the input interactive rather than inert", async () => {
const user = userEvent.setup();
render(
<ComboBox label="Terms">
<Item key="fall" id="fall">
Fall
</Item>
<Item key="spring" id="spring">
Spring
</Item>
</ComboBox>
);

const input = screen.getByRole("combobox");
await user.click(input);
await user.keyboard("Fa");

// `hidden: true` because `ariaHideOutside` marks the surrounding nodes
// aria-hidden, which the accessible queries otherwise filter out.
expect(screen.getByRole("listbox", { hidden: true })).toBeInTheDocument();
expect(input.closest("[inert]")).toBeNull();
expect(document.activeElement).toBe(input);
expect(input).toHaveValue("Fa");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ function ComboBoxFieldInner<T extends object>(
triggerRef: inputWrapperRef.current ? inputWrapperRef : inputRef,
isOpen: state.isOpen,
variant: "listbox",
// A ComboBox keeps focus in its own text input while the listbox
// is open, so its popover must not be modal. `usePopover`'s modal
// branch calls `ariaHideOutside(..., { shouldUseInert: true })`,
// which marks everything outside the popover `inert` — including
// the input that opened it, leaving it impossible to focus or
// type in. React Aria Components sets this on ComboBox popovers
// for the same reason.
isNonModal: true,
},
],
[ListBoxContext.Provider, listBoxProps],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { render } from "@testing-library/react";
import { parseDate } from "@internationalized/date";
import { DateField } from ".";
import { Label, Message, ErrorMessage, ComboInput } from "../";
import { freezeTime } from "../../../test/freezeTime";

describe("DateField", () => {
describe("matches snapshots", () => {
freezeTime();

test("basic composition", () => {
const result = render(
<DateField>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ exports[`DateField > matches snapshots > basic composition 1`] = `
aria-labelledby="react-aria-:r5: react-aria-:r1:"
aria-valuemax="12"
aria-valuemin="1"
aria-valuenow="6"
aria-valuenow="3"
aria-valuetext="Empty"
autocorrect="off"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
Expand All @@ -51,9 +51,9 @@ exports[`DateField > matches snapshots > basic composition 1`] = `
<div
aria-label="day, "
aria-labelledby="react-aria-:r9: react-aria-:r1:"
aria-valuemax="30"
aria-valuemax="31"
aria-valuemin="1"
aria-valuenow="18"
aria-valuenow="15"
aria-valuetext="Empty"
autocorrect="off"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
Expand All @@ -80,7 +80,7 @@ exports[`DateField > matches snapshots > basic composition 1`] = `
aria-labelledby="react-aria-:rd: react-aria-:r1:"
aria-valuemax="9999"
aria-valuemin="1"
aria-valuenow="2026"
aria-valuenow="2024"
aria-valuetext="Empty"
autocorrect="off"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
Expand Down Expand Up @@ -131,7 +131,7 @@ exports[`DateField > matches snapshots > disabled state 1`] = `
aria-labelledby="react-aria-:r1i: react-aria-:r1e:"
aria-valuemax="12"
aria-valuemin="1"
aria-valuenow="6"
aria-valuenow="3"
aria-valuetext="Empty"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
contenteditable="false"
Expand All @@ -152,9 +152,9 @@ exports[`DateField > matches snapshots > disabled state 1`] = `
aria-disabled="true"
aria-label="day, "
aria-labelledby="react-aria-:r1m: react-aria-:r1e:"
aria-valuemax="30"
aria-valuemax="31"
aria-valuemin="1"
aria-valuenow="18"
aria-valuenow="15"
aria-valuetext="Empty"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
contenteditable="false"
Expand All @@ -177,7 +177,7 @@ exports[`DateField > matches snapshots > disabled state 1`] = `
aria-labelledby="react-aria-:r1q: react-aria-:r1e:"
aria-valuemax="9999"
aria-valuemin="1"
aria-valuenow="2026"
aria-valuenow="2024"
aria-valuetext="Empty"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
contenteditable="false"
Expand Down Expand Up @@ -222,7 +222,7 @@ exports[`DateField > matches snapshots > required state 1`] = `
aria-required="true"
aria-valuemax="12"
aria-valuemin="1"
aria-valuenow="6"
aria-valuenow="3"
aria-valuetext="Empty"
autocorrect="off"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
Expand All @@ -248,9 +248,9 @@ exports[`DateField > matches snapshots > required state 1`] = `
aria-label="day, "
aria-labelledby="react-aria-:r25: react-aria-:r1t:"
aria-required="true"
aria-valuemax="30"
aria-valuemax="31"
aria-valuemin="1"
aria-valuenow="18"
aria-valuenow="15"
aria-valuetext="Empty"
autocorrect="off"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
Expand Down Expand Up @@ -278,7 +278,7 @@ exports[`DateField > matches snapshots > required state 1`] = `
aria-required="true"
aria-valuemax="9999"
aria-valuemin="1"
aria-valuenow="2026"
aria-valuenow="2024"
aria-valuetext="Empty"
autocorrect="off"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
Expand Down Expand Up @@ -326,7 +326,7 @@ exports[`DateField > matches snapshots > with granularity hour 1`] = `
aria-labelledby="react-aria-:r2g: react-aria-:r2c:"
aria-valuemax="12"
aria-valuemin="1"
aria-valuenow="6"
aria-valuenow="3"
aria-valuetext="Empty"
autocorrect="off"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
Expand All @@ -351,9 +351,9 @@ exports[`DateField > matches snapshots > with granularity hour 1`] = `
<div
aria-label="day, "
aria-labelledby="react-aria-:r2k: react-aria-:r2c:"
aria-valuemax="30"
aria-valuemax="31"
aria-valuemin="1"
aria-valuenow="18"
aria-valuenow="15"
aria-valuetext="Empty"
autocorrect="off"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
Expand All @@ -380,7 +380,7 @@ exports[`DateField > matches snapshots > with granularity hour 1`] = `
aria-labelledby="react-aria-:r2o: react-aria-:r2c:"
aria-valuemax="9999"
aria-valuemin="1"
aria-valuenow="2026"
aria-valuenow="2024"
aria-valuetext="Empty"
autocorrect="off"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
Expand Down Expand Up @@ -503,7 +503,7 @@ exports[`DateField > matches snapshots > with message and error 1`] = `
aria-labelledby="react-aria-:r13: react-aria-:rv:"
aria-valuemax="12"
aria-valuemin="1"
aria-valuenow="6"
aria-valuenow="3"
aria-valuetext="Empty"
autocorrect="off"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
Expand All @@ -530,9 +530,9 @@ exports[`DateField > matches snapshots > with message and error 1`] = `
aria-invalid="true"
aria-label="day, "
aria-labelledby="react-aria-:r17: react-aria-:rv:"
aria-valuemax="30"
aria-valuemax="31"
aria-valuemin="1"
aria-valuenow="18"
aria-valuenow="15"
aria-valuetext="Empty"
autocorrect="off"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
Expand Down Expand Up @@ -561,7 +561,7 @@ exports[`DateField > matches snapshots > with message and error 1`] = `
aria-labelledby="react-aria-:r1b: react-aria-:rv:"
aria-valuemax="9999"
aria-valuemin="1"
aria-valuenow="2026"
aria-valuenow="2024"
aria-valuetext="Empty"
autocorrect="off"
class="sc-iCECmn cMzUyD aje-input__segment placeholder"
Expand Down
Loading
Loading