haze-ui 1.16.0 → 1.17.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
@@ -190,7 +190,7 @@ render-prop are mutually exclusive.
190
190
  - With a typed form, `validate`'s value argument is the field's actual
191
191
  type (`PathValueOf<TValues, P>`), not `any`.
192
192
 
193
- #### `input`: declarative binding for haze-ui cores (typed prop forwarding)
193
+ #### `input`: declarative binding for cores and raw DOM controls (typed prop forwarding)
194
194
 
195
195
  The ergonomic form for the controlled cores — pass the component and the
196
196
  rest of the JSX goes straight to it, type-checked against its own props:
@@ -243,6 +243,45 @@ checkbox-style core pairs with `valueToProps`). The differences from
243
243
  own `label`) is unreachable through `input` — use the render-prop or
244
244
  `as`/`asProps` for it.
245
245
 
246
+ `input` also takes raw DOM bindings — no core required. The two raw
247
+ forms are explicit about their `eventToValue` adapter, so the value
248
+ channel is never guessed:
249
+
250
+ ```jsx
251
+ // a native form element: the binding pairs the tag with its adapter,
252
+ // and the rest of the JSX is type-checked against that element's own
253
+ // HTML attributes (rows on a textarea, options as a select's children)
254
+ <FormItem
255
+ form={form}
256
+ name="bio"
257
+ label="Bio"
258
+ input={{element: 'textarea', eventToValue: (e) => e.target.value}}
259
+ rows={4}
260
+ />
261
+
262
+ // a DOM-element-shaped component: the top-level eventToValue is the
263
+ // explicit opt-in from plain-value (core) to event-emitting (raw)
264
+ <FormItem
265
+ form={form}
266
+ name="email"
267
+ label="Email"
268
+ input={NativeInput}
269
+ eventToValue={(e) => e.target.value}
270
+ />
271
+ ```
272
+
273
+ - The element binding accepts `'input' | 'textarea' | 'select'` and
274
+ **requires** its `eventToValue` — `input={{element: 'input'}}` without
275
+ the adapter is a compile error (at runtime an untyped caller that
276
+ skips it still gets `e.target.value`, the DOM contract, never an Event
277
+ in the store).
278
+ - The top-level `eventToValue` next to a component `input` switches that
279
+ binding to raw semantics, mirroring the `as` channel; forwarded props
280
+ still check against the component's own props.
281
+ - The same reserved-prop rule applies: `id`, `onBlur`, `onChange`,
282
+ `value`/`checked`, aria-*, and FormItem's own names are never
283
+ forwarded on the raw channel either.
284
+
246
285
  #### `mode`: per-field validation timing (react-f0rm ≥ 0.6)
247
286
 
248
287
  Pass `mode` to validate one field on its own schedule instead of the
@@ -15,9 +15,9 @@ function c({ form: c, name: l, label: u, validate: d, mode: f, validateDebounce:
15
15
  validateDebounce: p,
16
16
  delayError: m,
17
17
  rules: h
18
- }), j = A.length > 0, M = x ?? ((e) => e);
18
+ }), j = A.length > 0, M = b && typeof b == "object" && "element" in b ? b : void 0, N = M?.eventToValue ?? x ?? (M ? (e) => e.target?.value : (e) => e);
19
19
  if (b && typeof C == "function") throw Error("FormItem: `input` and the render-prop `children` are mutually exclusive — the input component is wired declaratively; remove the render-prop.");
20
- let N = b;
20
+ let P = M ? M.element : b;
21
21
  return /* @__PURE__ */ n("div", {
22
22
  className: e([a, g]),
23
23
  children: [
@@ -26,13 +26,13 @@ function c({ form: c, name: l, label: u, validate: d, mode: f, validateDebounce:
26
26
  className: e(o),
27
27
  children: u
28
28
  }),
29
- N ? /* @__PURE__ */ t(N, {
29
+ P ? /* @__PURE__ */ t(P, {
30
30
  ...w,
31
31
  id: T,
32
32
  "aria-invalid": j || void 0,
33
33
  "aria-describedby": j ? E : void 0,
34
34
  onBlur: k,
35
- onChange: (e) => O(M(e)),
35
+ onChange: (e) => O(N(e)),
36
36
  ...S ? S(D) : { value: D },
37
37
  children: C
38
38
  }) : v ? /* @__PURE__ */ t(v, {
@@ -40,7 +40,7 @@ function c({ form: c, name: l, label: u, validate: d, mode: f, validateDebounce:
40
40
  "aria-invalid": j || void 0,
41
41
  "aria-describedby": j ? E : void 0,
42
42
  onBlur: k,
43
- onChange: (e) => O(M(e)),
43
+ onChange: (e) => O(N(e)),
44
44
  ...y,
45
45
  ...S ? S(D) : { value: D }
46
46
  }) : C({
@@ -1,4 +1,4 @@
1
- import type { ComponentType, ReactNode } from 'react';
1
+ import type { ComponentPropsWithoutRef, ComponentType, ReactNode } from 'react';
2
2
  import type { FieldError, FieldPath, FieldRules, Name, ValidationMode, FormInstance, PathValueOf } from 'react-f0rm';
3
3
  /**
4
4
  * Field-level validator, structurally compatible with react-f0rm's
@@ -52,16 +52,36 @@ export type FormItemAsProps = {
52
52
  * value props, so `value`/`valueToProps` win conflicts — the same
53
53
  * precedence react-f0rm's `Field` uses. */
54
54
  asProps?: Record<string, any>;
55
- /** Converts what the `as` component passes to its `onChange` into the
56
- * field value. Defaults to identity — haze cores' `onChange` emits the
57
- * next plain value; pass `(e) => e.target.value` when `as` is a raw
58
- * DOM-element component. */
55
+ /** Converts what the control passes to its `onChange` into the field
56
+ * value. Defaults to identity — haze cores' `onChange` emits the next
57
+ * plain value; pass `(e) => e.target.value` when the control is
58
+ * DOM-element-shaped (a raw `as` component, or a raw component on the
59
+ * `input` channel, where passing the adapter is also the explicit
60
+ * opt-in from core to raw semantics). */
59
61
  eventToValue?: (e: any) => any;
60
62
  /** Derives the value props for the `as` component from the field value —
61
63
  * e.g. `(checked) => ({checked})` for `CheckboxCore`. Defaults to
62
64
  * passing `{value}`. */
63
65
  valueToProps?: (value: any) => Record<string, any>;
64
66
  };
67
+ /**
68
+ * The raw DOM form elements `FormItem`'s `input` channel accepts by tag
69
+ * name. Their `onChange` emits a DOM event, not a plain value, so the
70
+ * binding always carries its own `eventToValue` adapter — see
71
+ * `FormItemRawElementBinding`.
72
+ */
73
+ export type FormItemRawElement = 'input' | 'textarea' | 'select';
74
+ /**
75
+ * Raw-DOM binding for `FormItem`'s `input` channel — the explicit raw
76
+ * counterpart of passing a core component: a native form element by tag
77
+ * name, paired with the `eventToValue` adapter that extracts the next
78
+ * value from the DOM event. Every other JSX prop forwards to the
79
+ * element, type-checked against that element's own HTML attributes.
80
+ */
81
+ export type FormItemRawElementBinding<TRawElement extends FormItemRawElement = FormItemRawElement> = {
82
+ element: TRawElement;
83
+ eventToValue: (e: any) => any;
84
+ };
65
85
  /** FormItem's own, non-polymorphic props — everything the item itself
66
86
  * consumes regardless of how the control is bound (render-prop, `as`
67
87
  * or `input`). */
@@ -104,7 +124,7 @@ export type FormItemOwnProps<TValues extends Record<string, any> = any, P extend
104
124
  };
105
125
  /**
106
126
  * Props FormItem wires itself onto an `input`/`as` control — the bridge's
107
- * own contract. Used to keep them out of `input`'s forwarded rest props
127
+ * own contract. Used to keep them out of the forwarded rest props
108
128
  * (type level) and to document that they always win (runtime level):
109
129
  * passing one anyway is a compile error, never a silent override.
110
130
  */
@@ -119,35 +139,64 @@ type FormItemWiredProps = {
119
139
  'aria-invalid'?: unknown;
120
140
  'aria-describedby'?: unknown;
121
141
  };
122
- export type FormItemProps<TValues extends Record<string, any> = any, P extends FieldPath<TValues> | Name = Name, TInputProps extends Record<string, any> = Record<never, never>> = FormItemOwnProps<TValues, P> & FormItemAsProps & {
142
+ /**
143
+ * Prop names the bridge owns or wires — excluded from the forwarded rest
144
+ * props on every channel (core component, raw element, `as`).
145
+ */
146
+ type FormItemReservedProps<TValues extends Record<string, any> = any, P extends FieldPath<TValues> | Name = Name> = keyof FormItemOwnProps<TValues, P> | keyof FormItemAsProps | keyof FormItemWiredProps | 'input';
147
+ export type FormItemProps<TValues extends Record<string, any> = any, P extends FieldPath<TValues> | Name = Name, TInputProps extends Record<string, any> = Record<never, never>, TRawElement extends FormItemRawElement = never> = FormItemOwnProps<TValues, P> & Omit<FormItemAsProps, 'eventToValue'> & {
123
148
  /**
124
- * Declarative binding for haze-ui cores: pass the component
125
- * (`InputCore`, `TextareaCore`, `TagInputCore`, `SelectCore`,
126
- * `CheckboxCore`, …) and FormItem wires `id`, `aria-invalid`,
127
- * `aria-describedby`, `onBlur`, `onChange` and the value channel
128
- * itself. Every other prop — and JSX children (a `SelectCore`'s
129
- * `<option>`s) is forwarded to the component, fully type-checked
130
- * against its own props. Cores' `onChange` emits the next plain value
131
- * (identity `eventToValue`); checkbox-style controls pair with
132
- * `valueToProps={(checked) => ({checked})}`. Props FormItem owns or
133
- * wires (label, className, id, aria-*, onBlur, onChange, value,
134
- * checked, …) are reserved and cannot be forwarded use the
135
- * render-prop or `as`/`asProps` for a colliding control prop.
149
+ * Declarative binding for the field control, in two forms:
150
+ *
151
+ * - a component (`InputCore`, `TextareaCore`, `TagInputCore`,
152
+ * `SelectCore`, `CheckboxCore`, …, or any DOM-element-shaped
153
+ * component): FormItem wires `id`, `aria-invalid`,
154
+ * `aria-describedby`, `onBlur`, `onChange` and the value channel
155
+ * itself. Haze cores' `onChange` emits the next plain value
156
+ * (identity `eventToValue`); checkbox-style controls pair with
157
+ * `valueToProps={(checked) => ({checked})}`. A DOM-element-shaped
158
+ * component opts into raw semantics by passing `eventToValue`
159
+ * (e.g. `(e) => e.target.value`) the adapter's presence is the
160
+ * explicit switch from value-direct to event-emitting.
161
+ * - a `FormItemRawElementBinding` — `{element: 'input' |
162
+ * 'textarea' | 'select', eventToValue}` — for a native DOM
163
+ * element: same wiring, value extracted from the DOM event by the
164
+ * required adapter (the top-level `eventToValue` is not this
165
+ * form's slot).
166
+ *
167
+ * Every other prop — and JSX children (a `SelectCore`'s
168
+ * `<option>`s) — is forwarded to the control, fully type-checked
169
+ * against its own props (the component's, or the raw element's HTML
170
+ * attributes). Props FormItem owns or wires (label, className, id,
171
+ * aria-*, onBlur, onChange, value, checked, …) are reserved and
172
+ * cannot be forwarded — use the render-prop or `as`/`asProps` for a
173
+ * colliding control prop.
136
174
  */
137
- input?: ComponentType<TInputProps>;
138
- } & Omit<TInputProps, keyof FormItemOwnProps<TValues, P> | keyof FormItemAsProps | keyof FormItemWiredProps | 'input'> & ({
175
+ input?: ComponentType<TInputProps> | FormItemRawElementBinding<TRawElement>;
176
+ } & ([TRawElement] extends [never] ? Omit<TInputProps, FormItemReservedProps<TValues, P>> : Omit<ComponentPropsWithoutRef<TRawElement>, FormItemReservedProps<TValues, P>>) & ({
139
177
  as: ComponentType<any>;
140
178
  input?: never;
141
179
  children?: never;
180
+ eventToValue?: FormItemAsProps['eventToValue'];
142
181
  } | {
143
182
  as?: undefined;
144
183
  input: ComponentType<TInputProps>;
184
+ eventToValue?: FormItemAsProps['eventToValue'];
145
185
  /** JSX children forward to the control (SelectCore's options);
146
186
  * the render-prop form is mutually exclusive with `input`. */
147
187
  children?: 'children' extends keyof TInputProps ? TInputProps['children'] : undefined;
188
+ } | {
189
+ as?: undefined;
190
+ input: FormItemRawElementBinding<TRawElement>;
191
+ /** the raw element binding carries its own `eventToValue` — the
192
+ * top-level prop is not accepted next to it */
193
+ eventToValue?: never;
194
+ /** JSX children forward to the element (a select's options). */
195
+ children?: ReactNode;
148
196
  } | {
149
197
  as?: undefined;
150
198
  input?: undefined;
199
+ eventToValue?: never;
151
200
  children: (binding: FormItemBinding<TValues, P>) => ReactNode;
152
201
  });
153
202
  /**
@@ -206,9 +255,32 @@ export type FormItemProps<TValues extends Record<string, any> = any, P extends F
206
255
  * collides (CheckboxCore's `label`) needs the render-prop or
207
256
  * `as`/`asProps` channel.
208
257
  *
258
+ * `input` also accepts raw DOM bindings — no core required:
259
+ *
260
+ * ```tsx
261
+ * // a native element: the binding carries its own eventToValue, and the
262
+ * // rest of the JSX is type-checked against that element's attributes
263
+ * <FormItem
264
+ * form={form}
265
+ * name='email'
266
+ * input={{element: 'input', eventToValue: (e) => e.target.value}}
267
+ * type='email'
268
+ * placeholder='Email'
269
+ * />
270
+ *
271
+ * // a DOM-element-shaped component: the top-level eventToValue is the
272
+ * // explicit opt-in from plain-value (core) to event-emitting (raw)
273
+ * <FormItem
274
+ * form={form}
275
+ * name='email'
276
+ * input={NativeInput}
277
+ * eventToValue={(e) => e.target.value}
278
+ * />
279
+ * ```
280
+ *
209
281
  * When the field has errors, the first error's message is rendered into a
210
282
  * `<span id={errorId} role='alert'>` next to the control; with no errors
211
283
  * no extra element is rendered.
212
284
  */
213
- export default function FormItem<TValues extends Record<string, any> = any, P extends FieldPath<TValues> | Name = Name, TInputProps extends Record<string, any> = Record<never, never>>({ form, name, label, validate, mode, validateDebounce, delayError, rules, className, renderError, as: As, asProps, input: Input, eventToValue, valueToProps, children, ...inputProps }: FormItemProps<TValues, P, TInputProps>): import("react").JSX.Element;
285
+ export default function FormItem<TValues extends Record<string, any> = any, P extends FieldPath<TValues> | Name = Name, TInputProps extends Record<string, any> = Record<never, never>, TRawElement extends FormItemRawElement = never>({ form, name, label, validate, mode, validateDebounce, delayError, rules, className, renderError, as: As, asProps, input: Input, eventToValue, valueToProps, children, ...inputProps }: FormItemProps<TValues, P, TInputProps, TRawElement>): import("react").JSX.Element;
214
286
  export {};
@@ -1,3 +1,3 @@
1
1
  export { default as FormItem } from './FormItem';
2
- export type { FieldValidator, FormItemAsProps, FormItemBinding, FormItemOwnProps, FormItemProps } from './FormItem';
2
+ export type { FieldValidator, FormItemAsProps, FormItemBinding, FormItemOwnProps, FormItemProps, FormItemRawElement, FormItemRawElementBinding } from './FormItem';
3
3
  export type { FormInstance, PathValueOf } from 'react-f0rm';
@@ -182,6 +182,6 @@ export type { DiffViewerProps, DiffLine } from './components/DiffViewer';
182
182
  export { LogViewer } from './components/LogViewer';
183
183
  export type { LogViewerProps, LogEntry, LogLevel } from './components/LogViewer';
184
184
  export { FormItem } from './form';
185
- export type { FieldValidator, FormItemAsProps, FormItemBinding, FormItemOwnProps, FormItemProps, FormInstance, PathValueOf, } from './form';
185
+ export type { FieldValidator, FormItemAsProps, FormItemBinding, FormItemOwnProps, FormItemProps, FormItemRawElement, FormItemRawElementBinding, FormInstance, PathValueOf, } from './form';
186
186
  export { useControl } from 'react-use-control';
187
187
  export type { Control, ControlOrValue } from 'react-use-control';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haze-ui",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "type": "module",
5
5
  "description": "A React UI component library powered by react-use-control and Linaria",
6
6
  "main": "./dist/index.js",
@@ -52,7 +52,7 @@
52
52
  "peerDependencies": {
53
53
  "@linaria/core": "^7.0.0 || ^8.0.0",
54
54
  "react": "^19.0.0",
55
- "react-f0rm": ">=0.7.0 <0.9.0"
55
+ "react-f0rm": ">=0.7.0 <0.11.0"
56
56
  },
57
57
  "dependencies": {
58
58
  "react-use-control": "^1.5.0"