fontdue-js 3.5.1 → 3.7.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/CHANGELOG.md +19 -0
- package/README.md +2 -0
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/CustomerSignInRequestLoginCodeMutation.graphql.d.ts +24 -0
- package/dist/__generated__/CustomerSignInRequestLoginCodeMutation.graphql.js +71 -0
- package/dist/__generated__/CustomerSignInVerifyLoginCodeMutation.graphql.d.ts +30 -0
- package/dist/__generated__/CustomerSignInVerifyLoginCodeMutation.graphql.js +98 -0
- package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +4 -1
- package/dist/__generated__/FontFamiliesQuery.graphql.js +46 -39
- package/dist/__generated__/NewsletterSignupUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/NewsletterSignupUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/PriceBar_node.graphql.d.ts +4 -1
- package/dist/__generated__/PriceBar_node.graphql.js +23 -16
- package/dist/__generated__/StoreModalFamily_collection.graphql.d.ts +3 -2
- package/dist/__generated__/StoreModalFamily_collection.graphql.js +3 -3
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.d.ts +23 -0
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.js +303 -0
- package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +4 -9
- package/dist/__generated__/StoreModalIndexQuery.graphql.js +90 -53
- package/dist/__generated__/StoreModalIndex_viewer.graphql.d.ts +24 -0
- package/dist/__generated__/StoreModalIndex_viewer.graphql.js +134 -0
- package/dist/__generated__/StoreModalProductContent_collection.graphql.d.ts +4 -1
- package/dist/__generated__/StoreModalProductContent_collection.graphql.js +12 -5
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.d.ts +2 -1
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.js +27 -24
- package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +2 -19
- package/dist/__generated__/StoreModalProductQuery.graphql.js +143 -174
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +54 -53
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.d.ts +38 -0
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.js +750 -0
- package/dist/__generated__/StoreModalProductViewer.graphql.d.ts +34 -0
- package/dist/__generated__/StoreModalProductViewer.graphql.js +149 -0
- package/dist/__generated__/StoreModalUnifiedCheckoutUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/StoreModalUnifiedCheckoutUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +3 -1
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.js +17 -10
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +17 -10
- package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersSlugQuery.graphql.js +17 -10
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.d.ts +41 -0
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.js +65 -0
- package/dist/__generated__/useCurrentCustomerQuery.graphql.d.ts +23 -0
- package/dist/__generated__/{ServerConfigProviderQuery.graphql.js → useCurrentCustomerQuery.graphql.js} +24 -39
- package/dist/__tests__/bundleSelectionOrder.test.js +92 -0
- package/dist/__tests__/collectionBundleSelection.test.js +441 -1446
- package/dist/__tests__/customerSession.test.js +456 -0
- package/dist/__tests__/infiniteScroll.test.js +180 -0
- package/dist/__tests__/licenseExclusions.test.js +2 -1
- package/dist/__tests__/storeModalRoutes.test.js +118 -0
- package/dist/__tests__/storefrontEvents.test.js +167 -0
- package/dist/__tests__/stripeElements.test.js +173 -0
- package/dist/__tests__/typeTesterAutofitFeatures.test.js +138 -0
- package/dist/__tests__/typeTesterInitialSize.test.js +82 -0
- package/dist/__tests__/typeTesterTruncate.test.js +218 -0
- package/dist/auth/session.d.ts +42 -0
- package/dist/auth/session.js +139 -0
- package/dist/auth/tokenStore.d.ts +61 -0
- package/dist/auth/tokenStore.js +162 -0
- package/dist/components/Cart/Checkout.js +19 -9
- package/dist/components/Cart/storefrontEvents.d.ts +34 -0
- package/dist/components/Cart/storefrontEvents.js +138 -0
- package/dist/components/ConfigContext.d.ts +2 -2
- package/dist/components/CustomerSignIn/index.d.ts +33 -0
- package/dist/components/CustomerSignIn/index.js +170 -0
- package/dist/components/FontFamilies/index.js +4 -3
- package/dist/components/NewsletterSignup/index.js +5 -1
- package/dist/components/Precart/index.js +6 -0
- package/dist/components/PriceBar/index.js +5 -3
- package/dist/components/Root/index.js +4 -34
- package/dist/components/Root/productState.d.ts +2 -8
- package/dist/components/Root/productState.js +46 -57
- package/dist/components/SKUPrice/index.js +7 -8
- package/dist/components/StoreModal/StoreModalFamily.js +4 -2
- package/dist/components/StoreModal/StoreModalIndex.js +48 -7
- package/dist/components/StoreModal/StoreModalProduct.d.ts +5 -1
- package/dist/components/StoreModal/StoreModalProduct.js +69 -16
- package/dist/components/StoreModal/index.js +14 -14
- package/dist/components/StoreModal/routes.js +9 -1
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.d.ts +2 -1
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.js +8 -3
- package/dist/components/StoreModalProductLicenseSelection/index.js +3 -2
- package/dist/components/TestFontsForm/index.js +5 -1
- package/dist/components/TypeTester/TypeTesterContent.js +9 -2
- package/dist/components/TypeTester/TypeTesterContext.js +17 -4
- package/dist/components/TypeTester/TypeTesterFitter.js +7 -2
- package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -0
- package/dist/components/TypeTester/TypeTesterStandalone.js +2 -0
- package/dist/components/TypeTester/TypeTesterStandaloneElement.d.ts +1 -0
- package/dist/components/TypeTester/TypeTesterStandaloneElement.js +4 -1
- package/dist/components/TypeTester/TypeTesterVariableAxes.js +16 -10
- package/dist/components/TypeTester/index.d.ts +3 -1
- package/dist/components/TypeTester/index.js +2 -1
- package/dist/components/TypeTester/truncate.d.ts +12 -0
- package/dist/components/TypeTester/truncate.js +95 -0
- package/dist/components/TypeTester/useTypeTesterStyler.d.ts +3 -1
- package/dist/components/TypeTester/useTypeTesterStyler.js +36 -7
- package/dist/components/TypeTesters/TypeTestersElement.d.ts +2 -1
- package/dist/components/TypeTesters/TypeTestersElement.js +4 -1
- package/dist/components/TypeTesters/index.d.ts +2 -0
- package/dist/components/elements/StoreModalContainer/StoreModalContainerContext.d.ts +1 -0
- package/dist/components/elements/StoreModalContainer/index.js +9 -4
- package/dist/components/elements/StoreModalIndexLoadMore/index.d.ts +8 -0
- package/dist/components/elements/StoreModalIndexLoadMore/index.js +21 -0
- package/dist/components/elements/StoreModalUnifiedCheckout.d.ts +1 -1
- package/dist/components/elements/StoreModalUnifiedCheckout.js +20 -17
- package/dist/components/useInfiniteScroll.d.ts +28 -0
- package/dist/components/useInfiniteScroll.js +50 -0
- package/dist/fontdue.css +63 -0
- package/dist/hooks/useAutofit.d.ts +2 -1
- package/dist/hooks/useAutofit.js +10 -4
- package/dist/hooks/useCurrentCustomer.d.ts +34 -0
- package/dist/hooks/useCurrentCustomer.js +93 -0
- package/dist/reducer.d.ts +5 -5
- package/dist/reducer.js +9 -87
- package/dist/relay/environment.js +91 -50
- package/dist/selection.d.ts +40 -0
- package/dist/selection.js +494 -0
- package/dist/storeModalRoutes.d.ts +38 -0
- package/dist/storeModalRoutes.js +110 -0
- package/dist/stripeElements.d.ts +18 -0
- package/dist/stripeElements.js +49 -0
- package/dist/utils.d.ts +1 -12
- package/dist/utils.js +1 -36
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/__generated__/NodePasswordFormIDQuery.graphql.d.ts +0 -21
- package/dist/__generated__/NodePasswordFormIDQuery.graphql.js +0 -86
- package/dist/__generated__/NodePasswordFormSlugQuery.graphql.d.ts +0 -25
- package/dist/__generated__/NodePasswordFormSlugQuery.graphql.js +0 -108
- package/dist/__generated__/ServerConfigProviderQuery.graphql.d.ts +0 -24
- package/dist/__generated__/TestModeBannerQuery.graphql.d.ts +0 -20
- package/dist/__generated__/TestModeBannerQuery.graphql.js +0 -87
- package/dist/__generated__/ThemeConfigQuery.graphql.d.ts +0 -20
- package/dist/__generated__/ThemeConfigQuery.graphql.js +0 -87
- package/dist/components/CartButton/index.server.d.ts +0 -3
- package/dist/components/CartButton/index.server.js +0 -18
- package/dist/components/CorsErrorModal.d.ts +0 -1
- package/dist/components/CorsErrorModal.js +0 -92
- package/dist/components/FontdueProvider/FontdueProviderClientComponent.d.ts +0 -14
- package/dist/components/FontdueProvider/FontdueProviderClientComponent.js +0 -61
- package/dist/components/ThemeConfig/index.server.d.ts +0 -2
- package/dist/components/ThemeConfig/index.server.js +0 -17
- package/dist/components/TypeTester/features.d.ts +0 -5
- package/dist/components/TypeTester/features.js +0 -143
- package/dist/data/unicodeData.d.ts +0 -4
- package/dist/data/unicodeData.js +0 -1
|
@@ -3,6 +3,7 @@ import { Alignment, Direction, FeaturesProp } from './types.js';
|
|
|
3
3
|
import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
|
|
4
4
|
import { TypeTesterStandaloneQuery } from '../../__generated__/TypeTesterStandaloneQuery.graphql.js';
|
|
5
5
|
import { Config } from '../ConfigContext.js';
|
|
6
|
+
import type { TruncateOption } from './truncate.js';
|
|
6
7
|
interface TypeTesterStandaloneComponent_props {
|
|
7
8
|
lineHeight?: number | null;
|
|
8
9
|
letterSpacing?: number | null;
|
|
@@ -22,6 +23,7 @@ interface TypeTesterStandaloneComponent_props {
|
|
|
22
23
|
direction?: Direction;
|
|
23
24
|
alignment?: Alignment;
|
|
24
25
|
autofit?: boolean;
|
|
26
|
+
truncate?: TruncateOption;
|
|
25
27
|
onFocus?: () => void;
|
|
26
28
|
onBlur?: () => void;
|
|
27
29
|
}
|
|
@@ -30,6 +30,7 @@ function TypeTesterStandaloneComponent(_ref) {
|
|
|
30
30
|
alignment,
|
|
31
31
|
fontSize,
|
|
32
32
|
autofit,
|
|
33
|
+
truncate,
|
|
33
34
|
onFocus,
|
|
34
35
|
onBlur,
|
|
35
36
|
data
|
|
@@ -95,6 +96,7 @@ function TypeTesterStandaloneComponent(_ref) {
|
|
|
95
96
|
features: normalizeFeaturesProp(features),
|
|
96
97
|
productId: ((_data$viewer2 = data.viewer) === null || _data$viewer2 === void 0 ? void 0 : (_data$viewer2$fontSty = _data$viewer2.fontStyle) === null || _data$viewer2$fontSty === void 0 ? void 0 : (_data$viewer2$fontSty2 = _data$viewer2$fontSty.family) === null || _data$viewer2$fontSty2 === void 0 ? void 0 : (_data$viewer2$fontSty3 = _data$viewer2$fontSty2.parent) === null || _data$viewer2$fontSty3 === void 0 ? void 0 : _data$viewer2$fontSty3.id) ?? ((_data$viewer3 = data.viewer) === null || _data$viewer3 === void 0 ? void 0 : (_data$viewer3$fontSty = _data$viewer3.fontStyle) === null || _data$viewer3$fontSty === void 0 ? void 0 : (_data$viewer3$fontSty2 = _data$viewer3$fontSty.family) === null || _data$viewer3$fontSty2 === void 0 ? void 0 : _data$viewer3$fontSty2.id),
|
|
97
98
|
groupEdit: false,
|
|
99
|
+
truncate: truncate,
|
|
98
100
|
onFocus: onFocus,
|
|
99
101
|
onBlur: onBlur
|
|
100
102
|
}));
|
|
@@ -13,6 +13,7 @@ interface TypeTesterElement_props {
|
|
|
13
13
|
direction?: string;
|
|
14
14
|
alignment?: string;
|
|
15
15
|
autofit?: string;
|
|
16
|
+
truncate?: string;
|
|
16
17
|
}
|
|
17
18
|
export declare const TypeTesterStandaloneElement: React.FC<TypeTesterElement_props>;
|
|
18
19
|
export default TypeTesterStandaloneElement;
|
|
@@ -2,6 +2,7 @@ function _extends() { return _extends = Object.assign ? Object.assign.bind() : f
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import TypeTesterStandalone from './TypeTesterStandalone.js';
|
|
4
4
|
import { parseVariableSettings } from './parseVariableSettings.js';
|
|
5
|
+
import { parseTruncateAttribute } from './truncate.js';
|
|
5
6
|
const isAlignment = str => {
|
|
6
7
|
return str === 'left' || str === 'center' || str === 'right';
|
|
7
8
|
};
|
|
@@ -26,6 +27,7 @@ export const TypeTesterStandaloneElement = _ref => {
|
|
|
26
27
|
alignment,
|
|
27
28
|
direction,
|
|
28
29
|
autofit,
|
|
30
|
+
truncate,
|
|
29
31
|
...rest
|
|
30
32
|
} = _ref;
|
|
31
33
|
return /*#__PURE__*/React.createElement(TypeTesterStandalone, _extends({
|
|
@@ -41,7 +43,8 @@ export const TypeTesterStandaloneElement = _ref => {
|
|
|
41
43
|
})) : undefined,
|
|
42
44
|
alignment: isAlignment(alignment) ? alignment : undefined,
|
|
43
45
|
direction: isDirection(direction) ? direction : undefined,
|
|
44
|
-
variableSettings: parseVariableSettings(variableSettings)
|
|
46
|
+
variableSettings: parseVariableSettings(variableSettings),
|
|
47
|
+
truncate: parseTruncateAttribute(truncate, 'fontdue-type-tester')
|
|
45
48
|
}, rest));
|
|
46
49
|
};
|
|
47
50
|
export default TypeTesterStandaloneElement;
|
|
@@ -35,15 +35,7 @@ const TypeTesterVariableAxes = _ref => {
|
|
|
35
35
|
}, [requestedAxesKey, availableAxesKey]);
|
|
36
36
|
if (!variableSettings) return null;
|
|
37
37
|
if (!axes) return null;
|
|
38
|
-
const
|
|
39
|
-
setVariableSettings({
|
|
40
|
-
...variableSettings,
|
|
41
|
-
[axis]: value
|
|
42
|
-
});
|
|
43
|
-
};
|
|
44
|
-
return /*#__PURE__*/React.createElement("div", {
|
|
45
|
-
className: "type-tester__variable-axes"
|
|
46
|
-
}, axes.filter(showAxis => showAxis in variableSettings) // only show the slider if the value is set
|
|
38
|
+
const visibleAxes = axes.filter(showAxis => showAxis in variableSettings) // only show the slider if the value is set
|
|
47
39
|
.map(showAxis => {
|
|
48
40
|
var _fontStyle$variableAx;
|
|
49
41
|
return (_fontStyle$variableAx = fontStyle.variableAxes) === null || _fontStyle$variableAx === void 0 ? void 0 : _fontStyle$variableAx.find(_ref2 => {
|
|
@@ -52,7 +44,21 @@ const TypeTesterVariableAxes = _ref => {
|
|
|
52
44
|
} = _ref2;
|
|
53
45
|
return axis === showAxis;
|
|
54
46
|
});
|
|
55
|
-
}).filter(notEmpty)
|
|
47
|
+
}).filter(notEmpty);
|
|
48
|
+
|
|
49
|
+
// Nothing to show (static style, or every requested axis is missing): render
|
|
50
|
+
// no container at all, so its margin/padding/border in the toolbar, features
|
|
51
|
+
// panel and dropdown placements never leave an empty gap behind.
|
|
52
|
+
if (visibleAxes.length === 0) return null;
|
|
53
|
+
const handleChange = (axis, value) => {
|
|
54
|
+
setVariableSettings({
|
|
55
|
+
...variableSettings,
|
|
56
|
+
[axis]: value
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
60
|
+
className: "type-tester__variable-axes"
|
|
61
|
+
}, visibleAxes.map(_ref3 => {
|
|
56
62
|
let {
|
|
57
63
|
axis,
|
|
58
64
|
name,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { TypeTester_fontStyle$key } from '../../__generated__/TypeTester_fontStyle.graphql.js';
|
|
3
3
|
import { Alignment, TypeTesterAxesPosition } from './types.js';
|
|
4
|
+
import type { TruncateOption } from './truncate.js';
|
|
4
5
|
export interface TypeTesterConfig {
|
|
5
6
|
min?: number;
|
|
6
7
|
max?: number;
|
|
@@ -15,7 +16,7 @@ export interface TypeTesterConfig {
|
|
|
15
16
|
toolsPosition?: 'inline' | 'floating';
|
|
16
17
|
selectButtonStyle?: 'inline' | 'outlined';
|
|
17
18
|
selectButtonLabel?: string;
|
|
18
|
-
truncate?:
|
|
19
|
+
truncate?: TruncateOption;
|
|
19
20
|
bulletStyle?: 'square' | 'round';
|
|
20
21
|
priceText?: boolean;
|
|
21
22
|
autofitOnChange?: boolean;
|
|
@@ -61,6 +62,7 @@ export interface TypeTesterBaseProps {
|
|
|
61
62
|
features?: ReadonlyArray<string> | null;
|
|
62
63
|
axes?: ReadonlyArray<string> | null;
|
|
63
64
|
groupEdit?: boolean;
|
|
65
|
+
truncate?: TruncateOption;
|
|
64
66
|
onFocus?: () => void;
|
|
65
67
|
onBlur?: () => void;
|
|
66
68
|
}
|
|
@@ -26,6 +26,7 @@ const TypeTester = _ref => {
|
|
|
26
26
|
features,
|
|
27
27
|
axes,
|
|
28
28
|
groupEdit,
|
|
29
|
+
truncate,
|
|
29
30
|
onStyleSelect,
|
|
30
31
|
isLoading
|
|
31
32
|
} = _ref;
|
|
@@ -150,7 +151,7 @@ const TypeTester = _ref => {
|
|
|
150
151
|
ref: contentRef,
|
|
151
152
|
fontStyles: style,
|
|
152
153
|
verticalMetrics: verticalMetrics,
|
|
153
|
-
truncate: config.truncate,
|
|
154
|
+
truncate: truncate ?? config.truncate,
|
|
154
155
|
direction: props.direction,
|
|
155
156
|
min: config.size.min,
|
|
156
157
|
max: config.size.max,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface TruncateConfig {
|
|
2
|
+
lines?: number;
|
|
3
|
+
expandOnFocus?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export type TruncateOption = boolean | number | TruncateConfig;
|
|
6
|
+
export interface ResolvedTruncate {
|
|
7
|
+
lines: number;
|
|
8
|
+
expandOnFocus: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const normalizeTruncate: (value?: TruncateOption) => ResolvedTruncate;
|
|
11
|
+
export declare const shouldTruncate: ({ lines, expandOnFocus }: ResolvedTruncate, focused: boolean) => boolean;
|
|
12
|
+
export declare const parseTruncateAttribute: (input: string | undefined, elementName: string) => TruncateOption | undefined;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Shape and normalization for the type tester's `truncate` option, shared by
|
|
2
|
+
// the styler and by the custom element attribute parsers.
|
|
3
|
+
|
|
4
|
+
const toLines = value => Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
5
|
+
export const normalizeTruncate = value => {
|
|
6
|
+
if (value === true) return {
|
|
7
|
+
lines: 1,
|
|
8
|
+
expandOnFocus: true
|
|
9
|
+
};
|
|
10
|
+
if (typeof value === 'number') return {
|
|
11
|
+
lines: toLines(value),
|
|
12
|
+
expandOnFocus: false
|
|
13
|
+
};
|
|
14
|
+
if (value && typeof value === 'object') return {
|
|
15
|
+
lines: value.lines === undefined ? 0 : toLines(value.lines),
|
|
16
|
+
expandOnFocus: value.expandOnFocus ?? false
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
lines: 0,
|
|
20
|
+
expandOnFocus: false
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Whether the cap applies right now. Split out from the styler so the one
|
|
25
|
+
// behaviour FD-191 changes is unit-testable without a DOM.
|
|
26
|
+
export const shouldTruncate = (_ref, focused) => {
|
|
27
|
+
let {
|
|
28
|
+
lines,
|
|
29
|
+
expandOnFocus
|
|
30
|
+
} = _ref;
|
|
31
|
+
return lines > 0 && !(focused && expandOnFocus);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// A line count, or `true`/`false`.
|
|
35
|
+
const parseLineCount = token => {
|
|
36
|
+
if (token === 'true') return 1;
|
|
37
|
+
if (token === 'false') return 0;
|
|
38
|
+
const lines = Number(token);
|
|
39
|
+
if (token !== '' && Number.isInteger(lines) && lines >= 0) return lines;
|
|
40
|
+
return undefined;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// The optional second word: what kind of cap this is. Adjectives rather than
|
|
44
|
+
// imperatives, both because every other enumerated value in the config is one
|
|
45
|
+
// (`inline`/`floating`, `square`/`round`, `panel`/`dropdown`) and because they
|
|
46
|
+
// describe the cap accurately — nothing expands until a visitor clicks, so the
|
|
47
|
+
// cap is expandable, not expanding.
|
|
48
|
+
//
|
|
49
|
+
// Written out rather than looked up in an object so that `constructor` and
|
|
50
|
+
// friends are words like any other instead of inherited Object members.
|
|
51
|
+
const parseFocusWord = token => {
|
|
52
|
+
if (token === 'expandable') return true;
|
|
53
|
+
if (token === 'fixed') return false;
|
|
54
|
+
return undefined;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// `truncate="15"` caps at 15 lines, `truncate="true"` is the legacy one-line
|
|
58
|
+
// preview, and `truncate="false"` or `truncate="0"` turns it off for this
|
|
59
|
+
// tester.
|
|
60
|
+
//
|
|
61
|
+
// A second word says what kind of cap it is, which is the half of the object
|
|
62
|
+
// form (`expandOnFocus`) an attribute otherwise cannot carry:
|
|
63
|
+
// `truncate="15 expandable"` caps at 15 lines and still opens to the full
|
|
64
|
+
// paragraph when clicked, and `truncate="15 fixed"` spells out the default.
|
|
65
|
+
// Without it the scalar forms are untouched, so `truncate="true"` is still the
|
|
66
|
+
// boolean shorthand and `truncate="15"` still holds through focus.
|
|
67
|
+
//
|
|
68
|
+
// An absent attribute returns undefined so the tester inherits the site
|
|
69
|
+
// config. Anything else, including a valueless `truncate`, an unknown second
|
|
70
|
+
// word or a third one, warns and inherits: following parseVariableSettings, a
|
|
71
|
+
// typo must not silently read as "off". The warning lists the accepted forms,
|
|
72
|
+
// because the failure that grammar shipped in 3.4.1 was silent — a rejected
|
|
73
|
+
// value simply did nothing, with nothing in the markup explaining why.
|
|
74
|
+
export const parseTruncateAttribute = (input, elementName) => {
|
|
75
|
+
if (input === undefined) return undefined;
|
|
76
|
+
const [countToken = '', focusToken, ...rest] = input.trim().split(/\s+/);
|
|
77
|
+
const lines = parseLineCount(countToken);
|
|
78
|
+
const expandOnFocus = focusToken === undefined ? undefined : parseFocusWord(focusToken);
|
|
79
|
+
if (lines === undefined || rest.length > 0 || focusToken !== undefined && expandOnFocus === undefined) {
|
|
80
|
+
console.warn(`${elementName} failed to parse truncate value: "${input}", ignoring. ` + 'Expected a line count, "true", or "false", optionally followed by ' + '"expandable" or "fixed" — e.g. "15", "true", "15 expandable".');
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// No second word: return the scalar the caller wrote, so the existing forms
|
|
85
|
+
// keep their exact shape rather than becoming objects that normalize the same.
|
|
86
|
+
if (expandOnFocus === undefined) {
|
|
87
|
+
if (countToken === 'true') return true;
|
|
88
|
+
if (countToken === 'false') return false;
|
|
89
|
+
return lines;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
lines,
|
|
93
|
+
expandOnFocus
|
|
94
|
+
};
|
|
95
|
+
};
|
|
@@ -2,6 +2,7 @@ import { EditorState } from 'draft-js';
|
|
|
2
2
|
import { Alignment } from './types.js';
|
|
3
3
|
import { VariableSettings } from '../../utils.js';
|
|
4
4
|
import { VerticalMetrics } from '../useFontStyle.js';
|
|
5
|
+
import { TruncateOption } from './truncate.js';
|
|
5
6
|
export type ColumnsConfig = false | {
|
|
6
7
|
count: 'auto' | number;
|
|
7
8
|
width: string;
|
|
@@ -20,7 +21,7 @@ export interface UseTypeTesterStylerProps {
|
|
|
20
21
|
letterSpacing: number;
|
|
21
22
|
features: string[];
|
|
22
23
|
focused: boolean;
|
|
23
|
-
truncate:
|
|
24
|
+
truncate: TruncateOption;
|
|
24
25
|
fontStyles: React.CSSProperties;
|
|
25
26
|
verticalMetrics?: VerticalMetrics | null;
|
|
26
27
|
content: EditorState;
|
|
@@ -32,5 +33,6 @@ export interface UseTypeTesterStylerProps {
|
|
|
32
33
|
declare const useTypeTesterStyler: ({ size, autofit, autofitOnChange, features, setSize, min, max, truncate, focused, lineHeight, letterSpacing, fontStyles, verticalMetrics, content, contentEdited, alignment, variableSettings, columns, }: UseTypeTesterStylerProps) => {
|
|
33
34
|
ref: import("react").RefObject<HTMLDivElement | null>;
|
|
34
35
|
style: import("react").CSSProperties;
|
|
36
|
+
truncated: boolean;
|
|
35
37
|
};
|
|
36
38
|
export default useTypeTesterStyler;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "../../global-shim.js";
|
|
2
2
|
import { useLayoutEffect } from 'react';
|
|
3
3
|
import useAutofit from '../../hooks/useAutofit.js';
|
|
4
|
+
import { normalizeTruncate, shouldTruncate as resolveShouldTruncate } from './truncate.js';
|
|
4
5
|
const useTypeTesterStyler = _ref => {
|
|
5
6
|
let {
|
|
6
7
|
size,
|
|
@@ -39,6 +40,7 @@ const useTypeTesterStyler = _ref => {
|
|
|
39
40
|
fontWeight: fontStyles.fontWeight,
|
|
40
41
|
fontStyle: fontStyles.fontStyle,
|
|
41
42
|
fontVariationSettings,
|
|
43
|
+
fontFeatureSettings,
|
|
42
44
|
enabled: autofit && (!contentEdited || autofitOnChange),
|
|
43
45
|
padding: sideBuffer
|
|
44
46
|
});
|
|
@@ -53,11 +55,21 @@ const useTypeTesterStyler = _ref => {
|
|
|
53
55
|
}
|
|
54
56
|
}, [autofitSize]);
|
|
55
57
|
|
|
56
|
-
// `truncate` is `true` (
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
// `truncate` is `true` (the original one-line preview), a line count, an
|
|
59
|
+
// explicit `{ lines, expandOnFocus }`, or falsy (off). `expandOnFocus`
|
|
60
|
+
// decides whether clicking in lifts the cap: true for the one-line preview,
|
|
61
|
+
// which exists to bloom open, and false for a line count, where the
|
|
62
|
+
// paragraph has to stay N lines while it is edited (FD-191). For a line
|
|
63
|
+
// count nothing below depends on `focused` any more, so focusing a truncated
|
|
64
|
+
// tester is a zero-reflow event.
|
|
65
|
+
const {
|
|
66
|
+
lines: truncateLines,
|
|
67
|
+
expandOnFocus
|
|
68
|
+
} = normalizeTruncate(truncate);
|
|
69
|
+
const shouldTruncate = resolveShouldTruncate({
|
|
70
|
+
lines: truncateLines,
|
|
71
|
+
expandOnFocus
|
|
72
|
+
}, focused);
|
|
61
73
|
|
|
62
74
|
// How far a line's ink spills past its line-box, top and bottom, as a fraction
|
|
63
75
|
// of the font-size (em). `fontLoader` feeds these exact metrics into the
|
|
@@ -131,7 +143,23 @@ const useTypeTesterStyler = _ref => {
|
|
|
131
143
|
paddingRight: sideBearing,
|
|
132
144
|
marginTop: `${-lineOverflowEm}em`,
|
|
133
145
|
marginBottom: `${-lineOverflowEm}em`,
|
|
134
|
-
|
|
146
|
+
// `clip`, not `hidden`, once the cap outlives focus. A hidden box is
|
|
147
|
+
// still a scroll container, so the browser scrolls it to chase a
|
|
148
|
+
// caret past line N, and the offset is unrecoverable because there is
|
|
149
|
+
// no scrollbar and wheel events are ignored (measured: WebKit slides
|
|
150
|
+
// a whole column away on the first keystroke past the cap, and it
|
|
151
|
+
// survives blur). A clipped box is not a scroll container at all, so
|
|
152
|
+
// the visible lines can never slide.
|
|
153
|
+
//
|
|
154
|
+
// `expandOnFocus` stays on `hidden`, because there the cap is lifted
|
|
155
|
+
// before a caret can enter the box. That distinction is load-bearing:
|
|
156
|
+
// an unsupported CSSOM value is a no-op that leaves the previous
|
|
157
|
+
// inline value in place, so on an engine without `clip` a branch that
|
|
158
|
+
// toggles on focus would keep the `overflow: visible` written by the
|
|
159
|
+
// untruncated branch below and stop clipping for good. The branch
|
|
160
|
+
// that uses `clip` never toggles, so on those engines it falls
|
|
161
|
+
// through to the `[data-truncated]` rule in _type-tester.scss.
|
|
162
|
+
overflow: expandOnFocus ? 'hidden' : 'clip',
|
|
135
163
|
// Fill each column to its full N lines before spilling to the next, and
|
|
136
164
|
// allow a final column with a single line — otherwise the default
|
|
137
165
|
// `widows: 2` / `column-fill: balance` rebalance a 4-line paragraph into
|
|
@@ -156,7 +184,8 @@ const useTypeTesterStyler = _ref => {
|
|
|
156
184
|
};
|
|
157
185
|
return {
|
|
158
186
|
ref,
|
|
159
|
-
style
|
|
187
|
+
style,
|
|
188
|
+
truncated: shouldTruncate
|
|
160
189
|
};
|
|
161
190
|
};
|
|
162
191
|
export default useTypeTesterStyler;
|
|
@@ -7,6 +7,7 @@ interface TypeTestersElementProps {
|
|
|
7
7
|
features?: string;
|
|
8
8
|
tags?: string;
|
|
9
9
|
excludeTags?: string;
|
|
10
|
+
truncate?: string;
|
|
10
11
|
}
|
|
11
|
-
declare function TypeTestersElement({ autofit, tags, excludeTags, defaultMode, features, ...rest }: TypeTestersElementProps): React.JSX.Element;
|
|
12
|
+
declare function TypeTestersElement({ autofit, tags, excludeTags, defaultMode, features, truncate, ...rest }: TypeTestersElementProps): React.JSX.Element;
|
|
12
13
|
export default TypeTestersElement;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import TypeTesters from './index.js';
|
|
4
|
+
import { parseTruncateAttribute } from '../TypeTester/truncate.js';
|
|
4
5
|
const getDefaultMode = (stringValue, autofit) => {
|
|
5
6
|
if (autofit) return 'local';
|
|
6
7
|
if (stringValue === 'group') return 'group';
|
|
@@ -13,6 +14,7 @@ function TypeTestersElement(_ref) {
|
|
|
13
14
|
excludeTags,
|
|
14
15
|
defaultMode,
|
|
15
16
|
features,
|
|
17
|
+
truncate,
|
|
16
18
|
...rest
|
|
17
19
|
} = _ref;
|
|
18
20
|
return /*#__PURE__*/React.createElement(TypeTesters, _extends({
|
|
@@ -20,7 +22,8 @@ function TypeTestersElement(_ref) {
|
|
|
20
22
|
excludeTags: excludeTags === null || excludeTags === void 0 ? void 0 : excludeTags.split(','),
|
|
21
23
|
autofit: autofit ? autofit === 'true' : undefined,
|
|
22
24
|
defaultMode: getDefaultMode(defaultMode, autofit === 'true'),
|
|
23
|
-
features: features === null || features === void 0 ? void 0 : features.split(',')
|
|
25
|
+
features: features === null || features === void 0 ? void 0 : features.split(','),
|
|
26
|
+
truncate: parseTruncateAttribute(truncate, 'fontdue-type-testers')
|
|
24
27
|
}, rest));
|
|
25
28
|
}
|
|
26
29
|
export default TypeTestersElement;
|
|
@@ -4,11 +4,13 @@ import { TypeTestersSlugQuery } from '../../__generated__/TypeTestersSlugQuery.g
|
|
|
4
4
|
import { Config } from '../ConfigContext.js';
|
|
5
5
|
import { FeaturesProp } from '../TypeTester/types.js';
|
|
6
6
|
import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
|
|
7
|
+
import type { TruncateOption } from '../TypeTester/truncate.js';
|
|
7
8
|
export interface TypeTesters_props {
|
|
8
9
|
collectionId?: string;
|
|
9
10
|
collectionSlug?: string;
|
|
10
11
|
defaultMode?: 'group' | 'local';
|
|
11
12
|
autofit?: boolean;
|
|
13
|
+
truncate?: TruncateOption;
|
|
12
14
|
features?: FeaturesProp;
|
|
13
15
|
tags?: string[] | null;
|
|
14
16
|
excludeTags?: string[] | null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useRef, useCallback } from 'react';
|
|
1
|
+
import React, { useRef, useCallback, useMemo } from 'react';
|
|
2
2
|
import { ArrowLeft, CartNew, X } from '../../Icons/index.js';
|
|
3
3
|
import StoreModalContainerContext from './StoreModalContainerContext.js';
|
|
4
4
|
const StoreModalContainer = _ref => {
|
|
@@ -25,10 +25,15 @@ const StoreModalContainer = _ref => {
|
|
|
25
25
|
left: 0
|
|
26
26
|
});
|
|
27
27
|
}, [container]);
|
|
28
|
+
|
|
29
|
+
// Stable, so an effect that depends on the context value (the index's
|
|
30
|
+
// load-more observer) isn't rebuilt on every render of the modal.
|
|
31
|
+
const context = useMemo(() => ({
|
|
32
|
+
scrollToTop: handleScrollToTop,
|
|
33
|
+
scrollContainer: container
|
|
34
|
+
}), [handleScrollToTop]);
|
|
28
35
|
return /*#__PURE__*/React.createElement(StoreModalContainerContext.Provider, {
|
|
29
|
-
value:
|
|
30
|
-
scrollToTop: handleScrollToTop
|
|
31
|
-
}
|
|
36
|
+
value: context
|
|
32
37
|
}, /*#__PURE__*/React.createElement("div", {
|
|
33
38
|
className: "store-modal__container__overlay",
|
|
34
39
|
"data-route": routeName
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface StoreModalIndexLoadMoreProps {
|
|
3
|
+
sentinelRef: (node: Element | null) => void;
|
|
4
|
+
onClick: () => void;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
}
|
|
7
|
+
declare const StoreModalIndexLoadMore: React.FunctionComponent<StoreModalIndexLoadMoreProps>;
|
|
8
|
+
export default StoreModalIndexLoadMore;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
// Sits after the last collection while more remain. The button is what a
|
|
3
|
+
// visitor sees while a page is on its way, and the way to go on without
|
|
4
|
+
// scrolling — with a keyboard, or if the observer never fires.
|
|
5
|
+
const StoreModalIndexLoadMore = _ref => {
|
|
6
|
+
let {
|
|
7
|
+
sentinelRef,
|
|
8
|
+
onClick,
|
|
9
|
+
loading
|
|
10
|
+
} = _ref;
|
|
11
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
12
|
+
className: "store-modal__index__load-more",
|
|
13
|
+
ref: sentinelRef
|
|
14
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
15
|
+
type: "button",
|
|
16
|
+
className: "store-modal__index__load-more__button",
|
|
17
|
+
onClick: onClick,
|
|
18
|
+
disabled: loading
|
|
19
|
+
}, loading ? 'Loading…' : 'Load more'));
|
|
20
|
+
};
|
|
21
|
+
export default StoreModalIndexLoadMore;
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* - The Pay button is always enabled (as long as EULA is checked) even if billing
|
|
28
28
|
* info isn't fully saved yet
|
|
29
29
|
* - This is intentional: we need to allow the user to click Pay so we can call
|
|
30
|
-
*
|
|
30
|
+
* validateElements(), which triggers Stripe's validation and shows any validation
|
|
31
31
|
* errors inline in the Stripe UI elements
|
|
32
32
|
* - After Stripe validation passes, we check if billing identity is saved
|
|
33
33
|
* - If not saved, we show an error message above the Pay button asking them to
|
|
@@ -32,7 +32,7 @@ import _StoreModalUnifiedCheckout_viewer from "../../__generated__/StoreModalUni
|
|
|
32
32
|
* - The Pay button is always enabled (as long as EULA is checked) even if billing
|
|
33
33
|
* info isn't fully saved yet
|
|
34
34
|
* - This is intentional: we need to allow the user to click Pay so we can call
|
|
35
|
-
*
|
|
35
|
+
* validateElements(), which triggers Stripe's validation and shows any validation
|
|
36
36
|
* errors inline in the Stripe UI elements
|
|
37
37
|
* - After Stripe validation passes, we check if billing identity is saved
|
|
38
38
|
* - If not saved, we show an error message above the Pay button asking them to
|
|
@@ -79,6 +79,8 @@ import { Check, X } from '../Icons/index.js';
|
|
|
79
79
|
import ConfigContext from '../ConfigContext.js';
|
|
80
80
|
import { Price } from '../Price/index.js';
|
|
81
81
|
import { sendOrderTracking } from '../Cart/orderTracking.js';
|
|
82
|
+
import { trackPaymentInfoEntered } from '../Cart/storefrontEvents.js';
|
|
83
|
+
import { validateAddressElements, validateElements } from '../../stripeElements.js';
|
|
82
84
|
import { useRecaptchaTimeout, isRecaptchaScriptLoaded, RECAPTCHA_UNAVAILABLE_MESSAGE } from '../../hooks/useRecaptchaTimeout.js';
|
|
83
85
|
|
|
84
86
|
/* const LICENSEE_REQUIRED_FIELDS = ['organization', 'country'] as (keyof Identity)[]; */
|
|
@@ -286,6 +288,16 @@ export default function StoreModalUnifiedCheckout(_ref2) {
|
|
|
286
288
|
useEffect(() => {
|
|
287
289
|
sendOrderTracking(environment);
|
|
288
290
|
}, [environment]);
|
|
291
|
+
|
|
292
|
+
// The Payment Element reports every keystroke; the first time the details
|
|
293
|
+
// are complete is the "Payment Info Entered" moment. The server keeps it to
|
|
294
|
+
// one per order, so this only saves requests.
|
|
295
|
+
const paymentInfoTracked = useRef(false);
|
|
296
|
+
const handlePaymentChange = useCallback(event => {
|
|
297
|
+
if (!event.complete || paymentInfoTracked.current) return;
|
|
298
|
+
paymentInfoTracked.current = true;
|
|
299
|
+
trackPaymentInfoEntered(environment);
|
|
300
|
+
}, [environment]);
|
|
289
301
|
const [error, setError] = useState(null);
|
|
290
302
|
const [errorsObject, setErrorsObject] = useState(null);
|
|
291
303
|
const [customerErrorsObject, setCustomerErrorsObject] = useState(null);
|
|
@@ -559,14 +571,10 @@ export default function StoreModalUnifiedCheckout(_ref2) {
|
|
|
559
571
|
// Otherwise, only show when user has selected "Your client" option
|
|
560
572
|
const showLicenseeSection = order.customer && (hasCountryMismatch || !(order.licenseeIsBillingIdentity ?? false));
|
|
561
573
|
const handleCheckout = useCallback(async () => {
|
|
562
|
-
// Validate Stripe
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
// Stripe will show validation errors in the UI
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
574
|
+
// Validate the licensee address first; Stripe shows any errors in the UI.
|
|
575
|
+
// Not `elements.submit()`: a Stripe Tax order has no PaymentIntent, so
|
|
576
|
+
// this Elements group has no session and Stripe.js would throw.
|
|
577
|
+
if (!(await validateAddressElements(elements))) return;
|
|
570
578
|
|
|
571
579
|
// If licensee section is shown, save licensee data before navigating to checkout
|
|
572
580
|
if (showLicenseeSection) {
|
|
@@ -599,14 +607,8 @@ export default function StoreModalUnifiedCheckout(_ref2) {
|
|
|
599
607
|
return;
|
|
600
608
|
}
|
|
601
609
|
|
|
602
|
-
// Validate Stripe elements first
|
|
603
|
-
if (elements)
|
|
604
|
-
const result = await elements.submit();
|
|
605
|
-
if (result.error) {
|
|
606
|
-
// Stripe will show validation errors in the UI
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
+
// Validate Stripe elements first; Stripe shows any errors in the UI
|
|
611
|
+
if (!(await validateElements(elements))) return;
|
|
610
612
|
|
|
611
613
|
// If billing identity is not saved yet, don't proceed with payment
|
|
612
614
|
// The validation above will show errors for incomplete fields
|
|
@@ -798,6 +800,7 @@ export default function StoreModalUnifiedCheckout(_ref2) {
|
|
|
798
800
|
}, /*#__PURE__*/React.createElement(Sentry.ErrorBoundary, {
|
|
799
801
|
fallback: /*#__PURE__*/React.createElement("div", null, "Something went wrong")
|
|
800
802
|
}, /*#__PURE__*/React.createElement(PaymentElement, {
|
|
803
|
+
onChange: handlePaymentChange,
|
|
801
804
|
options: {
|
|
802
805
|
layout: {
|
|
803
806
|
type: 'tabs',
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface UseInfiniteScrollOptions {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
onLoadMore: () => void;
|
|
5
|
+
rootRef?: React.RefObject<Element | null>;
|
|
6
|
+
rootMargin?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Calls `onLoadMore` when a sentinel element scrolls into view, for lists that
|
|
10
|
+
* page as the visitor reaches the end of them.
|
|
11
|
+
*
|
|
12
|
+
* Returns a ref callback to put on that sentinel — render it after the last
|
|
13
|
+
* item, and only while there is more to load:
|
|
14
|
+
*
|
|
15
|
+
* const sentinelRef = useInfiniteScroll({
|
|
16
|
+
* enabled: hasNext && !isLoadingNext,
|
|
17
|
+
* onLoadMore: () => loadNext(25),
|
|
18
|
+
* rootRef: scrollContainer,
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* {hasNext && <div ref={sentinelRef} />}
|
|
22
|
+
*
|
|
23
|
+
* The sentinel is tracked as state rather than a ref so that mounting and
|
|
24
|
+
* unmounting it re-runs the effect; `onLoadMore` is read through a ref, so a
|
|
25
|
+
* fresh callback on every render doesn't tear the observer down and rebuild it.
|
|
26
|
+
*/
|
|
27
|
+
declare const useInfiniteScroll: ({ enabled, onLoadMore, rootRef, rootMargin, }: UseInfiniteScrollOptions) => ((node: Element | null) => void);
|
|
28
|
+
export default useInfiniteScroll;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* Calls `onLoadMore` when a sentinel element scrolls into view, for lists that
|
|
6
|
+
* page as the visitor reaches the end of them.
|
|
7
|
+
*
|
|
8
|
+
* Returns a ref callback to put on that sentinel — render it after the last
|
|
9
|
+
* item, and only while there is more to load:
|
|
10
|
+
*
|
|
11
|
+
* const sentinelRef = useInfiniteScroll({
|
|
12
|
+
* enabled: hasNext && !isLoadingNext,
|
|
13
|
+
* onLoadMore: () => loadNext(25),
|
|
14
|
+
* rootRef: scrollContainer,
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* {hasNext && <div ref={sentinelRef} />}
|
|
18
|
+
*
|
|
19
|
+
* The sentinel is tracked as state rather than a ref so that mounting and
|
|
20
|
+
* unmounting it re-runs the effect; `onLoadMore` is read through a ref, so a
|
|
21
|
+
* fresh callback on every render doesn't tear the observer down and rebuild it.
|
|
22
|
+
*/
|
|
23
|
+
const useInfiniteScroll = _ref => {
|
|
24
|
+
let {
|
|
25
|
+
enabled,
|
|
26
|
+
onLoadMore,
|
|
27
|
+
rootRef,
|
|
28
|
+
rootMargin
|
|
29
|
+
} = _ref;
|
|
30
|
+
const [sentinel, setSentinel] = useState(null);
|
|
31
|
+
const savedOnLoadMore = useRef(onLoadMore);
|
|
32
|
+
savedOnLoadMore.current = onLoadMore;
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!sentinel || !enabled) return;
|
|
35
|
+
// Absent while server-rendering, and in test environments without one.
|
|
36
|
+
if (typeof IntersectionObserver === 'undefined') return;
|
|
37
|
+
const observer = new IntersectionObserver(entries => {
|
|
38
|
+
if (entries.some(entry => entry.isIntersecting)) {
|
|
39
|
+
savedOnLoadMore.current();
|
|
40
|
+
}
|
|
41
|
+
}, {
|
|
42
|
+
root: (rootRef === null || rootRef === void 0 ? void 0 : rootRef.current) ?? null,
|
|
43
|
+
rootMargin
|
|
44
|
+
});
|
|
45
|
+
observer.observe(sentinel);
|
|
46
|
+
return () => observer.disconnect();
|
|
47
|
+
}, [sentinel, enabled, rootRef, rootMargin]);
|
|
48
|
+
return setSentinel;
|
|
49
|
+
};
|
|
50
|
+
export default useInfiniteScroll;
|