Skip to content

Commit

Permalink
fix(clerk-js): Fix captcha layout shift on invisible and custom flows (
Browse files Browse the repository at this point in the history
  • Loading branch information
anagstef authored Jan 31, 2025
1 parent fe89e62 commit 0f95982
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/fast-kiwis-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fix captcha layout shift on transfer flow, custom flows and invisible
2 changes: 1 addition & 1 deletion packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export const SignUpForm = (props: SignUpFormProps) => {
</Col>
)}
<Col center>
<CaptchaElement maxHeight='0' />
<CaptchaElement />
<Col
gap={6}
sx={{
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ function _SignUpStart(): JSX.Element {
/>
</Form.ControlRow>
)}
{!shouldShowForm && <CaptchaElement maxHeight='0' />}
{!shouldShowForm && <CaptchaElement />}
</Flex>
</Card.Content>

Expand Down
58 changes: 49 additions & 9 deletions packages/clerk-js/src/ui/elements/CaptchaElement.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,53 @@
import { useEffect, useRef } from 'react';

import { CAPTCHA_ELEMENT_ID } from '../../utils/captcha';
import { Box } from '../customizables';

type CaptchaElementProps = {
maxHeight?: string;
};
/**
* 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');
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) => (
<Box
id={CAPTCHA_ELEMENT_ID}
sx={{ display: 'block', alignSelf: 'center', maxHeight }}
/>
);
observer.observe(elementRef.current, {
attributes: true,
attributeFilter: ['style'],
});

return () => observer.disconnect();
}, []);

return (
<Box
ref={elementRef}
id={CAPTCHA_ELEMENT_ID}
style={{
display: 'block',
alignSelf: 'center',
maxHeight: maxHeightValueRef.current,
minHeight: minHeightValueRef.current,
marginBottom: marginBottomValueRef.current,
}}
/>
);
};
8 changes: 7 additions & 1 deletion packages/clerk-js/src/utils/captcha/turnstile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => {
if (visibleDiv) {
captchaWidgetType = 'smart';
widgetContainerQuerySelector = `#${CAPTCHA_ELEMENT_ID}`;
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',
Expand All @@ -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'; // This is to prevent the layout shift when the render method is called
document.body.appendChild(div);
}

Expand All @@ -178,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';
}
}
Expand Down

0 comments on commit 0f95982

Please sign in to comment.