fontdue-js 3.11.0 → 3.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/__generated__/CustomerLoginFormLoginMutation.graphql.d.ts +2 -1
- package/dist/__generated__/CustomerLoginFormLoginMutation.graphql.js +1 -1
- package/dist/__generated__/CustomerLoginFormRecaptchaQuery.graphql.d.ts +21 -0
- package/dist/__generated__/CustomerLoginFormRecaptchaQuery.graphql.js +86 -0
- package/dist/__generated__/CustomerSignInElementQuery.graphql.d.ts +22 -0
- package/dist/__generated__/CustomerSignInElementQuery.graphql.js +87 -0
- package/dist/__tests__/customerLoginRecaptcha.test.js +113 -0
- package/dist/__tests__/customerSignInElement.test.js +155 -0
- package/dist/__tests__/customerSignInRequest.test.js +85 -0
- package/dist/__tests__/stripeCheckoutError.test.js +110 -0
- package/dist/__tests__/typeTesterHeightAnimation.test.js +124 -0
- package/dist/__tests__/typeTesterPrefetchStyles.test.js +108 -0
- package/dist/__tests__/typeTesterShyFeaturesClose.test.js +133 -0
- package/dist/components/CustomerLoginForm/index.js +106 -11
- package/dist/components/CustomerSignIn/CustomerSignInElement.d.ts +22 -0
- package/dist/components/CustomerSignIn/CustomerSignInElement.js +132 -0
- package/dist/components/CustomerSignIn/index.js +4 -2
- package/dist/components/Root/index.js +2 -0
- package/dist/components/StripeProvider/index.js +19 -2
- package/dist/components/TypeTester/TypeTesterFeatures.js +27 -44
- package/dist/components/TypeTester/TypeTesterStyleSelect.js +6 -1
- package/dist/components/TypeTester/useCloseWhenToolbarHides.d.ts +17 -0
- package/dist/components/TypeTester/useCloseWhenToolbarHides.js +79 -0
- package/dist/components/TypeTester/useHeightAnimation.d.ts +27 -0
- package/dist/components/TypeTester/useHeightAnimation.js +85 -0
- package/dist/components/TypeTester/usePrefetchWhenNear.d.ts +14 -0
- package/dist/components/TypeTester/usePrefetchWhenNear.js +69 -0
- package/dist/fontdue.css +2 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
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); }
|
|
4
|
+
import _CustomerSignInElementQuery from "../../__generated__/CustomerSignInElementQuery.graphql.js";
|
|
5
|
+
import React, { Suspense, useCallback, useState, useSyncExternalStore } from 'react';
|
|
6
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
7
|
+
import { graphql, useLazyLoadQuery } from 'react-relay';
|
|
8
|
+
import CustomerSignIn from './index.js';
|
|
9
|
+
import { clearTokens, getTokens, subscribeToTokens } from '../../auth/tokenStore.js';
|
|
10
|
+
import { useFontdueUrl } from '../UrlContext.js';
|
|
11
|
+
const query = (_CustomerSignInElementQuery.hash && _CustomerSignInElementQuery.hash !== "e7d4a8fb8d19581d6bc36f29090230af" && console.error("The definition of 'CustomerSignInElementQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CustomerSignInElementQuery);
|
|
12
|
+
|
|
13
|
+
/** Dispatched on `window` once the customer is signed in; `detail.customer`. */
|
|
14
|
+
export const SIGNED_IN_EVENT = 'fontdue:customer-signed-in';
|
|
15
|
+
/** Dispatched on `window` when the customer clicks the element's sign-out button. */
|
|
16
|
+
export const SIGNED_OUT_EVENT = 'fontdue:customer-signed-out';
|
|
17
|
+
/**
|
|
18
|
+
* `<fontdue-customer-sign-in>`: the script-tag wrapper around `CustomerSignIn`.
|
|
19
|
+
*
|
|
20
|
+
* A page has no React of its own to read `useCurrentCustomer()` with, so the
|
|
21
|
+
* element shows the signed-in state itself – who is signed in and a way out –
|
|
22
|
+
* and tells the page about both changes with window events, as `setConsent`
|
|
23
|
+
* does with `fontdue:consent`.
|
|
24
|
+
*
|
|
25
|
+
* Not in the npm package's `exports`: sign-in is script-tag only for now.
|
|
26
|
+
*/
|
|
27
|
+
export default function CustomerSignInElement(_ref) {
|
|
28
|
+
let {
|
|
29
|
+
signOutLabel = 'Sign out',
|
|
30
|
+
...props
|
|
31
|
+
} = _ref;
|
|
32
|
+
const url = useFontdueUrl();
|
|
33
|
+
// The script tag never renders on a server, so reading the store during
|
|
34
|
+
// render can't cause a hydration mismatch – and it means a returning
|
|
35
|
+
// customer never sees the form flash before their session is read.
|
|
36
|
+
const accessToken = useSyncExternalStore(subscribeToTokens, () => {
|
|
37
|
+
var _getTokens;
|
|
38
|
+
return ((_getTokens = getTokens(url)) === null || _getTokens === void 0 ? void 0 : _getTokens.accessToken) ?? null;
|
|
39
|
+
}, () => null);
|
|
40
|
+
// The customer `verifyLoginCode` just returned, tied to the token it came
|
|
41
|
+
// with, so the signed-in state shows at once instead of after a lookup. Any
|
|
42
|
+
// other session – a page load, a sign-in in another element, a renewed
|
|
43
|
+
// token – is looked up.
|
|
44
|
+
const [justSignedIn, setJustSignedIn] = useState(null);
|
|
45
|
+
const handleSignedIn = useCallback(customer => {
|
|
46
|
+
if (customer) {
|
|
47
|
+
var _getTokens2;
|
|
48
|
+
setJustSignedIn({
|
|
49
|
+
accessToken: (_getTokens2 = getTokens(url)) === null || _getTokens2 === void 0 ? void 0 : _getTokens2.accessToken,
|
|
50
|
+
customer
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
window.dispatchEvent(new CustomEvent(SIGNED_IN_EVENT, {
|
|
54
|
+
detail: {
|
|
55
|
+
customer
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
}, [url]);
|
|
59
|
+
const signOut = useCallback(() => {
|
|
60
|
+
clearTokens(url);
|
|
61
|
+
setJustSignedIn(null);
|
|
62
|
+
window.dispatchEvent(new CustomEvent(SIGNED_OUT_EVENT));
|
|
63
|
+
}, [url]);
|
|
64
|
+
const form = /*#__PURE__*/React.createElement(CustomerSignIn, _extends({}, props, {
|
|
65
|
+
onSignedIn: handleSignedIn
|
|
66
|
+
}));
|
|
67
|
+
if (accessToken == null) return form;
|
|
68
|
+
if (justSignedIn && justSignedIn.accessToken === accessToken) {
|
|
69
|
+
return /*#__PURE__*/React.createElement(SignedIn, {
|
|
70
|
+
customer: justSignedIn.customer,
|
|
71
|
+
signOut: signOut,
|
|
72
|
+
signOutLabel: signOutLabel
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A lookup that fails leaves the visitor able to sign in again rather than
|
|
77
|
+
// facing an empty element.
|
|
78
|
+
return /*#__PURE__*/React.createElement(ErrorBoundary, {
|
|
79
|
+
fallback: form,
|
|
80
|
+
resetKeys: [accessToken]
|
|
81
|
+
}, /*#__PURE__*/React.createElement(Suspense, {
|
|
82
|
+
fallback: null
|
|
83
|
+
}, /*#__PURE__*/React.createElement(SignedInLookup, {
|
|
84
|
+
key: accessToken,
|
|
85
|
+
form: form,
|
|
86
|
+
signOut: signOut,
|
|
87
|
+
signOutLabel: signOutLabel
|
|
88
|
+
})));
|
|
89
|
+
}
|
|
90
|
+
function SignedInLookup(_ref2) {
|
|
91
|
+
var _data$viewer;
|
|
92
|
+
let {
|
|
93
|
+
form,
|
|
94
|
+
signOut,
|
|
95
|
+
signOutLabel
|
|
96
|
+
} = _ref2;
|
|
97
|
+
// network-only: who is signed in *now*, never a store hit left over from the
|
|
98
|
+
// previous customer.
|
|
99
|
+
const data = useLazyLoadQuery(query, {}, {
|
|
100
|
+
fetchPolicy: 'network-only'
|
|
101
|
+
});
|
|
102
|
+
const customer = (_data$viewer = data.viewer) === null || _data$viewer === void 0 ? void 0 : _data$viewer.currentCustomer;
|
|
103
|
+
|
|
104
|
+
// A token the server no longer honours is cleared by the network layer,
|
|
105
|
+
// which re-renders the element signed out. A null here without that is a
|
|
106
|
+
// session that names nobody; offer the form.
|
|
107
|
+
if (!customer) return form;
|
|
108
|
+
return /*#__PURE__*/React.createElement(SignedIn, {
|
|
109
|
+
customer: customer,
|
|
110
|
+
signOut: signOut,
|
|
111
|
+
signOutLabel: signOutLabel
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function SignedIn(_ref3) {
|
|
115
|
+
let {
|
|
116
|
+
customer,
|
|
117
|
+
signOut,
|
|
118
|
+
signOutLabel
|
|
119
|
+
} = _ref3;
|
|
120
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
121
|
+
className: "customer-sign-in",
|
|
122
|
+
"data-step": "signed-in"
|
|
123
|
+
}, /*#__PURE__*/React.createElement("p", {
|
|
124
|
+
className: "customer-sign-in__intro"
|
|
125
|
+
}, "Signed in as ", customer.email ?? customer.name, "."), /*#__PURE__*/React.createElement("div", {
|
|
126
|
+
className: "customer-sign-in__actions"
|
|
127
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
128
|
+
className: "customer-sign-in__back",
|
|
129
|
+
type: "button",
|
|
130
|
+
onClick: signOut
|
|
131
|
+
}, signOutLabel)));
|
|
132
|
+
}
|
|
@@ -54,9 +54,11 @@ function CustomerSignInFields(_ref) {
|
|
|
54
54
|
email
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
|
-
onCompleted: res => {
|
|
57
|
+
onCompleted: (res, errors) => {
|
|
58
58
|
var _res$requestLoginCode;
|
|
59
|
-
if (
|
|
59
|
+
if (errors && errors.length > 0) {
|
|
60
|
+
setError(errors[0].message);
|
|
61
|
+
} else if ((_res$requestLoginCode = res.requestLoginCode) !== null && _res$requestLoginCode !== void 0 && _res$requestLoginCode.success) {
|
|
60
62
|
setStep('code');
|
|
61
63
|
} else {
|
|
62
64
|
// The only unsuccessful answer is the rate limit, and it is about the
|
|
@@ -25,6 +25,8 @@ const customElementMap = {
|
|
|
25
25
|
'fontdue-cookie-notification': /*#__PURE__*/lazy(() => retryImport(() => import('../CookieNotification/index.js'))),
|
|
26
26
|
'fontdue-font-families': /*#__PURE__*/lazy(() => retryImport(() => import('../FontFamilies/index.js'))),
|
|
27
27
|
'fontdue-customer-login-form': /*#__PURE__*/lazy(() => retryImport(() => import('../CustomerLoginForm/index.js'))),
|
|
28
|
+
// Script tag only: CustomerSignIn isn't in the npm package's `exports` yet.
|
|
29
|
+
'fontdue-customer-sign-in': /*#__PURE__*/lazy(() => retryImport(() => import('../CustomerSignIn/CustomerSignInElement.js'))),
|
|
28
30
|
'fontdue-newsletter-signup': /*#__PURE__*/lazy(() => retryImport(() => import('../NewsletterSignup/NewsletterSignupElement.js'))),
|
|
29
31
|
'fontdue-node-password-form': /*#__PURE__*/lazy(() => retryImport(() => import('../NodePasswordForm/NodePasswordFormElement.js'))),
|
|
30
32
|
// @ts-ignore
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import _StripeProviderCreateCheckoutSessionMutation from "../../__generated__/StripeProviderCreateCheckoutSessionMutation.graphql.js";
|
|
2
2
|
import _StripeProvider_viewer from "../../__generated__/StripeProvider_viewer.graphql.js";
|
|
3
|
-
import React, { useCallback, useContext, useRef } from 'react';
|
|
3
|
+
import React, { useCallback, useContext, useRef, useState } from 'react';
|
|
4
4
|
import { commitMutation, graphql, useFragment, useRelayEnvironment } from 'react-relay';
|
|
5
5
|
import { Elements, EmbeddedCheckoutProvider } from '@stripe/react-stripe-js';
|
|
6
6
|
import { loadStripe } from '@stripe/stripe-js';
|
|
7
7
|
import { parseCustomProperties } from '../../utils.js';
|
|
8
8
|
import ConfigContext from '../ConfigContext.js';
|
|
9
|
+
import StoreModalPageContainer from '../elements/StoreModalPageContainer/index.js';
|
|
10
|
+
const CHECKOUT_ERROR = 'We couldn’t start the checkout – please try again';
|
|
9
11
|
const StripeProvider = _ref => {
|
|
10
12
|
var _data$currentOrder, _data$settings, _data$settings$uiFont, _data$settings$uiFont2, _data$settings$uiFont3, _data$settings2, _data$currentOrder2, _data$currentOrder2$s, _data$themeConfig;
|
|
11
13
|
let {
|
|
@@ -26,6 +28,7 @@ const StripeProvider = _ref => {
|
|
|
26
28
|
const {
|
|
27
29
|
stripe: config
|
|
28
30
|
} = useContext(ConfigContext);
|
|
31
|
+
const [checkoutError, setCheckoutError] = useState(null);
|
|
29
32
|
let customFontSource;
|
|
30
33
|
const woffSrc = (_data$settings = data.settings) === null || _data$settings === void 0 ? void 0 : (_data$settings$uiFont = _data$settings.uiFontStyle) === null || _data$settings$uiFont === void 0 ? void 0 : (_data$settings$uiFont2 = _data$settings$uiFont.webfontSources) === null || _data$settings$uiFont2 === void 0 ? void 0 : (_data$settings$uiFont3 = _data$settings$uiFont2.find(source => (source === null || source === void 0 ? void 0 : source.format) === 'woff2')) === null || _data$settings$uiFont3 === void 0 ? void 0 : _data$settings$uiFont3.url;
|
|
31
34
|
let fontFamily = (_data$settings2 = data.settings) !== null && _data$settings2 !== void 0 && _data$settings2.uiFontStyle ? `${data.settings.uiFontStyle.cssFamily} ${data.settings.uiFontStyle.name}` : undefined;
|
|
@@ -50,16 +53,22 @@ const StripeProvider = _ref => {
|
|
|
50
53
|
commitMutation(environment, {
|
|
51
54
|
mutation: (_StripeProviderCreateCheckoutSessionMutation.hash && _StripeProviderCreateCheckoutSessionMutation.hash !== "2ca9a7fcfed1292117e3cd6d0d95cca6" && console.error("The definition of 'StripeProviderCreateCheckoutSessionMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StripeProviderCreateCheckoutSessionMutation),
|
|
52
55
|
variables: {},
|
|
53
|
-
onCompleted: response => {
|
|
56
|
+
onCompleted: (response, errors) => {
|
|
54
57
|
var _response$createOrder, _response$createOrder2, _response$createOrder3;
|
|
55
58
|
const clientSecret = (_response$createOrder = response.createOrderCheckoutSession) === null || _response$createOrder === void 0 ? void 0 : (_response$createOrder2 = _response$createOrder.order) === null || _response$createOrder2 === void 0 ? void 0 : (_response$createOrder3 = _response$createOrder2.stripeCheckoutSession) === null || _response$createOrder3 === void 0 ? void 0 : _response$createOrder3.clientSecret;
|
|
56
59
|
if (clientSecret) {
|
|
57
60
|
resolve(clientSecret);
|
|
58
61
|
} else {
|
|
62
|
+
var _errors$;
|
|
63
|
+
// Stripe refused the session (e.g. a total under its minimum
|
|
64
|
+
// charge): show its reason, which the embedded checkout would
|
|
65
|
+
// otherwise swallow into a generic error.
|
|
66
|
+
setCheckoutError((errors === null || errors === void 0 ? void 0 : (_errors$ = errors[0]) === null || _errors$ === void 0 ? void 0 : _errors$.message) ?? CHECKOUT_ERROR);
|
|
59
67
|
reject("Didn't find client secret");
|
|
60
68
|
}
|
|
61
69
|
},
|
|
62
70
|
onError: error => {
|
|
71
|
+
setCheckoutError(CHECKOUT_ERROR);
|
|
63
72
|
reject(`Error creating checkout session: ${error.message}`);
|
|
64
73
|
}
|
|
65
74
|
});
|
|
@@ -147,6 +156,14 @@ const StripeProvider = _ref => {
|
|
|
147
156
|
syncAddressCheckbox: 'none'
|
|
148
157
|
};
|
|
149
158
|
if (providerType === 'checkout') {
|
|
159
|
+
if (checkoutError) {
|
|
160
|
+
return /*#__PURE__*/React.createElement(StoreModalPageContainer, null, {
|
|
161
|
+
body: /*#__PURE__*/React.createElement("div", {
|
|
162
|
+
className: "store-modal__cart__error",
|
|
163
|
+
role: "alert"
|
|
164
|
+
}, checkoutError)
|
|
165
|
+
});
|
|
166
|
+
}
|
|
150
167
|
return /*#__PURE__*/React.createElement(EmbeddedCheckoutProvider, {
|
|
151
168
|
stripe: stripePromiseRef.current,
|
|
152
169
|
options: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import _TypeTesterFeatures_fontStyle from "../../__generated__/TypeTesterFeatures_fontStyle.graphql.js";
|
|
2
|
-
import React, { useContext, useEffect, useMemo, useRef
|
|
2
|
+
import React, { useContext, useEffect, useMemo, useRef } from 'react';
|
|
3
3
|
import { graphql, useFragment } from 'react-relay';
|
|
4
4
|
import Bullet from '../TypeTester/TypeTesterBullet.js';
|
|
5
5
|
import useTypeTesterState from './TypeTesterState.js';
|
|
@@ -7,38 +7,8 @@ import Checkbox from '../Checkbox/index.js';
|
|
|
7
7
|
import ConfigContext from '../ConfigContext.js';
|
|
8
8
|
import useFeaturesData from './useFeaturesData.js';
|
|
9
9
|
import TypeTesterVariableAxes from './TypeTesterVariableAxes.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
let duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 350;
|
|
13
|
-
const contentRef = useRef(null);
|
|
14
|
-
const [height, setHeight] = useState(isOpen ? 'auto' : 0);
|
|
15
|
-
const changed = useRef(false);
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
var _contentRef$current;
|
|
18
|
-
const contentRect = (_contentRef$current = contentRef.current) === null || _contentRef$current === void 0 ? void 0 : _contentRef$current.getBoundingClientRect();
|
|
19
|
-
if (!contentRect) return;
|
|
20
|
-
if (changed.current === false) {
|
|
21
|
-
// don't run initially. couldn't think of a better solution
|
|
22
|
-
changed.current = true;
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
setHeight(`${contentRect.height}px`);
|
|
26
|
-
const first = setTimeout(() => setHeight(isOpen ? `${contentRect.height}px` : 0));
|
|
27
|
-
const second = setTimeout(() => {
|
|
28
|
-
if (isOpen) {
|
|
29
|
-
setHeight('auto');
|
|
30
|
-
}
|
|
31
|
-
}, duration);
|
|
32
|
-
return () => {
|
|
33
|
-
clearTimeout(first);
|
|
34
|
-
clearTimeout(second);
|
|
35
|
-
};
|
|
36
|
-
}, [isOpen]);
|
|
37
|
-
return {
|
|
38
|
-
height,
|
|
39
|
-
contentRef
|
|
40
|
-
};
|
|
41
|
-
}
|
|
10
|
+
import useCloseWhenToolbarHides from './useCloseWhenToolbarHides.js';
|
|
11
|
+
import useHeightAnimation from './useHeightAnimation.js';
|
|
42
12
|
const shouldIncludeFeature = (supportedFeatures, feature) => {
|
|
43
13
|
if (!supportedFeatures) return false;
|
|
44
14
|
return supportedFeatures.indexOf(feature) >= 0;
|
|
@@ -74,6 +44,17 @@ function useFeaturesSurface(_ref) {
|
|
|
74
44
|
} = useFeaturesData({
|
|
75
45
|
fontStyle
|
|
76
46
|
});
|
|
47
|
+
|
|
48
|
+
// Every surface renders an element carrying this ref inside its tester, so
|
|
49
|
+
// a shy toolbar takes the open surface with it when it hides. The floating
|
|
50
|
+
// toolbar has no hover-shy tools to follow (it requires shy: 'focus' and
|
|
51
|
+
// sits outside the tester).
|
|
52
|
+
const surfaceRef = useRef(null);
|
|
53
|
+
useCloseWhenToolbarHides(surfaceRef, {
|
|
54
|
+
open: featuresOpen,
|
|
55
|
+
setOpen: setFeaturesOpen,
|
|
56
|
+
shy: config.toolsPosition === 'inline' ? config.shy : false
|
|
57
|
+
});
|
|
77
58
|
const selectionStyle = config.openTypeFeatures.selectionStyle;
|
|
78
59
|
const supportedFeatures = (fontFeatures === null || fontFeatures === void 0 ? void 0 : fontFeatures.supportedFeatures) ?? null;
|
|
79
60
|
const expandedFeatures = useMemo(() => showFeatures !== null && showFeatures !== void 0 && showFeatures.includes('*') ? supportedFeatures ?? [] : (showFeatures ?? []).filter(feature => shouldIncludeFeature(supportedFeatures, feature)), [showFeatures, supportedFeatures]);
|
|
@@ -109,7 +90,8 @@ function useFeaturesSurface(_ref) {
|
|
|
109
90
|
selectionStyle,
|
|
110
91
|
supportedFeatures,
|
|
111
92
|
expandedFeatures,
|
|
112
|
-
renderFeature
|
|
93
|
+
renderFeature,
|
|
94
|
+
surfaceRef
|
|
113
95
|
};
|
|
114
96
|
}
|
|
115
97
|
function TypeTesterFeaturesDropdown(_ref2) {
|
|
@@ -128,7 +110,8 @@ function TypeTesterFeaturesDropdown(_ref2) {
|
|
|
128
110
|
selectionStyle,
|
|
129
111
|
supportedFeatures,
|
|
130
112
|
expandedFeatures,
|
|
131
|
-
renderFeature
|
|
113
|
+
renderFeature,
|
|
114
|
+
surfaceRef
|
|
132
115
|
} = useFeaturesSurface({
|
|
133
116
|
id,
|
|
134
117
|
fontStyle: fontStyleKey,
|
|
@@ -191,6 +174,7 @@ function TypeTesterFeaturesDropdown(_ref2) {
|
|
|
191
174
|
if (!featuresOpen) {
|
|
192
175
|
return /*#__PURE__*/React.createElement("div", {
|
|
193
176
|
className: "type-tester__features-dropdown-anchor",
|
|
177
|
+
ref: surfaceRef,
|
|
194
178
|
"data-open": false,
|
|
195
179
|
onMouseDown: anchorMouseDown
|
|
196
180
|
});
|
|
@@ -221,6 +205,7 @@ function TypeTesterFeaturesDropdown(_ref2) {
|
|
|
221
205
|
// unlike the chromeless panel, an empty dropdown would be a visible box.
|
|
222
206
|
return /*#__PURE__*/React.createElement("div", {
|
|
223
207
|
className: "type-tester__features-dropdown-anchor",
|
|
208
|
+
ref: surfaceRef,
|
|
224
209
|
"data-open": featuresOpen,
|
|
225
210
|
onMouseDown: anchorMouseDown
|
|
226
211
|
}, dropdownFeatures.length > 0 || hasAxes ? /*#__PURE__*/React.createElement("div", {
|
|
@@ -251,15 +236,16 @@ function TypeTesterFeaturesPanel(_ref3) {
|
|
|
251
236
|
selectionStyle,
|
|
252
237
|
supportedFeatures,
|
|
253
238
|
expandedFeatures,
|
|
254
|
-
renderFeature
|
|
239
|
+
renderFeature,
|
|
240
|
+
surfaceRef
|
|
255
241
|
} = useFeaturesSurface({
|
|
256
242
|
id,
|
|
257
243
|
fontStyle: fontStyleKey,
|
|
258
244
|
features
|
|
259
245
|
});
|
|
260
246
|
const {
|
|
261
|
-
|
|
262
|
-
|
|
247
|
+
ref: heightRef,
|
|
248
|
+
style: heightStyle
|
|
263
249
|
} = useHeightAnimation(featuresOpen, 300);
|
|
264
250
|
let featuresColumns;
|
|
265
251
|
if ((_config$openTypeFeatu2 = config.openTypeFeatures) !== null && _config$openTypeFeatu2 !== void 0 && (_config$openTypeFeatu3 = _config$openTypeFeatu2.columns) !== null && _config$openTypeFeatu3 !== void 0 && _config$openTypeFeatu3.length) {
|
|
@@ -276,15 +262,12 @@ function TypeTesterFeaturesPanel(_ref3) {
|
|
|
276
262
|
}
|
|
277
263
|
return /*#__PURE__*/React.createElement("div", {
|
|
278
264
|
className: "type-tester__features-container",
|
|
265
|
+
ref: heightRef,
|
|
279
266
|
onMouseDown: e => e.preventDefault(),
|
|
280
267
|
"data-open": featuresOpen,
|
|
281
|
-
style:
|
|
282
|
-
height: featuresHeight,
|
|
283
|
-
overflow: featuresHeight === 'auto' ? 'visible' : 'hidden',
|
|
284
|
-
transition: `height 300ms ease-out`
|
|
285
|
-
}
|
|
268
|
+
style: heightStyle
|
|
286
269
|
}, /*#__PURE__*/React.createElement("div", {
|
|
287
|
-
ref:
|
|
270
|
+
ref: surfaceRef
|
|
288
271
|
}, axes ? /*#__PURE__*/React.createElement(TypeTesterVariableAxes, {
|
|
289
272
|
id: id,
|
|
290
273
|
axes: axes,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
2
|
import { compareVariableSettings, variableInstanceSettings } from '../../utils.js';
|
|
3
3
|
import Select from '../Select/index.js';
|
|
4
|
+
import usePrefetchWhenNear from './usePrefetchWhenNear.js';
|
|
4
5
|
const VARIABLE_AXES = 'VARIABLE_AXES';
|
|
5
6
|
const LOADING = '__loading__';
|
|
6
7
|
const TypeTesterStyleSelect = _ref => {
|
|
@@ -19,6 +20,8 @@ const TypeTesterStyleSelect = _ref => {
|
|
|
19
20
|
onSelectFamilyId,
|
|
20
21
|
includeVariableAxesOption
|
|
21
22
|
} = _ref;
|
|
23
|
+
const containerRef = useRef(null);
|
|
24
|
+
usePrefetchWhenNear(containerRef, onOpen);
|
|
22
25
|
const handleSelectFontStyle = selection => {
|
|
23
26
|
if (selection === VARIABLE_AXES) {
|
|
24
27
|
onSelectVariableAxes();
|
|
@@ -90,7 +93,9 @@ const TypeTesterStyleSelect = _ref => {
|
|
|
90
93
|
cb(e.target.value);
|
|
91
94
|
};
|
|
92
95
|
return /*#__PURE__*/React.createElement("div", {
|
|
96
|
+
ref: containerRef,
|
|
93
97
|
className: "type-tester-select",
|
|
98
|
+
onPointerEnter: () => onOpen === null || onOpen === void 0 ? void 0 : onOpen(),
|
|
94
99
|
"data-loading": loading || undefined,
|
|
95
100
|
"aria-busy": loading || undefined,
|
|
96
101
|
style: {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
export declare const CLOSE_DELAY = 250;
|
|
3
|
+
/**
|
|
4
|
+
* Closes an open features surface (panel or dropdown) when a shy toolbar hides,
|
|
5
|
+
* so it never stays open without the button that toggles it.
|
|
6
|
+
*
|
|
7
|
+
* `shy` hides the toolbar's tools in CSS (`_type-tester.scss`): they show while
|
|
8
|
+
* the tester is hovered (`'hover'` only) or holds focus. This mirrors that
|
|
9
|
+
* predicate, re-checked after `CLOSE_DELAY` whenever the pointer leaves, focus
|
|
10
|
+
* leaves, or a touch lands outside the tester (touch has no hover to leave).
|
|
11
|
+
* `ref` is any element the surface always renders inside its `.type-tester`.
|
|
12
|
+
*/
|
|
13
|
+
export default function useCloseWhenToolbarHides(ref: RefObject<HTMLElement | null>, { open, setOpen, shy, }: {
|
|
14
|
+
open: boolean;
|
|
15
|
+
setOpen: (value: boolean) => void;
|
|
16
|
+
shy: boolean | 'hover' | 'focus';
|
|
17
|
+
}): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// How long the toolbar has to stay hidden before the features surface closes.
|
|
4
|
+
// Closing the panel animates its height and moves the page below it, so a
|
|
5
|
+
// cursor that only brushes past the tester's edge shouldn't trigger it.
|
|
6
|
+
export const CLOSE_DELAY = 250;
|
|
7
|
+
const matchesHover = element => {
|
|
8
|
+
try {
|
|
9
|
+
return element.matches(':hover');
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const focusInside = element => {
|
|
15
|
+
const root = element.getRootNode();
|
|
16
|
+
return Boolean(root.activeElement && element.contains(root.activeElement));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Closes an open features surface (panel or dropdown) when a shy toolbar hides,
|
|
21
|
+
* so it never stays open without the button that toggles it.
|
|
22
|
+
*
|
|
23
|
+
* `shy` hides the toolbar's tools in CSS (`_type-tester.scss`): they show while
|
|
24
|
+
* the tester is hovered (`'hover'` only) or holds focus. This mirrors that
|
|
25
|
+
* predicate, re-checked after `CLOSE_DELAY` whenever the pointer leaves, focus
|
|
26
|
+
* leaves, or a touch lands outside the tester (touch has no hover to leave).
|
|
27
|
+
* `ref` is any element the surface always renders inside its `.type-tester`.
|
|
28
|
+
*/
|
|
29
|
+
export default function useCloseWhenToolbarHides(ref, _ref) {
|
|
30
|
+
let {
|
|
31
|
+
open,
|
|
32
|
+
setOpen,
|
|
33
|
+
shy
|
|
34
|
+
} = _ref;
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
var _ref$current;
|
|
37
|
+
if (!open || !shy) return;
|
|
38
|
+
const tester = (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.closest('.type-tester');
|
|
39
|
+
if (!tester) return;
|
|
40
|
+
const hoverReveals = shy === true || shy === 'hover';
|
|
41
|
+
let hovered = matchesHover(tester);
|
|
42
|
+
let timer;
|
|
43
|
+
const toolbarShown = () => hoverReveals && hovered || focusInside(tester);
|
|
44
|
+
const scheduleCheck = () => {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
timer = setTimeout(() => {
|
|
47
|
+
if (!toolbarShown()) setOpen(false);
|
|
48
|
+
}, CLOSE_DELAY);
|
|
49
|
+
};
|
|
50
|
+
const handlePointerEnter = event => {
|
|
51
|
+
if (event.pointerType !== 'mouse') return;
|
|
52
|
+
hovered = true;
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
};
|
|
55
|
+
const handlePointerLeave = event => {
|
|
56
|
+
if (event.pointerType !== 'mouse') return;
|
|
57
|
+
hovered = false;
|
|
58
|
+
scheduleCheck();
|
|
59
|
+
};
|
|
60
|
+
const handleOutsidePointerDown = event => {
|
|
61
|
+
var _event$composedPath;
|
|
62
|
+
if (event.pointerType === 'mouse') return;
|
|
63
|
+
const target = ((_event$composedPath = event.composedPath) === null || _event$composedPath === void 0 ? void 0 : _event$composedPath.call(event)[0]) ?? null;
|
|
64
|
+
if (target && tester.contains(target)) return;
|
|
65
|
+
scheduleCheck();
|
|
66
|
+
};
|
|
67
|
+
tester.addEventListener('pointerenter', handlePointerEnter);
|
|
68
|
+
tester.addEventListener('pointerleave', handlePointerLeave);
|
|
69
|
+
tester.addEventListener('focusout', scheduleCheck);
|
|
70
|
+
document.addEventListener('pointerdown', handleOutsidePointerDown);
|
|
71
|
+
return () => {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
tester.removeEventListener('pointerenter', handlePointerEnter);
|
|
74
|
+
tester.removeEventListener('pointerleave', handlePointerLeave);
|
|
75
|
+
tester.removeEventListener('focusout', scheduleCheck);
|
|
76
|
+
document.removeEventListener('pointerdown', handleOutsidePointerDown);
|
|
77
|
+
};
|
|
78
|
+
}, [ref, open, setOpen, shy]);
|
|
79
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Animates a disclosure's height between 0 and its content height.
|
|
4
|
+
*
|
|
5
|
+
* Driven by the Web Animations API on the element itself rather than through
|
|
6
|
+
* React state: a CSS transition needs a committed pixel height before the
|
|
7
|
+
* target one, and `auto` → `0` in a single style recalc (the old
|
|
8
|
+
* setState-then-setTimeout approach) snaps shut with no animation. Here every
|
|
9
|
+
* run starts from the height actually on screen, so a close that interrupts an
|
|
10
|
+
* open (or the reverse) reverses smoothly from where it is.
|
|
11
|
+
*
|
|
12
|
+
* Settled open, the element is `height: auto; overflow: visible`, so content
|
|
13
|
+
* that changes size while open (axis sliders, wrapped features) still fits and
|
|
14
|
+
* slider tooltips aren't clipped. Settled closed, it is `0px` + `hidden`.
|
|
15
|
+
*
|
|
16
|
+
* Spread `style` onto the element (it only carries the initial state and never
|
|
17
|
+
* changes, so React never overwrites the inline styles set here) and attach
|
|
18
|
+
* `ref`.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const { ref, style } = useHeightAnimation(open, 300);
|
|
22
|
+
* return <div ref={ref} style={style}>{children}</div>;
|
|
23
|
+
*/
|
|
24
|
+
export default function useHeightAnimation(isOpen: boolean, duration?: number): {
|
|
25
|
+
ref: import("react").RefObject<HTMLDivElement | null>;
|
|
26
|
+
style: CSSProperties;
|
|
27
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
const prefersReducedMotion = () => {
|
|
3
|
+
var _window$matchMedia, _window;
|
|
4
|
+
return typeof window !== 'undefined' && ((_window$matchMedia = (_window = window).matchMedia) === null || _window$matchMedia === void 0 ? void 0 : _window$matchMedia.call(_window, '(prefers-reduced-motion: reduce)').matches) === true;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Animates a disclosure's height between 0 and its content height.
|
|
9
|
+
*
|
|
10
|
+
* Driven by the Web Animations API on the element itself rather than through
|
|
11
|
+
* React state: a CSS transition needs a committed pixel height before the
|
|
12
|
+
* target one, and `auto` → `0` in a single style recalc (the old
|
|
13
|
+
* setState-then-setTimeout approach) snaps shut with no animation. Here every
|
|
14
|
+
* run starts from the height actually on screen, so a close that interrupts an
|
|
15
|
+
* open (or the reverse) reverses smoothly from where it is.
|
|
16
|
+
*
|
|
17
|
+
* Settled open, the element is `height: auto; overflow: visible`, so content
|
|
18
|
+
* that changes size while open (axis sliders, wrapped features) still fits and
|
|
19
|
+
* slider tooltips aren't clipped. Settled closed, it is `0px` + `hidden`.
|
|
20
|
+
*
|
|
21
|
+
* Spread `style` onto the element (it only carries the initial state and never
|
|
22
|
+
* changes, so React never overwrites the inline styles set here) and attach
|
|
23
|
+
* `ref`.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* const { ref, style } = useHeightAnimation(open, 300);
|
|
27
|
+
* return <div ref={ref} style={style}>{children}</div>;
|
|
28
|
+
*/
|
|
29
|
+
export default function useHeightAnimation(isOpen) {
|
|
30
|
+
let duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 300;
|
|
31
|
+
const ref = useRef(null);
|
|
32
|
+
const animation = useRef(null);
|
|
33
|
+
const mounted = useRef(false);
|
|
34
|
+
const [style] = useState(() => isOpen ? {
|
|
35
|
+
height: 'auto',
|
|
36
|
+
overflow: 'visible'
|
|
37
|
+
} : {
|
|
38
|
+
height: 0,
|
|
39
|
+
overflow: 'hidden'
|
|
40
|
+
});
|
|
41
|
+
useLayoutEffect(() => {
|
|
42
|
+
var _animation$current;
|
|
43
|
+
const el = ref.current;
|
|
44
|
+
if (!el) return;
|
|
45
|
+
if (!mounted.current) {
|
|
46
|
+
mounted.current = true;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Measure before cancelling: a running animation's current frame is what
|
|
51
|
+
// the visitor sees, and cancelling it would jump to the settled style.
|
|
52
|
+
const from = el.getBoundingClientRect().height;
|
|
53
|
+
(_animation$current = animation.current) === null || _animation$current === void 0 ? void 0 : _animation$current.cancel();
|
|
54
|
+
animation.current = null;
|
|
55
|
+
el.style.height = 'auto';
|
|
56
|
+
const to = isOpen ? el.getBoundingClientRect().height : 0;
|
|
57
|
+
el.style.height = isOpen ? 'auto' : '0px';
|
|
58
|
+
el.style.overflow = 'hidden';
|
|
59
|
+
const settle = () => {
|
|
60
|
+
if (isOpen) el.style.overflow = 'visible';
|
|
61
|
+
};
|
|
62
|
+
if (from === to || typeof el.animate !== 'function' || prefersReducedMotion()) {
|
|
63
|
+
settle();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const run = el.animate([{
|
|
67
|
+
height: `${from}px`
|
|
68
|
+
}, {
|
|
69
|
+
height: `${to}px`
|
|
70
|
+
}], {
|
|
71
|
+
duration,
|
|
72
|
+
easing: 'ease-out'
|
|
73
|
+
});
|
|
74
|
+
animation.current = run;
|
|
75
|
+
run.onfinish = () => {
|
|
76
|
+
if (animation.current !== run) return;
|
|
77
|
+
animation.current = null;
|
|
78
|
+
settle();
|
|
79
|
+
};
|
|
80
|
+
}, [isOpen, duration]);
|
|
81
|
+
return {
|
|
82
|
+
ref,
|
|
83
|
+
style
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const PREFETCH_ROOT_MARGIN = "300px";
|
|
2
|
+
export declare const PREFETCH_IDLE_TIMEOUT = 2000;
|
|
3
|
+
/**
|
|
4
|
+
* Calls `prefetch` once, at idle time, after the element first comes near the
|
|
5
|
+
* viewport. Native `<select>` menus snapshot their options when they open (in
|
|
6
|
+
* Safari the open menu never updates), so data a dropdown needs has to be
|
|
7
|
+
* there before the first click – fetching on mousedown is too late. Waiting
|
|
8
|
+
* for idle keeps the request off the initial render, and waiting for the
|
|
9
|
+
* viewport keeps a long page of testers from fetching for every one at once.
|
|
10
|
+
*
|
|
11
|
+
* const ref = useRef<HTMLDivElement>(null);
|
|
12
|
+
* usePrefetchWhenNear(ref, loadOptions);
|
|
13
|
+
*/
|
|
14
|
+
export default function usePrefetchWhenNear(ref: React.RefObject<Element | null>, prefetch: (() => void) | undefined): void;
|