fontdue-js 3.10.1 → 3.12.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 +9 -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__/TestFontsForm_Query.graphql.d.ts +6 -1
- package/dist/__generated__/TestFontsForm_Query.graphql.js +25 -9
- package/dist/__tests__/customerLoginRecaptcha.test.js +113 -0
- package/dist/__tests__/testFontsFamilyChoice.test.js +74 -1
- package/dist/__tests__/typeTesterHeightAnimation.test.js +124 -0
- package/dist/__tests__/typeTesterShyFeaturesClose.test.js +133 -0
- package/dist/components/ConfigContext.d.ts +4 -0
- package/dist/components/ConfigContext.js +3 -2
- package/dist/components/CustomerLoginForm/index.js +106 -11
- package/dist/components/TestFontsForm/TestFontsFormElement.d.ts +2 -1
- package/dist/components/TestFontsForm/TestFontsFormElement.js +3 -0
- package/dist/components/TestFontsForm/familyGroups.d.ts +16 -0
- package/dist/components/TestFontsForm/familyGroups.js +34 -0
- package/dist/components/TestFontsForm/index.d.ts +2 -1
- package/dist/components/TestFontsForm/index.js +51 -21
- package/dist/components/TypeTester/TypeTesterFeatures.js +27 -44
- 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/fontLoader.d.ts +8 -6
- package/dist/fontLoader.js +8 -6
- package/dist/fontdue.css +3 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act, useRef, useState } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
5
|
+
import useCloseWhenToolbarHides, { CLOSE_DELAY } from '../components/TypeTester/useCloseWhenToolbarHides.js';
|
|
6
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
7
|
+
let open = false;
|
|
8
|
+
|
|
9
|
+
// The tester's markup reduced to what the hook reads: the `.type-tester` root,
|
|
10
|
+
// something focusable inside it (the editable text) and the surface's element.
|
|
11
|
+
function Tester(_ref) {
|
|
12
|
+
let {
|
|
13
|
+
shy
|
|
14
|
+
} = _ref;
|
|
15
|
+
const [featuresOpen, setFeaturesOpen] = useState(true);
|
|
16
|
+
open = featuresOpen;
|
|
17
|
+
const ref = useRef(null);
|
|
18
|
+
useCloseWhenToolbarHides(ref, {
|
|
19
|
+
open: featuresOpen,
|
|
20
|
+
setOpen: setFeaturesOpen,
|
|
21
|
+
shy
|
|
22
|
+
});
|
|
23
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
24
|
+
className: "type-tester"
|
|
25
|
+
}, /*#__PURE__*/React.createElement("input", {
|
|
26
|
+
className: "text"
|
|
27
|
+
}), /*#__PURE__*/React.createElement("div", {
|
|
28
|
+
className: "type-tester__features-container",
|
|
29
|
+
ref: ref
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
describe('features surface under a shy toolbar', () => {
|
|
33
|
+
let root;
|
|
34
|
+
let container;
|
|
35
|
+
let outside;
|
|
36
|
+
const tester = () => container.querySelector('.type-tester');
|
|
37
|
+
const text = () => container.querySelector('.text');
|
|
38
|
+
const pointer = function (target, type) {
|
|
39
|
+
let pointerType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'mouse';
|
|
40
|
+
return act(() => {
|
|
41
|
+
target.dispatchEvent(new PointerEvent(type, {
|
|
42
|
+
pointerType,
|
|
43
|
+
bubbles: true
|
|
44
|
+
}));
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const wait = ms => act(() => vi.advanceTimersByTime(ms));
|
|
48
|
+
const render = shy => act(() => root.render( /*#__PURE__*/React.createElement(Tester, {
|
|
49
|
+
shy: shy
|
|
50
|
+
})));
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.useFakeTimers();
|
|
53
|
+
container = document.createElement('div');
|
|
54
|
+
outside = document.createElement('button');
|
|
55
|
+
document.body.append(container, outside);
|
|
56
|
+
root = createRoot(container);
|
|
57
|
+
});
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
act(() => root.unmount());
|
|
60
|
+
container.remove();
|
|
61
|
+
outside.remove();
|
|
62
|
+
vi.useRealTimers();
|
|
63
|
+
});
|
|
64
|
+
describe("shy: 'hover'", () => {
|
|
65
|
+
beforeEach(async () => {
|
|
66
|
+
await render('hover');
|
|
67
|
+
await pointer(tester(), 'pointerenter');
|
|
68
|
+
});
|
|
69
|
+
it('closes once the pointer has been gone for the delay', async () => {
|
|
70
|
+
await pointer(tester(), 'pointerleave');
|
|
71
|
+
await wait(CLOSE_DELAY - 1);
|
|
72
|
+
expect(open).toBe(true);
|
|
73
|
+
await wait(1);
|
|
74
|
+
expect(open).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
it('stays open when the pointer comes back within the delay', async () => {
|
|
77
|
+
await pointer(tester(), 'pointerleave');
|
|
78
|
+
await wait(CLOSE_DELAY - 50);
|
|
79
|
+
await pointer(tester(), 'pointerenter');
|
|
80
|
+
await wait(CLOSE_DELAY);
|
|
81
|
+
expect(open).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
it('stays open while the tester holds focus, then closes when focus leaves', async () => {
|
|
84
|
+
act(() => text().focus());
|
|
85
|
+
await pointer(tester(), 'pointerleave');
|
|
86
|
+
await wait(CLOSE_DELAY);
|
|
87
|
+
expect(open).toBe(true);
|
|
88
|
+
act(() => outside.focus());
|
|
89
|
+
await wait(CLOSE_DELAY);
|
|
90
|
+
expect(open).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
it('stays open when focus leaves while the pointer is still over it', async () => {
|
|
93
|
+
act(() => text().focus());
|
|
94
|
+
act(() => outside.focus());
|
|
95
|
+
await wait(CLOSE_DELAY);
|
|
96
|
+
expect(open).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it('on a touch device (no hover), closes on a touch outside the tester', async () => {
|
|
100
|
+
await render('hover');
|
|
101
|
+
await pointer(tester(), 'pointerleave', 'touch');
|
|
102
|
+
await wait(CLOSE_DELAY);
|
|
103
|
+
expect(open).toBe(true);
|
|
104
|
+
await pointer(text(), 'pointerdown', 'touch');
|
|
105
|
+
await wait(CLOSE_DELAY);
|
|
106
|
+
expect(open).toBe(true);
|
|
107
|
+
await pointer(outside, 'pointerdown', 'touch');
|
|
108
|
+
await wait(CLOSE_DELAY);
|
|
109
|
+
expect(open).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
it('treats shy: true as hover', async () => {
|
|
112
|
+
await render(true);
|
|
113
|
+
await pointer(tester(), 'pointerenter');
|
|
114
|
+
await pointer(tester(), 'pointerleave');
|
|
115
|
+
await wait(CLOSE_DELAY);
|
|
116
|
+
expect(open).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
it("closes under shy: 'focus' when focus leaves, even while hovered", async () => {
|
|
119
|
+
await render('focus');
|
|
120
|
+
await pointer(tester(), 'pointerenter');
|
|
121
|
+
act(() => text().focus());
|
|
122
|
+
act(() => outside.focus());
|
|
123
|
+
await wait(CLOSE_DELAY);
|
|
124
|
+
expect(open).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
it('leaves the surface alone without shy, where its toggle stays visible', async () => {
|
|
127
|
+
await render(false);
|
|
128
|
+
await pointer(tester(), 'pointerenter');
|
|
129
|
+
await pointer(tester(), 'pointerleave');
|
|
130
|
+
await wait(CLOSE_DELAY);
|
|
131
|
+
expect(open).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -29,8 +29,10 @@ interface PreviewConfig {
|
|
|
29
29
|
clientSide?: boolean;
|
|
30
30
|
}
|
|
31
31
|
export type TestFontsFamilySelection = 'all' | 'choice';
|
|
32
|
+
export type TestFontsFamilyGrouping = 'flat' | 'superfamily';
|
|
32
33
|
interface TestFontsConfig {
|
|
33
34
|
familySelection?: TestFontsFamilySelection;
|
|
35
|
+
familyGrouping?: TestFontsFamilyGrouping;
|
|
34
36
|
}
|
|
35
37
|
export interface Config {
|
|
36
38
|
form?: FormConfig;
|
|
@@ -101,6 +103,7 @@ export declare const makeConfig: (config?: Config) => {
|
|
|
101
103
|
};
|
|
102
104
|
testFonts: {
|
|
103
105
|
familySelection: TestFontsFamilySelection | undefined;
|
|
106
|
+
familyGrouping: TestFontsFamilyGrouping | undefined;
|
|
104
107
|
};
|
|
105
108
|
storeModal: {
|
|
106
109
|
indexLayout: "styled-aa" | "styled-font-names";
|
|
@@ -182,6 +185,7 @@ declare const _default: React.Context<{
|
|
|
182
185
|
};
|
|
183
186
|
testFonts: {
|
|
184
187
|
familySelection: TestFontsFamilySelection | undefined;
|
|
188
|
+
familyGrouping: TestFontsFamilyGrouping | undefined;
|
|
185
189
|
};
|
|
186
190
|
storeModal: {
|
|
187
191
|
indexLayout: "styled-aa" | "styled-font-names";
|
|
@@ -88,7 +88,7 @@ export const mergeConfig = (base, override) => {
|
|
|
88
88
|
return deepMerge(base, override);
|
|
89
89
|
};
|
|
90
90
|
export const makeConfig = config => {
|
|
91
|
-
var _config$form, _config$testFonts, _config$storeModal, _config$storeModal2, _config$storeModal3, _config$storeModal4, _config$stripe, _config$tracking, _config$tracking2, _config$tracking3, _config$tracking4, _config$preview, _config$preview2, _config$preview3;
|
|
91
|
+
var _config$form, _config$testFonts, _config$testFonts2, _config$storeModal, _config$storeModal2, _config$storeModal3, _config$storeModal4, _config$stripe, _config$tracking, _config$tracking2, _config$tracking3, _config$tracking4, _config$preview, _config$preview2, _config$preview3;
|
|
92
92
|
return {
|
|
93
93
|
typeTester: makeTypeTesterConfig(config === null || config === void 0 ? void 0 : config.typeTester),
|
|
94
94
|
form: {
|
|
@@ -98,7 +98,8 @@ export const makeConfig = config => {
|
|
|
98
98
|
// Deliberately no default: the form falls back to the dashboard setting
|
|
99
99
|
// it reads in its own query (the npm path never merges server config
|
|
100
100
|
// into `children`'s context – see ServerConfigProvider).
|
|
101
|
-
familySelection: config === null || config === void 0 ? void 0 : (_config$testFonts = config.testFonts) === null || _config$testFonts === void 0 ? void 0 : _config$testFonts.familySelection
|
|
101
|
+
familySelection: config === null || config === void 0 ? void 0 : (_config$testFonts = config.testFonts) === null || _config$testFonts === void 0 ? void 0 : _config$testFonts.familySelection,
|
|
102
|
+
familyGrouping: config === null || config === void 0 ? void 0 : (_config$testFonts2 = config.testFonts) === null || _config$testFonts2 === void 0 ? void 0 : _config$testFonts2.familyGrouping
|
|
102
103
|
},
|
|
103
104
|
storeModal: {
|
|
104
105
|
indexLayout: (config === null || config === void 0 ? void 0 : (_config$storeModal = config.storeModal) === null || _config$storeModal === void 0 ? void 0 : _config$storeModal.indexLayout) ?? 'styled-aa',
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import _CustomerLoginFormRecaptchaQuery from "../../__generated__/CustomerLoginFormRecaptchaQuery.graphql.js";
|
|
3
4
|
import _CustomerLoginFormQuery from "../../__generated__/CustomerLoginFormQuery.graphql.js";
|
|
4
5
|
import _CustomerLoginFormLoginMutation from "../../__generated__/CustomerLoginFormLoginMutation.graphql.js";
|
|
5
|
-
import React, { Suspense, useState } from 'react';
|
|
6
|
+
import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react';
|
|
6
7
|
import { commitMutation, graphql, useLazyLoadQuery, useRelayEnvironment } from 'react-relay';
|
|
8
|
+
// The script-injecting reCAPTCHA wrapper (loads Google's api.js); see
|
|
9
|
+
// ../Recaptcha for why it isn't the package's named export.
|
|
10
|
+
import { ReCAPTCHA } from '../Recaptcha.js';
|
|
7
11
|
import TextField from '../TextField/index.js';
|
|
12
|
+
import { useRecaptchaTimeout, isRecaptchaScriptLoaded, RECAPTCHA_UNAVAILABLE_MESSAGE } from '../../hooks/useRecaptchaTimeout.js';
|
|
8
13
|
const loginMutation = (_CustomerLoginFormLoginMutation.hash && _CustomerLoginFormLoginMutation.hash !== "975b639fac1e0d88e0f0c9c55acd8b9c" && console.error("The definition of 'CustomerLoginFormLoginMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CustomerLoginFormLoginMutation);
|
|
9
14
|
const query = (_CustomerLoginFormQuery.hash && _CustomerLoginFormQuery.hash !== "f32c7b0b485a2b2a02ddf2cbc5b87a3c" && console.error("The definition of 'CustomerLoginFormQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CustomerLoginFormQuery);
|
|
10
|
-
const
|
|
15
|
+
const recaptchaQuery = (_CustomerLoginFormRecaptchaQuery.hash && _CustomerLoginFormRecaptchaQuery.hash !== "fe7289e19bcc08dbad62e72d67c48c97" && console.error("The definition of 'CustomerLoginFormRecaptchaQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CustomerLoginFormRecaptchaQuery);
|
|
16
|
+
const DEFAULT_SUBMITTED_LABEL = '<p>Submitted!</p><p>Please check your email inbox for a link to log in.</p><p>If you don’t receive an email, please contact us to retrieve your order information.</p>';
|
|
11
17
|
const CustomerLoginForm = _ref => {
|
|
12
18
|
let {
|
|
13
19
|
submitLabel = 'Submit'
|
|
@@ -16,24 +22,40 @@ const CustomerLoginForm = _ref => {
|
|
|
16
22
|
const [error, setError] = useState(null);
|
|
17
23
|
const [submitting, setSubmitting] = useState(false);
|
|
18
24
|
const [submitted, setSubmitted] = useState(false);
|
|
25
|
+
// The store's reCAPTCHA site key once known: null when it doesn't use
|
|
26
|
+
// reCAPTCHA. Undefined until the settings arrive, which is only after mount.
|
|
27
|
+
const [siteKey, setSiteKey] = useState(undefined);
|
|
28
|
+
const [recaptchaToken, setRecaptchaToken] = useState(null);
|
|
29
|
+
const [pendingSubmit, setPendingSubmit] = useState(false);
|
|
30
|
+
const [mounted, setMounted] = useState(false);
|
|
31
|
+
const recaptchaRef = useRef(null);
|
|
32
|
+
const recaptchaTimeout = useRecaptchaTimeout();
|
|
19
33
|
const environment = useRelayEnvironment();
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
useEffect(() => setMounted(true), []);
|
|
35
|
+
const resetRecaptcha = useCallback(() => {
|
|
36
|
+
var _recaptchaRef$current;
|
|
37
|
+
(_recaptchaRef$current = recaptchaRef.current) === null || _recaptchaRef$current === void 0 ? void 0 : _recaptchaRef$current.reset();
|
|
38
|
+
setRecaptchaToken(null);
|
|
39
|
+
}, []);
|
|
40
|
+
const submitForm = useCallback(token => {
|
|
41
|
+
recaptchaTimeout.clear();
|
|
42
|
+
setPendingSubmit(false);
|
|
22
43
|
setSubmitting(true);
|
|
23
44
|
commitMutation(environment, {
|
|
24
45
|
mutation: loginMutation,
|
|
25
46
|
variables: {
|
|
26
47
|
input: {
|
|
27
|
-
email
|
|
48
|
+
email,
|
|
49
|
+
recaptchaToken: token ?? undefined
|
|
28
50
|
}
|
|
29
51
|
},
|
|
30
52
|
onCompleted: (_res, errors) => {
|
|
53
|
+
setSubmitting(false);
|
|
31
54
|
if (errors && errors.length > 0) {
|
|
32
55
|
setError(errors[0].message);
|
|
33
|
-
|
|
56
|
+
resetRecaptcha();
|
|
34
57
|
} else {
|
|
35
58
|
setSubmitted(true);
|
|
36
|
-
setSubmitting(false);
|
|
37
59
|
setError(null);
|
|
38
60
|
}
|
|
39
61
|
},
|
|
@@ -43,8 +65,46 @@ const CustomerLoginForm = _ref => {
|
|
|
43
65
|
} = _ref2;
|
|
44
66
|
setError(message);
|
|
45
67
|
setSubmitting(false);
|
|
68
|
+
resetRecaptcha();
|
|
46
69
|
}
|
|
47
70
|
});
|
|
71
|
+
}, [environment, email, recaptchaTimeout, resetRecaptcha]);
|
|
72
|
+
const handleRecaptchaChange = useCallback(token => {
|
|
73
|
+
setRecaptchaToken(token);
|
|
74
|
+
if (token && pendingSubmit) submitForm(token);
|
|
75
|
+
}, [pendingSubmit, submitForm]);
|
|
76
|
+
const handleRecaptchaExpired = useCallback(() => setRecaptchaToken(null), []);
|
|
77
|
+
|
|
78
|
+
// Recover when the widget can't produce a token, so the button doesn't sit
|
|
79
|
+
// on “Submitting…” forever if Google's script fails to load.
|
|
80
|
+
const abortRecaptcha = useCallback(() => {
|
|
81
|
+
recaptchaTimeout.clear();
|
|
82
|
+
setPendingSubmit(false);
|
|
83
|
+
resetRecaptcha();
|
|
84
|
+
setError(RECAPTCHA_UNAVAILABLE_MESSAGE);
|
|
85
|
+
}, [recaptchaTimeout, resetRecaptcha]);
|
|
86
|
+
const handleRecaptchaErrored = useCallback(() => {
|
|
87
|
+
if (pendingSubmit) abortRecaptcha();
|
|
88
|
+
}, [pendingSubmit, abortRecaptcha]);
|
|
89
|
+
const handleSubmit = e => {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
setError(null);
|
|
92
|
+
if (siteKey && !recaptchaToken) {
|
|
93
|
+
var _recaptchaRef$current2;
|
|
94
|
+
setPendingSubmit(true);
|
|
95
|
+
// Give up if no token arrives and the script never loaded; a visitor
|
|
96
|
+
// mid-challenge keeps `grecaptcha` defined and isn't cut off.
|
|
97
|
+
recaptchaTimeout.start(() => {
|
|
98
|
+
if (isRecaptchaScriptLoaded()) return;
|
|
99
|
+
abortRecaptcha();
|
|
100
|
+
});
|
|
101
|
+
(_recaptchaRef$current2 = recaptchaRef.current) === null || _recaptchaRef$current2 === void 0 ? void 0 : _recaptchaRef$current2.execute();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Settings not in yet (a submit within moments of the page loading) goes
|
|
106
|
+
// without a token; the server says so if the store needs one.
|
|
107
|
+
submitForm(recaptchaToken);
|
|
48
108
|
};
|
|
49
109
|
const disabled = !email;
|
|
50
110
|
return /*#__PURE__*/React.createElement("div", {
|
|
@@ -69,23 +129,58 @@ const CustomerLoginForm = _ref => {
|
|
|
69
129
|
onChange: value => setEmail(value),
|
|
70
130
|
autocomplete: "email",
|
|
71
131
|
placeholder: "Your email"
|
|
132
|
+
})), mounted && /*#__PURE__*/React.createElement(Suspense, {
|
|
133
|
+
fallback: null
|
|
134
|
+
}, /*#__PURE__*/React.createElement(LoginRecaptcha, {
|
|
135
|
+
widgetRef: recaptchaRef,
|
|
136
|
+
onSiteKey: setSiteKey,
|
|
137
|
+
onChange: handleRecaptchaChange,
|
|
138
|
+
onExpired: handleRecaptchaExpired,
|
|
139
|
+
onErrored: handleRecaptchaErrored
|
|
72
140
|
})), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("button", {
|
|
73
141
|
className: "submit-button",
|
|
74
142
|
type: "submit",
|
|
75
|
-
disabled: submitting || disabled
|
|
76
|
-
}, submitting ? 'Submitting...' : submitLabel, /*#__PURE__*/React.createElement("span", {
|
|
143
|
+
disabled: submitting || pendingSubmit || disabled
|
|
144
|
+
}, submitting || pendingSubmit ? 'Submitting...' : submitLabel, /*#__PURE__*/React.createElement("span", {
|
|
77
145
|
className: "submit-button__arrow"
|
|
78
146
|
}, " \u2192")))));
|
|
79
147
|
};
|
|
80
148
|
|
|
149
|
+
// The store's reCAPTCHA settings and, when it uses reCAPTCHA, the invisible
|
|
150
|
+
// widget. Rendered only after mount, so the lazy query stays out of the SSR
|
|
151
|
+
// pass for the same reason as SubmittedMessage below.
|
|
152
|
+
function LoginRecaptcha(_ref3) {
|
|
153
|
+
var _data$viewer;
|
|
154
|
+
let {
|
|
155
|
+
widgetRef,
|
|
156
|
+
onSiteKey,
|
|
157
|
+
onChange,
|
|
158
|
+
onExpired,
|
|
159
|
+
onErrored
|
|
160
|
+
} = _ref3;
|
|
161
|
+
const data = useLazyLoadQuery(recaptchaQuery, {});
|
|
162
|
+
const settings = (_data$viewer = data.viewer) === null || _data$viewer === void 0 ? void 0 : _data$viewer.settings;
|
|
163
|
+
const siteKey = settings !== null && settings !== void 0 && settings.recaptchaEnabled ? settings.recaptchaSiteKey ?? null : null;
|
|
164
|
+
useEffect(() => onSiteKey(siteKey), [siteKey, onSiteKey]);
|
|
165
|
+
if (!siteKey) return null;
|
|
166
|
+
return /*#__PURE__*/React.createElement(ReCAPTCHA, {
|
|
167
|
+
ref: widgetRef,
|
|
168
|
+
sitekey: siteKey,
|
|
169
|
+
size: "invisible",
|
|
170
|
+
onChange: onChange,
|
|
171
|
+
onExpired: onExpired,
|
|
172
|
+
onErrored: onErrored
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
81
176
|
// The settings label is only shown after a submit, which can only happen in
|
|
82
177
|
// the browser — fetching it here (not in the form component) keeps the lazy
|
|
83
178
|
// query out of the SSR pass, where it has no session and, in multi-tenant
|
|
84
179
|
// setups, possibly no resolvable GraphQL URL.
|
|
85
180
|
function SubmittedMessage() {
|
|
86
|
-
var _data$
|
|
181
|
+
var _data$viewer2, _data$viewer2$setting;
|
|
87
182
|
const data = useLazyLoadQuery(query, {});
|
|
88
|
-
const submittedLabel = ((_data$
|
|
183
|
+
const submittedLabel = ((_data$viewer2 = data.viewer) === null || _data$viewer2 === void 0 ? void 0 : (_data$viewer2$setting = _data$viewer2.settings) === null || _data$viewer2$setting === void 0 ? void 0 : _data$viewer2$setting.customerLoginSubmittedLabel) || DEFAULT_SUBMITTED_LABEL;
|
|
89
184
|
return /*#__PURE__*/React.createElement("div", {
|
|
90
185
|
className: "login-form__submitted",
|
|
91
186
|
dangerouslySetInnerHTML: {
|
|
@@ -6,6 +6,7 @@ interface TestFontsFormElement_props {
|
|
|
6
6
|
collectionId?: string;
|
|
7
7
|
collectionSlug?: string;
|
|
8
8
|
familySelection?: string;
|
|
9
|
+
familyGrouping?: string;
|
|
9
10
|
}
|
|
10
|
-
declare const TestFontsFormElement: ({ familySelection, ...props }: TestFontsFormElement_props) => React.JSX.Element;
|
|
11
|
+
declare const TestFontsFormElement: ({ familySelection, familyGrouping, ...props }: TestFontsFormElement_props) => React.JSX.Element;
|
|
11
12
|
export default TestFontsFormElement;
|
|
@@ -2,12 +2,15 @@ function _extends() { return _extends = Object.assign ? Object.assign.bind() : f
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import TestFontsFormFetcher from './index.js';
|
|
4
4
|
const FAMILY_SELECTIONS = ['all', 'choice'];
|
|
5
|
+
const FAMILY_GROUPINGS = ['flat', 'superfamily'];
|
|
5
6
|
const TestFontsFormElement = _ref => {
|
|
6
7
|
let {
|
|
7
8
|
familySelection,
|
|
9
|
+
familyGrouping,
|
|
8
10
|
...props
|
|
9
11
|
} = _ref;
|
|
10
12
|
return /*#__PURE__*/React.createElement(TestFontsFormFetcher, _extends({}, props, {
|
|
13
|
+
familyGrouping: familyGrouping && FAMILY_GROUPINGS.includes(familyGrouping) ? familyGrouping : undefined,
|
|
11
14
|
familySelection: familySelection && FAMILY_SELECTIONS.includes(familySelection) ? familySelection : undefined,
|
|
12
15
|
newsletterCheckboxChecked: props.newsletterCheckboxChecked === 'true' ? true : false
|
|
13
16
|
}));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TestFontsFamilyGrouping } from '../ConfigContext.js';
|
|
2
|
+
interface Superfamily {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly name: string | null | undefined;
|
|
5
|
+
}
|
|
6
|
+
export interface TestFontsFamily {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly name: string | null | undefined;
|
|
9
|
+
readonly parent?: Superfamily | null;
|
|
10
|
+
}
|
|
11
|
+
export interface FamilyGroup<Family extends TestFontsFamily> {
|
|
12
|
+
superfamily: Superfamily | null;
|
|
13
|
+
families: Family[];
|
|
14
|
+
}
|
|
15
|
+
export declare function groupFamilies<Family extends TestFontsFamily>(families: readonly Family[], grouping: TestFontsFamilyGrouping): FamilyGroup<Family>[];
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// One row of the family checkboxes: a lone family (`superfamily` null), or a
|
|
2
|
+
// superfamily with its families nested beneath it.
|
|
3
|
+
|
|
4
|
+
// Lays the families out for the form's checkboxes, keeping their order: flat
|
|
5
|
+
// lists each family on its own; 'superfamily' gathers families under their
|
|
6
|
+
// superfamily, placed where its first family falls. A superfamily with only one
|
|
7
|
+
// family to offer adds nothing over that family, so the family stands alone.
|
|
8
|
+
export function groupFamilies(families, grouping) {
|
|
9
|
+
const alone = family => ({
|
|
10
|
+
superfamily: null,
|
|
11
|
+
families: [family]
|
|
12
|
+
});
|
|
13
|
+
if (grouping !== 'superfamily') return families.map(alone);
|
|
14
|
+
const groups = [];
|
|
15
|
+
const bySuperfamily = new Map();
|
|
16
|
+
for (const family of families) {
|
|
17
|
+
const superfamily = family.parent;
|
|
18
|
+
if (!superfamily) {
|
|
19
|
+
groups.push(alone(family));
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
let group = bySuperfamily.get(superfamily.id);
|
|
23
|
+
if (!group) {
|
|
24
|
+
group = {
|
|
25
|
+
superfamily,
|
|
26
|
+
families: []
|
|
27
|
+
};
|
|
28
|
+
bySuperfamily.set(superfamily.id, group);
|
|
29
|
+
groups.push(group);
|
|
30
|
+
}
|
|
31
|
+
group.families.push(family);
|
|
32
|
+
}
|
|
33
|
+
return groups.map(group => group.families.length > 1 ? group : alone(group.families[0]));
|
|
34
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { TestFontsForm_Query } from '../../__generated__/TestFontsForm_Query.graphql.js';
|
|
3
3
|
import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
|
|
4
|
-
import { Config, TestFontsFamilySelection } from '../ConfigContext.js';
|
|
4
|
+
import { Config, TestFontsFamilyGrouping, TestFontsFamilySelection } from '../ConfigContext.js';
|
|
5
5
|
import { TestFontsFormCollectionProps } from './variables.js';
|
|
6
6
|
export interface TestFontsForm_props extends TestFontsFormCollectionProps {
|
|
7
7
|
agreementLabel?: string;
|
|
8
8
|
downloadLabel?: string;
|
|
9
9
|
newsletterCheckboxChecked?: boolean;
|
|
10
10
|
familySelection?: TestFontsFamilySelection;
|
|
11
|
+
familyGrouping?: TestFontsFamilyGrouping;
|
|
11
12
|
}
|
|
12
13
|
export type TestFontsFormPreloadedQuery = SerializablePreloadedQuery<TestFontsForm_Query>;
|
|
13
14
|
export type LoadTestFontsFormQueryVariables = TestFontsFormCollectionProps;
|
|
@@ -18,6 +18,7 @@ import useSerializablePreloadedQuery from '../../relay/useSerializablePreloadedQ
|
|
|
18
18
|
import { EnsureFontdueContext, LazyQueryBoundary } from '../FontdueContextProvider/index.js';
|
|
19
19
|
import ConfigContext from '../ConfigContext.js';
|
|
20
20
|
import { testFontsFormQueryVariables } from './variables.js';
|
|
21
|
+
import { groupFamilies } from './familyGroups.js';
|
|
21
22
|
import { useRecaptchaTimeout, isRecaptchaScriptLoaded, RECAPTCHA_UNAVAILABLE_MESSAGE } from '../../hooks/useRecaptchaTimeout.js';
|
|
22
23
|
const updateCustomerMutation = (_TestFontsFormUpdateCustomerMutation.hash && _TestFontsFormUpdateCustomerMutation.hash !== "ba56958399f0893bd667ff02c33a6975" && console.error("The definition of 'TestFontsFormUpdateCustomerMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _TestFontsFormUpdateCustomerMutation);
|
|
23
24
|
// The archive endpoint's family filter: `collection_ids[]` narrows the ZIP to
|
|
@@ -41,7 +42,7 @@ const TestFontsDownloading = _ref => {
|
|
|
41
42
|
}, "here"), ".")));
|
|
42
43
|
};
|
|
43
44
|
const TestFontsFormComponent = _ref2 => {
|
|
44
|
-
var _data$viewer$testFont, _data$viewer$fontdueC, _data$viewer$settings, _data$viewer$settings2, _data$viewer$testFont2;
|
|
45
|
+
var _data$viewer$testFont, _data$viewer$fontdueC, _data$viewer$fontdueC2, _data$viewer$settings, _data$viewer$settings2, _data$viewer$testFont2;
|
|
45
46
|
let {
|
|
46
47
|
data,
|
|
47
48
|
agreementLabel,
|
|
@@ -49,7 +50,8 @@ const TestFontsFormComponent = _ref2 => {
|
|
|
49
50
|
newsletterCheckboxChecked = false,
|
|
50
51
|
collectionId,
|
|
51
52
|
collectionSlug,
|
|
52
|
-
familySelection: familySelectionProp
|
|
53
|
+
familySelection: familySelectionProp,
|
|
54
|
+
familyGrouping: familyGroupingProp
|
|
53
55
|
} = _ref2;
|
|
54
56
|
const [email, setEmail] = useState('');
|
|
55
57
|
const [name, setName] = useState('');
|
|
@@ -82,11 +84,30 @@ const TestFontsFormComponent = _ref2 => {
|
|
|
82
84
|
// This form's prop, then code config (which, on the script-tag path, already
|
|
83
85
|
// has the dashboard setting merged beneath it), then the dashboard setting.
|
|
84
86
|
const familySelection = familySelectionProp ?? config.testFonts.familySelection ?? ((_data$viewer$fontdueC = data.viewer.fontdueConfig) === null || _data$viewer$fontdueC === void 0 ? void 0 : _data$viewer$fontdueC.testFonts.familySelection) ?? 'all';
|
|
87
|
+
// Resolved the same way as the family selection.
|
|
88
|
+
const familyGrouping = familyGroupingProp ?? config.testFonts.familyGrouping ?? ((_data$viewer$fontdueC2 = data.viewer.fontdueConfig) === null || _data$viewer$fontdueC2 === void 0 ? void 0 : _data$viewer$fontdueC2.testFonts.familyGrouping) ?? 'flat';
|
|
85
89
|
// One family leaves nothing to choose.
|
|
86
90
|
const choosing = !limited && familySelection === 'choice' && families.length > 1;
|
|
87
91
|
const downloadIds = limited ? families.map(family => family.id) : choosing ? families.map(family => family.id).filter(id => chosenIds.includes(id)) : [];
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
|
|
93
|
+
// Ticks or unticks families – one, or a superfamily's.
|
|
94
|
+
const toggleFamilies = (toggled, checked) => setChosenIds(ids => [...ids.filter(id => !toggled.includes(id)), ...(checked ? toggled : [])]);
|
|
95
|
+
const allChosen = list => list.every(family => chosenIds.includes(family.id));
|
|
96
|
+
const familyGroups = groupFamilies(families, familyGrouping);
|
|
97
|
+
const familyField = family => {
|
|
98
|
+
const fieldId = `${idPrefix}-family-${families.indexOf(family)}`;
|
|
99
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
100
|
+
className: "test-fonts__checkbox-field test-fonts__family",
|
|
101
|
+
key: family.id
|
|
102
|
+
}, /*#__PURE__*/React.createElement(Checkbox, {
|
|
103
|
+
id: fieldId,
|
|
104
|
+
value: family.id,
|
|
105
|
+
checked: chosenIds.includes(family.id),
|
|
106
|
+
onChange: event => toggleFamilies([family.id], event.target.checked)
|
|
107
|
+
}), /*#__PURE__*/React.createElement("label", {
|
|
108
|
+
htmlFor: fieldId
|
|
109
|
+
}, family.name));
|
|
110
|
+
};
|
|
90
111
|
const recaptchaEnabled = ((_data$viewer$settings = data.viewer.settings) === null || _data$viewer$settings === void 0 ? void 0 : _data$viewer$settings.recaptchaEnabled) ?? false;
|
|
91
112
|
const recaptchaSiteKey = (_data$viewer$settings2 = data.viewer.settings) === null || _data$viewer$settings2 === void 0 ? void 0 : _data$viewer$settings2.recaptchaSiteKey;
|
|
92
113
|
const submitForm = useCallback(token => {
|
|
@@ -212,21 +233,30 @@ const TestFontsFormComponent = _ref2 => {
|
|
|
212
233
|
className: "test-fonts__checkbox-field test-fonts__families__all"
|
|
213
234
|
}, /*#__PURE__*/React.createElement(Checkbox, {
|
|
214
235
|
id: `${idPrefix}-families-all`,
|
|
215
|
-
checked: allChosen,
|
|
236
|
+
checked: allChosen(families),
|
|
216
237
|
onChange: event => setChosenIds(event.target.checked ? families.map(family => family.id) : [])
|
|
217
238
|
}), /*#__PURE__*/React.createElement("label", {
|
|
218
239
|
htmlFor: `${idPrefix}-families-all`
|
|
219
|
-
}, "Select all")),
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
240
|
+
}, "Select all")), familyGroups.map((_ref4, groupIndex) => {
|
|
241
|
+
let {
|
|
242
|
+
superfamily,
|
|
243
|
+
families: grouped
|
|
244
|
+
} = _ref4;
|
|
245
|
+
return superfamily ? /*#__PURE__*/React.createElement("div", {
|
|
246
|
+
className: "test-fonts__family-group",
|
|
247
|
+
key: superfamily.id
|
|
248
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
249
|
+
className: "test-fonts__checkbox-field test-fonts__superfamily"
|
|
250
|
+
}, /*#__PURE__*/React.createElement(Checkbox, {
|
|
251
|
+
id: `${idPrefix}-superfamily-${groupIndex}`,
|
|
252
|
+
checked: allChosen(grouped),
|
|
253
|
+
onChange: event => toggleFamilies(grouped.map(family => family.id), event.target.checked)
|
|
254
|
+
}), /*#__PURE__*/React.createElement("label", {
|
|
255
|
+
htmlFor: `${idPrefix}-superfamily-${groupIndex}`
|
|
256
|
+
}, superfamily.name)), /*#__PURE__*/React.createElement("div", {
|
|
257
|
+
className: "test-fonts__family-group__families"
|
|
258
|
+
}, grouped.map(familyField))) : familyField(grouped[0]);
|
|
259
|
+
})), /*#__PURE__*/React.createElement("div", {
|
|
230
260
|
className: "test-fonts__section"
|
|
231
261
|
}, /*#__PURE__*/React.createElement(TextField, {
|
|
232
262
|
label: "Name *",
|
|
@@ -289,7 +319,7 @@ const TestFontsFormComponent = _ref2 => {
|
|
|
289
319
|
value: id
|
|
290
320
|
}))));
|
|
291
321
|
};
|
|
292
|
-
const query = (_TestFontsForm_Query.hash && _TestFontsForm_Query.hash !== "
|
|
322
|
+
const query = (_TestFontsForm_Query.hash && _TestFontsForm_Query.hash !== "9ef2c42a7043469fec07a31649623766" && console.error("The definition of 'TestFontsForm_Query' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _TestFontsForm_Query);
|
|
293
323
|
// Pass the same `collectionId` / `collectionSlug` as the form's props.
|
|
294
324
|
export async function loadTestFontsFormQuery(variables, options) {
|
|
295
325
|
// Before the collection variables existed this took `options` alone.
|
|
@@ -298,11 +328,11 @@ export async function loadTestFontsFormQuery(variables, options) {
|
|
|
298
328
|
}
|
|
299
329
|
return loadSerializableQuery(TestFontsFormQueryNode, testFontsFormQueryVariables(variables ?? {}), options);
|
|
300
330
|
}
|
|
301
|
-
export function TestFontsFormPreloadedQueryRenderer(
|
|
331
|
+
export function TestFontsFormPreloadedQueryRenderer(_ref5) {
|
|
302
332
|
let {
|
|
303
333
|
preloadedQuery,
|
|
304
334
|
...rest
|
|
305
|
-
} =
|
|
335
|
+
} = _ref5;
|
|
306
336
|
// The query node lets the hook commit the payload into the store, so
|
|
307
337
|
// usePreloadedQuery resolves synchronously during SSR instead of
|
|
308
338
|
// refetching (the response cache only exists in the browser).
|
|
@@ -322,11 +352,11 @@ function TestFontsFormLazyQueryRenderer(props) {
|
|
|
322
352
|
// (server-side preload path) or fetches lazily on mount. `EnsureFontdueContext`
|
|
323
353
|
// only self-wraps with `FontdueContextProvider` when no parent Fontdue
|
|
324
354
|
// context is present, seeding it with `config`.
|
|
325
|
-
export default function TestFontsForm(
|
|
355
|
+
export default function TestFontsForm(_ref6) {
|
|
326
356
|
let {
|
|
327
357
|
config,
|
|
328
358
|
...props
|
|
329
|
-
} =
|
|
359
|
+
} = _ref6;
|
|
330
360
|
return /*#__PURE__*/React.createElement(EnsureFontdueContext, {
|
|
331
361
|
config: config
|
|
332
362
|
}, props.preloadedQuery ? /*#__PURE__*/React.createElement(TestFontsFormPreloadedQueryRenderer, _extends({}, props, {
|