@conform-to/react 0.5.0 → 0.5.1

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
@@ -9,8 +9,12 @@
9
9
  - [useForm](#useform)
10
10
  - [useFieldset](#usefieldset)
11
11
  - [useFieldList](#usefieldlist)
12
+ - [useInputEvent](#useinputevent)
12
13
  - [useControlledInput](#usecontrolledinput)
13
14
  - [conform](#conform)
15
+ - [list](#list)
16
+ - [validate](#validate)
17
+ - [requestCommand](#requestcommand)
14
18
  - [getFormElements](#getformelements)
15
19
  - [hasError](#haserror)
16
20
  - [parse](#parse)
@@ -278,14 +282,63 @@ function Example() {
278
282
 
279
283
  ---
280
284
 
285
+ ### useInputEvent
286
+
287
+ It returns a ref object and a set of helpers that dispatch corresponding dom event.
288
+
289
+ ```tsx
290
+ import { useForm, useInputEvent } from '@conform-to/react';
291
+ import { Select, MenuItem } from '@mui/material';
292
+ import { useState, useRef } from 'react';
293
+
294
+ function MuiForm() {
295
+ const [form, { category }] = useForm();
296
+ const [value, setValue] = useState(category.config.defaultValue ?? '');
297
+ const [ref, control] = useInputEvent({
298
+ onReset: () => setValue(category.config.defaultValue ?? ''),
299
+ });
300
+ const inputRef = useRef<HTMLInputElement>(null);
301
+
302
+ return (
303
+ <form {...form.props}>
304
+ {/* Render a shadow input somewhere */}
305
+ <input
306
+ ref={ref}
307
+ {...conform.input(category.config, { hidden: true })}
308
+ onChange={(e) => setValue(e.target.value)}
309
+ onFocus={() => inputRef.current?.focus()}
310
+ />
311
+
312
+ {/* MUI Select is a controlled component */}
313
+ <TextField
314
+ label="Category"
315
+ inputRef={inputRef}
316
+ value={value}
317
+ onChange={control.change}
318
+ onBlur={control.blur}
319
+ select
320
+ >
321
+ <MenuItem value="">Please select</MenuItem>
322
+ <MenuItem value="a">Category A</MenuItem>
323
+ <MenuItem value="b">Category B</MenuItem>
324
+ <MenuItem value="c">Category C</MenuItem>
325
+ </TextField>
326
+ </form>
327
+ );
328
+ }
329
+ ```
330
+
331
+ ---
332
+
281
333
  ### useControlledInput
282
334
 
335
+ > This API is deprecated and replaced with the [useInputEvent](#useinputevent) hook.
336
+
283
337
  It returns the properties required to configure a shadow input for validation and helper to integrate it. This is particularly useful when [integrating custom input components](/docs/integrations.md#custom-input-component) like dropdown and datepicker.
284
338
 
285
339
  ```tsx
286
340
  import { useForm, useControlledInput } from '@conform-to/react';
287
341
  import { Select, MenuItem } from '@mui/material';
288
- import { useRef } from 'react';
289
342
 
290
343
  function MuiForm() {
291
344
  const [form, { category }] = useForm();
package/helpers.d.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  import type { FieldConfig } from '@conform-to/dom';
2
- import type { HTMLInputTypeAttribute } from 'react';
2
+ import type { CSSProperties, HTMLInputTypeAttribute } from 'react';
3
3
  interface FieldProps {
4
4
  id?: string;
5
5
  name: string;
6
6
  form?: string;
7
7
  required?: boolean;
8
8
  autoFocus?: boolean;
9
+ tabIndex?: number;
10
+ style?: CSSProperties;
9
11
  'aria-invalid': boolean;
10
12
  'aria-describedby'?: string;
13
+ 'aria-hidden'?: boolean;
11
14
  }
12
15
  interface InputProps<Schema> extends FieldProps {
13
16
  type?: HTMLInputTypeAttribute;
@@ -33,18 +36,21 @@ interface TextareaProps extends FieldProps {
33
36
  }
34
37
  declare type InputOptions = {
35
38
  type: 'checkbox' | 'radio';
39
+ hidden?: boolean;
36
40
  value?: string;
37
41
  } | {
38
- type: 'file';
39
- value?: never;
40
- } | {
41
- type?: Exclude<HTMLInputTypeAttribute, 'button' | 'submit' | 'hidden' | 'file'>;
42
+ type?: Exclude<HTMLInputTypeAttribute, 'button' | 'submit' | 'hidden'>;
43
+ hidden?: boolean;
42
44
  value?: never;
43
45
  };
44
46
  export declare function input<Schema extends File | File[]>(config: FieldConfig<Schema>, options: {
45
47
  type: 'file';
46
48
  }): InputProps<Schema>;
47
49
  export declare function input<Schema extends any>(config: FieldConfig<Schema>, options?: InputOptions): InputProps<Schema>;
48
- export declare function select<Schema>(config: FieldConfig<Schema>): SelectProps;
49
- export declare function textarea<Schema>(config: FieldConfig<Schema>): TextareaProps;
50
+ export declare function select<Schema>(config: FieldConfig<Schema>, options?: {
51
+ hidden?: boolean;
52
+ }): SelectProps;
53
+ export declare function textarea<Schema>(config: FieldConfig<Schema>, options?: {
54
+ hidden?: boolean;
55
+ }): TextareaProps;
50
56
  export {};
package/helpers.js CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ /**
6
+ * Style to make the input element visually hidden
7
+ * Based on the `sr-only` class from tailwindcss
8
+ */
9
+ var hiddenStyle = {
10
+ position: 'absolute',
11
+ width: '1px',
12
+ height: '1px',
13
+ padding: 0,
14
+ margin: '-1px',
15
+ overflow: 'hidden',
16
+ clip: 'rect(0,0,0,0)',
17
+ whiteSpace: 'nowrap',
18
+ border: 0
19
+ };
5
20
  function input(config) {
6
21
  var _config$initialError;
7
22
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -21,6 +36,11 @@ function input(config) {
21
36
  'aria-invalid': Boolean((_config$initialError = config.initialError) === null || _config$initialError === void 0 ? void 0 : _config$initialError.length),
22
37
  'aria-describedby': config.errorId
23
38
  };
39
+ if (options !== null && options !== void 0 && options.hidden) {
40
+ attributes.style = hiddenStyle;
41
+ attributes.tabIndex = -1;
42
+ attributes['aria-hidden'] = true;
43
+ }
24
44
  if (config.initialError && config.initialError.length > 0) {
25
45
  attributes.autoFocus = true;
26
46
  }
@@ -33,7 +53,7 @@ function input(config) {
33
53
  }
34
54
  return attributes;
35
55
  }
36
- function select(config) {
56
+ function select(config, options) {
37
57
  var _config$defaultValue, _config$initialError2;
38
58
  var attributes = {
39
59
  id: config.id,
@@ -45,12 +65,17 @@ function select(config) {
45
65
  'aria-invalid': Boolean((_config$initialError2 = config.initialError) === null || _config$initialError2 === void 0 ? void 0 : _config$initialError2.length),
46
66
  'aria-describedby': config.errorId
47
67
  };
68
+ if (options !== null && options !== void 0 && options.hidden) {
69
+ attributes.style = hiddenStyle;
70
+ attributes.tabIndex = -1;
71
+ attributes['aria-hidden'] = true;
72
+ }
48
73
  if (config.initialError && config.initialError.length > 0) {
49
74
  attributes.autoFocus = true;
50
75
  }
51
76
  return attributes;
52
77
  }
53
- function textarea(config) {
78
+ function textarea(config, options) {
54
79
  var _config$defaultValue2, _config$initialError3;
55
80
  var attributes = {
56
81
  id: config.id,
@@ -64,6 +89,11 @@ function textarea(config) {
64
89
  'aria-invalid': Boolean((_config$initialError3 = config.initialError) === null || _config$initialError3 === void 0 ? void 0 : _config$initialError3.length),
65
90
  'aria-describedby': config.errorId
66
91
  };
92
+ if (options !== null && options !== void 0 && options.hidden) {
93
+ attributes.style = hiddenStyle;
94
+ attributes.tabIndex = -1;
95
+ attributes['aria-hidden'] = true;
96
+ }
67
97
  if (config.initialError && config.initialError.length > 0) {
68
98
  attributes.autoFocus = true;
69
99
  }
package/hooks.d.ts CHANGED
@@ -136,7 +136,7 @@ export declare function useFieldList<Payload = any>(ref: RefObject<HTMLFormEleme
136
136
  interface ShadowInputProps extends InputHTMLAttributes<HTMLInputElement> {
137
137
  ref: RefObject<HTMLInputElement>;
138
138
  }
139
- interface InputControl<Element extends {
139
+ interface LegacyInputControl<Element extends {
140
140
  focus: () => void;
141
141
  }> {
142
142
  ref: RefObject<Element>;
@@ -154,9 +154,33 @@ interface InputControl<Element extends {
154
154
  * This is particular useful when integrating dropdown and datepicker whichs
155
155
  * introduces custom input mode.
156
156
  *
157
+ * @deprecated Please use the `useInputEvent` hook instead
157
158
  * @see https://conform.guide/api/react#usecontrolledinput
158
159
  */
159
160
  export declare function useControlledInput<Element extends {
160
161
  focus: () => void;
161
- } = HTMLInputElement, Schema extends Primitive = Primitive>(config: FieldConfig<Schema>): [ShadowInputProps, InputControl<Element>];
162
+ } = HTMLInputElement, Schema extends Primitive = Primitive>(config: FieldConfig<Schema>): [ShadowInputProps, LegacyInputControl<Element>];
163
+ interface InputControl {
164
+ change: (eventOrValue: {
165
+ target: {
166
+ value: string;
167
+ };
168
+ } | string) => void;
169
+ focus: () => void;
170
+ blur: () => void;
171
+ }
172
+ /**
173
+ * Returns a ref object and a set of helpers that dispatch corresponding dom event.
174
+ *
175
+ * @see https://conform.guide/api/react#useinputevent
176
+ */
177
+ export declare function useInputEvent<RefShape extends FieldElement = HTMLInputElement>(options?: {
178
+ onSubmit?: (event: SubmitEvent) => void;
179
+ onReset?: (event: Event) => void;
180
+ }): [RefObject<RefShape>, InputControl];
181
+ export declare function useInputEvent<RefShape extends Exclude<any, FieldElement>>(options: {
182
+ getElement: (ref: RefShape | null) => FieldElement | null | undefined;
183
+ onSubmit?: (event: SubmitEvent) => void;
184
+ onReset?: (event: Event) => void;
185
+ }): [RefObject<RefShape>, InputControl];
162
186
  export {};
package/hooks.js CHANGED
@@ -489,6 +489,7 @@ function useFieldList(ref, config) {
489
489
  * This is particular useful when integrating dropdown and datepicker whichs
490
490
  * introduces custom input mode.
491
491
  *
492
+ * @deprecated Please use the `useInputEvent` hook instead
492
493
  * @see https://conform.guide/api/react#usecontrolledinput
493
494
  */
494
495
  function useControlledInput(config) {
@@ -544,24 +545,13 @@ function useControlledInput(config) {
544
545
  }, []);
545
546
  return [_rollupPluginBabelHelpers.objectSpread2({
546
547
  ref,
547
- style: {
548
- position: 'absolute',
549
- width: '1px',
550
- height: '1px',
551
- padding: 0,
552
- margin: '-1px',
553
- overflow: 'hidden',
554
- clip: 'rect(0,0,0,0)',
555
- whiteSpace: 'nowrap',
556
- borderWidth: 0
557
- },
558
- tabIndex: -1,
559
- 'aria-hidden': true,
560
548
  onFocus() {
561
549
  var _inputRef$current;
562
550
  (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : _inputRef$current.focus();
563
551
  }
564
- }, helpers.input(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, config), uncontrolledState))), {
552
+ }, helpers.input(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, config), uncontrolledState), {
553
+ hidden: true
554
+ })), {
565
555
  ref: inputRef,
566
556
  value,
567
557
  onChange: handleChange,
@@ -570,7 +560,183 @@ function useControlledInput(config) {
570
560
  }];
571
561
  }
572
562
 
563
+ /**
564
+ * Triggering react custom change event
565
+ * Solution based on dom-testing-library
566
+ * @see https://github.com/facebook/react/issues/10135#issuecomment-401496776
567
+ * @see https://github.com/testing-library/dom-testing-library/blob/main/src/events.js#L104-L123
568
+ */
569
+ function setNativeValue(element, value) {
570
+ if (element.value === value) {
571
+ // It will not trigger a change event if `element.value` is the same as the set value
572
+ return;
573
+ }
574
+ var {
575
+ set: valueSetter
576
+ } = Object.getOwnPropertyDescriptor(element, 'value') || {};
577
+ var prototype = Object.getPrototypeOf(element);
578
+ var {
579
+ set: prototypeValueSetter
580
+ } = Object.getOwnPropertyDescriptor(prototype, 'value') || {};
581
+ if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
582
+ prototypeValueSetter.call(element, value);
583
+ } else {
584
+ if (valueSetter) {
585
+ valueSetter.call(element, value);
586
+ } else {
587
+ throw new Error('The given element does not have a value setter');
588
+ }
589
+ }
590
+ }
591
+
592
+ /**
593
+ * useLayoutEffect is client-only.
594
+ * This basically makes it a no-op on server
595
+ */
596
+ var useSafeLayoutEffect = typeof document === 'undefined' ? react.useEffect : react.useLayoutEffect;
597
+ function useInputEvent(options) {
598
+ var ref = react.useRef(null);
599
+ var optionsRef = react.useRef(options);
600
+ var changeDispatched = react.useRef(false);
601
+ var focusDispatched = react.useRef(false);
602
+ var blurDispatched = react.useRef(false);
603
+ useSafeLayoutEffect(() => {
604
+ optionsRef.current = options;
605
+ });
606
+ useSafeLayoutEffect(() => {
607
+ var getInputElement = () => {
608
+ var _optionsRef$current$g, _optionsRef$current, _optionsRef$current$g2;
609
+ return (_optionsRef$current$g = (_optionsRef$current = optionsRef.current) === null || _optionsRef$current === void 0 ? void 0 : (_optionsRef$current$g2 = _optionsRef$current.getElement) === null || _optionsRef$current$g2 === void 0 ? void 0 : _optionsRef$current$g2.call(_optionsRef$current, ref.current)) !== null && _optionsRef$current$g !== void 0 ? _optionsRef$current$g : ref.current;
610
+ };
611
+ var inputHandler = event => {
612
+ var input = getInputElement();
613
+ if (input && event.target === input) {
614
+ changeDispatched.current = true;
615
+ }
616
+ };
617
+ var focusHandler = event => {
618
+ var input = getInputElement();
619
+ if (input && event.target === input) {
620
+ focusDispatched.current = true;
621
+ }
622
+ };
623
+ var blurHandler = event => {
624
+ var input = getInputElement();
625
+ if (input && event.target === input) {
626
+ blurDispatched.current = true;
627
+ }
628
+ };
629
+ var submitHandler = event => {
630
+ var input = getInputElement();
631
+ if (input !== null && input !== void 0 && input.form && event.target === input.form) {
632
+ var _optionsRef$current2, _optionsRef$current2$;
633
+ (_optionsRef$current2 = optionsRef.current) === null || _optionsRef$current2 === void 0 ? void 0 : (_optionsRef$current2$ = _optionsRef$current2.onSubmit) === null || _optionsRef$current2$ === void 0 ? void 0 : _optionsRef$current2$.call(_optionsRef$current2, event);
634
+ }
635
+ };
636
+ var resetHandler = event => {
637
+ var input = getInputElement();
638
+ if (input !== null && input !== void 0 && input.form && event.target === input.form) {
639
+ var _optionsRef$current3, _optionsRef$current3$;
640
+ (_optionsRef$current3 = optionsRef.current) === null || _optionsRef$current3 === void 0 ? void 0 : (_optionsRef$current3$ = _optionsRef$current3.onReset) === null || _optionsRef$current3$ === void 0 ? void 0 : _optionsRef$current3$.call(_optionsRef$current3, event);
641
+ }
642
+ };
643
+ document.addEventListener('input', inputHandler, true);
644
+ document.addEventListener('focus', focusHandler, true);
645
+ document.addEventListener('blur', blurHandler, true);
646
+ document.addEventListener('submit', submitHandler);
647
+ document.addEventListener('reset', resetHandler);
648
+ return () => {
649
+ document.removeEventListener('input', inputHandler, true);
650
+ document.removeEventListener('focus', focusHandler, true);
651
+ document.removeEventListener('blur', blurHandler, true);
652
+ document.removeEventListener('submit', submitHandler);
653
+ document.removeEventListener('reset', resetHandler);
654
+ };
655
+ }, []);
656
+ var control = react.useMemo(() => {
657
+ var getInputElement = () => {
658
+ var _optionsRef$current$g3, _optionsRef$current4, _optionsRef$current4$;
659
+ return (_optionsRef$current$g3 = (_optionsRef$current4 = optionsRef.current) === null || _optionsRef$current4 === void 0 ? void 0 : (_optionsRef$current4$ = _optionsRef$current4.getElement) === null || _optionsRef$current4$ === void 0 ? void 0 : _optionsRef$current4$.call(_optionsRef$current4, ref.current)) !== null && _optionsRef$current$g3 !== void 0 ? _optionsRef$current$g3 : ref.current;
660
+ };
661
+ return {
662
+ change(eventOrValue) {
663
+ var input = getInputElement();
664
+ if (!input) {
665
+ console.warn('Missing input ref; No change-related events will be dispatched');
666
+ return;
667
+ }
668
+ if (changeDispatched.current) {
669
+ changeDispatched.current = false;
670
+ return;
671
+ }
672
+ var previousValue = input.value;
673
+ var nextValue = typeof eventOrValue === 'string' ? eventOrValue : eventOrValue.target.value;
674
+
675
+ // This make sure no event is dispatched on the first effect run
676
+ if (nextValue === previousValue) {
677
+ return;
678
+ }
679
+
680
+ // Dispatch beforeinput event before updating the input value
681
+ input.dispatchEvent(new Event('beforeinput', {
682
+ bubbles: true
683
+ }));
684
+ // Update the input value to trigger a change event
685
+ setNativeValue(input, nextValue);
686
+ // Dispatch input event with the updated input value
687
+ input.dispatchEvent(new InputEvent('input', {
688
+ bubbles: true
689
+ }));
690
+ // Reset the dispatched flag
691
+ changeDispatched.current = false;
692
+ },
693
+ focus() {
694
+ var input = getInputElement();
695
+ if (!input) {
696
+ console.warn('Missing input ref; No focus-related events will be dispatched');
697
+ return;
698
+ }
699
+ if (focusDispatched.current) {
700
+ focusDispatched.current = false;
701
+ return;
702
+ }
703
+ var focusinEvent = new FocusEvent('focusin', {
704
+ bubbles: true
705
+ });
706
+ var focusEvent = new FocusEvent('focus');
707
+ input.dispatchEvent(focusinEvent);
708
+ input.dispatchEvent(focusEvent);
709
+
710
+ // Reset the dispatched flag
711
+ focusDispatched.current = false;
712
+ },
713
+ blur() {
714
+ var input = getInputElement();
715
+ if (!input) {
716
+ console.warn('Missing input ref; No blur-related events will be dispatched');
717
+ return;
718
+ }
719
+ if (blurDispatched.current) {
720
+ blurDispatched.current = false;
721
+ return;
722
+ }
723
+ var focusoutEvent = new FocusEvent('focusout', {
724
+ bubbles: true
725
+ });
726
+ var blurEvent = new FocusEvent('blur');
727
+ input.dispatchEvent(focusoutEvent);
728
+ input.dispatchEvent(blurEvent);
729
+
730
+ // Reset the dispatched flag
731
+ blurDispatched.current = false;
732
+ }
733
+ };
734
+ }, []);
735
+ return [ref, control];
736
+ }
737
+
573
738
  exports.useControlledInput = useControlledInput;
574
739
  exports.useFieldList = useFieldList;
575
740
  exports.useFieldset = useFieldset;
576
741
  exports.useForm = useForm;
742
+ exports.useInputEvent = useInputEvent;
package/index.js CHANGED
@@ -44,4 +44,5 @@ exports.useControlledInput = hooks.useControlledInput;
44
44
  exports.useFieldList = hooks.useFieldList;
45
45
  exports.useFieldset = hooks.useFieldset;
46
46
  exports.useForm = hooks.useForm;
47
+ exports.useInputEvent = hooks.useInputEvent;
47
48
  exports.conform = helpers;
package/module/helpers.js CHANGED
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Style to make the input element visually hidden
3
+ * Based on the `sr-only` class from tailwindcss
4
+ */
5
+ var hiddenStyle = {
6
+ position: 'absolute',
7
+ width: '1px',
8
+ height: '1px',
9
+ padding: 0,
10
+ margin: '-1px',
11
+ overflow: 'hidden',
12
+ clip: 'rect(0,0,0,0)',
13
+ whiteSpace: 'nowrap',
14
+ border: 0
15
+ };
1
16
  function input(config) {
2
17
  var _config$initialError;
3
18
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -17,6 +32,11 @@ function input(config) {
17
32
  'aria-invalid': Boolean((_config$initialError = config.initialError) === null || _config$initialError === void 0 ? void 0 : _config$initialError.length),
18
33
  'aria-describedby': config.errorId
19
34
  };
35
+ if (options !== null && options !== void 0 && options.hidden) {
36
+ attributes.style = hiddenStyle;
37
+ attributes.tabIndex = -1;
38
+ attributes['aria-hidden'] = true;
39
+ }
20
40
  if (config.initialError && config.initialError.length > 0) {
21
41
  attributes.autoFocus = true;
22
42
  }
@@ -29,7 +49,7 @@ function input(config) {
29
49
  }
30
50
  return attributes;
31
51
  }
32
- function select(config) {
52
+ function select(config, options) {
33
53
  var _config$defaultValue, _config$initialError2;
34
54
  var attributes = {
35
55
  id: config.id,
@@ -41,12 +61,17 @@ function select(config) {
41
61
  'aria-invalid': Boolean((_config$initialError2 = config.initialError) === null || _config$initialError2 === void 0 ? void 0 : _config$initialError2.length),
42
62
  'aria-describedby': config.errorId
43
63
  };
64
+ if (options !== null && options !== void 0 && options.hidden) {
65
+ attributes.style = hiddenStyle;
66
+ attributes.tabIndex = -1;
67
+ attributes['aria-hidden'] = true;
68
+ }
44
69
  if (config.initialError && config.initialError.length > 0) {
45
70
  attributes.autoFocus = true;
46
71
  }
47
72
  return attributes;
48
73
  }
49
- function textarea(config) {
74
+ function textarea(config, options) {
50
75
  var _config$defaultValue2, _config$initialError3;
51
76
  var attributes = {
52
77
  id: config.id,
@@ -60,6 +85,11 @@ function textarea(config) {
60
85
  'aria-invalid': Boolean((_config$initialError3 = config.initialError) === null || _config$initialError3 === void 0 ? void 0 : _config$initialError3.length),
61
86
  'aria-describedby': config.errorId
62
87
  };
88
+ if (options !== null && options !== void 0 && options.hidden) {
89
+ attributes.style = hiddenStyle;
90
+ attributes.tabIndex = -1;
91
+ attributes['aria-hidden'] = true;
92
+ }
63
93
  if (config.initialError && config.initialError.length > 0) {
64
94
  attributes.autoFocus = true;
65
95
  }
package/module/hooks.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js';
2
2
  import { shouldValidate, reportSubmission, getFormData, parse, isFieldElement, hasError, getPaths, getName, requestCommand, validate, getFormElement, parseListCommand, updateList } from '@conform-to/dom';
3
- import { useRef, useState, useEffect } from 'react';
3
+ import { useRef, useState, useEffect, useMemo, useLayoutEffect } from 'react';
4
4
  import { input } from './helpers.js';
5
5
 
6
6
  /**
@@ -485,6 +485,7 @@ function useFieldList(ref, config) {
485
485
  * This is particular useful when integrating dropdown and datepicker whichs
486
486
  * introduces custom input mode.
487
487
  *
488
+ * @deprecated Please use the `useInputEvent` hook instead
488
489
  * @see https://conform.guide/api/react#usecontrolledinput
489
490
  */
490
491
  function useControlledInput(config) {
@@ -540,24 +541,13 @@ function useControlledInput(config) {
540
541
  }, []);
541
542
  return [_objectSpread2({
542
543
  ref,
543
- style: {
544
- position: 'absolute',
545
- width: '1px',
546
- height: '1px',
547
- padding: 0,
548
- margin: '-1px',
549
- overflow: 'hidden',
550
- clip: 'rect(0,0,0,0)',
551
- whiteSpace: 'nowrap',
552
- borderWidth: 0
553
- },
554
- tabIndex: -1,
555
- 'aria-hidden': true,
556
544
  onFocus() {
557
545
  var _inputRef$current;
558
546
  (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : _inputRef$current.focus();
559
547
  }
560
- }, input(_objectSpread2(_objectSpread2({}, config), uncontrolledState))), {
548
+ }, input(_objectSpread2(_objectSpread2({}, config), uncontrolledState), {
549
+ hidden: true
550
+ })), {
561
551
  ref: inputRef,
562
552
  value,
563
553
  onChange: handleChange,
@@ -566,4 +556,179 @@ function useControlledInput(config) {
566
556
  }];
567
557
  }
568
558
 
569
- export { useControlledInput, useFieldList, useFieldset, useForm };
559
+ /**
560
+ * Triggering react custom change event
561
+ * Solution based on dom-testing-library
562
+ * @see https://github.com/facebook/react/issues/10135#issuecomment-401496776
563
+ * @see https://github.com/testing-library/dom-testing-library/blob/main/src/events.js#L104-L123
564
+ */
565
+ function setNativeValue(element, value) {
566
+ if (element.value === value) {
567
+ // It will not trigger a change event if `element.value` is the same as the set value
568
+ return;
569
+ }
570
+ var {
571
+ set: valueSetter
572
+ } = Object.getOwnPropertyDescriptor(element, 'value') || {};
573
+ var prototype = Object.getPrototypeOf(element);
574
+ var {
575
+ set: prototypeValueSetter
576
+ } = Object.getOwnPropertyDescriptor(prototype, 'value') || {};
577
+ if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
578
+ prototypeValueSetter.call(element, value);
579
+ } else {
580
+ if (valueSetter) {
581
+ valueSetter.call(element, value);
582
+ } else {
583
+ throw new Error('The given element does not have a value setter');
584
+ }
585
+ }
586
+ }
587
+
588
+ /**
589
+ * useLayoutEffect is client-only.
590
+ * This basically makes it a no-op on server
591
+ */
592
+ var useSafeLayoutEffect = typeof document === 'undefined' ? useEffect : useLayoutEffect;
593
+ function useInputEvent(options) {
594
+ var ref = useRef(null);
595
+ var optionsRef = useRef(options);
596
+ var changeDispatched = useRef(false);
597
+ var focusDispatched = useRef(false);
598
+ var blurDispatched = useRef(false);
599
+ useSafeLayoutEffect(() => {
600
+ optionsRef.current = options;
601
+ });
602
+ useSafeLayoutEffect(() => {
603
+ var getInputElement = () => {
604
+ var _optionsRef$current$g, _optionsRef$current, _optionsRef$current$g2;
605
+ return (_optionsRef$current$g = (_optionsRef$current = optionsRef.current) === null || _optionsRef$current === void 0 ? void 0 : (_optionsRef$current$g2 = _optionsRef$current.getElement) === null || _optionsRef$current$g2 === void 0 ? void 0 : _optionsRef$current$g2.call(_optionsRef$current, ref.current)) !== null && _optionsRef$current$g !== void 0 ? _optionsRef$current$g : ref.current;
606
+ };
607
+ var inputHandler = event => {
608
+ var input = getInputElement();
609
+ if (input && event.target === input) {
610
+ changeDispatched.current = true;
611
+ }
612
+ };
613
+ var focusHandler = event => {
614
+ var input = getInputElement();
615
+ if (input && event.target === input) {
616
+ focusDispatched.current = true;
617
+ }
618
+ };
619
+ var blurHandler = event => {
620
+ var input = getInputElement();
621
+ if (input && event.target === input) {
622
+ blurDispatched.current = true;
623
+ }
624
+ };
625
+ var submitHandler = event => {
626
+ var input = getInputElement();
627
+ if (input !== null && input !== void 0 && input.form && event.target === input.form) {
628
+ var _optionsRef$current2, _optionsRef$current2$;
629
+ (_optionsRef$current2 = optionsRef.current) === null || _optionsRef$current2 === void 0 ? void 0 : (_optionsRef$current2$ = _optionsRef$current2.onSubmit) === null || _optionsRef$current2$ === void 0 ? void 0 : _optionsRef$current2$.call(_optionsRef$current2, event);
630
+ }
631
+ };
632
+ var resetHandler = event => {
633
+ var input = getInputElement();
634
+ if (input !== null && input !== void 0 && input.form && event.target === input.form) {
635
+ var _optionsRef$current3, _optionsRef$current3$;
636
+ (_optionsRef$current3 = optionsRef.current) === null || _optionsRef$current3 === void 0 ? void 0 : (_optionsRef$current3$ = _optionsRef$current3.onReset) === null || _optionsRef$current3$ === void 0 ? void 0 : _optionsRef$current3$.call(_optionsRef$current3, event);
637
+ }
638
+ };
639
+ document.addEventListener('input', inputHandler, true);
640
+ document.addEventListener('focus', focusHandler, true);
641
+ document.addEventListener('blur', blurHandler, true);
642
+ document.addEventListener('submit', submitHandler);
643
+ document.addEventListener('reset', resetHandler);
644
+ return () => {
645
+ document.removeEventListener('input', inputHandler, true);
646
+ document.removeEventListener('focus', focusHandler, true);
647
+ document.removeEventListener('blur', blurHandler, true);
648
+ document.removeEventListener('submit', submitHandler);
649
+ document.removeEventListener('reset', resetHandler);
650
+ };
651
+ }, []);
652
+ var control = useMemo(() => {
653
+ var getInputElement = () => {
654
+ var _optionsRef$current$g3, _optionsRef$current4, _optionsRef$current4$;
655
+ return (_optionsRef$current$g3 = (_optionsRef$current4 = optionsRef.current) === null || _optionsRef$current4 === void 0 ? void 0 : (_optionsRef$current4$ = _optionsRef$current4.getElement) === null || _optionsRef$current4$ === void 0 ? void 0 : _optionsRef$current4$.call(_optionsRef$current4, ref.current)) !== null && _optionsRef$current$g3 !== void 0 ? _optionsRef$current$g3 : ref.current;
656
+ };
657
+ return {
658
+ change(eventOrValue) {
659
+ var input = getInputElement();
660
+ if (!input) {
661
+ console.warn('Missing input ref; No change-related events will be dispatched');
662
+ return;
663
+ }
664
+ if (changeDispatched.current) {
665
+ changeDispatched.current = false;
666
+ return;
667
+ }
668
+ var previousValue = input.value;
669
+ var nextValue = typeof eventOrValue === 'string' ? eventOrValue : eventOrValue.target.value;
670
+
671
+ // This make sure no event is dispatched on the first effect run
672
+ if (nextValue === previousValue) {
673
+ return;
674
+ }
675
+
676
+ // Dispatch beforeinput event before updating the input value
677
+ input.dispatchEvent(new Event('beforeinput', {
678
+ bubbles: true
679
+ }));
680
+ // Update the input value to trigger a change event
681
+ setNativeValue(input, nextValue);
682
+ // Dispatch input event with the updated input value
683
+ input.dispatchEvent(new InputEvent('input', {
684
+ bubbles: true
685
+ }));
686
+ // Reset the dispatched flag
687
+ changeDispatched.current = false;
688
+ },
689
+ focus() {
690
+ var input = getInputElement();
691
+ if (!input) {
692
+ console.warn('Missing input ref; No focus-related events will be dispatched');
693
+ return;
694
+ }
695
+ if (focusDispatched.current) {
696
+ focusDispatched.current = false;
697
+ return;
698
+ }
699
+ var focusinEvent = new FocusEvent('focusin', {
700
+ bubbles: true
701
+ });
702
+ var focusEvent = new FocusEvent('focus');
703
+ input.dispatchEvent(focusinEvent);
704
+ input.dispatchEvent(focusEvent);
705
+
706
+ // Reset the dispatched flag
707
+ focusDispatched.current = false;
708
+ },
709
+ blur() {
710
+ var input = getInputElement();
711
+ if (!input) {
712
+ console.warn('Missing input ref; No blur-related events will be dispatched');
713
+ return;
714
+ }
715
+ if (blurDispatched.current) {
716
+ blurDispatched.current = false;
717
+ return;
718
+ }
719
+ var focusoutEvent = new FocusEvent('focusout', {
720
+ bubbles: true
721
+ });
722
+ var blurEvent = new FocusEvent('blur');
723
+ input.dispatchEvent(focusoutEvent);
724
+ input.dispatchEvent(blurEvent);
725
+
726
+ // Reset the dispatched flag
727
+ blurDispatched.current = false;
728
+ }
729
+ };
730
+ }, []);
731
+ return [ref, control];
732
+ }
733
+
734
+ export { useControlledInput, useFieldList, useFieldset, useForm, useInputEvent };
package/module/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { getFormElements, hasError, list, parse, requestCommand, requestSubmit, shouldValidate, validate } from '@conform-to/dom';
2
- export { useControlledInput, useFieldList, useFieldset, useForm } from './hooks.js';
2
+ export { useControlledInput, useFieldList, useFieldset, useForm, useInputEvent } from './hooks.js';
3
3
  import * as helpers from './helpers.js';
4
4
  export { helpers as conform };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@conform-to/react",
3
3
  "description": "Conform view adapter for react",
4
4
  "license": "MIT",
5
- "version": "0.5.0",
5
+ "version": "0.5.1",
6
6
  "main": "index.js",
7
7
  "module": "module/index.js",
8
8
  "repository": {
@@ -19,7 +19,7 @@
19
19
  "url": "https://github.com/edmundhung/conform/issues"
20
20
  },
21
21
  "dependencies": {
22
- "@conform-to/dom": "0.5.0"
22
+ "@conform-to/dom": "0.5.1"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "react": ">=16.8"