-
Notifications
You must be signed in to change notification settings - Fork 405
fix(clerk-js,clerk-react): Avoid CLS when fallback is passed to PricingTable
#6644
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5e53e7f
ab7d503
051c3ca
e0fd38f
31115ef
399137d
08c8922
0650b55
4888ca6
943da66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| '@clerk/clerk-js': patch | ||
| '@clerk/clerk-react': patch | ||
| --- | ||
|
|
||
| Wait for pricing table data to be ready before hiding its fallback. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,62 +1,77 @@ | ||
| import { useEffect, useRef, useState } from 'react'; | ||
|
|
||
| /** | ||
| * Used to detect when a Clerk component has been added to the DOM. | ||
| */ | ||
| function waitForElementChildren(options: { selector?: string; root?: HTMLElement | null; timeout?: number }) { | ||
| const { root = document?.body, selector, timeout = 0 } = options; | ||
| const createAwaitableMutationObserver = ( | ||
| globalOptions: MutationObserverInit & { | ||
| isReady: (el: HTMLElement | null, selector: string) => boolean; | ||
| }, | ||
| ) => { | ||
| const isReady = globalOptions?.isReady; | ||
|
|
||
| return new Promise<void>((resolve, reject) => { | ||
| if (!root) { | ||
| reject(new Error('No root element provided')); | ||
| return; | ||
| } | ||
| return (options: { selector: string; root?: HTMLElement | null; timeout?: number }) => | ||
| new Promise<void>((resolve, reject) => { | ||
| const { root = document?.body, selector, timeout = 0 } = options; | ||
|
|
||
| let elementToWatch: HTMLElement | null = root; | ||
| if (selector) { | ||
| elementToWatch = root?.querySelector(selector); | ||
| } | ||
| if (!root) { | ||
| reject(new Error('No root element provided')); | ||
| return; | ||
| } | ||
|
|
||
| // Check if the element already has child nodes | ||
| const isElementAlreadyPresent = elementToWatch?.childElementCount && elementToWatch.childElementCount > 0; | ||
| if (isElementAlreadyPresent) { | ||
| resolve(); | ||
| return; | ||
| } | ||
| let elementToWatch: HTMLElement | null = root; | ||
| if (selector) { | ||
| elementToWatch = root?.querySelector(selector); | ||
| } | ||
|
|
||
| // Set up a MutationObserver to detect when the element has children | ||
| const observer = new MutationObserver(mutationsList => { | ||
| for (const mutation of mutationsList) { | ||
| if (mutation.type === 'childList') { | ||
| // Initial readiness check | ||
| if (isReady(elementToWatch, selector)) { | ||
| resolve(); | ||
| return; | ||
| } | ||
|
|
||
| // Set up a MutationObserver to detect when the element has children | ||
| const observer = new MutationObserver(mutationsList => { | ||
| for (const mutation of mutationsList) { | ||
| if (!elementToWatch && selector) { | ||
| elementToWatch = root?.querySelector(selector); | ||
| } | ||
|
|
||
| if (elementToWatch?.childElementCount && elementToWatch.childElementCount > 0) { | ||
| observer.disconnect(); | ||
| resolve(); | ||
| return; | ||
| if ( | ||
| (globalOptions.childList && mutation.type === 'childList') || | ||
| (globalOptions.attributes && mutation.type === 'attributes') | ||
| ) { | ||
| if (isReady(elementToWatch, selector)) { | ||
| observer.disconnect(); | ||
| resolve(); | ||
| return; | ||
| } | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| observer.observe(root, globalOptions); | ||
|
|
||
| // Set up an optional timeout to reject the promise if the element never gets child nodes | ||
| if (timeout > 0) { | ||
| setTimeout(() => { | ||
| observer.disconnect(); | ||
| reject(new Error(`Timeout waiting for ${selector}`)); | ||
| }, timeout); | ||
| } | ||
| }); | ||
| }; | ||
|
|
||
| observer.observe(root, { childList: true, subtree: true }); | ||
|
|
||
| // Set up an optional timeout to reject the promise if the element never gets child nodes | ||
| if (timeout > 0) { | ||
| setTimeout(() => { | ||
| observer.disconnect(); | ||
| reject(new Error(`Timeout waiting for element children`)); | ||
| }, timeout); | ||
| } | ||
| }); | ||
| } | ||
| const waitForElementChildren = createAwaitableMutationObserver({ | ||
| childList: true, | ||
| subtree: true, | ||
| isReady: (el, selector) => !!el?.childElementCount && el?.matches?.(selector) && el.childElementCount > 0, | ||
| }); | ||
|
|
||
| /** | ||
| * Detect when a Clerk component has mounted by watching DOM updates to an element with a `data-clerk-component="${component}"` property. | ||
| */ | ||
| export function useWaitForComponentMount(component?: string) { | ||
| export function useWaitForComponentMount( | ||
| component?: string, | ||
| options?: { selector: string }, | ||
| ): 'rendering' | 'rendered' | 'error' { | ||
| const watcherRef = useRef<Promise<void>>(); | ||
| const [status, setStatus] = useState<'rendering' | 'rendered' | 'error'>('rendering'); | ||
|
|
||
|
|
@@ -66,15 +81,22 @@ export function useWaitForComponentMount(component?: string) { | |
| } | ||
|
|
||
| if (typeof window !== 'undefined' && !watcherRef.current) { | ||
| watcherRef.current = waitForElementChildren({ selector: `[data-clerk-component="${component}"]` }) | ||
| const defaultSelector = `[data-clerk-component="${component}"]`; | ||
| const selector = options?.selector; | ||
| watcherRef.current = waitForElementChildren({ | ||
| selector: selector | ||
| ? // Allows for `[data-clerk-component="xxxx"][data-some-attribute="123"] .my-class` | ||
| defaultSelector + selector | ||
| : defaultSelector, | ||
|
Comment on lines
+87
to
+90
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need to join with a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The concatenation happens between |
||
| }) | ||
| .then(() => { | ||
| setStatus('rendered'); | ||
| }) | ||
| .catch(() => { | ||
| setStatus('error'); | ||
| }); | ||
| } | ||
| }, [component]); | ||
| }, [component, options?.selector]); | ||
|
|
||
| return status; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.