@applicaster/zapp-react-native-ui-components 16.0.0-rc.92 → 16.0.0-rc.94

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,8 +1,7 @@
1
1
  import * as React from "react";
2
2
  import * as R from "ramda";
3
3
 
4
- import { findPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
5
- import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
4
+ import { getPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
6
5
  import {
7
6
  inflateString,
8
7
  objectToReadableString,
@@ -40,12 +39,8 @@ export async function inflateUrl(url) {
40
39
 
41
40
  export function LinkHandler(props: Props) {
42
41
  const screenData = props?.screenData;
43
- const plugins = usePlugins();
44
42
 
45
- const ScreenPlugin = findPluginByIdentifier(
46
- WEBVIEW_SCREEN_IDENTIFIER,
47
- plugins
48
- );
43
+ const ScreenPlugin = getPluginByIdentifier(WEBVIEW_SCREEN_IDENTIFIER);
49
44
 
50
45
  const Component =
51
46
  R.path(["module", "Component"], ScreenPlugin) ||
@@ -1,5 +1,4 @@
1
- import { findPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
2
- import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
1
+ import { getPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
3
2
  import { loggerLiveImageManager } from "../../../VideoLive/loggerHelper";
4
3
 
5
4
  const { log_debug, log_error } = loggerLiveImageManager;
@@ -74,10 +73,9 @@ export async function executePreloadHooks({
74
73
  let payload: any = entry;
75
74
 
76
75
  let needsAbort = false;
77
- const plugins = appStore.get("plugins");
78
76
 
79
77
  for (const hookConfig of sortedHooks) {
80
- const plugin = findPluginByIdentifier(hookConfig.identifier, plugins, true);
78
+ const plugin = getPluginByIdentifier(hookConfig.identifier, true);
81
79
 
82
80
  if (!plugin?.module) {
83
81
  log_debug(
@@ -8,7 +8,7 @@ import Image from "../Image";
8
8
  import { useTrackCurrentAutoScrollingElement } from "@applicaster/zapp-react-native-utils/reactHooks/autoscrolling/useTrackCurrentAutoScrollingElement";
9
9
  import { useUIComponentContext } from "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext";
10
10
  import { getPropComponentType } from "@applicaster/zapp-react-native-utils/cellUtils";
11
- import { findPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
11
+ import { getPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
12
12
  import { useAccessibilityState } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager/hooks";
13
13
 
14
14
  type LiveImageProps = {
@@ -249,9 +249,8 @@ const LiveImageComponent = (props: LiveImageProps) => {
249
249
  getPlayerConfig(player_screen_id, actionIdentifier) ?? {};
250
250
 
251
251
  const playableEntry = playerPluginId ? getPlayableEntry(item) : null;
252
- const plugins = appStore.get("plugins");
253
252
 
254
- const playerPlugin = findPluginByIdentifier(playerPluginId, plugins);
253
+ const playerPlugin = getPluginByIdentifier(playerPluginId);
255
254
 
256
255
  const isShouldRender =
257
256
  playerPlugin &&
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Modal Building Block — Header
3
3
  *
4
- * Single-file Expo Snack demo for the Header building block spec.
5
- *
6
4
  * Green (configurable): title, closeButton (enable + asset), actionButtons styling
7
- * White (hardcoded): container, action button layout/enable/actions, close dimensions,
8
- * action button padding/radius, action labels
5
+ * (incl. Clear padding/radius for Variant 3 / section_header Studio keys)
6
+ * White (hardcoded): container gutters, action button slot enablement/actions,
7
+ * close dimensions, default action labels (overridable via HeaderData.clearLabel)
9
8
  */
10
9
 
11
10
  import React, { useCallback, useState } from "react";
@@ -94,6 +93,12 @@ export type ActionButtonsStyleConfiguration = {
94
93
  iosLetterSpacing: number;
95
94
  androidLetterSpacing: number;
96
95
  textTransform: TextTransform;
96
+ /** Green Variant 3 fields — defaults match ACTION_BUTTON_LAYOUT_SPEC. */
97
+ paddingTop?: number;
98
+ paddingRight?: number;
99
+ paddingBottom?: number;
100
+ paddingLeft?: number;
101
+ cornerRadius?: number;
97
102
  };
98
103
 
99
104
  export type HeaderConfiguration = {
@@ -133,6 +138,11 @@ export const DEFAULT_ACTION_BUTTONS_STYLE_CONFIGURATION: ActionButtonsStyleConfi
133
138
  iosLetterSpacing: 0,
134
139
  androidLetterSpacing: 0,
135
140
  textTransform: "default",
141
+ paddingTop: 6,
142
+ paddingRight: 10,
143
+ paddingBottom: 6,
144
+ paddingLeft: 10,
145
+ cornerRadius: 16,
136
146
  };
137
147
 
138
148
  // ---------------------------------------------------------------------------
@@ -141,6 +151,8 @@ export const DEFAULT_ACTION_BUTTONS_STYLE_CONFIGURATION: ActionButtonsStyleConfi
141
151
 
142
152
  export type HeaderData = {
143
153
  title: string;
154
+ /** Localization override for Clear; falls back to ACTION_LABELS.clear. */
155
+ clearLabel?: string;
144
156
  };
145
157
 
146
158
  export type AudioPlayerHeaderProps = {
@@ -437,6 +449,7 @@ type HeaderActionButtonProps = {
437
449
  actionType: HeaderActionType;
438
450
  configuration: ActionButtonsStyleConfiguration;
439
451
  focused?: boolean;
452
+ label?: string;
440
453
  onPress?: () => void;
441
454
  };
442
455
 
@@ -444,8 +457,24 @@ function HeaderActionButton({
444
457
  actionType,
445
458
  configuration,
446
459
  focused = false,
460
+ label,
447
461
  onPress,
448
462
  }: HeaderActionButtonProps) {
463
+ const paddingTop =
464
+ configuration.paddingTop ?? ACTION_BUTTON_LAYOUT_SPEC.paddingTop;
465
+
466
+ const paddingRight =
467
+ configuration.paddingRight ?? ACTION_BUTTON_LAYOUT_SPEC.paddingRight;
468
+
469
+ const paddingBottom =
470
+ configuration.paddingBottom ?? ACTION_BUTTON_LAYOUT_SPEC.paddingBottom;
471
+
472
+ const paddingLeft =
473
+ configuration.paddingLeft ?? ACTION_BUTTON_LAYOUT_SPEC.paddingLeft;
474
+
475
+ const cornerRadius =
476
+ configuration.cornerRadius ?? ACTION_BUTTON_LAYOUT_SPEC.cornerRadius;
477
+
449
478
  return (
450
479
  <Pressable
451
480
  onPress={onPress}
@@ -454,11 +483,11 @@ function HeaderActionButton({
454
483
  pressed || focused
455
484
  ? configuration.focusedBackgroundColor
456
485
  : configuration.defaultBackgroundColor,
457
- borderRadius: ACTION_BUTTON_LAYOUT_SPEC.cornerRadius,
458
- paddingTop: ACTION_BUTTON_LAYOUT_SPEC.paddingTop,
459
- paddingRight: ACTION_BUTTON_LAYOUT_SPEC.paddingRight,
460
- paddingBottom: ACTION_BUTTON_LAYOUT_SPEC.paddingBottom,
461
- paddingLeft: ACTION_BUTTON_LAYOUT_SPEC.paddingLeft,
486
+ borderRadius: cornerRadius,
487
+ paddingTop,
488
+ paddingRight,
489
+ paddingBottom,
490
+ paddingLeft,
462
491
  })}
463
492
  >
464
493
  <Text
@@ -481,7 +510,7 @@ function HeaderActionButton({
481
510
  }}
482
511
  numberOfLines={1}
483
512
  >
484
- {ACTION_LABELS[actionType]}
513
+ {label || ACTION_LABELS[actionType]}
485
514
  </Text>
486
515
  </Pressable>
487
516
  );
@@ -594,14 +623,20 @@ export function AudioPlayerHeader({
594
623
  onClear,
595
624
  }: AudioPlayerHeaderProps) {
596
625
  const modalBlocksStyle = useModalBlocksStyle();
597
- const headerBlock = modalBlocksStyle?.header;
626
+
627
+ // Next Up section header uses sectionHeader — never Theme modal `header`,
628
+ // so the Queue title styles do not collide with the section title.
629
+ const styleBlock =
630
+ configuration.variant === "section"
631
+ ? modalBlocksStyle?.sectionHeader
632
+ : modalBlocksStyle?.header;
598
633
 
599
634
  const effectiveConfig = React.useMemo(() => {
600
- return mergeHeaderConfiguration(configuration, headerBlock);
601
- }, [configuration, headerBlock]);
635
+ return mergeHeaderConfiguration(configuration, styleBlock);
636
+ }, [configuration, styleBlock]);
602
637
 
603
638
  const { variant, title, closeButton, actionButtons } = effectiveConfig;
604
- const { title: titleText } = data;
639
+ const { title: titleText, clearLabel } = data;
605
640
 
606
641
  const container = CONTAINER_SPEC[variant];
607
642
  const behavior = ACTION_BUTTONS_BEHAVIOR[variant];
@@ -612,16 +647,24 @@ export function AudioPlayerHeader({
612
647
  behavior.sectionEnabled
613
648
  );
614
649
 
615
- const showTrailingAction = shouldShowActionSlot(
616
- behavior.trailing,
617
- behavior.sectionEnabled
618
- );
650
+ const showTrailingAction =
651
+ shouldShowActionSlot(behavior.trailing, behavior.sectionEnabled) &&
652
+ (behavior.trailing.actionType !== "clear" || !!onClear);
653
+
654
+ const actionLabelFor = (actionType?: HeaderActionType) => {
655
+ if (actionType === "clear" && clearLabel) {
656
+ return clearLabel;
657
+ }
658
+
659
+ return actionType ? ACTION_LABELS[actionType] : undefined;
660
+ };
619
661
 
620
662
  const leadingContent = showLeadingAction ? (
621
663
  <HeaderActionButton
622
- actionType={behavior.leading.actionType}
664
+ actionType={behavior.leading.actionType as HeaderActionType}
623
665
  configuration={actionButtons}
624
666
  focused={focusedLeadingAction}
667
+ label={actionLabelFor(behavior.leading.actionType)}
625
668
  onPress={onCancel}
626
669
  />
627
670
  ) : onBack ? (
@@ -630,9 +673,10 @@ export function AudioPlayerHeader({
630
673
 
631
674
  const trailingContent = showTrailingAction ? (
632
675
  <HeaderActionButton
633
- actionType={behavior.trailing.actionType}
676
+ actionType={behavior.trailing.actionType as HeaderActionType}
634
677
  configuration={actionButtons}
635
678
  focused={focusedTrailingAction}
679
+ label={actionLabelFor(behavior.trailing.actionType)}
636
680
  onPress={behavior.trailing.actionType === "done" ? onDone : onClear}
637
681
  />
638
682
  ) : closeButton.enabled && !isCenterAligned ? (
@@ -14,12 +14,14 @@ type NowPlayingHeaderSectionProps = {
14
14
  onClear?: () => void;
15
15
  nextUpTitle?: string;
16
16
  nowPlayingTitle?: string;
17
+ clearLabel?: string;
17
18
  };
18
19
 
19
20
  export function NowPlayingHeaderSection({
20
21
  onClear,
21
22
  nextUpTitle = "Next Up",
22
23
  nowPlayingTitle = "Now Playing",
24
+ clearLabel,
23
25
  }: NowPlayingHeaderSectionProps) {
24
26
  const player = usePlayer();
25
27
  const playerState = usePlayerState("queue-now-playing-section");
@@ -74,7 +76,7 @@ export function NowPlayingHeaderSection({
74
76
  {renderNowPlayingTrack()}
75
77
  <AudioPlayerHeader
76
78
  configuration={VARIANT_SECTION_HEADER}
77
- data={{ title: nextUpTitle }}
79
+ data={{ title: nextUpTitle, clearLabel }}
78
80
  onClear={onClear}
79
81
  />
80
82
  </View>
@@ -1,7 +1,12 @@
1
1
  import {
2
2
  mergeItemConfiguration,
3
3
  mergeButtonConfiguration,
4
+ mergeHeaderConfiguration,
4
5
  } from "../mergeStyles";
6
+ import {
7
+ VARIANT_SECTION_HEADER,
8
+ VARIANT_SINGLE_LINE_HEADER,
9
+ } from "../../Components/Header";
5
10
 
6
11
  const base = {
7
12
  background: {
@@ -246,3 +251,106 @@ describe("mergeButtonConfiguration background", () => {
246
251
  expect(merged.title.textTransform).toBe("uppercase");
247
252
  });
248
253
  });
254
+
255
+ describe("mergeHeaderConfiguration section header (Variant 3)", () => {
256
+ it("merges section title + Clear action button styles", () => {
257
+ const merged = mergeHeaderConfiguration(VARIANT_SECTION_HEADER, {
258
+ title: {
259
+ default: {
260
+ fontColor: "#ABCDEF",
261
+ fontSize: 15,
262
+ lineHeight: 36,
263
+ fontFamily: "CustomSection",
264
+ letterSpacing: -0.2,
265
+ textTransform: "uppercase",
266
+ alignment: "left",
267
+ },
268
+ },
269
+ actionButtons: {
270
+ default: {
271
+ backgroundColor: "rgba(10, 10, 10, 1)",
272
+ fontColor: "#EEEEEE",
273
+ fontSize: 12,
274
+ borderRadius: 20,
275
+ paddingTop: 8,
276
+ paddingRight: 12,
277
+ paddingBottom: 8,
278
+ paddingLeft: 12,
279
+ },
280
+ focused: {
281
+ backgroundColor: "rgba(20, 20, 20, 1)",
282
+ fontColor: "#FFDD00",
283
+ },
284
+ },
285
+ });
286
+
287
+ expect(merged.title.fontColor).toBe("#ABCDEF");
288
+ expect(merged.title.fontSize).toBe(15);
289
+ expect(merged.title.lineHeight).toBe(36);
290
+ expect(merged.title.iosFontFamily).toBe("CustomSection");
291
+ expect(merged.title.androidFontFamily).toBe("CustomSection");
292
+ expect(merged.title.iosLetterSpacing).toBe(-0.2);
293
+ expect(merged.title.textTransform).toBe("uppercase");
294
+
295
+ expect(merged.actionButtons.defaultBackgroundColor).toBe(
296
+ "rgba(10, 10, 10, 1)"
297
+ );
298
+
299
+ expect(merged.actionButtons.focusedBackgroundColor).toBe(
300
+ "rgba(20, 20, 20, 1)"
301
+ );
302
+
303
+ expect(merged.actionButtons.fontColor).toBe("#EEEEEE");
304
+ expect(merged.actionButtons.focusedFontColor).toBe("#FFDD00");
305
+ expect(merged.actionButtons.fontSize).toBe(12);
306
+ expect(merged.actionButtons.cornerRadius).toBe(20);
307
+ expect(merged.actionButtons.paddingTop).toBe(8);
308
+ expect(merged.actionButtons.paddingLeft).toBe(12);
309
+ });
310
+
311
+ it("uses title.default when only platform font and letter spacing are set", () => {
312
+ const merged = mergeHeaderConfiguration(VARIANT_SECTION_HEADER, {
313
+ title: {
314
+ default: {
315
+ iosFontFamily: "SFProText-Semibold",
316
+ androidFontFamily: "Roboto-Medium",
317
+ iosLetterSpacing: -0.4,
318
+ androidLetterSpacing: 0,
319
+ },
320
+ },
321
+ });
322
+
323
+ expect(merged.title.iosFontFamily).toBe("SFProText-Semibold");
324
+ expect(merged.title.androidFontFamily).toBe("Roboto-Medium");
325
+ expect(merged.title.iosLetterSpacing).toBe(-0.4);
326
+ expect(merged.title.androidLetterSpacing).toBe(0);
327
+ expect(merged.title.fontSize).toBe(VARIANT_SECTION_HEADER.title.fontSize);
328
+ });
329
+
330
+ it("does not change section defaults when block is empty", () => {
331
+ const merged = mergeHeaderConfiguration(VARIANT_SECTION_HEADER, {
332
+ title: { default: {} },
333
+ actionButtons: { default: {} },
334
+ });
335
+
336
+ expect(merged.title.fontSize).toBe(VARIANT_SECTION_HEADER.title.fontSize);
337
+
338
+ expect(merged.actionButtons.defaultBackgroundColor).toBe(
339
+ VARIANT_SECTION_HEADER.actionButtons.defaultBackgroundColor
340
+ );
341
+ });
342
+
343
+ it("leaves single-line header title size untouched when only actionButtons set", () => {
344
+ const merged = mergeHeaderConfiguration(VARIANT_SINGLE_LINE_HEADER, {
345
+ actionButtons: {
346
+ default: { backgroundColor: "#123456" },
347
+ },
348
+ });
349
+
350
+ expect(merged.title.fontSize).toBe(
351
+ VARIANT_SINGLE_LINE_HEADER.title.fontSize
352
+ );
353
+
354
+ expect(merged.actionButtons.defaultBackgroundColor).toBe("#123456");
355
+ });
356
+ });
@@ -9,6 +9,7 @@ import {
9
9
  } from "../Components/Item";
10
10
  import {
11
11
  TitleConfiguration as HeaderTitleConfiguration,
12
+ ActionButtonsStyleConfiguration as HeaderActionButtonsStyleConfiguration,
12
13
  HeaderConfiguration,
13
14
  } from "../Components/Header";
14
15
  import {
@@ -210,6 +211,122 @@ function mergeHeaderTitle(
210
211
  ...base,
211
212
  ...(override.fontColor ? { fontColor: override.fontColor } : {}),
212
213
  ...(override.fontSize !== undefined ? { fontSize: override.fontSize } : {}),
214
+ ...(override.lineHeight !== undefined
215
+ ? { lineHeight: override.lineHeight }
216
+ : {}),
217
+ ...(typeof override.fontFamily === "string"
218
+ ? {
219
+ iosFontFamily: override.fontFamily,
220
+ androidFontFamily: override.fontFamily,
221
+ }
222
+ : {}),
223
+ ...(override.iosFontFamily
224
+ ? { iosFontFamily: override.iosFontFamily }
225
+ : {}),
226
+ ...(override.androidFontFamily
227
+ ? { androidFontFamily: override.androidFontFamily }
228
+ : {}),
229
+ ...(override.letterSpacing !== undefined
230
+ ? {
231
+ iosLetterSpacing: override.letterSpacing,
232
+ androidLetterSpacing: override.letterSpacing,
233
+ }
234
+ : {}),
235
+ ...(override.iosLetterSpacing !== undefined
236
+ ? { iosLetterSpacing: override.iosLetterSpacing }
237
+ : {}),
238
+ ...(override.androidLetterSpacing !== undefined
239
+ ? { androidLetterSpacing: override.androidLetterSpacing }
240
+ : {}),
241
+ ...(override.textTransform
242
+ ? { textTransform: override.textTransform }
243
+ : {}),
244
+ ...(override.alignment ? { alignment: override.alignment } : {}),
245
+ };
246
+ }
247
+
248
+ function mergeHeaderActionButtons(
249
+ base: HeaderActionButtonsStyleConfiguration,
250
+ defaultOverride?: Record<string, any>,
251
+ focusedOverride?: Record<string, any>
252
+ ): HeaderActionButtonsStyleConfiguration {
253
+ if (!defaultOverride && !focusedOverride) return base;
254
+
255
+ const focusedBackgroundColor =
256
+ focusedOverride?.backgroundColor ??
257
+ defaultOverride?.focusBackgroundColor ??
258
+ defaultOverride?.focusedBackgroundColor;
259
+
260
+ const focusedFontColor =
261
+ focusedOverride?.fontColor ??
262
+ defaultOverride?.focusFontColor ??
263
+ defaultOverride?.focusedFontColor;
264
+
265
+ return {
266
+ ...base,
267
+ ...(defaultOverride?.backgroundColor
268
+ ? { defaultBackgroundColor: defaultOverride.backgroundColor }
269
+ : {}),
270
+ ...(focusedBackgroundColor != null && focusedBackgroundColor !== ""
271
+ ? { focusedBackgroundColor }
272
+ : {}),
273
+ ...(defaultOverride?.fontColor
274
+ ? { fontColor: defaultOverride.fontColor }
275
+ : {}),
276
+ ...(focusedFontColor != null && focusedFontColor !== ""
277
+ ? { focusedFontColor }
278
+ : {}),
279
+ ...(defaultOverride?.fontSize !== undefined
280
+ ? { fontSize: defaultOverride.fontSize }
281
+ : {}),
282
+ ...(defaultOverride?.lineHeight !== undefined
283
+ ? { lineHeight: defaultOverride.lineHeight }
284
+ : {}),
285
+ ...(typeof defaultOverride?.fontFamily === "string"
286
+ ? {
287
+ iosFontFamily: defaultOverride.fontFamily,
288
+ androidFontFamily: defaultOverride.fontFamily,
289
+ }
290
+ : {}),
291
+ ...(defaultOverride?.iosFontFamily
292
+ ? { iosFontFamily: defaultOverride.iosFontFamily }
293
+ : {}),
294
+ ...(defaultOverride?.androidFontFamily
295
+ ? { androidFontFamily: defaultOverride.androidFontFamily }
296
+ : {}),
297
+ ...(defaultOverride?.letterSpacing !== undefined
298
+ ? {
299
+ iosLetterSpacing: defaultOverride.letterSpacing,
300
+ androidLetterSpacing: defaultOverride.letterSpacing,
301
+ }
302
+ : {}),
303
+ ...(defaultOverride?.iosLetterSpacing !== undefined
304
+ ? { iosLetterSpacing: defaultOverride.iosLetterSpacing }
305
+ : {}),
306
+ ...(defaultOverride?.androidLetterSpacing !== undefined
307
+ ? { androidLetterSpacing: defaultOverride.androidLetterSpacing }
308
+ : {}),
309
+ ...(defaultOverride?.textTransform
310
+ ? { textTransform: defaultOverride.textTransform }
311
+ : {}),
312
+ ...(defaultOverride?.borderRadius !== undefined
313
+ ? { cornerRadius: defaultOverride.borderRadius }
314
+ : {}),
315
+ ...(defaultOverride?.cornerRadius !== undefined
316
+ ? { cornerRadius: defaultOverride.cornerRadius }
317
+ : {}),
318
+ ...(defaultOverride?.paddingTop !== undefined
319
+ ? { paddingTop: defaultOverride.paddingTop }
320
+ : {}),
321
+ ...(defaultOverride?.paddingRight !== undefined
322
+ ? { paddingRight: defaultOverride.paddingRight }
323
+ : {}),
324
+ ...(defaultOverride?.paddingBottom !== undefined
325
+ ? { paddingBottom: defaultOverride.paddingBottom }
326
+ : {}),
327
+ ...(defaultOverride?.paddingLeft !== undefined
328
+ ? { paddingLeft: defaultOverride.paddingLeft }
329
+ : {}),
213
330
  };
214
331
  }
215
332
 
@@ -221,9 +338,39 @@ export function mergeHeaderConfiguration(
221
338
 
222
339
  const bgOverride = headerBlock.container?.default?.backgroundColor;
223
340
 
341
+ const actionButtonsDefault =
342
+ headerBlock.actionButtons?.default || headerBlock.actionButtons;
343
+
344
+ const actionButtonsFocused =
345
+ headerBlock.actionButtons?.focused || headerBlock.actionButtons?.focus;
346
+
347
+ // Parser shape is title.default.*; hydrate() returns a flat title object.
348
+ const titleDefault = headerBlock.title?.default;
349
+
350
+ const titleOverride =
351
+ titleDefault &&
352
+ (titleDefault.fontColor !== undefined ||
353
+ titleDefault.fontSize !== undefined ||
354
+ titleDefault.lineHeight !== undefined ||
355
+ titleDefault.fontFamily ||
356
+ titleDefault.iosFontFamily ||
357
+ titleDefault.androidFontFamily ||
358
+ titleDefault.letterSpacing !== undefined ||
359
+ titleDefault.iosLetterSpacing !== undefined ||
360
+ titleDefault.androidLetterSpacing !== undefined ||
361
+ titleDefault.textTransform ||
362
+ titleDefault.alignment)
363
+ ? titleDefault
364
+ : headerBlock.title;
365
+
224
366
  return {
225
367
  ...base,
226
- title: mergeHeaderTitle(base.title, headerBlock.title?.default),
368
+ title: mergeHeaderTitle(base.title, titleOverride),
369
+ actionButtons: mergeHeaderActionButtons(
370
+ base.actionButtons,
371
+ actionButtonsDefault,
372
+ actionButtonsFocused
373
+ ),
227
374
  ...(bgOverride ? { backgroundColor: bgOverride } : {}),
228
375
  };
229
376
  }
@@ -150,17 +150,81 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
150
150
  [plugins, themePluginId]
151
151
  );
152
152
 
153
- const styleOverrides = useMemo(() => {
154
- if (propStyleOverrides) {
155
- return propStyleOverrides;
156
- }
157
-
153
+ const styleOverrides = useMemo<ModalBlocksConfig | null>(() => {
158
154
  const configuration =
159
155
  plugin?.configuration || (plugin as any)?.configuration_json;
160
156
 
161
- if (!configuration || !themePluginId) return null;
157
+ const parsed =
158
+ configuration && themePluginId
159
+ ? parseModalBlocksConfiguration(configuration, themePluginId)
160
+ : null;
161
+
162
+ if (!propStyleOverrides) {
163
+ return parsed;
164
+ }
165
+
166
+ // hydrate(QueueStyles) only emits sectionHeader after the plugin class
167
+ // includes it. Until then, Studio/local keys in configuration_json still
168
+ // apply via the parser.
169
+ const hydratedSection = propStyleOverrides.sectionHeader;
170
+
171
+ const hasTitleStyle = (style?: Record<string, any>) =>
172
+ Boolean(
173
+ style &&
174
+ (style.fontColor ||
175
+ style.fontSize !== undefined ||
176
+ style.lineHeight !== undefined ||
177
+ style.fontFamily ||
178
+ style.iosFontFamily ||
179
+ style.androidFontFamily ||
180
+ style.letterSpacing !== undefined ||
181
+ style.iosLetterSpacing !== undefined ||
182
+ style.androidLetterSpacing !== undefined ||
183
+ style.textTransform ||
184
+ style.alignment)
185
+ );
186
+
187
+ const hasActionStyle = (style?: Record<string, any>) =>
188
+ Boolean(
189
+ style &&
190
+ (style.backgroundColor ||
191
+ style.fontColor ||
192
+ style.fontSize !== undefined ||
193
+ style.lineHeight !== undefined ||
194
+ style.fontFamily ||
195
+ style.iosFontFamily ||
196
+ style.androidFontFamily ||
197
+ style.letterSpacing !== undefined ||
198
+ style.iosLetterSpacing !== undefined ||
199
+ style.androidLetterSpacing !== undefined ||
200
+ style.textTransform ||
201
+ style.borderRadius !== undefined ||
202
+ style.cornerRadius !== undefined ||
203
+ style.paddingTop !== undefined ||
204
+ style.paddingRight !== undefined ||
205
+ style.paddingBottom !== undefined ||
206
+ style.paddingLeft !== undefined)
207
+ );
162
208
 
163
- return parseModalBlocksConfiguration(configuration, themePluginId);
209
+ const actionNode = hydratedSection?.actionButtons;
210
+
211
+ // An empty `default: {}` is truthy and would hide fields set on the flat
212
+ // object or on the focused bucket.
213
+ const hasHydratedSection =
214
+ hasTitleStyle(hydratedSection?.title?.default) ||
215
+ hasTitleStyle(hydratedSection?.title) ||
216
+ hasActionStyle(actionNode?.default) ||
217
+ hasActionStyle(actionNode?.focused) ||
218
+ hasActionStyle(actionNode?.focus) ||
219
+ hasActionStyle(actionNode);
220
+
221
+ return {
222
+ ...parsed,
223
+ ...propStyleOverrides,
224
+ sectionHeader: hasHydratedSection
225
+ ? hydratedSection
226
+ : parsed?.sectionHeader,
227
+ };
164
228
  }, [propStyleOverrides, plugin, themePluginId]);
165
229
 
166
230
  const pluginLocalizations = useLocalizedStrings({
@@ -178,17 +242,26 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
178
242
  const nextUpTitle =
179
243
  pluginLocalizations?.next_up_title ||
180
244
  (normalizedPluginId
181
- ? pluginLocalizations?.[`${normalizedPluginId}_next_up_title`]
245
+ ? pluginLocalizations?.[`${normalizedPluginId}_next_up_title`] ||
246
+ pluginLocalizations?.[`${normalizedPluginId}_label_next_up`]
182
247
  : undefined) ||
183
248
  "Next Up";
184
249
 
185
250
  const nowPlayingTitle =
186
251
  pluginLocalizations?.now_playing_title ||
187
252
  (normalizedPluginId
188
- ? pluginLocalizations?.[`${normalizedPluginId}_now_playing_title`]
253
+ ? pluginLocalizations?.[`${normalizedPluginId}_now_playing_title`] ||
254
+ pluginLocalizations?.[`${normalizedPluginId}_label_now_playing`]
189
255
  : undefined) ||
190
256
  "Now Playing";
191
257
 
258
+ const clearLabel =
259
+ pluginLocalizations?.clear_label ||
260
+ (normalizedPluginId
261
+ ? pluginLocalizations?.[`${normalizedPluginId}_label_clear`]
262
+ : undefined) ||
263
+ "Clear";
264
+
192
265
  const resolvedTitle = props.title || localizedHeaderTitle || title;
193
266
 
194
267
  const itemsToRender = viewModel ? fetchedItems : items;
@@ -336,6 +409,7 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
336
409
  onClear={hasClearAll ? handleDeleteCollection : undefined}
337
410
  nextUpTitle={nextUpTitle}
338
411
  nowPlayingTitle={nowPlayingTitle}
412
+ clearLabel={clearLabel}
339
413
  />
340
414
  ),
341
415
  });
@@ -361,6 +435,7 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
361
435
  handleDeleteCollection,
362
436
  nextUpTitle,
363
437
  nowPlayingTitle,
438
+ clearLabel,
364
439
  hasAdd,
365
440
  handleCreatePlaylist,
366
441
  ]);
@@ -470,6 +545,7 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
470
545
  onClear={hasClearAll ? handleDeleteCollection : undefined}
471
546
  nextUpTitle={nextUpTitle}
472
547
  nowPlayingTitle={nowPlayingTitle}
548
+ clearLabel={clearLabel}
473
549
  />
474
550
  ) : null}
475
551
  {hasAdd ? (
@@ -141,6 +141,10 @@ const {
141
141
  useGetScreenOrientation,
142
142
  } = require("@applicaster/zapp-react-native-utils/appUtils/orientationHelper");
143
143
 
144
+ const {
145
+ useIsScreenActive,
146
+ } = require("@applicaster/zapp-react-native-utils/reactHooks/navigation/useIsScreenActive");
147
+
144
148
  const navigator = {};
145
149
  const navBarProps = { nav_items: [] };
146
150
  const screenData = {};
@@ -195,4 +199,24 @@ describe("<Screen Component />", () => {
195
199
  expect(toJSON()).toMatchSnapshot();
196
200
  });
197
201
  });
202
+
203
+ describe("when the screen is inactive", () => {
204
+ afterEach(() => {
205
+ useIsScreenActive.mockReturnValue(true);
206
+ });
207
+
208
+ it("hides the screen from VoiceOver (iOS) and TalkBack (Android)", () => {
209
+ useIsScreenActive.mockReturnValue(false);
210
+
211
+ const { UNSAFE_root } = renderWithProviders(
212
+ <Screen {...screenProps} />,
213
+ store
214
+ );
215
+
216
+ expect(UNSAFE_root.findByType(View).props).toMatchObject({
217
+ accessibilityElementsHidden: true,
218
+ importantForAccessibility: "no-hide-descendants",
219
+ });
220
+ });
221
+ });
198
222
  });
@@ -13,6 +13,7 @@ exports[`<Screen Component /> when the navbar should be hidden renders correctly
13
13
  }
14
14
  >
15
15
  <View
16
+ accessibilityElementsHidden={false}
16
17
  importantForAccessibility="yes"
17
18
  style={
18
19
  {
@@ -58,6 +59,7 @@ exports[`<Screen Component /> when the navbar should show renders correctly 1`]
58
59
  }
59
60
  >
60
61
  <View
62
+ accessibilityElementsHidden={false}
61
63
  importantForAccessibility="yes"
62
64
  style={
63
65
  {
@@ -121,6 +121,11 @@ export function Screen(_props: Props) {
121
121
  ref={ref}
122
122
  style={style}
123
123
  importantForAccessibility={!isActive ? "no-hide-descendants" : "yes"}
124
+ // `importantForAccessibility` only affects Android (see RN docs).
125
+ // Without the iOS counterpart below, VoiceOver keeps traversing and
126
+ // focusing elements on an inactive/underlying screen (e.g. Albums In
127
+ // Order) even after a new screen (e.g. an Album) is pushed on top.
128
+ accessibilityElementsHidden={!isActive}
124
129
  >
125
130
  {isReady ? (
126
131
  <>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "16.0.0-rc.92",
3
+ "version": "16.0.0-rc.94",
4
4
  "description": "Applicaster Zapp React Native ui components for the Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,10 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "16.0.0-rc.92",
32
- "@applicaster/zapp-react-native-bridge": "16.0.0-rc.92",
33
- "@applicaster/zapp-react-native-redux": "16.0.0-rc.92",
34
- "@applicaster/zapp-react-native-utils": "16.0.0-rc.92",
31
+ "@applicaster/applicaster-types": "16.0.0-rc.94",
32
+ "@applicaster/zapp-react-native-bridge": "16.0.0-rc.94",
33
+ "@applicaster/zapp-react-native-redux": "16.0.0-rc.94",
34
+ "@applicaster/zapp-react-native-utils": "16.0.0-rc.94",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "react-native-sortables": "1.7.1",