merge new into master

This commit is contained in:
2026-02-05 17:31:20 +00:00
16267 changed files with 2194867 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,13 @@
# `react-password-toggle-field`
## Installation
```sh
$ yarn add radix-ui
# or
$ npm install radix-ui
```
## Usage
View docs [here](https://radix-ui.com/primitives/docs/components/password-toggle-field).

View File

@@ -0,0 +1,41 @@
import * as React from 'react';
interface PasswordToggleFieldProps {
id?: string;
visible?: boolean;
defaultVisible?: boolean;
onVisiblityChange?: (visible: boolean) => void;
children?: React.ReactNode;
}
declare const PasswordToggleField: React.FC<PasswordToggleFieldProps>;
type PrimitiveInputProps = React.ComponentPropsWithoutRef<'input'>;
interface PasswordToggleFieldOwnProps {
autoComplete?: 'current-password' | 'new-password';
}
interface PasswordToggleFieldInputProps extends PasswordToggleFieldOwnProps, Omit<PrimitiveInputProps, keyof PasswordToggleFieldOwnProps | 'type'> {
autoComplete?: 'current-password' | 'new-password';
}
declare const PasswordToggleFieldInput: React.ForwardRefExoticComponent<PasswordToggleFieldInputProps & React.RefAttributes<HTMLInputElement>>;
type PrimitiveButtonProps = React.ComponentPropsWithoutRef<'button'>;
interface PasswordToggleFieldToggleProps extends Omit<PrimitiveButtonProps, 'type'> {
}
declare const PasswordToggleFieldToggle: React.ForwardRefExoticComponent<PasswordToggleFieldToggleProps & React.RefAttributes<HTMLButtonElement>>;
interface PasswordToggleFieldSlotDeclarativeProps {
visible: React.ReactNode;
hidden: React.ReactNode;
}
interface PasswordToggleFieldSlotRenderProps {
render: (args: {
visible: boolean;
}) => React.ReactElement;
}
type PasswordToggleFieldSlotProps = PasswordToggleFieldSlotDeclarativeProps | PasswordToggleFieldSlotRenderProps;
declare const PasswordToggleFieldSlot: React.FC<PasswordToggleFieldSlotProps>;
type PrimitiveSvgProps = React.ComponentPropsWithoutRef<'svg'>;
interface PasswordToggleFieldIconProps extends Omit<PrimitiveSvgProps, 'children'> {
visible: React.ReactElement;
hidden: React.ReactElement;
}
declare const PasswordToggleFieldIcon: React.ForwardRefExoticComponent<PasswordToggleFieldIconProps & React.RefAttributes<SVGSVGElement>>;
export { PasswordToggleFieldIcon as Icon, PasswordToggleFieldInput as Input, PasswordToggleField, PasswordToggleFieldIcon, type PasswordToggleFieldIconProps, PasswordToggleFieldInput, type PasswordToggleFieldInputProps, type PasswordToggleFieldProps, PasswordToggleFieldSlot, type PasswordToggleFieldSlotProps, PasswordToggleFieldToggle, type PasswordToggleFieldToggleProps, PasswordToggleField as Root, PasswordToggleFieldSlot as Slot, PasswordToggleFieldToggle as Toggle };

View File

@@ -0,0 +1,41 @@
import * as React from 'react';
interface PasswordToggleFieldProps {
id?: string;
visible?: boolean;
defaultVisible?: boolean;
onVisiblityChange?: (visible: boolean) => void;
children?: React.ReactNode;
}
declare const PasswordToggleField: React.FC<PasswordToggleFieldProps>;
type PrimitiveInputProps = React.ComponentPropsWithoutRef<'input'>;
interface PasswordToggleFieldOwnProps {
autoComplete?: 'current-password' | 'new-password';
}
interface PasswordToggleFieldInputProps extends PasswordToggleFieldOwnProps, Omit<PrimitiveInputProps, keyof PasswordToggleFieldOwnProps | 'type'> {
autoComplete?: 'current-password' | 'new-password';
}
declare const PasswordToggleFieldInput: React.ForwardRefExoticComponent<PasswordToggleFieldInputProps & React.RefAttributes<HTMLInputElement>>;
type PrimitiveButtonProps = React.ComponentPropsWithoutRef<'button'>;
interface PasswordToggleFieldToggleProps extends Omit<PrimitiveButtonProps, 'type'> {
}
declare const PasswordToggleFieldToggle: React.ForwardRefExoticComponent<PasswordToggleFieldToggleProps & React.RefAttributes<HTMLButtonElement>>;
interface PasswordToggleFieldSlotDeclarativeProps {
visible: React.ReactNode;
hidden: React.ReactNode;
}
interface PasswordToggleFieldSlotRenderProps {
render: (args: {
visible: boolean;
}) => React.ReactElement;
}
type PasswordToggleFieldSlotProps = PasswordToggleFieldSlotDeclarativeProps | PasswordToggleFieldSlotRenderProps;
declare const PasswordToggleFieldSlot: React.FC<PasswordToggleFieldSlotProps>;
type PrimitiveSvgProps = React.ComponentPropsWithoutRef<'svg'>;
interface PasswordToggleFieldIconProps extends Omit<PrimitiveSvgProps, 'children'> {
visible: React.ReactElement;
hidden: React.ReactElement;
}
declare const PasswordToggleFieldIcon: React.ForwardRefExoticComponent<PasswordToggleFieldIconProps & React.RefAttributes<SVGSVGElement>>;
export { PasswordToggleFieldIcon as Icon, PasswordToggleFieldInput as Input, PasswordToggleField, PasswordToggleFieldIcon, type PasswordToggleFieldIconProps, PasswordToggleFieldInput, type PasswordToggleFieldInputProps, type PasswordToggleFieldProps, PasswordToggleFieldSlot, type PasswordToggleFieldSlotProps, PasswordToggleFieldToggle, type PasswordToggleFieldToggleProps, PasswordToggleField as Root, PasswordToggleFieldSlot as Slot, PasswordToggleFieldToggle as Toggle };

View File

@@ -0,0 +1,339 @@
"use strict";
"use client";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
Icon: () => PasswordToggleFieldIcon,
Input: () => PasswordToggleFieldInput,
PasswordToggleField: () => PasswordToggleField,
PasswordToggleFieldIcon: () => PasswordToggleFieldIcon,
PasswordToggleFieldInput: () => PasswordToggleFieldInput,
PasswordToggleFieldSlot: () => PasswordToggleFieldSlot,
PasswordToggleFieldToggle: () => PasswordToggleFieldToggle,
Root: () => PasswordToggleField,
Slot: () => PasswordToggleFieldSlot,
Toggle: () => PasswordToggleFieldToggle
});
module.exports = __toCommonJS(index_exports);
// src/password-toggle-field.tsx
var React = __toESM(require("react"));
var import_react_dom = require("react-dom");
var import_primitive = require("@radix-ui/primitive");
var import_react_use_controllable_state = require("@radix-ui/react-use-controllable-state");
var import_react_primitive = require("@radix-ui/react-primitive");
var import_react_compose_refs = require("@radix-ui/react-compose-refs");
var import_react_id = require("@radix-ui/react-id");
var import_react_use_is_hydrated = require("@radix-ui/react-use-is-hydrated");
var import_react_use_effect_event = require("@radix-ui/react-use-effect-event");
var import_react_context = require("@radix-ui/react-context");
var import_jsx_runtime = require("react/jsx-runtime");
var PASSWORD_TOGGLE_FIELD_NAME = "PasswordToggleField";
var [createPasswordToggleFieldContext] = (0, import_react_context.createContextScope)(PASSWORD_TOGGLE_FIELD_NAME);
var [PasswordToggleFieldProvider, usePasswordToggleFieldContext] = createPasswordToggleFieldContext(PASSWORD_TOGGLE_FIELD_NAME);
var INITIAL_FOCUS_STATE = {
clickTriggered: false,
selectionStart: null,
selectionEnd: null
};
var PasswordToggleField = ({
__scopePasswordToggleField,
...props
}) => {
const baseId = (0, import_react_id.useId)(props.id);
const defaultInputId = `${baseId}-input`;
const [inputIdState, setInputIdState] = React.useState(defaultInputId);
const inputId = inputIdState ?? defaultInputId;
const syncInputId = React.useCallback(
(providedId) => setInputIdState(providedId != null ? String(providedId) : null),
[]
);
const { visible: visibleProp, defaultVisible, onVisiblityChange, children } = props;
const [visible = false, setVisible] = (0, import_react_use_controllable_state.useControllableState)({
caller: PASSWORD_TOGGLE_FIELD_NAME,
prop: visibleProp,
defaultProp: defaultVisible ?? false,
onChange: onVisiblityChange
});
const inputRef = React.useRef(null);
const focusState = React.useRef(INITIAL_FOCUS_STATE);
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
PasswordToggleFieldProvider,
{
scope: __scopePasswordToggleField,
inputId,
inputRef,
setVisible,
syncInputId,
visible,
focusState,
children
}
);
};
PasswordToggleField.displayName = PASSWORD_TOGGLE_FIELD_NAME;
var PASSWORD_TOGGLE_FIELD_INPUT_NAME = PASSWORD_TOGGLE_FIELD_NAME + "Input";
var PasswordToggleFieldInput = React.forwardRef(
({
__scopePasswordToggleField,
autoComplete = "current-password",
autoCapitalize = "off",
spellCheck = false,
id: idProp,
...props
}, forwardedRef) => {
const { visible, inputRef, inputId, syncInputId, setVisible, focusState } = usePasswordToggleFieldContext(PASSWORD_TOGGLE_FIELD_INPUT_NAME, __scopePasswordToggleField);
React.useEffect(() => {
syncInputId(idProp);
}, [idProp, syncInputId]);
const _setVisible = (0, import_react_use_effect_event.useEffectEvent)(setVisible);
React.useEffect(() => {
const inputElement = inputRef.current;
const form = inputElement?.form;
if (!form) {
return;
}
const controller = new AbortController();
form.addEventListener(
"reset",
(event) => {
if (!event.defaultPrevented) {
_setVisible(false);
}
},
{ signal: controller.signal }
);
form.addEventListener(
"submit",
() => {
_setVisible(false);
},
{ signal: controller.signal }
);
return () => {
controller.abort();
};
}, [inputRef, _setVisible]);
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_react_primitive.Primitive.input,
{
...props,
id: idProp ?? inputId,
autoCapitalize,
autoComplete,
ref: (0, import_react_compose_refs.useComposedRefs)(forwardedRef, inputRef),
spellCheck,
type: visible ? "text" : "password",
onBlur: (0, import_primitive.composeEventHandlers)(props.onBlur, (event) => {
const { selectionStart, selectionEnd } = event.currentTarget;
focusState.current.selectionStart = selectionStart;
focusState.current.selectionEnd = selectionEnd;
})
}
);
}
);
PasswordToggleFieldInput.displayName = PASSWORD_TOGGLE_FIELD_INPUT_NAME;
var PASSWORD_TOGGLE_FIELD_TOGGLE_NAME = PASSWORD_TOGGLE_FIELD_NAME + "Toggle";
var PasswordToggleFieldToggle = React.forwardRef(
({
__scopePasswordToggleField,
onClick,
onPointerDown,
onPointerCancel,
onPointerUp,
onFocus,
children,
"aria-label": ariaLabelProp,
"aria-controls": ariaControls,
"aria-hidden": ariaHidden,
tabIndex,
...props
}, forwardedRef) => {
const { setVisible, visible, inputRef, inputId, focusState } = usePasswordToggleFieldContext(
PASSWORD_TOGGLE_FIELD_TOGGLE_NAME,
__scopePasswordToggleField
);
const [internalAriaLabel, setInternalAriaLabel] = React.useState(void 0);
const elementRef = React.useRef(null);
const ref = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, elementRef);
const isHydrated = (0, import_react_use_is_hydrated.useIsHydrated)();
React.useEffect(() => {
const element = elementRef.current;
if (!element || ariaLabelProp) {
setInternalAriaLabel(void 0);
return;
}
const DEFAULT_ARIA_LABEL = visible ? "Hide password" : "Show password";
function checkForInnerTextLabel(textContent) {
const text = textContent ? textContent : void 0;
setInternalAriaLabel(text ? void 0 : DEFAULT_ARIA_LABEL);
}
checkForInnerTextLabel(element.textContent);
const observer = new MutationObserver((entries) => {
let textContent;
for (const entry of entries) {
if (entry.type === "characterData") {
if (element.textContent) {
textContent = element.textContent;
}
}
}
checkForInnerTextLabel(textContent);
});
observer.observe(element, { characterData: true, subtree: true });
return () => {
observer.disconnect();
};
}, [visible, ariaLabelProp]);
const ariaLabel = ariaLabelProp || internalAriaLabel;
if (!isHydrated) {
ariaHidden ??= true;
tabIndex ??= -1;
} else {
ariaControls ??= inputId;
}
React.useEffect(() => {
let cleanup = () => {
};
const ownerWindow = elementRef.current?.ownerDocument?.defaultView || window;
const reset = () => focusState.current.clickTriggered = false;
const handlePointerUp = () => cleanup = requestIdleCallback(ownerWindow, reset);
ownerWindow.addEventListener("pointerup", handlePointerUp);
return () => {
cleanup();
ownerWindow.removeEventListener("pointerup", handlePointerUp);
};
}, [focusState]);
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_react_primitive.Primitive.button,
{
"aria-controls": ariaControls,
"aria-hidden": ariaHidden,
"aria-label": ariaLabel,
ref,
id: inputId,
...props,
onPointerDown: (0, import_primitive.composeEventHandlers)(onPointerDown, () => {
focusState.current.clickTriggered = true;
}),
onPointerCancel: (event) => {
onPointerCancel?.(event);
focusState.current = INITIAL_FOCUS_STATE;
},
onClick: (event) => {
onClick?.(event);
if (event.defaultPrevented) {
focusState.current = INITIAL_FOCUS_STATE;
return;
}
(0, import_react_dom.flushSync)(() => {
setVisible((s) => !s);
});
if (focusState.current.clickTriggered) {
const input = inputRef.current;
if (input) {
const { selectionStart, selectionEnd } = focusState.current;
input.focus();
if (selectionStart !== null || selectionEnd !== null) {
requestAnimationFrame(() => {
if (input.ownerDocument.activeElement === input) {
input.selectionStart = selectionStart;
input.selectionEnd = selectionEnd;
}
});
}
}
}
focusState.current = INITIAL_FOCUS_STATE;
},
onPointerUp: (event) => {
onPointerUp?.(event);
setTimeout(() => {
focusState.current = INITIAL_FOCUS_STATE;
}, 50);
},
type: "button",
children
}
);
}
);
PasswordToggleFieldToggle.displayName = PASSWORD_TOGGLE_FIELD_TOGGLE_NAME;
var PASSWORD_TOGGLE_FIELD_SLOT_NAME = PASSWORD_TOGGLE_FIELD_NAME + "Slot";
var PasswordToggleFieldSlot = ({
__scopePasswordToggleField,
...props
}) => {
const { visible } = usePasswordToggleFieldContext(
PASSWORD_TOGGLE_FIELD_SLOT_NAME,
__scopePasswordToggleField
);
return "render" in props ? (
//
props.render({ visible })
) : visible ? props.visible : props.hidden;
};
PasswordToggleFieldSlot.displayName = PASSWORD_TOGGLE_FIELD_SLOT_NAME;
var PASSWORD_TOGGLE_FIELD_ICON_NAME = PASSWORD_TOGGLE_FIELD_NAME + "Icon";
var PasswordToggleFieldIcon = React.forwardRef(
({
__scopePasswordToggleField,
// @ts-expect-error
children,
...props
}, forwardedRef) => {
const { visible } = usePasswordToggleFieldContext(
PASSWORD_TOGGLE_FIELD_ICON_NAME,
__scopePasswordToggleField
);
const { visible: visibleIcon, hidden: hiddenIcon, ...domProps } = props;
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_primitive.Primitive.svg, { ...domProps, ref: forwardedRef, "aria-hidden": true, asChild: true, children: visible ? visibleIcon : hiddenIcon });
}
);
PasswordToggleFieldIcon.displayName = PASSWORD_TOGGLE_FIELD_ICON_NAME;
function requestIdleCallback(window2, callback, options) {
if (window2.requestIdleCallback) {
const id2 = window2.requestIdleCallback(callback, options);
return () => {
window2.cancelIdleCallback(id2);
};
}
const start = Date.now();
const id = window2.setTimeout(() => {
const timeRemaining = () => Math.max(0, 50 - (Date.now() - start));
callback({ didTimeout: false, timeRemaining });
}, 1);
return () => {
window2.clearTimeout(id);
};
}
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,307 @@
"use client";
// src/password-toggle-field.tsx
import * as React from "react";
import { flushSync } from "react-dom";
import { composeEventHandlers } from "@radix-ui/primitive";
import { useControllableState } from "@radix-ui/react-use-controllable-state";
import { Primitive } from "@radix-ui/react-primitive";
import { useComposedRefs } from "@radix-ui/react-compose-refs";
import { useId } from "@radix-ui/react-id";
import { useIsHydrated } from "@radix-ui/react-use-is-hydrated";
import { useEffectEvent } from "@radix-ui/react-use-effect-event";
import { createContextScope } from "@radix-ui/react-context";
import { jsx } from "react/jsx-runtime";
var PASSWORD_TOGGLE_FIELD_NAME = "PasswordToggleField";
var [createPasswordToggleFieldContext] = createContextScope(PASSWORD_TOGGLE_FIELD_NAME);
var [PasswordToggleFieldProvider, usePasswordToggleFieldContext] = createPasswordToggleFieldContext(PASSWORD_TOGGLE_FIELD_NAME);
var INITIAL_FOCUS_STATE = {
clickTriggered: false,
selectionStart: null,
selectionEnd: null
};
var PasswordToggleField = ({
__scopePasswordToggleField,
...props
}) => {
const baseId = useId(props.id);
const defaultInputId = `${baseId}-input`;
const [inputIdState, setInputIdState] = React.useState(defaultInputId);
const inputId = inputIdState ?? defaultInputId;
const syncInputId = React.useCallback(
(providedId) => setInputIdState(providedId != null ? String(providedId) : null),
[]
);
const { visible: visibleProp, defaultVisible, onVisiblityChange, children } = props;
const [visible = false, setVisible] = useControllableState({
caller: PASSWORD_TOGGLE_FIELD_NAME,
prop: visibleProp,
defaultProp: defaultVisible ?? false,
onChange: onVisiblityChange
});
const inputRef = React.useRef(null);
const focusState = React.useRef(INITIAL_FOCUS_STATE);
return /* @__PURE__ */ jsx(
PasswordToggleFieldProvider,
{
scope: __scopePasswordToggleField,
inputId,
inputRef,
setVisible,
syncInputId,
visible,
focusState,
children
}
);
};
PasswordToggleField.displayName = PASSWORD_TOGGLE_FIELD_NAME;
var PASSWORD_TOGGLE_FIELD_INPUT_NAME = PASSWORD_TOGGLE_FIELD_NAME + "Input";
var PasswordToggleFieldInput = React.forwardRef(
({
__scopePasswordToggleField,
autoComplete = "current-password",
autoCapitalize = "off",
spellCheck = false,
id: idProp,
...props
}, forwardedRef) => {
const { visible, inputRef, inputId, syncInputId, setVisible, focusState } = usePasswordToggleFieldContext(PASSWORD_TOGGLE_FIELD_INPUT_NAME, __scopePasswordToggleField);
React.useEffect(() => {
syncInputId(idProp);
}, [idProp, syncInputId]);
const _setVisible = useEffectEvent(setVisible);
React.useEffect(() => {
const inputElement = inputRef.current;
const form = inputElement?.form;
if (!form) {
return;
}
const controller = new AbortController();
form.addEventListener(
"reset",
(event) => {
if (!event.defaultPrevented) {
_setVisible(false);
}
},
{ signal: controller.signal }
);
form.addEventListener(
"submit",
() => {
_setVisible(false);
},
{ signal: controller.signal }
);
return () => {
controller.abort();
};
}, [inputRef, _setVisible]);
return /* @__PURE__ */ jsx(
Primitive.input,
{
...props,
id: idProp ?? inputId,
autoCapitalize,
autoComplete,
ref: useComposedRefs(forwardedRef, inputRef),
spellCheck,
type: visible ? "text" : "password",
onBlur: composeEventHandlers(props.onBlur, (event) => {
const { selectionStart, selectionEnd } = event.currentTarget;
focusState.current.selectionStart = selectionStart;
focusState.current.selectionEnd = selectionEnd;
})
}
);
}
);
PasswordToggleFieldInput.displayName = PASSWORD_TOGGLE_FIELD_INPUT_NAME;
var PASSWORD_TOGGLE_FIELD_TOGGLE_NAME = PASSWORD_TOGGLE_FIELD_NAME + "Toggle";
var PasswordToggleFieldToggle = React.forwardRef(
({
__scopePasswordToggleField,
onClick,
onPointerDown,
onPointerCancel,
onPointerUp,
onFocus,
children,
"aria-label": ariaLabelProp,
"aria-controls": ariaControls,
"aria-hidden": ariaHidden,
tabIndex,
...props
}, forwardedRef) => {
const { setVisible, visible, inputRef, inputId, focusState } = usePasswordToggleFieldContext(
PASSWORD_TOGGLE_FIELD_TOGGLE_NAME,
__scopePasswordToggleField
);
const [internalAriaLabel, setInternalAriaLabel] = React.useState(void 0);
const elementRef = React.useRef(null);
const ref = useComposedRefs(forwardedRef, elementRef);
const isHydrated = useIsHydrated();
React.useEffect(() => {
const element = elementRef.current;
if (!element || ariaLabelProp) {
setInternalAriaLabel(void 0);
return;
}
const DEFAULT_ARIA_LABEL = visible ? "Hide password" : "Show password";
function checkForInnerTextLabel(textContent) {
const text = textContent ? textContent : void 0;
setInternalAriaLabel(text ? void 0 : DEFAULT_ARIA_LABEL);
}
checkForInnerTextLabel(element.textContent);
const observer = new MutationObserver((entries) => {
let textContent;
for (const entry of entries) {
if (entry.type === "characterData") {
if (element.textContent) {
textContent = element.textContent;
}
}
}
checkForInnerTextLabel(textContent);
});
observer.observe(element, { characterData: true, subtree: true });
return () => {
observer.disconnect();
};
}, [visible, ariaLabelProp]);
const ariaLabel = ariaLabelProp || internalAriaLabel;
if (!isHydrated) {
ariaHidden ??= true;
tabIndex ??= -1;
} else {
ariaControls ??= inputId;
}
React.useEffect(() => {
let cleanup = () => {
};
const ownerWindow = elementRef.current?.ownerDocument?.defaultView || window;
const reset = () => focusState.current.clickTriggered = false;
const handlePointerUp = () => cleanup = requestIdleCallback(ownerWindow, reset);
ownerWindow.addEventListener("pointerup", handlePointerUp);
return () => {
cleanup();
ownerWindow.removeEventListener("pointerup", handlePointerUp);
};
}, [focusState]);
return /* @__PURE__ */ jsx(
Primitive.button,
{
"aria-controls": ariaControls,
"aria-hidden": ariaHidden,
"aria-label": ariaLabel,
ref,
id: inputId,
...props,
onPointerDown: composeEventHandlers(onPointerDown, () => {
focusState.current.clickTriggered = true;
}),
onPointerCancel: (event) => {
onPointerCancel?.(event);
focusState.current = INITIAL_FOCUS_STATE;
},
onClick: (event) => {
onClick?.(event);
if (event.defaultPrevented) {
focusState.current = INITIAL_FOCUS_STATE;
return;
}
flushSync(() => {
setVisible((s) => !s);
});
if (focusState.current.clickTriggered) {
const input = inputRef.current;
if (input) {
const { selectionStart, selectionEnd } = focusState.current;
input.focus();
if (selectionStart !== null || selectionEnd !== null) {
requestAnimationFrame(() => {
if (input.ownerDocument.activeElement === input) {
input.selectionStart = selectionStart;
input.selectionEnd = selectionEnd;
}
});
}
}
}
focusState.current = INITIAL_FOCUS_STATE;
},
onPointerUp: (event) => {
onPointerUp?.(event);
setTimeout(() => {
focusState.current = INITIAL_FOCUS_STATE;
}, 50);
},
type: "button",
children
}
);
}
);
PasswordToggleFieldToggle.displayName = PASSWORD_TOGGLE_FIELD_TOGGLE_NAME;
var PASSWORD_TOGGLE_FIELD_SLOT_NAME = PASSWORD_TOGGLE_FIELD_NAME + "Slot";
var PasswordToggleFieldSlot = ({
__scopePasswordToggleField,
...props
}) => {
const { visible } = usePasswordToggleFieldContext(
PASSWORD_TOGGLE_FIELD_SLOT_NAME,
__scopePasswordToggleField
);
return "render" in props ? (
//
props.render({ visible })
) : visible ? props.visible : props.hidden;
};
PasswordToggleFieldSlot.displayName = PASSWORD_TOGGLE_FIELD_SLOT_NAME;
var PASSWORD_TOGGLE_FIELD_ICON_NAME = PASSWORD_TOGGLE_FIELD_NAME + "Icon";
var PasswordToggleFieldIcon = React.forwardRef(
({
__scopePasswordToggleField,
// @ts-expect-error
children,
...props
}, forwardedRef) => {
const { visible } = usePasswordToggleFieldContext(
PASSWORD_TOGGLE_FIELD_ICON_NAME,
__scopePasswordToggleField
);
const { visible: visibleIcon, hidden: hiddenIcon, ...domProps } = props;
return /* @__PURE__ */ jsx(Primitive.svg, { ...domProps, ref: forwardedRef, "aria-hidden": true, asChild: true, children: visible ? visibleIcon : hiddenIcon });
}
);
PasswordToggleFieldIcon.displayName = PASSWORD_TOGGLE_FIELD_ICON_NAME;
function requestIdleCallback(window2, callback, options) {
if (window2.requestIdleCallback) {
const id2 = window2.requestIdleCallback(callback, options);
return () => {
window2.cancelIdleCallback(id2);
};
}
const start = Date.now();
const id = window2.setTimeout(() => {
const timeRemaining = () => Math.max(0, 50 - (Date.now() - start));
callback({ didTimeout: false, timeRemaining });
}, 1);
return () => {
window2.clearTimeout(id);
};
}
export {
PasswordToggleFieldIcon as Icon,
PasswordToggleFieldInput as Input,
PasswordToggleField,
PasswordToggleFieldIcon,
PasswordToggleFieldInput,
PasswordToggleFieldSlot,
PasswordToggleFieldToggle,
PasswordToggleField as Root,
PasswordToggleFieldSlot as Slot,
PasswordToggleFieldToggle as Toggle
};
//# sourceMappingURL=index.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,76 @@
{
"name": "@radix-ui/react-password-toggle-field",
"version": "0.1.3",
"license": "MIT",
"source": "./src/index.ts",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"files": [
"src",
"dist",
"README.md"
],
"sideEffects": false,
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-is-hydrated": "0.1.0"
},
"devDependencies": {
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"eslint": "^9.18.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript": "^5.7.3",
"@repo/builder": "0.0.0",
"@repo/eslint-config": "0.0.0",
"@repo/typescript-config": "0.0.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
},
"homepage": "https://radix-ui.com/primitives",
"repository": {
"type": "git",
"url": "git+https://github.com/radix-ui/primitives.git"
},
"bugs": {
"url": "https://github.com/radix-ui/primitives/issues"
},
"scripts": {
"lint": "eslint --max-warnings 0 src",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit",
"build": "radix-build"
},
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
}
}

View File

@@ -0,0 +1,21 @@
'use client';
export type {
PasswordToggleFieldProps,
PasswordToggleFieldInputProps,
PasswordToggleFieldToggleProps,
PasswordToggleFieldIconProps,
PasswordToggleFieldSlotProps,
} from './password-toggle-field';
export {
PasswordToggleField,
PasswordToggleFieldInput,
PasswordToggleFieldToggle,
PasswordToggleFieldIcon,
PasswordToggleFieldSlot,
//
Root,
Input,
Toggle,
Icon,
Slot,
} from './password-toggle-field';

View File

@@ -0,0 +1,219 @@
import { axe } from 'vitest-axe';
import type { RenderResult } from '@testing-library/react';
import { act, cleanup, render, screen } from '@testing-library/react';
import * as PasswordToggleField from './password-toggle-field';
import { afterEach, describe, it, beforeEach, expect } from 'vitest';
import { userEvent, type UserEvent } from '@testing-library/user-event';
describe('given a default PasswordToggleField', () => {
let rendered: RenderResult;
let user: UserEvent;
afterEach(cleanup);
beforeEach(() => {
user = userEvent.setup();
rendered = render(
<>
<label htmlFor="password">Password</label>
<PasswordToggleField.Root>
<PasswordToggleField.Input id="password" />
<PasswordToggleField.Toggle>
<PasswordToggleField.Slot visible="Hide" hidden="Show" />
</PasswordToggleField.Toggle>
</PasswordToggleField.Root>
</>
);
});
afterEach(cleanup);
it('should have no accessibility violations', async () => {
expect(await axe(rendered.container)).toHaveNoViolations();
});
it('should initially be hidden', () => {
const toggle = screen.getByRole('button', { name: 'Show' });
expect(toggle.textContent).toBe('Show');
});
it('toggles the children when clicked', async () => {
const toggle = screen.getByRole('button', { name: 'Show' });
await act(async () => await user.click(toggle));
expect(toggle.textContent).toBe('Hide');
});
it('should initially render input as `type=password`', async () => {
const input = screen.getByLabelText<HTMLInputElement>('Password');
expect(input.type).toBe('password');
});
it('should initially render input as `type=text` when toggled', async () => {
const input = screen.getByLabelText<HTMLInputElement>('Password');
const toggle = screen.getByRole('button', { name: 'Show' });
await act(async () => await user.click(toggle));
expect(input.type).toBe('text');
});
it('should re-focus the input after toggling', async () => {
const input = screen.getByLabelText<HTMLInputElement>('Password');
const toggle = screen.getByRole('button', { name: 'Show' });
await act(async () => await user.click(toggle));
expect(document.activeElement).toBe(input);
});
it("should restore the input's selection after toggling", async () => {
const input = screen.getByLabelText<HTMLInputElement>('Password');
const toggle = screen.getByRole('button', { name: 'Show' });
await act(async () => await user.click(input));
await act(async () => await user.type(input, 'p'));
await act(async () => await user.click(toggle));
// selection should be at the end of the input value
expect(input.selectionStart).toBe(1);
expect(input.selectionEnd).toBe(1);
await act(async () => await user.type(input, 'assword'));
input.selectionStart = 0;
input.selectionEnd = 4;
await act(async () => await user.click(toggle));
expect(input.selectionStart).toBe(0);
expect(input.selectionEnd).toBe(4);
});
});
describe('given a PasswordToggleField with an Icon', () => {
let rendered: RenderResult;
let user: UserEvent;
afterEach(cleanup);
beforeEach(() => {
user = userEvent.setup();
rendered = render(
<>
<label htmlFor="password">Password</label>
<PasswordToggleField.Root>
<PasswordToggleField.Input id="password" />
<PasswordToggleField.Toggle>
<PasswordToggleField.Icon
visible={<EyeOpenIcon data-testid="open" />}
hidden={<EyeClosedIcon data-testid="closed" />}
/>
</PasswordToggleField.Toggle>
</PasswordToggleField.Root>
</>
);
});
afterEach(cleanup);
it('should have no accessibility violations', async () => {
expect(await axe(rendered.container)).toHaveNoViolations();
});
it('should initially be hidden', () => {
const icon = screen.getByTestId('closed');
expect(icon).toBeVisible();
expect(screen.queryByTestId('open')).not.toBeInTheDocument();
});
it('toggles the icon when clicked', async () => {
const toggle = screen.getByRole('button', { name: 'Show password' });
await act(async () => await user.click(toggle));
expect(screen.getByTestId('open')).toBeVisible();
expect(screen.queryByTestId('closed')).not.toBeInTheDocument();
});
it('should initially render input as `type=password`', async () => {
const input = screen.getByLabelText<HTMLInputElement>('Password');
expect(input.type).toBe('password');
});
it('should initially render input as `type=text` when toggled', async () => {
const input = screen.getByLabelText<HTMLInputElement>('Password');
const toggle = screen.getByRole('button', { name: 'Show password' });
await act(async () => await user.click(toggle));
expect(input.type).toBe('text');
});
it('should retain its value when toggled', async () => {
const input = screen.getByLabelText('Password');
const toggle = screen.getByRole('button', { name: 'Show password' });
await act(async () => await user.type(input, 'pass123'));
await act(async () => await user.click(toggle));
expect(input).toHaveValue('pass123');
});
});
describe('given a PasswordToggleField in a form', () => {
let user: UserEvent;
afterEach(cleanup);
beforeEach(() => {
user = userEvent.setup();
render(
<form onSubmit={(e) => e.preventDefault()}>
<label htmlFor="password">Password</label>
<PasswordToggleField.Root>
<PasswordToggleField.Input id="password" />
<PasswordToggleField.Toggle>
<PasswordToggleField.Slot visible="Hide" hidden="Show" />
</PasswordToggleField.Toggle>
</PasswordToggleField.Root>
<button>Submit</button>
</form>
);
});
afterEach(cleanup);
it('should reset visibility to hidden after submission', async () => {
const toggle = screen.getByRole('button', { name: 'Show' });
const input = screen.getByLabelText<HTMLInputElement>('Password');
await act(() => user.click(toggle));
expect(input.type).toBe('text');
const submitButton = screen.getByText('Submit');
await act(() => user.click(submitButton));
expect(input.type).toBe('password');
});
});
function EyeClosedIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M14.7649 6.07596C14.9991 6.22231 15.0703 6.53079 14.9239 6.76495C14.4849 7.46743 13.9632 8.10645 13.3702 8.66305L14.5712 9.86406C14.7664 10.0593 14.7664 10.3759 14.5712 10.5712C14.3759 10.7664 14.0593 10.7664 13.8641 10.5712L12.6011 9.30817C11.805 9.90283 10.9089 10.3621 9.93375 10.651L10.383 12.3277C10.4544 12.5944 10.2961 12.8685 10.0294 12.94C9.76267 13.0115 9.4885 12.8532 9.41704 12.5865L8.95917 10.8775C8.48743 10.958 8.00036 10.9999 7.50001 10.9999C6.99965 10.9999 6.51257 10.958 6.04082 10.8775L5.58299 12.5864C5.51153 12.8532 5.23737 13.0115 4.97064 12.94C4.7039 12.8686 4.5456 12.5944 4.61706 12.3277L5.06625 10.651C4.09111 10.3621 3.19503 9.90282 2.3989 9.30815L1.1359 10.5712C0.940638 10.7664 0.624058 10.7664 0.428798 10.5712C0.233537 10.3759 0.233537 10.0593 0.428798 9.86405L1.62982 8.66303C1.03682 8.10643 0.515113 7.46742 0.0760677 6.76495C-0.0702867 6.53079 0.000898544 6.22231 0.235065 6.07596C0.469231 5.9296 0.777703 6.00079 0.924058 6.23496C1.40354 7.00213 1.989 7.68057 2.66233 8.2427C2.67315 8.25096 2.6837 8.25972 2.69397 8.26898C4.00897 9.35527 5.65537 9.99991 7.50001 9.99991C10.3078 9.99991 12.6564 8.5063 14.076 6.23495C14.2223 6.00079 14.5308 5.9296 14.7649 6.07596Z"
fillRule="evenodd"
clipRule="evenodd"
/>
</svg>
);
}
function EyeOpenIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M7.5 11C4.80285 11 2.52952 9.62184 1.09622 7.50001C2.52952 5.37816 4.80285 4 7.5 4C10.1971 4 12.4705 5.37816 13.9038 7.50001C12.4705 9.62183 10.1971 11 7.5 11ZM7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C1.65639 10.2936 4.30786 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C13.3436 4.70638 10.6921 3 7.5 3ZM7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5C9.5 6.39543 8.60457 5.5 7.5 5.5C6.39543 5.5 5.5 6.39543 5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5Z"
fillRule="evenodd"
clipRule="evenodd"
/>
</svg>
);
}

View File

@@ -0,0 +1,479 @@
import * as React from 'react';
import { flushSync } from 'react-dom';
import { composeEventHandlers } from '@radix-ui/primitive';
import { useControllableState } from '@radix-ui/react-use-controllable-state';
import { Primitive } from '@radix-ui/react-primitive';
import { useComposedRefs } from '@radix-ui/react-compose-refs';
import { useId } from '@radix-ui/react-id';
import { useIsHydrated } from '@radix-ui/react-use-is-hydrated';
import { useEffectEvent } from '@radix-ui/react-use-effect-event';
import type { Scope } from '@radix-ui/react-context';
import { createContextScope } from '@radix-ui/react-context';
const PASSWORD_TOGGLE_FIELD_NAME = 'PasswordToggleField';
/* -------------------------------------------------------------------------------------------------
* PasswordToggleFieldProvider
* -----------------------------------------------------------------------------------------------*/
type InternalFocusState = {
clickTriggered: boolean;
selectionStart: number | null;
selectionEnd: number | null;
};
interface PasswordToggleFieldContextValue {
inputId: string;
inputRef: React.RefObject<HTMLInputElement | null>;
visible: boolean;
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
syncInputId: (providedId: string | number | undefined) => void;
focusState: React.RefObject<InternalFocusState>;
}
const [createPasswordToggleFieldContext] = createContextScope(PASSWORD_TOGGLE_FIELD_NAME);
const [PasswordToggleFieldProvider, usePasswordToggleFieldContext] =
createPasswordToggleFieldContext<PasswordToggleFieldContextValue>(PASSWORD_TOGGLE_FIELD_NAME);
/* -------------------------------------------------------------------------------------------------
* PasswordToggleField
* -----------------------------------------------------------------------------------------------*/
type ScopedProps<P> = P & { __scopePasswordToggleField?: Scope };
interface PasswordToggleFieldProps {
id?: string;
visible?: boolean;
defaultVisible?: boolean;
onVisiblityChange?: (visible: boolean) => void;
children?: React.ReactNode;
}
const INITIAL_FOCUS_STATE: InternalFocusState = {
clickTriggered: false,
selectionStart: null,
selectionEnd: null,
};
const PasswordToggleField: React.FC<PasswordToggleFieldProps> = ({
__scopePasswordToggleField,
...props
}: ScopedProps<PasswordToggleFieldProps>) => {
const baseId = useId(props.id);
const defaultInputId = `${baseId}-input`;
const [inputIdState, setInputIdState] = React.useState<null | string>(defaultInputId);
const inputId = inputIdState ?? defaultInputId;
const syncInputId = React.useCallback(
(providedId: string | number | undefined) =>
setInputIdState(providedId != null ? String(providedId) : null),
[]
);
const { visible: visibleProp, defaultVisible, onVisiblityChange, children } = props;
const [visible = false, setVisible] = useControllableState({
caller: PASSWORD_TOGGLE_FIELD_NAME,
prop: visibleProp,
defaultProp: defaultVisible ?? false,
onChange: onVisiblityChange,
});
const inputRef = React.useRef<HTMLInputElement | null>(null);
const focusState = React.useRef<InternalFocusState>(INITIAL_FOCUS_STATE);
return (
<PasswordToggleFieldProvider
scope={__scopePasswordToggleField}
inputId={inputId}
inputRef={inputRef}
setVisible={setVisible}
syncInputId={syncInputId}
visible={visible}
focusState={focusState}
>
{children}
</PasswordToggleFieldProvider>
);
};
PasswordToggleField.displayName = PASSWORD_TOGGLE_FIELD_NAME;
/* -------------------------------------------------------------------------------------------------
* PasswordToggleFieldInput
* -----------------------------------------------------------------------------------------------*/
const PASSWORD_TOGGLE_FIELD_INPUT_NAME = PASSWORD_TOGGLE_FIELD_NAME + 'Input';
type PrimitiveInputProps = React.ComponentPropsWithoutRef<'input'>;
interface PasswordToggleFieldOwnProps {
autoComplete?: 'current-password' | 'new-password';
}
interface PasswordToggleFieldInputProps
extends PasswordToggleFieldOwnProps,
Omit<PrimitiveInputProps, keyof PasswordToggleFieldOwnProps | 'type'> {
autoComplete?: 'current-password' | 'new-password';
}
const PasswordToggleFieldInput = React.forwardRef<HTMLInputElement, PasswordToggleFieldInputProps>(
(
{
__scopePasswordToggleField,
autoComplete = 'current-password',
autoCapitalize = 'off',
spellCheck = false,
id: idProp,
...props
}: ScopedProps<PasswordToggleFieldInputProps>,
forwardedRef
) => {
const { visible, inputRef, inputId, syncInputId, setVisible, focusState } =
usePasswordToggleFieldContext(PASSWORD_TOGGLE_FIELD_INPUT_NAME, __scopePasswordToggleField);
React.useEffect(() => {
syncInputId(idProp);
}, [idProp, syncInputId]);
// We want to reset the visibility to `false` to revert the input to
// `type="password"` when:
// - The form is reset (for consistency with other form controls)
// - The form is submitted (to prevent the browser from remembering the
// input's value.
//
// See "Keeping things secure":
// https://technology.blog.gov.uk/2021/04/19/simple-things-are-complicated-making-a-show-password-option/)
const _setVisible = useEffectEvent(setVisible);
React.useEffect(() => {
const inputElement = inputRef.current;
const form = inputElement?.form;
if (!form) {
return;
}
const controller = new AbortController();
form.addEventListener(
'reset',
(event) => {
if (!event.defaultPrevented) {
_setVisible(false);
}
},
{ signal: controller.signal }
);
form.addEventListener(
'submit',
() => {
// always reset the visibility on submit regardless of whether the
// default action is prevented
_setVisible(false);
},
{ signal: controller.signal }
);
return () => {
controller.abort();
};
}, [inputRef, _setVisible]);
return (
<Primitive.input
{...props}
id={idProp ?? inputId}
autoCapitalize={autoCapitalize}
autoComplete={autoComplete}
ref={useComposedRefs(forwardedRef, inputRef)}
spellCheck={spellCheck}
type={visible ? 'text' : 'password'}
onBlur={composeEventHandlers(props.onBlur, (event) => {
// get the cursor position
const { selectionStart, selectionEnd } = event.currentTarget;
focusState.current.selectionStart = selectionStart;
focusState.current.selectionEnd = selectionEnd;
})}
/>
);
}
);
PasswordToggleFieldInput.displayName = PASSWORD_TOGGLE_FIELD_INPUT_NAME;
/* -------------------------------------------------------------------------------------------------
* PasswordToggleFieldToggle
* -----------------------------------------------------------------------------------------------*/
const PASSWORD_TOGGLE_FIELD_TOGGLE_NAME = PASSWORD_TOGGLE_FIELD_NAME + 'Toggle';
type PrimitiveButtonProps = React.ComponentPropsWithoutRef<'button'>;
interface PasswordToggleFieldToggleProps extends Omit<PrimitiveButtonProps, 'type'> {}
const PasswordToggleFieldToggle = React.forwardRef<
HTMLButtonElement,
PasswordToggleFieldToggleProps
>(
(
{
__scopePasswordToggleField,
onClick,
onPointerDown,
onPointerCancel,
onPointerUp,
onFocus,
children,
'aria-label': ariaLabelProp,
'aria-controls': ariaControls,
'aria-hidden': ariaHidden,
tabIndex,
...props
}: ScopedProps<PasswordToggleFieldToggleProps>,
forwardedRef
) => {
const { setVisible, visible, inputRef, inputId, focusState } = usePasswordToggleFieldContext(
PASSWORD_TOGGLE_FIELD_TOGGLE_NAME,
__scopePasswordToggleField
);
const [internalAriaLabel, setInternalAriaLabel] = React.useState<string | undefined>(undefined);
const elementRef = React.useRef<HTMLButtonElement>(null);
const ref = useComposedRefs(forwardedRef, elementRef);
const isHydrated = useIsHydrated();
React.useEffect(() => {
const element = elementRef.current;
if (!element || ariaLabelProp) {
setInternalAriaLabel(undefined);
return;
}
const DEFAULT_ARIA_LABEL = visible ? 'Hide password' : 'Show password';
function checkForInnerTextLabel(textContent: string | undefined | null) {
const text = textContent ? textContent : undefined;
// If the element has inner text, no need to force an aria-label.
setInternalAriaLabel(text ? undefined : DEFAULT_ARIA_LABEL);
}
checkForInnerTextLabel(element.textContent);
const observer = new MutationObserver((entries) => {
let textContent: string | undefined;
for (const entry of entries) {
if (entry.type === 'characterData') {
if (element.textContent) {
textContent = element.textContent;
}
}
}
checkForInnerTextLabel(textContent);
});
observer.observe(element, { characterData: true, subtree: true });
return () => {
observer.disconnect();
};
}, [visible, ariaLabelProp]);
const ariaLabel = ariaLabelProp || internalAriaLabel;
// Before hydration the button will not work, but we want to render it
// regardless to prevent potential layout shift. Hide it from assistive tech
// by default. Post-hydration it will be visible, focusable and associated
// with the input via aria-controls.
if (!isHydrated) {
ariaHidden ??= true;
tabIndex ??= -1;
} else {
ariaControls ??= inputId;
}
React.useEffect(() => {
let cleanup = () => {};
const ownerWindow = elementRef.current?.ownerDocument?.defaultView || window;
const reset = () => (focusState.current.clickTriggered = false);
const handlePointerUp = () => (cleanup = requestIdleCallback(ownerWindow, reset));
ownerWindow.addEventListener('pointerup', handlePointerUp);
return () => {
cleanup();
ownerWindow.removeEventListener('pointerup', handlePointerUp);
};
}, [focusState]);
return (
<Primitive.button
aria-controls={ariaControls}
aria-hidden={ariaHidden}
aria-label={ariaLabel}
ref={ref}
id={inputId}
{...props}
onPointerDown={composeEventHandlers(onPointerDown, () => {
focusState.current.clickTriggered = true;
})}
onPointerCancel={(event) => {
// do not use `composeEventHandlers` here because we always want to
// reset the ref on cancellation, regardless of whether the user has
// called preventDefault on the event
onPointerCancel?.(event);
focusState.current = INITIAL_FOCUS_STATE;
}}
// do not use `composeEventHandlers` here because we always want to
// reset the ref after click, regardless of whether the user has
// called preventDefault on the event
onClick={(event) => {
onClick?.(event);
if (event.defaultPrevented) {
focusState.current = INITIAL_FOCUS_STATE;
return;
}
flushSync(() => {
setVisible((s) => !s);
});
if (focusState.current.clickTriggered) {
const input = inputRef.current;
if (input) {
const { selectionStart, selectionEnd } = focusState.current;
input.focus();
if (selectionStart !== null || selectionEnd !== null) {
// wait a tick so that focus has settled, then restore select position
requestAnimationFrame(() => {
// make sure the input still has focus (developer may have
// programatically moved focus elsewhere)
if (input.ownerDocument.activeElement === input) {
input.selectionStart = selectionStart;
input.selectionEnd = selectionEnd;
}
});
}
}
}
focusState.current = INITIAL_FOCUS_STATE;
}}
onPointerUp={(event) => {
onPointerUp?.(event);
// if click handler hasn't been called at this point, it may have been
// intercepted, in which case we still want to reset our internal
// state
setTimeout(() => {
focusState.current = INITIAL_FOCUS_STATE;
}, 50);
}}
type="button"
>
{children}
</Primitive.button>
);
}
);
PasswordToggleFieldToggle.displayName = PASSWORD_TOGGLE_FIELD_TOGGLE_NAME;
/* -------------------------------------------------------------------------------------------------
* PasswordToggleFieldSlot
* -----------------------------------------------------------------------------------------------*/
const PASSWORD_TOGGLE_FIELD_SLOT_NAME = PASSWORD_TOGGLE_FIELD_NAME + 'Slot';
interface PasswordToggleFieldSlotDeclarativeProps {
visible: React.ReactNode;
hidden: React.ReactNode;
}
interface PasswordToggleFieldSlotRenderProps {
render: (args: { visible: boolean }) => React.ReactElement;
}
type PasswordToggleFieldSlotProps =
| PasswordToggleFieldSlotDeclarativeProps
| PasswordToggleFieldSlotRenderProps;
const PasswordToggleFieldSlot: React.FC<PasswordToggleFieldSlotProps> = ({
__scopePasswordToggleField,
...props
}: ScopedProps<PasswordToggleFieldSlotProps>) => {
const { visible } = usePasswordToggleFieldContext(
PASSWORD_TOGGLE_FIELD_SLOT_NAME,
__scopePasswordToggleField
);
return 'render' in props
? //
props.render({ visible })
: visible
? props.visible
: props.hidden;
};
PasswordToggleFieldSlot.displayName = PASSWORD_TOGGLE_FIELD_SLOT_NAME;
/* -------------------------------------------------------------------------------------------------
* PasswordToggleFieldIcon
* -----------------------------------------------------------------------------------------------*/
const PASSWORD_TOGGLE_FIELD_ICON_NAME = PASSWORD_TOGGLE_FIELD_NAME + 'Icon';
type PrimitiveSvgProps = React.ComponentPropsWithoutRef<'svg'>;
interface PasswordToggleFieldIconProps extends Omit<PrimitiveSvgProps, 'children'> {
visible: React.ReactElement;
hidden: React.ReactElement;
}
const PasswordToggleFieldIcon = React.forwardRef<SVGSVGElement, PasswordToggleFieldIconProps>(
(
{
__scopePasswordToggleField,
// @ts-expect-error
children,
...props
}: ScopedProps<PasswordToggleFieldIconProps>,
forwardedRef
) => {
const { visible } = usePasswordToggleFieldContext(
PASSWORD_TOGGLE_FIELD_ICON_NAME,
__scopePasswordToggleField
);
const { visible: visibleIcon, hidden: hiddenIcon, ...domProps } = props;
return (
<Primitive.svg {...domProps} ref={forwardedRef} aria-hidden asChild>
{visible ? visibleIcon : hiddenIcon}
</Primitive.svg>
);
}
);
PasswordToggleFieldIcon.displayName = PASSWORD_TOGGLE_FIELD_ICON_NAME;
export {
PasswordToggleField,
PasswordToggleFieldInput,
PasswordToggleFieldToggle,
PasswordToggleFieldSlot,
PasswordToggleFieldIcon,
//
PasswordToggleField as Root,
PasswordToggleFieldInput as Input,
PasswordToggleFieldToggle as Toggle,
PasswordToggleFieldSlot as Slot,
PasswordToggleFieldIcon as Icon,
};
export type {
PasswordToggleFieldProps,
PasswordToggleFieldInputProps,
PasswordToggleFieldToggleProps,
PasswordToggleFieldIconProps,
PasswordToggleFieldSlotProps,
};
function requestIdleCallback(
window: Window,
callback: IdleRequestCallback,
options?: IdleRequestOptions
): () => void {
if ((window as any).requestIdleCallback) {
const id = window.requestIdleCallback(callback, options);
return () => {
window.cancelIdleCallback(id);
};
}
const start = Date.now();
const id = window.setTimeout(() => {
const timeRemaining = () => Math.max(0, 50 - (Date.now() - start));
callback({ didTimeout: false, timeRemaining });
}, 1);
return () => {
window.clearTimeout(id);
};
}