@octanejs/radix 0.1.2
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 +100 -0
- package/package.json +45 -0
- package/src/AccessibleIcon.ts +26 -0
- package/src/Accordion.ts +282 -0
- package/src/AlertDialog.ts +110 -0
- package/src/Arrow.ts +21 -0
- package/src/AspectRatio.ts +34 -0
- package/src/Avatar.ts +152 -0
- package/src/Checkbox.ts +325 -0
- package/src/Collapsible.ts +170 -0
- package/src/ContextMenu.ts +303 -0
- package/src/Dialog.ts +308 -0
- package/src/DismissableLayer.ts +413 -0
- package/src/DropdownMenu.ts +275 -0
- package/src/FocusScope.ts +266 -0
- package/src/Form.ts +621 -0
- package/src/HoverCard.ts +318 -0
- package/src/Label.ts +25 -0
- package/src/Menu.ts +1057 -0
- package/src/Menubar.ts +572 -0
- package/src/NavigationMenu.ts +1236 -0
- package/src/OneTimePasswordField.ts +977 -0
- package/src/PasswordToggleField.ts +495 -0
- package/src/Popover.ts +354 -0
- package/src/Popper.ts +401 -0
- package/src/Portal.ts +19 -0
- package/src/Presence.ts +225 -0
- package/src/Primitive.ts +53 -0
- package/src/Progress.ts +92 -0
- package/src/Radio.ts +262 -0
- package/src/RadioGroup.ts +212 -0
- package/src/RovingFocusGroup.ts +259 -0
- package/src/ScrollArea.ts +966 -0
- package/src/Select.ts +1901 -0
- package/src/Separator.ts +36 -0
- package/src/Slider.ts +745 -0
- package/src/Slot.ts +105 -0
- package/src/Switch.ts +278 -0
- package/src/Tabs.ts +177 -0
- package/src/Toast.ts +923 -0
- package/src/Toggle.ts +31 -0
- package/src/ToggleGroup.ts +157 -0
- package/src/Toolbar.ts +130 -0
- package/src/Tooltip.ts +669 -0
- package/src/VisuallyHidden.ts +29 -0
- package/src/collection.ts +97 -0
- package/src/compose-event-handlers.ts +15 -0
- package/src/compose-refs.ts +53 -0
- package/src/context.ts +109 -0
- package/src/direction.ts +19 -0
- package/src/focus-guards.ts +56 -0
- package/src/index.ts +54 -0
- package/src/internal.ts +42 -0
- package/src/scroll-lock.ts +57 -0
- package/src/use-callback-ref.ts +28 -0
- package/src/use-effect-event.ts +36 -0
- package/src/use-is-hydrated.ts +23 -0
- package/src/use-previous.ts +28 -0
- package/src/use-size.ts +57 -0
- package/src/useControllableState.ts +78 -0
- package/src/useId.ts +15 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
// Ported from @radix-ui/react-password-toggle-field (source:
|
|
2
|
+
// .radix-primitives/packages/react/password-toggle-field/src/password-toggle-field.tsx).
|
|
3
|
+
// A password input with a visibility toggle. `Root` owns the visibility state (via
|
|
4
|
+
// useControllableState), the shared input id, and cross-part focus bookkeeping. `Input`
|
|
5
|
+
// renders the native input, switching `type` between "password"/"text", and resets
|
|
6
|
+
// visibility to hidden on the owning form's `reset`/`submit` (so the browser never
|
|
7
|
+
// remembers a revealed value). `Toggle` is a plain `type=button` that flips visibility;
|
|
8
|
+
// it derives a default aria-label ("Show password"/"Hide password") only when it has no
|
|
9
|
+
// text content (MutationObserver-tracked), is aria-hidden/untabbable until hydrated, and
|
|
10
|
+
// keeps focus (and the selection recorded at input blur) inside the input when the
|
|
11
|
+
// toggle interaction started from a pointer (the `clickTriggered` focusState ref, reset
|
|
12
|
+
// via window `pointerup` + requestIdleCallback). `Slot` renders per-state children
|
|
13
|
+
// (visible/hidden props or a render callback); `Icon` projects the per-state icon
|
|
14
|
+
// element through `Primitive.svg asChild`.
|
|
15
|
+
//
|
|
16
|
+
// octane adaptations (all previously established in this binding):
|
|
17
|
+
// - Plain `.ts` + createElement; no forwardRef (`ref: forwardedRef` prop); explicit hook
|
|
18
|
+
// slots via S/subSlot (files skip the compiler's auto-slotting pass).
|
|
19
|
+
// - Events are NATIVE delegated DOM events (onClick/onPointerDown/onPointerCancel/
|
|
20
|
+
// onPointerUp/onBlur); the runtime patches `event.currentTarget` to the handler's
|
|
21
|
+
// element during delegated dispatch, so the Input's blur selection-capture works
|
|
22
|
+
// unchanged. `event.defaultPrevented` is the real DOM flag.
|
|
23
|
+
// - React's `flushSync` from 'react-dom' → octane's `flushSync`.
|
|
24
|
+
// - `spellCheck={false}` must render the literal `spellcheck="false"` attribute (octane's
|
|
25
|
+
// setAttribute removes `false`-valued non-aria attributes), so booleans are stringified.
|
|
26
|
+
// - octane's useControllableState takes `{ prop, defaultProp, onChange }` (no `caller` —
|
|
27
|
+
// that only feeds React dev warnings).
|
|
28
|
+
// - Dev-only console.warn surfaces are skipped (repo policy: functional outcomes only) —
|
|
29
|
+
// the misspelled `onVisiblityChange` prop FALLBACK is functional and is kept; its
|
|
30
|
+
// deprecation warning effect is not.
|
|
31
|
+
// Faithfully-kept source quirks (do not "fix"): the Toggle button sets `id={inputId}`
|
|
32
|
+
// before its prop spread (upstream renders the duplicate id), it destructures the
|
|
33
|
+
// user's `onFocus` without ever attaching it (upstream swallows it), and it computes a
|
|
34
|
+
// pre-hydration `tabIndex ??= -1` that is never rendered onto the button (upstream
|
|
35
|
+
// swallows `tabIndex` too).
|
|
36
|
+
import { createElement, flushSync, useCallback, useEffect, useRef, useState } from 'octane';
|
|
37
|
+
|
|
38
|
+
import { composeEventHandlers } from './compose-event-handlers';
|
|
39
|
+
import { useComposedRefs } from './compose-refs';
|
|
40
|
+
import { createContextScope } from './context';
|
|
41
|
+
import { S, subSlot } from './internal';
|
|
42
|
+
import { Primitive } from './Primitive';
|
|
43
|
+
import { useEffectEvent } from './use-effect-event';
|
|
44
|
+
import { useId } from './useId';
|
|
45
|
+
import { useIsHydrated } from './use-is-hydrated';
|
|
46
|
+
import { useControllableState } from './useControllableState';
|
|
47
|
+
|
|
48
|
+
const PASSWORD_TOGGLE_FIELD_NAME = 'PasswordToggleField';
|
|
49
|
+
|
|
50
|
+
/* -------------------------------------------------------------------------------------------------
|
|
51
|
+
* PasswordToggleFieldProvider
|
|
52
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
53
|
+
|
|
54
|
+
interface InternalFocusState {
|
|
55
|
+
clickTriggered: boolean;
|
|
56
|
+
selectionStart: number | null;
|
|
57
|
+
selectionEnd: number | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface PasswordToggleFieldContextValue {
|
|
61
|
+
inputId: string;
|
|
62
|
+
inputRef: { current: HTMLInputElement | null };
|
|
63
|
+
visible: boolean;
|
|
64
|
+
setVisible: (visible: boolean | ((prev: boolean) => boolean)) => void;
|
|
65
|
+
syncInputId: (providedId: string | number | undefined) => void;
|
|
66
|
+
focusState: { current: InternalFocusState };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const [createPasswordToggleFieldContext, createPasswordToggleFieldScope] = createContextScope(
|
|
70
|
+
PASSWORD_TOGGLE_FIELD_NAME,
|
|
71
|
+
);
|
|
72
|
+
export { createPasswordToggleFieldScope };
|
|
73
|
+
|
|
74
|
+
const [PasswordToggleFieldProvider, usePasswordToggleFieldContext] =
|
|
75
|
+
createPasswordToggleFieldContext<PasswordToggleFieldContextValue>(PASSWORD_TOGGLE_FIELD_NAME);
|
|
76
|
+
|
|
77
|
+
/* -------------------------------------------------------------------------------------------------
|
|
78
|
+
* PasswordToggleField
|
|
79
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
80
|
+
|
|
81
|
+
const INITIAL_FOCUS_STATE: InternalFocusState = {
|
|
82
|
+
clickTriggered: false,
|
|
83
|
+
selectionStart: null,
|
|
84
|
+
selectionEnd: null,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export function PasswordToggleField(props: any): any {
|
|
88
|
+
const slot = S('PasswordToggleField.Root');
|
|
89
|
+
const {
|
|
90
|
+
__scopePasswordToggleField,
|
|
91
|
+
visible: visibleProp,
|
|
92
|
+
defaultVisible,
|
|
93
|
+
children,
|
|
94
|
+
} = props ?? {};
|
|
95
|
+
|
|
96
|
+
const baseId = useId(props?.id, subSlot(slot, 'id'));
|
|
97
|
+
const defaultInputId = `${baseId}-input`;
|
|
98
|
+
const [inputIdState, setInputIdState] = useState<null | string>(
|
|
99
|
+
defaultInputId,
|
|
100
|
+
subSlot(slot, 'inputId'),
|
|
101
|
+
);
|
|
102
|
+
const inputId = inputIdState ?? defaultInputId;
|
|
103
|
+
const syncInputId = useCallback(
|
|
104
|
+
(providedId: string | number | undefined) =>
|
|
105
|
+
setInputIdState(providedId != null ? String(providedId) : null),
|
|
106
|
+
[],
|
|
107
|
+
subSlot(slot, 'syncId'),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Functional half of the source's misspelled-prop handling: honor a legacy
|
|
111
|
+
// `onVisiblityChange` when the correctly-spelled prop is absent. (The dev-only
|
|
112
|
+
// deprecation console.warn effect is intentionally not ported.)
|
|
113
|
+
let onVisibilityChange = props?.onVisibilityChange;
|
|
114
|
+
if (
|
|
115
|
+
!onVisibilityChange &&
|
|
116
|
+
props != null &&
|
|
117
|
+
'onVisiblityChange' in props &&
|
|
118
|
+
typeof props.onVisiblityChange === 'function'
|
|
119
|
+
) {
|
|
120
|
+
onVisibilityChange = props.onVisiblityChange;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const [visible = false, setVisible] = useControllableState<boolean>(
|
|
124
|
+
{
|
|
125
|
+
prop: visibleProp,
|
|
126
|
+
defaultProp: defaultVisible ?? false,
|
|
127
|
+
onChange: onVisibilityChange,
|
|
128
|
+
},
|
|
129
|
+
subSlot(slot, 'visible'),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const inputRef = useRef<HTMLInputElement | null>(null, subSlot(slot, 'inputRef'));
|
|
133
|
+
const focusState = useRef<InternalFocusState>(INITIAL_FOCUS_STATE, subSlot(slot, 'focusState'));
|
|
134
|
+
|
|
135
|
+
return createElement(PasswordToggleFieldProvider, {
|
|
136
|
+
scope: __scopePasswordToggleField,
|
|
137
|
+
inputId,
|
|
138
|
+
inputRef,
|
|
139
|
+
setVisible,
|
|
140
|
+
syncInputId,
|
|
141
|
+
visible,
|
|
142
|
+
focusState,
|
|
143
|
+
children,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* -------------------------------------------------------------------------------------------------
|
|
148
|
+
* PasswordToggleFieldInput
|
|
149
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
150
|
+
|
|
151
|
+
const PASSWORD_TOGGLE_FIELD_INPUT_NAME = PASSWORD_TOGGLE_FIELD_NAME + 'Input';
|
|
152
|
+
|
|
153
|
+
export function PasswordToggleFieldInput(props: any): any {
|
|
154
|
+
const slot = S('PasswordToggleField.Input');
|
|
155
|
+
const {
|
|
156
|
+
__scopePasswordToggleField,
|
|
157
|
+
autoComplete = 'current-password',
|
|
158
|
+
autoCapitalize = 'off',
|
|
159
|
+
spellCheck = false,
|
|
160
|
+
id: idProp,
|
|
161
|
+
ref: forwardedRef,
|
|
162
|
+
onBlur,
|
|
163
|
+
...inputProps
|
|
164
|
+
} = props ?? {};
|
|
165
|
+
const { visible, inputRef, inputId, syncInputId, setVisible, focusState } =
|
|
166
|
+
usePasswordToggleFieldContext(PASSWORD_TOGGLE_FIELD_INPUT_NAME, __scopePasswordToggleField);
|
|
167
|
+
|
|
168
|
+
useEffect(
|
|
169
|
+
() => {
|
|
170
|
+
syncInputId(idProp);
|
|
171
|
+
},
|
|
172
|
+
[idProp, syncInputId],
|
|
173
|
+
subSlot(slot, 'e:syncId'),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// We want to reset the visibility to `false` to revert the input to
|
|
177
|
+
// `type="password"` when:
|
|
178
|
+
// - The form is reset (for consistency with other form controls)
|
|
179
|
+
// - The form is submitted (to prevent the browser from remembering the
|
|
180
|
+
// input's value.
|
|
181
|
+
//
|
|
182
|
+
// See "Keeping things secure":
|
|
183
|
+
// https://technology.blog.gov.uk/2021/04/19/simple-things-are-complicated-making-a-show-password-option/)
|
|
184
|
+
const _setVisible = useEffectEvent(setVisible, subSlot(slot, 'setVisible'));
|
|
185
|
+
useEffect(
|
|
186
|
+
() => {
|
|
187
|
+
const inputElement = inputRef.current;
|
|
188
|
+
const form = inputElement?.form;
|
|
189
|
+
if (!form) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const controller = new AbortController();
|
|
194
|
+
form.addEventListener(
|
|
195
|
+
'reset',
|
|
196
|
+
(event: Event) => {
|
|
197
|
+
if (!event.defaultPrevented) {
|
|
198
|
+
_setVisible(false);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{ signal: controller.signal },
|
|
202
|
+
);
|
|
203
|
+
form.addEventListener(
|
|
204
|
+
'submit',
|
|
205
|
+
() => {
|
|
206
|
+
// always reset the visibility on submit regardless of whether the
|
|
207
|
+
// default action is prevented
|
|
208
|
+
_setVisible(false);
|
|
209
|
+
},
|
|
210
|
+
{ signal: controller.signal },
|
|
211
|
+
);
|
|
212
|
+
return () => {
|
|
213
|
+
controller.abort();
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
[inputRef],
|
|
217
|
+
subSlot(slot, 'e:form'),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const composedRefs = useComposedRefs(forwardedRef, inputRef, subSlot(slot, 'refs'));
|
|
221
|
+
|
|
222
|
+
return createElement(Primitive.input, {
|
|
223
|
+
...inputProps,
|
|
224
|
+
id: idProp ?? inputId,
|
|
225
|
+
autoCapitalize,
|
|
226
|
+
autoComplete,
|
|
227
|
+
ref: composedRefs,
|
|
228
|
+
// octane: boolean spellCheck must render the literal "true"/"false" attribute value
|
|
229
|
+
// (octane's setAttribute removes `false`-valued attributes; React stringifies it).
|
|
230
|
+
spellCheck: typeof spellCheck === 'boolean' ? String(spellCheck) : spellCheck,
|
|
231
|
+
type: visible ? 'text' : 'password',
|
|
232
|
+
onBlur: composeEventHandlers(onBlur, (event: FocusEvent) => {
|
|
233
|
+
// get the cursor position
|
|
234
|
+
const { selectionStart, selectionEnd } = event.currentTarget as HTMLInputElement;
|
|
235
|
+
focusState.current.selectionStart = selectionStart;
|
|
236
|
+
focusState.current.selectionEnd = selectionEnd;
|
|
237
|
+
}),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* -------------------------------------------------------------------------------------------------
|
|
242
|
+
* PasswordToggleFieldToggle
|
|
243
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
244
|
+
|
|
245
|
+
const PASSWORD_TOGGLE_FIELD_TOGGLE_NAME = PASSWORD_TOGGLE_FIELD_NAME + 'Toggle';
|
|
246
|
+
|
|
247
|
+
export function PasswordToggleFieldToggle(props: any): any {
|
|
248
|
+
const slot = S('PasswordToggleField.Toggle');
|
|
249
|
+
const {
|
|
250
|
+
__scopePasswordToggleField,
|
|
251
|
+
onClick,
|
|
252
|
+
onPointerDown,
|
|
253
|
+
onPointerCancel,
|
|
254
|
+
onPointerUp,
|
|
255
|
+
// Kept verbatim from the source: `onFocus` is destructured off the props and never
|
|
256
|
+
// attached to the button (the upstream component swallows it).
|
|
257
|
+
onFocus: _onFocus,
|
|
258
|
+
children,
|
|
259
|
+
'aria-label': ariaLabelProp,
|
|
260
|
+
ref: forwardedRef,
|
|
261
|
+
...toggleProps
|
|
262
|
+
} = props ?? {};
|
|
263
|
+
let { 'aria-controls': ariaControls, 'aria-hidden': ariaHidden, tabIndex } = toggleProps;
|
|
264
|
+
delete toggleProps['aria-controls'];
|
|
265
|
+
delete toggleProps['aria-hidden'];
|
|
266
|
+
delete toggleProps.tabIndex;
|
|
267
|
+
|
|
268
|
+
const { setVisible, visible, inputRef, inputId, focusState } = usePasswordToggleFieldContext(
|
|
269
|
+
PASSWORD_TOGGLE_FIELD_TOGGLE_NAME,
|
|
270
|
+
__scopePasswordToggleField,
|
|
271
|
+
);
|
|
272
|
+
const [internalAriaLabel, setInternalAriaLabel] = useState<string | undefined>(
|
|
273
|
+
undefined,
|
|
274
|
+
subSlot(slot, 'ariaLabel'),
|
|
275
|
+
);
|
|
276
|
+
const elementRef = useRef<HTMLButtonElement | null>(null, subSlot(slot, 'el'));
|
|
277
|
+
const ref = useComposedRefs(forwardedRef, elementRef, subSlot(slot, 'refs'));
|
|
278
|
+
const isHydrated = useIsHydrated(subSlot(slot, 'hydrated'));
|
|
279
|
+
|
|
280
|
+
useEffect(
|
|
281
|
+
() => {
|
|
282
|
+
const element = elementRef.current;
|
|
283
|
+
if (!element || ariaLabelProp) {
|
|
284
|
+
setInternalAriaLabel(undefined);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const DEFAULT_ARIA_LABEL = visible ? 'Hide password' : 'Show password';
|
|
289
|
+
|
|
290
|
+
function checkForInnerTextLabel(textContent: string | undefined | null): void {
|
|
291
|
+
const text = textContent ? textContent : undefined;
|
|
292
|
+
// If the element has inner text, no need to force an aria-label.
|
|
293
|
+
setInternalAriaLabel(text ? undefined : DEFAULT_ARIA_LABEL);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
checkForInnerTextLabel(element.textContent);
|
|
297
|
+
|
|
298
|
+
const observer = new MutationObserver((entries) => {
|
|
299
|
+
let textContent: string | undefined;
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
if (entry.type === 'characterData') {
|
|
302
|
+
if (element.textContent) {
|
|
303
|
+
textContent = element.textContent;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
checkForInnerTextLabel(textContent);
|
|
308
|
+
});
|
|
309
|
+
observer.observe(element, { characterData: true, subtree: true });
|
|
310
|
+
return () => {
|
|
311
|
+
observer.disconnect();
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
[visible, ariaLabelProp],
|
|
315
|
+
subSlot(slot, 'e:label'),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const ariaLabel = ariaLabelProp || internalAriaLabel;
|
|
319
|
+
|
|
320
|
+
// Before hydration the button will not work, but we want to render it
|
|
321
|
+
// regardless to prevent potential layout shift. Hide it from assistive tech
|
|
322
|
+
// by default. Post-hydration it will be visible, focusable and associated
|
|
323
|
+
// with the input via aria-controls.
|
|
324
|
+
if (!isHydrated) {
|
|
325
|
+
ariaHidden ??= true;
|
|
326
|
+
// Kept verbatim from the source: `tabIndex` is destructured and defaulted here but
|
|
327
|
+
// never rendered onto the button (upstream swallows it, user-provided or not).
|
|
328
|
+
tabIndex ??= -1;
|
|
329
|
+
} else {
|
|
330
|
+
ariaControls ??= inputId;
|
|
331
|
+
}
|
|
332
|
+
void tabIndex;
|
|
333
|
+
|
|
334
|
+
useEffect(
|
|
335
|
+
() => {
|
|
336
|
+
let cleanup = (): void => {};
|
|
337
|
+
const ownerWindow = elementRef.current?.ownerDocument?.defaultView || window;
|
|
338
|
+
const reset = (): boolean => (focusState.current.clickTriggered = false);
|
|
339
|
+
const handlePointerUp = (): void => {
|
|
340
|
+
cleanup = requestIdleCallback(ownerWindow, reset);
|
|
341
|
+
};
|
|
342
|
+
ownerWindow.addEventListener('pointerup', handlePointerUp);
|
|
343
|
+
return () => {
|
|
344
|
+
cleanup();
|
|
345
|
+
ownerWindow.removeEventListener('pointerup', handlePointerUp);
|
|
346
|
+
};
|
|
347
|
+
},
|
|
348
|
+
[focusState],
|
|
349
|
+
subSlot(slot, 'e:pointerup'),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
return createElement(Primitive.button, {
|
|
353
|
+
'aria-controls': ariaControls,
|
|
354
|
+
'aria-hidden': ariaHidden,
|
|
355
|
+
'aria-label': ariaLabel,
|
|
356
|
+
ref,
|
|
357
|
+
id: inputId,
|
|
358
|
+
...toggleProps,
|
|
359
|
+
onPointerDown: composeEventHandlers(onPointerDown, () => {
|
|
360
|
+
focusState.current.clickTriggered = true;
|
|
361
|
+
}),
|
|
362
|
+
onPointerCancel: (event: PointerEvent) => {
|
|
363
|
+
// do not use `composeEventHandlers` here because we always want to
|
|
364
|
+
// reset the ref on cancellation, regardless of whether the user has
|
|
365
|
+
// called preventDefault on the event
|
|
366
|
+
onPointerCancel?.(event);
|
|
367
|
+
focusState.current = INITIAL_FOCUS_STATE;
|
|
368
|
+
},
|
|
369
|
+
// do not use `composeEventHandlers` here because we always want to
|
|
370
|
+
// reset the ref after click, regardless of whether the user has
|
|
371
|
+
// called preventDefault on the event
|
|
372
|
+
onClick: (event: MouseEvent) => {
|
|
373
|
+
onClick?.(event);
|
|
374
|
+
if (event.defaultPrevented) {
|
|
375
|
+
focusState.current = INITIAL_FOCUS_STATE;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
flushSync(() => {
|
|
380
|
+
setVisible((s: boolean) => !s);
|
|
381
|
+
});
|
|
382
|
+
if (focusState.current.clickTriggered) {
|
|
383
|
+
const input = inputRef.current;
|
|
384
|
+
if (input) {
|
|
385
|
+
const { selectionStart, selectionEnd } = focusState.current;
|
|
386
|
+
input.focus();
|
|
387
|
+
if (selectionStart !== null || selectionEnd !== null) {
|
|
388
|
+
// wait a tick so that focus has settled, then restore select position
|
|
389
|
+
requestAnimationFrame(() => {
|
|
390
|
+
// make sure the input still has focus (developer may have
|
|
391
|
+
// programatically moved focus elsewhere)
|
|
392
|
+
if (input.ownerDocument.activeElement === input) {
|
|
393
|
+
input.selectionStart = selectionStart;
|
|
394
|
+
input.selectionEnd = selectionEnd;
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
focusState.current = INITIAL_FOCUS_STATE;
|
|
401
|
+
},
|
|
402
|
+
onPointerUp: (event: PointerEvent) => {
|
|
403
|
+
onPointerUp?.(event);
|
|
404
|
+
// if click handler hasn't been called at this point, it may have been
|
|
405
|
+
// intercepted, in which case we still want to reset our internal
|
|
406
|
+
// state
|
|
407
|
+
setTimeout(() => {
|
|
408
|
+
focusState.current = INITIAL_FOCUS_STATE;
|
|
409
|
+
}, 50);
|
|
410
|
+
},
|
|
411
|
+
type: 'button',
|
|
412
|
+
children,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/* -------------------------------------------------------------------------------------------------
|
|
417
|
+
* PasswordToggleFieldSlot
|
|
418
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
419
|
+
|
|
420
|
+
const PASSWORD_TOGGLE_FIELD_SLOT_NAME = PASSWORD_TOGGLE_FIELD_NAME + 'Slot';
|
|
421
|
+
|
|
422
|
+
export function PasswordToggleFieldSlot(props: any): any {
|
|
423
|
+
const { __scopePasswordToggleField, ...slotProps } = props ?? {};
|
|
424
|
+
const { visible } = usePasswordToggleFieldContext(
|
|
425
|
+
PASSWORD_TOGGLE_FIELD_SLOT_NAME,
|
|
426
|
+
__scopePasswordToggleField,
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
return 'render' in slotProps
|
|
430
|
+
? //
|
|
431
|
+
slotProps.render({ visible })
|
|
432
|
+
: visible
|
|
433
|
+
? slotProps.visible
|
|
434
|
+
: slotProps.hidden;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* -------------------------------------------------------------------------------------------------
|
|
438
|
+
* PasswordToggleFieldIcon
|
|
439
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
440
|
+
|
|
441
|
+
const PASSWORD_TOGGLE_FIELD_ICON_NAME = PASSWORD_TOGGLE_FIELD_NAME + 'Icon';
|
|
442
|
+
|
|
443
|
+
export function PasswordToggleFieldIcon(props: any): any {
|
|
444
|
+
const {
|
|
445
|
+
__scopePasswordToggleField,
|
|
446
|
+
// (source drops `children` — the icon elements come through the
|
|
447
|
+
// `visible`/`hidden` props)
|
|
448
|
+
children: _children,
|
|
449
|
+
ref: forwardedRef,
|
|
450
|
+
...rest
|
|
451
|
+
} = props ?? {};
|
|
452
|
+
const { visible } = usePasswordToggleFieldContext(
|
|
453
|
+
PASSWORD_TOGGLE_FIELD_ICON_NAME,
|
|
454
|
+
__scopePasswordToggleField,
|
|
455
|
+
);
|
|
456
|
+
const { visible: visibleIcon, hidden: hiddenIcon, ...domProps } = rest;
|
|
457
|
+
return createElement(Primitive.svg, {
|
|
458
|
+
...domProps,
|
|
459
|
+
ref: forwardedRef,
|
|
460
|
+
'aria-hidden': true,
|
|
461
|
+
asChild: true,
|
|
462
|
+
children: visible ? visibleIcon : hiddenIcon,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/* -----------------------------------------------------------------------------------------------*/
|
|
467
|
+
|
|
468
|
+
export {
|
|
469
|
+
PasswordToggleField as Root,
|
|
470
|
+
PasswordToggleFieldInput as Input,
|
|
471
|
+
PasswordToggleFieldToggle as Toggle,
|
|
472
|
+
PasswordToggleFieldSlot as Slot,
|
|
473
|
+
PasswordToggleFieldIcon as Icon,
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
function requestIdleCallback(
|
|
477
|
+
window: Window & typeof globalThis,
|
|
478
|
+
callback: IdleRequestCallback,
|
|
479
|
+
options?: IdleRequestOptions,
|
|
480
|
+
): () => void {
|
|
481
|
+
if ((window as any).requestIdleCallback) {
|
|
482
|
+
const id = window.requestIdleCallback(callback, options);
|
|
483
|
+
return () => {
|
|
484
|
+
window.cancelIdleCallback(id);
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const start = Date.now();
|
|
488
|
+
const id = window.setTimeout(() => {
|
|
489
|
+
const timeRemaining = (): number => Math.max(0, 50 - (Date.now() - start));
|
|
490
|
+
callback({ didTimeout: false, timeRemaining });
|
|
491
|
+
}, 1);
|
|
492
|
+
return () => {
|
|
493
|
+
window.clearTimeout(id);
|
|
494
|
+
};
|
|
495
|
+
}
|