@swan-io/lake 2.5.0 → 2.6.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/package.json +1 -1
- package/src/components/TabView.d.ts +9 -1
- package/src/components/TabView.js +67 -33
package/package.json
CHANGED
|
@@ -7,13 +7,21 @@ type Tab = {
|
|
|
7
7
|
icon?: IconName;
|
|
8
8
|
withSeparator?: boolean;
|
|
9
9
|
count?: number;
|
|
10
|
+
} | {
|
|
11
|
+
label: string;
|
|
12
|
+
icon?: IconName;
|
|
13
|
+
withSeparator?: boolean;
|
|
14
|
+
count?: number;
|
|
15
|
+
id: string;
|
|
10
16
|
};
|
|
11
17
|
type Props = {
|
|
18
|
+
activeTabId?: string;
|
|
19
|
+
onChange?: (id: string) => void;
|
|
12
20
|
tabs: Tab[];
|
|
13
21
|
otherLabel: string;
|
|
14
22
|
hideIfSingleItem?: boolean;
|
|
15
23
|
padding?: SpacingValue;
|
|
16
24
|
sticky?: boolean;
|
|
17
25
|
};
|
|
18
|
-
export declare const TabView: ({ tabs, otherLabel, hideIfSingleItem, sticky, padding, }: Props) => import("react/jsx-runtime").JSX.Element | null;
|
|
26
|
+
export declare const TabView: ({ tabs, otherLabel, hideIfSingleItem, sticky, padding, activeTabId, onChange, }: Props) => import("react/jsx-runtime").JSX.Element | null;
|
|
19
27
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useLocation } from "@swan-io/chicane";
|
|
3
3
|
import { Fragment, forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, } from "react";
|
|
4
|
-
import { StyleSheet, Text, View } from "react-native";
|
|
4
|
+
import { StyleSheet, Text, View, } from "react-native";
|
|
5
5
|
import { P, match } from "ts-pattern";
|
|
6
6
|
import { animations, backgroundColor, colors, negativeSpacings, radii, shadows, spacings, texts, } from "../constants/design";
|
|
7
7
|
import { useHover } from "../hooks/useHover";
|
|
@@ -121,24 +121,38 @@ const styles = StyleSheet.create({
|
|
|
121
121
|
top: -1,
|
|
122
122
|
},
|
|
123
123
|
});
|
|
124
|
-
const
|
|
124
|
+
const TabViewLink = forwardRef(({ children, style, tab, onChange, activeTabId, onBlur, onFocus, onPress }, ref) => {
|
|
125
|
+
return match(tab)
|
|
126
|
+
.with({ url: P.string }, ({ url }) => (_jsx(Link, { ref: ref, to: url, style: style, onFocus: onFocus, onBlur: onBlur, onPress: onPress, children: children })))
|
|
127
|
+
.with({ id: P.string }, ({ id }) => {
|
|
128
|
+
const tabId = getTabId(tab);
|
|
129
|
+
const isActive = tabId === activeTabId;
|
|
130
|
+
return (_jsx(PressableText, { ref: ref, style: state => style({ ...state, active: isActive }), onPress: () => {
|
|
131
|
+
onChange?.(id);
|
|
132
|
+
onPress?.();
|
|
133
|
+
}, onFocus: onFocus, onBlur: onBlur, children: children }));
|
|
134
|
+
})
|
|
135
|
+
.exhaustive();
|
|
136
|
+
});
|
|
137
|
+
const Dropdown = ({ tabs, onHoverStart, onHoverEnd, onLinkFocus, onLinkBlur, onLinkPress, activeTabId, onChange, }) => {
|
|
125
138
|
const containerRef = useRef(null);
|
|
126
139
|
useHover(containerRef, {
|
|
127
140
|
onHoverStart,
|
|
128
141
|
onHoverEnd,
|
|
129
142
|
});
|
|
130
|
-
return (_jsx(View, { role: "menu", style: styles.dropdown, ref: containerRef, children: tabs.map(
|
|
131
|
-
|
|
143
|
+
return (_jsx(View, { role: "menu", style: styles.dropdown, ref: containerRef, children: tabs.map(tab => {
|
|
144
|
+
const tabId = getTabId(tab);
|
|
145
|
+
return (_jsx(TabViewLink, { onChange: onChange, activeTabId: activeTabId, tab: tab, onFocus: onLinkFocus, onBlur: onLinkBlur, onPress: onLinkPress, role: "menuitem", ariaCurrentValue: "location", style: ({ active, hovered }) => [
|
|
132
146
|
styles.dropdownLink,
|
|
133
147
|
active && styles.dropdownLinkTextActive,
|
|
134
148
|
hovered && styles.dropdownLinkTextHovered,
|
|
135
|
-
], children: label },
|
|
149
|
+
], children: tab.label }, tabId));
|
|
136
150
|
}) }));
|
|
137
151
|
};
|
|
138
152
|
const SHOULD_AUTOFOCUS = new Set(["ForcedOpen", "OpenFromFocus"]);
|
|
139
153
|
const SHOULD_OPEN = new Set(["Open", "ForcedOpen", "OpenFromFocus"]);
|
|
140
154
|
const SHOULD_LOCK_FOCUS = new Set(["ForcedOpen"]);
|
|
141
|
-
const DropdownItems = forwardRef(({ tabs, otherLabel, currentUrl }, ref) => {
|
|
155
|
+
const DropdownItems = forwardRef(({ tabs, otherLabel, currentUrl, activeTabId, onChange }, ref) => {
|
|
142
156
|
const [openingStatus, dispatch] = useReducer((state, action) => {
|
|
143
157
|
return match([action, state])
|
|
144
158
|
.returnType()
|
|
@@ -251,13 +265,21 @@ const DropdownItems = forwardRef(({ tabs, otherLabel, currentUrl }, ref) => {
|
|
|
251
265
|
onHoverEnd,
|
|
252
266
|
});
|
|
253
267
|
const mergedRef = useMergeRefs(containerRef, ref);
|
|
254
|
-
const activeTab = tabs.find(({
|
|
268
|
+
const activeTab = tabs.find(tab => isTabActive({ activeTabId, currentLocationURL: currentUrl, tab }));
|
|
255
269
|
return (_jsxs(View, { style: styles.dropdownHandleContainer, ref: mergedRef, children: [_jsxs(PressableText, { ref: handleRef, role: "button", "aria-expanded": shouldOpen, "aria-haspopup": "true", onFocus: onHandleFocus, onBlur: onAnyBlur, onPress: onPress, style: ({ hovered }) => [
|
|
256
270
|
styles.link,
|
|
257
271
|
isNotNullish(activeTab) ? styles.activeLink : hovered ? styles.hoveredLink : null,
|
|
258
|
-
], children: [_jsx(Text, { children: otherLabel }), _jsx(Space, { width: 8 }), _jsx(Text, { style: styles.count, children: tabs.length }), _jsx(Space, { width: 4 }), _jsx(Icon, { name: "chevron-down-filled", size: 12 })] }), _jsx(TransitionView, { ...animations.fadeAndSlideInFromBottom, style: styles.dropdownPlacement, children: shouldOpen ? (_jsx(FocusTrap, { autoFocus: shouldAutoFocus, focusLock: shouldLockFocus, returnFocus: shouldLockFocus, onClickOutside: onPressOutside, onEscapeKey: shouldLockFocus ? onEscapeKey : undefined, children: _jsx(Dropdown, { tabs: tabs, onHoverStart: onHoverStart, onHoverEnd: onHoverEnd, onLinkFocus: onLinkFocus, onLinkBlur: onAnyBlur, onLinkPress: onEscapeKey }) })) : null })] }));
|
|
272
|
+
], children: [_jsx(Text, { children: otherLabel }), _jsx(Space, { width: 8 }), _jsx(Text, { style: styles.count, children: tabs.length }), _jsx(Space, { width: 4 }), _jsx(Icon, { name: "chevron-down-filled", size: 12 })] }), _jsx(TransitionView, { ...animations.fadeAndSlideInFromBottom, style: styles.dropdownPlacement, children: shouldOpen ? (_jsx(FocusTrap, { autoFocus: shouldAutoFocus, focusLock: shouldLockFocus, returnFocus: shouldLockFocus, onClickOutside: onPressOutside, onEscapeKey: shouldLockFocus ? onEscapeKey : undefined, children: _jsx(Dropdown, { onChange: onChange, activeTabId: activeTabId, tabs: tabs, onHoverStart: onHoverStart, onHoverEnd: onHoverEnd, onLinkFocus: onLinkFocus, onLinkBlur: onAnyBlur, onLinkPress: onEscapeKey }) })) : null })] }));
|
|
259
273
|
});
|
|
260
|
-
|
|
274
|
+
const isTabActive = ({ tab, activeTabId, currentLocationURL }) => match(tab)
|
|
275
|
+
.with({ url: P.string }, ({ url }) => currentLocationURL.startsWith(url))
|
|
276
|
+
.with({ id: P.string }, ({ id }) => isNotNullish(activeTabId) && id === activeTabId)
|
|
277
|
+
.exhaustive();
|
|
278
|
+
const getTabId = (tab) => match(tab)
|
|
279
|
+
.with({ url: P.string }, ({ url }) => url)
|
|
280
|
+
.with({ id: P.string }, ({ id }) => id)
|
|
281
|
+
.exhaustive();
|
|
282
|
+
export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = false, padding, activeTabId, onChange, }) => {
|
|
261
283
|
const containerRef = useRef(null);
|
|
262
284
|
const placeholderRef = useRef(null);
|
|
263
285
|
const otherPlaceholderRef = useRef(null);
|
|
@@ -273,8 +295,10 @@ export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = fa
|
|
|
273
295
|
if (isNotNullish(linksRefs.current)) {
|
|
274
296
|
const values = Object.entries(linksRefs.current);
|
|
275
297
|
const container = containerRef.current;
|
|
276
|
-
for (const [
|
|
277
|
-
if ("/" + path.join("/") ===
|
|
298
|
+
for (const [tabId, node] of values) {
|
|
299
|
+
if ((tabId === activeTabId || "/" + path.join("/") === tabId) &&
|
|
300
|
+
isNotNullish(node) &&
|
|
301
|
+
isNotNullish(container)) {
|
|
278
302
|
node.measureLayout(container, (left, _, width) => {
|
|
279
303
|
const leftOffset = padding ?? 0;
|
|
280
304
|
setUnderlinePosition({ left: left - leftOffset, width });
|
|
@@ -284,14 +308,15 @@ export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = fa
|
|
|
284
308
|
}
|
|
285
309
|
}
|
|
286
310
|
setUnderlinePosition({ left: 0, width: 0 });
|
|
287
|
-
}, [path, kept, collapsed, padding]);
|
|
311
|
+
}, [path, kept, collapsed, padding, activeTabId]);
|
|
288
312
|
useEffect(() => {
|
|
289
313
|
setHasRendered(width > 0);
|
|
290
314
|
}, [width]);
|
|
291
315
|
const reajustLayout = useCallback(({ width }) => {
|
|
292
316
|
const items = tabs.map(tab => {
|
|
293
317
|
if (placeholderLinkRef.current) {
|
|
294
|
-
const
|
|
318
|
+
const tabId = getTabId(tab);
|
|
319
|
+
const ref = placeholderLinkRef.current[tabId];
|
|
295
320
|
if (isNotNullish(ref)) {
|
|
296
321
|
const element = ref;
|
|
297
322
|
const width = element.getBoundingClientRect().width;
|
|
@@ -314,7 +339,7 @@ export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = fa
|
|
|
314
339
|
kept.push({ ...item.tab, width: item.width });
|
|
315
340
|
}
|
|
316
341
|
else {
|
|
317
|
-
if (currentLocationURL
|
|
342
|
+
if (isTabActive({ activeTabId, currentLocationURL, tab: item.tab })) {
|
|
318
343
|
while (kept.length !== 0 &&
|
|
319
344
|
kept.reduce((acc, item) => acc + item.width, 0) + (item.width + 16) >= width) {
|
|
320
345
|
const last = kept.pop();
|
|
@@ -336,7 +361,7 @@ export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = fa
|
|
|
336
361
|
const otherLabel = otherLabelRef;
|
|
337
362
|
otherLabelWidth = otherLabel.getBoundingClientRect().width;
|
|
338
363
|
}
|
|
339
|
-
const activeInKeptIndex = kept.findIndex(item => currentLocationURL
|
|
364
|
+
const activeInKeptIndex = kept.findIndex(item => isTabActive({ activeTabId, currentLocationURL, tab: item }));
|
|
340
365
|
if (activeInKeptIndex !== -1) {
|
|
341
366
|
const activeInKept = kept[activeInKeptIndex];
|
|
342
367
|
const activeInKeptWidth = activeInKept?.width ?? 0;
|
|
@@ -364,7 +389,7 @@ export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = fa
|
|
|
364
389
|
else {
|
|
365
390
|
setKeptCollapsed([kept, collapsed]);
|
|
366
391
|
}
|
|
367
|
-
}, [tabs, currentLocationURL]);
|
|
392
|
+
}, [tabs, activeTabId, currentLocationURL]);
|
|
368
393
|
const onLayout = useCallback(({ target, nativeEvent: { layout: { width }, }, }) => {
|
|
369
394
|
reajustLayout({ container: target, width });
|
|
370
395
|
}, [reajustLayout]);
|
|
@@ -379,27 +404,36 @@ export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = fa
|
|
|
379
404
|
if (tabs.length <= 1 && hideIfSingleItem) {
|
|
380
405
|
return null;
|
|
381
406
|
}
|
|
382
|
-
return (_jsxs(Box, { alignItems: "center", direction: "row", role: "tablist", ref: containerRef, style: [styles.container, sticky && styles.sticky, { paddingHorizontal: padding }], children: [_jsxs(View, { style: styles.placeholder, "aria-hidden": true, ref: placeholderRef, onLayout: onLayout, children: [tabs.map(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
407
|
+
return (_jsxs(Box, { alignItems: "center", direction: "row", role: "tablist", ref: containerRef, style: [styles.container, sticky && styles.sticky, { paddingHorizontal: padding }], children: [_jsxs(View, { style: styles.placeholder, "aria-hidden": true, ref: placeholderRef, onLayout: onLayout, children: [tabs.map(tab => {
|
|
408
|
+
const { label, icon, count } = tab;
|
|
409
|
+
const tabId = getTabId(tab);
|
|
410
|
+
return (_jsxs(Fragment, { children: [_jsxs(TabViewLink, { ref: ref => {
|
|
411
|
+
if (placeholderLinkRef.current) {
|
|
412
|
+
placeholderLinkRef.current[tabId] = ref;
|
|
413
|
+
}
|
|
414
|
+
}, activeTabId: activeTabId, tab: tab, onChange: onChange, style: ({ active, hovered }) => [
|
|
415
|
+
styles.link,
|
|
416
|
+
active ? styles.activeLink : hovered ? styles.hoveredLink : null,
|
|
417
|
+
], children: [isNotNullish(icon) && (_jsxs(_Fragment, { children: [_jsx(Icon, { name: icon, size: 16, color: "currentColor" }), _jsx(Space, { width: 8 })] })), _jsx(Text, { children: label }), count != null ? (_jsxs(_Fragment, { children: [_jsx(Space, { width: 8 }), _jsx(Text, { style: styles.count, children: count })] })) : null] }), _jsx(Space, { width: 32 })] }, tabId));
|
|
418
|
+
}), _jsxs(LakeText, { ref: otherPlaceholderRef, style: styles.link, children: [_jsx(Text, { children: otherLabel }), _jsx(Space, { width: 8 }), _jsx(Text, { style: styles.count, children: tabs.length }), _jsx(Space, { width: 4 }), _jsx(Icon, { name: "chevron-down-filled", size: 12 })] })] }), kept.map(tab => {
|
|
419
|
+
const { label, icon, withSeparator, count } = tab;
|
|
420
|
+
const tabId = getTabId(tab);
|
|
421
|
+
return (_jsxs(Fragment, { children: [_jsxs(TabViewLink, { ref: ref => {
|
|
422
|
+
if (linksRefs.current) {
|
|
423
|
+
linksRefs.current[tabId] = ref;
|
|
424
|
+
}
|
|
425
|
+
}, onChange: onChange, activeTabId: activeTabId, tab: tab, role: "tab", style: ({ active, hovered }) => [
|
|
426
|
+
styles.link,
|
|
427
|
+
active ? styles.activeLink : hovered ? styles.hoveredLink : null,
|
|
428
|
+
], children: [withSeparator === true && _jsx(View, { style: styles.separator, role: "none" }), isNotNullish(icon) && (_jsxs(_Fragment, { children: [_jsx(Icon, { name: icon, size: 16, color: "currentColor" }), _jsx(Space, { width: 8 })] })), _jsx(Text, { children: label }), count != null ? (_jsxs(_Fragment, { children: [_jsx(Space, { width: 8 }), _jsx(Text, { style: styles.count, children: count })] })) : null] }), _jsx(Space, { width: 32 })] }, tabId));
|
|
429
|
+
}), collapsed.length > 0 ? (_jsx(DropdownItems, { ref: ref => {
|
|
397
430
|
collapsed.forEach(item => {
|
|
398
431
|
if (linksRefs.current) {
|
|
399
|
-
|
|
432
|
+
const tabId = getTabId(item);
|
|
433
|
+
linksRefs.current[tabId] = ref;
|
|
400
434
|
}
|
|
401
435
|
});
|
|
402
|
-
}, tabs: collapsed, currentUrl: currentLocationURL, otherLabel: otherLabel })) : null, hasRendered && (_jsx(View, { style: [
|
|
436
|
+
}, onChange: onChange, tabs: collapsed, currentUrl: currentLocationURL, otherLabel: otherLabel, activeTabId: activeTabId })) : null, hasRendered && (_jsx(View, { style: [
|
|
403
437
|
styles.underline,
|
|
404
438
|
styles.animatedUnderline,
|
|
405
439
|
{ transform: `translateX(${left}px) scaleX(${width})` },
|