@radix-ui/react-password-toggle-field 0.1.0-rc.1745439717073
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/LICENSE +21 -0
- package/README.md +13 -0
- package/dist/index.d.mts +41 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +339 -0
- package/dist/index.js.map +7 -0
- package/dist/index.mjs +307 -0
- package/dist/index.mjs.map +7 -0
- package/package.json +76 -0
- package/src/index.ts +21 -0
- package/src/password-toggle-field.test.tsx +219 -0
- package/src/password-toggle-field.tsx +479 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { flushSync } from 'react-dom';
|
|
3
|
+
import { composeEventHandlers } from '@radix-ui/primitive';
|
|
4
|
+
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
5
|
+
import { Primitive } from '@radix-ui/react-primitive';
|
|
6
|
+
import { useComposedRefs } from '@radix-ui/react-compose-refs';
|
|
7
|
+
import { useId } from '@radix-ui/react-id';
|
|
8
|
+
import { useIsHydrated } from '@radix-ui/react-use-is-hydrated';
|
|
9
|
+
import { useEffectEvent } from '@radix-ui/react-use-effect-event';
|
|
10
|
+
import type { Scope } from '@radix-ui/react-context';
|
|
11
|
+
import { createContextScope } from '@radix-ui/react-context';
|
|
12
|
+
|
|
13
|
+
const PASSWORD_TOGGLE_FIELD_NAME = 'PasswordToggleField';
|
|
14
|
+
|
|
15
|
+
/* -------------------------------------------------------------------------------------------------
|
|
16
|
+
* PasswordToggleFieldProvider
|
|
17
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
18
|
+
|
|
19
|
+
type InternalFocusState = {
|
|
20
|
+
clickTriggered: boolean;
|
|
21
|
+
selectionStart: number | null;
|
|
22
|
+
selectionEnd: number | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
interface PasswordToggleFieldContextValue {
|
|
26
|
+
inputId: string;
|
|
27
|
+
inputRef: React.RefObject<HTMLInputElement | null>;
|
|
28
|
+
visible: boolean;
|
|
29
|
+
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
|
30
|
+
syncInputId: (providedId: string | number | undefined) => void;
|
|
31
|
+
focusState: React.RefObject<InternalFocusState>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const [createPasswordToggleFieldContext] = createContextScope(PASSWORD_TOGGLE_FIELD_NAME);
|
|
35
|
+
const [PasswordToggleFieldProvider, usePasswordToggleFieldContext] =
|
|
36
|
+
createPasswordToggleFieldContext<PasswordToggleFieldContextValue>(PASSWORD_TOGGLE_FIELD_NAME);
|
|
37
|
+
|
|
38
|
+
/* -------------------------------------------------------------------------------------------------
|
|
39
|
+
* PasswordToggleField
|
|
40
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
41
|
+
|
|
42
|
+
type ScopedProps<P> = P & { __scopePasswordToggleField?: Scope };
|
|
43
|
+
|
|
44
|
+
interface PasswordToggleFieldProps {
|
|
45
|
+
id?: string;
|
|
46
|
+
visible?: boolean;
|
|
47
|
+
defaultVisible?: boolean;
|
|
48
|
+
onVisiblityChange?: (visible: boolean) => void;
|
|
49
|
+
children?: React.ReactNode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const INITIAL_FOCUS_STATE: InternalFocusState = {
|
|
53
|
+
clickTriggered: false,
|
|
54
|
+
selectionStart: null,
|
|
55
|
+
selectionEnd: null,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const PasswordToggleField: React.FC<PasswordToggleFieldProps> = ({
|
|
59
|
+
__scopePasswordToggleField,
|
|
60
|
+
...props
|
|
61
|
+
}: ScopedProps<PasswordToggleFieldProps>) => {
|
|
62
|
+
const baseId = useId(props.id);
|
|
63
|
+
const defaultInputId = `${baseId}-input`;
|
|
64
|
+
const [inputIdState, setInputIdState] = React.useState<null | string>(defaultInputId);
|
|
65
|
+
const inputId = inputIdState ?? defaultInputId;
|
|
66
|
+
const syncInputId = React.useCallback(
|
|
67
|
+
(providedId: string | number | undefined) =>
|
|
68
|
+
setInputIdState(providedId != null ? String(providedId) : null),
|
|
69
|
+
[]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const { visible: visibleProp, defaultVisible, onVisiblityChange, children } = props;
|
|
73
|
+
const [visible = false, setVisible] = useControllableState({
|
|
74
|
+
caller: PASSWORD_TOGGLE_FIELD_NAME,
|
|
75
|
+
prop: visibleProp,
|
|
76
|
+
defaultProp: defaultVisible ?? false,
|
|
77
|
+
onChange: onVisiblityChange,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
|
81
|
+
const focusState = React.useRef<InternalFocusState>(INITIAL_FOCUS_STATE);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<PasswordToggleFieldProvider
|
|
85
|
+
scope={__scopePasswordToggleField}
|
|
86
|
+
inputId={inputId}
|
|
87
|
+
inputRef={inputRef}
|
|
88
|
+
setVisible={setVisible}
|
|
89
|
+
syncInputId={syncInputId}
|
|
90
|
+
visible={visible}
|
|
91
|
+
focusState={focusState}
|
|
92
|
+
>
|
|
93
|
+
{children}
|
|
94
|
+
</PasswordToggleFieldProvider>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
PasswordToggleField.displayName = PASSWORD_TOGGLE_FIELD_NAME;
|
|
98
|
+
|
|
99
|
+
/* -------------------------------------------------------------------------------------------------
|
|
100
|
+
* PasswordToggleFieldInput
|
|
101
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
102
|
+
|
|
103
|
+
const PASSWORD_TOGGLE_FIELD_INPUT_NAME = PASSWORD_TOGGLE_FIELD_NAME + 'Input';
|
|
104
|
+
|
|
105
|
+
type PrimitiveInputProps = React.ComponentPropsWithoutRef<'input'>;
|
|
106
|
+
|
|
107
|
+
interface PasswordToggleFieldOwnProps {
|
|
108
|
+
autoComplete?: 'current-password' | 'new-password';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface PasswordToggleFieldInputProps
|
|
112
|
+
extends PasswordToggleFieldOwnProps,
|
|
113
|
+
Omit<PrimitiveInputProps, keyof PasswordToggleFieldOwnProps | 'type'> {
|
|
114
|
+
autoComplete?: 'current-password' | 'new-password';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const PasswordToggleFieldInput = React.forwardRef<HTMLInputElement, PasswordToggleFieldInputProps>(
|
|
118
|
+
(
|
|
119
|
+
{
|
|
120
|
+
__scopePasswordToggleField,
|
|
121
|
+
autoComplete = 'current-password',
|
|
122
|
+
autoCapitalize = 'off',
|
|
123
|
+
spellCheck = false,
|
|
124
|
+
id: idProp,
|
|
125
|
+
...props
|
|
126
|
+
}: ScopedProps<PasswordToggleFieldInputProps>,
|
|
127
|
+
forwardedRef
|
|
128
|
+
) => {
|
|
129
|
+
const { visible, inputRef, inputId, syncInputId, setVisible, focusState } =
|
|
130
|
+
usePasswordToggleFieldContext(PASSWORD_TOGGLE_FIELD_INPUT_NAME, __scopePasswordToggleField);
|
|
131
|
+
|
|
132
|
+
React.useEffect(() => {
|
|
133
|
+
syncInputId(idProp);
|
|
134
|
+
}, [idProp, syncInputId]);
|
|
135
|
+
|
|
136
|
+
// We want to reset the visibility to `false` to revert the input to
|
|
137
|
+
// `type="password"` when:
|
|
138
|
+
// - The form is reset (for consistency with other form controls)
|
|
139
|
+
// - The form is submitted (to prevent the browser from remembering the
|
|
140
|
+
// input's value.
|
|
141
|
+
//
|
|
142
|
+
// See "Keeping things secure":
|
|
143
|
+
// https://technology.blog.gov.uk/2021/04/19/simple-things-are-complicated-making-a-show-password-option/)
|
|
144
|
+
const _setVisible = useEffectEvent(setVisible);
|
|
145
|
+
React.useEffect(() => {
|
|
146
|
+
const inputElement = inputRef.current;
|
|
147
|
+
const form = inputElement?.form;
|
|
148
|
+
if (!form) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
form.addEventListener(
|
|
154
|
+
'reset',
|
|
155
|
+
(event) => {
|
|
156
|
+
if (!event.defaultPrevented) {
|
|
157
|
+
_setVisible(false);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{ signal: controller.signal }
|
|
161
|
+
);
|
|
162
|
+
form.addEventListener(
|
|
163
|
+
'submit',
|
|
164
|
+
() => {
|
|
165
|
+
// always reset the visibility on submit regardless of whether the
|
|
166
|
+
// default action is prevented
|
|
167
|
+
_setVisible(false);
|
|
168
|
+
},
|
|
169
|
+
{ signal: controller.signal }
|
|
170
|
+
);
|
|
171
|
+
return () => {
|
|
172
|
+
controller.abort();
|
|
173
|
+
};
|
|
174
|
+
}, [inputRef, _setVisible]);
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<Primitive.input
|
|
178
|
+
{...props}
|
|
179
|
+
id={idProp ?? inputId}
|
|
180
|
+
autoCapitalize={autoCapitalize}
|
|
181
|
+
autoComplete={autoComplete}
|
|
182
|
+
ref={useComposedRefs(forwardedRef, inputRef)}
|
|
183
|
+
spellCheck={spellCheck}
|
|
184
|
+
type={visible ? 'text' : 'password'}
|
|
185
|
+
onBlur={composeEventHandlers(props.onBlur, (event) => {
|
|
186
|
+
// get the cursor position
|
|
187
|
+
const { selectionStart, selectionEnd } = event.currentTarget;
|
|
188
|
+
focusState.current.selectionStart = selectionStart;
|
|
189
|
+
focusState.current.selectionEnd = selectionEnd;
|
|
190
|
+
})}
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
PasswordToggleFieldInput.displayName = PASSWORD_TOGGLE_FIELD_INPUT_NAME;
|
|
196
|
+
|
|
197
|
+
/* -------------------------------------------------------------------------------------------------
|
|
198
|
+
* PasswordToggleFieldToggle
|
|
199
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
200
|
+
|
|
201
|
+
const PASSWORD_TOGGLE_FIELD_TOGGLE_NAME = PASSWORD_TOGGLE_FIELD_NAME + 'Toggle';
|
|
202
|
+
|
|
203
|
+
type PrimitiveButtonProps = React.ComponentPropsWithoutRef<'button'>;
|
|
204
|
+
|
|
205
|
+
interface PasswordToggleFieldToggleProps extends Omit<PrimitiveButtonProps, 'type'> {}
|
|
206
|
+
|
|
207
|
+
const PasswordToggleFieldToggle = React.forwardRef<
|
|
208
|
+
HTMLButtonElement,
|
|
209
|
+
PasswordToggleFieldToggleProps
|
|
210
|
+
>(
|
|
211
|
+
(
|
|
212
|
+
{
|
|
213
|
+
__scopePasswordToggleField,
|
|
214
|
+
onClick,
|
|
215
|
+
onPointerDown,
|
|
216
|
+
onPointerCancel,
|
|
217
|
+
onPointerUp,
|
|
218
|
+
onFocus,
|
|
219
|
+
children,
|
|
220
|
+
'aria-label': ariaLabelProp,
|
|
221
|
+
'aria-controls': ariaControls,
|
|
222
|
+
'aria-hidden': ariaHidden,
|
|
223
|
+
tabIndex,
|
|
224
|
+
...props
|
|
225
|
+
}: ScopedProps<PasswordToggleFieldToggleProps>,
|
|
226
|
+
forwardedRef
|
|
227
|
+
) => {
|
|
228
|
+
const { setVisible, visible, inputRef, inputId, focusState } = usePasswordToggleFieldContext(
|
|
229
|
+
PASSWORD_TOGGLE_FIELD_TOGGLE_NAME,
|
|
230
|
+
__scopePasswordToggleField
|
|
231
|
+
);
|
|
232
|
+
const [internalAriaLabel, setInternalAriaLabel] = React.useState<string | undefined>(undefined);
|
|
233
|
+
const elementRef = React.useRef<HTMLButtonElement>(null);
|
|
234
|
+
const ref = useComposedRefs(forwardedRef, elementRef);
|
|
235
|
+
const isHydrated = useIsHydrated();
|
|
236
|
+
|
|
237
|
+
React.useEffect(() => {
|
|
238
|
+
const element = elementRef.current;
|
|
239
|
+
if (!element || ariaLabelProp) {
|
|
240
|
+
setInternalAriaLabel(undefined);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const DEFAULT_ARIA_LABEL = visible ? 'Hide password' : 'Show password';
|
|
245
|
+
|
|
246
|
+
function checkForInnerTextLabel(textContent: string | undefined | null) {
|
|
247
|
+
const text = textContent ? textContent : undefined;
|
|
248
|
+
// If the element has inner text, no need to force an aria-label.
|
|
249
|
+
setInternalAriaLabel(text ? undefined : DEFAULT_ARIA_LABEL);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
checkForInnerTextLabel(element.textContent);
|
|
253
|
+
|
|
254
|
+
const observer = new MutationObserver((entries) => {
|
|
255
|
+
let textContent: string | undefined;
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
if (entry.type === 'characterData') {
|
|
258
|
+
if (element.textContent) {
|
|
259
|
+
textContent = element.textContent;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
checkForInnerTextLabel(textContent);
|
|
264
|
+
});
|
|
265
|
+
observer.observe(element, { characterData: true, subtree: true });
|
|
266
|
+
return () => {
|
|
267
|
+
observer.disconnect();
|
|
268
|
+
};
|
|
269
|
+
}, [visible, ariaLabelProp]);
|
|
270
|
+
|
|
271
|
+
const ariaLabel = ariaLabelProp || internalAriaLabel;
|
|
272
|
+
|
|
273
|
+
// Before hydration the button will not work, but we want to render it
|
|
274
|
+
// regardless to prevent potential layout shift. Hide it from assistive tech
|
|
275
|
+
// by default. Post-hydration it will be visible, focusable and associated
|
|
276
|
+
// with the input via aria-controls.
|
|
277
|
+
if (!isHydrated) {
|
|
278
|
+
ariaHidden ??= true;
|
|
279
|
+
tabIndex ??= -1;
|
|
280
|
+
} else {
|
|
281
|
+
ariaControls ??= inputId;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
React.useEffect(() => {
|
|
285
|
+
let cleanup = () => {};
|
|
286
|
+
const ownerWindow = elementRef.current?.ownerDocument?.defaultView || window;
|
|
287
|
+
const reset = () => (focusState.current.clickTriggered = false);
|
|
288
|
+
const handlePointerUp = () => (cleanup = requestIdleCallback(ownerWindow, reset));
|
|
289
|
+
ownerWindow.addEventListener('pointerup', handlePointerUp);
|
|
290
|
+
return () => {
|
|
291
|
+
cleanup();
|
|
292
|
+
ownerWindow.removeEventListener('pointerup', handlePointerUp);
|
|
293
|
+
};
|
|
294
|
+
}, [focusState]);
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<Primitive.button
|
|
298
|
+
aria-controls={ariaControls}
|
|
299
|
+
aria-hidden={ariaHidden}
|
|
300
|
+
aria-label={ariaLabel}
|
|
301
|
+
ref={ref}
|
|
302
|
+
id={inputId}
|
|
303
|
+
{...props}
|
|
304
|
+
onPointerDown={composeEventHandlers(onPointerDown, () => {
|
|
305
|
+
focusState.current.clickTriggered = true;
|
|
306
|
+
})}
|
|
307
|
+
onPointerCancel={(event) => {
|
|
308
|
+
// do not use `composeEventHandlers` here because we always want to
|
|
309
|
+
// reset the ref on cancellation, regardless of whether the user has
|
|
310
|
+
// called preventDefault on the event
|
|
311
|
+
onPointerCancel?.(event);
|
|
312
|
+
focusState.current = INITIAL_FOCUS_STATE;
|
|
313
|
+
}}
|
|
314
|
+
// do not use `composeEventHandlers` here because we always want to
|
|
315
|
+
// reset the ref after click, regardless of whether the user has
|
|
316
|
+
// called preventDefault on the event
|
|
317
|
+
onClick={(event) => {
|
|
318
|
+
onClick?.(event);
|
|
319
|
+
if (event.defaultPrevented) {
|
|
320
|
+
focusState.current = INITIAL_FOCUS_STATE;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
flushSync(() => {
|
|
325
|
+
setVisible((s) => !s);
|
|
326
|
+
});
|
|
327
|
+
if (focusState.current.clickTriggered) {
|
|
328
|
+
const input = inputRef.current;
|
|
329
|
+
if (input) {
|
|
330
|
+
const { selectionStart, selectionEnd } = focusState.current;
|
|
331
|
+
input.focus();
|
|
332
|
+
if (selectionStart !== null || selectionEnd !== null) {
|
|
333
|
+
// wait a tick so that focus has settled, then restore select position
|
|
334
|
+
requestAnimationFrame(() => {
|
|
335
|
+
// make sure the input still has focus (developer may have
|
|
336
|
+
// programatically moved focus elsewhere)
|
|
337
|
+
if (input.ownerDocument.activeElement === input) {
|
|
338
|
+
input.selectionStart = selectionStart;
|
|
339
|
+
input.selectionEnd = selectionEnd;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
focusState.current = INITIAL_FOCUS_STATE;
|
|
346
|
+
}}
|
|
347
|
+
onPointerUp={(event) => {
|
|
348
|
+
onPointerUp?.(event);
|
|
349
|
+
// if click handler hasn't been called at this point, it may have been
|
|
350
|
+
// intercepted, in which case we still want to reset our internal
|
|
351
|
+
// state
|
|
352
|
+
setTimeout(() => {
|
|
353
|
+
focusState.current = INITIAL_FOCUS_STATE;
|
|
354
|
+
}, 50);
|
|
355
|
+
}}
|
|
356
|
+
type="button"
|
|
357
|
+
>
|
|
358
|
+
{children}
|
|
359
|
+
</Primitive.button>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
PasswordToggleFieldToggle.displayName = PASSWORD_TOGGLE_FIELD_TOGGLE_NAME;
|
|
364
|
+
|
|
365
|
+
/* -------------------------------------------------------------------------------------------------
|
|
366
|
+
* PasswordToggleFieldSlot
|
|
367
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
368
|
+
|
|
369
|
+
const PASSWORD_TOGGLE_FIELD_SLOT_NAME = PASSWORD_TOGGLE_FIELD_NAME + 'Slot';
|
|
370
|
+
|
|
371
|
+
interface PasswordToggleFieldSlotDeclarativeProps {
|
|
372
|
+
visible: React.ReactNode;
|
|
373
|
+
hidden: React.ReactNode;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
interface PasswordToggleFieldSlotRenderProps {
|
|
377
|
+
render: (args: { visible: boolean }) => React.ReactElement;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
type PasswordToggleFieldSlotProps =
|
|
381
|
+
| PasswordToggleFieldSlotDeclarativeProps
|
|
382
|
+
| PasswordToggleFieldSlotRenderProps;
|
|
383
|
+
|
|
384
|
+
const PasswordToggleFieldSlot: React.FC<PasswordToggleFieldSlotProps> = ({
|
|
385
|
+
__scopePasswordToggleField,
|
|
386
|
+
...props
|
|
387
|
+
}: ScopedProps<PasswordToggleFieldSlotProps>) => {
|
|
388
|
+
const { visible } = usePasswordToggleFieldContext(
|
|
389
|
+
PASSWORD_TOGGLE_FIELD_SLOT_NAME,
|
|
390
|
+
__scopePasswordToggleField
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
return 'render' in props
|
|
394
|
+
? //
|
|
395
|
+
props.render({ visible })
|
|
396
|
+
: visible
|
|
397
|
+
? props.visible
|
|
398
|
+
: props.hidden;
|
|
399
|
+
};
|
|
400
|
+
PasswordToggleFieldSlot.displayName = PASSWORD_TOGGLE_FIELD_SLOT_NAME;
|
|
401
|
+
|
|
402
|
+
/* -------------------------------------------------------------------------------------------------
|
|
403
|
+
* PasswordToggleFieldIcon
|
|
404
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
405
|
+
|
|
406
|
+
const PASSWORD_TOGGLE_FIELD_ICON_NAME = PASSWORD_TOGGLE_FIELD_NAME + 'Icon';
|
|
407
|
+
|
|
408
|
+
type PrimitiveSvgProps = React.ComponentPropsWithoutRef<'svg'>;
|
|
409
|
+
|
|
410
|
+
interface PasswordToggleFieldIconProps extends Omit<PrimitiveSvgProps, 'children'> {
|
|
411
|
+
visible: React.ReactElement;
|
|
412
|
+
hidden: React.ReactElement;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const PasswordToggleFieldIcon = React.forwardRef<SVGSVGElement, PasswordToggleFieldIconProps>(
|
|
416
|
+
(
|
|
417
|
+
{
|
|
418
|
+
__scopePasswordToggleField,
|
|
419
|
+
// @ts-expect-error
|
|
420
|
+
children,
|
|
421
|
+
...props
|
|
422
|
+
}: ScopedProps<PasswordToggleFieldIconProps>,
|
|
423
|
+
forwardedRef
|
|
424
|
+
) => {
|
|
425
|
+
const { visible } = usePasswordToggleFieldContext(
|
|
426
|
+
PASSWORD_TOGGLE_FIELD_ICON_NAME,
|
|
427
|
+
__scopePasswordToggleField
|
|
428
|
+
);
|
|
429
|
+
const { visible: visibleIcon, hidden: hiddenIcon, ...domProps } = props;
|
|
430
|
+
return (
|
|
431
|
+
<Primitive.svg {...domProps} ref={forwardedRef} aria-hidden asChild>
|
|
432
|
+
{visible ? visibleIcon : hiddenIcon}
|
|
433
|
+
</Primitive.svg>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
PasswordToggleFieldIcon.displayName = PASSWORD_TOGGLE_FIELD_ICON_NAME;
|
|
438
|
+
|
|
439
|
+
export {
|
|
440
|
+
PasswordToggleField,
|
|
441
|
+
PasswordToggleFieldInput,
|
|
442
|
+
PasswordToggleFieldToggle,
|
|
443
|
+
PasswordToggleFieldSlot,
|
|
444
|
+
PasswordToggleFieldIcon,
|
|
445
|
+
//
|
|
446
|
+
PasswordToggleField as Root,
|
|
447
|
+
PasswordToggleFieldInput as Input,
|
|
448
|
+
PasswordToggleFieldToggle as Toggle,
|
|
449
|
+
PasswordToggleFieldSlot as Slot,
|
|
450
|
+
PasswordToggleFieldIcon as Icon,
|
|
451
|
+
};
|
|
452
|
+
export type {
|
|
453
|
+
PasswordToggleFieldProps,
|
|
454
|
+
PasswordToggleFieldInputProps,
|
|
455
|
+
PasswordToggleFieldToggleProps,
|
|
456
|
+
PasswordToggleFieldIconProps,
|
|
457
|
+
PasswordToggleFieldSlotProps,
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
function requestIdleCallback(
|
|
461
|
+
window: Window,
|
|
462
|
+
callback: IdleRequestCallback,
|
|
463
|
+
options?: IdleRequestOptions
|
|
464
|
+
): () => void {
|
|
465
|
+
if ((window as any).requestIdleCallback) {
|
|
466
|
+
const id = window.requestIdleCallback(callback, options);
|
|
467
|
+
return () => {
|
|
468
|
+
window.cancelIdleCallback(id);
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const start = Date.now();
|
|
472
|
+
const id = window.setTimeout(() => {
|
|
473
|
+
const timeRemaining = () => Math.max(0, 50 - (Date.now() - start));
|
|
474
|
+
callback({ didTimeout: false, timeRemaining });
|
|
475
|
+
}, 1);
|
|
476
|
+
return () => {
|
|
477
|
+
window.clearTimeout(id);
|
|
478
|
+
};
|
|
479
|
+
}
|