react-fit-list 1.0.0

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/dist/index.js ADDED
@@ -0,0 +1,413 @@
1
+ import * as React from 'react';
2
+ import { useRef, useState, useCallback, useEffect, useMemo, useLayoutEffect } from 'react';
3
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
4
+
5
+ // src/components/FitList.tsx
6
+ function useControllableState({
7
+ value,
8
+ defaultValue,
9
+ onChange
10
+ }) {
11
+ const [internal, setInternal] = useState(defaultValue);
12
+ const isControlled = value !== void 0;
13
+ const current = isControlled ? value : internal;
14
+ const setValue = useCallback(
15
+ (next) => {
16
+ if (!isControlled) {
17
+ setInternal(next);
18
+ }
19
+ onChange?.(next);
20
+ },
21
+ [isControlled, onChange]
22
+ );
23
+ return [current, setValue];
24
+ }
25
+
26
+ // src/hooks/useFitList.tsx
27
+ var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
28
+ function getEstimatedWidth(item, index, estimatedItemWidth, fallback) {
29
+ if (typeof estimatedItemWidth === "function")
30
+ return estimatedItemWidth(item, index);
31
+ if (typeof estimatedItemWidth === "number") return estimatedItemWidth;
32
+ return fallback;
33
+ }
34
+ function useFitList({
35
+ items,
36
+ getKey,
37
+ reserveOverflowSpace = false,
38
+ overflowWidth,
39
+ gap = 8,
40
+ collapseFrom = "end",
41
+ estimatedItemWidth,
42
+ measurementMode = "live",
43
+ expanded,
44
+ defaultExpanded = false,
45
+ onExpandedChange,
46
+ measureOverflowWidth
47
+ }) {
48
+ const containerRef = useRef(null);
49
+ const overflowRef = useRef(null);
50
+ const itemNodeMap = useRef(/* @__PURE__ */ new Map());
51
+ const measureNodeMap = useRef(/* @__PURE__ */ new Map());
52
+ const [visibleCount, setVisibleCount] = useState(items.length);
53
+ const [isExpanded, setExpanded] = useControllableState({
54
+ value: expanded,
55
+ defaultValue: defaultExpanded,
56
+ onChange: onExpandedChange
57
+ });
58
+ const compute = useCallback(() => {
59
+ if (isExpanded) {
60
+ setVisibleCount(items.length);
61
+ return;
62
+ }
63
+ const container = containerRef.current;
64
+ if (!container) {
65
+ setVisibleCount(items.length);
66
+ return;
67
+ }
68
+ const containerWidth = container.clientWidth;
69
+ if (!containerWidth) {
70
+ setVisibleCount(items.length);
71
+ return;
72
+ }
73
+ const keys = items.map(getKey);
74
+ const itemWidths = items.map((item, index) => {
75
+ const key = keys[index];
76
+ const measureNode = measureNodeMap.current.get(key);
77
+ const liveNode = itemNodeMap.current.get(key);
78
+ if (measurementMode === "live") {
79
+ if (measureNode) return measureNode.offsetWidth;
80
+ if (liveNode) return liveNode.offsetWidth;
81
+ }
82
+ return getEstimatedWidth(item, index, estimatedItemWidth, 96);
83
+ });
84
+ let nextVisible = items.length;
85
+ for (let count = items.length; count >= 0; count -= 1) {
86
+ const hiddenCount = items.length - count;
87
+ const visibleWidths = collapseFrom === "end" ? itemWidths.slice(0, count) : itemWidths.slice(items.length - count);
88
+ const itemsWidth = visibleWidths.reduce((sum, width) => sum + width, 0);
89
+ const itemsGap = count > 1 ? gap * (count - 1) : 0;
90
+ let currentOverflowWidth = 0;
91
+ if (hiddenCount > 0) {
92
+ if (typeof overflowWidth === "number") {
93
+ currentOverflowWidth = overflowWidth;
94
+ } else if (measureOverflowWidth) {
95
+ currentOverflowWidth = measureOverflowWidth(hiddenCount);
96
+ } else {
97
+ currentOverflowWidth = overflowRef.current?.offsetWidth ?? 44;
98
+ }
99
+ } else if (reserveOverflowSpace) {
100
+ if (typeof overflowWidth === "number") {
101
+ currentOverflowWidth = overflowWidth;
102
+ } else {
103
+ currentOverflowWidth = overflowRef.current?.offsetWidth ?? 44;
104
+ }
105
+ }
106
+ const overflowGap = (hiddenCount > 0 || reserveOverflowSpace) && count > 0 ? gap : 0;
107
+ const total = itemsWidth + itemsGap + overflowGap + currentOverflowWidth;
108
+ if (total <= containerWidth) {
109
+ nextVisible = count;
110
+ break;
111
+ }
112
+ }
113
+ setVisibleCount((prev) => prev === nextVisible ? prev : nextVisible);
114
+ }, [
115
+ collapseFrom,
116
+ estimatedItemWidth,
117
+ gap,
118
+ getKey,
119
+ isExpanded,
120
+ items,
121
+ measurementMode,
122
+ measureOverflowWidth,
123
+ overflowWidth,
124
+ reserveOverflowSpace
125
+ ]);
126
+ useIsoLayoutEffect(() => {
127
+ compute();
128
+ }, [compute]);
129
+ useIsoLayoutEffect(() => {
130
+ const container = containerRef.current;
131
+ if (!container || typeof ResizeObserver === "undefined") return;
132
+ const observer = new ResizeObserver(() => {
133
+ requestAnimationFrame(compute);
134
+ });
135
+ observer.observe(container);
136
+ return () => observer.disconnect();
137
+ }, [compute]);
138
+ useEffect(() => {
139
+ if (typeof window === "undefined") return;
140
+ const onResize = () => compute();
141
+ window.addEventListener("resize", onResize);
142
+ return () => window.removeEventListener("resize", onResize);
143
+ }, [compute]);
144
+ const registerItem = useCallback(
145
+ (key) => (node) => {
146
+ if (node) {
147
+ itemNodeMap.current.set(key, node);
148
+ } else {
149
+ itemNodeMap.current.delete(key);
150
+ }
151
+ },
152
+ []
153
+ );
154
+ const registerMeasureItem = useCallback(
155
+ (key) => (node) => {
156
+ if (node) {
157
+ measureNodeMap.current.set(key, node);
158
+ } else {
159
+ measureNodeMap.current.delete(key);
160
+ }
161
+ },
162
+ []
163
+ );
164
+ const registerOverflow = useCallback((node) => {
165
+ overflowRef.current = node;
166
+ }, []);
167
+ const clampedVisibleCount = Math.max(0, Math.min(visibleCount, items.length));
168
+ const visibleItems = useMemo(() => {
169
+ if (isExpanded) return [...items];
170
+ if (collapseFrom === "end") return items.slice(0, clampedVisibleCount);
171
+ return items.slice(items.length - clampedVisibleCount);
172
+ }, [clampedVisibleCount, collapseFrom, isExpanded, items]);
173
+ const hiddenItems = useMemo(() => {
174
+ if (isExpanded) return [];
175
+ if (collapseFrom === "end") return items.slice(clampedVisibleCount);
176
+ return items.slice(0, items.length - clampedVisibleCount);
177
+ }, [clampedVisibleCount, collapseFrom, isExpanded, items]);
178
+ const toggleExpanded = useCallback(() => {
179
+ setExpanded(!isExpanded);
180
+ }, [isExpanded, setExpanded]);
181
+ return {
182
+ containerRef,
183
+ registerItem,
184
+ registerMeasureItem,
185
+ registerOverflow,
186
+ visibleItems,
187
+ hiddenItems,
188
+ hiddenCount: hiddenItems.length,
189
+ isExpanded,
190
+ setExpanded,
191
+ toggleExpanded,
192
+ recompute: compute
193
+ };
194
+ }
195
+ function defaultOverflow({ hiddenCount }) {
196
+ return /* @__PURE__ */ jsxs("span", { children: [
197
+ "+",
198
+ hiddenCount
199
+ ] });
200
+ }
201
+ function FitList({
202
+ items,
203
+ getKey,
204
+ renderItem,
205
+ renderOverflow = defaultOverflow,
206
+ className,
207
+ listClassName,
208
+ itemClassName,
209
+ overflowClassName,
210
+ measureClassName,
211
+ emptyFallback = null,
212
+ gap = 8,
213
+ collapseFrom = "end",
214
+ reserveOverflowSpace = false,
215
+ overflowWidth,
216
+ estimatedItemWidth,
217
+ measurementMode = "live",
218
+ expanded,
219
+ defaultExpanded = false,
220
+ onExpandedChange,
221
+ as = "div",
222
+ onOverflowClick,
223
+ overflowAs = "button"
224
+ }) {
225
+ const Component = as;
226
+ const OverflowComponent = overflowAs;
227
+ const overflowMeasureRef = React.useRef(null);
228
+ const isDefaultOverflowRenderer = renderOverflow === defaultOverflow;
229
+ const measureOverflowWidth = React.useCallback(
230
+ (hiddenCount2) => {
231
+ if (typeof overflowWidth === "number") return overflowWidth;
232
+ const node = overflowMeasureRef.current;
233
+ if (!node) return 44;
234
+ if (isDefaultOverflowRenderer) {
235
+ const previous = node.textContent;
236
+ node.textContent = `+${hiddenCount2}`;
237
+ const width = node.offsetWidth;
238
+ node.textContent = previous;
239
+ return width;
240
+ }
241
+ return node.offsetWidth;
242
+ },
243
+ [isDefaultOverflowRenderer, overflowWidth]
244
+ );
245
+ const {
246
+ containerRef,
247
+ registerItem,
248
+ registerMeasureItem,
249
+ registerOverflow,
250
+ visibleItems,
251
+ hiddenItems,
252
+ hiddenCount,
253
+ isExpanded,
254
+ setExpanded,
255
+ toggleExpanded
256
+ } = useFitList({
257
+ items,
258
+ getKey,
259
+ gap,
260
+ collapseFrom,
261
+ reserveOverflowSpace,
262
+ overflowWidth,
263
+ estimatedItemWidth,
264
+ measurementMode,
265
+ expanded,
266
+ defaultExpanded,
267
+ onExpandedChange,
268
+ measureOverflowWidth
269
+ });
270
+ const visibleEntries = React.useMemo(() => {
271
+ if (isExpanded) {
272
+ return items.map((item, index) => ({ item, index }));
273
+ }
274
+ if (collapseFrom === "end") {
275
+ return items.slice(0, visibleItems.length).map((item, index) => ({ item, index }));
276
+ }
277
+ const startIndex = items.length - visibleItems.length;
278
+ return items.slice(startIndex).map((item, index) => ({ item, index: startIndex + index }));
279
+ }, [collapseFrom, isExpanded, items, visibleItems.length]);
280
+ if (items.length === 0) {
281
+ return /* @__PURE__ */ jsx(Fragment, { children: emptyFallback });
282
+ }
283
+ const overflowArgs = {
284
+ hiddenCount,
285
+ hiddenItems: [...hiddenItems],
286
+ visibleItems: [...visibleItems],
287
+ isExpanded,
288
+ setExpanded,
289
+ toggle: toggleExpanded
290
+ };
291
+ const overflowChildren = renderOverflow(overflowArgs);
292
+ const overflowButtonProps = {
293
+ className: overflowClassName,
294
+ type: "button",
295
+ onClick: (event) => onOverflowClick?.(overflowArgs, event),
296
+ "aria-expanded": isExpanded,
297
+ children: overflowChildren
298
+ };
299
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
300
+ /* @__PURE__ */ jsx(
301
+ "div",
302
+ {
303
+ className: listClassName,
304
+ style: {
305
+ display: "flex",
306
+ alignItems: "center",
307
+ gap,
308
+ minWidth: 0,
309
+ flex: "1 1 auto",
310
+ overflow: "hidden"
311
+ },
312
+ children: visibleEntries.map(({ item, index }) => {
313
+ const key = getKey(item, index);
314
+ return /* @__PURE__ */ jsx(
315
+ "div",
316
+ {
317
+ ref: registerItem(key),
318
+ className: itemClassName,
319
+ style: {
320
+ minWidth: 0,
321
+ flex: "0 0 auto",
322
+ whiteSpace: "nowrap"
323
+ },
324
+ children: renderItem(item, index)
325
+ },
326
+ key
327
+ );
328
+ })
329
+ }
330
+ ),
331
+ (hiddenCount > 0 || reserveOverflowSpace) && /* @__PURE__ */ jsx(
332
+ "div",
333
+ {
334
+ ref: registerOverflow,
335
+ style: {
336
+ visibility: hiddenCount > 0 ? "visible" : "hidden",
337
+ flex: "0 0 auto",
338
+ whiteSpace: "nowrap",
339
+ display: "block"
340
+ },
341
+ children: hiddenCount > 0 ? overflowAs === "button" ? /* @__PURE__ */ jsx("button", { ...overflowButtonProps }) : React.createElement(
342
+ OverflowComponent,
343
+ { className: overflowClassName },
344
+ overflowChildren
345
+ ) : /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "+0" })
346
+ }
347
+ )
348
+ ] });
349
+ const root = React.createElement(
350
+ Component,
351
+ {
352
+ ref: containerRef,
353
+ className,
354
+ style: {
355
+ display: "flex",
356
+ alignItems: "center",
357
+ gap,
358
+ minWidth: 0,
359
+ whiteSpace: "nowrap"
360
+ }
361
+ },
362
+ content
363
+ );
364
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
365
+ root,
366
+ /* @__PURE__ */ jsx(
367
+ "div",
368
+ {
369
+ "aria-hidden": "true",
370
+ style: {
371
+ pointerEvents: "none",
372
+ position: "fixed",
373
+ top: 0,
374
+ left: 0,
375
+ zIndex: -1,
376
+ overflow: "hidden",
377
+ opacity: 0
378
+ },
379
+ children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap }, children: [
380
+ items.map((item, index) => {
381
+ const key = getKey(item, index);
382
+ return /* @__PURE__ */ jsx(
383
+ "span",
384
+ {
385
+ ref: registerMeasureItem(key),
386
+ className: measureClassName ?? itemClassName,
387
+ style: {
388
+ display: "inline-flex",
389
+ whiteSpace: "nowrap"
390
+ },
391
+ children: renderItem(item, index)
392
+ },
393
+ `measure:${String(key)}`
394
+ );
395
+ }),
396
+ /* @__PURE__ */ jsx(
397
+ "span",
398
+ {
399
+ ref: overflowMeasureRef,
400
+ className: overflowClassName,
401
+ style: { display: "inline-flex", whiteSpace: "nowrap" },
402
+ children: overflowChildren
403
+ }
404
+ )
405
+ ] })
406
+ }
407
+ )
408
+ ] });
409
+ }
410
+
411
+ export { FitList, useFitList };
412
+ //# sourceMappingURL=index.js.map
413
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/useControllableState.ts","../src/hooks/useFitList.tsx","../src/components/FitList.tsx"],"names":["useState","useCallback","hiddenCount"],"mappings":"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","file":"index.js","sourcesContent":["import { useCallback, useState } from \"react\";\n\n/**\n * Shared controlled/uncontrolled state helper.\n *\n * Mirrors the common React component API pattern:\n * - pass `value` to control state externally\n * - omit `value` and use `defaultValue` for internal state\n * - observe changes through `onChange`\n */\nexport function useControllableState<T>({\n value,\n defaultValue,\n onChange,\n}: {\n /** Controlled value. When defined, internal state is ignored. */\n value: T | undefined;\n /** Initial value used for uncontrolled state. */\n defaultValue: T;\n /** Called whenever the state setter is invoked. */\n onChange?: (next: T) => void;\n}) {\n const [internal, setInternal] = useState(defaultValue);\n const isControlled = value !== undefined;\n const current = isControlled ? (value as T) : internal;\n\n const setValue = useCallback(\n (next: T) => {\n if (!isControlled) {\n setInternal(next);\n }\n onChange?.(next);\n },\n [isControlled, onChange]\n );\n\n return [current, setValue] as const;\n}\n","import type * as React from \"react\";\nimport {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useControllableState } from \"./useControllableState\";\nimport type { UseFitListOptions, UseFitListResult } from \"../types\";\n\nconst useIsoLayoutEffect =\n typeof window !== \"undefined\" ? useLayoutEffect : useEffect;\n\nfunction getEstimatedWidth<T>(\n item: T,\n index: number,\n estimatedItemWidth: number | ((item: T, index: number) => number) | undefined,\n fallback: number\n) {\n if (typeof estimatedItemWidth === \"function\")\n return estimatedItemWidth(item, index);\n if (typeof estimatedItemWidth === \"number\") return estimatedItemWidth;\n return fallback;\n}\n\n/**\n * Headless hook that calculates which items can fit in a single horizontal row.\n *\n * The hook measures the container and item widths, then returns visible/hidden\n * slices plus refs and callbacks needed to wire the calculation into your own UI.\n * Use this when you need the fitting logic without the default `FitList`\n * renderer.\n *\n * @example\n * ```tsx\n * const fit = useFitList({\n * items: tags,\n * getKey: (tag) => tag.id,\n * gap: 8,\n * });\n * ```\n */\nexport function useFitList<T>({\n items,\n getKey,\n reserveOverflowSpace = false,\n overflowWidth,\n gap = 8,\n collapseFrom = \"end\",\n estimatedItemWidth,\n measurementMode = \"live\",\n expanded,\n defaultExpanded = false,\n onExpandedChange,\n measureOverflowWidth,\n}: UseFitListOptions<T>): UseFitListResult<T> {\n const containerRef = useRef<HTMLDivElement | null>(null);\n const overflowRef = useRef<HTMLElement | null>(null);\n const itemNodeMap = useRef(new Map<React.Key, HTMLElement>());\n const measureNodeMap = useRef(new Map<React.Key, HTMLElement>());\n const [visibleCount, setVisibleCount] = useState(items.length);\n const [isExpanded, setExpanded] = useControllableState<boolean>({\n value: expanded,\n defaultValue: defaultExpanded,\n onChange: onExpandedChange,\n });\n\n const compute = useCallback(() => {\n if (isExpanded) {\n setVisibleCount(items.length);\n return;\n }\n\n const container = containerRef.current;\n if (!container) {\n setVisibleCount(items.length);\n return;\n }\n\n const containerWidth = container.clientWidth;\n if (!containerWidth) {\n setVisibleCount(items.length);\n return;\n }\n\n const keys = items.map(getKey);\n const itemWidths = items.map((item, index) => {\n const key = keys[index];\n const measureNode = measureNodeMap.current.get(key);\n const liveNode = itemNodeMap.current.get(key);\n if (measurementMode === \"live\") {\n if (measureNode) return measureNode.offsetWidth;\n if (liveNode) return liveNode.offsetWidth;\n }\n return getEstimatedWidth(item, index, estimatedItemWidth, 96);\n });\n\n let nextVisible = items.length;\n\n // Walk down from \"all items visible\" until the row fits within the container.\n for (let count = items.length; count >= 0; count -= 1) {\n const hiddenCount = items.length - count;\n const visibleWidths =\n collapseFrom === \"end\"\n ? itemWidths.slice(0, count)\n : itemWidths.slice(items.length - count);\n\n const itemsWidth = visibleWidths.reduce((sum, width) => sum + width, 0);\n const itemsGap = count > 1 ? gap * (count - 1) : 0;\n\n let currentOverflowWidth = 0;\n if (hiddenCount > 0) {\n if (typeof overflowWidth === \"number\") {\n currentOverflowWidth = overflowWidth;\n } else if (measureOverflowWidth) {\n currentOverflowWidth = measureOverflowWidth(hiddenCount);\n } else {\n currentOverflowWidth = overflowRef.current?.offsetWidth ?? 44;\n }\n } else if (reserveOverflowSpace) {\n if (typeof overflowWidth === \"number\") {\n currentOverflowWidth = overflowWidth;\n } else {\n currentOverflowWidth = overflowRef.current?.offsetWidth ?? 44;\n }\n }\n\n const overflowGap =\n (hiddenCount > 0 || reserveOverflowSpace) && count > 0 ? gap : 0;\n const total = itemsWidth + itemsGap + overflowGap + currentOverflowWidth;\n\n if (total <= containerWidth) {\n nextVisible = count;\n break;\n }\n }\n\n setVisibleCount((prev) => (prev === nextVisible ? prev : nextVisible));\n }, [\n collapseFrom,\n estimatedItemWidth,\n gap,\n getKey,\n isExpanded,\n items,\n measurementMode,\n measureOverflowWidth,\n overflowWidth,\n reserveOverflowSpace,\n ]);\n\n useIsoLayoutEffect(() => {\n compute();\n }, [compute]);\n\n useIsoLayoutEffect(() => {\n const container = containerRef.current;\n if (!container || typeof ResizeObserver === \"undefined\") return;\n\n const observer = new ResizeObserver(() => {\n requestAnimationFrame(compute);\n });\n\n observer.observe(container);\n return () => observer.disconnect();\n }, [compute]);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const onResize = () => compute();\n window.addEventListener(\"resize\", onResize);\n return () => window.removeEventListener(\"resize\", onResize);\n }, [compute]);\n\n const registerItem = useCallback(\n (key: React.Key) => (node: HTMLElement | null) => {\n if (node) {\n itemNodeMap.current.set(key, node);\n } else {\n itemNodeMap.current.delete(key);\n }\n },\n []\n );\n\n const registerMeasureItem = useCallback(\n (key: React.Key) => (node: HTMLElement | null) => {\n if (node) {\n measureNodeMap.current.set(key, node);\n } else {\n measureNodeMap.current.delete(key);\n }\n },\n []\n );\n\n const registerOverflow = useCallback((node: HTMLElement | null) => {\n overflowRef.current = node;\n }, []);\n\n const clampedVisibleCount = Math.max(0, Math.min(visibleCount, items.length));\n\n const visibleItems = useMemo(() => {\n if (isExpanded) return [...items];\n if (collapseFrom === \"end\") return items.slice(0, clampedVisibleCount);\n return items.slice(items.length - clampedVisibleCount);\n }, [clampedVisibleCount, collapseFrom, isExpanded, items]);\n\n const hiddenItems = useMemo(() => {\n if (isExpanded) return [];\n if (collapseFrom === \"end\") return items.slice(clampedVisibleCount);\n return items.slice(0, items.length - clampedVisibleCount);\n }, [clampedVisibleCount, collapseFrom, isExpanded, items]);\n\n const toggleExpanded = useCallback(() => {\n setExpanded(!isExpanded);\n }, [isExpanded, setExpanded]);\n\n return {\n containerRef,\n registerItem,\n registerMeasureItem,\n registerOverflow,\n visibleItems: visibleItems as T[],\n hiddenItems: hiddenItems as T[],\n hiddenCount: hiddenItems.length,\n isExpanded,\n setExpanded,\n toggleExpanded,\n recompute: compute,\n };\n}\n","import * as React from \"react\";\nimport { useFitList } from \"../hooks/useFitList\";\nimport type { FitListOverflowRenderArgs, FitListProps } from \"../types\";\n\ntype ButtonLikeProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {\n className?: string;\n children?: React.ReactNode;\n};\n\nfunction defaultOverflow({ hiddenCount }: { hiddenCount: number }) {\n return <span>+{hiddenCount}</span>;\n}\n\n/**\n * Responsive single-row list that hides overflowing items behind a configurable\n * overflow affordance.\n *\n * `FitList` is useful for chips, tags, breadcrumbs, recipients, filters, and\n * other horizontally laid out items where preserving a clean single-row layout\n * matters more than showing every element at once.\n *\n * Features:\n * - automatic fit calculation based on available width\n * - customizable overflow renderer (`+3`, `Show more`, badge, etc.)\n * - controlled or uncontrolled expanded state\n * - collapse from the start or the end of the list\n * - live DOM measurement or estimated-width mode\n */\nexport function FitList<T>({\n items,\n getKey,\n renderItem,\n renderOverflow = defaultOverflow,\n className,\n listClassName,\n itemClassName,\n overflowClassName,\n measureClassName,\n emptyFallback = null,\n gap = 8,\n collapseFrom = \"end\",\n reserveOverflowSpace = false,\n overflowWidth,\n estimatedItemWidth,\n measurementMode = \"live\",\n expanded,\n defaultExpanded = false,\n onExpandedChange,\n as = \"div\",\n onOverflowClick,\n overflowAs = \"button\",\n}: FitListProps<T>) {\n const Component = as as keyof React.JSX.IntrinsicElements;\n const OverflowComponent = overflowAs as keyof React.JSX.IntrinsicElements;\n const overflowMeasureRef = React.useRef<HTMLSpanElement | null>(null);\n const isDefaultOverflowRenderer = renderOverflow === defaultOverflow;\n\n const measureOverflowWidth = React.useCallback(\n (hiddenCount: number) => {\n if (typeof overflowWidth === \"number\") return overflowWidth;\n const node = overflowMeasureRef.current;\n if (!node) return 44;\n\n // The default overflow label changes width with the hidden count, so we\n // temporarily swap its text content to measure the exact width needed.\n if (isDefaultOverflowRenderer) {\n const previous = node.textContent;\n node.textContent = `+${hiddenCount}`;\n const width = node.offsetWidth;\n node.textContent = previous;\n return width;\n }\n\n return node.offsetWidth;\n },\n [isDefaultOverflowRenderer, overflowWidth]\n );\n\n const {\n containerRef,\n registerItem,\n registerMeasureItem,\n registerOverflow,\n visibleItems,\n hiddenItems,\n hiddenCount,\n isExpanded,\n setExpanded,\n toggleExpanded,\n } = useFitList({\n items,\n getKey,\n gap,\n collapseFrom,\n reserveOverflowSpace,\n overflowWidth,\n estimatedItemWidth,\n measurementMode,\n expanded,\n defaultExpanded,\n onExpandedChange,\n measureOverflowWidth,\n });\n\n const visibleEntries = React.useMemo(() => {\n if (isExpanded) {\n return items.map((item, index) => ({ item, index }));\n }\n\n if (collapseFrom === \"end\") {\n return items\n .slice(0, visibleItems.length)\n .map((item, index) => ({ item, index }));\n }\n\n const startIndex = items.length - visibleItems.length;\n return items\n .slice(startIndex)\n .map((item, index) => ({ item, index: startIndex + index }));\n }, [collapseFrom, isExpanded, items, visibleItems.length]);\n\n if (items.length === 0) {\n return <>{emptyFallback}</>;\n }\n\n const overflowArgs: FitListOverflowRenderArgs<T> = {\n hiddenCount,\n hiddenItems: [...hiddenItems] as T[],\n visibleItems: [...visibleItems] as T[],\n isExpanded,\n setExpanded,\n toggle: toggleExpanded,\n };\n\n const overflowChildren = renderOverflow(overflowArgs);\n\n const overflowButtonProps: ButtonLikeProps = {\n className: overflowClassName,\n type: \"button\",\n onClick: (event) => onOverflowClick?.(overflowArgs, event as React.MouseEvent<HTMLElement>),\n \"aria-expanded\": isExpanded,\n children: overflowChildren,\n };\n\n const content = (\n <>\n <div\n className={listClassName}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap,\n minWidth: 0,\n flex: \"1 1 auto\",\n overflow: \"hidden\",\n }}\n >\n {visibleEntries.map(({ item, index }) => {\n const key = getKey(item, index);\n return (\n <div\n key={key}\n ref={registerItem(key)}\n className={itemClassName}\n style={{\n minWidth: 0,\n flex: \"0 0 auto\",\n whiteSpace: \"nowrap\",\n }}\n >\n {renderItem(item, index)}\n </div>\n );\n })}\n </div>\n\n {(hiddenCount > 0 || reserveOverflowSpace) && (\n <div\n ref={registerOverflow}\n style={{\n visibility: hiddenCount > 0 ? \"visible\" : \"hidden\",\n flex: \"0 0 auto\",\n whiteSpace: \"nowrap\",\n display: \"block\",\n }}\n >\n {hiddenCount > 0 ? (\n overflowAs === \"button\" ? (\n <button {...overflowButtonProps} />\n ) : (\n React.createElement(\n OverflowComponent,\n { className: overflowClassName },\n overflowChildren\n )\n )\n ) : (\n <span aria-hidden=\"true\">+0</span>\n )}\n </div>\n )}\n </>\n );\n\n const root = React.createElement(\n Component,\n {\n ref: containerRef as React.Ref<any>,\n className,\n style: {\n display: \"flex\",\n alignItems: \"center\",\n gap,\n minWidth: 0,\n whiteSpace: \"nowrap\",\n },\n },\n content\n );\n\n return (\n <>\n {root}\n\n {/*\n Hidden measurement tree used to capture accurate intrinsic widths without\n affecting layout or interactivity.\n */}\n <div\n aria-hidden=\"true\"\n style={{\n pointerEvents: \"none\",\n position: \"fixed\",\n top: 0,\n left: 0,\n zIndex: -1,\n overflow: \"hidden\",\n opacity: 0,\n }}\n >\n <div style={{ display: \"flex\", alignItems: \"center\", gap }}>\n {items.map((item, index) => {\n const key = getKey(item, index);\n return (\n <span\n key={`measure:${String(key)}`}\n ref={registerMeasureItem(key)}\n className={measureClassName ?? itemClassName}\n style={{\n display: \"inline-flex\",\n whiteSpace: \"nowrap\",\n }}\n >\n {renderItem(item, index)}\n </span>\n );\n })}\n\n <span\n ref={overflowMeasureRef}\n className={overflowClassName}\n style={{ display: \"inline-flex\", whiteSpace: \"nowrap\" }}\n >\n {overflowChildren}\n </span>\n </div>\n </div>\n </>\n );\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "react-fit-list",
3
+ "version": "1.0.0",
4
+ "description": "A headless React component for rendering a single-line list that collapses overflowing items into a +N indicator.",
5
+ "keywords": [
6
+ "react",
7
+ "overflow",
8
+ "list",
9
+ "badges",
10
+ "chips",
11
+ "responsive",
12
+ "headless",
13
+ "ui"
14
+ ],
15
+ "homepage": "https://github.com/SincerelyFaust/react-fit-list#readme",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/SincerelyFaust/react-fit-list.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/SincerelyFaust/react-fit-list/issues"
22
+ },
23
+ "license": "MIT",
24
+ "author": "Marin Heđeš",
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js",
34
+ "require": "./dist/index.cjs"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "peerDependencies": {
46
+ "react": ">=18",
47
+ "react-dom": ">=18"
48
+ },
49
+ "devDependencies": {
50
+ "@testing-library/react": "^16.3.0",
51
+ "@types/react": "^19.1.16",
52
+ "@types/react-dom": "^19.1.9",
53
+ "jsdom": "^26.1.0",
54
+ "tsup": "^8.5.0",
55
+ "typescript": "^5.9.3",
56
+ "vitest": "^3.2.4"
57
+ },
58
+ "scripts": {
59
+ "build": "tsup",
60
+ "dev": "tsup --watch",
61
+ "typecheck": "tsc --noEmit",
62
+ "test": "vitest run",
63
+ "test:watch": "vitest",
64
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
65
+ }
66
+ }