From 299b92e128a8da7b05ed8f1430dffefc8f8bd3f7 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Thu, 30 Jan 2025 19:07:34 +0200 Subject: [PATCH 1/4] fix(clerk-js): Refactor how we handle the style on the captcha element --- .changeset/fast-kiwis-visit.md | 5 ++ .../src/ui/components/SignUp/SignUpForm.tsx | 2 +- .../src/ui/components/SignUp/SignUpStart.tsx | 2 +- .../src/ui/elements/CaptchaElement.tsx | 52 +++++++++++++++---- 4 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 .changeset/fast-kiwis-visit.md diff --git a/.changeset/fast-kiwis-visit.md b/.changeset/fast-kiwis-visit.md new file mode 100644 index 00000000000..9ef10a5dab5 --- /dev/null +++ b/.changeset/fast-kiwis-visit.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Fix layout shift on transfer flow when captcha is enabled diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx index 6cb45a6426b..2bd85057e71 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx @@ -113,7 +113,7 @@ export const SignUpForm = (props: SignUpFormProps) => { )} - + )} - {!shouldShowForm && } + {!shouldShowForm && } diff --git a/packages/clerk-js/src/ui/elements/CaptchaElement.tsx b/packages/clerk-js/src/ui/elements/CaptchaElement.tsx index 26edf2c5a8c..edd4735164d 100644 --- a/packages/clerk-js/src/ui/elements/CaptchaElement.tsx +++ b/packages/clerk-js/src/ui/elements/CaptchaElement.tsx @@ -1,13 +1,47 @@ +import { useEffect, useRef } from 'react'; + import { CAPTCHA_ELEMENT_ID } from '../../utils/captcha'; import { Box } from '../customizables'; -type CaptchaElementProps = { - maxHeight?: string; -}; +export const CaptchaElement = () => { + const elementRef = useRef(null); + const maxHeightValueRef = useRef('0'); + const minHeightValueRef = useRef('unset'); + const marginBottomValueRef = useRef('unset'); + + useEffect(() => { + if (!elementRef.current) return; + + const observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + const target = mutation.target as HTMLDivElement; + if (mutation.type === 'attributes' && mutation.attributeName === 'style' && elementRef.current) { + maxHeightValueRef.current = target.style.maxHeight || '0'; + minHeightValueRef.current = target.style.minHeight || 'unset'; + marginBottomValueRef.current = target.style.marginBottom || 'unset'; + } + }); + }); -export const CaptchaElement = ({ maxHeight }: CaptchaElementProps) => ( - -); + observer.observe(elementRef.current, { + attributes: true, + attributeFilter: ['style'], + }); + + return () => observer.disconnect(); + }, []); + + return ( + + ); +}; From edb4e748f1d6efab2e1660695ffbbbe2589e681c Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Fri, 31 Jan 2025 10:27:19 +0200 Subject: [PATCH 2/4] before rendering the challenge initialize max-height to 0 for all --- packages/clerk-js/src/utils/captcha/turnstile.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/clerk-js/src/utils/captcha/turnstile.ts b/packages/clerk-js/src/utils/captcha/turnstile.ts index b160a8e9bf9..10fded020db 100644 --- a/packages/clerk-js/src/utils/captcha/turnstile.ts +++ b/packages/clerk-js/src/utils/captcha/turnstile.ts @@ -141,6 +141,7 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => { if (visibleDiv) { captchaWidgetType = 'smart'; widgetContainerQuerySelector = `#${CAPTCHA_ELEMENT_ID}`; + visibleDiv.style.maxHeight = '0'; } else { console.error( 'Cannot initialize Smart CAPTCHA widget because the `clerk-captcha` DOM element was not found; falling back to Invisible CAPTCHA widget. If you are using a custom flow, visit https://clerk.com/docs/custom-flows/bot-sign-up-protection for instructions', @@ -155,6 +156,7 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => { widgetContainerQuerySelector = `.${CAPTCHA_INVISIBLE_CLASSNAME}`; const div = document.createElement('div'); div.classList.add(CAPTCHA_INVISIBLE_CLASSNAME); + div.style.maxHeight = '0'; document.body.appendChild(div); } From e79e8ac969261f2cd4f090e655e8cf41808ae204 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Fri, 31 Jan 2025 12:48:27 +0200 Subject: [PATCH 3/4] add comments --- packages/clerk-js/src/ui/elements/CaptchaElement.tsx | 6 ++++++ packages/clerk-js/src/utils/captcha/turnstile.ts | 10 +++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/src/ui/elements/CaptchaElement.tsx b/packages/clerk-js/src/ui/elements/CaptchaElement.tsx index edd4735164d..7a5a5408f20 100644 --- a/packages/clerk-js/src/ui/elements/CaptchaElement.tsx +++ b/packages/clerk-js/src/ui/elements/CaptchaElement.tsx @@ -3,6 +3,12 @@ import { useEffect, useRef } from 'react'; import { CAPTCHA_ELEMENT_ID } from '../../utils/captcha'; import { Box } from '../customizables'; +/** + * This component uses a MutationObserver to listen for DOM changes made by our Turnstile logic, + * which operates outside the React lifecycle. It stores the observed state in ref to ensure that + * any external style changes, such as updates to max-height, min-height, or margin-bottom persist across re-renders, + * preventing unwanted layout resets. + */ export const CaptchaElement = () => { const elementRef = useRef(null); const maxHeightValueRef = useRef('0'); diff --git a/packages/clerk-js/src/utils/captcha/turnstile.ts b/packages/clerk-js/src/utils/captcha/turnstile.ts index 10fded020db..1d2724cb07d 100644 --- a/packages/clerk-js/src/utils/captcha/turnstile.ts +++ b/packages/clerk-js/src/utils/captcha/turnstile.ts @@ -141,7 +141,7 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => { if (visibleDiv) { captchaWidgetType = 'smart'; widgetContainerQuerySelector = `#${CAPTCHA_ELEMENT_ID}`; - visibleDiv.style.maxHeight = '0'; + visibleDiv.style.maxHeight = '0'; // This is to prevent the layout shift when the render method is called } else { console.error( 'Cannot initialize Smart CAPTCHA widget because the `clerk-captcha` DOM element was not found; falling back to Invisible CAPTCHA widget. If you are using a custom flow, visit https://clerk.com/docs/custom-flows/bot-sign-up-protection for instructions', @@ -156,7 +156,7 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => { widgetContainerQuerySelector = `.${CAPTCHA_INVISIBLE_CLASSNAME}`; const div = document.createElement('div'); div.classList.add(CAPTCHA_INVISIBLE_CLASSNAME); - div.style.maxHeight = '0'; + div.style.maxHeight = '0'; // This is to prevent the layout shift when the render method is called document.body.appendChild(div); } @@ -180,8 +180,12 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => { } else { const visibleWidget = document.getElementById(CAPTCHA_ELEMENT_ID); if (visibleWidget) { + // We unset the max-height to allow the widget to expand visibleWidget.style.maxHeight = 'unset'; - visibleWidget.style.minHeight = '68px'; // this is the height of the Turnstile widget + // We set the min-height to the height of the Turnstile widget + // because the widget initially does a small layout shift + // and then expands to the correct height + visibleWidget.style.minHeight = '68px'; visibleWidget.style.marginBottom = '1.5rem'; } } From 703b576e3db6d15207c6985d0cf241035b3717d1 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Fri, 31 Jan 2025 16:33:58 +0200 Subject: [PATCH 4/4] Update .changeset/fast-kiwis-visit.md --- .changeset/fast-kiwis-visit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fast-kiwis-visit.md b/.changeset/fast-kiwis-visit.md index 9ef10a5dab5..a09c3e74212 100644 --- a/.changeset/fast-kiwis-visit.md +++ b/.changeset/fast-kiwis-visit.md @@ -2,4 +2,4 @@ '@clerk/clerk-js': patch --- -Fix layout shift on transfer flow when captcha is enabled +Fix captcha layout shift on transfer flow, custom flows and invisible