@unofficialbox/box-open-elements-react 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,21 @@ The core package stays framework-agnostic. This adapter is a thin layer that:
6
6
 
7
7
  1. imports the automatically registered custom element
8
8
  2. syncs React props onto element **properties** (not fragile attribute stringification)
9
- 3. forwards refs and DOM events
9
+ 3. forwards refs, and binds event callbacks directly to the element
10
+
11
+ Callbacks an adapter **declares** receive the **native** DOM event, not a React
12
+ `SyntheticEvent`. They are registered with `addEventListener` on the custom
13
+ element so the listener travels with it: React delegates from its root
14
+ container, so a node relocated out of that container silently stops receiving
15
+ delegated handlers. `Button` declares its own `onClick`, typed as
16
+ `(event: MouseEvent) => void`, for exactly that reason.
17
+
18
+ Props an adapter does not declare — including React's `onClick` on `Select`,
19
+ `TextField` and `Dialog` — are forwarded as ordinary host props and stay
20
+ delegated. That is fine unless something moves the node out of the React root,
21
+ and it is React's delegation model rather than these components': a plain
22
+ `<div onClick>` in a relocated subtree behaves the same. (`box-drawer` used to
23
+ relocate its subtree and no longer does — it uses the top layer.)
10
24
 
11
25
  See [`docs/integration/react.md`](../../docs/integration/react.md) for the React
12
26
  boundary and the [framework adapter tracker](../../docs/integration/framework-adapters.md)
@@ -43,27 +57,27 @@ export function SaveAction() {
43
57
  }
44
58
  ```
45
59
 
46
- The release-candidate surface also includes a controlled `Dialog` wrapper and
60
+ The supported surface also includes a controlled `Dialog` wrapper and
47
61
  `useExplorerSelectionController`, which subscribes React to the existing
48
62
  headless selection controller without duplicating its state.
49
63
 
50
64
  ## Status
51
65
 
52
- **Release candidate** — `Button`, `TextField`, `Select`, and `Dialog` prove native and
53
- composed events, value and structured property synchronization, latest callback
54
- routing, forwarded refs, controlled overlay focus behavior, and server-safe host
55
- rendering. `useExplorerSelectionController` proves headless controller
56
- composition. Package exports, version contracts, the Next.js hydration fixture,
57
- and CI validation are release-ready. Supported status follows the first public
58
- npm publication and a clean registry-install verification. React releases in
59
- lockstep with the Angular, Vue, and Svelte adapters under `adapters-vX.Y.Z`.
66
+ **Supported** as of `0.7.0` — `Button`, `TextField`, `Select`, and `Dialog` prove
67
+ native and composed events, value and structured property synchronization, latest
68
+ callback routing, forwarded refs, controlled overlay focus behavior, and
69
+ server-safe host rendering. `useExplorerSelectionController` proves headless
70
+ controller composition. The two conditions this section used to defer on are met:
71
+ the first public npm publication happened at `0.7.0`, and a clean registry install
72
+ resolves the package and loads its exports. React releases in lockstep with the
73
+ Angular, Vue, and Svelte adapters under `adapters-vX.Y.Z`, at the core's version.
60
74
 
61
75
  ## Supported versions
62
76
 
63
77
  | Dependency | Contract |
64
78
  | --- | --- |
65
79
  | React / React DOM | `^19.0.0` |
66
- | `@unofficialbox/box-open-elements` | `^0.5.0` |
80
+ | `@unofficialbox/box-open-elements` | `^0.9.0` |
67
81
  | Node.js for SSR | `>=20.9.0` |
68
82
  | Next.js validation host | `16.2.12` |
69
83
 
package/dist/button.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { MouseEventHandler } from "react";
2
1
  import { Button as ButtonElement } from "@unofficialbox/box-open-elements/button";
3
2
  import { type WebComponentProps } from "./create-web-component.js";
4
- export type ButtonProps = WebComponentProps & {
3
+ import type { NativeEventHandler } from "./events.js";
4
+ export type ButtonProps = Omit<WebComponentProps, "onClick"> & {
5
5
  /** Button label text (maps to the `label` property / attribute). */
6
6
  label?: string;
7
7
  /** Visual tone: `primary` (default), `neutral`, `danger`. */
@@ -9,16 +9,26 @@ export type ButtonProps = WebComponentProps & {
9
9
  /** Control size: `small`, `medium` (default), `large`. */
10
10
  size?: string;
11
11
  disabled?: boolean;
12
- onClick?: MouseEventHandler<ButtonElement>;
12
+ /**
13
+ * Click callback, bound directly to the element.
14
+ *
15
+ * Receives a **native** `MouseEvent`, not a React `SyntheticEvent`. See
16
+ * `NativeEventHandler` for why the binding is direct.
17
+ */
18
+ onClick?: NativeEventHandler<ButtonElement, MouseEvent>;
13
19
  };
14
20
  /**
15
21
  * React wrapper for `<box-button>`. Registers the custom element on first render
16
22
  * and syncs props as element properties for the supported React 19 contract.
23
+ *
24
+ * `click` is bound through the factory's `events` map rather than left to
25
+ * React's `onClick`. React delegates from its root container, so a button that
26
+ * has been relocated out of it — every button inside a `box-drawer`, which
27
+ * portals to `document.body` on open — never sees a delegated event and the
28
+ * callback silently does nothing. A listener on the element travels with it.
29
+ * The other adapters already bind this way; Button was the one that didn't.
17
30
  */
18
- export declare const Button: import("react").ForwardRefExoticComponent<{
19
- className?: string;
20
- style?: import("react").CSSProperties;
21
- } & Omit<import("react").HTMLAttributes<HTMLElement>, "className" | "style" | "children" | "onCancel"> & {
31
+ export declare const Button: import("react").ForwardRefExoticComponent<Omit<WebComponentProps, "onClick"> & {
22
32
  /** Button label text (maps to the `label` property / attribute). */
23
33
  label?: string;
24
34
  /** Visual tone: `primary` (default), `neutral`, `danger`. */
@@ -26,5 +36,11 @@ export declare const Button: import("react").ForwardRefExoticComponent<{
26
36
  /** Control size: `small`, `medium` (default), `large`. */
27
37
  size?: string;
28
38
  disabled?: boolean;
29
- onClick?: MouseEventHandler<ButtonElement>;
39
+ /**
40
+ * Click callback, bound directly to the element.
41
+ *
42
+ * Receives a **native** `MouseEvent`, not a React `SyntheticEvent`. See
43
+ * `NativeEventHandler` for why the binding is direct.
44
+ */
45
+ onClick?: NativeEventHandler<ButtonElement, MouseEvent>;
30
46
  } & import("react").RefAttributes<ButtonElement>>;
package/dist/button.js CHANGED
@@ -4,11 +4,19 @@ ButtonElement.register();
4
4
  /**
5
5
  * React wrapper for `<box-button>`. Registers the custom element on first render
6
6
  * and syncs props as element properties for the supported React 19 contract.
7
+ *
8
+ * `click` is bound through the factory's `events` map rather than left to
9
+ * React's `onClick`. React delegates from its root container, so a button that
10
+ * has been relocated out of it — every button inside a `box-drawer`, which
11
+ * portals to `document.body` on open — never sees a delegated event and the
12
+ * callback silently does nothing. A listener on the element travels with it.
13
+ * The other adapters already bind this way; Button was the one that didn't.
7
14
  */
8
15
  export const Button = createWebComponent({
9
16
  tagName: "box-button",
10
17
  displayName: "Button",
11
18
  propertyNames: ["label", "tone", "size", "disabled"],
19
+ events: [{ propName: "onClick", eventName: "click" }],
12
20
  sync: (element, props) => {
13
21
  if (props.label !== undefined) {
14
22
  element.label = props.label;
@@ -1,9 +1,45 @@
1
1
  import { type CSSProperties, type HTMLAttributes } from "react";
2
+ /**
3
+ * Host props an adapter accepts.
4
+ *
5
+ * `onCancel` is omitted because `Dialog` redeclares it with a native-event
6
+ * signature, and an intersection of two function types is an overload that no
7
+ * single handler satisfies.
8
+ *
9
+ * React's `onClick` stays. It is delegated from the React root container, which
10
+ * means it does **not** fire for an element that has relocated outside that
11
+ * container — every host inside an open `box-drawer`, which portals its subtree
12
+ * to `document.body`. That is a real trap, but it is React's trap and it catches
13
+ * a plain `<div onClick>` in a drawer just the same; removing the prop from
14
+ * three adapters would not fix the class of bug, only make those three behave
15
+ * unlike every other element in the tree while breaking handlers that work
16
+ * perfectly well outside overlays. An adapter that needs a click callback to
17
+ * survive relocation declares its own and binds through `events` below, as
18
+ * `Button` does.
19
+ */
2
20
  export type WebComponentProps = {
3
21
  className?: string;
4
22
  style?: CSSProperties;
5
23
  } & Omit<HTMLAttributes<HTMLElement>, "className" | "style" | "children" | "onCancel">;
6
- type CreateWebComponentOptions<E extends HTMLElement, P extends WebComponentProps> = {
24
+ /**
25
+ * What the factory itself requires of an adapter's props.
26
+ *
27
+ * Deliberately narrower than `WebComponentProps`: the factory reads `className`
28
+ * and `style` and spreads the rest, so those two are the whole requirement.
29
+ * Constraining to the full host-prop type instead would forbid an adapter from
30
+ * *narrowing* a host prop — `Button` redeclaring `onClick` as a native-event
31
+ * handler is not assignable to React's `MouseEventHandler` (parameters are
32
+ * contravariant, and a `SyntheticEvent` is not a `MouseEvent`), so the whole
33
+ * prop would have to come off the base type for every adapter to keep one
34
+ * adapter's narrowing legal. Each adapter still builds its props from
35
+ * `WebComponentProps`; the constraint just stops one adapter's choice from
36
+ * dictating the shared type.
37
+ */
38
+ type AdapterHostProps = {
39
+ className?: string;
40
+ style?: CSSProperties;
41
+ };
42
+ type CreateWebComponentOptions<E extends HTMLElement, P extends AdapterHostProps> = {
7
43
  tagName: string;
8
44
  /** Sync React props onto the custom element instance (prefer properties over attributes). */
9
45
  sync: (element: E, props: P) => void;
@@ -20,5 +56,5 @@ type CreateWebComponentOptions<E extends HTMLElement, P extends WebComponentProp
20
56
  * Thin React adapter factory for a box-open-elements custom element.
21
57
  * Defines the element once, syncs props via properties, and forwards refs/events.
22
58
  */
23
- export declare const createWebComponent: <E extends HTMLElement, P extends WebComponentProps>(options: CreateWebComponentOptions<E, P>) => import("react").ForwardRefExoticComponent<import("react").PropsWithoutRef<P> & import("react").RefAttributes<E>>;
59
+ export declare const createWebComponent: <E extends HTMLElement, P extends AdapterHostProps>(options: CreateWebComponentOptions<E, P>) => import("react").ForwardRefExoticComponent<import("react").PropsWithoutRef<P> & import("react").RefAttributes<E>>;
24
60
  export {};
package/dist/events.d.ts CHANGED
@@ -4,3 +4,19 @@ export type CustomEventHandler<E extends HTMLElement, Detail> = (event: CustomEv
4
4
  export type ValueChangedDetail = {
5
5
  value: string;
6
6
  };
7
+ /**
8
+ * A callback bound with `addEventListener`, receiving the **native** event.
9
+ *
10
+ * Not a React `SyntheticEvent`. Adapter callbacks are registered directly on
11
+ * the custom element rather than routed through React's delegation, so what
12
+ * arrives is the real DOM event — `event.nativeEvent` does not exist on it, and
13
+ * `stopPropagation` acts on the real tree.
14
+ *
15
+ * That indirection is not a style choice. React delegates from the root
16
+ * container, and an element that relocates itself outside that container —
17
+ * `box-drawer` portals to `document.body` when it opens — stops receiving
18
+ * delegated events entirely. A listener on the element itself travels with it.
19
+ */
20
+ export type NativeEventHandler<E extends HTMLElement, Ev extends Event = Event> = (event: Ev & {
21
+ currentTarget: E;
22
+ }) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unofficialbox/box-open-elements-react",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Optional React wrappers for box-open-elements Web Components. Thin adapters — core stays framework-agnostic.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://unofficialbox.github.io/box-open-elements",
@@ -37,7 +37,7 @@
37
37
  "node": ">=20.9.0"
38
38
  },
39
39
  "peerDependencies": {
40
- "@unofficialbox/box-open-elements": "^0.7.0",
40
+ "@unofficialbox/box-open-elements": "^0.9.0",
41
41
  "react": "^19.0.0",
42
42
  "react-dom": "^19.0.0"
43
43
  },