@workos-inc/widgets 1.20.2-next.1792439904014 → 1.20.2-next.1792451814155
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/CHANGELOG.md +3 -1
- package/dist/cjs/lib/compose-refs.cjs.map +1 -1
- package/dist/cjs/lib/elevated-access.cjs +15 -14
- package/dist/cjs/lib/elevated-access.cjs.map +1 -1
- package/dist/cjs/lib/otp-input.cjs +20 -19
- package/dist/cjs/lib/otp-input.cjs.map +1 -1
- package/dist/cjs/lib/otp-input.d.cts.map +1 -1
- package/dist/cjs/lib/save-button.cjs +23 -18
- package/dist/cjs/lib/save-button.cjs.map +1 -1
- package/dist/esm/lib/compose-refs.js.map +1 -1
- package/dist/esm/lib/elevated-access.js +16 -15
- package/dist/esm/lib/elevated-access.js.map +1 -1
- package/dist/esm/lib/otp-input.d.ts.map +1 -1
- package/dist/esm/lib/otp-input.js +20 -19
- package/dist/esm/lib/otp-input.js.map +1 -1
- package/dist/esm/lib/save-button.js +23 -18
- package/dist/esm/lib/save-button.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/compose-refs.ts +1 -1
- package/src/lib/elevated-access.tsx +13 -10
- package/src/lib/otp-input.tsx +33 -40
- package/src/lib/save-button.tsx +41 -29
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.20.2-next.
|
|
3
|
+
## 1.20.2-next.1792451814155
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
- Honor `patternFlags` when validating configuration fields in `PipesAdmin`, so uppercase Snowflake account identifiers are accepted.
|
|
14
14
|
|
|
15
|
+
- Fixed edge-case bugs with React's concurrent rendering
|
|
16
|
+
|
|
15
17
|
- Memoize i18n translate function
|
|
16
18
|
|
|
17
19
|
- Update internal GraphQL API calls
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose-refs.cjs","names":["React"],"sources":["../../../src/lib/compose-refs.ts"],"sourcesContent":["import * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n } else if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup == \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup == \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n //
|
|
1
|
+
{"version":3,"file":"compose-refs.cjs","names":["React"],"sources":["../../../src/lib/compose-refs.ts"],"sourcesContent":["import * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n } else if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup == \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup == \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { useComposedRefs };\n"],"mappings":";;;;;;;;;AAQA,SAAS,OAAU,KAAqB,OAAU;CAChD,IAAI,OAAO,QAAQ,YACjB,OAAO,IAAI,KAAK;MACX,IAAI,QAAQ,QAAQ,QAAQ,QACjC,IAAI,UAAU;AAElB;;;;;AAMA,SAAS,YAAe,GAAG,MAA8C;CACvE,QAAQ,SAAS;EACf,IAAI,aAAa;EACjB,MAAM,WAAW,KAAK,KAAK,QAAQ;GACjC,MAAM,UAAU,OAAO,KAAK,IAAI;GAChC,IAAI,CAAC,cAAc,OAAO,WAAW,YACnC,aAAa;GAEf,OAAO;EACT,CAAC;EAMD,IAAI,YACF,aAAa;GACX,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;IACxC,MAAM,UAAU,SAAS;IACzB,IAAI,OAAO,WAAW,YACpB,QAAQ;SAER,OAAO,KAAK,IAAI,IAAI;GAExB;EACF;CAEJ;AACF;;;;;AAMA,SAAS,gBAAmB,GAAG,MAA8C;CAE3E,OAAOA,MAAM,YAAY,YAAY,GAAG,IAAI,GAAG,IAAI;AACrD"}
|
|
@@ -18,11 +18,15 @@ let _radix_ui_themes_components_alert_dialog = require("@radix-ui/themes/compone
|
|
|
18
18
|
function ElevatedAccess({ type = "dialog", children, onVerified }) {
|
|
19
19
|
const { elevatedAccess } = require_api_api_provider.useElevatedAccessToken();
|
|
20
20
|
const [authenticationChallengeId, setAuthenticationChallengeId] = (0, react.useState)();
|
|
21
|
-
const prevAccessToken = (0, react.
|
|
21
|
+
const [{ prevAccessToken, hasTokenExpired }, setPrevAccessState] = (0, react.useState)({
|
|
22
|
+
prevAccessToken: elevatedAccess,
|
|
23
|
+
hasTokenExpired: false
|
|
24
|
+
});
|
|
25
|
+
if (elevatedAccess !== prevAccessToken) setPrevAccessState({
|
|
26
|
+
prevAccessToken: elevatedAccess,
|
|
27
|
+
hasTokenExpired: hasTokenExpired || !!prevAccessToken && !elevatedAccess
|
|
28
|
+
});
|
|
22
29
|
const hasCalledOnVerified = (0, react.useRef)(false);
|
|
23
|
-
(0, react.useEffect)(() => {
|
|
24
|
-
prevAccessToken.current = elevatedAccess;
|
|
25
|
-
}, [elevatedAccess]);
|
|
26
30
|
const handleVerificationSuccess = (0, react.useCallback)((elevatedAccessToken) => {
|
|
27
31
|
setAuthenticationChallengeId(void 0);
|
|
28
32
|
if (!hasCalledOnVerified.current) {
|
|
@@ -31,16 +35,13 @@ function ElevatedAccess({ type = "dialog", children, onVerified }) {
|
|
|
31
35
|
}
|
|
32
36
|
}, [onVerified]);
|
|
33
37
|
if (elevatedAccess) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children });
|
|
34
|
-
if (!authenticationChallengeId) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
}
|
|
38
|
+
if (!authenticationChallengeId) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SendVerificationEmailForm, {
|
|
39
|
+
type,
|
|
40
|
+
hasTokenExpired,
|
|
41
|
+
onSuccess: (challengeId) => {
|
|
42
|
+
setAuthenticationChallengeId(challengeId);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
44
45
|
if (authenticationChallengeId) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VerificationIdentityForm, {
|
|
45
46
|
type,
|
|
46
47
|
authenticationChallengeId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"elevated-access.cjs","names":["useElevatedAccessToken","useState","useRef","useCallback","useMe","useClientGraphqlMutation","expectResultMember","Translation","Text","Callout","Flex","Button"],"sources":["../../../src/lib/elevated-access.tsx"],"sourcesContent":["import { Callout, Flex, Text } from \"@radix-ui/themes\";\nimport type { PropsWithChildren } from \"react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useElevatedAccessToken } from \"../api/api-provider.js\";\nimport { useClientGraphqlMutation } from \"../api/client-graphql/hooks.js\";\nimport { useMe } from \"./api/me.js\";\nimport { expectResultMember } from \"./client-graphql-result.js\";\nimport { AlertDialog, Button, Dialog } from \"./elements.js\";\nimport * as Form from \"./form.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport * as Otp from \"./otp-input.js\";\n\ninterface ElevatedAccessProps extends PropsWithChildren {\n onVerified?: (elevatedAccessToken: string) => Promise<unknown>;\n type?: \"dialog\" | \"alert\";\n}\n\nexport function ElevatedAccess({\n type = \"dialog\",\n children,\n onVerified,\n}: ElevatedAccessProps) {\n const { elevatedAccess } = useElevatedAccessToken();\n const [authenticationChallengeId, setAuthenticationChallengeId] =\n useState<string>();\n\n const prevAccessToken = useRef(elevatedAccess);\n const hasCalledOnVerified = useRef(false);\n\n useEffect(() => {\n prevAccessToken.current = elevatedAccess;\n }, [elevatedAccess]);\n\n const handleVerificationSuccess = useCallback(\n (elevatedAccessToken: string) => {\n setAuthenticationChallengeId(undefined);\n\n // Only call onVerified on the initial verification, not on\n // re-verifications after token expiry, so side effects (e.g. creating a\n // new TOTP factor) don't re-run and discard the user's progress.\n if (!hasCalledOnVerified.current) {\n hasCalledOnVerified.current = true;\n return onVerified?.(elevatedAccessToken);\n }\n },\n [onVerified],\n );\n\n if (elevatedAccess) {\n return <>{children}</>;\n }\n\n if (!authenticationChallengeId) {\n // FIXME: This should be refactored\n // eslint-disable-next-line react-hooks/refs\n const hasTokenExpired = !!prevAccessToken.current;\n\n return (\n <SendVerificationEmailForm\n type={type}\n hasTokenExpired={hasTokenExpired}\n onSuccess={(challengeId) => {\n setAuthenticationChallengeId(challengeId);\n }}\n />\n );\n }\n\n if (authenticationChallengeId) {\n return (\n <VerificationIdentityForm\n type={type}\n authenticationChallengeId={authenticationChallengeId}\n onSuccess={handleVerificationSuccess}\n />\n );\n }\n\n return null;\n}\n\ninterface SendVerificationEmailFormProps {\n onSuccess: (challengeId: string) => unknown | Promise<unknown>;\n type: \"dialog\" | \"alert\";\n hasTokenExpired: boolean;\n}\n\nfunction SendVerificationEmailForm({\n onSuccess,\n type,\n hasTokenExpired,\n}: SendVerificationEmailFormProps) {\n const { data: me } = useMe();\n const sendVerification = useClientGraphqlMutation({\n mutationFn: async (sdk) =>\n expectResultMember(\n (await sdk.SendVerificationCode()).sendVerificationCode,\n \"VerificationCodeSent\",\n ),\n onSuccess: (data) => onSuccess(data.authenticationChallengeId),\n });\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n sendVerification.mutate();\n };\n\n return (\n <form onSubmit={handleSubmit}>\n <Title>\n {hasTokenExpired ? (\n <Translation\n defaultMessage=\"Your verification token has expired\"\n id=\"lb/AsC\"\n description=\"Dialog title when verification token has expired\"\n />\n ) : (\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n )}\n </Title>\n\n <Description mb=\"5\">\n <Translation\n defaultMessage=\"To continue, we need to confirm your identity. We'll send a temporary verification code to {email}.\"\n id=\"FTUSRM\"\n description=\"Description explaining verification code will be sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n {sendVerification.error && (\n <Callout.Root color=\"red\" mt=\"-2\" mb=\"0\">\n <Callout.Text>\n {getMutationErrorMessage(sendVerification.error)}\n </Callout.Text>\n </Callout.Root>\n )}\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button\n variant=\"secondary\"\n type=\"button\"\n disabled={sendVerification.isPending}\n >\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={sendVerification.isPending}>\n <Translation\n defaultMessage=\"Send verification code\"\n id=\"XsTeXJ\"\n description=\"Button text to send verification code\"\n />\n </Button>\n </Flex>\n </form>\n );\n}\n\ninterface VerificationIdentityFormProps {\n onSuccess?: (elevatedAccessToken: string) => unknown | Promise<unknown>;\n authenticationChallengeId: string;\n type: \"dialog\" | \"alert\";\n}\n\nfunction VerificationIdentityForm({\n onSuccess,\n authenticationChallengeId,\n type,\n}: VerificationIdentityFormProps) {\n const { data: me } = useMe();\n const { setElevatedAccess } = useElevatedAccessToken();\n const verifyIdentity = useClientGraphqlMutation({\n mutationFn: async (\n sdk,\n input: { code: string; authenticationChallengeId: string },\n ) =>\n expectResultMember(\n (await sdk.VerifyCurrentEmail({ input })).verifyCurrentEmail,\n \"CurrentEmailVerified\",\n ),\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [otpValue, setOtpValue] = useState<string>(\"\");\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n const formData = new FormData(event.currentTarget);\n const code = formData.get(\"otp-code\")?.toString() ?? \"\";\n\n setIsSubmitting(true);\n\n try {\n // Mutate async so we can wait for the onSuccess callback as well\n const newAuthState = await verifyIdentity.mutateAsync({\n code,\n authenticationChallengeId,\n });\n\n setElevatedAccess({\n token: newAuthState.elevatedAccessToken,\n expiresAt: newAuthState.expiresAt,\n });\n\n if (onSuccess) {\n await new Promise((resolve) => setTimeout(resolve, 200));\n await onSuccess(newAuthState.elevatedAccessToken);\n }\n } catch (error) {\n console.error(error);\n }\n\n setIsSubmitting(false);\n };\n\n return (\n <Form.Root onSubmit={handleSubmit}>\n <Title>\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n </Title>\n\n <Description>\n <Translation\n defaultMessage=\"A verification code was sent to {email}. Please enter it below.\"\n id=\"kvxE0S\"\n description=\"Description explaining where verification code was sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n <Flex direction=\"column\" gap=\"2\" mt=\"5\" mx=\"auto\" width=\"fit-content\">\n <Otp.Root\n autoSubmit\n gap=\"2\"\n justify=\"center\"\n columns=\"repeat(6, 48px)\"\n width=\"fit-content\"\n rows=\"48px\"\n name=\"otp-code\"\n readOnly={isSubmitting}\n value={otpValue}\n onValueChange={setOtpValue}\n >\n <Otp.Input required autoFocus autoComplete=\"off\" />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n </Otp.Root>\n\n {verifyIdentity.error && (\n <Text color=\"red\" size=\"2\" as=\"p\">\n {getMutationErrorMessage(verifyIdentity.error)}\n </Text>\n )}\n </Flex>\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button variant=\"secondary\" type=\"button\">\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={isSubmitting}>\n <Translation\n defaultMessage=\"Confirm\"\n id=\"EmYENK\"\n description=\"Confirm button text for verification\"\n />\n </Button>\n </Flex>\n </Form.Root>\n );\n}\n\nfunction getMutationErrorMessage(error: unknown) {\n let message = typeof error === \"string\" ? error : \"\";\n\n if (error instanceof Error) {\n message = error.message;\n } else if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof error.message === \"string\"\n ) {\n message = error.message;\n }\n\n if (!message || message === \"Bad Request\") {\n message = \"Invalid code, please try again.\";\n }\n\n return message;\n}\n\n// Note: Error messages in getMutationErrorMessage are kept as plain strings\n// since they are displayed dynamically\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"elevated-access.cjs","names":["useElevatedAccessToken","useState","useRef","useCallback","useMe","useClientGraphqlMutation","expectResultMember","Translation","Text","Callout","Flex","Button"],"sources":["../../../src/lib/elevated-access.tsx"],"sourcesContent":["import { Callout, Flex, Text } from \"@radix-ui/themes\";\nimport type { PropsWithChildren } from \"react\";\nimport { useCallback, useRef, useState } from \"react\";\nimport { useElevatedAccessToken } from \"../api/api-provider.js\";\nimport { useClientGraphqlMutation } from \"../api/client-graphql/hooks.js\";\nimport { useMe } from \"./api/me.js\";\nimport { expectResultMember } from \"./client-graphql-result.js\";\nimport { AlertDialog, Button, Dialog } from \"./elements.js\";\nimport * as Form from \"./form.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport * as Otp from \"./otp-input.js\";\n\ninterface ElevatedAccessProps extends PropsWithChildren {\n onVerified?: (elevatedAccessToken: string) => Promise<unknown>;\n type?: \"dialog\" | \"alert\";\n}\n\nexport function ElevatedAccess({\n type = \"dialog\",\n children,\n onVerified,\n}: ElevatedAccessProps) {\n const { elevatedAccess } = useElevatedAccessToken();\n const [authenticationChallengeId, setAuthenticationChallengeId] =\n useState<string>();\n\n const [{ prevAccessToken, hasTokenExpired }, setPrevAccessState] = useState({\n prevAccessToken: elevatedAccess,\n hasTokenExpired: false,\n });\n if (elevatedAccess !== prevAccessToken) {\n setPrevAccessState({\n prevAccessToken: elevatedAccess,\n hasTokenExpired:\n hasTokenExpired || (!!prevAccessToken && !elevatedAccess),\n });\n }\n\n const hasCalledOnVerified = useRef(false);\n\n const handleVerificationSuccess = useCallback(\n (elevatedAccessToken: string) => {\n setAuthenticationChallengeId(undefined);\n\n // Only call onVerified on the initial verification, not on\n // re-verifications after token expiry, so side effects (e.g. creating a\n // new TOTP factor) don't re-run and discard the user's progress.\n if (!hasCalledOnVerified.current) {\n hasCalledOnVerified.current = true;\n return onVerified?.(elevatedAccessToken);\n }\n },\n [onVerified],\n );\n\n if (elevatedAccess) {\n return <>{children}</>;\n }\n\n if (!authenticationChallengeId) {\n return (\n <SendVerificationEmailForm\n type={type}\n hasTokenExpired={hasTokenExpired}\n onSuccess={(challengeId) => {\n setAuthenticationChallengeId(challengeId);\n }}\n />\n );\n }\n\n if (authenticationChallengeId) {\n return (\n <VerificationIdentityForm\n type={type}\n authenticationChallengeId={authenticationChallengeId}\n onSuccess={handleVerificationSuccess}\n />\n );\n }\n\n return null;\n}\n\ninterface SendVerificationEmailFormProps {\n onSuccess: (challengeId: string) => unknown | Promise<unknown>;\n type: \"dialog\" | \"alert\";\n hasTokenExpired: boolean;\n}\n\nfunction SendVerificationEmailForm({\n onSuccess,\n type,\n hasTokenExpired,\n}: SendVerificationEmailFormProps) {\n const { data: me } = useMe();\n const sendVerification = useClientGraphqlMutation({\n mutationFn: async (sdk) =>\n expectResultMember(\n (await sdk.SendVerificationCode()).sendVerificationCode,\n \"VerificationCodeSent\",\n ),\n onSuccess: (data) => onSuccess(data.authenticationChallengeId),\n });\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n sendVerification.mutate();\n };\n\n return (\n <form onSubmit={handleSubmit}>\n <Title>\n {hasTokenExpired ? (\n <Translation\n defaultMessage=\"Your verification token has expired\"\n id=\"lb/AsC\"\n description=\"Dialog title when verification token has expired\"\n />\n ) : (\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n )}\n </Title>\n\n <Description mb=\"5\">\n <Translation\n defaultMessage=\"To continue, we need to confirm your identity. We'll send a temporary verification code to {email}.\"\n id=\"FTUSRM\"\n description=\"Description explaining verification code will be sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n {sendVerification.error && (\n <Callout.Root color=\"red\" mt=\"-2\" mb=\"0\">\n <Callout.Text>\n {getMutationErrorMessage(sendVerification.error)}\n </Callout.Text>\n </Callout.Root>\n )}\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button\n variant=\"secondary\"\n type=\"button\"\n disabled={sendVerification.isPending}\n >\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={sendVerification.isPending}>\n <Translation\n defaultMessage=\"Send verification code\"\n id=\"XsTeXJ\"\n description=\"Button text to send verification code\"\n />\n </Button>\n </Flex>\n </form>\n );\n}\n\ninterface VerificationIdentityFormProps {\n onSuccess?: (elevatedAccessToken: string) => unknown | Promise<unknown>;\n authenticationChallengeId: string;\n type: \"dialog\" | \"alert\";\n}\n\nfunction VerificationIdentityForm({\n onSuccess,\n authenticationChallengeId,\n type,\n}: VerificationIdentityFormProps) {\n const { data: me } = useMe();\n const { setElevatedAccess } = useElevatedAccessToken();\n const verifyIdentity = useClientGraphqlMutation({\n mutationFn: async (\n sdk,\n input: { code: string; authenticationChallengeId: string },\n ) =>\n expectResultMember(\n (await sdk.VerifyCurrentEmail({ input })).verifyCurrentEmail,\n \"CurrentEmailVerified\",\n ),\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [otpValue, setOtpValue] = useState<string>(\"\");\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n const formData = new FormData(event.currentTarget);\n const code = formData.get(\"otp-code\")?.toString() ?? \"\";\n\n setIsSubmitting(true);\n\n try {\n // Mutate async so we can wait for the onSuccess callback as well\n const newAuthState = await verifyIdentity.mutateAsync({\n code,\n authenticationChallengeId,\n });\n\n setElevatedAccess({\n token: newAuthState.elevatedAccessToken,\n expiresAt: newAuthState.expiresAt,\n });\n\n if (onSuccess) {\n await new Promise((resolve) => setTimeout(resolve, 200));\n await onSuccess(newAuthState.elevatedAccessToken);\n }\n } catch (error) {\n console.error(error);\n }\n\n setIsSubmitting(false);\n };\n\n return (\n <Form.Root onSubmit={handleSubmit}>\n <Title>\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n </Title>\n\n <Description>\n <Translation\n defaultMessage=\"A verification code was sent to {email}. Please enter it below.\"\n id=\"kvxE0S\"\n description=\"Description explaining where verification code was sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n <Flex direction=\"column\" gap=\"2\" mt=\"5\" mx=\"auto\" width=\"fit-content\">\n <Otp.Root\n autoSubmit\n gap=\"2\"\n justify=\"center\"\n columns=\"repeat(6, 48px)\"\n width=\"fit-content\"\n rows=\"48px\"\n name=\"otp-code\"\n readOnly={isSubmitting}\n value={otpValue}\n onValueChange={setOtpValue}\n >\n <Otp.Input required autoFocus autoComplete=\"off\" />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n </Otp.Root>\n\n {verifyIdentity.error && (\n <Text color=\"red\" size=\"2\" as=\"p\">\n {getMutationErrorMessage(verifyIdentity.error)}\n </Text>\n )}\n </Flex>\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button variant=\"secondary\" type=\"button\">\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={isSubmitting}>\n <Translation\n defaultMessage=\"Confirm\"\n id=\"EmYENK\"\n description=\"Confirm button text for verification\"\n />\n </Button>\n </Flex>\n </Form.Root>\n );\n}\n\nfunction getMutationErrorMessage(error: unknown) {\n let message = typeof error === \"string\" ? error : \"\";\n\n if (error instanceof Error) {\n message = error.message;\n } else if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof error.message === \"string\"\n ) {\n message = error.message;\n }\n\n if (!message || message === \"Bad Request\") {\n message = \"Invalid code, please try again.\";\n }\n\n return message;\n}\n\n// Note: Error messages in getMutationErrorMessage are kept as plain strings\n// since they are displayed dynamically\n"],"mappings":"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"}
|
|
@@ -10,9 +10,11 @@ react = require_runtime.__toESM(react, 1);
|
|
|
10
10
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
11
|
//#region src/lib/otp-input.tsx
|
|
12
12
|
const OtpContext = react.createContext(void 0);
|
|
13
|
+
OtpContext.displayName = "OtpContext";
|
|
14
|
+
const OtpIndexContext = react.createContext(-1);
|
|
15
|
+
OtpIndexContext.displayName = "OtpIndexContext";
|
|
13
16
|
const Root = react.forwardRef(function Root({ name, id, value: valueProp, onValueChange, autoSubmit, children, readOnly, state, ...gridProps }, forwardedRef) {
|
|
14
17
|
const [lastCharIndex, setLastCharIndex] = react.useState(0);
|
|
15
|
-
const [allChildrenAdded, setAllChildrenAdded] = react.useState(false);
|
|
16
18
|
const childCount = react.Children.count(children);
|
|
17
19
|
const [value, setValueState] = react.useState(() => getValueAsArray(valueProp, childCount));
|
|
18
20
|
const [previousValueProp, setPreviousValueProp] = react.useState(valueProp);
|
|
@@ -36,13 +38,9 @@ const Root = react.forwardRef(function Root({ name, id, value: valueProp, onValu
|
|
|
36
38
|
autoSubmit
|
|
37
39
|
]);
|
|
38
40
|
const handleEnterPressed = react.useCallback(() => attemptAutoSubmit(true), [attemptAutoSubmit]);
|
|
39
|
-
const handleChildAdd = react.useCallback((
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
childrenRefs.current.push(input);
|
|
43
|
-
} else childrenRefs.current.pop();
|
|
44
|
-
if (childrenRefs.current.length === childCount) setAllChildrenAdded(true);
|
|
45
|
-
}, [childCount]);
|
|
41
|
+
const handleChildAdd = react.useCallback((element, index) => {
|
|
42
|
+
childrenRefs.current[index] = element;
|
|
43
|
+
}, []);
|
|
46
44
|
const handleCharChange = react.useCallback((char, index) => {
|
|
47
45
|
setValue((previousValue) => {
|
|
48
46
|
const newValue = [...previousValue];
|
|
@@ -55,18 +53,16 @@ const Root = react.forwardRef(function Root({ name, id, value: valueProp, onValu
|
|
|
55
53
|
value,
|
|
56
54
|
readOnly,
|
|
57
55
|
state,
|
|
58
|
-
allChildrenAdded,
|
|
59
56
|
onEnterPressed: handleEnterPressed,
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
onCharChange: handleCharChange,
|
|
58
|
+
onChildAdd: handleChildAdd
|
|
62
59
|
}), [
|
|
63
60
|
value,
|
|
64
|
-
allChildrenAdded,
|
|
65
61
|
readOnly,
|
|
66
62
|
state,
|
|
67
63
|
handleEnterPressed,
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
handleCharChange,
|
|
65
|
+
handleChildAdd
|
|
70
66
|
]);
|
|
71
67
|
react.useEffect(attemptAutoSubmit, [attemptAutoSubmit]);
|
|
72
68
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OtpContext.Provider, {
|
|
@@ -83,7 +79,10 @@ const Root = react.forwardRef(function Root({ name, id, value: valueProp, onValu
|
|
|
83
79
|
const index = Math.min(sanitizedValue.length, childCount - 1);
|
|
84
80
|
childrenRefs.current?.[index]?.focus();
|
|
85
81
|
},
|
|
86
|
-
children: [children, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
82
|
+
children: [react.Children.map(children, (child, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OtpIndexContext.Provider, {
|
|
83
|
+
value: index,
|
|
84
|
+
children: child
|
|
85
|
+
})), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
87
86
|
ref: require_lib_compose_refs.useComposedRefs(forwardedRef, hiddenInputRef),
|
|
88
87
|
defaultValue: value.join(""),
|
|
89
88
|
minLength: childCount,
|
|
@@ -95,9 +94,12 @@ const Root = react.forwardRef(function Root({ name, id, value: valueProp, onValu
|
|
|
95
94
|
});
|
|
96
95
|
const Input = react.forwardRef(function Input({ style, readOnly, autoComplete = "off", ...props }, forwardedRef) {
|
|
97
96
|
const otpContext = useOptContext();
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
97
|
+
const { onChildAdd } = otpContext;
|
|
98
|
+
const index = react.useContext(OtpIndexContext);
|
|
99
|
+
const inputRef = react.useCallback((input) => {
|
|
100
|
+
onChildAdd(input, index);
|
|
101
|
+
}, [onChildAdd, index]);
|
|
102
|
+
const composedInputRef = require_lib_compose_refs.useComposedRefs(forwardedRef, inputRef);
|
|
101
103
|
const char = otpContext.value[index] ?? "";
|
|
102
104
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_lib_form.Field, {
|
|
103
105
|
name: `otp-${index}`,
|
|
@@ -124,7 +126,6 @@ const Input = react.forwardRef(function Input({ style, readOnly, autoComplete =
|
|
|
124
126
|
onChange: (event) => {
|
|
125
127
|
if (event.target.validity.valid || event.target.value === "") {
|
|
126
128
|
const char = event.target.value;
|
|
127
|
-
const index = Number(event.target.dataset.index ?? -1);
|
|
128
129
|
otpContext.onCharChange(char, index);
|
|
129
130
|
focusSibling(event.currentTarget, { back: char === "" });
|
|
130
131
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otp-input.cjs","names":["React","Grid","useComposedRefs","TextField"],"sources":["../../../src/lib/otp-input.tsx"],"sourcesContent":["\"use client\";\nimport { Grid } from \"@radix-ui/themes\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"./compose-refs.js\";\nimport { TextField } from \"./elements.js\";\nimport * as Form from \"./form.js\";\n\ninterface OptContextType {\n value: string[];\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n onEnterPressed: () => void;\n onChildAdd: (input: HTMLInputElement) => void;\n onCharChange: (char: string, index: number) => void;\n allChildrenAdded: boolean;\n}\n\nconst OtpContext = React.createContext<OptContextType | undefined>(undefined);\n\ntype OtpRootProps = React.ComponentPropsWithoutRef<typeof Grid> & {\n id?: string;\n name?: string;\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n value: string;\n onValueChange: React.Dispatch<React.SetStateAction<string>>;\n autoSubmit?: boolean;\n};\n\nexport const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(\n function Root(\n {\n name,\n id,\n value: valueProp,\n onValueChange,\n autoSubmit,\n children,\n readOnly,\n state,\n ...gridProps\n },\n forwardedRef,\n ) {\n const [lastCharIndex, setLastCharIndex] = React.useState<number>(0);\n const [allChildrenAdded, setAllChildrenAdded] =\n React.useState<boolean>(false);\n const childCount = React.Children.count(children);\n\n // Internal array state is the source of truth so that empty slots between\n // filled digits are preserved. A `value` string can't encode a gap (e.g.\n // `[\"1\", \"\", \"3\"]` and `[\"1\", \"3\"]` both serialize to \"13\"), so deriving\n // state straight from the prop would shift digits whenever an inner box is\n // cleared or filled out of order.\n const [value, setValueState] = React.useState<string[]>(() =>\n getValueAsArray(valueProp, childCount),\n );\n\n const [previousValueProp, setPreviousValueProp] = React.useState(valueProp);\n if (previousValueProp !== valueProp) {\n setPreviousValueProp(valueProp);\n if (valueProp !== value.join(\"\")) {\n setValueState(getValueAsArray(valueProp, childCount));\n }\n }\n\n const setValue = React.useCallback(\n (action: React.SetStateAction<string[]>) => {\n const nextArrayValue =\n typeof action === \"function\" ? action(value) : action;\n setValueState(nextArrayValue);\n onValueChange(nextArrayValue.join(\"\"));\n },\n [value, onValueChange],\n );\n\n const hiddenInputRef = React.useRef<HTMLInputElement>(null);\n const childrenRefs = React.useRef<HTMLInputElement[]>([]);\n\n const attemptAutoSubmit = React.useCallback(\n (enterPressed = false) => {\n if (\n autoSubmit &&\n value.length > 0 &&\n value.every((char) => char !== \"\") &&\n (enterPressed || lastCharIndex + 1 === childCount)\n ) {\n hiddenInputRef.current?.form?.requestSubmit();\n }\n },\n [value, childCount, lastCharIndex, autoSubmit],\n );\n\n const handleEnterPressed = React.useCallback(\n () => attemptAutoSubmit(true),\n [attemptAutoSubmit],\n );\n\n const handleChildAdd = React.useCallback(\n (input: HTMLInputElement) => {\n if (input) {\n input.dataset.index = `${childrenRefs.current.length}`;\n childrenRefs.current.push(input);\n } else {\n childrenRefs.current.pop();\n }\n\n if (childrenRefs.current.length === childCount) {\n setAllChildrenAdded(true);\n }\n },\n [childCount],\n );\n\n const handleCharChange = React.useCallback(\n (char: string, index: number) => {\n setValue((previousValue) => {\n const newValue = [...previousValue];\n newValue[index] = char;\n return newValue;\n });\n setLastCharIndex(index);\n },\n [setValue],\n );\n\n const otpContext = React.useMemo(\n () => ({\n value,\n readOnly,\n state,\n allChildrenAdded,\n onEnterPressed: handleEnterPressed,\n onChildAdd: handleChildAdd,\n onCharChange: handleCharChange,\n }),\n [\n value,\n allChildrenAdded,\n readOnly,\n state,\n handleEnterPressed,\n handleChildAdd,\n handleCharChange,\n ],\n );\n\n React.useEffect(attemptAutoSubmit, [attemptAutoSubmit]);\n\n return (\n <OtpContext.Provider value={otpContext}>\n <Grid\n columns={`repeat(${childCount}, 1fr)`}\n {...gridProps}\n onPaste={(event: React.ClipboardEvent<HTMLDivElement>) => {\n event.preventDefault();\n const pastedValue = event.clipboardData.getData(\"Text\");\n const sanitizedValue = pastedValue\n .replace(/[^\\d]/g, \"\")\n .slice(0, childCount);\n const value = sanitizedValue\n .padEnd(childCount, \"#\")\n .split(\"\")\n .map((char) => (char === \"#\" ? \"\" : char));\n\n setValue(value);\n setLastCharIndex(sanitizedValue.length - 1);\n\n const index = Math.min(sanitizedValue.length, childCount - 1);\n childrenRefs.current?.[index]?.focus();\n }}\n >\n {children}\n <input\n ref={useComposedRefs(forwardedRef, hiddenInputRef)}\n defaultValue={value.join(\"\")}\n minLength={childCount}\n name={name}\n type=\"hidden\"\n />\n </Grid>\n </OtpContext.Provider>\n );\n },\n);\n\ninterface InputProps extends React.ComponentProps<typeof TextField> {\n autoComplete?: \"one-time-code\" | \"off\";\n}\n\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(\n function Input(\n { style, readOnly, autoComplete = \"off\", ...props },\n forwardedRef,\n ) {\n const otpContext = useOptContext();\n const inputRef = React.useRef<HTMLInputElement>(null);\n const composedInputRef = useComposedRefs(\n forwardedRef,\n inputRef,\n otpContext.onChildAdd,\n );\n\n // FIXME: This should be refactored\n // eslint-disable-next-line react-hooks/refs\n const index = Number(inputRef.current?.dataset.index ?? -1);\n const char = otpContext.value[index] ?? \"\";\n\n return (\n <Form.Field name={`otp-${index}`} asChild>\n <Form.Control asChild>\n <TextField\n ref={composedInputRef}\n autoComplete={index === 0 ? autoComplete : \"off\"}\n color={otpContext.state === \"invalid\" ? \"red\" : undefined}\n inputMode=\"numeric\"\n maxLength={1}\n pattern=\"\\d{1}\"\n readOnly={readOnly ?? otpContext.readOnly}\n size=\"3\"\n value={char}\n variant={otpContext.state === \"invalid\" ? \"soft\" : undefined}\n style={{\n ...style,\n height: \"auto\",\n \"--text-field-padding\": 0,\n textAlign: \"center\",\n }}\n onChange={(event) => {\n // Update the value if it matches the input pattern (number only) or is empty (backspace)\n if (event.target.validity.valid || event.target.value === \"\") {\n const char = event.target.value;\n const index = Number(event.target.dataset.index ?? -1);\n otpContext.onCharChange(char, index);\n focusSibling(event.currentTarget, { back: char === \"\" });\n }\n }}\n onKeyDown={(event) => {\n if (event.key === \"ArrowLeft\") {\n focusSibling(event.currentTarget, { back: true });\n event.preventDefault();\n } else if (event.key === \"ArrowRight\") {\n focusSibling(event.currentTarget);\n event.preventDefault();\n } else if (event.key === \"Backspace\" && char === \"\") {\n focusSibling(event.currentTarget, { back: true });\n } else if (event.key === \"Enter\" && char !== \"\") {\n otpContext.onEnterPressed();\n }\n }}\n {...props}\n />\n </Form.Control>\n </Form.Field>\n );\n },\n);\n\nconst useOptContext = () => {\n const optContext = React.useContext(OtpContext);\n\n if (!optContext) {\n throw new Error(\n \"OtpInput compound components cannot be rendered outside the OtpRoot component\",\n );\n }\n\n return optContext;\n};\n\nfunction focusSibling(input: HTMLInputElement, { back = false } = {}) {\n const sibling = back\n ? input.parentElement?.previousSibling\n : input.parentElement?.nextSibling;\n const siblingInput = sibling?.firstChild;\n if (siblingInput && siblingInput instanceof HTMLInputElement) {\n siblingInput?.focus();\n siblingInput?.select();\n }\n}\n\nconst getValueAsArray = (value: string | undefined, length: number) => {\n if (!value) {\n return createEmptyArray(length);\n }\n\n return createEmptyArray(length).map((_, index) => value?.[index] ?? \"\");\n};\n\nconst createEmptyArray = (length: number): string[] =>\n Array.from<string>({ length }).fill(\"\");\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"otp-input.cjs","names":["React","Grid","useComposedRefs","TextField"],"sources":["../../../src/lib/otp-input.tsx"],"sourcesContent":["\"use client\";\nimport { Grid } from \"@radix-ui/themes\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"./compose-refs.js\";\nimport { TextField } from \"./elements.js\";\nimport * as Form from \"./form.js\";\n\ninterface OptContextType {\n value: string[];\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n onEnterPressed: () => void;\n onCharChange: (char: string, index: number) => void;\n onChildAdd: (element: HTMLInputElement | null, index: number) => void;\n}\n\nconst OtpContext = React.createContext<OptContextType | undefined>(undefined);\nOtpContext.displayName = \"OtpContext\";\n\nconst OtpIndexContext = React.createContext<number>(-1);\nOtpIndexContext.displayName = \"OtpIndexContext\";\n\ntype OtpRootProps = React.ComponentPropsWithoutRef<typeof Grid> & {\n id?: string;\n name?: string;\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n value: string;\n onValueChange: React.Dispatch<React.SetStateAction<string>>;\n autoSubmit?: boolean;\n};\n\nexport const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(\n function Root(\n {\n name,\n id,\n value: valueProp,\n onValueChange,\n autoSubmit,\n children,\n readOnly,\n state,\n ...gridProps\n },\n forwardedRef,\n ) {\n const [lastCharIndex, setLastCharIndex] = React.useState<number>(0);\n const childCount = React.Children.count(children);\n\n // Internal array state is the source of truth so that empty slots between\n // filled digits are preserved. A `value` string can't encode a gap (e.g.\n // `[\"1\", \"\", \"3\"]` and `[\"1\", \"3\"]` both serialize to \"13\"), so deriving\n // state straight from the prop would shift digits whenever an inner box is\n // cleared or filled out of order.\n const [value, setValueState] = React.useState<string[]>(() =>\n getValueAsArray(valueProp, childCount),\n );\n\n const [previousValueProp, setPreviousValueProp] = React.useState(valueProp);\n if (previousValueProp !== valueProp) {\n setPreviousValueProp(valueProp);\n if (valueProp !== value.join(\"\")) {\n setValueState(getValueAsArray(valueProp, childCount));\n }\n }\n\n const setValue = React.useCallback(\n (action: React.SetStateAction<string[]>) => {\n const nextArrayValue =\n typeof action === \"function\" ? action(value) : action;\n setValueState(nextArrayValue);\n onValueChange(nextArrayValue.join(\"\"));\n },\n [value, onValueChange],\n );\n\n const hiddenInputRef = React.useRef<HTMLInputElement>(null);\n const childrenRefs = React.useRef<(HTMLInputElement | null)[]>([]);\n\n const attemptAutoSubmit = React.useCallback(\n (enterPressed = false) => {\n if (\n autoSubmit &&\n value.length > 0 &&\n value.every((char) => char !== \"\") &&\n (enterPressed || lastCharIndex + 1 === childCount)\n ) {\n hiddenInputRef.current?.form?.requestSubmit();\n }\n },\n [value, childCount, lastCharIndex, autoSubmit],\n );\n\n const handleEnterPressed = React.useCallback(\n () => attemptAutoSubmit(true),\n [attemptAutoSubmit],\n );\n\n const handleChildAdd = React.useCallback(\n (element: HTMLInputElement | null, index: number) => {\n childrenRefs.current[index] = element;\n },\n [],\n );\n\n const handleCharChange = React.useCallback(\n (char: string, index: number) => {\n setValue((previousValue) => {\n const newValue = [...previousValue];\n newValue[index] = char;\n return newValue;\n });\n setLastCharIndex(index);\n },\n [setValue],\n );\n\n const otpContext = React.useMemo<OptContextType>(\n () =>\n ({\n value,\n readOnly,\n state,\n onEnterPressed: handleEnterPressed,\n onCharChange: handleCharChange,\n onChildAdd: handleChildAdd,\n }) satisfies OptContextType,\n [\n value,\n readOnly,\n state,\n handleEnterPressed,\n handleCharChange,\n handleChildAdd,\n ],\n );\n\n React.useEffect(attemptAutoSubmit, [attemptAutoSubmit]);\n\n return (\n <OtpContext.Provider value={otpContext}>\n <Grid\n columns={`repeat(${childCount}, 1fr)`}\n {...gridProps}\n onPaste={(event: React.ClipboardEvent<HTMLDivElement>) => {\n event.preventDefault();\n const pastedValue = event.clipboardData.getData(\"Text\");\n const sanitizedValue = pastedValue\n .replace(/[^\\d]/g, \"\")\n .slice(0, childCount);\n const value = sanitizedValue\n .padEnd(childCount, \"#\")\n .split(\"\")\n .map((char) => (char === \"#\" ? \"\" : char));\n\n setValue(value);\n setLastCharIndex(sanitizedValue.length - 1);\n\n const index = Math.min(sanitizedValue.length, childCount - 1);\n childrenRefs.current?.[index]?.focus();\n }}\n >\n {React.Children.map(children, (child, index) => (\n <OtpIndexContext.Provider value={index}>\n {child}\n </OtpIndexContext.Provider>\n ))}\n <input\n ref={useComposedRefs(forwardedRef, hiddenInputRef)}\n defaultValue={value.join(\"\")}\n minLength={childCount}\n name={name}\n type=\"hidden\"\n />\n </Grid>\n </OtpContext.Provider>\n );\n },\n);\n\ninterface InputProps extends React.ComponentProps<typeof TextField> {\n autoComplete?: \"one-time-code\" | \"off\";\n}\n\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(\n function Input(\n { style, readOnly, autoComplete = \"off\", ...props },\n forwardedRef,\n ) {\n const otpContext = useOptContext();\n const { onChildAdd } = otpContext;\n const index = React.useContext(OtpIndexContext);\n const inputRef = React.useCallback(\n (input: HTMLInputElement | null) => {\n onChildAdd(input, index);\n },\n [onChildAdd, index],\n );\n const composedInputRef = useComposedRefs(forwardedRef, inputRef);\n\n const char = otpContext.value[index] ?? \"\";\n\n return (\n <Form.Field name={`otp-${index}`} asChild>\n <Form.Control asChild>\n <TextField\n ref={composedInputRef}\n autoComplete={index === 0 ? autoComplete : \"off\"}\n color={otpContext.state === \"invalid\" ? \"red\" : undefined}\n inputMode=\"numeric\"\n maxLength={1}\n pattern=\"\\d{1}\"\n readOnly={readOnly ?? otpContext.readOnly}\n size=\"3\"\n value={char}\n variant={otpContext.state === \"invalid\" ? \"soft\" : undefined}\n style={{\n ...style,\n height: \"auto\",\n \"--text-field-padding\": 0,\n textAlign: \"center\",\n }}\n onChange={(event) => {\n // Update the value if it matches the input pattern (number only) or is empty (backspace)\n if (event.target.validity.valid || event.target.value === \"\") {\n const char = event.target.value;\n otpContext.onCharChange(char, index);\n focusSibling(event.currentTarget, { back: char === \"\" });\n }\n }}\n onKeyDown={(event) => {\n if (event.key === \"ArrowLeft\") {\n focusSibling(event.currentTarget, { back: true });\n event.preventDefault();\n } else if (event.key === \"ArrowRight\") {\n focusSibling(event.currentTarget);\n event.preventDefault();\n } else if (event.key === \"Backspace\" && char === \"\") {\n focusSibling(event.currentTarget, { back: true });\n } else if (event.key === \"Enter\" && char !== \"\") {\n otpContext.onEnterPressed();\n }\n }}\n {...props}\n />\n </Form.Control>\n </Form.Field>\n );\n },\n);\n\nconst useOptContext = () => {\n const optContext = React.useContext(OtpContext);\n if (!optContext) {\n throw new Error(\n \"OtpInput compound components cannot be rendered outside the OtpRoot component\",\n );\n }\n\n return optContext;\n};\n\nfunction focusSibling(input: HTMLInputElement, { back = false } = {}) {\n const sibling = back\n ? input.parentElement?.previousSibling\n : input.parentElement?.nextSibling;\n const siblingInput = sibling?.firstChild;\n if (siblingInput && siblingInput instanceof HTMLInputElement) {\n siblingInput?.focus();\n siblingInput?.select();\n }\n}\n\nconst getValueAsArray = (value: string | undefined, length: number) => {\n if (!value) {\n return createEmptyArray(length);\n }\n\n return createEmptyArray(length).map((_, index) => value?.[index] ?? \"\");\n};\n\nconst createEmptyArray = (length: number): string[] =>\n Array.from<string>({ length }).fill(\"\");\n"],"mappings":";;;;;;;;;;;AAgBA,MAAM,aAAaA,MAAM,cAA0C,MAAS;AAC5E,WAAW,cAAc;AAEzB,MAAM,kBAAkBA,MAAM,cAAsB,EAAE;AACtD,gBAAgB,cAAc;AAY9B,MAAa,OAAOA,MAAM,WACxB,SAAS,KACP,EACE,MACA,IACA,OAAO,WACP,eACA,YACA,UACA,UACA,OACA,GAAG,aAEL,cACA;CACA,MAAM,CAAC,eAAe,oBAAoBA,MAAM,SAAiB,CAAC;CAClE,MAAM,aAAaA,MAAM,SAAS,MAAM,QAAQ;CAOhD,MAAM,CAAC,OAAO,iBAAiBA,MAAM,eACnC,gBAAgB,WAAW,UAAU,CACvC;CAEA,MAAM,CAAC,mBAAmB,wBAAwBA,MAAM,SAAS,SAAS;CAC1E,IAAI,sBAAsB,WAAW;EACnC,qBAAqB,SAAS;EAC9B,IAAI,cAAc,MAAM,KAAK,EAAE,GAC7B,cAAc,gBAAgB,WAAW,UAAU,CAAC;CAExD;CAEA,MAAM,WAAWA,MAAM,aACpB,WAA2C;EAC1C,MAAM,iBACJ,OAAO,WAAW,aAAa,OAAO,KAAK,IAAI;EACjD,cAAc,cAAc;EAC5B,cAAc,eAAe,KAAK,EAAE,CAAC;CACvC,GACA,CAAC,OAAO,aAAa,CACvB;CAEA,MAAM,iBAAiBA,MAAM,OAAyB,IAAI;CAC1D,MAAM,eAAeA,MAAM,OAAoC,CAAC,CAAC;CAEjE,MAAM,oBAAoBA,MAAM,aAC7B,eAAe,UAAU;EACxB,IACE,cACA,MAAM,SAAS,KACf,MAAM,OAAO,SAAS,SAAS,EAAE,MAChC,gBAAgB,gBAAgB,MAAM,aAEvC,eAAe,SAAS,MAAM,cAAc;CAEhD,GACA;EAAC;EAAO;EAAY;EAAe;CAAU,CAC/C;CAEA,MAAM,qBAAqBA,MAAM,kBACzB,kBAAkB,IAAI,GAC5B,CAAC,iBAAiB,CACpB;CAEA,MAAM,iBAAiBA,MAAM,aAC1B,SAAkC,UAAkB;EACnD,aAAa,QAAQ,SAAS;CAChC,GACA,CAAC,CACH;CAEA,MAAM,mBAAmBA,MAAM,aAC5B,MAAc,UAAkB;EAC/B,UAAU,kBAAkB;GAC1B,MAAM,WAAW,CAAC,GAAG,aAAa;GAClC,SAAS,SAAS;GAClB,OAAO;EACT,CAAC;EACD,iBAAiB,KAAK;CACxB,GACA,CAAC,QAAQ,CACX;CAEA,MAAM,aAAaA,MAAM,eAEpB;EACC;EACA;EACA;EACA,gBAAgB;EAChB,cAAc;EACd,YAAY;CACd,IACF;EACE;EACA;EACA;EACA;EACA;EACA;CACF,CACF;CAEA,MAAM,UAAU,mBAAmB,CAAC,iBAAiB,CAAC;CAEtD,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAW,UAAZ;EAAqB,OAAO;EAC1B,UAAA,iBAAA,GAAA,kBAAA,KAAA,CAACC,iBAAAA,MAAD;GACE,SAAS,UAAU,WAAW;GAC9B,GAAI;GACJ,UAAU,UAAgD;IACxD,MAAM,eAAe;IAErB,MAAM,iBADc,MAAM,cAAc,QAAQ,MACzB,CAAA,CACpB,QAAQ,UAAU,EAAE,CAAC,CACrB,MAAM,GAAG,UAAU;IACtB,MAAM,QAAQ,eACX,OAAO,YAAY,GAAG,CAAC,CACvB,MAAM,EAAE,CAAC,CACT,KAAK,SAAU,SAAS,MAAM,KAAK,IAAK;IAE3C,SAAS,KAAK;IACd,iBAAiB,eAAe,SAAS,CAAC;IAE1C,MAAM,QAAQ,KAAK,IAAI,eAAe,QAAQ,aAAa,CAAC;IAC5D,aAAa,UAAU,MAAM,EAAE,MAAM;GACvC;GAnBF,UAAA,CAqBGD,MAAM,SAAS,IAAI,WAAW,OAAO,UACpC,iBAAA,GAAA,kBAAA,IAAA,CAAC,gBAAgB,UAAjB;IAA0B,OAAO;IAC9B,UAAA;GACuB,CAAA,CAC3B,GACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;IACE,KAAKE,yBAAAA,gBAAgB,cAAc,cAAc;IACjD,cAAc,MAAM,KAAK,EAAE;IAC3B,WAAW;IACL;IACN,MAAK;GACN,CAAA,CACG;;CACa,CAAA;AAEzB,CACF;AAMA,MAAa,QAAQF,MAAM,WACzB,SAAS,MACP,EAAE,OAAO,UAAU,eAAe,OAAO,GAAG,SAC5C,cACA;CACA,MAAM,aAAa,cAAc;CACjC,MAAM,EAAE,eAAe;CACvB,MAAM,QAAQA,MAAM,WAAW,eAAe;CAC9C,MAAM,WAAWA,MAAM,aACpB,UAAmC;EAClC,WAAW,OAAO,KAAK;CACzB,GACA,CAAC,YAAY,KAAK,CACpB;CACA,MAAM,mBAAmBE,yBAAAA,gBAAgB,cAAc,QAAQ;CAE/D,MAAM,OAAO,WAAW,MAAM,UAAU;CAExC,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAA,iBAAA,OAAA;EAAY,MAAM,OAAO;EAAS,SAAA;EAChC,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAA,iBAAA,SAAA;GAAc,SAAA;GACZ,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAACC,qBAAAA,WAAD;IACE,KAAK;IACL,cAAc,UAAU,IAAI,eAAe;IAC3C,OAAO,WAAW,UAAU,YAAY,QAAQ;IAChD,WAAU;IACV,WAAW;IACX,SAAQ;IACR,UAAU,YAAY,WAAW;IACjC,MAAK;IACL,OAAO;IACP,SAAS,WAAW,UAAU,YAAY,SAAS;IACnD,OAAO;KACL,GAAG;KACH,QAAQ;KACR,wBAAwB;KACxB,WAAW;IACb;IACA,WAAW,UAAU;KAEnB,IAAI,MAAM,OAAO,SAAS,SAAS,MAAM,OAAO,UAAU,IAAI;MAC5D,MAAM,OAAO,MAAM,OAAO;MAC1B,WAAW,aAAa,MAAM,KAAK;MACnC,aAAa,MAAM,eAAe,EAAE,MAAM,SAAS,GAAG,CAAC;KACzD;IACF;IACA,YAAY,UAAU;KACpB,IAAI,MAAM,QAAQ,aAAa;MAC7B,aAAa,MAAM,eAAe,EAAE,MAAM,KAAK,CAAC;MAChD,MAAM,eAAe;KACvB,OAAO,IAAI,MAAM,QAAQ,cAAc;MACrC,aAAa,MAAM,aAAa;MAChC,MAAM,eAAe;KACvB,OAAO,IAAI,MAAM,QAAQ,eAAe,SAAS,IAC/C,aAAa,MAAM,eAAe,EAAE,MAAM,KAAK,CAAC;UAC3C,IAAI,MAAM,QAAQ,WAAW,SAAS,IAC3C,WAAW,eAAe;IAE9B;IACA,GAAI;GACL,CAAA;EACW,CAAA;CACJ,CAAA;AAEhB,CACF;AAEA,MAAM,sBAAsB;CAC1B,MAAM,aAAaH,MAAM,WAAW,UAAU;CAC9C,IAAI,CAAC,YACH,MAAM,IAAI,MACR,+EACF;CAGF,OAAO;AACT;AAEA,SAAS,aAAa,OAAyB,EAAE,OAAO,UAAU,CAAC,GAAG;CAIpE,MAAM,gBAHU,OACZ,MAAM,eAAe,kBACrB,MAAM,eAAe,YACJ,EAAS;CAC9B,IAAI,gBAAgB,wBAAwB,kBAAkB;EAC5D,cAAc,MAAM;EACpB,cAAc,OAAO;CACvB;AACF;AAEA,MAAM,mBAAmB,OAA2B,WAAmB;CACrE,IAAI,CAAC,OACH,OAAO,iBAAiB,MAAM;CAGhC,OAAO,iBAAiB,MAAM,CAAC,CAAC,KAAK,GAAG,UAAU,QAAQ,UAAU,EAAE;AACxE;AAEA,MAAM,oBAAoB,WACxB,MAAM,KAAa,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otp-input.d.cts","names":["TextField"],"sources":["../../../src/lib/otp-input.tsx"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"otp-input.d.cts","names":["TextField"],"sources":["../../../src/lib/otp-input.tsx"],"mappings":";;;;KAsBK,eAAe,MAAM,gCAAgC;EACxD;EACA;EACA;EACA;EACA;EACA,eAAe,MAAM,SAAS,MAAM;EACpC;;qBAGW,MAAI,MAAA,0BAAA,eAAA,MAAA,cAAA;UAqJP,mBAAmB,MAAM,sBAAsBA;EACvD;;qBAGW,OAAK,MAAA,0BAAA,KAAA,qBAAA,MAAA,cAAA"}
|
|
@@ -10,7 +10,13 @@ let _radix_ui_react_icons = require("@radix-ui/react-icons");
|
|
|
10
10
|
const DONE_TIMEOUT_MS = 1500;
|
|
11
11
|
const SAVING_TIMEOUT_MS = 600;
|
|
12
12
|
function SaveButton({ asChild = false, children, loading, done, onDone, ...props }) {
|
|
13
|
-
const [
|
|
13
|
+
const [minElapsed, setMinElapsed] = (0, react.useState)(!!done);
|
|
14
|
+
const [prevLoading, setPrevLoading] = (0, react.useState)(loading);
|
|
15
|
+
if (loading !== prevLoading) {
|
|
16
|
+
setPrevLoading(loading);
|
|
17
|
+
if (loading) setMinElapsed(false);
|
|
18
|
+
}
|
|
19
|
+
const state = loading ? "loading" : done ? minElapsed ? "done" : "loading" : "idle";
|
|
14
20
|
const loadingStartTime = (0, react.useRef)(null);
|
|
15
21
|
const onDoneRef = (0, react.useRef)(onDone);
|
|
16
22
|
const ButtonComponent = asChild ? _radix_ui_themes.Slot : require_lib_elements.Button;
|
|
@@ -19,24 +25,23 @@ function SaveButton({ asChild = false, children, loading, done, onDone, ...props
|
|
|
19
25
|
}, [onDone]);
|
|
20
26
|
(0, react.useEffect)(() => {
|
|
21
27
|
if (loading) {
|
|
22
|
-
setState("loading");
|
|
23
28
|
loadingStartTime.current = Date.now();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!done) return;
|
|
32
|
+
const loadingDuration = loadingStartTime.current ? Date.now() - loadingStartTime.current : 0;
|
|
33
|
+
const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);
|
|
34
|
+
let doneTimer = null;
|
|
35
|
+
const minElapsedTimer = window.setTimeout(() => {
|
|
36
|
+
setMinElapsed(true);
|
|
37
|
+
doneTimer = window.setTimeout(() => {
|
|
38
|
+
onDoneRef.current?.();
|
|
39
|
+
}, DONE_TIMEOUT_MS);
|
|
40
|
+
}, remainingDelay);
|
|
41
|
+
return () => {
|
|
42
|
+
window.clearTimeout(minElapsedTimer);
|
|
43
|
+
if (doneTimer !== null) window.clearTimeout(doneTimer);
|
|
44
|
+
};
|
|
40
45
|
}, [loading, done]);
|
|
41
46
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ButtonComponent, {
|
|
42
47
|
...props,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"save-button.cjs","names":["useState","useRef","Slot","Button","namespaceClassNames","Slottable","Flex","Spinner","CheckIcon","Text","Translation"],"sources":["../../../src/lib/save-button.tsx"],"sourcesContent":["import { CheckIcon } from \"@radix-ui/react-icons\";\nimport { Flex, Slot, Slottable, Spinner, Text } from \"@radix-ui/themes\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Button } from \"./elements.js\";\nimport type { ButtonProps } from \"./elements.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport { namespaceClassNames } from \"./widget-dom-props.js\";\n\nconst DONE_TIMEOUT_MS = 1500;\nconst SAVING_TIMEOUT_MS = 600;\n\ninterface SaveButtonProps extends ButtonProps {\n asChild?: boolean;\n children: React.ReactNode;\n loading?: boolean;\n done?: boolean;\n onDone?: () => void;\n}\n\nexport function SaveButton({\n asChild = false,\n children,\n loading,\n done,\n onDone,\n ...props\n}: SaveButtonProps) {\n const [
|
|
1
|
+
{"version":3,"file":"save-button.cjs","names":["useState","useRef","Slot","Button","namespaceClassNames","Slottable","Flex","Spinner","CheckIcon","Text","Translation"],"sources":["../../../src/lib/save-button.tsx"],"sourcesContent":["import { CheckIcon } from \"@radix-ui/react-icons\";\nimport { Flex, Slot, Slottable, Spinner, Text } from \"@radix-ui/themes\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Button } from \"./elements.js\";\nimport type { ButtonProps } from \"./elements.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport { namespaceClassNames } from \"./widget-dom-props.js\";\n\nconst DONE_TIMEOUT_MS = 1500;\nconst SAVING_TIMEOUT_MS = 600;\n\ninterface SaveButtonProps extends ButtonProps {\n asChild?: boolean;\n children: React.ReactNode;\n loading?: boolean;\n done?: boolean;\n onDone?: () => void;\n}\n\nexport function SaveButton({\n asChild = false,\n children,\n loading,\n done,\n onDone,\n ...props\n}: SaveButtonProps) {\n // Whether the spinner has been visible for at least SAVING_TIMEOUT_MS, so a\n // fast response doesn't flash it for a single frame.\n const [minElapsed, setMinElapsed] = useState(!!done);\n const [prevLoading, setPrevLoading] = useState(loading);\n if (loading !== prevLoading) {\n setPrevLoading(loading);\n if (loading) {\n setMinElapsed(false);\n }\n }\n\n const state = loading\n ? \"loading\"\n : done\n ? minElapsed\n ? \"done\"\n : \"loading\"\n : \"idle\";\n\n const loadingStartTime = useRef<number | null>(null);\n const onDoneRef = useRef(onDone);\n const ButtonComponent = asChild ? Slot : Button;\n\n useEffect(() => {\n onDoneRef.current = onDone;\n }, [onDone]);\n\n useEffect(() => {\n if (loading) {\n loadingStartTime.current = Date.now();\n return;\n }\n if (!done) {\n return;\n }\n\n const loadingDuration = loadingStartTime.current\n ? Date.now() - loadingStartTime.current\n : 0;\n const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);\n\n let doneTimer: number | null = null;\n const minElapsedTimer = window.setTimeout(() => {\n setMinElapsed(true);\n\n doneTimer = window.setTimeout(() => {\n onDoneRef.current?.();\n }, DONE_TIMEOUT_MS);\n }, remainingDelay);\n\n return () => {\n window.clearTimeout(minElapsedTimer);\n if (doneTimer !== null) {\n window.clearTimeout(doneTimer);\n }\n };\n }, [loading, done]);\n\n return (\n <ButtonComponent\n {...props}\n className={namespaceClassNames(\"save-button\")}\n data-save-state={state}\n >\n <Slottable>{children}</Slottable>\n {state === \"loading\" && (\n <Flex\n as=\"span\"\n align=\"center\"\n justify=\"center\"\n position=\"absolute\"\n inset=\"0\"\n >\n <Spinner size=\"1\" />\n </Flex>\n )}\n\n {state === \"done\" && (\n <Flex\n as=\"span\"\n align=\"center\"\n justify=\"center\"\n position=\"absolute\"\n inset=\"0\"\n gap=\"1\"\n >\n <CheckIcon\n width=\"18px\"\n height=\"18px\"\n style={{ marginLeft: \"-4px\" }}\n className={namespaceClassNames(\"save-button__done-icon\")}\n />\n <Text className={namespaceClassNames(\"save-button__done-text\")}>\n <Translation\n defaultMessage=\"Done\"\n id=\"wjt0K3\"\n description=\"Button label indicating an action has completed successfully\"\n />\n </Text>\n </Flex>\n )}\n </ButtonComponent>\n );\n}\n"],"mappings":";;;;;;;;;AAQA,MAAM,kBAAkB;AACxB,MAAM,oBAAoB;AAU1B,SAAgB,WAAW,EACzB,UAAU,OACV,UACA,SACA,MACA,QACA,GAAG,SACe;CAGlB,MAAM,CAAC,YAAY,kBAAA,GAAiBA,MAAAA,SAAAA,CAAS,CAAC,CAAC,IAAI;CACnD,MAAM,CAAC,aAAa,mBAAA,GAAkBA,MAAAA,SAAAA,CAAS,OAAO;CACtD,IAAI,YAAY,aAAa;EAC3B,eAAe,OAAO;EACtB,IAAI,SACF,cAAc,KAAK;CAEvB;CAEA,MAAM,QAAQ,UACV,YACA,OACE,aACE,SACA,YACF;CAEN,MAAM,oBAAA,GAAmBC,MAAAA,OAAAA,CAAsB,IAAI;CACnD,MAAM,aAAA,GAAYA,MAAAA,OAAAA,CAAO,MAAM;CAC/B,MAAM,kBAAkB,UAAUC,iBAAAA,OAAOC,qBAAAA;CAEzC,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,UAAU,UAAU;CACtB,GAAG,CAAC,MAAM,CAAC;CAEX,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,IAAI,SAAS;GACX,iBAAiB,UAAU,KAAK,IAAI;GACpC;EACF;EACA,IAAI,CAAC,MACH;EAGF,MAAM,kBAAkB,iBAAiB,UACrC,KAAK,IAAI,IAAI,iBAAiB,UAC9B;EACJ,MAAM,iBAAiB,KAAK,IAAI,GAAG,oBAAoB,eAAe;EAEtE,IAAI,YAA2B;EAC/B,MAAM,kBAAkB,OAAO,iBAAiB;GAC9C,cAAc,IAAI;GAElB,YAAY,OAAO,iBAAiB;IAClC,UAAU,UAAU;GACtB,GAAG,eAAe;EACpB,GAAG,cAAc;EAEjB,aAAa;GACX,OAAO,aAAa,eAAe;GACnC,IAAI,cAAc,MAChB,OAAO,aAAa,SAAS;EAEjC;CACF,GAAG,CAAC,SAAS,IAAI,CAAC;CAElB,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,iBAAD;EACE,GAAI;EACJ,WAAWC,6BAAAA,oBAAoB,aAAa;EAC5C,mBAAiB;EAHnB,UAAA;GAKE,iBAAA,GAAA,kBAAA,IAAA,CAACC,iBAAAA,WAAD,EAAY,SAAoB,CAAA;GAC/B,UAAU,aACT,iBAAA,GAAA,kBAAA,IAAA,CAACC,iBAAAA,MAAD;IACE,IAAG;IACH,OAAM;IACN,SAAQ;IACR,UAAS;IACT,OAAM;IAEN,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAACC,iBAAAA,SAAD,EAAS,MAAK,IAAK,CAAA;GACf,CAAA;GAGP,UAAU,UACT,iBAAA,GAAA,kBAAA,KAAA,CAACD,iBAAAA,MAAD;IACE,IAAG;IACH,OAAM;IACN,SAAQ;IACR,UAAS;IACT,OAAM;IACN,KAAI;IANN,UAAA,CAQE,iBAAA,GAAA,kBAAA,IAAA,CAACE,sBAAAA,WAAD;KACE,OAAM;KACN,QAAO;KACP,OAAO,EAAE,YAAY,OAAO;KAC5B,WAAWJ,6BAAAA,oBAAoB,wBAAwB;IACxD,CAAA,GACD,iBAAA,GAAA,kBAAA,IAAA,CAACK,iBAAAA,MAAD;KAAM,WAAWL,6BAAAA,oBAAoB,wBAAwB;KAC3D,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAACM,6BAAAA,aAAD;MACE,gBAAe;MACf,IAAG;MACH,aAAY;KACb,CAAA;IACG,CAAA,CACF;;EAEO;;AAErB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose-refs.js","names":[],"sources":["../../../src/lib/compose-refs.ts"],"sourcesContent":["import * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n } else if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup == \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup == \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n //
|
|
1
|
+
{"version":3,"file":"compose-refs.js","names":[],"sources":["../../../src/lib/compose-refs.ts"],"sourcesContent":["import * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n } else if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup == \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup == \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { useComposedRefs };\n"],"mappings":";;;;;;AAQA,SAAS,OAAU,KAAqB,OAAU;CAChD,IAAI,OAAO,QAAQ,YACjB,OAAO,IAAI,KAAK;MACX,IAAI,QAAQ,QAAQ,QAAQ,QACjC,IAAI,UAAU;AAElB;;;;;AAMA,SAAS,YAAe,GAAG,MAA8C;CACvE,QAAQ,SAAS;EACf,IAAI,aAAa;EACjB,MAAM,WAAW,KAAK,KAAK,QAAQ;GACjC,MAAM,UAAU,OAAO,KAAK,IAAI;GAChC,IAAI,CAAC,cAAc,OAAO,WAAW,YACnC,aAAa;GAEf,OAAO;EACT,CAAC;EAMD,IAAI,YACF,aAAa;GACX,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;IACxC,MAAM,UAAU,SAAS;IACzB,IAAI,OAAO,WAAW,YACpB,QAAQ;SAER,OAAO,KAAK,IAAI,IAAI;GAExB;EACF;CAEJ;AACF;;;;;AAMA,SAAS,gBAAmB,GAAG,MAA8C;CAE3E,OAAO,MAAM,YAAY,YAAY,GAAG,IAAI,GAAG,IAAI;AACrD"}
|
|
@@ -9,17 +9,21 @@ import { expectResultMember } from "./client-graphql-result.js";
|
|
|
9
9
|
import { Form } from "./form.js";
|
|
10
10
|
import { Input, Root } from "./otp-input.js";
|
|
11
11
|
import { Callout, Flex, Text } from "@radix-ui/themes";
|
|
12
|
-
import { useCallback,
|
|
12
|
+
import { useCallback, useRef, useState } from "react";
|
|
13
13
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
14
14
|
//#region src/lib/elevated-access.tsx
|
|
15
15
|
function ElevatedAccess({ type = "dialog", children, onVerified }) {
|
|
16
16
|
const { elevatedAccess } = useElevatedAccessToken();
|
|
17
17
|
const [authenticationChallengeId, setAuthenticationChallengeId] = useState();
|
|
18
|
-
const prevAccessToken =
|
|
18
|
+
const [{ prevAccessToken, hasTokenExpired }, setPrevAccessState] = useState({
|
|
19
|
+
prevAccessToken: elevatedAccess,
|
|
20
|
+
hasTokenExpired: false
|
|
21
|
+
});
|
|
22
|
+
if (elevatedAccess !== prevAccessToken) setPrevAccessState({
|
|
23
|
+
prevAccessToken: elevatedAccess,
|
|
24
|
+
hasTokenExpired: hasTokenExpired || !!prevAccessToken && !elevatedAccess
|
|
25
|
+
});
|
|
19
26
|
const hasCalledOnVerified = useRef(false);
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
prevAccessToken.current = elevatedAccess;
|
|
22
|
-
}, [elevatedAccess]);
|
|
23
27
|
const handleVerificationSuccess = useCallback((elevatedAccessToken) => {
|
|
24
28
|
setAuthenticationChallengeId(void 0);
|
|
25
29
|
if (!hasCalledOnVerified.current) {
|
|
@@ -28,16 +32,13 @@ function ElevatedAccess({ type = "dialog", children, onVerified }) {
|
|
|
28
32
|
}
|
|
29
33
|
}, [onVerified]);
|
|
30
34
|
if (elevatedAccess) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
31
|
-
if (!authenticationChallengeId) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
}
|
|
35
|
+
if (!authenticationChallengeId) return /* @__PURE__ */ jsx(SendVerificationEmailForm, {
|
|
36
|
+
type,
|
|
37
|
+
hasTokenExpired,
|
|
38
|
+
onSuccess: (challengeId) => {
|
|
39
|
+
setAuthenticationChallengeId(challengeId);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
41
42
|
if (authenticationChallengeId) return /* @__PURE__ */ jsx(VerificationIdentityForm, {
|
|
42
43
|
type,
|
|
43
44
|
authenticationChallengeId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"elevated-access.js","names":["Title","Dialog.Title","AlertDialog.Title","Dialog.Description","AlertDialog.Description","Close","Dialog.Close","AlertDialog.Cancel","Button","Form.Root","Otp.Root","Otp.Input"],"sources":["../../../src/lib/elevated-access.tsx"],"sourcesContent":["import { Callout, Flex, Text } from \"@radix-ui/themes\";\nimport type { PropsWithChildren } from \"react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useElevatedAccessToken } from \"../api/api-provider.js\";\nimport { useClientGraphqlMutation } from \"../api/client-graphql/hooks.js\";\nimport { useMe } from \"./api/me.js\";\nimport { expectResultMember } from \"./client-graphql-result.js\";\nimport { AlertDialog, Button, Dialog } from \"./elements.js\";\nimport * as Form from \"./form.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport * as Otp from \"./otp-input.js\";\n\ninterface ElevatedAccessProps extends PropsWithChildren {\n onVerified?: (elevatedAccessToken: string) => Promise<unknown>;\n type?: \"dialog\" | \"alert\";\n}\n\nexport function ElevatedAccess({\n type = \"dialog\",\n children,\n onVerified,\n}: ElevatedAccessProps) {\n const { elevatedAccess } = useElevatedAccessToken();\n const [authenticationChallengeId, setAuthenticationChallengeId] =\n useState<string>();\n\n const prevAccessToken = useRef(elevatedAccess);\n const hasCalledOnVerified = useRef(false);\n\n useEffect(() => {\n prevAccessToken.current = elevatedAccess;\n }, [elevatedAccess]);\n\n const handleVerificationSuccess = useCallback(\n (elevatedAccessToken: string) => {\n setAuthenticationChallengeId(undefined);\n\n // Only call onVerified on the initial verification, not on\n // re-verifications after token expiry, so side effects (e.g. creating a\n // new TOTP factor) don't re-run and discard the user's progress.\n if (!hasCalledOnVerified.current) {\n hasCalledOnVerified.current = true;\n return onVerified?.(elevatedAccessToken);\n }\n },\n [onVerified],\n );\n\n if (elevatedAccess) {\n return <>{children}</>;\n }\n\n if (!authenticationChallengeId) {\n // FIXME: This should be refactored\n // eslint-disable-next-line react-hooks/refs\n const hasTokenExpired = !!prevAccessToken.current;\n\n return (\n <SendVerificationEmailForm\n type={type}\n hasTokenExpired={hasTokenExpired}\n onSuccess={(challengeId) => {\n setAuthenticationChallengeId(challengeId);\n }}\n />\n );\n }\n\n if (authenticationChallengeId) {\n return (\n <VerificationIdentityForm\n type={type}\n authenticationChallengeId={authenticationChallengeId}\n onSuccess={handleVerificationSuccess}\n />\n );\n }\n\n return null;\n}\n\ninterface SendVerificationEmailFormProps {\n onSuccess: (challengeId: string) => unknown | Promise<unknown>;\n type: \"dialog\" | \"alert\";\n hasTokenExpired: boolean;\n}\n\nfunction SendVerificationEmailForm({\n onSuccess,\n type,\n hasTokenExpired,\n}: SendVerificationEmailFormProps) {\n const { data: me } = useMe();\n const sendVerification = useClientGraphqlMutation({\n mutationFn: async (sdk) =>\n expectResultMember(\n (await sdk.SendVerificationCode()).sendVerificationCode,\n \"VerificationCodeSent\",\n ),\n onSuccess: (data) => onSuccess(data.authenticationChallengeId),\n });\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n sendVerification.mutate();\n };\n\n return (\n <form onSubmit={handleSubmit}>\n <Title>\n {hasTokenExpired ? (\n <Translation\n defaultMessage=\"Your verification token has expired\"\n id=\"lb/AsC\"\n description=\"Dialog title when verification token has expired\"\n />\n ) : (\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n )}\n </Title>\n\n <Description mb=\"5\">\n <Translation\n defaultMessage=\"To continue, we need to confirm your identity. We'll send a temporary verification code to {email}.\"\n id=\"FTUSRM\"\n description=\"Description explaining verification code will be sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n {sendVerification.error && (\n <Callout.Root color=\"red\" mt=\"-2\" mb=\"0\">\n <Callout.Text>\n {getMutationErrorMessage(sendVerification.error)}\n </Callout.Text>\n </Callout.Root>\n )}\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button\n variant=\"secondary\"\n type=\"button\"\n disabled={sendVerification.isPending}\n >\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={sendVerification.isPending}>\n <Translation\n defaultMessage=\"Send verification code\"\n id=\"XsTeXJ\"\n description=\"Button text to send verification code\"\n />\n </Button>\n </Flex>\n </form>\n );\n}\n\ninterface VerificationIdentityFormProps {\n onSuccess?: (elevatedAccessToken: string) => unknown | Promise<unknown>;\n authenticationChallengeId: string;\n type: \"dialog\" | \"alert\";\n}\n\nfunction VerificationIdentityForm({\n onSuccess,\n authenticationChallengeId,\n type,\n}: VerificationIdentityFormProps) {\n const { data: me } = useMe();\n const { setElevatedAccess } = useElevatedAccessToken();\n const verifyIdentity = useClientGraphqlMutation({\n mutationFn: async (\n sdk,\n input: { code: string; authenticationChallengeId: string },\n ) =>\n expectResultMember(\n (await sdk.VerifyCurrentEmail({ input })).verifyCurrentEmail,\n \"CurrentEmailVerified\",\n ),\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [otpValue, setOtpValue] = useState<string>(\"\");\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n const formData = new FormData(event.currentTarget);\n const code = formData.get(\"otp-code\")?.toString() ?? \"\";\n\n setIsSubmitting(true);\n\n try {\n // Mutate async so we can wait for the onSuccess callback as well\n const newAuthState = await verifyIdentity.mutateAsync({\n code,\n authenticationChallengeId,\n });\n\n setElevatedAccess({\n token: newAuthState.elevatedAccessToken,\n expiresAt: newAuthState.expiresAt,\n });\n\n if (onSuccess) {\n await new Promise((resolve) => setTimeout(resolve, 200));\n await onSuccess(newAuthState.elevatedAccessToken);\n }\n } catch (error) {\n console.error(error);\n }\n\n setIsSubmitting(false);\n };\n\n return (\n <Form.Root onSubmit={handleSubmit}>\n <Title>\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n </Title>\n\n <Description>\n <Translation\n defaultMessage=\"A verification code was sent to {email}. Please enter it below.\"\n id=\"kvxE0S\"\n description=\"Description explaining where verification code was sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n <Flex direction=\"column\" gap=\"2\" mt=\"5\" mx=\"auto\" width=\"fit-content\">\n <Otp.Root\n autoSubmit\n gap=\"2\"\n justify=\"center\"\n columns=\"repeat(6, 48px)\"\n width=\"fit-content\"\n rows=\"48px\"\n name=\"otp-code\"\n readOnly={isSubmitting}\n value={otpValue}\n onValueChange={setOtpValue}\n >\n <Otp.Input required autoFocus autoComplete=\"off\" />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n </Otp.Root>\n\n {verifyIdentity.error && (\n <Text color=\"red\" size=\"2\" as=\"p\">\n {getMutationErrorMessage(verifyIdentity.error)}\n </Text>\n )}\n </Flex>\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button variant=\"secondary\" type=\"button\">\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={isSubmitting}>\n <Translation\n defaultMessage=\"Confirm\"\n id=\"EmYENK\"\n description=\"Confirm button text for verification\"\n />\n </Button>\n </Flex>\n </Form.Root>\n );\n}\n\nfunction getMutationErrorMessage(error: unknown) {\n let message = typeof error === \"string\" ? error : \"\";\n\n if (error instanceof Error) {\n message = error.message;\n } else if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof error.message === \"string\"\n ) {\n message = error.message;\n }\n\n if (!message || message === \"Bad Request\") {\n message = \"Invalid code, please try again.\";\n }\n\n return message;\n}\n\n// Note: Error messages in getMutationErrorMessage are kept as plain strings\n// since they are displayed dynamically\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"elevated-access.js","names":["Title","Dialog.Title","AlertDialog.Title","Dialog.Description","AlertDialog.Description","Close","Dialog.Close","AlertDialog.Cancel","Button","Form.Root","Otp.Root","Otp.Input"],"sources":["../../../src/lib/elevated-access.tsx"],"sourcesContent":["import { Callout, Flex, Text } from \"@radix-ui/themes\";\nimport type { PropsWithChildren } from \"react\";\nimport { useCallback, useRef, useState } from \"react\";\nimport { useElevatedAccessToken } from \"../api/api-provider.js\";\nimport { useClientGraphqlMutation } from \"../api/client-graphql/hooks.js\";\nimport { useMe } from \"./api/me.js\";\nimport { expectResultMember } from \"./client-graphql-result.js\";\nimport { AlertDialog, Button, Dialog } from \"./elements.js\";\nimport * as Form from \"./form.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport * as Otp from \"./otp-input.js\";\n\ninterface ElevatedAccessProps extends PropsWithChildren {\n onVerified?: (elevatedAccessToken: string) => Promise<unknown>;\n type?: \"dialog\" | \"alert\";\n}\n\nexport function ElevatedAccess({\n type = \"dialog\",\n children,\n onVerified,\n}: ElevatedAccessProps) {\n const { elevatedAccess } = useElevatedAccessToken();\n const [authenticationChallengeId, setAuthenticationChallengeId] =\n useState<string>();\n\n const [{ prevAccessToken, hasTokenExpired }, setPrevAccessState] = useState({\n prevAccessToken: elevatedAccess,\n hasTokenExpired: false,\n });\n if (elevatedAccess !== prevAccessToken) {\n setPrevAccessState({\n prevAccessToken: elevatedAccess,\n hasTokenExpired:\n hasTokenExpired || (!!prevAccessToken && !elevatedAccess),\n });\n }\n\n const hasCalledOnVerified = useRef(false);\n\n const handleVerificationSuccess = useCallback(\n (elevatedAccessToken: string) => {\n setAuthenticationChallengeId(undefined);\n\n // Only call onVerified on the initial verification, not on\n // re-verifications after token expiry, so side effects (e.g. creating a\n // new TOTP factor) don't re-run and discard the user's progress.\n if (!hasCalledOnVerified.current) {\n hasCalledOnVerified.current = true;\n return onVerified?.(elevatedAccessToken);\n }\n },\n [onVerified],\n );\n\n if (elevatedAccess) {\n return <>{children}</>;\n }\n\n if (!authenticationChallengeId) {\n return (\n <SendVerificationEmailForm\n type={type}\n hasTokenExpired={hasTokenExpired}\n onSuccess={(challengeId) => {\n setAuthenticationChallengeId(challengeId);\n }}\n />\n );\n }\n\n if (authenticationChallengeId) {\n return (\n <VerificationIdentityForm\n type={type}\n authenticationChallengeId={authenticationChallengeId}\n onSuccess={handleVerificationSuccess}\n />\n );\n }\n\n return null;\n}\n\ninterface SendVerificationEmailFormProps {\n onSuccess: (challengeId: string) => unknown | Promise<unknown>;\n type: \"dialog\" | \"alert\";\n hasTokenExpired: boolean;\n}\n\nfunction SendVerificationEmailForm({\n onSuccess,\n type,\n hasTokenExpired,\n}: SendVerificationEmailFormProps) {\n const { data: me } = useMe();\n const sendVerification = useClientGraphqlMutation({\n mutationFn: async (sdk) =>\n expectResultMember(\n (await sdk.SendVerificationCode()).sendVerificationCode,\n \"VerificationCodeSent\",\n ),\n onSuccess: (data) => onSuccess(data.authenticationChallengeId),\n });\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n sendVerification.mutate();\n };\n\n return (\n <form onSubmit={handleSubmit}>\n <Title>\n {hasTokenExpired ? (\n <Translation\n defaultMessage=\"Your verification token has expired\"\n id=\"lb/AsC\"\n description=\"Dialog title when verification token has expired\"\n />\n ) : (\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n )}\n </Title>\n\n <Description mb=\"5\">\n <Translation\n defaultMessage=\"To continue, we need to confirm your identity. We'll send a temporary verification code to {email}.\"\n id=\"FTUSRM\"\n description=\"Description explaining verification code will be sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n {sendVerification.error && (\n <Callout.Root color=\"red\" mt=\"-2\" mb=\"0\">\n <Callout.Text>\n {getMutationErrorMessage(sendVerification.error)}\n </Callout.Text>\n </Callout.Root>\n )}\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button\n variant=\"secondary\"\n type=\"button\"\n disabled={sendVerification.isPending}\n >\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={sendVerification.isPending}>\n <Translation\n defaultMessage=\"Send verification code\"\n id=\"XsTeXJ\"\n description=\"Button text to send verification code\"\n />\n </Button>\n </Flex>\n </form>\n );\n}\n\ninterface VerificationIdentityFormProps {\n onSuccess?: (elevatedAccessToken: string) => unknown | Promise<unknown>;\n authenticationChallengeId: string;\n type: \"dialog\" | \"alert\";\n}\n\nfunction VerificationIdentityForm({\n onSuccess,\n authenticationChallengeId,\n type,\n}: VerificationIdentityFormProps) {\n const { data: me } = useMe();\n const { setElevatedAccess } = useElevatedAccessToken();\n const verifyIdentity = useClientGraphqlMutation({\n mutationFn: async (\n sdk,\n input: { code: string; authenticationChallengeId: string },\n ) =>\n expectResultMember(\n (await sdk.VerifyCurrentEmail({ input })).verifyCurrentEmail,\n \"CurrentEmailVerified\",\n ),\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [otpValue, setOtpValue] = useState<string>(\"\");\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n const formData = new FormData(event.currentTarget);\n const code = formData.get(\"otp-code\")?.toString() ?? \"\";\n\n setIsSubmitting(true);\n\n try {\n // Mutate async so we can wait for the onSuccess callback as well\n const newAuthState = await verifyIdentity.mutateAsync({\n code,\n authenticationChallengeId,\n });\n\n setElevatedAccess({\n token: newAuthState.elevatedAccessToken,\n expiresAt: newAuthState.expiresAt,\n });\n\n if (onSuccess) {\n await new Promise((resolve) => setTimeout(resolve, 200));\n await onSuccess(newAuthState.elevatedAccessToken);\n }\n } catch (error) {\n console.error(error);\n }\n\n setIsSubmitting(false);\n };\n\n return (\n <Form.Root onSubmit={handleSubmit}>\n <Title>\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n </Title>\n\n <Description>\n <Translation\n defaultMessage=\"A verification code was sent to {email}. Please enter it below.\"\n id=\"kvxE0S\"\n description=\"Description explaining where verification code was sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n <Flex direction=\"column\" gap=\"2\" mt=\"5\" mx=\"auto\" width=\"fit-content\">\n <Otp.Root\n autoSubmit\n gap=\"2\"\n justify=\"center\"\n columns=\"repeat(6, 48px)\"\n width=\"fit-content\"\n rows=\"48px\"\n name=\"otp-code\"\n readOnly={isSubmitting}\n value={otpValue}\n onValueChange={setOtpValue}\n >\n <Otp.Input required autoFocus autoComplete=\"off\" />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n </Otp.Root>\n\n {verifyIdentity.error && (\n <Text color=\"red\" size=\"2\" as=\"p\">\n {getMutationErrorMessage(verifyIdentity.error)}\n </Text>\n )}\n </Flex>\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button variant=\"secondary\" type=\"button\">\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={isSubmitting}>\n <Translation\n defaultMessage=\"Confirm\"\n id=\"EmYENK\"\n description=\"Confirm button text for verification\"\n />\n </Button>\n </Flex>\n </Form.Root>\n );\n}\n\nfunction getMutationErrorMessage(error: unknown) {\n let message = typeof error === \"string\" ? error : \"\";\n\n if (error instanceof Error) {\n message = error.message;\n } else if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof error.message === \"string\"\n ) {\n message = error.message;\n }\n\n if (!message || message === \"Bad Request\") {\n message = \"Invalid code, please try again.\";\n }\n\n return message;\n}\n\n// Note: Error messages in getMutationErrorMessage are kept as plain strings\n// since they are displayed dynamically\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otp-input.d.ts","names":["TextField"],"sources":["../../../src/lib/otp-input.tsx"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"otp-input.d.ts","names":["TextField"],"sources":["../../../src/lib/otp-input.tsx"],"mappings":";;;;KAsBK,eAAe,MAAM,gCAAgC;EACxD;EACA;EACA;EACA;EACA;EACA,eAAe,MAAM,SAAS,MAAM;EACpC;;qBAGW,MAAI,MAAA,0BAAA,eAAA,MAAA,cAAA;UAqJP,mBAAmB,MAAM,sBAAsBA;EACvD;;qBAGW,OAAK,MAAA,0BAAA,KAAA,qBAAA,MAAA,cAAA"}
|
|
@@ -7,9 +7,11 @@ import * as React from "react";
|
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
//#region src/lib/otp-input.tsx
|
|
9
9
|
const OtpContext = React.createContext(void 0);
|
|
10
|
+
OtpContext.displayName = "OtpContext";
|
|
11
|
+
const OtpIndexContext = React.createContext(-1);
|
|
12
|
+
OtpIndexContext.displayName = "OtpIndexContext";
|
|
10
13
|
const Root = React.forwardRef(function Root({ name, id, value: valueProp, onValueChange, autoSubmit, children, readOnly, state, ...gridProps }, forwardedRef) {
|
|
11
14
|
const [lastCharIndex, setLastCharIndex] = React.useState(0);
|
|
12
|
-
const [allChildrenAdded, setAllChildrenAdded] = React.useState(false);
|
|
13
15
|
const childCount = React.Children.count(children);
|
|
14
16
|
const [value, setValueState] = React.useState(() => getValueAsArray(valueProp, childCount));
|
|
15
17
|
const [previousValueProp, setPreviousValueProp] = React.useState(valueProp);
|
|
@@ -33,13 +35,9 @@ const Root = React.forwardRef(function Root({ name, id, value: valueProp, onValu
|
|
|
33
35
|
autoSubmit
|
|
34
36
|
]);
|
|
35
37
|
const handleEnterPressed = React.useCallback(() => attemptAutoSubmit(true), [attemptAutoSubmit]);
|
|
36
|
-
const handleChildAdd = React.useCallback((
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
childrenRefs.current.push(input);
|
|
40
|
-
} else childrenRefs.current.pop();
|
|
41
|
-
if (childrenRefs.current.length === childCount) setAllChildrenAdded(true);
|
|
42
|
-
}, [childCount]);
|
|
38
|
+
const handleChildAdd = React.useCallback((element, index) => {
|
|
39
|
+
childrenRefs.current[index] = element;
|
|
40
|
+
}, []);
|
|
43
41
|
const handleCharChange = React.useCallback((char, index) => {
|
|
44
42
|
setValue((previousValue) => {
|
|
45
43
|
const newValue = [...previousValue];
|
|
@@ -52,18 +50,16 @@ const Root = React.forwardRef(function Root({ name, id, value: valueProp, onValu
|
|
|
52
50
|
value,
|
|
53
51
|
readOnly,
|
|
54
52
|
state,
|
|
55
|
-
allChildrenAdded,
|
|
56
53
|
onEnterPressed: handleEnterPressed,
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
onCharChange: handleCharChange,
|
|
55
|
+
onChildAdd: handleChildAdd
|
|
59
56
|
}), [
|
|
60
57
|
value,
|
|
61
|
-
allChildrenAdded,
|
|
62
58
|
readOnly,
|
|
63
59
|
state,
|
|
64
60
|
handleEnterPressed,
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
handleCharChange,
|
|
62
|
+
handleChildAdd
|
|
67
63
|
]);
|
|
68
64
|
React.useEffect(attemptAutoSubmit, [attemptAutoSubmit]);
|
|
69
65
|
return /* @__PURE__ */ jsx(OtpContext.Provider, {
|
|
@@ -80,7 +76,10 @@ const Root = React.forwardRef(function Root({ name, id, value: valueProp, onValu
|
|
|
80
76
|
const index = Math.min(sanitizedValue.length, childCount - 1);
|
|
81
77
|
childrenRefs.current?.[index]?.focus();
|
|
82
78
|
},
|
|
83
|
-
children: [children, /* @__PURE__ */ jsx(
|
|
79
|
+
children: [React.Children.map(children, (child, index) => /* @__PURE__ */ jsx(OtpIndexContext.Provider, {
|
|
80
|
+
value: index,
|
|
81
|
+
children: child
|
|
82
|
+
})), /* @__PURE__ */ jsx("input", {
|
|
84
83
|
ref: useComposedRefs(forwardedRef, hiddenInputRef),
|
|
85
84
|
defaultValue: value.join(""),
|
|
86
85
|
minLength: childCount,
|
|
@@ -92,9 +91,12 @@ const Root = React.forwardRef(function Root({ name, id, value: valueProp, onValu
|
|
|
92
91
|
});
|
|
93
92
|
const Input = React.forwardRef(function Input({ style, readOnly, autoComplete = "off", ...props }, forwardedRef) {
|
|
94
93
|
const otpContext = useOptContext();
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
94
|
+
const { onChildAdd } = otpContext;
|
|
95
|
+
const index = React.useContext(OtpIndexContext);
|
|
96
|
+
const inputRef = React.useCallback((input) => {
|
|
97
|
+
onChildAdd(input, index);
|
|
98
|
+
}, [onChildAdd, index]);
|
|
99
|
+
const composedInputRef = useComposedRefs(forwardedRef, inputRef);
|
|
98
100
|
const char = otpContext.value[index] ?? "";
|
|
99
101
|
return /* @__PURE__ */ jsx(FormField, {
|
|
100
102
|
name: `otp-${index}`,
|
|
@@ -121,7 +123,6 @@ const Input = React.forwardRef(function Input({ style, readOnly, autoComplete =
|
|
|
121
123
|
onChange: (event) => {
|
|
122
124
|
if (event.target.validity.valid || event.target.value === "") {
|
|
123
125
|
const char = event.target.value;
|
|
124
|
-
const index = Number(event.target.dataset.index ?? -1);
|
|
125
126
|
otpContext.onCharChange(char, index);
|
|
126
127
|
focusSibling(event.currentTarget, { back: char === "" });
|
|
127
128
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otp-input.js","names":["Form.Field","Form.Control","TextField"],"sources":["../../../src/lib/otp-input.tsx"],"sourcesContent":["\"use client\";\nimport { Grid } from \"@radix-ui/themes\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"./compose-refs.js\";\nimport { TextField } from \"./elements.js\";\nimport * as Form from \"./form.js\";\n\ninterface OptContextType {\n value: string[];\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n onEnterPressed: () => void;\n onChildAdd: (input: HTMLInputElement) => void;\n onCharChange: (char: string, index: number) => void;\n allChildrenAdded: boolean;\n}\n\nconst OtpContext = React.createContext<OptContextType | undefined>(undefined);\n\ntype OtpRootProps = React.ComponentPropsWithoutRef<typeof Grid> & {\n id?: string;\n name?: string;\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n value: string;\n onValueChange: React.Dispatch<React.SetStateAction<string>>;\n autoSubmit?: boolean;\n};\n\nexport const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(\n function Root(\n {\n name,\n id,\n value: valueProp,\n onValueChange,\n autoSubmit,\n children,\n readOnly,\n state,\n ...gridProps\n },\n forwardedRef,\n ) {\n const [lastCharIndex, setLastCharIndex] = React.useState<number>(0);\n const [allChildrenAdded, setAllChildrenAdded] =\n React.useState<boolean>(false);\n const childCount = React.Children.count(children);\n\n // Internal array state is the source of truth so that empty slots between\n // filled digits are preserved. A `value` string can't encode a gap (e.g.\n // `[\"1\", \"\", \"3\"]` and `[\"1\", \"3\"]` both serialize to \"13\"), so deriving\n // state straight from the prop would shift digits whenever an inner box is\n // cleared or filled out of order.\n const [value, setValueState] = React.useState<string[]>(() =>\n getValueAsArray(valueProp, childCount),\n );\n\n const [previousValueProp, setPreviousValueProp] = React.useState(valueProp);\n if (previousValueProp !== valueProp) {\n setPreviousValueProp(valueProp);\n if (valueProp !== value.join(\"\")) {\n setValueState(getValueAsArray(valueProp, childCount));\n }\n }\n\n const setValue = React.useCallback(\n (action: React.SetStateAction<string[]>) => {\n const nextArrayValue =\n typeof action === \"function\" ? action(value) : action;\n setValueState(nextArrayValue);\n onValueChange(nextArrayValue.join(\"\"));\n },\n [value, onValueChange],\n );\n\n const hiddenInputRef = React.useRef<HTMLInputElement>(null);\n const childrenRefs = React.useRef<HTMLInputElement[]>([]);\n\n const attemptAutoSubmit = React.useCallback(\n (enterPressed = false) => {\n if (\n autoSubmit &&\n value.length > 0 &&\n value.every((char) => char !== \"\") &&\n (enterPressed || lastCharIndex + 1 === childCount)\n ) {\n hiddenInputRef.current?.form?.requestSubmit();\n }\n },\n [value, childCount, lastCharIndex, autoSubmit],\n );\n\n const handleEnterPressed = React.useCallback(\n () => attemptAutoSubmit(true),\n [attemptAutoSubmit],\n );\n\n const handleChildAdd = React.useCallback(\n (input: HTMLInputElement) => {\n if (input) {\n input.dataset.index = `${childrenRefs.current.length}`;\n childrenRefs.current.push(input);\n } else {\n childrenRefs.current.pop();\n }\n\n if (childrenRefs.current.length === childCount) {\n setAllChildrenAdded(true);\n }\n },\n [childCount],\n );\n\n const handleCharChange = React.useCallback(\n (char: string, index: number) => {\n setValue((previousValue) => {\n const newValue = [...previousValue];\n newValue[index] = char;\n return newValue;\n });\n setLastCharIndex(index);\n },\n [setValue],\n );\n\n const otpContext = React.useMemo(\n () => ({\n value,\n readOnly,\n state,\n allChildrenAdded,\n onEnterPressed: handleEnterPressed,\n onChildAdd: handleChildAdd,\n onCharChange: handleCharChange,\n }),\n [\n value,\n allChildrenAdded,\n readOnly,\n state,\n handleEnterPressed,\n handleChildAdd,\n handleCharChange,\n ],\n );\n\n React.useEffect(attemptAutoSubmit, [attemptAutoSubmit]);\n\n return (\n <OtpContext.Provider value={otpContext}>\n <Grid\n columns={`repeat(${childCount}, 1fr)`}\n {...gridProps}\n onPaste={(event: React.ClipboardEvent<HTMLDivElement>) => {\n event.preventDefault();\n const pastedValue = event.clipboardData.getData(\"Text\");\n const sanitizedValue = pastedValue\n .replace(/[^\\d]/g, \"\")\n .slice(0, childCount);\n const value = sanitizedValue\n .padEnd(childCount, \"#\")\n .split(\"\")\n .map((char) => (char === \"#\" ? \"\" : char));\n\n setValue(value);\n setLastCharIndex(sanitizedValue.length - 1);\n\n const index = Math.min(sanitizedValue.length, childCount - 1);\n childrenRefs.current?.[index]?.focus();\n }}\n >\n {children}\n <input\n ref={useComposedRefs(forwardedRef, hiddenInputRef)}\n defaultValue={value.join(\"\")}\n minLength={childCount}\n name={name}\n type=\"hidden\"\n />\n </Grid>\n </OtpContext.Provider>\n );\n },\n);\n\ninterface InputProps extends React.ComponentProps<typeof TextField> {\n autoComplete?: \"one-time-code\" | \"off\";\n}\n\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(\n function Input(\n { style, readOnly, autoComplete = \"off\", ...props },\n forwardedRef,\n ) {\n const otpContext = useOptContext();\n const inputRef = React.useRef<HTMLInputElement>(null);\n const composedInputRef = useComposedRefs(\n forwardedRef,\n inputRef,\n otpContext.onChildAdd,\n );\n\n // FIXME: This should be refactored\n // eslint-disable-next-line react-hooks/refs\n const index = Number(inputRef.current?.dataset.index ?? -1);\n const char = otpContext.value[index] ?? \"\";\n\n return (\n <Form.Field name={`otp-${index}`} asChild>\n <Form.Control asChild>\n <TextField\n ref={composedInputRef}\n autoComplete={index === 0 ? autoComplete : \"off\"}\n color={otpContext.state === \"invalid\" ? \"red\" : undefined}\n inputMode=\"numeric\"\n maxLength={1}\n pattern=\"\\d{1}\"\n readOnly={readOnly ?? otpContext.readOnly}\n size=\"3\"\n value={char}\n variant={otpContext.state === \"invalid\" ? \"soft\" : undefined}\n style={{\n ...style,\n height: \"auto\",\n \"--text-field-padding\": 0,\n textAlign: \"center\",\n }}\n onChange={(event) => {\n // Update the value if it matches the input pattern (number only) or is empty (backspace)\n if (event.target.validity.valid || event.target.value === \"\") {\n const char = event.target.value;\n const index = Number(event.target.dataset.index ?? -1);\n otpContext.onCharChange(char, index);\n focusSibling(event.currentTarget, { back: char === \"\" });\n }\n }}\n onKeyDown={(event) => {\n if (event.key === \"ArrowLeft\") {\n focusSibling(event.currentTarget, { back: true });\n event.preventDefault();\n } else if (event.key === \"ArrowRight\") {\n focusSibling(event.currentTarget);\n event.preventDefault();\n } else if (event.key === \"Backspace\" && char === \"\") {\n focusSibling(event.currentTarget, { back: true });\n } else if (event.key === \"Enter\" && char !== \"\") {\n otpContext.onEnterPressed();\n }\n }}\n {...props}\n />\n </Form.Control>\n </Form.Field>\n );\n },\n);\n\nconst useOptContext = () => {\n const optContext = React.useContext(OtpContext);\n\n if (!optContext) {\n throw new Error(\n \"OtpInput compound components cannot be rendered outside the OtpRoot component\",\n );\n }\n\n return optContext;\n};\n\nfunction focusSibling(input: HTMLInputElement, { back = false } = {}) {\n const sibling = back\n ? input.parentElement?.previousSibling\n : input.parentElement?.nextSibling;\n const siblingInput = sibling?.firstChild;\n if (siblingInput && siblingInput instanceof HTMLInputElement) {\n siblingInput?.focus();\n siblingInput?.select();\n }\n}\n\nconst getValueAsArray = (value: string | undefined, length: number) => {\n if (!value) {\n return createEmptyArray(length);\n }\n\n return createEmptyArray(length).map((_, index) => value?.[index] ?? \"\");\n};\n\nconst createEmptyArray = (length: number): string[] =>\n Array.from<string>({ length }).fill(\"\");\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"otp-input.js","names":["Form.Field","Form.Control","TextField"],"sources":["../../../src/lib/otp-input.tsx"],"sourcesContent":["\"use client\";\nimport { Grid } from \"@radix-ui/themes\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"./compose-refs.js\";\nimport { TextField } from \"./elements.js\";\nimport * as Form from \"./form.js\";\n\ninterface OptContextType {\n value: string[];\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n onEnterPressed: () => void;\n onCharChange: (char: string, index: number) => void;\n onChildAdd: (element: HTMLInputElement | null, index: number) => void;\n}\n\nconst OtpContext = React.createContext<OptContextType | undefined>(undefined);\nOtpContext.displayName = \"OtpContext\";\n\nconst OtpIndexContext = React.createContext<number>(-1);\nOtpIndexContext.displayName = \"OtpIndexContext\";\n\ntype OtpRootProps = React.ComponentPropsWithoutRef<typeof Grid> & {\n id?: string;\n name?: string;\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n value: string;\n onValueChange: React.Dispatch<React.SetStateAction<string>>;\n autoSubmit?: boolean;\n};\n\nexport const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(\n function Root(\n {\n name,\n id,\n value: valueProp,\n onValueChange,\n autoSubmit,\n children,\n readOnly,\n state,\n ...gridProps\n },\n forwardedRef,\n ) {\n const [lastCharIndex, setLastCharIndex] = React.useState<number>(0);\n const childCount = React.Children.count(children);\n\n // Internal array state is the source of truth so that empty slots between\n // filled digits are preserved. A `value` string can't encode a gap (e.g.\n // `[\"1\", \"\", \"3\"]` and `[\"1\", \"3\"]` both serialize to \"13\"), so deriving\n // state straight from the prop would shift digits whenever an inner box is\n // cleared or filled out of order.\n const [value, setValueState] = React.useState<string[]>(() =>\n getValueAsArray(valueProp, childCount),\n );\n\n const [previousValueProp, setPreviousValueProp] = React.useState(valueProp);\n if (previousValueProp !== valueProp) {\n setPreviousValueProp(valueProp);\n if (valueProp !== value.join(\"\")) {\n setValueState(getValueAsArray(valueProp, childCount));\n }\n }\n\n const setValue = React.useCallback(\n (action: React.SetStateAction<string[]>) => {\n const nextArrayValue =\n typeof action === \"function\" ? action(value) : action;\n setValueState(nextArrayValue);\n onValueChange(nextArrayValue.join(\"\"));\n },\n [value, onValueChange],\n );\n\n const hiddenInputRef = React.useRef<HTMLInputElement>(null);\n const childrenRefs = React.useRef<(HTMLInputElement | null)[]>([]);\n\n const attemptAutoSubmit = React.useCallback(\n (enterPressed = false) => {\n if (\n autoSubmit &&\n value.length > 0 &&\n value.every((char) => char !== \"\") &&\n (enterPressed || lastCharIndex + 1 === childCount)\n ) {\n hiddenInputRef.current?.form?.requestSubmit();\n }\n },\n [value, childCount, lastCharIndex, autoSubmit],\n );\n\n const handleEnterPressed = React.useCallback(\n () => attemptAutoSubmit(true),\n [attemptAutoSubmit],\n );\n\n const handleChildAdd = React.useCallback(\n (element: HTMLInputElement | null, index: number) => {\n childrenRefs.current[index] = element;\n },\n [],\n );\n\n const handleCharChange = React.useCallback(\n (char: string, index: number) => {\n setValue((previousValue) => {\n const newValue = [...previousValue];\n newValue[index] = char;\n return newValue;\n });\n setLastCharIndex(index);\n },\n [setValue],\n );\n\n const otpContext = React.useMemo<OptContextType>(\n () =>\n ({\n value,\n readOnly,\n state,\n onEnterPressed: handleEnterPressed,\n onCharChange: handleCharChange,\n onChildAdd: handleChildAdd,\n }) satisfies OptContextType,\n [\n value,\n readOnly,\n state,\n handleEnterPressed,\n handleCharChange,\n handleChildAdd,\n ],\n );\n\n React.useEffect(attemptAutoSubmit, [attemptAutoSubmit]);\n\n return (\n <OtpContext.Provider value={otpContext}>\n <Grid\n columns={`repeat(${childCount}, 1fr)`}\n {...gridProps}\n onPaste={(event: React.ClipboardEvent<HTMLDivElement>) => {\n event.preventDefault();\n const pastedValue = event.clipboardData.getData(\"Text\");\n const sanitizedValue = pastedValue\n .replace(/[^\\d]/g, \"\")\n .slice(0, childCount);\n const value = sanitizedValue\n .padEnd(childCount, \"#\")\n .split(\"\")\n .map((char) => (char === \"#\" ? \"\" : char));\n\n setValue(value);\n setLastCharIndex(sanitizedValue.length - 1);\n\n const index = Math.min(sanitizedValue.length, childCount - 1);\n childrenRefs.current?.[index]?.focus();\n }}\n >\n {React.Children.map(children, (child, index) => (\n <OtpIndexContext.Provider value={index}>\n {child}\n </OtpIndexContext.Provider>\n ))}\n <input\n ref={useComposedRefs(forwardedRef, hiddenInputRef)}\n defaultValue={value.join(\"\")}\n minLength={childCount}\n name={name}\n type=\"hidden\"\n />\n </Grid>\n </OtpContext.Provider>\n );\n },\n);\n\ninterface InputProps extends React.ComponentProps<typeof TextField> {\n autoComplete?: \"one-time-code\" | \"off\";\n}\n\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(\n function Input(\n { style, readOnly, autoComplete = \"off\", ...props },\n forwardedRef,\n ) {\n const otpContext = useOptContext();\n const { onChildAdd } = otpContext;\n const index = React.useContext(OtpIndexContext);\n const inputRef = React.useCallback(\n (input: HTMLInputElement | null) => {\n onChildAdd(input, index);\n },\n [onChildAdd, index],\n );\n const composedInputRef = useComposedRefs(forwardedRef, inputRef);\n\n const char = otpContext.value[index] ?? \"\";\n\n return (\n <Form.Field name={`otp-${index}`} asChild>\n <Form.Control asChild>\n <TextField\n ref={composedInputRef}\n autoComplete={index === 0 ? autoComplete : \"off\"}\n color={otpContext.state === \"invalid\" ? \"red\" : undefined}\n inputMode=\"numeric\"\n maxLength={1}\n pattern=\"\\d{1}\"\n readOnly={readOnly ?? otpContext.readOnly}\n size=\"3\"\n value={char}\n variant={otpContext.state === \"invalid\" ? \"soft\" : undefined}\n style={{\n ...style,\n height: \"auto\",\n \"--text-field-padding\": 0,\n textAlign: \"center\",\n }}\n onChange={(event) => {\n // Update the value if it matches the input pattern (number only) or is empty (backspace)\n if (event.target.validity.valid || event.target.value === \"\") {\n const char = event.target.value;\n otpContext.onCharChange(char, index);\n focusSibling(event.currentTarget, { back: char === \"\" });\n }\n }}\n onKeyDown={(event) => {\n if (event.key === \"ArrowLeft\") {\n focusSibling(event.currentTarget, { back: true });\n event.preventDefault();\n } else if (event.key === \"ArrowRight\") {\n focusSibling(event.currentTarget);\n event.preventDefault();\n } else if (event.key === \"Backspace\" && char === \"\") {\n focusSibling(event.currentTarget, { back: true });\n } else if (event.key === \"Enter\" && char !== \"\") {\n otpContext.onEnterPressed();\n }\n }}\n {...props}\n />\n </Form.Control>\n </Form.Field>\n );\n },\n);\n\nconst useOptContext = () => {\n const optContext = React.useContext(OtpContext);\n if (!optContext) {\n throw new Error(\n \"OtpInput compound components cannot be rendered outside the OtpRoot component\",\n );\n }\n\n return optContext;\n};\n\nfunction focusSibling(input: HTMLInputElement, { back = false } = {}) {\n const sibling = back\n ? input.parentElement?.previousSibling\n : input.parentElement?.nextSibling;\n const siblingInput = sibling?.firstChild;\n if (siblingInput && siblingInput instanceof HTMLInputElement) {\n siblingInput?.focus();\n siblingInput?.select();\n }\n}\n\nconst getValueAsArray = (value: string | undefined, length: number) => {\n if (!value) {\n return createEmptyArray(length);\n }\n\n return createEmptyArray(length).map((_, index) => value?.[index] ?? \"\");\n};\n\nconst createEmptyArray = (length: number): string[] =>\n Array.from<string>({ length }).fill(\"\");\n"],"mappings":"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"}
|
|
@@ -9,7 +9,13 @@ import { CheckIcon } from "@radix-ui/react-icons";
|
|
|
9
9
|
const DONE_TIMEOUT_MS = 1500;
|
|
10
10
|
const SAVING_TIMEOUT_MS = 600;
|
|
11
11
|
function SaveButton({ asChild = false, children, loading, done, onDone, ...props }) {
|
|
12
|
-
const [
|
|
12
|
+
const [minElapsed, setMinElapsed] = useState(!!done);
|
|
13
|
+
const [prevLoading, setPrevLoading] = useState(loading);
|
|
14
|
+
if (loading !== prevLoading) {
|
|
15
|
+
setPrevLoading(loading);
|
|
16
|
+
if (loading) setMinElapsed(false);
|
|
17
|
+
}
|
|
18
|
+
const state = loading ? "loading" : done ? minElapsed ? "done" : "loading" : "idle";
|
|
13
19
|
const loadingStartTime = useRef(null);
|
|
14
20
|
const onDoneRef = useRef(onDone);
|
|
15
21
|
const ButtonComponent = asChild ? Slot : Button$1;
|
|
@@ -18,24 +24,23 @@ function SaveButton({ asChild = false, children, loading, done, onDone, ...props
|
|
|
18
24
|
}, [onDone]);
|
|
19
25
|
useEffect(() => {
|
|
20
26
|
if (loading) {
|
|
21
|
-
setState("loading");
|
|
22
27
|
loadingStartTime.current = Date.now();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!done) return;
|
|
31
|
+
const loadingDuration = loadingStartTime.current ? Date.now() - loadingStartTime.current : 0;
|
|
32
|
+
const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);
|
|
33
|
+
let doneTimer = null;
|
|
34
|
+
const minElapsedTimer = window.setTimeout(() => {
|
|
35
|
+
setMinElapsed(true);
|
|
36
|
+
doneTimer = window.setTimeout(() => {
|
|
37
|
+
onDoneRef.current?.();
|
|
38
|
+
}, DONE_TIMEOUT_MS);
|
|
39
|
+
}, remainingDelay);
|
|
40
|
+
return () => {
|
|
41
|
+
window.clearTimeout(minElapsedTimer);
|
|
42
|
+
if (doneTimer !== null) window.clearTimeout(doneTimer);
|
|
43
|
+
};
|
|
39
44
|
}, [loading, done]);
|
|
40
45
|
return /* @__PURE__ */ jsxs(ButtonComponent, {
|
|
41
46
|
...props,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"save-button.js","names":["Button"],"sources":["../../../src/lib/save-button.tsx"],"sourcesContent":["import { CheckIcon } from \"@radix-ui/react-icons\";\nimport { Flex, Slot, Slottable, Spinner, Text } from \"@radix-ui/themes\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Button } from \"./elements.js\";\nimport type { ButtonProps } from \"./elements.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport { namespaceClassNames } from \"./widget-dom-props.js\";\n\nconst DONE_TIMEOUT_MS = 1500;\nconst SAVING_TIMEOUT_MS = 600;\n\ninterface SaveButtonProps extends ButtonProps {\n asChild?: boolean;\n children: React.ReactNode;\n loading?: boolean;\n done?: boolean;\n onDone?: () => void;\n}\n\nexport function SaveButton({\n asChild = false,\n children,\n loading,\n done,\n onDone,\n ...props\n}: SaveButtonProps) {\n const [
|
|
1
|
+
{"version":3,"file":"save-button.js","names":["Button"],"sources":["../../../src/lib/save-button.tsx"],"sourcesContent":["import { CheckIcon } from \"@radix-ui/react-icons\";\nimport { Flex, Slot, Slottable, Spinner, Text } from \"@radix-ui/themes\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Button } from \"./elements.js\";\nimport type { ButtonProps } from \"./elements.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport { namespaceClassNames } from \"./widget-dom-props.js\";\n\nconst DONE_TIMEOUT_MS = 1500;\nconst SAVING_TIMEOUT_MS = 600;\n\ninterface SaveButtonProps extends ButtonProps {\n asChild?: boolean;\n children: React.ReactNode;\n loading?: boolean;\n done?: boolean;\n onDone?: () => void;\n}\n\nexport function SaveButton({\n asChild = false,\n children,\n loading,\n done,\n onDone,\n ...props\n}: SaveButtonProps) {\n // Whether the spinner has been visible for at least SAVING_TIMEOUT_MS, so a\n // fast response doesn't flash it for a single frame.\n const [minElapsed, setMinElapsed] = useState(!!done);\n const [prevLoading, setPrevLoading] = useState(loading);\n if (loading !== prevLoading) {\n setPrevLoading(loading);\n if (loading) {\n setMinElapsed(false);\n }\n }\n\n const state = loading\n ? \"loading\"\n : done\n ? minElapsed\n ? \"done\"\n : \"loading\"\n : \"idle\";\n\n const loadingStartTime = useRef<number | null>(null);\n const onDoneRef = useRef(onDone);\n const ButtonComponent = asChild ? Slot : Button;\n\n useEffect(() => {\n onDoneRef.current = onDone;\n }, [onDone]);\n\n useEffect(() => {\n if (loading) {\n loadingStartTime.current = Date.now();\n return;\n }\n if (!done) {\n return;\n }\n\n const loadingDuration = loadingStartTime.current\n ? Date.now() - loadingStartTime.current\n : 0;\n const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);\n\n let doneTimer: number | null = null;\n const minElapsedTimer = window.setTimeout(() => {\n setMinElapsed(true);\n\n doneTimer = window.setTimeout(() => {\n onDoneRef.current?.();\n }, DONE_TIMEOUT_MS);\n }, remainingDelay);\n\n return () => {\n window.clearTimeout(minElapsedTimer);\n if (doneTimer !== null) {\n window.clearTimeout(doneTimer);\n }\n };\n }, [loading, done]);\n\n return (\n <ButtonComponent\n {...props}\n className={namespaceClassNames(\"save-button\")}\n data-save-state={state}\n >\n <Slottable>{children}</Slottable>\n {state === \"loading\" && (\n <Flex\n as=\"span\"\n align=\"center\"\n justify=\"center\"\n position=\"absolute\"\n inset=\"0\"\n >\n <Spinner size=\"1\" />\n </Flex>\n )}\n\n {state === \"done\" && (\n <Flex\n as=\"span\"\n align=\"center\"\n justify=\"center\"\n position=\"absolute\"\n inset=\"0\"\n gap=\"1\"\n >\n <CheckIcon\n width=\"18px\"\n height=\"18px\"\n style={{ marginLeft: \"-4px\" }}\n className={namespaceClassNames(\"save-button__done-icon\")}\n />\n <Text className={namespaceClassNames(\"save-button__done-text\")}>\n <Translation\n defaultMessage=\"Done\"\n id=\"wjt0K3\"\n description=\"Button label indicating an action has completed successfully\"\n />\n </Text>\n </Flex>\n )}\n </ButtonComponent>\n );\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,kBAAkB;AACxB,MAAM,oBAAoB;AAU1B,SAAgB,WAAW,EACzB,UAAU,OACV,UACA,SACA,MACA,QACA,GAAG,SACe;CAGlB,MAAM,CAAC,YAAY,iBAAiB,SAAS,CAAC,CAAC,IAAI;CACnD,MAAM,CAAC,aAAa,kBAAkB,SAAS,OAAO;CACtD,IAAI,YAAY,aAAa;EAC3B,eAAe,OAAO;EACtB,IAAI,SACF,cAAc,KAAK;CAEvB;CAEA,MAAM,QAAQ,UACV,YACA,OACE,aACE,SACA,YACF;CAEN,MAAM,mBAAmB,OAAsB,IAAI;CACnD,MAAM,YAAY,OAAO,MAAM;CAC/B,MAAM,kBAAkB,UAAU,OAAOA;CAEzC,gBAAgB;EACd,UAAU,UAAU;CACtB,GAAG,CAAC,MAAM,CAAC;CAEX,gBAAgB;EACd,IAAI,SAAS;GACX,iBAAiB,UAAU,KAAK,IAAI;GACpC;EACF;EACA,IAAI,CAAC,MACH;EAGF,MAAM,kBAAkB,iBAAiB,UACrC,KAAK,IAAI,IAAI,iBAAiB,UAC9B;EACJ,MAAM,iBAAiB,KAAK,IAAI,GAAG,oBAAoB,eAAe;EAEtE,IAAI,YAA2B;EAC/B,MAAM,kBAAkB,OAAO,iBAAiB;GAC9C,cAAc,IAAI;GAElB,YAAY,OAAO,iBAAiB;IAClC,UAAU,UAAU;GACtB,GAAG,eAAe;EACpB,GAAG,cAAc;EAEjB,aAAa;GACX,OAAO,aAAa,eAAe;GACnC,IAAI,cAAc,MAChB,OAAO,aAAa,SAAS;EAEjC;CACF,GAAG,CAAC,SAAS,IAAI,CAAC;CAElB,OACE,qBAAC,iBAAD;EACE,GAAI;EACJ,WAAW,oBAAoB,aAAa;EAC5C,mBAAiB;EAHnB,UAAA;GAKE,oBAAC,WAAD,EAAY,SAAoB,CAAA;GAC/B,UAAU,aACT,oBAAC,MAAD;IACE,IAAG;IACH,OAAM;IACN,SAAQ;IACR,UAAS;IACT,OAAM;IAEN,UAAA,oBAAC,SAAD,EAAS,MAAK,IAAK,CAAA;GACf,CAAA;GAGP,UAAU,UACT,qBAAC,MAAD;IACE,IAAG;IACH,OAAM;IACN,SAAQ;IACR,UAAS;IACT,OAAM;IACN,KAAI;IANN,UAAA,CAQE,oBAAC,WAAD;KACE,OAAM;KACN,QAAO;KACP,OAAO,EAAE,YAAY,OAAO;KAC5B,WAAW,oBAAoB,wBAAwB;IACxD,CAAA,GACD,oBAAC,MAAD;KAAM,WAAW,oBAAoB,wBAAwB;KAC3D,UAAA,oBAAC,aAAD;MACE,gBAAe;MACf,IAAG;MACH,aAAY;KACb,CAAA;IACG,CAAA,CACF;;EAEO;;AAErB"}
|
package/package.json
CHANGED
package/src/lib/compose-refs.ts
CHANGED
|
@@ -53,7 +53,7 @@ function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
|
|
|
53
53
|
* Accepts callback refs and RefObject(s)
|
|
54
54
|
*/
|
|
55
55
|
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
|
|
56
|
-
//
|
|
56
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
57
57
|
return React.useCallback(composeRefs(...refs), refs);
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Callout, Flex, Text } from "@radix-ui/themes";
|
|
2
2
|
import type { PropsWithChildren } from "react";
|
|
3
|
-
import { useCallback,
|
|
3
|
+
import { useCallback, useRef, useState } from "react";
|
|
4
4
|
import { useElevatedAccessToken } from "../api/api-provider.js";
|
|
5
5
|
import { useClientGraphqlMutation } from "../api/client-graphql/hooks.js";
|
|
6
6
|
import { useMe } from "./api/me.js";
|
|
@@ -24,12 +24,19 @@ export function ElevatedAccess({
|
|
|
24
24
|
const [authenticationChallengeId, setAuthenticationChallengeId] =
|
|
25
25
|
useState<string>();
|
|
26
26
|
|
|
27
|
-
const prevAccessToken =
|
|
28
|
-
|
|
27
|
+
const [{ prevAccessToken, hasTokenExpired }, setPrevAccessState] = useState({
|
|
28
|
+
prevAccessToken: elevatedAccess,
|
|
29
|
+
hasTokenExpired: false,
|
|
30
|
+
});
|
|
31
|
+
if (elevatedAccess !== prevAccessToken) {
|
|
32
|
+
setPrevAccessState({
|
|
33
|
+
prevAccessToken: elevatedAccess,
|
|
34
|
+
hasTokenExpired:
|
|
35
|
+
hasTokenExpired || (!!prevAccessToken && !elevatedAccess),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
prevAccessToken.current = elevatedAccess;
|
|
32
|
-
}, [elevatedAccess]);
|
|
39
|
+
const hasCalledOnVerified = useRef(false);
|
|
33
40
|
|
|
34
41
|
const handleVerificationSuccess = useCallback(
|
|
35
42
|
(elevatedAccessToken: string) => {
|
|
@@ -51,10 +58,6 @@ export function ElevatedAccess({
|
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
if (!authenticationChallengeId) {
|
|
54
|
-
// FIXME: This should be refactored
|
|
55
|
-
// eslint-disable-next-line react-hooks/refs
|
|
56
|
-
const hasTokenExpired = !!prevAccessToken.current;
|
|
57
|
-
|
|
58
61
|
return (
|
|
59
62
|
<SendVerificationEmailForm
|
|
60
63
|
type={type}
|
package/src/lib/otp-input.tsx
CHANGED
|
@@ -10,12 +10,15 @@ interface OptContextType {
|
|
|
10
10
|
readOnly?: boolean;
|
|
11
11
|
state?: "valid" | "invalid";
|
|
12
12
|
onEnterPressed: () => void;
|
|
13
|
-
onChildAdd: (input: HTMLInputElement) => void;
|
|
14
13
|
onCharChange: (char: string, index: number) => void;
|
|
15
|
-
|
|
14
|
+
onChildAdd: (element: HTMLInputElement | null, index: number) => void;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
const OtpContext = React.createContext<OptContextType | undefined>(undefined);
|
|
18
|
+
OtpContext.displayName = "OtpContext";
|
|
19
|
+
|
|
20
|
+
const OtpIndexContext = React.createContext<number>(-1);
|
|
21
|
+
OtpIndexContext.displayName = "OtpIndexContext";
|
|
19
22
|
|
|
20
23
|
type OtpRootProps = React.ComponentPropsWithoutRef<typeof Grid> & {
|
|
21
24
|
id?: string;
|
|
@@ -43,8 +46,6 @@ export const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(
|
|
|
43
46
|
forwardedRef,
|
|
44
47
|
) {
|
|
45
48
|
const [lastCharIndex, setLastCharIndex] = React.useState<number>(0);
|
|
46
|
-
const [allChildrenAdded, setAllChildrenAdded] =
|
|
47
|
-
React.useState<boolean>(false);
|
|
48
49
|
const childCount = React.Children.count(children);
|
|
49
50
|
|
|
50
51
|
// Internal array state is the source of truth so that empty slots between
|
|
@@ -75,7 +76,7 @@ export const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(
|
|
|
75
76
|
);
|
|
76
77
|
|
|
77
78
|
const hiddenInputRef = React.useRef<HTMLInputElement>(null);
|
|
78
|
-
const childrenRefs = React.useRef<HTMLInputElement[]>([]);
|
|
79
|
+
const childrenRefs = React.useRef<(HTMLInputElement | null)[]>([]);
|
|
79
80
|
|
|
80
81
|
const attemptAutoSubmit = React.useCallback(
|
|
81
82
|
(enterPressed = false) => {
|
|
@@ -97,19 +98,10 @@ export const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(
|
|
|
97
98
|
);
|
|
98
99
|
|
|
99
100
|
const handleChildAdd = React.useCallback(
|
|
100
|
-
(
|
|
101
|
-
|
|
102
|
-
input.dataset.index = `${childrenRefs.current.length}`;
|
|
103
|
-
childrenRefs.current.push(input);
|
|
104
|
-
} else {
|
|
105
|
-
childrenRefs.current.pop();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (childrenRefs.current.length === childCount) {
|
|
109
|
-
setAllChildrenAdded(true);
|
|
110
|
-
}
|
|
101
|
+
(element: HTMLInputElement | null, index: number) => {
|
|
102
|
+
childrenRefs.current[index] = element;
|
|
111
103
|
},
|
|
112
|
-
[
|
|
104
|
+
[],
|
|
113
105
|
);
|
|
114
106
|
|
|
115
107
|
const handleCharChange = React.useCallback(
|
|
@@ -124,24 +116,23 @@ export const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(
|
|
|
124
116
|
[setValue],
|
|
125
117
|
);
|
|
126
118
|
|
|
127
|
-
const otpContext = React.useMemo(
|
|
128
|
-
() =>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
119
|
+
const otpContext = React.useMemo<OptContextType>(
|
|
120
|
+
() =>
|
|
121
|
+
({
|
|
122
|
+
value,
|
|
123
|
+
readOnly,
|
|
124
|
+
state,
|
|
125
|
+
onEnterPressed: handleEnterPressed,
|
|
126
|
+
onCharChange: handleCharChange,
|
|
127
|
+
onChildAdd: handleChildAdd,
|
|
128
|
+
}) satisfies OptContextType,
|
|
137
129
|
[
|
|
138
130
|
value,
|
|
139
|
-
allChildrenAdded,
|
|
140
131
|
readOnly,
|
|
141
132
|
state,
|
|
142
133
|
handleEnterPressed,
|
|
143
|
-
handleChildAdd,
|
|
144
134
|
handleCharChange,
|
|
135
|
+
handleChildAdd,
|
|
145
136
|
],
|
|
146
137
|
);
|
|
147
138
|
|
|
@@ -170,7 +161,11 @@ export const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(
|
|
|
170
161
|
childrenRefs.current?.[index]?.focus();
|
|
171
162
|
}}
|
|
172
163
|
>
|
|
173
|
-
{children
|
|
164
|
+
{React.Children.map(children, (child, index) => (
|
|
165
|
+
<OtpIndexContext.Provider value={index}>
|
|
166
|
+
{child}
|
|
167
|
+
</OtpIndexContext.Provider>
|
|
168
|
+
))}
|
|
174
169
|
<input
|
|
175
170
|
ref={useComposedRefs(forwardedRef, hiddenInputRef)}
|
|
176
171
|
defaultValue={value.join("")}
|
|
@@ -194,16 +189,16 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
194
189
|
forwardedRef,
|
|
195
190
|
) {
|
|
196
191
|
const otpContext = useOptContext();
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
192
|
+
const { onChildAdd } = otpContext;
|
|
193
|
+
const index = React.useContext(OtpIndexContext);
|
|
194
|
+
const inputRef = React.useCallback(
|
|
195
|
+
(input: HTMLInputElement | null) => {
|
|
196
|
+
onChildAdd(input, index);
|
|
197
|
+
},
|
|
198
|
+
[onChildAdd, index],
|
|
202
199
|
);
|
|
200
|
+
const composedInputRef = useComposedRefs(forwardedRef, inputRef);
|
|
203
201
|
|
|
204
|
-
// FIXME: This should be refactored
|
|
205
|
-
// eslint-disable-next-line react-hooks/refs
|
|
206
|
-
const index = Number(inputRef.current?.dataset.index ?? -1);
|
|
207
202
|
const char = otpContext.value[index] ?? "";
|
|
208
203
|
|
|
209
204
|
return (
|
|
@@ -230,7 +225,6 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
230
225
|
// Update the value if it matches the input pattern (number only) or is empty (backspace)
|
|
231
226
|
if (event.target.validity.valid || event.target.value === "") {
|
|
232
227
|
const char = event.target.value;
|
|
233
|
-
const index = Number(event.target.dataset.index ?? -1);
|
|
234
228
|
otpContext.onCharChange(char, index);
|
|
235
229
|
focusSibling(event.currentTarget, { back: char === "" });
|
|
236
230
|
}
|
|
@@ -258,7 +252,6 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
258
252
|
|
|
259
253
|
const useOptContext = () => {
|
|
260
254
|
const optContext = React.useContext(OtpContext);
|
|
261
|
-
|
|
262
255
|
if (!optContext) {
|
|
263
256
|
throw new Error(
|
|
264
257
|
"OtpInput compound components cannot be rendered outside the OtpRoot component",
|
package/src/lib/save-button.tsx
CHANGED
|
@@ -25,9 +25,25 @@ export function SaveButton({
|
|
|
25
25
|
onDone,
|
|
26
26
|
...props
|
|
27
27
|
}: SaveButtonProps) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
);
|
|
28
|
+
// Whether the spinner has been visible for at least SAVING_TIMEOUT_MS, so a
|
|
29
|
+
// fast response doesn't flash it for a single frame.
|
|
30
|
+
const [minElapsed, setMinElapsed] = useState(!!done);
|
|
31
|
+
const [prevLoading, setPrevLoading] = useState(loading);
|
|
32
|
+
if (loading !== prevLoading) {
|
|
33
|
+
setPrevLoading(loading);
|
|
34
|
+
if (loading) {
|
|
35
|
+
setMinElapsed(false);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const state = loading
|
|
40
|
+
? "loading"
|
|
41
|
+
: done
|
|
42
|
+
? minElapsed
|
|
43
|
+
? "done"
|
|
44
|
+
: "loading"
|
|
45
|
+
: "idle";
|
|
46
|
+
|
|
31
47
|
const loadingStartTime = useRef<number | null>(null);
|
|
32
48
|
const onDoneRef = useRef(onDone);
|
|
33
49
|
const ButtonComponent = asChild ? Slot : Button;
|
|
@@ -38,37 +54,33 @@ export function SaveButton({
|
|
|
38
54
|
|
|
39
55
|
useEffect(() => {
|
|
40
56
|
if (loading) {
|
|
41
|
-
// FIXME: This should be refactored
|
|
42
|
-
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
43
|
-
setState("loading");
|
|
44
57
|
loadingStartTime.current = Date.now();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!done) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
const loadingDuration = loadingStartTime.current
|
|
65
|
+
? Date.now() - loadingStartTime.current
|
|
66
|
+
: 0;
|
|
67
|
+
const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);
|
|
53
68
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
let doneTimer: number | null = null;
|
|
70
|
+
const minElapsedTimer = window.setTimeout(() => {
|
|
71
|
+
setMinElapsed(true);
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
doneTimer = window.setTimeout(() => {
|
|
74
|
+
onDoneRef.current?.();
|
|
75
|
+
}, DONE_TIMEOUT_MS);
|
|
76
|
+
}, remainingDelay);
|
|
62
77
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} else if (!loading && !done) {
|
|
70
|
-
setState("idle");
|
|
71
|
-
}
|
|
78
|
+
return () => {
|
|
79
|
+
window.clearTimeout(minElapsedTimer);
|
|
80
|
+
if (doneTimer !== null) {
|
|
81
|
+
window.clearTimeout(doneTimer);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
72
84
|
}, [loading, done]);
|
|
73
85
|
|
|
74
86
|
return (
|