superdesk-ui-framework 5.2.0 → 5.2.1

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.
Files changed (32) hide show
  1. package/app/styles/app.scss +1 -0
  2. package/app/styles/components/_overflow-stack.scss +120 -0
  3. package/app-typescript/components/CopyableTextBox.tsx +94 -0
  4. package/app-typescript/components/OverflowStack/OverflowStack.tsx +418 -0
  5. package/app-typescript/components/OverflowStack/OverflowStackPopover.tsx +45 -0
  6. package/app-typescript/components/OverflowStack/utils.tsx +16 -0
  7. package/app-typescript/components/SearchBar.tsx +149 -99
  8. package/app-typescript/index.ts +2 -0
  9. package/app-typescript/translations.ts +2 -0
  10. package/dist/components/CopyableTextBox.tsx +98 -0
  11. package/dist/components/Index.tsx +16 -1
  12. package/dist/components/OverflowStack.tsx +915 -0
  13. package/dist/components/SearchBar.tsx +394 -0
  14. package/dist/examples.bundle.js +5182 -3996
  15. package/dist/superdesk-ui.bundle.css +85 -0
  16. package/dist/superdesk-ui.bundle.js +3935 -3375
  17. package/dist/vendor.bundle.js +19 -19
  18. package/package.json +1 -1
  19. package/react/components/CopyableTextBox.d.ts +12 -0
  20. package/react/components/CopyableTextBox.js +84 -0
  21. package/react/components/OverflowStack/OverflowStack.d.ts +87 -0
  22. package/react/components/OverflowStack/OverflowStack.js +305 -0
  23. package/react/components/OverflowStack/OverflowStackPopover.d.ts +14 -0
  24. package/react/components/OverflowStack/OverflowStackPopover.js +78 -0
  25. package/react/components/OverflowStack/utils.d.ts +4 -0
  26. package/react/components/OverflowStack/utils.js +49 -0
  27. package/react/components/SearchBar.d.ts +19 -19
  28. package/react/components/SearchBar.js +90 -83
  29. package/react/index.d.ts +2 -0
  30. package/react/index.js +7 -3
  31. package/react/translations.d.ts +2 -0
  32. package/react/translations.js +2 -0
@@ -58,6 +58,7 @@
58
58
  @import 'components/card-item';
59
59
  @import 'components/sd-grid-item';
60
60
  @import 'components/sd-searchbar';
61
+ @import 'components/overflow-stack';
61
62
  @import 'components/sd-collapse-box';
62
63
  @import 'components/sd-photo-preview';
63
64
  @import 'components/sd-media-carousel';
@@ -0,0 +1,120 @@
1
+ // Overflow Stack Component
2
+ // A generic component for stacking items with overflow indicator
3
+
4
+ .overflow-stack {
5
+ display: inline-flex;
6
+ align-items: center;
7
+ position: relative;
8
+
9
+ // Gap variations (for non-overlapping items)
10
+ &.overflow-stack--gap-compact {
11
+ gap: var(--gap-0-5); // 4px
12
+ }
13
+
14
+ &.overflow-stack--gap-loose {
15
+ gap: var(--gap-1); // 8px
16
+ }
17
+
18
+ &.overflow-stack--gap-none {
19
+ gap: 0; // no gap
20
+ }
21
+
22
+ // Overlapping mode (like avatars)
23
+ &.overflow-stack--overlap {
24
+ gap: 0;
25
+
26
+ .overflow-stack__item {
27
+ margin-inline-start: calc(var(--space--1) * -1);
28
+ transition: transform 0.2s ease, z-index 0.2s ease;
29
+ position: relative;
30
+ z-index: 1;
31
+
32
+ &:first-child {
33
+ margin-inline-start: 0;
34
+ }
35
+
36
+ &:hover {
37
+ transform: translateY(-2px);
38
+ z-index: 10;
39
+ > * {
40
+ outline: 2px solid var(--color-surface-base);
41
+ }
42
+ }
43
+ }
44
+
45
+ .overflow-stack__indicator {
46
+ margin-inline-start: calc(var(--space--1) * -1);
47
+ z-index: 0;
48
+
49
+ &:hover {
50
+ transform: translateY(-2px);
51
+ z-index: 10;
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ .overflow-stack__item {
58
+ display: inline-flex;
59
+ align-items: center;
60
+ }
61
+
62
+ .overflow-stack__indicator {
63
+ display: inline-flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ min-width: 3.2rem;
67
+ height: 3.2rem;
68
+ padding: 0 var(--space--0-5);
69
+ background-color: var(--sd-colour-primary);
70
+ color: var(--white);
71
+ border: none;
72
+ border-radius: var(--border-radius--medium);
73
+ font-size: 1.3rem;
74
+ font-weight: 500;
75
+ cursor: pointer;
76
+ transition: background-color 0.2s ease, transform 0.2s ease;
77
+ position: relative;
78
+
79
+ &:hover {
80
+ background-color: var(--sd-colour-primary--darker);
81
+ }
82
+
83
+ &:active {
84
+ transform: scale(0.95);
85
+ }
86
+
87
+ &:focus-visible {
88
+ outline: 2px solid var(--sd-colour-primary);
89
+ outline-offset: 2px;
90
+ }
91
+ }
92
+
93
+ .overflow-stack__indicator-content {
94
+ font-size: var(--text-size--small);
95
+ color: var(--color-text-muted);
96
+ line-height: 1;
97
+ display: inline-flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ white-space: nowrap;
101
+ padding: var(--space--0-5);
102
+ }
103
+
104
+ .overflow-stack__popover {
105
+ background-color: var(--color-dropdown-menu-Bg);
106
+ border-radius: var(--b-radius--large);
107
+ padding: var(--space--1-5);
108
+ box-shadow: var(--sd-shadow__dropdown);
109
+ display: flex;
110
+ flex-direction: column;
111
+ min-width: max-content;
112
+ overflow-y: auto;
113
+ }
114
+
115
+ .overflow-stack__popover-item {
116
+ display: flex;
117
+ align-items: center;
118
+ padding-block: var(--space--0-5);
119
+ transition: all 0.2s ease;
120
+ }
@@ -0,0 +1,94 @@
1
+ import React, {useEffect} from 'react';
2
+ import {Button} from './Button';
3
+ import {gettext} from '../translations';
4
+ import {InputWrapper} from './Form';
5
+ import nextId from 'react-id-generator';
6
+ import {IInputCommon} from './Form/InputWrapper';
7
+ import {toasted} from './Toast';
8
+
9
+ interface ICopyableTextBoxProps extends IInputCommon {
10
+ value: string;
11
+
12
+ /**
13
+ * Defaults to medium
14
+ */
15
+ size?: 'medium' | 'large';
16
+
17
+ 'data-test-id'?: string;
18
+ }
19
+
20
+ export const CopyableTextBox: React.FC<ICopyableTextBoxProps> = (props) => {
21
+ const htmlId = nextId();
22
+ const timeoutIdRef = React.useRef<number>();
23
+ const [copied, setCopied] = React.useState(false);
24
+
25
+ useEffect(() => {
26
+ return () => {
27
+ if (timeoutIdRef.current) {
28
+ window.clearTimeout(timeoutIdRef.current);
29
+ }
30
+ };
31
+ }, []);
32
+
33
+ const handleCopy = () => {
34
+ navigator.clipboard
35
+ .writeText(props.value)
36
+ .then(() => {
37
+ setCopied(true);
38
+
39
+ if (timeoutIdRef.current) {
40
+ window.clearTimeout(timeoutIdRef.current);
41
+ }
42
+
43
+ timeoutIdRef.current = window.setTimeout(() => {
44
+ setCopied(false);
45
+ }, 2000);
46
+ })
47
+ .catch(() => {
48
+ toasted.notify(gettext("Couldn't copy"), {
49
+ type: 'warning',
50
+ });
51
+ });
52
+ };
53
+
54
+ return (
55
+ <InputWrapper
56
+ label={props.label}
57
+ disabled={props.disabled}
58
+ value={props.value}
59
+ error={props.error}
60
+ invalid={props.error != null}
61
+ info={props.info}
62
+ size={props.size ?? 'medium'}
63
+ fullWidth={true}
64
+ htmlId={htmlId}
65
+ tabindex={props.tabindex}
66
+ >
67
+ <div className="d-flex items-center gap-1">
68
+ <input
69
+ id={htmlId}
70
+ aria-describedby={htmlId + 'label'}
71
+ type="text"
72
+ className="sd-input__input"
73
+ style={{
74
+ border: '1px solid var(--color-input-border)',
75
+ borderRadius: 'var(--b-radius--medium)',
76
+ }}
77
+ value={props.value}
78
+ disabled
79
+ data-test-id={props['data-test-id']}
80
+ />
81
+ <Button
82
+ text={gettext('Copy')}
83
+ icon={copied ? 'ok' : 'copy'}
84
+ iconOnly={true}
85
+ onClick={handleCopy}
86
+ type="default"
87
+ style="hollow"
88
+ size={props.size === 'medium' ? 'normal' : (props.size ?? 'normal')}
89
+ data-test-id="copy-button"
90
+ />
91
+ </div>
92
+ </InputWrapper>
93
+ );
94
+ };
@@ -0,0 +1,418 @@
1
+ import * as React from 'react';
2
+ import classNames from 'classnames';
3
+ import debounce from 'lodash/debounce';
4
+ import {WithPopover} from '../WithPopover';
5
+ import {HeadlessButton} from '../HeadlessButton';
6
+ import {IconButton} from '../IconButton';
7
+ import {OverflowStackPopover} from './OverflowStackPopover';
8
+ import {defaultIndicator, HIDDEN_ITEM_STYLE} from './utils';
9
+
10
+ interface IPropsOverflowStackBase {
11
+ /**
12
+ * Default: {type: `fixed`, max: 4}
13
+ */
14
+ overflow?:
15
+ | {
16
+ type: 'fixed';
17
+ max?: number | 'show-all';
18
+ }
19
+ | {
20
+ type: 'auto';
21
+ };
22
+
23
+ /**
24
+ * Defaults to `compact`
25
+ */
26
+ gap?: 'compact' | 'loose' | 'none';
27
+
28
+ /**
29
+ * When `true`, items will have negative margin and expand on hover
30
+ */
31
+ overlap?: boolean;
32
+
33
+ showOnlyHiddenInPopover?: boolean;
34
+
35
+ /**
36
+ * Defaults to `count`
37
+ * `count`: Shows "+N" with the number of hidden items
38
+ * `dots`: Shows a three dots icon without the count
39
+ */
40
+ indicatorStyle?: 'count' | 'dots';
41
+
42
+ renderIndicator?: (count: number) => React.ReactNode;
43
+ onIndicatorClick?: () => void;
44
+
45
+ /**
46
+ * Defaults to `full`
47
+ * Border radius for the indicator button
48
+ */
49
+ indicatorRadius?: 'x-small' | 'small' | 'medium' | 'full';
50
+
51
+ containerClassName?: string;
52
+ }
53
+
54
+ interface IPropsOverflowStackSimple extends IPropsOverflowStackBase {
55
+ type: 'simple';
56
+ items: Array<React.ReactNode>;
57
+
58
+ /**
59
+ * Custom render function for items in the popover.
60
+ * Useful if you just want to wrap children, but still keep everything else default.
61
+ * If not provided, items will be rendered as-is
62
+ */
63
+ renderPopoverItem?: (item: React.ReactNode, index: number) => React.ReactNode;
64
+ }
65
+
66
+ interface IPropsOverflowStackData<T> extends IPropsOverflowStackBase {
67
+ type: 'data';
68
+ items: Array<T>;
69
+
70
+ renderVisibleItem: (data: T, index: number) => React.ReactNode;
71
+
72
+ /**
73
+ * If not provided, `renderVisibleItem` will be used
74
+ */
75
+ renderHiddenItem?: (data: T, index: number) => React.ReactNode;
76
+ }
77
+
78
+ export type IPropsOverflowStack<T> = IPropsOverflowStackSimple | IPropsOverflowStackData<T>;
79
+
80
+ interface IStateOverflowStack {
81
+ /**
82
+ * Number of visible items when using auto overflow
83
+ */
84
+ visibleCount: number;
85
+ }
86
+
87
+ export class OverflowStack<T> extends React.PureComponent<IPropsOverflowStack<T>, IStateOverflowStack> {
88
+ private containerRef = React.createRef<HTMLDivElement>();
89
+ private itemRefs: Array<React.RefObject<HTMLDivElement>> = [];
90
+ private indicatorRef = React.createRef<HTMLDivElement>();
91
+ private resizeObserver: ResizeObserver | null = null;
92
+ private popoverContentRef = React.createRef<OverflowStackPopover<T>>();
93
+
94
+ /**
95
+ * Use a ref to avoid closure when passing props.
96
+ */
97
+ private currentPropsRef: {current: IPropsOverflowStack<T> | null} = {current: null};
98
+
99
+ /**
100
+ * Debounced calculation to avoid excessive updates during resize
101
+ */
102
+ private debouncedCalculate = debounce(() => {
103
+ this.calculateVisibleItems();
104
+ }, 50);
105
+
106
+ constructor(props: IPropsOverflowStack<T>) {
107
+ super(props);
108
+ const itemCount = this.getItemCount(props);
109
+ this.state = {
110
+ visibleCount: itemCount,
111
+ };
112
+
113
+ this.itemRefs = Array.from({length: itemCount}, () => React.createRef<HTMLDivElement>());
114
+ this.currentPropsRef.current = props;
115
+ }
116
+
117
+ private getItemCount(props: IPropsOverflowStack<T>): number {
118
+ if (props.type === 'data') {
119
+ return props.items.length;
120
+ }
121
+
122
+ return props.items?.length ?? 0;
123
+ }
124
+
125
+ componentDidMount() {
126
+ if (this.props.overflow?.type === 'auto') {
127
+ this.setupResizeObserver();
128
+
129
+ requestAnimationFrame(() => {
130
+ this.calculateVisibleItems();
131
+ });
132
+ }
133
+ }
134
+
135
+ componentDidUpdate(prevProps: IPropsOverflowStack<T>) {
136
+ // Update the props ref for the popover
137
+ this.currentPropsRef.current = this.props;
138
+ this.popoverContentRef.current?.forceUpdate?.();
139
+
140
+ const isAutoMode = this.props.overflow?.type === 'auto';
141
+
142
+ if (prevProps.overflow !== this.props.overflow) {
143
+ if (isAutoMode) {
144
+ this.setupResizeObserver();
145
+ } else {
146
+ this.cleanupResizeObserver();
147
+ }
148
+ }
149
+
150
+ if (isAutoMode) {
151
+ const prevItemCount = this.getItemCount(prevProps);
152
+ const currentItemCount = this.getItemCount(this.props);
153
+
154
+ if (prevItemCount !== currentItemCount) {
155
+ // Recreate refs if item count changed
156
+ this.itemRefs = Array.from({length: currentItemCount}, () => React.createRef<HTMLDivElement>());
157
+
158
+ this.setState({visibleCount: currentItemCount}, () => {
159
+ requestAnimationFrame(() => this.calculateVisibleItems());
160
+ });
161
+ } else {
162
+ requestAnimationFrame(() => this.calculateVisibleItems());
163
+ }
164
+ }
165
+ }
166
+
167
+ componentWillUnmount() {
168
+ this.cleanupResizeObserver();
169
+ this.debouncedCalculate.cancel();
170
+ }
171
+
172
+ private setupResizeObserver() {
173
+ if (this.resizeObserver) {
174
+ return;
175
+ }
176
+
177
+ this.resizeObserver = new ResizeObserver(() => {
178
+ this.debouncedCalculate();
179
+ });
180
+
181
+ if (this.containerRef.current) {
182
+ this.resizeObserver.observe(this.containerRef.current);
183
+ }
184
+ }
185
+
186
+ private cleanupResizeObserver() {
187
+ if (this.resizeObserver) {
188
+ this.resizeObserver.disconnect();
189
+ this.resizeObserver = null;
190
+ }
191
+ }
192
+
193
+ private getGapSize(): number {
194
+ const {gap = 'compact', overlap = false} = this.props;
195
+
196
+ if (overlap) {
197
+ return -8; // Overlapping items have negative margin
198
+ }
199
+
200
+ switch (gap) {
201
+ case 'compact':
202
+ return 4;
203
+ case 'loose':
204
+ return 8;
205
+ case 'none':
206
+ return 0;
207
+ default:
208
+ return 4;
209
+ }
210
+ }
211
+
212
+ private calculateVisibleItems() {
213
+ if (!this.containerRef.current || this.props.overflow?.type !== 'auto') {
214
+ return;
215
+ }
216
+
217
+ const containerWidth = this.containerRef.current.offsetWidth;
218
+
219
+ // If container has no width yet, skip calculation
220
+ if (containerWidth === 0) {
221
+ return;
222
+ }
223
+
224
+ const gapSize = this.getGapSize();
225
+ const indicatorWidth = this.indicatorRef.current?.offsetWidth || 40;
226
+ const totalItemCount = this.getItemCount(this.props);
227
+
228
+ let totalWidth = 0;
229
+ let visibleCount = 0;
230
+
231
+ // Calculate how many items fit
232
+ for (let i = 0; i < this.itemRefs.length; i++) {
233
+ const itemRef = this.itemRefs[i];
234
+
235
+ if (!itemRef.current) {
236
+ visibleCount = totalItemCount;
237
+ break;
238
+ }
239
+
240
+ const itemWidth = itemRef.current.offsetWidth;
241
+
242
+ if (itemWidth === 0) {
243
+ visibleCount = totalItemCount;
244
+ break;
245
+ }
246
+
247
+ const widthWithGap = itemWidth + (i > 0 ? gapSize : 0);
248
+ const needsIndicator = i < totalItemCount - 1;
249
+ const availableWidth = containerWidth - (needsIndicator ? indicatorWidth + gapSize : 0);
250
+
251
+ if (totalWidth + widthWithGap <= availableWidth) {
252
+ totalWidth += widthWithGap;
253
+ visibleCount = i + 1;
254
+ } else {
255
+ break;
256
+ }
257
+ }
258
+
259
+ if (visibleCount === 0 && totalItemCount > 0) {
260
+ visibleCount = 1;
261
+ }
262
+
263
+ if (this.state.visibleCount !== visibleCount) {
264
+ this.setState({visibleCount});
265
+ }
266
+ }
267
+
268
+ render() {
269
+ const {
270
+ gap = 'compact',
271
+ overlap = false,
272
+ overflow = {
273
+ type: 'fixed',
274
+ max: 4,
275
+ },
276
+ showOnlyHiddenInPopover = false,
277
+ indicatorStyle = 'count',
278
+ renderIndicator,
279
+ onIndicatorClick,
280
+ indicatorRadius = 'full',
281
+ containerClassName: className,
282
+ } = this.props;
283
+
284
+ const itemCount = this.getItemCount(this.props);
285
+
286
+ let max: number;
287
+
288
+ if (overflow.type === 'auto') {
289
+ max = this.state.visibleCount;
290
+ } else if (overflow.max === 'show-all') {
291
+ max = itemCount;
292
+ } else {
293
+ max = overflow.max ?? 4;
294
+ }
295
+
296
+ const itemsOverLimit = itemCount - max;
297
+
298
+ const renderVisibleItems = () => {
299
+ const renderItemWrapper = (content: React.ReactNode, index: number) => {
300
+ const isVisible = index < max;
301
+
302
+ return (
303
+ <div
304
+ key={index}
305
+ ref={overflow.type === 'auto' ? this.itemRefs[index] : undefined}
306
+ className="overflow-stack__item"
307
+ style={overflow.type === 'auto' && !isVisible ? HIDDEN_ITEM_STYLE : undefined}
308
+ >
309
+ {content}
310
+ </div>
311
+ );
312
+ };
313
+
314
+ const {props} = this;
315
+
316
+ if (props.type === 'data') {
317
+ const itemsToRender = overflow.type === 'auto' ? props.items : props.items.slice(0, max);
318
+
319
+ return itemsToRender.map((data, index) =>
320
+ renderItemWrapper(props.renderVisibleItem(data, index), index),
321
+ );
322
+ } else {
323
+ const itemsToRender = overflow.type === 'auto' ? props.items : props.items.slice(0, max);
324
+
325
+ return itemsToRender.map((item, index) => renderItemWrapper(item, index));
326
+ }
327
+ };
328
+
329
+ const ariaLabel = showOnlyHiddenInPopover
330
+ ? `Show ${itemsOverLimit} hidden items`
331
+ : `Show ${itemsOverLimit} more items`;
332
+
333
+ const renderIndicatorButton = (onToggle: (ref: HTMLElement) => void) => {
334
+ const handleClick = (e: React.MouseEvent<HTMLElement>) => {
335
+ e.stopPropagation();
336
+
337
+ if (onIndicatorClick == null) {
338
+ onToggle(e.currentTarget);
339
+ } else {
340
+ onIndicatorClick();
341
+ }
342
+ };
343
+
344
+ let indicator: React.ReactNode;
345
+
346
+ if (indicatorStyle === 'dots') {
347
+ indicator = <IconButton size="small" icon="dots" ariaValue={ariaLabel} onClick={handleClick} />;
348
+ } else {
349
+ const content = renderIndicator?.(itemsOverLimit) ?? defaultIndicator(itemsOverLimit);
350
+
351
+ indicator = (
352
+ <HeadlessButton
353
+ radius={indicatorRadius}
354
+ onClick={handleClick}
355
+ ariaLabel={ariaLabel}
356
+ tooltip={ariaLabel}
357
+ >
358
+ {content}
359
+ </HeadlessButton>
360
+ );
361
+ }
362
+
363
+ // Wrap in a div with ref for auto mode (needed for width measurement)
364
+ if (overflow.type === 'auto') {
365
+ return (
366
+ <div ref={this.indicatorRef} style={{display: 'inline-flex'}}>
367
+ {indicator}
368
+ </div>
369
+ );
370
+ }
371
+
372
+ return indicator;
373
+ };
374
+
375
+ const classes = classNames(
376
+ 'overflow-stack',
377
+ {
378
+ 'overflow-stack--overlap': overlap,
379
+ [`overflow-stack--gap-${gap}`]: !overlap,
380
+ },
381
+ className,
382
+ );
383
+
384
+ return (
385
+ <WithPopover
386
+ component={({closePopup}) => (
387
+ <OverflowStackPopover
388
+ ref={this.popoverContentRef}
389
+ propsRef={this.currentPropsRef}
390
+ max={max}
391
+ showOnlyHiddenInPopover={showOnlyHiddenInPopover}
392
+ closePopup={closePopup}
393
+ />
394
+ )}
395
+ >
396
+ {(onToggle) => {
397
+ const stackContent = (
398
+ <div
399
+ ref={this.containerRef}
400
+ className={classes}
401
+ role="group"
402
+ style={overflow.type === 'auto' ? {width: '100%'} : undefined}
403
+ >
404
+ {renderVisibleItems()}
405
+ {itemsOverLimit > 0 && renderIndicatorButton(onToggle)}
406
+ </div>
407
+ );
408
+
409
+ if (overflow.type === 'auto') {
410
+ return <div style={{display: 'flex', width: '100%', minWidth: 0}}>{stackContent}</div>;
411
+ }
412
+
413
+ return stackContent;
414
+ }}
415
+ </WithPopover>
416
+ );
417
+ }
418
+ }
@@ -0,0 +1,45 @@
1
+ import * as React from 'react';
2
+ import {renderHiddenItemDefault} from './utils';
3
+ import type {IPropsOverflowStack} from './OverflowStack';
4
+
5
+ interface IPopoverContentProps<T> {
6
+ max: number;
7
+ closePopup(): void;
8
+ showOnlyHiddenInPopover: boolean;
9
+ propsRef: {current: IPropsOverflowStack<T> | null};
10
+ }
11
+
12
+ export class OverflowStackPopover<T> extends React.Component<IPopoverContentProps<T>> {
13
+ render() {
14
+ const {propsRef, max, showOnlyHiddenInPopover} = this.props;
15
+ const props = propsRef.current;
16
+
17
+ if (props == null) {
18
+ return null;
19
+ }
20
+
21
+ const itemsToShow = showOnlyHiddenInPopover ? props.items.slice(max) : props.items;
22
+
23
+ if (props.type === 'data') {
24
+ const renderFn = props.renderHiddenItem ?? props.renderVisibleItem;
25
+
26
+ return (
27
+ <div className="overflow-stack__popover">
28
+ {itemsToShow.map((data, index) => (
29
+ <div key={index} className="overflow-stack__popover-item">
30
+ {renderFn(data as T, showOnlyHiddenInPopover ? index + max : index)}
31
+ </div>
32
+ ))}
33
+ </div>
34
+ );
35
+ } else {
36
+ const renderFn = props.renderPopoverItem ?? renderHiddenItemDefault;
37
+
38
+ return (
39
+ <div className="overflow-stack__popover">
40
+ {itemsToShow.map((item, index) => renderFn(item, showOnlyHiddenInPopover ? index + max : index))}
41
+ </div>
42
+ );
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,16 @@
1
+ import * as React from 'react';
2
+
3
+ export const HIDDEN_ITEM_STYLE: React.CSSProperties = {
4
+ position: 'absolute',
5
+ visibility: 'hidden',
6
+ pointerEvents: 'none',
7
+ whiteSpace: 'nowrap',
8
+ } as const;
9
+
10
+ export const defaultIndicator = (count: number) => <span className="overflow-stack__indicator-content">+{count}</span>;
11
+
12
+ export const renderHiddenItemDefault = (item: React.ReactNode, index: number) => (
13
+ <div key={index} className="overflow-stack__popover-item">
14
+ {item}
15
+ </div>
16
+ );