@fire-spark/rn-ui 0.5.0 → 0.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/dist/index.d.ts +2 -2
- package/dist/use-active-category.d.ts +43 -13
- package/dist/use-active-category.js +112 -15
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ import { Category, CategoryContent, CategoryDescription, CategoryEmpty, Category
|
|
|
4
4
|
import { Checkout, CheckoutAside, CheckoutBack, CheckoutBanner, CheckoutColumn, CheckoutDescription, CheckoutFeeInfo, CheckoutFeeLabel, CheckoutFeeList, CheckoutFeeRow, CheckoutFeeValue, CheckoutFooter, CheckoutFulfillment, CheckoutFulfillmentOption, CheckoutHeader, CheckoutMain, CheckoutNav, CheckoutNavBrand, CheckoutOption, CheckoutOptionControl, CheckoutOptionLabel, CheckoutPayment, CheckoutPaymentOption, CheckoutPaymentPanel, CheckoutPaymentPanelHeader, CheckoutPromo, CheckoutPromoChevron, CheckoutPromoIcon, CheckoutPromoLabel, CheckoutRow, CheckoutRowAside, CheckoutRowChevron, CheckoutRowContent, CheckoutRowDescription, CheckoutRowMedia, CheckoutRowTitle, CheckoutSection, CheckoutSectionAction, CheckoutSectionHeader, CheckoutSectionTitle, CheckoutStore, CheckoutStoreContent, CheckoutStoreDescription, CheckoutStoreImage, CheckoutStoreMedia, CheckoutStoreTitle, CheckoutSubmit, CheckoutTip, CheckoutTipCustom, CheckoutTipList, CheckoutTipOption, CheckoutTitle } from "./checkout.js";
|
|
5
5
|
import { Menu, MenuEmpty, MenuEmptyContent, MenuEmptyDescription, MenuEmptyHeader, MenuEmptyMedia, MenuEmptyTitle } from "./menu.js";
|
|
6
6
|
import { ProductCard, ProductCardAddButton, ProductCardAnchorPrice, ProductCardBadge, ProductCardContent, ProductCardDescription, ProductCardDiscount, ProductCardFooter, ProductCardHeader, ProductCardImage, ProductCardImageSlot, ProductCardMedia, ProductCardMeta, ProductCardPrice, ProductCardRating, ProductCardTitle } from "./product-card.js";
|
|
7
|
-
import { UseActiveCategoryOptions, useActiveCategory } from "./use-active-category.js";
|
|
8
|
-
export { Cart, CartAction, CartBanner, CartBody, CartDescription, CartEmpty, CartEmptyDescription, CartEmptyTitle, CartFooter, CartHeader, CartItem, CartItemAction, CartItemActions, CartItemAnchorPrice, CartItemAside, CartItemContent, CartItemDescription, CartItemHeader, CartItemImage, CartItemImageSlot, CartItemMedia, CartItemMeta, CartItemPrice, CartItemTitle, CartList, CartOption, CartOptionControl, CartOptionLabel, CartQuantity, CartQuantityButton, CartQuantityValue, CartScroller, CartSection, CartSectionHeader, CartSectionTitle, CartStore, CartStoreContent, CartStoreDescription, CartStoreImage, CartStoreMedia, CartStoreTitle, CartSubmit, CartSummary, CartSummaryLabel, CartSummaryRow, CartSummaryValue, CartTitle, Category, CategoryContent, CategoryDescription, CategoryEmpty, CategoryEmptyContent, CategoryEmptyDescription, CategoryEmptyHeader, CategoryEmptyMedia, CategoryEmptyTitle, CategoryGrid, CategoryGridItem, CategoryHeader, CategoryList, CategoryListItem, CategoryNav, CategoryNavExtra, CategoryNavItem, CategoryNavLink, CategoryNavList, CategoryNavOrientation, CategoryTitle, Checkout, CheckoutAside, CheckoutBack, CheckoutBanner, CheckoutColumn, CheckoutDescription, CheckoutFeeInfo, CheckoutFeeLabel, CheckoutFeeList, CheckoutFeeRow, CheckoutFeeValue, CheckoutFooter, CheckoutFulfillment, CheckoutFulfillmentOption, CheckoutHeader, CheckoutMain, CheckoutNav, CheckoutNavBrand, CheckoutOption, CheckoutOptionControl, CheckoutOptionLabel, CheckoutPayment, CheckoutPaymentOption, CheckoutPaymentPanel, CheckoutPaymentPanelHeader, CheckoutPromo, CheckoutPromoChevron, CheckoutPromoIcon, CheckoutPromoLabel, CheckoutRow, CheckoutRowAside, CheckoutRowChevron, CheckoutRowContent, CheckoutRowDescription, CheckoutRowMedia, CheckoutRowTitle, CheckoutSection, CheckoutSectionAction, CheckoutSectionHeader, CheckoutSectionTitle, CheckoutStore, CheckoutStoreContent, CheckoutStoreDescription, CheckoutStoreImage, CheckoutStoreMedia, CheckoutStoreTitle, CheckoutSubmit, CheckoutTip, CheckoutTipCustom, CheckoutTipList, CheckoutTipOption, CheckoutTitle, Menu, MenuEmpty, MenuEmptyContent, MenuEmptyDescription, MenuEmptyHeader, MenuEmptyMedia, MenuEmptyTitle, ProductCard, ProductCardAddButton, ProductCardAnchorPrice, ProductCardBadge, ProductCardContent, ProductCardDescription, ProductCardDiscount, ProductCardFooter, ProductCardHeader, ProductCardImage, ProductCardImageSlot, ProductCardMedia, ProductCardMeta, ProductCardPrice, ProductCardRating, ProductCardTitle, UseActiveCategoryOptions, useActiveCategory };
|
|
7
|
+
import { ActiveCategoryViewToken, ActiveCategoryViewableInfo, ScrollableList, UseActiveCategoryIndexOptions, UseActiveCategoryOffsetOptions, UseActiveCategoryOptions, useActiveCategory } from "./use-active-category.js";
|
|
8
|
+
export { ActiveCategoryViewToken, ActiveCategoryViewableInfo, Cart, CartAction, CartBanner, CartBody, CartDescription, CartEmpty, CartEmptyDescription, CartEmptyTitle, CartFooter, CartHeader, CartItem, CartItemAction, CartItemActions, CartItemAnchorPrice, CartItemAside, CartItemContent, CartItemDescription, CartItemHeader, CartItemImage, CartItemImageSlot, CartItemMedia, CartItemMeta, CartItemPrice, CartItemTitle, CartList, CartOption, CartOptionControl, CartOptionLabel, CartQuantity, CartQuantityButton, CartQuantityValue, CartScroller, CartSection, CartSectionHeader, CartSectionTitle, CartStore, CartStoreContent, CartStoreDescription, CartStoreImage, CartStoreMedia, CartStoreTitle, CartSubmit, CartSummary, CartSummaryLabel, CartSummaryRow, CartSummaryValue, CartTitle, Category, CategoryContent, CategoryDescription, CategoryEmpty, CategoryEmptyContent, CategoryEmptyDescription, CategoryEmptyHeader, CategoryEmptyMedia, CategoryEmptyTitle, CategoryGrid, CategoryGridItem, CategoryHeader, CategoryList, CategoryListItem, CategoryNav, CategoryNavExtra, CategoryNavItem, CategoryNavLink, CategoryNavList, CategoryNavOrientation, CategoryTitle, Checkout, CheckoutAside, CheckoutBack, CheckoutBanner, CheckoutColumn, CheckoutDescription, CheckoutFeeInfo, CheckoutFeeLabel, CheckoutFeeList, CheckoutFeeRow, CheckoutFeeValue, CheckoutFooter, CheckoutFulfillment, CheckoutFulfillmentOption, CheckoutHeader, CheckoutMain, CheckoutNav, CheckoutNavBrand, CheckoutOption, CheckoutOptionControl, CheckoutOptionLabel, CheckoutPayment, CheckoutPaymentOption, CheckoutPaymentPanel, CheckoutPaymentPanelHeader, CheckoutPromo, CheckoutPromoChevron, CheckoutPromoIcon, CheckoutPromoLabel, CheckoutRow, CheckoutRowAside, CheckoutRowChevron, CheckoutRowContent, CheckoutRowDescription, CheckoutRowMedia, CheckoutRowTitle, CheckoutSection, CheckoutSectionAction, CheckoutSectionHeader, CheckoutSectionTitle, CheckoutStore, CheckoutStoreContent, CheckoutStoreDescription, CheckoutStoreImage, CheckoutStoreMedia, CheckoutStoreTitle, CheckoutSubmit, CheckoutTip, CheckoutTipCustom, CheckoutTipList, CheckoutTipOption, CheckoutTitle, Menu, MenuEmpty, MenuEmptyContent, MenuEmptyDescription, MenuEmptyHeader, MenuEmptyMedia, MenuEmptyTitle, ProductCard, ProductCardAddButton, ProductCardAnchorPrice, ProductCardBadge, ProductCardContent, ProductCardDescription, ProductCardDiscount, ProductCardFooter, ProductCardHeader, ProductCardImage, ProductCardImageSlot, ProductCardMedia, ProductCardMeta, ProductCardPrice, ProductCardRating, ProductCardTitle, ScrollableList, UseActiveCategoryIndexOptions, UseActiveCategoryOffsetOptions, UseActiveCategoryOptions, useActiveCategory };
|
|
@@ -1,29 +1,59 @@
|
|
|
1
1
|
import { LayoutChangeEvent } from "react-native";
|
|
2
2
|
import { RefObject } from "react";
|
|
3
3
|
//#region src/use-active-category.d.ts
|
|
4
|
-
type
|
|
4
|
+
type OffsetScrollable = {
|
|
5
5
|
scrollTo: (opts: {
|
|
6
6
|
y: number;
|
|
7
7
|
animated?: boolean;
|
|
8
8
|
}) => void;
|
|
9
9
|
};
|
|
10
|
-
|
|
10
|
+
/** List ref shared by FlashList, FlatList, and LegendList. No list package. */
|
|
11
|
+
type ScrollableList = {
|
|
12
|
+
scrollToIndex: (params: {
|
|
13
|
+
index: number;
|
|
14
|
+
animated?: boolean;
|
|
15
|
+
viewOffset?: number;
|
|
16
|
+
}) => unknown;
|
|
17
|
+
getFirstVisibleIndex?: () => number;
|
|
18
|
+
};
|
|
19
|
+
type ActiveCategoryViewToken = {
|
|
20
|
+
index: number | null;
|
|
21
|
+
isViewable?: boolean;
|
|
22
|
+
};
|
|
23
|
+
type ActiveCategoryViewableInfo = {
|
|
24
|
+
viewableItems: readonly ActiveCategoryViewToken[];
|
|
25
|
+
};
|
|
26
|
+
type ScrollOffsetEvent = {
|
|
27
|
+
nativeEvent: {
|
|
28
|
+
contentOffset: {
|
|
29
|
+
y: number;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
type UseActiveCategoryOffsetOptions = {
|
|
11
34
|
categoryIds: readonly string[];
|
|
12
35
|
offset?: number;
|
|
13
|
-
scrollRef?: RefObject<
|
|
36
|
+
scrollRef?: RefObject<OffsetScrollable | null>;
|
|
14
37
|
};
|
|
38
|
+
type UseActiveCategoryIndexOptions = {
|
|
39
|
+
/** Section id for every list row (row i belongs to rowSectionIds[i]). */
|
|
40
|
+
rowSectionIds: readonly string[];
|
|
41
|
+
/** Section id → index of its header row. */
|
|
42
|
+
headerIndex: ReadonlyMap<string, number>;
|
|
43
|
+
listRef: RefObject<ScrollableList | null>;
|
|
44
|
+
/** Pixels kept above the header after a jump (sticky chrome). */
|
|
45
|
+
offset?: number;
|
|
46
|
+
/** Section shown as active before the first scroll (deep links). */
|
|
47
|
+
initialCategoryId?: string;
|
|
48
|
+
};
|
|
49
|
+
type UseActiveCategoryOptions = UseActiveCategoryOffsetOptions | UseActiveCategoryIndexOptions;
|
|
15
50
|
declare const useActiveCategory: (opts: UseActiveCategoryOptions) => {
|
|
51
|
+
onViewableItemsChanged: (info: ActiveCategoryViewableInfo) => void;
|
|
52
|
+
selectCategory: (categoryId: string) => void;
|
|
53
|
+
onScroll: (event?: ScrollOffsetEvent) => void;
|
|
54
|
+
registerCategory: (categoryId: string) => (_event: LayoutChangeEvent) => void;
|
|
16
55
|
activeCategoryId: string;
|
|
17
56
|
setActiveCategoryId: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
18
|
-
selectCategory: (categoryId: string) => void;
|
|
19
|
-
onScroll: (event: {
|
|
20
|
-
nativeEvent: {
|
|
21
|
-
contentOffset: {
|
|
22
|
-
y: number;
|
|
23
|
-
};
|
|
24
|
-
};
|
|
25
|
-
}) => void;
|
|
26
|
-
registerCategory: (categoryId: string) => (event: LayoutChangeEvent) => void;
|
|
27
57
|
};
|
|
28
58
|
//#endregion
|
|
29
|
-
export { UseActiveCategoryOptions, useActiveCategory };
|
|
59
|
+
export { ActiveCategoryViewToken, ActiveCategoryViewableInfo, ScrollableList, UseActiveCategoryIndexOptions, UseActiveCategoryOffsetOptions, UseActiveCategoryOptions, useActiveCategory };
|
|
@@ -4,24 +4,56 @@ import { useEffect, useRef, useState } from "react";
|
|
|
4
4
|
const SCROLL_SETTLE_MS = 140;
|
|
5
5
|
/** Fallback if scroll never starts; scroll handler re-arms while moving. */
|
|
6
6
|
const SCROLL_LOCK_FALLBACK_MS = 1e3;
|
|
7
|
+
const EMPTY_IDS = [];
|
|
8
|
+
const EMPTY_HEADERS = /* @__PURE__ */ new Map();
|
|
9
|
+
const EMPTY_LIST_REF = { current: null };
|
|
10
|
+
const isIndexMode = (opts) => {
|
|
11
|
+
return "rowSectionIds" in opts;
|
|
12
|
+
};
|
|
13
|
+
const firstSectionId = (headerIndex, rowSectionIds) => {
|
|
14
|
+
let bestId = "";
|
|
15
|
+
let bestAt = Number.POSITIVE_INFINITY;
|
|
16
|
+
for (const [id, index] of headerIndex) {
|
|
17
|
+
if (index >= bestAt) continue;
|
|
18
|
+
bestAt = index;
|
|
19
|
+
bestId = id;
|
|
20
|
+
}
|
|
21
|
+
if (bestId) return bestId;
|
|
22
|
+
return rowSectionIds[0] ?? "";
|
|
23
|
+
};
|
|
24
|
+
const ignoreLayout = (_event) => {};
|
|
25
|
+
const ignoreViewable = (_info) => {};
|
|
7
26
|
const useActiveCategory = (opts) => {
|
|
8
|
-
const
|
|
9
|
-
const
|
|
27
|
+
const indexMode = isIndexMode(opts);
|
|
28
|
+
const offset = opts.offset ?? 0;
|
|
29
|
+
const categoryIds = indexMode ? EMPTY_IDS : opts.categoryIds;
|
|
30
|
+
const rowSectionIds = indexMode ? opts.rowSectionIds : EMPTY_IDS;
|
|
31
|
+
const headerIndex = indexMode ? opts.headerIndex : EMPTY_HEADERS;
|
|
32
|
+
const listRef = indexMode ? opts.listRef : EMPTY_LIST_REF;
|
|
33
|
+
const scrollRef = indexMode ? void 0 : opts.scrollRef;
|
|
34
|
+
const initialCategoryId = indexMode ? opts.initialCategoryId : void 0;
|
|
35
|
+
const [storedId, setStoredId] = useState(() => {
|
|
36
|
+
if (indexMode) return initialCategoryId ?? "";
|
|
10
37
|
return categoryIds[0] ?? "";
|
|
11
38
|
});
|
|
12
39
|
const offsetsRef = useRef(/* @__PURE__ */ new Map());
|
|
13
40
|
const lockedIdRef = useRef(null);
|
|
14
41
|
const settleTimerRef = useRef(null);
|
|
15
42
|
useEffect(() => {
|
|
43
|
+
if (indexMode) return;
|
|
16
44
|
if (categoryIds.length === 0) {
|
|
17
|
-
|
|
45
|
+
setStoredId("");
|
|
18
46
|
return;
|
|
19
47
|
}
|
|
20
|
-
|
|
48
|
+
setStoredId((current) => {
|
|
21
49
|
if (categoryIds.includes(current)) return current;
|
|
22
50
|
return categoryIds[0] ?? "";
|
|
23
51
|
});
|
|
24
|
-
}, [
|
|
52
|
+
}, [
|
|
53
|
+
indexMode,
|
|
54
|
+
categoryIds,
|
|
55
|
+
categoryIds.join("\0")
|
|
56
|
+
]);
|
|
25
57
|
useEffect(() => {
|
|
26
58
|
return () => {
|
|
27
59
|
if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
|
|
@@ -33,13 +65,34 @@ const useActiveCategory = (opts) => {
|
|
|
33
65
|
lockedIdRef.current = null;
|
|
34
66
|
}, SCROLL_SETTLE_MS);
|
|
35
67
|
};
|
|
68
|
+
let activeCategoryId = storedId;
|
|
69
|
+
if (indexMode && !headerIndex.has(storedId)) activeCategoryId = firstSectionId(headerIndex, rowSectionIds);
|
|
36
70
|
const registerCategory = (categoryId) => {
|
|
71
|
+
if (indexMode) return ignoreLayout;
|
|
37
72
|
return (event) => {
|
|
38
73
|
const y = event.nativeEvent.layout.y;
|
|
39
74
|
offsetsRef.current.set(categoryId, y);
|
|
40
75
|
};
|
|
41
76
|
};
|
|
77
|
+
const applyVisibleIndex = (index) => {
|
|
78
|
+
if (typeof index !== "number" || index < 0) return;
|
|
79
|
+
const sectionId = rowSectionIds[index];
|
|
80
|
+
if (!sectionId) return;
|
|
81
|
+
setStoredId((previous) => {
|
|
82
|
+
if (previous === sectionId) return previous;
|
|
83
|
+
return sectionId;
|
|
84
|
+
});
|
|
85
|
+
};
|
|
42
86
|
const onScroll = (event) => {
|
|
87
|
+
if (indexMode) {
|
|
88
|
+
if (lockedIdRef.current) {
|
|
89
|
+
armSettleUnlock();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
applyVisibleIndex(listRef.current?.getFirstVisibleIndex?.());
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!event) return;
|
|
43
96
|
const scrollY = event.nativeEvent.contentOffset.y;
|
|
44
97
|
if (lockedIdRef.current) {
|
|
45
98
|
armSettleUnlock();
|
|
@@ -51,31 +104,75 @@ const useActiveCategory = (opts) => {
|
|
|
51
104
|
const top = offsetsRef.current.get(id);
|
|
52
105
|
if (top !== void 0 && top <= threshold) current = id;
|
|
53
106
|
}
|
|
54
|
-
|
|
107
|
+
setStoredId((previous) => {
|
|
55
108
|
return previous === current ? previous : current;
|
|
56
109
|
});
|
|
57
110
|
};
|
|
111
|
+
const onViewableItemsChanged = (info) => {
|
|
112
|
+
if (!indexMode) return;
|
|
113
|
+
if (lockedIdRef.current) {
|
|
114
|
+
armSettleUnlock();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
let lowest = null;
|
|
118
|
+
for (const token of info.viewableItems) {
|
|
119
|
+
if (token.isViewable === false) continue;
|
|
120
|
+
const tokenIndex = token.index;
|
|
121
|
+
if (tokenIndex == null || tokenIndex < 0) continue;
|
|
122
|
+
if (lowest == null || tokenIndex < lowest) lowest = tokenIndex;
|
|
123
|
+
}
|
|
124
|
+
if (lowest == null) return;
|
|
125
|
+
applyVisibleIndex(lowest);
|
|
126
|
+
};
|
|
127
|
+
const onViewableRef = useRef(onViewableItemsChanged);
|
|
128
|
+
onViewableRef.current = onViewableItemsChanged;
|
|
129
|
+
const stableViewable = useRef((info) => {
|
|
130
|
+
onViewableRef.current(info);
|
|
131
|
+
}).current;
|
|
58
132
|
const selectCategory = (categoryId) => {
|
|
59
133
|
lockedIdRef.current = categoryId;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
134
|
+
setStoredId(categoryId);
|
|
135
|
+
if (indexMode) {
|
|
136
|
+
const index = headerIndex.get(categoryId);
|
|
137
|
+
const list = listRef.current;
|
|
138
|
+
if (index !== void 0 && list) {
|
|
139
|
+
const jump = {
|
|
140
|
+
index,
|
|
141
|
+
animated: true,
|
|
142
|
+
viewOffset: offset
|
|
143
|
+
};
|
|
144
|
+
const pending = list.scrollToIndex(jump);
|
|
145
|
+
Promise.resolve(pending).catch(() => void 0);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
const top = offsetsRef.current.get(categoryId) ?? 0;
|
|
149
|
+
const y = Math.max(0, top - offset);
|
|
150
|
+
scrollRef?.current?.scrollTo({
|
|
151
|
+
y,
|
|
152
|
+
animated: true
|
|
153
|
+
});
|
|
154
|
+
}
|
|
67
155
|
if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
|
|
68
156
|
settleTimerRef.current = setTimeout(() => {
|
|
69
157
|
lockedIdRef.current = null;
|
|
70
158
|
}, SCROLL_LOCK_FALLBACK_MS);
|
|
71
159
|
};
|
|
72
|
-
|
|
160
|
+
const viewable = indexMode ? stableViewable : ignoreViewable;
|
|
161
|
+
const b1 = {
|
|
73
162
|
activeCategoryId,
|
|
74
|
-
setActiveCategoryId
|
|
163
|
+
setActiveCategoryId: setStoredId
|
|
164
|
+
};
|
|
165
|
+
const b2 = {
|
|
75
166
|
selectCategory,
|
|
76
167
|
onScroll,
|
|
77
168
|
registerCategory
|
|
78
169
|
};
|
|
170
|
+
const b3 = { onViewableItemsChanged: viewable };
|
|
171
|
+
return {
|
|
172
|
+
...b1,
|
|
173
|
+
...b2,
|
|
174
|
+
...b3
|
|
175
|
+
};
|
|
79
176
|
};
|
|
80
177
|
//#endregion
|
|
81
178
|
export { useActiveCategory };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fire-spark/rn-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Fire Spark React Native UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -121,6 +121,7 @@
|
|
|
121
121
|
"build": "tsdown",
|
|
122
122
|
"dev": "tsdown --watch",
|
|
123
123
|
"check-types": "tsc --noEmit",
|
|
124
|
+
"test:utils": "vitest run",
|
|
124
125
|
"lint": "oxlint ./src --max-warnings=0",
|
|
125
126
|
"format": "oxfmt ./src",
|
|
126
127
|
"check-format": "oxfmt --check ./src"
|