@inkra/react 0.1.0 → 0.2.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/README.md +104 -1
- package/dist/PencilIcon.js +14 -16
- package/dist/PencilLine.js +18 -12
- package/dist/PencilText.js +192 -59
- package/dist/alert-dialog/index.d.ts +64 -0
- package/dist/alert-dialog/index.js +109 -0
- package/dist/audio-player/index.d.ts +60 -0
- package/dist/audio-player/index.js +101 -0
- package/dist/carousel/index.d.ts +51 -0
- package/dist/carousel/index.js +290 -0
- package/dist/choice-grid/index.d.ts +38 -0
- package/dist/choice-grid/index.js +85 -0
- package/dist/color-picker/color.d.ts +15 -0
- package/dist/color-picker/color.js +55 -0
- package/dist/color-picker/index.d.ts +31 -0
- package/dist/color-picker/index.js +69 -0
- package/dist/command/index.d.ts +23 -0
- package/dist/command/index.js +39 -0
- package/dist/context-menu/index.d.ts +76 -0
- package/dist/context-menu/index.js +240 -0
- package/dist/data-table/index.d.ts +47 -0
- package/dist/data-table/index.js +91 -0
- package/dist/date-picker/index.d.ts +50 -0
- package/dist/date-picker/index.js +180 -0
- package/dist/display/ProgressRing.d.ts +6 -0
- package/dist/display/ProgressRing.js +146 -0
- package/dist/display/index.d.ts +27 -0
- package/dist/display/index.js +18 -2
- package/dist/dockable-panel/index.d.ts +41 -0
- package/dist/dockable-panel/index.js +253 -0
- package/dist/dockable-panel/model.d.ts +21 -0
- package/dist/dockable-panel/model.js +61 -0
- package/dist/drawer/index.d.ts +63 -0
- package/dist/drawer/index.js +86 -0
- package/dist/editable/index.d.ts +53 -0
- package/dist/editable/index.js +162 -0
- package/dist/empty/index.d.ts +21 -0
- package/dist/empty/index.js +23 -0
- package/dist/flip/index.d.ts +49 -0
- package/dist/flip/index.js +138 -0
- package/dist/form/EditablePencilText.js +157 -146
- package/dist/form/Textarea.d.ts +12 -0
- package/dist/form/Textarea.js +83 -4
- package/dist/form/index.d.ts +1 -1
- package/dist/heatmap/index.d.ts +59 -0
- package/dist/heatmap/index.js +248 -0
- package/dist/hotkey/index.d.ts +37 -0
- package/dist/hotkey/index.js +130 -0
- package/dist/hotkey/model.d.ts +27 -0
- package/dist/hotkey/model.js +97 -0
- package/dist/hover-card/index.d.ts +33 -0
- package/dist/hover-card/index.js +141 -0
- package/dist/image-cropper/index.d.ts +82 -0
- package/dist/image-cropper/index.js +261 -0
- package/dist/image-cropper/model.d.ts +42 -0
- package/dist/image-cropper/model.js +94 -0
- package/dist/image-viewer/index.d.ts +79 -0
- package/dist/image-viewer/index.js +220 -0
- package/dist/image-viewer/model.d.ts +9 -0
- package/dist/image-viewer/model.js +18 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +47 -0
- package/dist/internal/cache.d.ts +1 -1
- package/dist/internal/cache.js +1 -1
- package/dist/internal/renderTokens.d.ts +10 -0
- package/dist/internal/renderTokens.js +13 -0
- package/dist/internal/surfaceRenderer.d.ts +14 -6
- package/dist/internal/surfaceRenderer.js +15 -12
- package/dist/internal/textRenderer.d.ts +1 -0
- package/dist/internal/textRenderer.js +33 -7
- package/dist/internal/usePencilSurface.js +16 -8
- package/dist/internal/useSelectionDrawing.js +15 -11
- package/dist/journey-map/MapScale.js +3 -1
- package/dist/journey-map/index.js +3 -1
- package/dist/keypad/index.d.ts +23 -0
- package/dist/keypad/index.js +63 -0
- package/dist/keypad/model.d.ts +20 -0
- package/dist/keypad/model.js +33 -0
- package/dist/layout/index.d.ts +63 -0
- package/dist/layout/index.js +62 -0
- package/dist/log/index.d.ts +45 -0
- package/dist/log/index.js +159 -0
- package/dist/log/model.d.ts +16 -0
- package/dist/log/model.js +13 -0
- package/dist/node-graph/index.d.ts +123 -0
- package/dist/node-graph/index.js +304 -0
- package/dist/number-ticker/index.d.ts +29 -0
- package/dist/number-ticker/index.js +132 -0
- package/dist/number-ticker/model.d.ts +8 -0
- package/dist/number-ticker/model.js +34 -0
- package/dist/overflow-list/index.d.ts +39 -0
- package/dist/overflow-list/index.js +230 -0
- package/dist/overflow-list/layout.d.ts +19 -0
- package/dist/overflow-list/layout.js +44 -0
- package/dist/pagination/index.d.ts +23 -0
- package/dist/pagination/index.js +41 -0
- package/dist/pagination/model.d.ts +4 -0
- package/dist/pagination/model.js +42 -0
- package/dist/pan-zoom/index.d.ts +71 -0
- package/dist/pan-zoom/index.js +175 -0
- package/dist/radar/index.d.ts +54 -0
- package/dist/radar/index.js +220 -0
- package/dist/range-slider/index.d.ts +16 -0
- package/dist/range-slider/index.js +22 -0
- package/dist/rating/index.d.ts +28 -0
- package/dist/rating/index.js +127 -0
- package/dist/resizable/index.d.ts +32 -0
- package/dist/resizable/index.js +50 -0
- package/dist/scroll-area/index.d.ts +36 -0
- package/dist/scroll-area/index.js +44 -0
- package/dist/segmented-progress/SegmentedProgress.d.ts +17 -0
- package/dist/segmented-progress/SegmentedProgress.js +42 -0
- package/dist/slider/index.js +1 -1
- package/dist/slot/index.d.ts +24 -0
- package/dist/slot/index.js +29 -0
- package/dist/sortable/index.d.ts +50 -0
- package/dist/sortable/index.js +278 -0
- package/dist/sortable/model.d.ts +30 -0
- package/dist/sortable/model.js +64 -0
- package/dist/sparkline/index.d.ts +19 -0
- package/dist/sparkline/index.js +240 -0
- package/dist/sparkline/model.d.ts +16 -0
- package/dist/sparkline/model.js +28 -0
- package/dist/steps/index.d.ts +16 -0
- package/dist/steps/index.js +37 -0
- package/dist/styles.css +4850 -287
- package/dist/tag-input/index.d.ts +69 -0
- package/dist/tag-input/index.js +325 -0
- package/dist/tag-input/model.d.ts +14 -0
- package/dist/tag-input/model.js +38 -0
- package/dist/time-picker/index.d.ts +15 -0
- package/dist/time-picker/index.js +168 -0
- package/dist/timeline/index.d.ts +14 -0
- package/dist/timeline/index.js +16 -0
- package/dist/toast/index.d.ts +27 -0
- package/dist/toast/index.js +36 -0
- package/dist/toggle-group/index.d.ts +17 -0
- package/dist/toggle-group/index.js +26 -0
- package/dist/tokens.css +10 -9
- package/dist/transfer/index.d.ts +64 -0
- package/dist/transfer/index.js +174 -0
- package/dist/transfer/model.d.ts +12 -0
- package/dist/transfer/model.js +33 -0
- package/dist/tree/index.d.ts +39 -0
- package/dist/tree/index.js +36 -0
- package/dist/tree-select/index.d.ts +59 -0
- package/dist/tree-select/index.js +142 -0
- package/dist/tree-select/model.d.ts +31 -0
- package/dist/tree-select/model.js +135 -0
- package/dist/typewriter/index.d.ts +21 -0
- package/dist/typewriter/index.js +101 -0
- package/dist/typewriter/model.d.ts +7 -0
- package/dist/typewriter/model.js +45 -0
- package/dist/upload/index.d.ts +59 -0
- package/dist/upload/index.js +230 -0
- package/dist/virtual-list/index.d.ts +44 -0
- package/dist/virtual-list/index.js +128 -0
- package/dist/virtual-list/model.d.ts +7 -0
- package/dist/virtual-list/model.js +27 -0
- package/package.json +22 -3
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { PencilText } from '../PencilText.js';
|
|
4
|
+
import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
|
|
5
|
+
import { canAnimateTickerValue, createNumberFormatter, easeOutCubic, formatTickerDelta, formatTickerValue, interpolateTickerValue, tickerValueChanged, } from './model.js';
|
|
6
|
+
function usePrefersReducedMotion() {
|
|
7
|
+
const [reduced, setReduced] = useState(false);
|
|
8
|
+
useBrowserLayoutEffect(() => {
|
|
9
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function')
|
|
10
|
+
return undefined;
|
|
11
|
+
const query = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
12
|
+
const update = () => setReduced(query.matches);
|
|
13
|
+
update();
|
|
14
|
+
query.addEventListener('change', update);
|
|
15
|
+
return () => query.removeEventListener('change', update);
|
|
16
|
+
}, []);
|
|
17
|
+
return reduced;
|
|
18
|
+
}
|
|
19
|
+
function lettering(content, seed) {
|
|
20
|
+
return typeof content === 'string' ? _jsx(PencilText, { seed: seed, children: content }) : content;
|
|
21
|
+
}
|
|
22
|
+
function accessibleAffix(content) {
|
|
23
|
+
return typeof content === 'string' || typeof content === 'number' ? String(content) : '';
|
|
24
|
+
}
|
|
25
|
+
export const NumberTicker = forwardRef(function NumberTicker({ animate = true, className, deltaDuration = 1600, deltaFormatOptions, duration = 420, easing = easeOutCubic, formatOptions, getValueLabel, locale, prefix, renderDelta, renderValue, showDelta = false, suffix, value, ...props }, forwardedRef) {
|
|
26
|
+
const rootRef = useRef(null);
|
|
27
|
+
const frameRef = useRef(null);
|
|
28
|
+
const deltaTimerRef = useRef(null);
|
|
29
|
+
const displayedRef = useRef(value);
|
|
30
|
+
const targetRef = useRef(value);
|
|
31
|
+
const [displayedValue, setDisplayedValue] = useState(value);
|
|
32
|
+
const [announcedValue, setAnnouncedValue] = useState(value);
|
|
33
|
+
const [animating, setAnimating] = useState(false);
|
|
34
|
+
const [delta, setDelta] = useState(null);
|
|
35
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
36
|
+
const localeKey = Array.isArray(locale) ? locale.join('\u0000') : locale;
|
|
37
|
+
const formatOptionsKey = JSON.stringify(formatOptions ?? {});
|
|
38
|
+
const deltaFormatOptionsKey = JSON.stringify(deltaFormatOptions ?? formatOptions ?? {});
|
|
39
|
+
const formatter = useMemo(() => createNumberFormatter(locale, formatOptions), [localeKey, formatOptionsKey]);
|
|
40
|
+
const deltaFormatter = useMemo(() => createNumberFormatter(locale, deltaFormatOptions ?? formatOptions), [localeKey, deltaFormatOptionsKey]);
|
|
41
|
+
useImperativeHandle(forwardedRef, () => rootRef.current);
|
|
42
|
+
useBrowserLayoutEffect(() => {
|
|
43
|
+
const previousTarget = targetRef.current;
|
|
44
|
+
targetRef.current = value;
|
|
45
|
+
if (showDelta && tickerValueChanged(previousTarget, value) && Number.isFinite(previousTarget) && Number.isFinite(value)) {
|
|
46
|
+
const nextDelta = value - previousTarget;
|
|
47
|
+
setDelta(nextDelta);
|
|
48
|
+
if (deltaTimerRef.current !== null)
|
|
49
|
+
clearTimeout(deltaTimerRef.current);
|
|
50
|
+
if (deltaDuration > 0 && Number.isFinite(deltaDuration)) {
|
|
51
|
+
deltaTimerRef.current = setTimeout(() => {
|
|
52
|
+
deltaTimerRef.current = null;
|
|
53
|
+
setDelta(null);
|
|
54
|
+
}, deltaDuration);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (!showDelta) {
|
|
58
|
+
setDelta(null);
|
|
59
|
+
}
|
|
60
|
+
if (frameRef.current !== null) {
|
|
61
|
+
cancelAnimationFrame(frameRef.current);
|
|
62
|
+
frameRef.current = null;
|
|
63
|
+
}
|
|
64
|
+
const reduceImmediately = reducedMotion
|
|
65
|
+
|| (typeof window !== 'undefined'
|
|
66
|
+
&& typeof window.matchMedia === 'function'
|
|
67
|
+
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|
68
|
+
const from = displayedRef.current;
|
|
69
|
+
if (!animate || reduceImmediately || !canAnimateTickerValue(from, value, duration)) {
|
|
70
|
+
displayedRef.current = value;
|
|
71
|
+
setDisplayedValue(value);
|
|
72
|
+
setAnnouncedValue(value);
|
|
73
|
+
setAnimating(false);
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
let startTime = null;
|
|
77
|
+
setAnimating(true);
|
|
78
|
+
const tick = (time) => {
|
|
79
|
+
if (startTime === null)
|
|
80
|
+
startTime = time;
|
|
81
|
+
const progress = Math.min(1, Math.max(0, (time - startTime) / duration));
|
|
82
|
+
const next = interpolateTickerValue(from, value, easing(progress));
|
|
83
|
+
displayedRef.current = next;
|
|
84
|
+
setDisplayedValue(next);
|
|
85
|
+
if (progress < 1) {
|
|
86
|
+
frameRef.current = requestAnimationFrame(tick);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
frameRef.current = null;
|
|
90
|
+
displayedRef.current = value;
|
|
91
|
+
setDisplayedValue(value);
|
|
92
|
+
setAnnouncedValue(value);
|
|
93
|
+
setAnimating(false);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
frameRef.current = requestAnimationFrame(tick);
|
|
97
|
+
return () => {
|
|
98
|
+
if (frameRef.current !== null) {
|
|
99
|
+
cancelAnimationFrame(frameRef.current);
|
|
100
|
+
frameRef.current = null;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}, [animate, deltaDuration, duration, easing, reducedMotion, showDelta, value]);
|
|
104
|
+
useEffect(() => () => {
|
|
105
|
+
if (frameRef.current !== null)
|
|
106
|
+
cancelAnimationFrame(frameRef.current);
|
|
107
|
+
if (deltaTimerRef.current !== null)
|
|
108
|
+
clearTimeout(deltaTimerRef.current);
|
|
109
|
+
}, []);
|
|
110
|
+
const formattedValue = formatTickerValue(formatter, displayedValue);
|
|
111
|
+
const finalFormattedValue = formatTickerValue(formatter, value);
|
|
112
|
+
const announcedFormattedValue = formatTickerValue(formatter, announcedValue);
|
|
113
|
+
const defaultAccessibleLabel = [accessibleAffix(prefix), announcedFormattedValue, accessibleAffix(suffix)]
|
|
114
|
+
.filter(Boolean).join(' ');
|
|
115
|
+
const accessibleLabel = getValueLabel?.(announcedValue, announcedFormattedValue) ?? defaultAccessibleLabel;
|
|
116
|
+
const valueContent = renderValue?.(displayedValue, { animating, formattedValue })
|
|
117
|
+
?? (animating
|
|
118
|
+
? _jsx("span", { className: "inkra-number-ticker__native-value", children: formattedValue })
|
|
119
|
+
: _jsx(PencilText, { seed: `number-ticker-${finalFormattedValue}`, children: finalFormattedValue }));
|
|
120
|
+
const deltaContent = delta === null ? null : (() => {
|
|
121
|
+
const formattedDelta = formatTickerDelta(deltaFormatter, delta);
|
|
122
|
+
const details = {
|
|
123
|
+
direction: delta < 0 ? 'decrease' : 'increase',
|
|
124
|
+
formattedDelta,
|
|
125
|
+
};
|
|
126
|
+
return renderDelta?.(delta, details) ?? _jsx(PencilText, { seed: `number-ticker-delta-${formattedDelta}`, children: formattedDelta });
|
|
127
|
+
})();
|
|
128
|
+
const classes = ['inkra-number-ticker', animating && 'inkra-number-ticker--animating', className]
|
|
129
|
+
.filter(Boolean).join(' ');
|
|
130
|
+
return _jsxs("span", { ...props, className: classes, ref: rootRef, "data-animating": animating || undefined, children: [_jsxs("span", { "aria-hidden": "true", className: "inkra-number-ticker__visual", children: [prefix !== undefined && _jsx("span", { className: "inkra-number-ticker__prefix", children: lettering(prefix, 'number-ticker-prefix') }), _jsx("span", { className: "inkra-number-ticker__value", children: valueContent }), suffix !== undefined && _jsx("span", { className: "inkra-number-ticker__suffix", children: lettering(suffix, 'number-ticker-suffix') }), deltaContent !== null && _jsx("span", { className: "inkra-number-ticker__delta", "data-direction": delta < 0 ? 'decrease' : 'increase', children: deltaContent })] }), _jsx("span", { className: "inkra-number-ticker__announcement", "aria-atomic": "true", "aria-live": "polite", children: accessibleLabel })] });
|
|
131
|
+
});
|
|
132
|
+
export { createNumberFormatter, easeOutCubic, formatTickerDelta, formatTickerValue, interpolateTickerValue, } from './model.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type NumberTickerLocale = string | readonly string[];
|
|
2
|
+
export declare function createNumberFormatter(locale?: NumberTickerLocale, options?: Intl.NumberFormatOptions): Intl.NumberFormat;
|
|
3
|
+
export declare function formatTickerValue(formatter: Intl.NumberFormat, value: number): string;
|
|
4
|
+
export declare function formatTickerDelta(formatter: Intl.NumberFormat, delta: number): string;
|
|
5
|
+
export declare function interpolateTickerValue(from: number, to: number, progress: number): number;
|
|
6
|
+
export declare function tickerValueChanged(previous: number, next: number): boolean;
|
|
7
|
+
export declare function canAnimateTickerValue(from: number, to: number, duration: number): boolean;
|
|
8
|
+
export declare function easeOutCubic(progress: number): number;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function createNumberFormatter(locale = 'en-US', options) {
|
|
2
|
+
return new Intl.NumberFormat(locale, options);
|
|
3
|
+
}
|
|
4
|
+
export function formatTickerValue(formatter, value) {
|
|
5
|
+
return formatter.format(value);
|
|
6
|
+
}
|
|
7
|
+
export function formatTickerDelta(formatter, delta) {
|
|
8
|
+
const sign = delta > 0 ? '+' : delta < 0 ? '\u2212' : '';
|
|
9
|
+
const magnitude = formatter.formatToParts(Math.abs(delta))
|
|
10
|
+
.filter(part => part.type !== 'plusSign' && part.type !== 'minusSign')
|
|
11
|
+
.map(part => part.value)
|
|
12
|
+
.join('');
|
|
13
|
+
return `${sign}${magnitude}`;
|
|
14
|
+
}
|
|
15
|
+
export function interpolateTickerValue(from, to, progress) {
|
|
16
|
+
if (!Number.isFinite(from) || !Number.isFinite(to))
|
|
17
|
+
return to;
|
|
18
|
+
const boundedProgress = Math.min(1, Math.max(0, progress));
|
|
19
|
+
return from + (to - from) * boundedProgress;
|
|
20
|
+
}
|
|
21
|
+
export function tickerValueChanged(previous, next) {
|
|
22
|
+
return !Object.is(previous, next);
|
|
23
|
+
}
|
|
24
|
+
export function canAnimateTickerValue(from, to, duration) {
|
|
25
|
+
return Number.isFinite(from)
|
|
26
|
+
&& Number.isFinite(to)
|
|
27
|
+
&& Number.isFinite(duration)
|
|
28
|
+
&& duration > 0
|
|
29
|
+
&& tickerValueChanged(from, to);
|
|
30
|
+
}
|
|
31
|
+
export function easeOutCubic(progress) {
|
|
32
|
+
const boundedProgress = Math.min(1, Math.max(0, progress));
|
|
33
|
+
return 1 - (1 - boundedProgress) ** 3;
|
|
34
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CSSProperties, HTMLAttributes, ReactElement, ReactNode, RefAttributes } from 'react';
|
|
2
|
+
import type { ButtonProps } from '../Button.js';
|
|
3
|
+
import type { OverflowListStrategy } from './layout.js';
|
|
4
|
+
export type { OverflowListStrategy } from './layout.js';
|
|
5
|
+
export interface OverflowListItem {
|
|
6
|
+
id: string | number;
|
|
7
|
+
label: ReactNode;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
priority?: number;
|
|
10
|
+
measureKey?: string | number;
|
|
11
|
+
}
|
|
12
|
+
export interface OverflowListRenderContext {
|
|
13
|
+
id: string | number;
|
|
14
|
+
index: number;
|
|
15
|
+
}
|
|
16
|
+
export interface OverflowListOverflowContext<Item extends OverflowListItem> {
|
|
17
|
+
count: number;
|
|
18
|
+
items: readonly Item[];
|
|
19
|
+
}
|
|
20
|
+
export interface OverflowListChange<Item extends OverflowListItem> {
|
|
21
|
+
visibleItems: readonly Item[];
|
|
22
|
+
overflowItems: readonly Item[];
|
|
23
|
+
}
|
|
24
|
+
type OverflowButtonProps = Omit<ButtonProps, 'aria-label' | 'children' | 'ref'>;
|
|
25
|
+
export interface OverflowListProps<Item extends OverflowListItem = OverflowListItem> extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onChange'> {
|
|
26
|
+
items: readonly Item[];
|
|
27
|
+
strategy?: OverflowListStrategy;
|
|
28
|
+
measurementKey?: string | number;
|
|
29
|
+
gap?: CSSProperties['gap'];
|
|
30
|
+
renderItem?: (item: Item, context: OverflowListRenderContext) => ReactNode;
|
|
31
|
+
renderOverflowItem?: (item: Item, context: OverflowListRenderContext) => ReactNode;
|
|
32
|
+
renderOverflow?: (context: OverflowListOverflowContext<Item>) => ReactNode;
|
|
33
|
+
getOverflowItemIcon?: (item: Item, context: OverflowListRenderContext) => ReactNode;
|
|
34
|
+
overflowLabel?: string | ((count: number) => string);
|
|
35
|
+
overflowButtonProps?: OverflowButtonProps;
|
|
36
|
+
onOverflowItemSelect?: (item: Item, event: Event) => void;
|
|
37
|
+
onOverflowChange?: (change: OverflowListChange<Item>) => void;
|
|
38
|
+
}
|
|
39
|
+
export declare const OverflowList: <Item extends OverflowListItem = OverflowListItem>(props: OverflowListProps<Item> & RefAttributes<HTMLDivElement>) => ReactElement;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { Button } from '../Button.js';
|
|
4
|
+
import { DropdownMenu } from '../dropdown-menu/index.js';
|
|
5
|
+
import { PencilIcon } from '../PencilIcon.js';
|
|
6
|
+
import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
|
|
7
|
+
import { calculateOverflowListLayout } from './layout.js';
|
|
8
|
+
const ellipsisPaths = [{ d: 'M5 12h.01M12 12h.01M19 12h.01' }];
|
|
9
|
+
function itemKey(id) {
|
|
10
|
+
return `${typeof id}:${String(id)}`;
|
|
11
|
+
}
|
|
12
|
+
function finitePriority(value) {
|
|
13
|
+
if (value === Number.POSITIVE_INFINITY)
|
|
14
|
+
return Number.MAX_SAFE_INTEGER;
|
|
15
|
+
if (value === Number.NEGATIVE_INFINITY)
|
|
16
|
+
return Number.MIN_SAFE_INTEGER;
|
|
17
|
+
return Number.isFinite(value) ? value : 0;
|
|
18
|
+
}
|
|
19
|
+
function sameKeys(current, next) {
|
|
20
|
+
if (current.size !== next.length)
|
|
21
|
+
return false;
|
|
22
|
+
return next.every(key => current.has(key));
|
|
23
|
+
}
|
|
24
|
+
function assignRef(ref, value) {
|
|
25
|
+
if (typeof ref === 'function')
|
|
26
|
+
ref(value);
|
|
27
|
+
else if (ref)
|
|
28
|
+
ref.current = value;
|
|
29
|
+
}
|
|
30
|
+
function defaultOverflowLabel(count) {
|
|
31
|
+
return `More items (${count})`;
|
|
32
|
+
}
|
|
33
|
+
function OverflowListInner({ 'aria-label': ariaLabel, className, gap, getOverflowItemIcon, items, measurementKey = '', onOverflowChange, onOverflowItemSelect, overflowButtonProps, overflowLabel = defaultOverflowLabel, renderItem, renderOverflow, renderOverflowItem, role, strategy = 'end', style, ...props }, forwardedRef) {
|
|
34
|
+
const generatedId = useId();
|
|
35
|
+
const rootRef = useRef(null);
|
|
36
|
+
const triggerRef = useRef(null);
|
|
37
|
+
const itemRefs = useRef(new Map());
|
|
38
|
+
const widthCache = useRef(new Map());
|
|
39
|
+
const frameRef = useRef(null);
|
|
40
|
+
const focusOverflowRef = useRef(false);
|
|
41
|
+
const lastChangeRef = useRef('');
|
|
42
|
+
const onOverflowChangeRef = useRef(onOverflowChange);
|
|
43
|
+
onOverflowChangeRef.current = onOverflowChange;
|
|
44
|
+
const entries = useMemo(() => {
|
|
45
|
+
const nextEntries = items.map((item, index) => ({
|
|
46
|
+
item,
|
|
47
|
+
index,
|
|
48
|
+
key: itemKey(item.id),
|
|
49
|
+
priority: finitePriority(item.priority),
|
|
50
|
+
}));
|
|
51
|
+
const duplicateCheck = new Set();
|
|
52
|
+
for (const entry of nextEntries) {
|
|
53
|
+
if (duplicateCheck.has(entry.key)) {
|
|
54
|
+
throw new Error(`OverflowList item ids must be unique. Received duplicate id: ${String(entry.item.id)}`);
|
|
55
|
+
}
|
|
56
|
+
duplicateCheck.add(entry.key);
|
|
57
|
+
}
|
|
58
|
+
return nextEntries;
|
|
59
|
+
}, [items]);
|
|
60
|
+
const signature = useMemo(() => JSON.stringify([
|
|
61
|
+
measurementKey,
|
|
62
|
+
strategy,
|
|
63
|
+
entries.map(entry => [entry.key, entry.item.measureKey ?? entry.key, entry.priority]),
|
|
64
|
+
]), [entries, measurementKey, strategy]);
|
|
65
|
+
const allKeys = useMemo(() => entries.map(entry => entry.key), [entries]);
|
|
66
|
+
const [layout, setLayout] = useState(() => ({
|
|
67
|
+
signature,
|
|
68
|
+
visibleKeys: new Set(allKeys),
|
|
69
|
+
needsOverflow: false,
|
|
70
|
+
measuring: true,
|
|
71
|
+
}));
|
|
72
|
+
const effectiveLayout = layout.signature === signature
|
|
73
|
+
? layout
|
|
74
|
+
: { signature, visibleKeys: new Set(allKeys), needsOverflow: false, measuring: true };
|
|
75
|
+
const visibleEntries = useMemo(() => effectiveLayout.measuring
|
|
76
|
+
? entries
|
|
77
|
+
: entries.filter(entry => effectiveLayout.visibleKeys.has(entry.key)), [effectiveLayout.measuring, effectiveLayout.visibleKeys, entries]);
|
|
78
|
+
const overflowEntries = useMemo(() => effectiveLayout.measuring
|
|
79
|
+
? []
|
|
80
|
+
: entries.filter(entry => !effectiveLayout.visibleKeys.has(entry.key)), [effectiveLayout.measuring, effectiveLayout.visibleKeys, entries]);
|
|
81
|
+
const setRootRef = useCallback((node) => {
|
|
82
|
+
rootRef.current = node;
|
|
83
|
+
assignRef(forwardedRef, node);
|
|
84
|
+
}, [forwardedRef]);
|
|
85
|
+
const setItemRef = useCallback((key, node) => {
|
|
86
|
+
if (node)
|
|
87
|
+
itemRefs.current.set(key, node);
|
|
88
|
+
else
|
|
89
|
+
itemRefs.current.delete(key);
|
|
90
|
+
}, []);
|
|
91
|
+
const updateLayout = useCallback(() => {
|
|
92
|
+
const root = rootRef.current;
|
|
93
|
+
if (!root)
|
|
94
|
+
return;
|
|
95
|
+
const containerWidth = root.getBoundingClientRect().width;
|
|
96
|
+
if (containerWidth <= 0)
|
|
97
|
+
return;
|
|
98
|
+
const computedGap = Number.parseFloat(getComputedStyle(root).columnGap) || 0;
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
const node = itemRefs.current.get(entry.key);
|
|
101
|
+
if (!node)
|
|
102
|
+
continue;
|
|
103
|
+
const width = node.getBoundingClientRect().width;
|
|
104
|
+
if (width > 0)
|
|
105
|
+
widthCache.current.set(entry.key, width);
|
|
106
|
+
}
|
|
107
|
+
if (entries.some(entry => !widthCache.current.has(entry.key)))
|
|
108
|
+
return;
|
|
109
|
+
const triggerWidth = triggerRef.current?.getBoundingClientRect().width;
|
|
110
|
+
const result = calculateOverflowListLayout({
|
|
111
|
+
containerWidth,
|
|
112
|
+
gap: computedGap,
|
|
113
|
+
items: entries.map(entry => ({
|
|
114
|
+
key: entry.key,
|
|
115
|
+
priority: entry.priority,
|
|
116
|
+
width: widthCache.current.get(entry.key) ?? 0,
|
|
117
|
+
})),
|
|
118
|
+
strategy,
|
|
119
|
+
triggerWidth,
|
|
120
|
+
});
|
|
121
|
+
if (result.needsTriggerMeasurement) {
|
|
122
|
+
setLayout(current => current.signature === signature
|
|
123
|
+
&& current.needsOverflow
|
|
124
|
+
&& current.measuring
|
|
125
|
+
&& sameKeys(current.visibleKeys, allKeys)
|
|
126
|
+
? current
|
|
127
|
+
: { signature, visibleKeys: new Set(allKeys), needsOverflow: true, measuring: true });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const activeElement = typeof document === 'undefined' ? null : document.activeElement;
|
|
131
|
+
if (activeElement && result.overflowKeys.some(key => itemRefs.current.get(key)?.contains(activeElement))) {
|
|
132
|
+
focusOverflowRef.current = true;
|
|
133
|
+
}
|
|
134
|
+
setLayout(current => current.signature === signature
|
|
135
|
+
&& !current.measuring
|
|
136
|
+
&& current.needsOverflow === (result.overflowKeys.length > 0)
|
|
137
|
+
&& sameKeys(current.visibleKeys, result.visibleKeys)
|
|
138
|
+
? current
|
|
139
|
+
: {
|
|
140
|
+
signature,
|
|
141
|
+
visibleKeys: new Set(result.visibleKeys),
|
|
142
|
+
needsOverflow: result.overflowKeys.length > 0,
|
|
143
|
+
measuring: false,
|
|
144
|
+
});
|
|
145
|
+
}, [allKeys, entries, signature, strategy]);
|
|
146
|
+
const scheduleLayout = useCallback(() => {
|
|
147
|
+
if (frameRef.current !== null || typeof window === 'undefined')
|
|
148
|
+
return;
|
|
149
|
+
frameRef.current = window.requestAnimationFrame(() => {
|
|
150
|
+
frameRef.current = null;
|
|
151
|
+
updateLayout();
|
|
152
|
+
});
|
|
153
|
+
}, [updateLayout]);
|
|
154
|
+
useBrowserLayoutEffect(() => {
|
|
155
|
+
if (layout.signature === signature)
|
|
156
|
+
return;
|
|
157
|
+
widthCache.current.clear();
|
|
158
|
+
setLayout({ signature, visibleKeys: new Set(allKeys), needsOverflow: false, measuring: true });
|
|
159
|
+
}, [allKeys, layout.signature, signature]);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
const root = rootRef.current;
|
|
162
|
+
if (!root)
|
|
163
|
+
return undefined;
|
|
164
|
+
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(scheduleLayout);
|
|
165
|
+
observer?.observe(root);
|
|
166
|
+
itemRefs.current.forEach(node => observer?.observe(node));
|
|
167
|
+
if (triggerRef.current)
|
|
168
|
+
observer?.observe(triggerRef.current);
|
|
169
|
+
if (!observer)
|
|
170
|
+
window.addEventListener('resize', scheduleLayout);
|
|
171
|
+
scheduleLayout();
|
|
172
|
+
return () => {
|
|
173
|
+
observer?.disconnect();
|
|
174
|
+
window.removeEventListener('resize', scheduleLayout);
|
|
175
|
+
if (frameRef.current !== null) {
|
|
176
|
+
window.cancelAnimationFrame(frameRef.current);
|
|
177
|
+
frameRef.current = null;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}, [effectiveLayout.measuring, effectiveLayout.needsOverflow, scheduleLayout, signature]);
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
const fonts = typeof document === 'undefined' ? undefined : document.fonts;
|
|
183
|
+
if (!fonts?.addEventListener)
|
|
184
|
+
return undefined;
|
|
185
|
+
const remeasure = () => {
|
|
186
|
+
widthCache.current.clear();
|
|
187
|
+
setLayout({ signature, visibleKeys: new Set(allKeys), needsOverflow: effectiveLayout.needsOverflow, measuring: true });
|
|
188
|
+
};
|
|
189
|
+
fonts.addEventListener('loadingdone', remeasure);
|
|
190
|
+
return () => fonts.removeEventListener('loadingdone', remeasure);
|
|
191
|
+
}, [allKeys, effectiveLayout.needsOverflow, signature]);
|
|
192
|
+
useBrowserLayoutEffect(() => {
|
|
193
|
+
if (!focusOverflowRef.current || !effectiveLayout.needsOverflow || effectiveLayout.measuring)
|
|
194
|
+
return;
|
|
195
|
+
focusOverflowRef.current = false;
|
|
196
|
+
triggerRef.current?.focus();
|
|
197
|
+
}, [effectiveLayout.measuring, effectiveLayout.needsOverflow, effectiveLayout.visibleKeys]);
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (effectiveLayout.measuring)
|
|
200
|
+
return;
|
|
201
|
+
const changeKey = `${signature}:${visibleEntries.map(entry => entry.key).join('|')}`;
|
|
202
|
+
if (lastChangeRef.current === changeKey)
|
|
203
|
+
return;
|
|
204
|
+
lastChangeRef.current = changeKey;
|
|
205
|
+
onOverflowChangeRef.current?.({
|
|
206
|
+
visibleItems: visibleEntries.map(entry => entry.item),
|
|
207
|
+
overflowItems: overflowEntries.map(entry => entry.item),
|
|
208
|
+
});
|
|
209
|
+
}, [effectiveLayout.measuring, overflowEntries, signature, visibleEntries]);
|
|
210
|
+
const resolvedLabel = typeof overflowLabel === 'function'
|
|
211
|
+
? overflowLabel(overflowEntries.length)
|
|
212
|
+
: overflowLabel;
|
|
213
|
+
const overflowContext = useMemo(() => ({
|
|
214
|
+
count: overflowEntries.length,
|
|
215
|
+
items: overflowEntries.map(entry => entry.item),
|
|
216
|
+
}), [overflowEntries]);
|
|
217
|
+
const { className: overflowButtonClassName, size: overflowButtonSize, title: overflowButtonTitle, variant: overflowButtonVariant, ...overflowButtonRest } = overflowButtonProps ?? {};
|
|
218
|
+
const resolvedStyle = {
|
|
219
|
+
...style,
|
|
220
|
+
...(gap !== undefined ? { '--inkra-overflow-list-gap': typeof gap === 'number' ? `${gap}px` : String(gap) } : {}),
|
|
221
|
+
};
|
|
222
|
+
return _jsxs("div", { ...props, "aria-label": ariaLabel, className: ['inkra-overflow-list', className].filter(Boolean).join(' '), "data-measuring": effectiveLayout.measuring || undefined, ref: setRootRef, role: role, style: resolvedStyle, children: [visibleEntries.map(({ item, index, key }) => _jsx("div", { className: "inkra-overflow-list__item", "data-overflow-list-item": String(item.id), ref: node => setItemRef(key, node), children: renderItem
|
|
223
|
+
? renderItem(item, { id: item.id, index })
|
|
224
|
+
: _jsx("span", { className: "inkra-overflow-list__default-item", children: item.label }) }, key)), effectiveLayout.needsOverflow ? _jsxs(DropdownMenu.Root, { children: [_jsx(DropdownMenu.Trigger, { asChild: true, children: _jsx(Button, { ...overflowButtonRest, "aria-label": resolvedLabel, className: ['inkra-overflow-list__trigger', overflowButtonClassName].filter(Boolean).join(' '), ref: triggerRef, size: overflowButtonSize ?? (renderOverflow ? 'sm' : 'icon-sm'), title: overflowButtonTitle ?? resolvedLabel, variant: overflowButtonVariant ?? 'outline', children: renderOverflow
|
|
225
|
+
? renderOverflow(overflowContext)
|
|
226
|
+
: _jsx(PencilIcon, { paths: ellipsisPaths, seed: `${generatedId}-overflow`, size: 16, sourceSize: 24 }) }) }), _jsx(DropdownMenu.Portal, { children: _jsx(DropdownMenu.Content, { align: "end", "aria-label": resolvedLabel, children: overflowEntries.map(({ item, index, key }) => _jsx(DropdownMenu.Item, { disabled: item.disabled, icon: getOverflowItemIcon?.(item, { id: item.id, index }), onSelect: event => onOverflowItemSelect?.(item, event), children: renderOverflowItem
|
|
227
|
+
? renderOverflowItem(item, { id: item.id, index })
|
|
228
|
+
: item.label }, key)) }) })] }) : null] });
|
|
229
|
+
}
|
|
230
|
+
export const OverflowList = forwardRef(OverflowListInner);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type OverflowListStrategy = 'end' | 'start' | 'priority';
|
|
2
|
+
export interface OverflowListLayoutItem {
|
|
3
|
+
key: string;
|
|
4
|
+
priority: number;
|
|
5
|
+
width: number;
|
|
6
|
+
}
|
|
7
|
+
export interface OverflowListLayoutInput {
|
|
8
|
+
items: readonly OverflowListLayoutItem[];
|
|
9
|
+
containerWidth: number;
|
|
10
|
+
gap: number;
|
|
11
|
+
strategy: OverflowListStrategy;
|
|
12
|
+
triggerWidth?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface OverflowListLayoutResult {
|
|
15
|
+
visibleKeys: string[];
|
|
16
|
+
overflowKeys: string[];
|
|
17
|
+
needsTriggerMeasurement: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function calculateOverflowListLayout({ containerWidth, gap, items, strategy, triggerWidth, }: OverflowListLayoutInput): OverflowListLayoutResult;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const finiteSize = (value) => Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
2
|
+
function itemsWidth(items, gap) {
|
|
3
|
+
if (items.length === 0)
|
|
4
|
+
return 0;
|
|
5
|
+
return items.reduce((total, item) => total + finiteSize(item.width), 0)
|
|
6
|
+
+ finiteSize(gap) * (items.length - 1);
|
|
7
|
+
}
|
|
8
|
+
function candidateOrder(items, strategy) {
|
|
9
|
+
const indexed = items.map((item, index) => ({ item, index }));
|
|
10
|
+
if (strategy === 'start')
|
|
11
|
+
return indexed.reverse();
|
|
12
|
+
if (strategy === 'priority') {
|
|
13
|
+
return indexed.sort((left, right) => right.item.priority - left.item.priority || left.index - right.index);
|
|
14
|
+
}
|
|
15
|
+
return indexed;
|
|
16
|
+
}
|
|
17
|
+
export function calculateOverflowListLayout({ containerWidth, gap, items, strategy, triggerWidth, }) {
|
|
18
|
+
const availableWidth = finiteSize(containerWidth);
|
|
19
|
+
const resolvedGap = finiteSize(gap);
|
|
20
|
+
const allKeys = items.map(item => item.key);
|
|
21
|
+
if (itemsWidth(items, resolvedGap) <= availableWidth + 0.5) {
|
|
22
|
+
return { visibleKeys: allKeys, overflowKeys: [], needsTriggerMeasurement: false };
|
|
23
|
+
}
|
|
24
|
+
if (triggerWidth === undefined || !Number.isFinite(triggerWidth) || triggerWidth <= 0) {
|
|
25
|
+
return { visibleKeys: allKeys, overflowKeys: [], needsTriggerMeasurement: true };
|
|
26
|
+
}
|
|
27
|
+
const visible = new Set();
|
|
28
|
+
let occupied = finiteSize(triggerWidth);
|
|
29
|
+
for (const { item } of candidateOrder(items, strategy)) {
|
|
30
|
+
const nextWidth = occupied + resolvedGap + finiteSize(item.width);
|
|
31
|
+
if (nextWidth > availableWidth + 0.5) {
|
|
32
|
+
if (strategy === 'priority')
|
|
33
|
+
continue;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
visible.add(item.key);
|
|
37
|
+
occupied = nextWidth;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
visibleKeys: allKeys.filter(key => visible.has(key)),
|
|
41
|
+
overflowKeys: allKeys.filter(key => !visible.has(key)),
|
|
42
|
+
needsTriggerMeasurement: false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react';
|
|
2
|
+
export interface PaginationLabels {
|
|
3
|
+
nav: string;
|
|
4
|
+
first: string;
|
|
5
|
+
previous: string;
|
|
6
|
+
next: string;
|
|
7
|
+
last: string;
|
|
8
|
+
page: (page: number) => string;
|
|
9
|
+
ellipsis: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PaginationProps extends Omit<HTMLAttributes<HTMLElement>, 'children' | 'onChange'> {
|
|
12
|
+
totalPages: number;
|
|
13
|
+
page?: number;
|
|
14
|
+
defaultPage?: number;
|
|
15
|
+
onPageChange?: (page: number) => void;
|
|
16
|
+
siblingCount?: number;
|
|
17
|
+
boundaryCount?: number;
|
|
18
|
+
showFirstLast?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
size?: 'sm' | 'md' | 'lg';
|
|
21
|
+
labels?: Partial<PaginationLabels>;
|
|
22
|
+
}
|
|
23
|
+
export declare const Pagination: import("react").ForwardRefExoticComponent<PaginationProps & import("react").RefAttributes<HTMLElement>>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useState } from 'react';
|
|
3
|
+
import { Button } from '../Button.js';
|
|
4
|
+
import { PencilIcon } from '../PencilIcon.js';
|
|
5
|
+
import { PencilSurface } from '../PencilSurface.js';
|
|
6
|
+
import { PencilText } from '../PencilText.js';
|
|
7
|
+
import { clampPage, getPaginationEntries, normalizeTotal } from './model.js';
|
|
8
|
+
const defaultLabels = {
|
|
9
|
+
nav: 'Pagination', first: 'First page', previous: 'Previous page', next: 'Next page', last: 'Last page',
|
|
10
|
+
page: page => `Page ${page}`, ellipsis: 'More pages',
|
|
11
|
+
};
|
|
12
|
+
export const Pagination = forwardRef(function Pagination({ totalPages, page, defaultPage = 1, onPageChange, siblingCount = 1, boundaryCount = 1, showFirstLast = false, disabled = false, size = 'md', labels, className, ...props }, ref) {
|
|
13
|
+
const total = normalizeTotal(totalPages);
|
|
14
|
+
const [internalPage, setInternalPage] = useState(() => clampPage(defaultPage, total));
|
|
15
|
+
const current = clampPage(page ?? internalPage, total);
|
|
16
|
+
const text = { ...defaultLabels, ...labels };
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (page === undefined)
|
|
19
|
+
setInternalPage(value => clampPage(value, total));
|
|
20
|
+
}, [page, total]);
|
|
21
|
+
function select(next) {
|
|
22
|
+
const target = clampPage(next, total);
|
|
23
|
+
if (disabled || !total || target === current)
|
|
24
|
+
return;
|
|
25
|
+
if (page === undefined)
|
|
26
|
+
setInternalPage(target);
|
|
27
|
+
onPageChange?.(target);
|
|
28
|
+
}
|
|
29
|
+
const arrow = (kind, target, unavailable) => {
|
|
30
|
+
const backwards = kind === 'first' || kind === 'previous';
|
|
31
|
+
const paths = [{ d: backwards ? 'm15 18-6-6 6-6' : 'm9 18 6-6-6-6' }];
|
|
32
|
+
if (kind === 'first')
|
|
33
|
+
paths.push({ d: 'M5 6v12' });
|
|
34
|
+
if (kind === 'last')
|
|
35
|
+
paths.push({ d: 'M19 6v12' });
|
|
36
|
+
return _jsx("li", { children: _jsx(Button, { variant: "ghost", size: size === 'md' ? 'icon' : `icon-${size}`, "aria-label": text[kind], title: text[kind], disabled: disabled || unavailable, onClick: () => select(target), children: _jsx(PencilIcon, { className: "inkra-pagination__arrow", paths: paths, size: 20, sourceSize: 24 }) }) }, kind);
|
|
37
|
+
};
|
|
38
|
+
return _jsx("nav", { "aria-label": text.nav, ...props, ref: ref, className: ['inkra-pagination', className].filter(Boolean).join(' '), "data-empty": !total || undefined, children: _jsxs("ul", { className: "inkra-pagination__list", children: [showFirstLast && arrow('first', 1, current <= 1), arrow('previous', current - 1, current <= 1), getPaginationEntries(total, current, siblingCount, boundaryCount).map(entry => typeof entry === 'number'
|
|
39
|
+
? _jsx("li", { children: _jsxs(Button, { size: size, variant: "ghost", className: "inkra-pagination__page", "aria-label": text.page(entry), "aria-current": entry === current ? 'page' : undefined, disabled: disabled, onClick: () => select(entry), children: [entry === current && _jsx(PencilSurface, { as: "span", "aria-hidden": "true", variant: "selection", className: "inkra-pagination__selection", seed: `page-${entry}` }), _jsx(PencilText, { children: String(entry) })] }) }, entry)
|
|
40
|
+
: _jsxs("li", { className: "inkra-pagination__ellipsis", children: [_jsx("span", { className: "inkra-pagination__sr-only", children: text.ellipsis }), _jsx(PencilText, { "aria-hidden": "true", children: "..." })] }, entry)), arrow('next', current + 1, !total || current >= total), showFirstLast && arrow('last', total, !total || current >= total)] }) });
|
|
41
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type PaginationEntry = number | 'ellipsis-start' | 'ellipsis-end';
|
|
2
|
+
export declare function normalizeTotal(total: number): number;
|
|
3
|
+
export declare function clampPage(page: number, total: number): number;
|
|
4
|
+
export declare function getPaginationEntries(total: number, page: number, siblings?: number, boundaries?: number): PaginationEntry[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function normalizeTotal(total) {
|
|
2
|
+
return Number.isFinite(total) ? Math.max(0, Math.min(Number.MAX_SAFE_INTEGER, Math.floor(total))) : 0;
|
|
3
|
+
}
|
|
4
|
+
export function clampPage(page, total) {
|
|
5
|
+
if (total === 0)
|
|
6
|
+
return 0;
|
|
7
|
+
return Math.min(total, Math.max(1, Number.isFinite(page) ? Math.floor(page) : 1));
|
|
8
|
+
}
|
|
9
|
+
function windowCount(value) {
|
|
10
|
+
return Number.isFinite(value) ? Math.max(0, Math.min(3, Math.floor(value))) : 1;
|
|
11
|
+
}
|
|
12
|
+
export function getPaginationEntries(total, page, siblings = 1, boundaries = 1) {
|
|
13
|
+
total = normalizeTotal(total);
|
|
14
|
+
if (!total)
|
|
15
|
+
return [];
|
|
16
|
+
page = clampPage(page, total);
|
|
17
|
+
siblings = windowCount(siblings);
|
|
18
|
+
boundaries = windowCount(boundaries);
|
|
19
|
+
const visible = new Set([page]);
|
|
20
|
+
for (let offset = 1; offset <= boundaries; offset += 1) {
|
|
21
|
+
visible.add(offset);
|
|
22
|
+
visible.add(total - offset + 1);
|
|
23
|
+
}
|
|
24
|
+
for (let offset = -siblings; offset <= siblings; offset += 1)
|
|
25
|
+
visible.add(page + offset);
|
|
26
|
+
const pages = [...visible].filter(value => value >= 1 && value <= total).sort((a, b) => a - b);
|
|
27
|
+
const entries = [];
|
|
28
|
+
let previous = 0;
|
|
29
|
+
for (const value of pages) {
|
|
30
|
+
if (previous && value - previous === 2)
|
|
31
|
+
entries.push(previous + 1);
|
|
32
|
+
else if (previous && value - previous > 2)
|
|
33
|
+
entries.push(value <= page ? 'ellipsis-start' : 'ellipsis-end');
|
|
34
|
+
else if (!previous && value > 1)
|
|
35
|
+
entries.push('ellipsis-start');
|
|
36
|
+
entries.push(value);
|
|
37
|
+
previous = value;
|
|
38
|
+
}
|
|
39
|
+
if (previous < total)
|
|
40
|
+
entries.push('ellipsis-end');
|
|
41
|
+
return entries;
|
|
42
|
+
}
|