mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-29 08:37:05 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2153bead6d | |||
| 38f60f2633 | |||
| be658e1b32 | |||
| e745c88166 | |||
| f61578f01f |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Bump Next.js to 16.4.3, keeping the dev-mode OOM patch.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Load the inline expression runtime only on pages whose code blocks or text use expressions, so other pages ship less JavaScript.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Split llms-full.txt into parts by size so large sites no longer fail with "content too large"
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Follow site and space redirects when the MCP getPage tool resolves a page URL.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Let analytics tools like GA4 track outbound link clicks by using native link navigation.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Resolve the published content with the full URL in a single lookup and always send the visitor country.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Keep lists, paragraphs and code blocks as HTML in Updates RSS feeds, instead of running all the text together
|
||||
@@ -110,7 +110,7 @@
|
||||
},
|
||||
"packages/gitbook": {
|
||||
"name": "gitbook",
|
||||
"version": "0.28.0",
|
||||
"version": "0.28.1",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "catalog:",
|
||||
"@cloudflare/workers-types": "^4.20251011.0",
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# gitbook
|
||||
|
||||
## 0.28.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7e096d5: Bump Next.js to 16.4.3, keeping the dev-mode OOM patch.
|
||||
- f61578f: Preconnect each asset origin once, skip same-origin icon URLs, and open the fonts preconnect in CORS mode so font requests can reuse it.
|
||||
- bd431f1: Split llms-full.txt into parts by size so large sites no longer fail with "content too large"
|
||||
- a5d58bc: Follow site and space redirects when the MCP getPage tool resolves a page URL.
|
||||
- e745c88: Add a search input to filter models by name on OpenAPI models pages.
|
||||
- 96c5683: Let analytics tools like GA4 track outbound link clicks by using native link navigation.
|
||||
- c00a7b3: Resolve the published content with the full URL in a single lookup and always send the visitor country.
|
||||
- b7b8ae6: Keep lists, paragraphs and code blocks as HTML in Updates RSS feeds, instead of running all the text together
|
||||
- 38f60f2: Render soft line breaks in paragraphs that contain buttons.
|
||||
- be658e1: Truncate long tab titles one by one and show the selected tab's full title, instead of hiding tabs behind a "…" menu
|
||||
|
||||
## 0.28.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.28.0",
|
||||
"version": "0.28.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@base-ui/react": "catalog:",
|
||||
|
||||
@@ -6,10 +6,11 @@ import { useDebounceCallback } from 'usehooks-ts';
|
||||
import type { CustomizationThemedCodeTheme, DocumentBlockCode } from '@gitbook/api';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
import type { InlineExpressionVariables } from '../InlineExpression/types';
|
||||
import { type InlineExpressionVariables, useEvaluateInlineExpression } from '../InlineExpression';
|
||||
import { CodeBlockRenderer } from './CodeBlockRenderer';
|
||||
import type { HighlightTheme, RenderedInline } from './highlight-tokens';
|
||||
import { plainHighlight } from './plain-highlight';
|
||||
import { useAdaptiveVisitor } from '@/components/Adaptive';
|
||||
import { useInViewportListener } from '@/components/hooks/useInViewportListener';
|
||||
import { useScrollListener } from '@/components/hooks/useScrollListener';
|
||||
import { Button, ToggleChevron } from '@/components/primitives';
|
||||
@@ -19,8 +20,6 @@ import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
export type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
|
||||
inlines: RenderedInline[];
|
||||
inlineExprVariables: InlineExpressionVariables;
|
||||
/** Only set for blocks with inline expressions, keeping `@gitbook/expr` out of the bundle. */
|
||||
evaluateInlineExpression?: (expression: string) => string;
|
||||
mode: BlockProps<DocumentBlockCode>['context']['mode'];
|
||||
themes?: CustomizationThemedCodeTheme;
|
||||
embedded?: boolean;
|
||||
@@ -35,12 +34,18 @@ export const CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT = 10;
|
||||
* It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting.
|
||||
*/
|
||||
export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
const { block, mode, style, inlines, evaluateInlineExpression, themes, embedded, commentKey } =
|
||||
const { block, mode, style, inlines, inlineExprVariables, themes, embedded, commentKey } =
|
||||
props;
|
||||
const blockRef = useRef<HTMLDivElement>(null);
|
||||
const isInViewportRef = useRef(false);
|
||||
const [isInViewport, setIsInViewport] = useState(false);
|
||||
|
||||
const getAdaptiveVisitorClaims = useAdaptiveVisitor();
|
||||
const visitorClaims = getAdaptiveVisitorClaims();
|
||||
const evaluateInlineExpression = useEvaluateInlineExpression({
|
||||
visitorClaims,
|
||||
variables: inlineExprVariables,
|
||||
});
|
||||
const plainTheme = useMemo(
|
||||
() => plainHighlight(block, inlines, { evaluateInlineExpression, themes }),
|
||||
[block, inlines, evaluateInlineExpression, themes]
|
||||
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ClientCodeBlock, type ClientBlockProps } from './ClientCodeBlock';
|
||||
import { useAdaptiveVisitor } from '@/components/Adaptive';
|
||||
import { useEvaluateInlineExpression } from '@/components/DocumentView/InlineExpression/useEvaluateInlineExpression';
|
||||
|
||||
/**
|
||||
* Code block with inline expressions; its own module because it pulls in `@gitbook/expr`.
|
||||
*/
|
||||
export function ClientCodeBlockWithExpressions(props: ClientBlockProps) {
|
||||
const getAdaptiveVisitorClaims = useAdaptiveVisitor();
|
||||
const evaluateInlineExpression = useEvaluateInlineExpression({
|
||||
visitorClaims: getAdaptiveVisitorClaims(),
|
||||
variables: props.inlineExprVariables,
|
||||
});
|
||||
|
||||
return <ClientCodeBlock {...props} evaluateInlineExpression={evaluateInlineExpression} />;
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import type { ClientBlockProps } from './ClientCodeBlock';
|
||||
|
||||
// Keeps `@gitbook/expr` out of the eager bundle; `ssr: true` keeps values in the HTML.
|
||||
const ClientCodeBlockWithExpressions = dynamic(
|
||||
() =>
|
||||
import('./ClientCodeBlockWithExpressions').then(
|
||||
(mod) => mod.ClientCodeBlockWithExpressions
|
||||
),
|
||||
{ ssr: true }
|
||||
);
|
||||
|
||||
export function ClientCodeBlockWithExpressionsLazy(props: ClientBlockProps) {
|
||||
return <ClientCodeBlockWithExpressions {...props} />;
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import type { BlockProps } from '../Block';
|
||||
import { Blocks } from '../Blocks';
|
||||
import { getCommentKeyProp, getCommentMarkerProps } from '../utils';
|
||||
import { ClientCodeBlock } from './ClientCodeBlock';
|
||||
import { ClientCodeBlockWithExpressionsLazy } from './ClientCodeBlockWithExpressionsLazy';
|
||||
import { CodeBlockRenderer } from './CodeBlockRenderer';
|
||||
import { highlight } from './highlight';
|
||||
import { type RenderedInline, getInlines } from './highlight-tokens';
|
||||
@@ -131,11 +130,8 @@ export async function CodeBlock(
|
||||
{isMermaid ? (
|
||||
<MermaidCodeBlockLazy
|
||||
{...clientProps}
|
||||
hasInlineExpression={hasInlineExpression}
|
||||
mermaidRuntimeURL={getAssetURL(MERMAID_RUNTIME_PATH)}
|
||||
/>
|
||||
) : hasInlineExpression ? (
|
||||
<ClientCodeBlockWithExpressionsLazy {...clientProps} />
|
||||
) : (
|
||||
<ClientCodeBlock {...clientProps} />
|
||||
)}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useSta
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { type ClientBlockProps, ClientCodeBlock } from './ClientCodeBlock';
|
||||
import { ClientCodeBlockWithExpressions } from './ClientCodeBlockWithExpressions';
|
||||
import { getPlainCodeBlock } from './highlight-tokens';
|
||||
import { MermaidPanZoomControls } from './MermaidPanZoomControls';
|
||||
import { useHasBeenInViewport } from '@/components/hooks/useHasBeenInViewport';
|
||||
@@ -21,7 +20,6 @@ import { tcls } from '@/lib/tailwind';
|
||||
export function MermaidCodeBlock(
|
||||
props: ClientBlockProps & {
|
||||
mermaidRuntimeURL: string;
|
||||
hasInlineExpression: boolean;
|
||||
}
|
||||
) {
|
||||
const { block, mode, style, mermaidRuntimeURL } = props;
|
||||
@@ -171,11 +169,7 @@ export function MermaidCodeBlock(
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return props.hasInlineExpression ? (
|
||||
<ClientCodeBlockWithExpressions {...props} />
|
||||
) : (
|
||||
<ClientCodeBlock {...props} />
|
||||
);
|
||||
return <ClientCodeBlock {...props} />;
|
||||
}
|
||||
|
||||
// The live diagram subtree. It is portaled into a stable host that moves between the
|
||||
|
||||
@@ -11,8 +11,6 @@ const MermaidCodeBlock = dynamic(
|
||||
{ ssr: true }
|
||||
);
|
||||
|
||||
export function MermaidCodeBlockLazy(
|
||||
props: ClientBlockProps & { mermaidRuntimeURL: string; hasInlineExpression: boolean }
|
||||
) {
|
||||
export function MermaidCodeBlockLazy(props: ClientBlockProps & { mermaidRuntimeURL: string }) {
|
||||
return <MermaidCodeBlock {...props} />;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import type { DocumentInlineExpression } from '@gitbook/api';
|
||||
|
||||
import type { InlineProps } from '../Inline';
|
||||
import { InlineExpressionValueLazy } from './InlineExpressionValueLazy';
|
||||
import { InlineExpressionValue } from './InlineExpressionValue';
|
||||
|
||||
/**
|
||||
* Render an inline expression.
|
||||
@@ -25,7 +25,7 @@ export function InlineExpression(props: InlineProps<DocumentInlineExpression>) {
|
||||
|
||||
return (
|
||||
<React.Suspense fallback={null}>
|
||||
<InlineExpressionValueLazy expression={data.expression} variables={variables} />
|
||||
<InlineExpressionValue expression={data.expression} variables={variables} />
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import type { InlineExpressionVariables } from './types';
|
||||
|
||||
// Keeps `@gitbook/expr` out of the eager bundle; `ssr: true` keeps values in the HTML.
|
||||
const InlineExpressionValue = dynamic(
|
||||
() => import('./InlineExpressionValue').then((mod) => mod.InlineExpressionValue),
|
||||
{ ssr: true }
|
||||
);
|
||||
|
||||
export function InlineExpressionValueLazy(props: {
|
||||
expression: string;
|
||||
variables: InlineExpressionVariables;
|
||||
}) {
|
||||
return <InlineExpressionValue {...props} />;
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './InlineExpression';
|
||||
export * from './types';
|
||||
export * from './useEvaluateInlineExpression';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import type React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { CustomizationThemedCodeTheme } from '@gitbook/api';
|
||||
import {
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
|
||||
import { ClientPlainCodeBlock } from '../CodeBlock/ClientPlainCodeBlock';
|
||||
import type { DocumentContext } from '../DocumentView';
|
||||
import { matchesText } from '../Table/searchMatch';
|
||||
import { MIN_RECORDS_FOR_SEARCH } from '../Table/shouldShowSearch';
|
||||
import { Input } from '@/components/primitives';
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
|
||||
// Only what survives the RSC boundary. Rebuilding the renderers on the client is what lets this
|
||||
// whole subtree sit behind a `next/dynamic` chunk instead of the route's eager entry.
|
||||
@@ -74,14 +78,51 @@ export function OpenAPIBlockClient(props: OpenAPIBlockClientProps) {
|
||||
);
|
||||
case 'webhook':
|
||||
return <BaseOpenAPIWebhook data={props.data} context={context} className={className} />;
|
||||
case 'schemas':
|
||||
case 'schemas': {
|
||||
const SchemasComponent =
|
||||
mode !== 'print' && props.data.schemas.length >= MIN_RECORDS_FOR_SEARCH
|
||||
? OpenAPISchemasWithSearch
|
||||
: BaseOpenAPISchemas;
|
||||
return (
|
||||
<BaseOpenAPISchemas
|
||||
<SchemasComponent
|
||||
data={props.data}
|
||||
grouped={props.grouped}
|
||||
context={context}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function OpenAPISchemasWithSearch(props: React.ComponentProps<typeof BaseOpenAPISchemas>) {
|
||||
const { data, ...rest } = props;
|
||||
const language = useLanguage();
|
||||
const [query, setQuery] = React.useState('');
|
||||
// A models page can hold hundreds of schemas; keep typing responsive while the list re-renders.
|
||||
const deferredQuery = React.useDeferredValue(query).trim();
|
||||
|
||||
const schemas = data.schemas.filter(({ name }) => matchesText(name, deferredQuery));
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-4">
|
||||
<Input
|
||||
label={tString(language, 'search')}
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
leading="magnifying-glass"
|
||||
clearButton
|
||||
sizing="small"
|
||||
keyboardShortcut={false}
|
||||
/>
|
||||
{schemas.length > 0 ? (
|
||||
// `grouped` keeps the list layout when the search narrows it down to one model.
|
||||
<BaseOpenAPISchemas {...rest} data={{ ...data, schemas }} grouped />
|
||||
) : (
|
||||
<div className="mb-14 py-8 text-center text-sm text-tint">
|
||||
{tString(language, 'search_no_results_for', deferredQuery)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,7 +141,8 @@ const TabItemList = memo(function TabItemList(props: {
|
||||
onSelect: (tabId: string) => void;
|
||||
}) {
|
||||
const { tabs, activeTabId, onSelect } = props;
|
||||
const { containerRef, itemRef, overflowing, isMeasuring } = useListOverflow();
|
||||
// The active tab's width decides how many others fit, so a new selection re-measures.
|
||||
const { containerRef, itemRef, overflowing, isMeasuring } = useListOverflow(activeTabId);
|
||||
const overflowingTabs = useMemo(
|
||||
() =>
|
||||
Array.from(overflowing, (id) => {
|
||||
@@ -181,6 +182,7 @@ const TabItemList = memo(function TabItemList(props: {
|
||||
key={tab.id}
|
||||
ref={itemRef}
|
||||
isActive={tab.id === activeTabId}
|
||||
isMeasuring={isMeasuring}
|
||||
tab={tab}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
@@ -239,10 +241,11 @@ function TabsDropdownMenu(props: {
|
||||
const TabItem = memo(function TabItem(props: {
|
||||
ref: React.Ref<HTMLButtonElement>;
|
||||
isActive: boolean;
|
||||
isMeasuring: boolean;
|
||||
tab: TabsItem;
|
||||
onSelect: (tabId: string) => void;
|
||||
}) {
|
||||
const { ref, tab, isActive, onSelect } = props;
|
||||
const { ref, tab, isActive, isMeasuring, onSelect } = props;
|
||||
return (
|
||||
<TabButton
|
||||
ref={ref}
|
||||
@@ -250,7 +253,20 @@ const TabItem = memo(function TabItem(props: {
|
||||
aria-selected={isActive}
|
||||
aria-controls={tab.id}
|
||||
id={getTabButtonId(tab.id)}
|
||||
title={tab.title}
|
||||
onClick={() => onSelect(tab.id)}
|
||||
wrapperClassName={
|
||||
isActive
|
||||
? // Keeps room for the overflow menu button.
|
||||
'max-w-[calc(100%-3rem)]'
|
||||
: isMeasuring
|
||||
? // Measured after the active tab, at their narrowest, so the menu only takes
|
||||
// the tabs that can't fit even truncated.
|
||||
'order-1 max-w-28'
|
||||
: // Equal shares capped at their own width, so no tab drops below that
|
||||
// narrowest size; the high grow leaves the strip's filler only the leftover.
|
||||
'min-w-0 max-w-max basis-0 grow-[1000]'
|
||||
}
|
||||
>
|
||||
{tab.icon && <Icon icon={tab.icon} className="size-4 shrink-0" />}
|
||||
<span className="min-w-0 truncate">{tab.title}</span>
|
||||
@@ -264,9 +280,10 @@ const TabItem = memo(function TabItem(props: {
|
||||
function TabButton(
|
||||
props: Omit<ComponentPropsWithRef<'button'>, 'type'> & {
|
||||
isActive?: boolean;
|
||||
wrapperClassName?: string;
|
||||
}
|
||||
) {
|
||||
const { isActive, ...rest } = props;
|
||||
const { isActive, wrapperClassName, ...rest } = props;
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
@@ -310,14 +327,15 @@ function TabButton(
|
||||
'[:has(+_&.active-tab)]:after:border-transparent',
|
||||
'[:has(&_+)]:after:border-transparent',
|
||||
]
|
||||
: null
|
||||
: null,
|
||||
wrapperClassName
|
||||
)}
|
||||
>
|
||||
<button
|
||||
{...rest}
|
||||
type="button"
|
||||
className={tcls(
|
||||
'relative inline-flex max-w-full items-center gap-1.5 px-4 py-2.5 font-medium text-sm transition-[color]',
|
||||
'relative inline-flex min-w-0 max-w-full items-center gap-1.5 px-4 py-2.5 font-medium text-sm transition-[color]',
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -38,12 +38,21 @@ export function Text(props: { text: DocumentText }) {
|
||||
{children}
|
||||
</Mark>
|
||||
);
|
||||
}, leaf.text)}
|
||||
}, withSoftBreaks(leaf.text))}
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Paragraphs with buttons are flex containers, where a `\n` alone can't move the next items to a new line.
|
||||
function withSoftBreaks(text: string): React.ReactNode {
|
||||
const lines = text.split('\n');
|
||||
// Keep `\n` in the same text node as a preceding `\r`: split apart, the HTML parser reads the lone `\r` as an extra line.
|
||||
return lines.flatMap((line, index) =>
|
||||
index < lines.length - 1 ? [`${line}\n`, <span key={index} className="basis-full" />] : line
|
||||
);
|
||||
}
|
||||
|
||||
const MARK_STYLES = {
|
||||
bold: Bold,
|
||||
italic: Italic,
|
||||
|
||||
@@ -49,7 +49,7 @@ import { defaultCustomization } from '@/lib/utils';
|
||||
|
||||
function preloadFont(fontData: FontData) {
|
||||
if (fontData.type === 'custom') {
|
||||
ReactDOM.preconnect(GITBOOK_FONTS_URL);
|
||||
ReactDOM.preconnect(GITBOOK_FONTS_URL, { crossOrigin: 'anonymous' });
|
||||
fontData.preloadSources
|
||||
.flatMap((face) => face.sources)
|
||||
.forEach(({ url, format }) => {
|
||||
|
||||
@@ -64,11 +64,12 @@ export async function SiteLayout(props: {
|
||||
// Scripts are disabled when tracking is disabled
|
||||
const scripts = withTracking ? context.scripts : [];
|
||||
|
||||
ReactDOM.preconnect(GITBOOK_API_PUBLIC_URL);
|
||||
ReactDOM.preconnect(GITBOOK_ICONS_URL);
|
||||
if (GITBOOK_ASSETS_URL) {
|
||||
ReactDOM.preconnect(GITBOOK_ASSETS_URL);
|
||||
}
|
||||
// React dedupes preconnects by href, so reduce to the origin; relative URLs are same-origin.
|
||||
[GITBOOK_API_PUBLIC_URL, GITBOOK_ICONS_URL, GITBOOK_ASSETS_URL].forEach((url) => {
|
||||
if (url && URL.canParse(url)) {
|
||||
ReactDOM.preconnect(new URL(url).origin);
|
||||
}
|
||||
});
|
||||
|
||||
// Start the search-index download from the HTML itself. `crossOrigin` must match the
|
||||
// client `fetch()` (cors + same-origin credentials) or the preload is ignored and the
|
||||
|
||||
@@ -30,8 +30,10 @@ interface OverflowState {
|
||||
* shows *because* items overflow (a "more" menu, say) has to be rendered ahead of the items in that
|
||||
* phase: the measurement discounts the space taken before the first item, so a list that fits on its
|
||||
* own is not reported as overflowing merely because that menu was reserving room for itself.
|
||||
*
|
||||
* The list is measured again whenever `remeasureKey` changes.
|
||||
*/
|
||||
export function useListOverflow(): OverflowState {
|
||||
export function useListOverflow(remeasureKey?: unknown): OverflowState {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [overflowing, setOverflowing] = useState<Set<string>>(new Set());
|
||||
// Measuring is a request/completed pair of counters rather than a boolean, because a boolean
|
||||
@@ -58,6 +60,12 @@ export function useListOverflow(): OverflowState {
|
||||
|
||||
const requestMeasure = useCallback(() => setMeasureRequest((request) => request + 1), []);
|
||||
|
||||
const [measuredKey, setMeasuredKey] = useState(remeasureKey);
|
||||
if (measuredKey !== remeasureKey) {
|
||||
setMeasuredKey(remeasureKey);
|
||||
setMeasureRequest((request) => request + 1);
|
||||
}
|
||||
|
||||
// Measure on mount and when container size changes
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) {
|
||||
|
||||
Reference in New Issue
Block a user