@radix-ui/react-one-time-password-field 0.1.0-rc.1744898528774 → 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/dist/index.js +168 -157
- package/dist/index.js.map +2 -2
- package/dist/index.mjs +168 -157
- package/dist/index.mjs.map +2 -2
- package/package.json +8 -8
- package/src/one-time-password-field.tsx +214 -196
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.
|
|
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.
|
|
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-
|
|
22
|
-
"@radix-ui/react-
|
|
23
|
-
"@radix-ui/react-
|
|
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-
|
|
26
|
-
"@radix-ui/react-use-
|
|
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",
|
|
@@ -62,6 +62,7 @@ interface OneTimePasswordFieldContextValue {
|
|
|
62
62
|
userActionRef: React.RefObject<KeyboardActionDetails | null>;
|
|
63
63
|
validationType: InputValidationType;
|
|
64
64
|
value: string[];
|
|
65
|
+
sanitizeValue: (arg: string | string[]) => string[];
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
const ONE_TIME_PASSWORD_FIELD_NAME = 'OneTimePasswordField';
|
|
@@ -457,6 +458,7 @@ const OneTimePasswordField = React.forwardRef<HTMLDivElement, OneTimePasswordFie
|
|
|
457
458
|
orientation={orientation}
|
|
458
459
|
preHydrationIndexTracker={preHydrationIndexTracker}
|
|
459
460
|
isHydrated={isHydrated}
|
|
461
|
+
sanitizeValue={sanitizeValue}
|
|
460
462
|
>
|
|
461
463
|
<Collection.Provider scope={__scopeOneTimePasswordField} state={collectionState}>
|
|
462
464
|
<Collection.Slot scope={__scopeOneTimePasswordField}>
|
|
@@ -475,10 +477,6 @@ const OneTimePasswordField = React.forwardRef<HTMLDivElement, OneTimePasswordFie
|
|
|
475
477
|
(event: React.ClipboardEvent<HTMLDivElement>) => {
|
|
476
478
|
event.preventDefault();
|
|
477
479
|
const pastedValue = event.clipboardData.getData('Text');
|
|
478
|
-
const value = sanitizeValue(pastedValue);
|
|
479
|
-
if (!value) {
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
480
|
dispatch({ type: 'PASTE', value: pastedValue });
|
|
483
481
|
}
|
|
484
482
|
)}
|
|
@@ -642,133 +640,40 @@ const OneTimePasswordFieldInput = React.forwardRef<
|
|
|
642
640
|
focusable={!context.disabled && isFocusable}
|
|
643
641
|
active={index === lastSelectableIndex}
|
|
644
642
|
>
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
userActionRef.current = {
|
|
671
|
-
type: 'cut',
|
|
672
|
-
};
|
|
673
|
-
// Set a short timeout to clear the action tracker after the change
|
|
674
|
-
// handler has had time to complete.
|
|
675
|
-
keyboardActionTimeoutRef.current = window.setTimeout(() => {
|
|
676
|
-
userActionRef.current = null;
|
|
677
|
-
}, 10);
|
|
678
|
-
}
|
|
679
|
-
})}
|
|
680
|
-
onChange={composeEventHandlers(props.onChange, (event) => {
|
|
681
|
-
event.preventDefault();
|
|
682
|
-
const action = userActionRef.current;
|
|
683
|
-
userActionRef.current = null;
|
|
684
|
-
|
|
685
|
-
if (action) {
|
|
686
|
-
switch (action.type) {
|
|
687
|
-
case 'cut':
|
|
688
|
-
// TODO: do we want to assume the user wantt to clear the
|
|
689
|
-
// entire value here and copy the code to the clipboard instead
|
|
690
|
-
// of just the value of the given input?
|
|
691
|
-
dispatch({ type: 'CLEAR_CHAR', index, reason: 'Cut' });
|
|
692
|
-
return;
|
|
693
|
-
case 'keydown': {
|
|
694
|
-
if (action.key === 'Char') {
|
|
695
|
-
// update resulting from a keydown event that set a value
|
|
696
|
-
// directly. Ignore.
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const isClearing =
|
|
701
|
-
action.key === 'Backspace' && (action.metaKey || action.ctrlKey);
|
|
702
|
-
if (action.key === 'Clear' || isClearing) {
|
|
703
|
-
dispatch({ type: 'CLEAR', reason: 'Backspace' });
|
|
704
|
-
} else {
|
|
705
|
-
dispatch({ type: 'CLEAR_CHAR', index, reason: action.key });
|
|
706
|
-
}
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
default:
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// Only update the value if it matches the input pattern
|
|
715
|
-
if (event.target.validity.valid) {
|
|
716
|
-
if (event.target.value === '') {
|
|
717
|
-
let reason: 'Backspace' | 'Delete' | 'Cut' = 'Backspace';
|
|
718
|
-
if (isInputEvent(event.nativeEvent)) {
|
|
719
|
-
const inputType = event.nativeEvent.inputType;
|
|
720
|
-
if (inputType === 'deleteContentBackward') {
|
|
721
|
-
reason = 'Backspace';
|
|
722
|
-
} else if (inputType === 'deleteByCut') {
|
|
723
|
-
reason = 'Cut';
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
dispatch({ type: 'CLEAR_CHAR', index, reason });
|
|
727
|
-
} else {
|
|
728
|
-
dispatch({ type: 'SET_CHAR', char: event.target.value, index, event });
|
|
729
|
-
}
|
|
730
|
-
} else {
|
|
731
|
-
const element = event.target;
|
|
732
|
-
onInvalidChange?.(element.value);
|
|
733
|
-
requestAnimationFrame(() => {
|
|
734
|
-
if (element.ownerDocument.activeElement === element) {
|
|
735
|
-
element.select();
|
|
736
|
-
}
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
})}
|
|
740
|
-
onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {
|
|
741
|
-
switch (event.key) {
|
|
742
|
-
case 'Clear':
|
|
743
|
-
case 'Delete':
|
|
744
|
-
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) => {
|
|
745
668
|
const currentValue = event.currentTarget.value;
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
//
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if (isClearing) {
|
|
753
|
-
dispatch({ type: 'CLEAR', reason: 'Backspace' });
|
|
754
|
-
} else {
|
|
755
|
-
const element = event.currentTarget;
|
|
756
|
-
requestAnimationFrame(() => {
|
|
757
|
-
focusInput(collection.from(element, -1)?.element);
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
} else {
|
|
761
|
-
// In this case the value will be cleared, but we don't want
|
|
762
|
-
// to set it directly because the user may want to prevent
|
|
763
|
-
// default behavior in the onChange handler. The userActionRef
|
|
764
|
-
// will is set temporarily so the change handler can behave
|
|
765
|
-
// correctly in response to the key vs. clearing the value by
|
|
766
|
-
// 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.
|
|
767
675
|
userActionRef.current = {
|
|
768
|
-
type: '
|
|
769
|
-
key: event.key,
|
|
770
|
-
metaKey: event.metaKey,
|
|
771
|
-
ctrlKey: event.ctrlKey,
|
|
676
|
+
type: 'cut',
|
|
772
677
|
};
|
|
773
678
|
// Set a short timeout to clear the action tracker after the change
|
|
774
679
|
// handler has had time to complete.
|
|
@@ -776,90 +681,203 @@ const OneTimePasswordFieldInput = React.forwardRef<
|
|
|
776
681
|
userActionRef.current = null;
|
|
777
682
|
}, 10);
|
|
778
683
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
case 'ArrowDown':
|
|
788
|
-
case 'ArrowUp': {
|
|
789
|
-
if (context.orientation === 'horizontal') {
|
|
790
|
-
// in horizontal orientation, the up/down will de-select the
|
|
791
|
-
// 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.
|
|
792
692
|
event.preventDefault();
|
|
693
|
+
dispatch({ type: 'PASTE', value });
|
|
793
694
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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 });
|
|
819
744
|
} else {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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' });
|
|
831
771
|
} else {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
index: index + 1,
|
|
836
|
-
event,
|
|
772
|
+
const element = event.currentTarget;
|
|
773
|
+
requestAnimationFrame(() => {
|
|
774
|
+
focusInput(collection.from(element, -1)?.element);
|
|
837
775
|
});
|
|
838
776
|
}
|
|
839
|
-
|
|
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.
|
|
840
784
|
userActionRef.current = {
|
|
841
785
|
type: 'keydown',
|
|
842
|
-
key:
|
|
786
|
+
key: event.key,
|
|
843
787
|
metaKey: event.metaKey,
|
|
844
788
|
ctrlKey: event.ctrlKey,
|
|
845
789
|
};
|
|
790
|
+
// Set a short timeout to clear the action tracker after the change
|
|
791
|
+
// handler has had time to complete.
|
|
846
792
|
keyboardActionTimeoutRef.current = window.setTimeout(() => {
|
|
847
793
|
userActionRef.current = null;
|
|
848
794
|
}, 10);
|
|
849
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
|
+
}
|
|
850
869
|
}
|
|
851
870
|
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
/>
|
|
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
|
+
}}
|
|
863
881
|
</RovingFocusGroup.Item>
|
|
864
882
|
</Collection.ItemSlot>
|
|
865
883
|
);
|