@radix-ui/react-one-time-password-field 0.1.0-rc.1744905634543 → 0.1.0-rc.1744910682821

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radix-ui/react-one-time-password-field",
3
- "version": "0.1.0-rc.1744905634543",
3
+ "version": "0.1.0-rc.1744910682821",
4
4
  "license": "MIT",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./dist/index.js",
@@ -13,17 +13,17 @@
13
13
  "sideEffects": false,
14
14
  "dependencies": {
15
15
  "@radix-ui/primitive": "1.1.2",
16
- "@radix-ui/react-collection": "1.1.4-rc.1744905634543",
17
- "@radix-ui/react-compose-refs": "1.1.2",
16
+ "@radix-ui/react-collection": "1.1.4-rc.1744910682821",
18
17
  "@radix-ui/react-context": "1.1.2",
19
18
  "@radix-ui/react-direction": "1.1.1",
19
+ "@radix-ui/react-compose-refs": "1.1.2",
20
20
  "@radix-ui/number": "1.1.1",
21
- "@radix-ui/react-primitive": "2.1.0-rc.1744905634543",
22
- "@radix-ui/react-use-controllable-state": "1.2.0-rc.1744905634543",
23
- "@radix-ui/react-roving-focus": "1.1.4-rc.1744905634543",
21
+ "@radix-ui/react-roving-focus": "1.1.4-rc.1744910682821",
22
+ "@radix-ui/react-primitive": "2.1.0-rc.1744910682821",
23
+ "@radix-ui/react-use-controllable-state": "1.2.0-rc.1744910682821",
24
24
  "@radix-ui/react-use-effect-event": "0.0.0",
25
- "@radix-ui/react-use-is-hydrated": "0.0.0",
26
- "@radix-ui/react-use-layout-effect": "1.1.1"
25
+ "@radix-ui/react-use-layout-effect": "1.1.1",
26
+ "@radix-ui/react-use-is-hydrated": "0.0.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/react": "^19.0.7",
@@ -640,145 +640,40 @@ const OneTimePasswordFieldInput = React.forwardRef<
640
640
  focusable={!context.disabled && isFocusable}
641
641
  active={index === lastSelectableIndex}
642
642
  >
643
- <Primitive.Root.input
644
- ref={composedInputRef}
645
- type="text"
646
- aria-label={`Character ${index + 1} of ${collection.size}`}
647
- autoComplete={index === 0 ? context.autoComplete : 'off'}
648
- inputMode={validation?.inputMode}
649
- maxLength={1}
650
- pattern={validation?.pattern}
651
- readOnly={context.readOnly}
652
- value={char}
653
- placeholder={placeholder}
654
- data-radix-otp-input=""
655
- data-radix-index={index}
656
- {...domProps}
657
- onFocus={composeEventHandlers(props.onFocus, (event) => {
658
- event.currentTarget.select();
659
- })}
660
- onCut={composeEventHandlers(props.onCut, (event) => {
661
- const currentValue = event.currentTarget.value;
662
- if (currentValue !== '') {
663
- // In this case the value will be cleared, but we don't want to
664
- // set it directly because the user may want to prevent default
665
- // behavior in the onChange handler. The userActionRef will
666
- // is set temporarily so the change handler can behave correctly
667
- // in response to the action.
668
- userActionRef.current = {
669
- type: 'cut',
670
- };
671
- // Set a short timeout to clear the action tracker after the change
672
- // handler has had time to complete.
673
- keyboardActionTimeoutRef.current = window.setTimeout(() => {
674
- userActionRef.current = null;
675
- }, 10);
676
- }
677
- })}
678
- onInput={composeEventHandlers(props.onInput, (event) => {
679
- const value = event.currentTarget.value;
680
- if (value.length > 1) {
681
- // Password managers may try to insert the code into a single
682
- // input, in which case form validation will fail to prevent
683
- // additional input. Handle this the same as if a user were
684
- // pasting a value.
685
- event.preventDefault();
686
- dispatch({ type: 'PASTE', value });
687
- }
688
- })}
689
- onChange={composeEventHandlers(props.onChange, (event) => {
690
- const value = event.target.value;
691
- event.preventDefault();
692
- const action = userActionRef.current;
693
- userActionRef.current = null;
694
-
695
- if (action) {
696
- switch (action.type) {
697
- case 'cut':
698
- // TODO: do we want to assume the user wantt to clear the
699
- // entire value here and copy the code to the clipboard instead
700
- // of just the value of the given input?
701
- dispatch({ type: 'CLEAR_CHAR', index, reason: 'Cut' });
702
- return;
703
- case 'keydown': {
704
- if (action.key === 'Char') {
705
- // update resulting from a keydown event that set a value
706
- // directly. Ignore.
707
- return;
708
- }
709
-
710
- const isClearing =
711
- action.key === 'Backspace' && (action.metaKey || action.ctrlKey);
712
- if (action.key === 'Clear' || isClearing) {
713
- dispatch({ type: 'CLEAR', reason: 'Backspace' });
714
- } else {
715
- dispatch({ type: 'CLEAR_CHAR', index, reason: action.key });
716
- }
717
- return;
718
- }
719
- default:
720
- return;
721
- }
722
- }
723
-
724
- // Only update the value if it matches the input pattern
725
- if (event.target.validity.valid) {
726
- if (value === '') {
727
- let reason: 'Backspace' | 'Delete' | 'Cut' = 'Backspace';
728
- if (isInputEvent(event.nativeEvent)) {
729
- const inputType = event.nativeEvent.inputType;
730
- if (inputType === 'deleteContentBackward') {
731
- reason = 'Backspace';
732
- } else if (inputType === 'deleteByCut') {
733
- reason = 'Cut';
734
- }
735
- }
736
- dispatch({ type: 'CLEAR_CHAR', index, reason });
737
- } else {
738
- dispatch({ type: 'SET_CHAR', char: value, index, event });
739
- }
740
- } else {
741
- const element = event.target;
742
- onInvalidChange?.(element.value);
743
- requestAnimationFrame(() => {
744
- if (element.ownerDocument.activeElement === element) {
745
- element.select();
746
- }
747
- });
748
- }
749
- })}
750
- onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {
751
- switch (event.key) {
752
- case 'Clear':
753
- case 'Delete':
754
- case 'Backspace': {
643
+ {({ isCurrentTabStop }) => {
644
+ const supportsAutoComplete = isHydrated ? isCurrentTabStop : index === 0;
645
+ return (
646
+ <Primitive.Root.input
647
+ ref={composedInputRef}
648
+ type="text"
649
+ aria-label={`Character ${index + 1} of ${collection.size}`}
650
+ autoComplete={supportsAutoComplete ? context.autoComplete : 'off'}
651
+ data-1p-ignore={supportsAutoComplete ? undefined : 'true'}
652
+ data-lpignore={supportsAutoComplete ? undefined : 'true'}
653
+ data-protonpass-ignore={supportsAutoComplete ? undefined : 'true'}
654
+ data-bwignore={supportsAutoComplete ? undefined : 'true'}
655
+ inputMode={validation?.inputMode}
656
+ maxLength={1}
657
+ pattern={validation?.pattern}
658
+ readOnly={context.readOnly}
659
+ value={char}
660
+ placeholder={placeholder}
661
+ data-radix-otp-input=""
662
+ data-radix-index={index}
663
+ {...domProps}
664
+ onFocus={composeEventHandlers(props.onFocus, (event) => {
665
+ event.currentTarget.select();
666
+ })}
667
+ onCut={composeEventHandlers(props.onCut, (event) => {
755
668
  const currentValue = event.currentTarget.value;
756
- // if current value is empty, no change event will fire
757
- if (currentValue === '') {
758
- // if the user presses delete when there is no value, noop
759
- if (event.key === 'Delete') return;
760
-
761
- const isClearing = event.key === 'Clear' || event.metaKey || event.ctrlKey;
762
- if (isClearing) {
763
- dispatch({ type: 'CLEAR', reason: 'Backspace' });
764
- } else {
765
- const element = event.currentTarget;
766
- requestAnimationFrame(() => {
767
- focusInput(collection.from(element, -1)?.element);
768
- });
769
- }
770
- } else {
771
- // In this case the value will be cleared, but we don't want
772
- // to set it directly because the user may want to prevent
773
- // default behavior in the onChange handler. The userActionRef
774
- // will is set temporarily so the change handler can behave
775
- // correctly in response to the key vs. clearing the value by
776
- // setting state externally.
669
+ if (currentValue !== '') {
670
+ // In this case the value will be cleared, but we don't want to
671
+ // set it directly because the user may want to prevent default
672
+ // behavior in the onChange handler. The userActionRef will
673
+ // is set temporarily so the change handler can behave correctly
674
+ // in response to the action.
777
675
  userActionRef.current = {
778
- type: 'keydown',
779
- key: event.key,
780
- metaKey: event.metaKey,
781
- ctrlKey: event.ctrlKey,
676
+ type: 'cut',
782
677
  };
783
678
  // Set a short timeout to clear the action tracker after the change
784
679
  // handler has had time to complete.
@@ -786,89 +681,203 @@ const OneTimePasswordFieldInput = React.forwardRef<
786
681
  userActionRef.current = null;
787
682
  }, 10);
788
683
  }
789
-
790
- return;
791
- }
792
- case 'Enter': {
793
- event.preventDefault();
794
- context.attemptSubmit();
795
- return;
796
- }
797
- case 'ArrowDown':
798
- case 'ArrowUp': {
799
- if (context.orientation === 'horizontal') {
800
- // in horizontal orientation, the up/down will de-select the
801
- // input instead of moving focus
684
+ })}
685
+ onInput={composeEventHandlers(props.onInput, (event) => {
686
+ const value = event.currentTarget.value;
687
+ if (value.length > 1) {
688
+ // Password managers may try to insert the code into a single
689
+ // input, in which case form validation will fail to prevent
690
+ // additional input. Handle this the same as if a user were
691
+ // pasting a value.
802
692
  event.preventDefault();
693
+ dispatch({ type: 'PASTE', value });
803
694
  }
804
- return;
805
- }
806
- // TODO: Handle left/right arrow keys in vertical writing mode
807
- default: {
808
- if (event.currentTarget.value === event.key) {
809
- // if current value is same as the key press, no change event
810
- // will fire. Focus the next input.
811
- const element = event.currentTarget;
812
- event.preventDefault();
813
- focusInput(collection.from(element, 1)?.element);
814
- return;
815
- } else if (
816
- // input already has a value, but...
817
- event.currentTarget.value &&
818
- // the value is not selected
819
- !(
820
- event.currentTarget.selectionStart === 0 &&
821
- event.currentTarget.selectionEnd != null &&
822
- event.currentTarget.selectionEnd > 0
823
- )
824
- ) {
825
- const attemptedValue = event.key;
826
- if (event.key.length > 1 || event.key === ' ') {
827
- // not a character; do nothing
828
- return;
695
+ })}
696
+ onChange={composeEventHandlers(props.onChange, (event) => {
697
+ const value = event.target.value;
698
+ event.preventDefault();
699
+ const action = userActionRef.current;
700
+ userActionRef.current = null;
701
+
702
+ if (action) {
703
+ switch (action.type) {
704
+ case 'cut':
705
+ // TODO: do we want to assume the user wantt to clear the
706
+ // entire value here and copy the code to the clipboard instead
707
+ // of just the value of the given input?
708
+ dispatch({ type: 'CLEAR_CHAR', index, reason: 'Cut' });
709
+ return;
710
+ case 'keydown': {
711
+ if (action.key === 'Char') {
712
+ // update resulting from a keydown event that set a value
713
+ // directly. Ignore.
714
+ return;
715
+ }
716
+
717
+ const isClearing =
718
+ action.key === 'Backspace' && (action.metaKey || action.ctrlKey);
719
+ if (action.key === 'Clear' || isClearing) {
720
+ dispatch({ type: 'CLEAR', reason: 'Backspace' });
721
+ } else {
722
+ dispatch({ type: 'CLEAR_CHAR', index, reason: action.key });
723
+ }
724
+ return;
725
+ }
726
+ default:
727
+ return;
728
+ }
729
+ }
730
+
731
+ // Only update the value if it matches the input pattern
732
+ if (event.target.validity.valid) {
733
+ if (value === '') {
734
+ let reason: 'Backspace' | 'Delete' | 'Cut' = 'Backspace';
735
+ if (isInputEvent(event.nativeEvent)) {
736
+ const inputType = event.nativeEvent.inputType;
737
+ if (inputType === 'deleteContentBackward') {
738
+ reason = 'Backspace';
739
+ } else if (inputType === 'deleteByCut') {
740
+ reason = 'Cut';
741
+ }
742
+ }
743
+ dispatch({ type: 'CLEAR_CHAR', index, reason });
829
744
  } else {
830
- // user is attempting to enter a character, but the input
831
- // will not update by default since it's limited to a single
832
- // character.
833
- const nextInput = collection.from(event.currentTarget, 1)?.element;
834
- const lastInput = collection.at(-1)?.element;
835
- if (nextInput !== lastInput && event.currentTarget !== lastInput) {
836
- // if selection is before the value, set the value of the
837
- // current input. Otherwise set the value of the next
838
- // input.
839
- if (event.currentTarget.selectionStart === 0) {
840
- dispatch({ type: 'SET_CHAR', char: attemptedValue, index, event });
745
+ dispatch({ type: 'SET_CHAR', char: value, index, event });
746
+ }
747
+ } else {
748
+ const element = event.target;
749
+ onInvalidChange?.(element.value);
750
+ requestAnimationFrame(() => {
751
+ if (element.ownerDocument.activeElement === element) {
752
+ element.select();
753
+ }
754
+ });
755
+ }
756
+ })}
757
+ onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {
758
+ switch (event.key) {
759
+ case 'Clear':
760
+ case 'Delete':
761
+ case 'Backspace': {
762
+ const currentValue = event.currentTarget.value;
763
+ // if current value is empty, no change event will fire
764
+ if (currentValue === '') {
765
+ // if the user presses delete when there is no value, noop
766
+ if (event.key === 'Delete') return;
767
+
768
+ const isClearing = event.key === 'Clear' || event.metaKey || event.ctrlKey;
769
+ if (isClearing) {
770
+ dispatch({ type: 'CLEAR', reason: 'Backspace' });
841
771
  } else {
842
- dispatch({
843
- type: 'SET_CHAR',
844
- char: attemptedValue,
845
- index: index + 1,
846
- event,
772
+ const element = event.currentTarget;
773
+ requestAnimationFrame(() => {
774
+ focusInput(collection.from(element, -1)?.element);
847
775
  });
848
776
  }
849
-
777
+ } else {
778
+ // In this case the value will be cleared, but we don't want
779
+ // to set it directly because the user may want to prevent
780
+ // default behavior in the onChange handler. The userActionRef
781
+ // will is set temporarily so the change handler can behave
782
+ // correctly in response to the key vs. clearing the value by
783
+ // setting state externally.
850
784
  userActionRef.current = {
851
785
  type: 'keydown',
852
- key: 'Char',
786
+ key: event.key,
853
787
  metaKey: event.metaKey,
854
788
  ctrlKey: event.ctrlKey,
855
789
  };
790
+ // Set a short timeout to clear the action tracker after the change
791
+ // handler has had time to complete.
856
792
  keyboardActionTimeoutRef.current = window.setTimeout(() => {
857
793
  userActionRef.current = null;
858
794
  }, 10);
859
795
  }
796
+
797
+ return;
798
+ }
799
+ case 'Enter': {
800
+ event.preventDefault();
801
+ context.attemptSubmit();
802
+ return;
803
+ }
804
+ case 'ArrowDown':
805
+ case 'ArrowUp': {
806
+ if (context.orientation === 'horizontal') {
807
+ // in horizontal orientation, the up/down will de-select the
808
+ // input instead of moving focus
809
+ event.preventDefault();
810
+ }
811
+ return;
812
+ }
813
+ // TODO: Handle left/right arrow keys in vertical writing mode
814
+ default: {
815
+ if (event.currentTarget.value === event.key) {
816
+ // if current value is same as the key press, no change event
817
+ // will fire. Focus the next input.
818
+ const element = event.currentTarget;
819
+ event.preventDefault();
820
+ focusInput(collection.from(element, 1)?.element);
821
+ return;
822
+ } else if (
823
+ // input already has a value, but...
824
+ event.currentTarget.value &&
825
+ // the value is not selected
826
+ !(
827
+ event.currentTarget.selectionStart === 0 &&
828
+ event.currentTarget.selectionEnd != null &&
829
+ event.currentTarget.selectionEnd > 0
830
+ )
831
+ ) {
832
+ const attemptedValue = event.key;
833
+ if (event.key.length > 1 || event.key === ' ') {
834
+ // not a character; do nothing
835
+ return;
836
+ } else {
837
+ // user is attempting to enter a character, but the input
838
+ // will not update by default since it's limited to a single
839
+ // character.
840
+ const nextInput = collection.from(event.currentTarget, 1)?.element;
841
+ const lastInput = collection.at(-1)?.element;
842
+ if (nextInput !== lastInput && event.currentTarget !== lastInput) {
843
+ // if selection is before the value, set the value of the
844
+ // current input. Otherwise set the value of the next
845
+ // input.
846
+ if (event.currentTarget.selectionStart === 0) {
847
+ dispatch({ type: 'SET_CHAR', char: attemptedValue, index, event });
848
+ } else {
849
+ dispatch({
850
+ type: 'SET_CHAR',
851
+ char: attemptedValue,
852
+ index: index + 1,
853
+ event,
854
+ });
855
+ }
856
+
857
+ userActionRef.current = {
858
+ type: 'keydown',
859
+ key: 'Char',
860
+ metaKey: event.metaKey,
861
+ ctrlKey: event.ctrlKey,
862
+ };
863
+ keyboardActionTimeoutRef.current = window.setTimeout(() => {
864
+ userActionRef.current = null;
865
+ }, 10);
866
+ }
867
+ }
868
+ }
860
869
  }
861
870
  }
862
- }
863
- }
864
- })}
865
- onPointerDown={composeEventHandlers(props.onPointerDown, (event) => {
866
- event.preventDefault();
867
- const indexToFocus = Math.min(index, lastSelectableIndex);
868
- const element = collection.at(indexToFocus)?.element;
869
- focusInput(element);
870
- })}
871
- />
871
+ })}
872
+ onPointerDown={composeEventHandlers(props.onPointerDown, (event) => {
873
+ event.preventDefault();
874
+ const indexToFocus = Math.min(index, lastSelectableIndex);
875
+ const element = collection.at(indexToFocus)?.element;
876
+ focusInput(element);
877
+ })}
878
+ />
879
+ );
880
+ }}
872
881
  </RovingFocusGroup.Item>
873
882
  </Collection.ItemSlot>
874
883
  );