@skyscanner/backpack-web 42.27.0 → 42.27.2

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.
@@ -1,4 +1,4 @@
1
- export { BpkProvider } from './src/BpkProvider';
1
+ export { BpkProvider, BpkEmotionCacheContext } from './src/BpkProvider';
2
2
  export { BpkBox } from './src/BpkBox';
3
3
  export { BpkVessel } from './src/BpkVessel';
4
4
  export { BpkFlex } from './src/BpkFlex';
@@ -16,7 +16,7 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- export { BpkProvider } from "./src/BpkProvider";
19
+ export { BpkProvider, BpkEmotionCacheContext } from "./src/BpkProvider";
20
20
  export { BpkBox } from "./src/BpkBox";
21
21
  export { BpkVessel } from "./src/BpkVessel";
22
22
  export { BpkFlex } from "./src/BpkFlex";
@@ -1,11 +1,14 @@
1
- import type { ReactElement, ReactNode } from 'react';
1
+ import type { ReactNode, ReactElement } from 'react';
2
+ import type { EmotionCache } from '@emotion/cache';
2
3
  export interface BpkProviderProps {
3
4
  children: ReactNode;
4
5
  }
6
+ export declare const BpkEmotionCacheContext: import("react").Context<EmotionCache | null>;
5
7
  /**
6
8
  * BpkProvider - Provides context for Backpack layout and Ark-based components.
7
9
  *
8
10
  * Wraps children with:
11
+ * - Emotion CacheProvider (own cache, or external cache injected via BpkEmotionCacheContext)
9
12
  * - Chakra UI system context (for layout components: BpkFlex, BpkGrid, etc.)
10
13
  * - Ark UI LocaleProvider (for Ark-based components: BpkCheckboxV2, BpkSegmentedControlV2, etc.)
11
14
  *
@@ -13,6 +16,11 @@ export interface BpkProviderProps {
13
16
  * the appropriate locale to Ark's LocaleProvider. All Ark-based components in the
14
17
  * tree render correctly in RTL without requiring additional wrapping or prop changes.
15
18
  *
19
+ * External cache injection: host apps can supply an Emotion cache via
20
+ * BpkEmotionCacheContext. When a non-null value is provided, BpkProvider uses it
21
+ * directly and skips creating its own cache. This allows the host to swap in a
22
+ * fresh cache after a hydration error so all Backpack styles are re-injected.
23
+ *
16
24
  * @param {BpkProviderProps} props - The provider props.
17
25
  * @returns {ReactElement} The provider wrapping its children with Chakra and Ark context.
18
26
  */
@@ -23,6 +23,12 @@ import createCache from '@emotion/cache';
23
23
  import { CacheProvider } from '@emotion/react';
24
24
  import { createBpkConfig } from "./theme";
25
25
  import { jsx as _jsx } from "react/jsx-runtime";
26
+ // Exported so host apps can inject an externally-managed Emotion cache into
27
+ // BpkProvider (e.g. to force re-injection after a hydration error recovery).
28
+ // When a non-null value is provided via this context, BpkProvider operates in
29
+ // "external cache" mode: it skips creating its own cache and delegates to the external one.
30
+ export const BpkEmotionCacheContext = /*#__PURE__*/createContext(null);
31
+
26
32
  /**
27
33
  * Creates a Chakra UI system with Backpack token mappings.
28
34
  *
@@ -37,6 +43,14 @@ const bpkSystem = createSystem(defaultBaseConfig, createBpkConfig());
37
43
  // Force speedy:false + recreate after mount in Cypress. Remove once Emotion /
38
44
  // React Router 7 provide a cleaner hydration story. Ported from hotels-website#12025.
39
45
 
46
+ // `'css'` is shared with Chakra v3's internal key on purpose — keeps this
47
+ // boundary in front of Chakra's auto-created cache.
48
+ const createBpkEmotionCache = speedy => createCache(speedy === undefined ? {
49
+ key: 'css'
50
+ } : {
51
+ key: 'css',
52
+ speedy
53
+ });
40
54
  const isCypressEnv = () => {
41
55
  if (typeof window === 'undefined') return false;
42
56
  const win = window;
@@ -49,16 +63,6 @@ const isCypressEnv = () => {
49
63
  return false; // cross-origin parent frame
50
64
  }
51
65
  };
52
-
53
- // `'css'` is shared with Chakra v3's internal key on purpose — keeps this
54
- // boundary in front of Chakra's auto-created cache.
55
- const createBpkEmotionCache = speedy => createCache(speedy === undefined ? {
56
- key: 'css'
57
- } : {
58
- key: 'css',
59
- speedy
60
- });
61
- const BpkEmotionCacheContext = /*#__PURE__*/createContext(null);
62
66
  // Fallback locale mapping used when no explicit locale is available on the document.
63
67
  // Maps DOM direction to minimal BCP 47 locales understood by Ark's isRTL() utility.
64
68
  // 'ar-SA' is the minimal RTL locale — Ark only uses it to derive dir='rtl'.
@@ -71,6 +75,19 @@ const FALLBACK_LOCALE_BY_DIRECTION = {
71
75
  // Intl.Locale.textInfo is unavailable (Node < 22, older browsers).
72
76
  const RTL_LANGUAGE_SUBTAGS = new Set(['ar', 'he', 'fa', 'ur', 'yi', 'iw', 'ps', 'sd', 'ug', 'ku']);
73
77
 
78
+ // Returns true when `locale` is a BCP 47 string that Intl.Locale accepts.
79
+ // Ark's LocaleProvider calls `new Intl.Locale(locale)` without a try/catch,
80
+ // so any value we hand it must be validated here first or it throws
81
+ // "Incorrect locale information provided".
82
+ const isValidLocale = locale => {
83
+ try {
84
+ // Reading a property on the result (rather than bare `new`) satisfies the no-new lint rule.
85
+ return Boolean(new Intl.Locale(locale).baseName);
86
+ } catch {
87
+ return false;
88
+ }
89
+ };
90
+
74
91
  // Returns the text direction implied by a BCP 47 locale string.
75
92
  // Uses Intl.Locale.textInfo when available (Chrome 99+, Safari 15.4+, Firefox 126+, Node 22+);
76
93
  // falls back to a known-RTL-subtag lookup.
@@ -88,11 +105,18 @@ const getLangDir = locale => {
88
105
  //
89
106
  // Priority rules:
90
107
  // 1. If html[dir] is explicitly set:
91
- // - Use html[lang] only when its direction is consistent with html[dir].
108
+ // - Use html[lang] only when it is a valid locale AND its direction is
109
+ // consistent with html[dir].
92
110
  // - Otherwise fall back to FALLBACK_LOCALE_BY_DIRECTION[dir].
93
111
  // This prevents an LTR html[lang] (e.g. 'en' from a page template) from
94
112
  // overriding an explicit html[dir]="rtl" signal (e.g. from a dev RTL toggle).
95
- // 2. If html[dir] is not set: use html[lang] if present, else 'en-US'.
113
+ // 2. If html[dir] is not set: use html[lang] if it is a valid locale, else 'en-US'.
114
+ //
115
+ // Every value returned here is validated with isValidLocale() because Ark's
116
+ // LocaleProvider passes it straight to `new Intl.Locale()`, which throws on
117
+ // malformed input (e.g. '', '123', 'en_US'). An unvalidated value crashes the
118
+ // provider, and when the ErrorBoundary fallback also mounts BpkProvider the same
119
+ // bad value is re-read on every remount, producing an indefinite crash loop.
96
120
  //
97
121
  // SSR-safe: returns 'en-US' when document is unavailable.
98
122
  const getArkLocale = () => {
@@ -100,17 +124,12 @@ const getArkLocale = () => {
100
124
  const explicitDir = document.documentElement.getAttribute('dir');
101
125
  const lang = document.documentElement.getAttribute('lang');
102
126
  if (explicitDir === 'rtl' || explicitDir === 'ltr') {
103
- if (lang && getLangDir(lang) === explicitDir) return lang;
104
- return FALLBACK_LOCALE_BY_DIRECTION[explicitDir];
105
- }
106
- if (lang) {
107
- try {
108
- const locale = new Intl.Locale(lang);
109
- if (locale) return lang;
110
- } catch {
111
- // Invalid locale string — fall through to default
127
+ if (lang && isValidLocale(lang) && getLangDir(lang) === explicitDir) {
128
+ return lang;
112
129
  }
130
+ return FALLBACK_LOCALE_BY_DIRECTION[explicitDir];
113
131
  }
132
+ if (lang && isValidLocale(lang)) return lang;
114
133
  return 'en-US';
115
134
  };
116
135
 
@@ -138,6 +157,7 @@ const useArkLocale = () => {
138
157
  * BpkProvider - Provides context for Backpack layout and Ark-based components.
139
158
  *
140
159
  * Wraps children with:
160
+ * - Emotion CacheProvider (own cache, or external cache injected via BpkEmotionCacheContext)
141
161
  * - Chakra UI system context (for layout components: BpkFlex, BpkGrid, etc.)
142
162
  * - Ark UI LocaleProvider (for Ark-based components: BpkCheckboxV2, BpkSegmentedControlV2, etc.)
143
163
  *
@@ -145,16 +165,21 @@ const useArkLocale = () => {
145
165
  * the appropriate locale to Ark's LocaleProvider. All Ark-based components in the
146
166
  * tree render correctly in RTL without requiring additional wrapping or prop changes.
147
167
  *
168
+ * External cache injection: host apps can supply an Emotion cache via
169
+ * BpkEmotionCacheContext. When a non-null value is provided, BpkProvider uses it
170
+ * directly and skips creating its own cache. This allows the host to swap in a
171
+ * fresh cache after a hydration error so all Backpack styles are re-injected.
172
+ *
148
173
  * @param {BpkProviderProps} props - The provider props.
149
174
  * @returns {ReactElement} The provider wrapping its children with Chakra and Ark context.
150
175
  */
151
176
  export const BpkProvider = ({
152
177
  children
153
178
  }) => {
154
- const parentCache = useContext(BpkEmotionCacheContext);
155
- const isNested = parentCache !== null;
179
+ const externalCache = useContext(BpkEmotionCacheContext);
180
+ const hasExternalCache = externalCache !== null;
156
181
  const [isCypress] = useState(isCypressEnv);
157
- const [ownCache, setOwnCache] = useState(() => isNested ? parentCache : createBpkEmotionCache(isCypress ? false : undefined));
182
+ const [ownCache, setOwnCache] = useState(() => hasExternalCache ? externalCache : createBpkEmotionCache(isCypress ? false : undefined));
158
183
  const hasRecreated = useRef(false);
159
184
  const locale = useArkLocale();
160
185
 
@@ -162,12 +187,19 @@ export const BpkProvider = ({
162
187
  // nodes the hydrator stripped. `hasRecreated` guards StrictMode double-invoke.
163
188
  // Deps stable for provider lifetime → empty array is intentional.
164
189
  useEffect(() => {
165
- if (isNested || !isCypress) return;
190
+ if (hasExternalCache || !isCypress) return;
166
191
  if (hasRecreated.current) return;
167
192
  hasRecreated.current = true;
168
193
  setOwnCache(createBpkEmotionCache(false));
169
194
  // eslint-disable-next-line react-hooks/exhaustive-deps
170
195
  }, []);
196
+
197
+ // NOTE: if externalCache changes from non-null to null at runtime, ownCache
198
+ // will still hold the value it was initialised with (the old external cache).
199
+ // This is intentional: BpkEmotionCacheContext.Provider is expected to be
200
+ // mounted for the lifetime of the app once set — toggling it off is not a
201
+ // supported use case. The state initialiser runs only once per mount.
202
+ const activeCache = hasExternalCache ? externalCache : ownCache;
171
203
  const inner = /*#__PURE__*/_jsx(ChakraProvider, {
172
204
  value: bpkSystem,
173
205
  children: /*#__PURE__*/_jsx(LocaleProvider, {
@@ -175,13 +207,10 @@ export const BpkProvider = ({
175
207
  children: children
176
208
  })
177
209
  });
178
- if (isNested) {
179
- return inner;
180
- }
181
210
  return /*#__PURE__*/_jsx(BpkEmotionCacheContext.Provider, {
182
- value: ownCache,
211
+ value: activeCache,
183
212
  children: /*#__PURE__*/_jsx(CacheProvider, {
184
- value: ownCache,
213
+ value: activeCache,
185
214
  children: inner
186
215
  })
187
216
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyscanner/backpack-web",
3
- "version": "42.27.0",
3
+ "version": "42.27.2",
4
4
  "description": "Backpack Design System web library",
5
5
  "repository": {
6
6
  "type": "git",