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.
Files changed (30) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/__generated__/CustomerLoginFormLoginMutation.graphql.d.ts +2 -1
  3. package/dist/__generated__/CustomerLoginFormLoginMutation.graphql.js +1 -1
  4. package/dist/__generated__/CustomerLoginFormRecaptchaQuery.graphql.d.ts +21 -0
  5. package/dist/__generated__/CustomerLoginFormRecaptchaQuery.graphql.js +86 -0
  6. package/dist/__generated__/TestFontsForm_Query.graphql.d.ts +6 -1
  7. package/dist/__generated__/TestFontsForm_Query.graphql.js +25 -9
  8. package/dist/__tests__/customerLoginRecaptcha.test.js +113 -0
  9. package/dist/__tests__/testFontsFamilyChoice.test.js +74 -1
  10. package/dist/__tests__/typeTesterHeightAnimation.test.js +124 -0
  11. package/dist/__tests__/typeTesterShyFeaturesClose.test.js +133 -0
  12. package/dist/components/ConfigContext.d.ts +4 -0
  13. package/dist/components/ConfigContext.js +3 -2
  14. package/dist/components/CustomerLoginForm/index.js +106 -11
  15. package/dist/components/TestFontsForm/TestFontsFormElement.d.ts +2 -1
  16. package/dist/components/TestFontsForm/TestFontsFormElement.js +3 -0
  17. package/dist/components/TestFontsForm/familyGroups.d.ts +16 -0
  18. package/dist/components/TestFontsForm/familyGroups.js +34 -0
  19. package/dist/components/TestFontsForm/index.d.ts +2 -1
  20. package/dist/components/TestFontsForm/index.js +51 -21
  21. package/dist/components/TypeTester/TypeTesterFeatures.js +27 -44
  22. package/dist/components/TypeTester/useCloseWhenToolbarHides.d.ts +17 -0
  23. package/dist/components/TypeTester/useCloseWhenToolbarHides.js +79 -0
  24. package/dist/components/TypeTester/useHeightAnimation.d.ts +27 -0
  25. package/dist/components/TypeTester/useHeightAnimation.js +85 -0
  26. package/dist/fontLoader.d.ts +8 -6
  27. package/dist/fontLoader.js +8 -6
  28. package/dist/fontdue.css +3 -0
  29. package/dist/version.js +1 -1
  30. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import _TypeTesterFeatures_fontStyle from "../../__generated__/TypeTesterFeatures_fontStyle.graphql.js";
2
- import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
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
- function useHeightAnimation() {
11
- let isOpen = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
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
- height: featuresHeight,
262
- contentRef: featuresContentRef
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: featuresContentRef
270
+ ref: surfaceRef
288
271
  }, axes ? /*#__PURE__*/React.createElement(TypeTesterVariableAxes, {
289
272
  id: id,
290
273
  axes: axes,
@@ -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
+ }
@@ -14,12 +14,14 @@ interface VerticalMetrics {
14
14
  * CSS must use to render it: `"<cssFamily> <styleName>"`, made safe to parse
15
15
  * as an unquoted CSS font-family.
16
16
  *
17
- * Some Safari builds parse the FontFace family argument as CSS, so a name
18
- * like "Hell's Gate fairypools" (the apostrophe opens an unterminated string)
19
- * or "0712 Regular" (an identifier can't start with a digit) makes `load()`
20
- * reject with "SyntaxError: The string did not match the expected pattern."
21
- * and the style never renders, while Chrome and other WebKit builds take the
22
- * argument literally. A name that is valid either way works everywhere:
17
+ * Safari 26.0.x (macOS Tahoe / iOS 26 before their point updates) parses the
18
+ * FontFace family argument as CSS, so a name like "Hell's Gate fairypools"
19
+ * (the apostrophe opens an unterminated string) or "0712 Regular" (an
20
+ * identifier can't start with a digit) makes `load()` reject with
21
+ * "SyntaxError: The string did not match the expected pattern." and the style
22
+ * never renders. Chrome and other Safari builds (17.3, 18.4, 26.4+, 27.0 were
23
+ * checked) take the argument literally. A name that is valid either way
24
+ * works everywhere:
23
25
  *
24
26
  * - an ASCII character other than a letter, digit, `-` or `_` is escaped as
25
27
  * `_<hex code>` (`'` → `_27`), so distinct names stay distinct;
@@ -7,12 +7,14 @@ const RESERVED_FAMILY_WORDS = new Set(['inherit', 'initial', 'unset', 'revert',
7
7
  * CSS must use to render it: `"<cssFamily> <styleName>"`, made safe to parse
8
8
  * as an unquoted CSS font-family.
9
9
  *
10
- * Some Safari builds parse the FontFace family argument as CSS, so a name
11
- * like "Hell's Gate fairypools" (the apostrophe opens an unterminated string)
12
- * or "0712 Regular" (an identifier can't start with a digit) makes `load()`
13
- * reject with "SyntaxError: The string did not match the expected pattern."
14
- * and the style never renders, while Chrome and other WebKit builds take the
15
- * argument literally. A name that is valid either way works everywhere:
10
+ * Safari 26.0.x (macOS Tahoe / iOS 26 before their point updates) parses the
11
+ * FontFace family argument as CSS, so a name like "Hell's Gate fairypools"
12
+ * (the apostrophe opens an unterminated string) or "0712 Regular" (an
13
+ * identifier can't start with a digit) makes `load()` reject with
14
+ * "SyntaxError: The string did not match the expected pattern." and the style
15
+ * never renders. Chrome and other Safari builds (17.3, 18.4, 26.4+, 27.0 were
16
+ * checked) take the argument literally. A name that is valid either way
17
+ * works everywhere:
16
18
  *
17
19
  * - an ASCII character other than a letter, digit, `-` or `_` is escaped as
18
20
  * `_<hex code>` (`'` → `_27`), so distinct names stay distinct;
package/dist/fontdue.css CHANGED
@@ -185,6 +185,9 @@
185
185
  .test-fonts__families .test-fonts__checkbox-field {
186
186
  margin-bottom: 10px;
187
187
  }
188
+ .test-fonts__family-group__families {
189
+ padding-left: calc(var(--checkbox-size, 20px) + var(--checkbox-margin, 10px));
190
+ }
188
191
  .test-fonts__agreement[data-highlighted=true] {
189
192
  color: red;
190
193
  }
package/dist/version.js CHANGED
@@ -8,4 +8,4 @@
8
8
  // (Fontage.ClientVersions) to spot sites that need to upgrade before a
9
9
  // feature that depends on a newer client can work for them.
10
10
  export const CLIENT_VERSION_HEADER = 'fontdue-client-version';
11
- export const version = "3.10.1";
11
+ export const version = "3.12.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontdue-js",
3
- "version": "3.10.1",
3
+ "version": "3.12.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "npm run relay && run-p build-js build-css build-ts",