@applicaster/zapp-react-native-ui-components 16.0.0-alpha.9235607812 → 16.0.0-alpha.9338540010

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 (33) hide show
  1. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +12 -3
  2. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +1 -1
  3. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +4 -2
  4. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +2 -2
  5. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +2 -1
  6. package/Components/MasterCell/__tests__/masterCell.test.js +60 -0
  7. package/Components/MasterCell/index.tsx +28 -4
  8. package/Components/MasterCell/utils/__tests__/withoutFocusedState.test.js +20 -0
  9. package/Components/MasterCell/utils/index.ts +18 -0
  10. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +22 -1
  11. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +52 -1
  12. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +77 -0
  13. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +58 -16
  14. package/Components/ModalComponent/BottomSheetModalContent.tsx +9 -74
  15. package/Components/ModalComponent/__tests__/showTextInput.test.ts +25 -0
  16. package/Components/ModalComponent/showTextInput.ts +2 -0
  17. package/Components/River/ComponentsMap/ComponentsMap.tsx +4 -1
  18. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
  19. package/Components/Transitioner/AnimationManager.js +1 -1
  20. package/Components/Transitioner/Scene.tsx +4 -3
  21. package/Components/Transitioner/Transitioner.tsx +69 -30
  22. package/Components/Transitioner/__tests__/transitioner.test.js +308 -20
  23. package/Components/VideoModal/PlayerWrapper.tsx +4 -1
  24. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  25. package/Components/VideoModal/__tests__/playerWrapperStyle.test.ts +58 -0
  26. package/Components/VideoModal/__tests__/playerWrapperUtils.test.ts +16 -0
  27. package/Components/VideoModal/playerWrapperStyle.ts +21 -3
  28. package/Components/VideoModal/playerWrapperUtils.ts +7 -1
  29. package/Decorators/ZappPipesDataConnector/__tests__/screenToolbox.test.ts +347 -0
  30. package/Decorators/ZappPipesDataConnector/__tests__/screenToolboxUnavailable.test.ts +53 -0
  31. package/Decorators/ZappPipesDataConnector/index.tsx +19 -4
  32. package/Decorators/ZappPipesDataConnector/screenToolbox.ts +384 -0
  33. package/package.json +5 -5
@@ -245,10 +245,19 @@ const LiveImageComponent = (props: LiveImageProps) => {
245
245
 
246
246
  const isCurrentlyFocused = useTrackCurrentAutoScrollingElement(componentId);
247
247
 
248
- const { playerPluginId, screenConfig, preloadHooks } =
249
- getPlayerConfig(player_screen_id, actionIdentifier) ?? {};
248
+ // New object identities here restart PlayerLiveImage's register effect,
249
+ // which throws away the in-flight player and runs preload hooks again.
250
+ const playerConfig = React.useMemo(
251
+ () => getPlayerConfig(player_screen_id, actionIdentifier),
252
+ [player_screen_id, actionIdentifier]
253
+ );
254
+
255
+ const { playerPluginId, screenConfig, preloadHooks } = playerConfig ?? {};
250
256
 
251
- const playableEntry = playerPluginId ? getPlayableEntry(item) : null;
257
+ const playableEntry = React.useMemo(
258
+ () => (playerPluginId ? getPlayableEntry(item) : null),
259
+ [playerPluginId, item?.id]
260
+ );
252
261
 
253
262
  const playerPlugin = getPluginByIdentifier(playerPluginId);
254
263
 
@@ -166,7 +166,7 @@ export const ActionButton = ({
166
166
  <PressableView
167
167
  testOnly_pressed={props.testOnly_pressed}
168
168
  testID={props.testID || `${entry?.id}`}
169
- style={style}
169
+ style={[style, props.alignment]}
170
170
  pressedStyle={focusedStyles}
171
171
  onPress={renderProps.onPress}
172
172
  accessibilityLabel={props.accessibilityLabel || `${entry?.id}`}
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { Pressable, Text as RNText, View } from "react-native";
2
+ import { Pressable, StyleSheet, Text as RNText, View } from "react-native";
3
3
  import { render, fireEvent } from "@testing-library/react-native";
4
4
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
5
5
 
@@ -282,7 +282,9 @@ describe("MobileActionButton", () => {
282
282
  expect(pressable.props.testID).toBe("mobile_action_button_1");
283
283
  expect(pressable.props.accessibilityLabel).toBe("entry-1");
284
284
 
285
- expect(pressable.props.style({ pressed: false })[0]).toMatchObject({
285
+ expect(
286
+ StyleSheet.flatten(pressable.props.style({ pressed: false }))
287
+ ).toMatchObject({
286
288
  backgroundColor: "rgba(1,1,1,1)",
287
289
  borderColor: "rgba(2,2,2,1)",
288
290
  borderWidth: 1,
@@ -20,13 +20,13 @@ const getValue = (key: string) => {
20
20
  };
21
21
 
22
22
  describe("CollapsibleTextContainer view tree", () => {
23
- it("stretches to the parent width so labels wrap using the cell width", () => {
23
+ it("aligns the label to the text alignment instead of stretching the cell", () => {
24
24
  const result = CollapsibleTextContainer({
25
25
  getValue,
26
26
  elements: [],
27
27
  additionalStyles: { backgroundColor: "transparent" },
28
28
  });
29
29
 
30
- expect(result.style.alignSelf).toBe("stretch");
30
+ expect(result.style.alignSelf).toBe("flex-start");
31
31
  });
32
32
  });
@@ -3,6 +3,7 @@ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/he
3
3
  import {
4
4
  getColorFromData,
5
5
  getKeyForLabel,
6
+ mapSelfAlignment,
6
7
  } from "@applicaster/zapp-react-native-utils/cellUtils";
7
8
  import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
8
9
 
@@ -22,7 +23,7 @@ export const CollapsibleTextContainer = ({
22
23
  return {
23
24
  type: "CollapsibleTextContainer", // Collapsible Text Container,
24
25
  style: {
25
- alignSelf: "stretch",
26
+ alignSelf: mapSelfAlignment(getValue("text_alignment")),
26
27
  marginTop: toNumberWithDefaultZero(getValue("margin_top")),
27
28
  marginRight: toNumberWithDefaultZero(getValue("margin_right")),
28
29
  marginBottom: toNumberWithDefaultZero(getValue("margin_bottom")),
@@ -76,6 +76,66 @@ describe("masterCellBuilder", () => {
76
76
  });
77
77
  });
78
78
 
79
+ describe("cellOptions.interactive", () => {
80
+ // renders a single View tagged with the state the cell was built with
81
+ const dataAdapter =
82
+ () =>
83
+ ({ state }) => [
84
+ { type: "View", style: {}, props: { testID: `state-${state}` } },
85
+ ];
86
+
87
+ // Only the root View gets this testID: cell elements pass their own testID, which overrides it
88
+ const ROOT_TEST_ID = "master-cell-root";
89
+
90
+ // forwardRef: MasterCell attaches a ref to its root View
91
+ const TestView = React.forwardRef((props, ref) => (
92
+ <View testID={ROOT_TEST_ID} {...props} ref={ref} />
93
+ ));
94
+
95
+ const renderCell = (cellOptions, state) => {
96
+ const MasterCell = masterCellBuilder({
97
+ cellConfiguration: singleView,
98
+ dataAdapter,
99
+ components: { View: TestView },
100
+ cellOptions,
101
+ });
102
+
103
+ return renderWithProviders(<MasterCell item={entry} state={state} />);
104
+ };
105
+
106
+ const getRoot = (wrapper) => wrapper.getByTestId(ROOT_TEST_ID);
107
+
108
+ it("keeps focused state and default root props when omitted", () => {
109
+ const wrapper = renderCell(undefined, "focused");
110
+ const root = getRoot(wrapper);
111
+
112
+ expect(wrapper.getByTestId("state-focused")).toBeDefined();
113
+ expect(root.props.onStartShouldSetResponder).toBeUndefined();
114
+ expect(root.props.collapsable).toBeUndefined();
115
+ });
116
+
117
+ it("claims body touches on a non-collapsable root when false", () => {
118
+ const wrapper = renderCell({ interactive: false }, "default");
119
+ const root = getRoot(wrapper);
120
+
121
+ expect(root.props.onStartShouldSetResponder()).toBe(true);
122
+ expect(root.props.collapsable).toBe(false);
123
+ });
124
+
125
+ it("renders focused as default when false", () => {
126
+ const wrapper = renderCell({ interactive: false }, "focused");
127
+
128
+ expect(wrapper.getByTestId("state-default")).toBeDefined();
129
+ expect(wrapper.queryByTestId("state-focused")).toBeNull();
130
+ });
131
+
132
+ it("renders focused_selected as selected when false", () => {
133
+ const wrapper = renderCell({ interactive: false }, "focused_selected");
134
+
135
+ expect(wrapper.getByTestId("state-selected")).toBeDefined();
136
+ });
137
+ });
138
+
79
139
  describe.skip("when using custom default components", () => {
80
140
  // the collapsable prop is added here to easily check from the snapshot that the mock
81
141
  // component is being used instead of the default one
@@ -10,7 +10,11 @@ import { useScreenState } from "@applicaster/zapp-react-native-utils/screenState
10
10
  import { defaultComponents } from "./DefaultComponents";
11
11
  import { defaultDataAdapter } from "./dataAdapter";
12
12
  import { elementMapper, mapElementWithKey } from "./elementMapper";
13
- import { getEntryState, hasFocusableInsideBuilder } from "./utils";
13
+ import {
14
+ getEntryState,
15
+ hasFocusableInsideBuilder,
16
+ withoutFocusedState,
17
+ } from "./utils";
14
18
 
15
19
  type Props = {
16
20
  item: ZappEntry;
@@ -19,6 +23,16 @@ type Props = {
19
23
 
20
24
  const defaultContainerStyle = { flex: 1 };
21
25
 
26
+ /**
27
+ * Root props of a non-interactive cell: the root view claims touches on the cell body,
28
+ * so the host list's touchable never fires (buttons, being deeper, still receive theirs).
29
+ * `collapsable: false` keeps Android / Fabric from flattening the view away.
30
+ */
31
+ const nonInteractiveRootProps = {
32
+ onStartShouldSetResponder: () => true,
33
+ collapsable: false,
34
+ };
35
+
22
36
  /**
23
37
  * Constructs a MasterCell with the provided options
24
38
  * @param {Object} options
@@ -27,6 +41,8 @@ const defaultContainerStyle = { flex: 1 };
27
41
  * @param {Function} options.dataAdapter optional dataAdapter to use instead of the default one.
28
42
  * See dataAdapter.js for more info
29
43
  * @param {containerStyles} options.containerStyle style to apply at the top level of the master cell
44
+ * @param {Object} options.cellOptions optional cell options. `interactive: false` makes the cell body
45
+ * ignore taps and render without focused / pressed states (inner buttons keep working). Defaults to `true`.
30
46
  * @returns {Function} Functional React Component to render the masterCell with the provided options
31
47
  */
32
48
 
@@ -39,6 +55,7 @@ type MasterCellBuilderOptions = {
39
55
  isRTL?: boolean;
40
56
  skipButtons?: boolean;
41
57
  component?: ZappComponent;
58
+ interactive?: boolean;
42
59
  };
43
60
  };
44
61
 
@@ -49,7 +66,7 @@ export function masterCellBuilder({
49
66
  containerStyle = defaultContainerStyle,
50
67
  cellOptions = {},
51
68
  }: MasterCellBuilderOptions) {
52
- const { skipButtons = false, component } = cellOptions;
69
+ const { skipButtons = false, component, interactive = true } = cellOptions;
53
70
 
54
71
  const componentsMap = R.merge(defaultComponents, components);
55
72
  const { View } = componentsMap;
@@ -88,7 +105,10 @@ export function masterCellBuilder({
88
105
  () =>
89
106
  elementsBuilder({
90
107
  entry: item,
91
- state: getEntryState(state, entryIsSelected),
108
+ state: getEntryState(
109
+ interactive ? state : withoutFocusedState(state),
110
+ entryIsSelected
111
+ ),
92
112
  allowDynamicColorsOutsideExtensions:
93
113
  theme?.allow_dynamic_colors_outside_extensions,
94
114
  }),
@@ -134,7 +154,11 @@ export function masterCellBuilder({
134
154
  );
135
155
 
136
156
  return (
137
- <View style={containerStyle} ref={wrapperRef}>
157
+ <View
158
+ style={containerStyle}
159
+ ref={wrapperRef}
160
+ {...(interactive ? undefined : nonInteractiveRootProps)}
161
+ >
138
162
  {elements.map(remapElements)}
139
163
  </View>
140
164
  );
@@ -0,0 +1,20 @@
1
+ import { withoutFocusedState } from "..";
2
+
3
+ describe("withoutFocusedState", () => {
4
+ it("maps focused to default", () => {
5
+ expect(withoutFocusedState("focused")).toBe("default");
6
+ });
7
+
8
+ it("maps focused_selected to selected", () => {
9
+ expect(withoutFocusedState("focused_selected")).toBe("selected");
10
+ });
11
+
12
+ it("keeps default and selected states", () => {
13
+ expect(withoutFocusedState("default")).toBe("default");
14
+ expect(withoutFocusedState("selected")).toBe("selected");
15
+ });
16
+
17
+ it("keeps a missing state", () => {
18
+ expect(withoutFocusedState(undefined)).toBeUndefined();
19
+ });
20
+ });
@@ -386,3 +386,21 @@ export function getEntryState(state, selected) {
386
386
 
387
387
  return selected ? "selected" : "default";
388
388
  }
389
+
390
+ /**
391
+ * Drops focused (pressed) variants of a cell state, so a non-interactive cell
392
+ * always renders its idle look. `selected` is kept – it is not touch feedback.
393
+ */
394
+ export function withoutFocusedState(
395
+ state?: CellState | null
396
+ ): CellState | null | undefined {
397
+ if (state === "focused_selected") {
398
+ return "selected";
399
+ }
400
+
401
+ if (state === "focused") {
402
+ return "default";
403
+ }
404
+
405
+ return state;
406
+ }
@@ -6,7 +6,7 @@
6
6
  * Green (configurable): background, image, nowPlayingAsset, nowPlayingLabel,
7
7
  * textLabel1, textLabel2
8
8
  * White (hardcoded): container padding/gutters, nowPlaying container layout,
9
- * button sizes/padding/assets
9
+ * button sizes/padding. Play/pause icons use plugin assets when set.
10
10
  *
11
11
  * Variants (VARIANT_*) are preset configuration bundles — not passed as a field.
12
12
  */
@@ -94,6 +94,10 @@ export type ActionButtonAssetsConfiguration = {
94
94
  removeAsset?: string;
95
95
  /** Trailing drag handle on Edit Order / queue rows. */
96
96
  dragAsset?: string;
97
+ /** Shown on the Now Playing row while the track is paused. */
98
+ playAsset?: string;
99
+ /** Shown on the Now Playing row while the track is playing. */
100
+ pauseAsset?: string;
97
101
  };
98
102
 
99
103
  export type ItemConfiguration = {
@@ -504,6 +508,23 @@ function ItemButton({
504
508
  const renderIcon = () => {
505
509
  switch (action) {
506
510
  case "playPause": {
511
+ const playbackAsset = isSelected
512
+ ? actionButtonAssets?.pauseAsset
513
+ : actionButtonAssets?.playAsset;
514
+
515
+ if (playbackAsset) {
516
+ return (
517
+ <Image
518
+ source={{ uri: playbackAsset }}
519
+ style={{
520
+ width: layout.assetWidth,
521
+ height: layout.assetHeight,
522
+ }}
523
+ resizeMode="contain"
524
+ />
525
+ );
526
+ }
527
+
507
528
  const iconSize = layout.assetWidth;
508
529
  const iconRadius = iconSize / 2;
509
530
 
@@ -1,8 +1,10 @@
1
1
  import React from "react";
2
- import { Pressable } from "react-native";
2
+ import { Image, Pressable } from "react-native";
3
3
  import { fireEvent, render, screen } from "@testing-library/react-native";
4
+ import { ModalBlocksStyleContext } from "../../../ModalBlocksStyleContext";
4
5
  import {
5
6
  AudioPlayerTrackItem,
7
+ VARIANT_NOW_PLAYING,
6
8
  VARIANT_PLAYLIST_ITEM,
7
9
  mapMenuItemToNowPlayingData,
8
10
  } from "../Item";
@@ -101,6 +103,55 @@ describe("mapMenuItemToNowPlayingData", () => {
101
103
  expect(data.textLabel2).toBe("Artist Name");
102
104
  });
103
105
 
106
+ it("uses the plugin play asset while paused and the pause asset while playing", () => {
107
+ const menuItem = {
108
+ id: "item_1",
109
+ title: "Track Title",
110
+ summary: "Artist Name",
111
+ };
112
+
113
+ const style = {
114
+ nowPlayingItem: {
115
+ actionButtons: {
116
+ default: {
117
+ playAsset: "https://example.com/play.png",
118
+ pauseAsset: "https://example.com/pause.png",
119
+ },
120
+ },
121
+ },
122
+ };
123
+
124
+ const { UNSAFE_getAllByType, rerender } = render(
125
+ <ModalBlocksStyleContext.Provider value={style}>
126
+ <AudioPlayerTrackItem
127
+ configuration={VARIANT_NOW_PLAYING}
128
+ data={mapMenuItemToNowPlayingData(menuItem as any, true)}
129
+ />
130
+ </ModalBlocksStyleContext.Provider>
131
+ );
132
+
133
+ expect(
134
+ UNSAFE_getAllByType(Image).some(
135
+ (node) => node.props.source?.uri === "https://example.com/play.png"
136
+ )
137
+ ).toBe(true);
138
+
139
+ rerender(
140
+ <ModalBlocksStyleContext.Provider value={style}>
141
+ <AudioPlayerTrackItem
142
+ configuration={VARIANT_NOW_PLAYING}
143
+ data={mapMenuItemToNowPlayingData(menuItem as any, false)}
144
+ />
145
+ </ModalBlocksStyleContext.Provider>
146
+ );
147
+
148
+ expect(
149
+ UNSAFE_getAllByType(Image).some(
150
+ (node) => node.props.source?.uri === "https://example.com/pause.png"
151
+ )
152
+ ).toBe(true);
153
+ });
154
+
104
155
  it("uses custom nowPlayingLabel when provided", () => {
105
156
  const data = mapMenuItemToNowPlayingData(
106
157
  item as any,
@@ -131,6 +131,43 @@ describe("mergeItemConfiguration background", () => {
131
131
  "https://placehold.co/24x24/FF3B30/FFFFFF/png?text=RH"
132
132
  );
133
133
  });
134
+
135
+ it("applies a text label font family and ignores a non-string font family", () => {
136
+ const withFont = mergeItemConfiguration(base as any, {
137
+ textLabel1: { default: { fontFamily: "Roboto-Bold" } },
138
+ });
139
+
140
+ expect(withFont.textLabel1.iosFontFamily).toBe("Roboto-Bold");
141
+ expect(withFont.textLabel1.androidFontFamily).toBe("Roboto-Bold");
142
+
143
+ const withEmpty = mergeItemConfiguration(base as any, {
144
+ textLabel1: {
145
+ default: { fontFamily: {}, iosFontFamily: "SFProText-Semibold" },
146
+ },
147
+ });
148
+
149
+ expect(withEmpty.textLabel1.iosFontFamily).toBe("SFProText-Semibold");
150
+ expect(withEmpty.textLabel1.androidFontFamily).toBeUndefined();
151
+ });
152
+
153
+ it("applies play and pause assets onto actionButtonAssets", () => {
154
+ const merged = mergeItemConfiguration(base as any, {
155
+ actionButtons: {
156
+ default: {
157
+ playAsset: "https://example.com/play.png",
158
+ pauseAsset: "https://example.com/pause.png",
159
+ },
160
+ },
161
+ });
162
+
163
+ expect(merged.actionButtonAssets?.playAsset).toBe(
164
+ "https://example.com/play.png"
165
+ );
166
+
167
+ expect(merged.actionButtonAssets?.pauseAsset).toBe(
168
+ "https://example.com/pause.png"
169
+ );
170
+ });
134
171
  });
135
172
 
136
173
  describe("mergeButtonConfiguration background", () => {
@@ -250,6 +287,46 @@ describe("mergeButtonConfiguration background", () => {
250
287
  expect(merged.title.androidLetterSpacing).toBe(-0.5);
251
288
  expect(merged.title.textTransform).toBe("uppercase");
252
289
  });
290
+
291
+ it("uses the family string inside a font-selector object and ignores {}", () => {
292
+ const fromObject = mergeButtonConfiguration(buttonBase as any, {
293
+ title: {
294
+ default: { fontFamily: { fontFamily: "GreycliffCF-Bold" } },
295
+ },
296
+ });
297
+
298
+ expect(fromObject.title.iosFontFamily).toBe("GreycliffCF-Bold");
299
+ expect(fromObject.title.androidFontFamily).toBe("GreycliffCF-Bold");
300
+
301
+ const fromEmpty = mergeButtonConfiguration(buttonBase as any, {
302
+ title: {
303
+ default: { fontFamily: {} },
304
+ },
305
+ });
306
+
307
+ expect(fromEmpty.title.iosFontFamily).toBe(buttonBase.title.iosFontFamily);
308
+
309
+ expect(fromEmpty.title.androidFontFamily).toBe(
310
+ buttonBase.title.androidFontFamily
311
+ );
312
+ });
313
+
314
+ it("maps inactive color, gutter, and focused and inactive font colors", () => {
315
+ const merged = mergeButtonConfiguration(buttonBase as any, {
316
+ background: {
317
+ default: { inactiveColor: "#999999", horizontalGutter: 6 },
318
+ },
319
+ title: {
320
+ default: { inactiveFontColor: "#111111" },
321
+ focused: { fontColor: "#222222" },
322
+ },
323
+ });
324
+
325
+ expect(merged.background.inactiveColor).toBe("#999999");
326
+ expect(merged.background.horizontalGutter).toBe(6);
327
+ expect(merged.title.inactiveFontColor).toBe("#111111");
328
+ expect(merged.title.focusedFontColor).toBe("#222222");
329
+ });
253
330
  });
254
331
 
255
332
  describe("mergeHeaderConfiguration section header (Variant 3)", () => {
@@ -82,7 +82,7 @@ function mergeItemLabel(
82
82
  ...(override.lineHeight !== undefined
83
83
  ? { lineHeight: override.lineHeight }
84
84
  : {}),
85
- ...(override.fontFamily
85
+ ...(typeof override.fontFamily === "string" && override.fontFamily
86
86
  ? {
87
87
  iosFontFamily: override.fontFamily,
88
88
  androidFontFamily: override.fontFamily,
@@ -147,6 +147,8 @@ function mergeItemActionButtonAssets(
147
147
  ...base,
148
148
  ...(override.removeAsset ? { removeAsset: override.removeAsset } : {}),
149
149
  ...(override.dragAsset ? { dragAsset: override.dragAsset } : {}),
150
+ ...(override.playAsset ? { playAsset: override.playAsset } : {}),
151
+ ...(override.pauseAsset ? { pauseAsset: override.pauseAsset } : {}),
150
152
  };
151
153
  }
152
154
 
@@ -379,6 +381,22 @@ export function mergeHeaderConfiguration(
379
381
  // Button Merging Helpers
380
382
  // ---------------------------------------------------------------------------
381
383
 
384
+ function fontFamilyName(value: unknown): string | undefined {
385
+ if (typeof value === "string" && value.trim()) {
386
+ return value;
387
+ }
388
+
389
+ if (value && typeof value === "object") {
390
+ const family = (value as { fontFamily?: unknown }).fontFamily;
391
+
392
+ if (typeof family === "string" && family.trim()) {
393
+ return family;
394
+ }
395
+ }
396
+
397
+ return undefined;
398
+ }
399
+
382
400
  function mergeButtonBackground(
383
401
  base: ButtonBackgroundConfiguration,
384
402
  defaultOverride?: Record<string, any>,
@@ -397,6 +415,9 @@ function mergeButtonBackground(
397
415
  ...(defaultOverride?.backgroundColor
398
416
  ? { defaultColor: defaultOverride.backgroundColor }
399
417
  : {}),
418
+ ...(defaultOverride?.inactiveColor
419
+ ? { inactiveColor: defaultOverride.inactiveColor }
420
+ : {}),
400
421
  ...(focusedColor != null && focusedColor !== "" ? { focusedColor } : {}),
401
422
  ...(defaultOverride?.borderRadius !== undefined
402
423
  ? { cornerRadius: defaultOverride.borderRadius }
@@ -413,47 +434,62 @@ function mergeButtonBackground(
413
434
  ...(defaultOverride?.paddingLeft !== undefined
414
435
  ? { paddingLeft: defaultOverride.paddingLeft }
415
436
  : {}),
437
+ ...(defaultOverride?.horizontalGutter !== undefined
438
+ ? { horizontalGutter: defaultOverride.horizontalGutter }
439
+ : {}),
416
440
  };
417
441
  }
418
442
 
419
443
  function mergeButtonTitle(
420
444
  base: ButtonTitleConfiguration,
421
- override?: Record<string, any>
445
+ override?: Record<string, any>,
446
+ focusedOverride?: Record<string, any>
422
447
  ): ButtonTitleConfiguration {
423
- if (!override) return base;
448
+ if (!override && !focusedOverride) return base;
449
+
450
+ const fontFamily = fontFamilyName(override?.fontFamily);
451
+
452
+ const focusedFontColor =
453
+ focusedOverride?.fontColor || override?.focusedFontColor;
454
+
455
+ const inactiveFontColor = override?.inactiveFontColor;
424
456
 
425
457
  return {
426
458
  ...base,
427
- ...(override.fontColor ? { fontColor: override.fontColor } : {}),
428
- ...(override.fontSize !== undefined ? { fontSize: override.fontSize } : {}),
429
- ...(override.lineHeight !== undefined
459
+ ...(override?.fontColor ? { fontColor: override.fontColor } : {}),
460
+ ...(focusedFontColor ? { focusedFontColor } : {}),
461
+ ...(inactiveFontColor ? { inactiveFontColor } : {}),
462
+ ...(override?.fontSize !== undefined
463
+ ? { fontSize: override.fontSize }
464
+ : {}),
465
+ ...(override?.lineHeight !== undefined
430
466
  ? { lineHeight: override.lineHeight }
431
467
  : {}),
432
- ...(override.fontFamily
468
+ ...(fontFamily
433
469
  ? {
434
- iosFontFamily: override.fontFamily,
435
- androidFontFamily: override.fontFamily,
470
+ iosFontFamily: fontFamily,
471
+ androidFontFamily: fontFamily,
436
472
  }
437
473
  : {}),
438
- ...(override.iosFontFamily
474
+ ...(override?.iosFontFamily
439
475
  ? { iosFontFamily: override.iosFontFamily }
440
476
  : {}),
441
- ...(override.androidFontFamily
477
+ ...(override?.androidFontFamily
442
478
  ? { androidFontFamily: override.androidFontFamily }
443
479
  : {}),
444
- ...(override.letterSpacing !== undefined
480
+ ...(override?.letterSpacing !== undefined
445
481
  ? {
446
482
  iosLetterSpacing: override.letterSpacing,
447
483
  androidLetterSpacing: override.letterSpacing,
448
484
  }
449
485
  : {}),
450
- ...(override.iosLetterSpacing !== undefined
486
+ ...(override?.iosLetterSpacing !== undefined
451
487
  ? { iosLetterSpacing: override.iosLetterSpacing }
452
488
  : {}),
453
- ...(override.androidLetterSpacing !== undefined
489
+ ...(override?.androidLetterSpacing !== undefined
454
490
  ? { androidLetterSpacing: override.androidLetterSpacing }
455
491
  : {}),
456
- ...(override.textTransform
492
+ ...(override?.textTransform
457
493
  ? { textTransform: override.textTransform }
458
494
  : {}),
459
495
  };
@@ -509,13 +545,19 @@ export function mergeButtonConfiguration(
509
545
  const titleOverride =
510
546
  buttonBlock?.title?.default || actionBlock?.title?.default;
511
547
 
548
+ const titleFocused =
549
+ buttonBlock?.title?.focused ||
550
+ buttonBlock?.title?.focus ||
551
+ actionBlock?.title?.focused ||
552
+ actionBlock?.title?.focus;
553
+
512
554
  const iconsOverride = buttonBlock?.icons || actionBlock?.icons;
513
555
  const leadingOverride = buttonBlock?.leadingIcon || actionBlock?.leadingIcon;
514
556
 
515
557
  return {
516
558
  ...base,
517
559
  background: mergeButtonBackground(base.background, bgDefault, bgFocused),
518
- title: mergeButtonTitle(base.title, titleOverride),
560
+ title: mergeButtonTitle(base.title, titleOverride, titleFocused),
519
561
  icons: mergeButtonIcons(base.icons, iconsOverride, leadingOverride),
520
562
  };
521
563
  }