@applicaster/zapp-react-native-ui-components 15.1.15-rc.0 → 15.1.16-rc.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/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
- package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +1 -2
- package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +64 -20
- package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +40 -15
- package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +25 -10
- package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
- package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +114 -6
- package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +148 -13
- package/Components/ModalComponent/BottomSheetModalContent.tsx +95 -9
- package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +81 -4
- package/package.json +5 -5
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CollapsibleTextContainer } from "../index";
|
|
2
|
+
|
|
3
|
+
const getValue = (key: string) => {
|
|
4
|
+
const values: Record<string, unknown> = {
|
|
5
|
+
text_alignment: "left",
|
|
6
|
+
margin_top: 0,
|
|
7
|
+
margin_right: 0,
|
|
8
|
+
margin_bottom: 0,
|
|
9
|
+
margin_left: 0,
|
|
10
|
+
padding_top: 0,
|
|
11
|
+
padding_right: 0,
|
|
12
|
+
padding_bottom: 0,
|
|
13
|
+
padding_left: 0,
|
|
14
|
+
corner_radius: 0,
|
|
15
|
+
data_key: "title",
|
|
16
|
+
custom_data_key: undefined,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return values[key];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe("CollapsibleTextContainer view tree", () => {
|
|
23
|
+
it("stretches to the parent width so labels wrap using the cell width", () => {
|
|
24
|
+
const result = CollapsibleTextContainer({
|
|
25
|
+
getValue,
|
|
26
|
+
elements: [],
|
|
27
|
+
additionalStyles: { backgroundColor: "transparent" },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(result.style.alignSelf).toBe("stretch");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -3,7 +3,6 @@ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/he
|
|
|
3
3
|
import {
|
|
4
4
|
getColorFromData,
|
|
5
5
|
getKeyForLabel,
|
|
6
|
-
mapSelfAlignment,
|
|
7
6
|
} from "@applicaster/zapp-react-native-utils/cellUtils";
|
|
8
7
|
import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
|
|
9
8
|
|
|
@@ -23,7 +22,7 @@ export const CollapsibleTextContainer = ({
|
|
|
23
22
|
return {
|
|
24
23
|
type: "CollapsibleTextContainer", // Collapsible Text Container,
|
|
25
24
|
style: {
|
|
26
|
-
alignSelf:
|
|
25
|
+
alignSelf: "stretch",
|
|
27
26
|
marginTop: toNumberWithDefaultZero(getValue("margin_top")),
|
|
28
27
|
marginRight: toNumberWithDefaultZero(getValue("margin_right")),
|
|
29
28
|
marginBottom: toNumberWithDefaultZero(getValue("margin_bottom")),
|
|
@@ -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
|
-
*
|
|
8
|
-
*
|
|
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:
|
|
458
|
-
paddingTop
|
|
459
|
-
paddingRight
|
|
460
|
-
paddingBottom
|
|
461
|
-
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
|
-
|
|
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,
|
|
601
|
-
}, [configuration,
|
|
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 =
|
|
616
|
-
behavior.trailing,
|
|
617
|
-
behavior.
|
|
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 ? (
|
|
@@ -54,6 +54,8 @@ export type BackgroundConfiguration = {
|
|
|
54
54
|
paddingRight?: number;
|
|
55
55
|
paddingBottom?: number;
|
|
56
56
|
paddingLeft?: number;
|
|
57
|
+
highlightInsetLeft?: number;
|
|
58
|
+
highlightInsetRight?: number;
|
|
57
59
|
};
|
|
58
60
|
|
|
59
61
|
export type ImageConfiguration = {
|
|
@@ -203,9 +205,9 @@ const CONTAINER_SPEC_LARGE_IMAGE: ContainerSpec = {
|
|
|
203
205
|
|
|
204
206
|
const CONTAINER_SPEC_DEFAULT: ContainerSpec = {
|
|
205
207
|
paddingTop: 8,
|
|
206
|
-
paddingRight:
|
|
208
|
+
paddingRight: 10,
|
|
207
209
|
paddingBottom: 8,
|
|
208
|
-
paddingLeft:
|
|
210
|
+
paddingLeft: 10,
|
|
209
211
|
horizontalGutter: 12,
|
|
210
212
|
verticalGutter: 2,
|
|
211
213
|
};
|
|
@@ -350,21 +352,30 @@ function ItemContainer({
|
|
|
350
352
|
onPress,
|
|
351
353
|
}: ItemContainerProps) {
|
|
352
354
|
const container = resolveContainerSpec(image);
|
|
353
|
-
const
|
|
355
|
+
const insetLeft = background.highlightInsetLeft ?? 0;
|
|
356
|
+
const insetRight = background.highlightInsetRight ?? 0;
|
|
354
357
|
|
|
355
358
|
return (
|
|
356
|
-
<Pressable
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
359
|
+
<Pressable onPress={onPress}>
|
|
360
|
+
{({ pressed }) => (
|
|
361
|
+
<View
|
|
362
|
+
style={{
|
|
363
|
+
marginLeft: insetLeft,
|
|
364
|
+
marginRight: insetRight,
|
|
365
|
+
backgroundColor:
|
|
366
|
+
pressed || focused
|
|
367
|
+
? background.focusedColor
|
|
368
|
+
: background.defaultColor,
|
|
369
|
+
borderRadius: background.cornerRadius,
|
|
370
|
+
paddingTop: background.paddingTop ?? container.paddingTop,
|
|
371
|
+
paddingRight: background.paddingRight ?? container.paddingRight,
|
|
372
|
+
paddingBottom: background.paddingBottom ?? container.paddingBottom,
|
|
373
|
+
paddingLeft: background.paddingLeft ?? container.paddingLeft,
|
|
374
|
+
}}
|
|
375
|
+
>
|
|
376
|
+
{children}
|
|
377
|
+
</View>
|
|
378
|
+
)}
|
|
368
379
|
</Pressable>
|
|
369
380
|
);
|
|
370
381
|
}
|
|
@@ -927,6 +938,10 @@ export const VARIANT_NOW_PLAYING: ItemConfiguration = {
|
|
|
927
938
|
defaultColor: "transparent",
|
|
928
939
|
focusedColor: "transparent",
|
|
929
940
|
cornerRadius: 0,
|
|
941
|
+
paddingTop: 8,
|
|
942
|
+
paddingRight: 20,
|
|
943
|
+
paddingBottom: 8,
|
|
944
|
+
paddingLeft: 20,
|
|
930
945
|
},
|
|
931
946
|
image: {
|
|
932
947
|
width: 72,
|
|
@@ -947,6 +962,12 @@ export const VARIANT_QUEUED_ITEM: ItemConfiguration = {
|
|
|
947
962
|
defaultColor: "transparent",
|
|
948
963
|
focusedColor: "rgba(62, 62, 62, 1)",
|
|
949
964
|
cornerRadius: 12,
|
|
965
|
+
paddingTop: 8,
|
|
966
|
+
paddingRight: 10,
|
|
967
|
+
paddingBottom: 8,
|
|
968
|
+
paddingLeft: 10,
|
|
969
|
+
highlightInsetLeft: 10,
|
|
970
|
+
highlightInsetRight: 10,
|
|
950
971
|
},
|
|
951
972
|
image: {
|
|
952
973
|
width: 48,
|
|
@@ -967,6 +988,10 @@ export const VARIANT_PLAYLIST_ITEM: ItemConfiguration = {
|
|
|
967
988
|
defaultColor: "transparent",
|
|
968
989
|
focusedColor: "rgba(62, 62, 62, 1)",
|
|
969
990
|
cornerRadius: 12,
|
|
991
|
+
paddingTop: 8,
|
|
992
|
+
paddingRight: 10,
|
|
993
|
+
paddingBottom: 8,
|
|
994
|
+
paddingLeft: 10,
|
|
970
995
|
},
|
|
971
996
|
image: {
|
|
972
997
|
width: 48,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { StyleSheet, View } from "react-native";
|
|
2
|
+
import { StyleSheet, Text, View } from "react-native";
|
|
3
3
|
import {
|
|
4
4
|
AudioPlayerTrackItem,
|
|
5
5
|
VARIANT_NOW_PLAYING,
|
|
@@ -10,6 +10,8 @@ import type { MenuItem } from "@applicaster/zapp-react-native-utils/modalState/t
|
|
|
10
10
|
import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
|
|
11
11
|
import { usePlayerState } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerState";
|
|
12
12
|
|
|
13
|
+
const DEFAULT_EMPTY_MESSAGE = "Queued items will be added here";
|
|
14
|
+
|
|
13
15
|
const styles = StyleSheet.create({
|
|
14
16
|
divider: {
|
|
15
17
|
height: StyleSheet.hairlineWidth,
|
|
@@ -17,18 +19,33 @@ const styles = StyleSheet.create({
|
|
|
17
19
|
marginHorizontal: 20,
|
|
18
20
|
marginVertical: 8,
|
|
19
21
|
},
|
|
22
|
+
emptyMessage: {
|
|
23
|
+
paddingTop: 32,
|
|
24
|
+
paddingBottom: 32,
|
|
25
|
+
paddingHorizontal: 40,
|
|
26
|
+
textAlign: "center",
|
|
27
|
+
color: "#999999",
|
|
28
|
+
fontSize: 13,
|
|
29
|
+
lineHeight: 16,
|
|
30
|
+
},
|
|
20
31
|
});
|
|
21
32
|
|
|
22
33
|
type NowPlayingHeaderSectionProps = {
|
|
23
34
|
onClear?: () => void;
|
|
24
35
|
nextUpTitle?: string;
|
|
25
36
|
nowPlayingTitle?: string;
|
|
37
|
+
isEmpty?: boolean;
|
|
38
|
+
emptyMessage?: string;
|
|
39
|
+
clearLabel?: string;
|
|
26
40
|
};
|
|
27
41
|
|
|
28
42
|
export function NowPlayingHeaderSection({
|
|
29
43
|
onClear,
|
|
30
44
|
nextUpTitle = "Next Up",
|
|
31
45
|
nowPlayingTitle = "Now Playing",
|
|
46
|
+
isEmpty = false,
|
|
47
|
+
emptyMessage = DEFAULT_EMPTY_MESSAGE,
|
|
48
|
+
clearLabel,
|
|
32
49
|
}: NowPlayingHeaderSectionProps) {
|
|
33
50
|
const player = usePlayer();
|
|
34
51
|
const playerState = usePlayerState("queue-now-playing-section");
|
|
@@ -70,14 +87,11 @@ export function NowPlayingHeaderSection({
|
|
|
70
87
|
};
|
|
71
88
|
|
|
72
89
|
return (
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
/>
|
|
79
|
-
<View style={styles.divider} />
|
|
80
|
-
</View>
|
|
90
|
+
<AudioPlayerTrackItem
|
|
91
|
+
configuration={VARIANT_NOW_PLAYING}
|
|
92
|
+
data={data}
|
|
93
|
+
onTrailingButtonPress={handlePlayPause}
|
|
94
|
+
/>
|
|
81
95
|
);
|
|
82
96
|
};
|
|
83
97
|
|
|
@@ -86,9 +100,10 @@ export function NowPlayingHeaderSection({
|
|
|
86
100
|
{renderNowPlayingTrack()}
|
|
87
101
|
<AudioPlayerHeader
|
|
88
102
|
configuration={VARIANT_SECTION_HEADER}
|
|
89
|
-
data={{ title: nextUpTitle }}
|
|
103
|
+
data={{ title: nextUpTitle, clearLabel }}
|
|
90
104
|
onClear={onClear}
|
|
91
105
|
/>
|
|
106
|
+
{isEmpty ? <Text style={styles.emptyMessage}>{emptyMessage}</Text> : null}
|
|
92
107
|
</View>
|
|
93
108
|
);
|
|
94
109
|
}
|
package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react-native";
|
|
3
|
+
import { NowPlayingHeaderSection } from "../NowPlayingHeaderSection";
|
|
4
|
+
|
|
5
|
+
jest.mock(
|
|
6
|
+
"@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer",
|
|
7
|
+
() => ({
|
|
8
|
+
usePlayer: () => null,
|
|
9
|
+
})
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
jest.mock(
|
|
13
|
+
"@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerState",
|
|
14
|
+
() => ({
|
|
15
|
+
usePlayerState: () => ({ isPaused: true }),
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const EMPTY_COPY = "Queued items will be added here";
|
|
20
|
+
|
|
21
|
+
describe("NowPlayingHeaderSection empty queue", () => {
|
|
22
|
+
it("renders the empty message under Next Up when the list is empty", () => {
|
|
23
|
+
render(<NowPlayingHeaderSection isEmpty emptyMessage={EMPTY_COPY} />);
|
|
24
|
+
|
|
25
|
+
expect(screen.getByText("Next Up")).toBeTruthy();
|
|
26
|
+
expect(screen.getByText(EMPTY_COPY)).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("does not render the empty message when the list has items", () => {
|
|
30
|
+
render(
|
|
31
|
+
<NowPlayingHeaderSection isEmpty={false} emptyMessage={EMPTY_COPY} />
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(screen.queryByText(EMPTY_COPY)).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -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: {
|
|
@@ -57,7 +62,7 @@ describe("mergeItemConfiguration background", () => {
|
|
|
57
62
|
expect(merged.textLabel1.lineHeight).toBe(40);
|
|
58
63
|
});
|
|
59
64
|
|
|
60
|
-
it("
|
|
65
|
+
it("does not merge Studio item background padding (white spec field)", () => {
|
|
61
66
|
const merged = mergeItemConfiguration(base as any, {
|
|
62
67
|
background: {
|
|
63
68
|
default: {
|
|
@@ -70,11 +75,11 @@ describe("mergeItemConfiguration background", () => {
|
|
|
70
75
|
},
|
|
71
76
|
});
|
|
72
77
|
|
|
73
|
-
expect(merged.background.
|
|
74
|
-
expect(merged.background.
|
|
75
|
-
expect(merged.background.
|
|
76
|
-
expect(merged.background.
|
|
77
|
-
|
|
78
|
+
expect(merged.background.defaultColor).toBe("#2E7D32");
|
|
79
|
+
expect(merged.background.paddingTop).toBeUndefined();
|
|
80
|
+
expect(merged.background.paddingBottom).toBeUndefined();
|
|
81
|
+
expect(merged.background.paddingLeft).toBeUndefined();
|
|
82
|
+
expect(merged.background.paddingRight).toBeUndefined();
|
|
78
83
|
expect(merged.background.focusedColor).toBe("rgba(62, 62, 62, 1)");
|
|
79
84
|
});
|
|
80
85
|
|
|
@@ -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 {
|
|
@@ -45,18 +46,6 @@ function mergeItemBackground(
|
|
|
45
46
|
...(defaultOverride?.borderRadius !== undefined
|
|
46
47
|
? { cornerRadius: defaultOverride.borderRadius }
|
|
47
48
|
: {}),
|
|
48
|
-
...(defaultOverride?.paddingTop !== undefined
|
|
49
|
-
? { paddingTop: defaultOverride.paddingTop }
|
|
50
|
-
: {}),
|
|
51
|
-
...(defaultOverride?.paddingRight !== undefined
|
|
52
|
-
? { paddingRight: defaultOverride.paddingRight }
|
|
53
|
-
: {}),
|
|
54
|
-
...(defaultOverride?.paddingBottom !== undefined
|
|
55
|
-
? { paddingBottom: defaultOverride.paddingBottom }
|
|
56
|
-
: {}),
|
|
57
|
-
...(defaultOverride?.paddingLeft !== undefined
|
|
58
|
-
? { paddingLeft: defaultOverride.paddingLeft }
|
|
59
|
-
: {}),
|
|
60
49
|
};
|
|
61
50
|
}
|
|
62
51
|
|
|
@@ -222,6 +211,122 @@ function mergeHeaderTitle(
|
|
|
222
211
|
...base,
|
|
223
212
|
...(override.fontColor ? { fontColor: override.fontColor } : {}),
|
|
224
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
|
+
: {}),
|
|
225
330
|
};
|
|
226
331
|
}
|
|
227
332
|
|
|
@@ -233,9 +338,39 @@ export function mergeHeaderConfiguration(
|
|
|
233
338
|
|
|
234
339
|
const bgOverride = headerBlock.container?.default?.backgroundColor;
|
|
235
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
|
+
|
|
236
366
|
return {
|
|
237
367
|
...base,
|
|
238
|
-
title: mergeHeaderTitle(base.title,
|
|
368
|
+
title: mergeHeaderTitle(base.title, titleOverride),
|
|
369
|
+
actionButtons: mergeHeaderActionButtons(
|
|
370
|
+
base.actionButtons,
|
|
371
|
+
actionButtonsDefault,
|
|
372
|
+
actionButtonsFocused
|
|
373
|
+
),
|
|
239
374
|
...(bgOverride ? { backgroundColor: bgOverride } : {}),
|
|
240
375
|
};
|
|
241
376
|
}
|
|
@@ -93,6 +93,8 @@ type ModalComponentProps = {
|
|
|
93
93
|
contentComponentProps?: Record<string, any>;
|
|
94
94
|
styleOverrides?: ModalBlocksConfig | Record<string, any>;
|
|
95
95
|
measurementPhase?: boolean;
|
|
96
|
+
/** Copy under Next Up when the queue has no upcoming items. */
|
|
97
|
+
emptyMessage?: string;
|
|
96
98
|
};
|
|
97
99
|
|
|
98
100
|
function _BottomSheetModalContent(props: ModalComponentProps) {
|
|
@@ -116,6 +118,7 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
116
118
|
contentComponentProps = {},
|
|
117
119
|
styleOverrides: propStyleOverrides,
|
|
118
120
|
measurementPhase = false,
|
|
121
|
+
emptyMessage,
|
|
119
122
|
} = props;
|
|
120
123
|
|
|
121
124
|
const route = useRef(currentRoute);
|
|
@@ -151,17 +154,81 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
151
154
|
[plugins, themePluginId]
|
|
152
155
|
);
|
|
153
156
|
|
|
154
|
-
const styleOverrides = useMemo(() => {
|
|
155
|
-
if (propStyleOverrides) {
|
|
156
|
-
return propStyleOverrides;
|
|
157
|
-
}
|
|
158
|
-
|
|
157
|
+
const styleOverrides = useMemo<ModalBlocksConfig | null>(() => {
|
|
159
158
|
const configuration =
|
|
160
159
|
plugin?.configuration || (plugin as any)?.configuration_json;
|
|
161
160
|
|
|
162
|
-
|
|
161
|
+
const parsed =
|
|
162
|
+
configuration && themePluginId
|
|
163
|
+
? parseModalBlocksConfiguration(configuration, themePluginId)
|
|
164
|
+
: null;
|
|
165
|
+
|
|
166
|
+
if (!propStyleOverrides) {
|
|
167
|
+
return parsed;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// hydrate(QueueStyles) only emits sectionHeader after the plugin class
|
|
171
|
+
// includes it. Until then, Studio/local keys in configuration_json still
|
|
172
|
+
// apply via the parser.
|
|
173
|
+
const hydratedSection = propStyleOverrides.sectionHeader;
|
|
174
|
+
|
|
175
|
+
const hasTitleStyle = (style?: Record<string, any>) =>
|
|
176
|
+
Boolean(
|
|
177
|
+
style &&
|
|
178
|
+
(style.fontColor ||
|
|
179
|
+
style.fontSize !== undefined ||
|
|
180
|
+
style.lineHeight !== undefined ||
|
|
181
|
+
style.fontFamily ||
|
|
182
|
+
style.iosFontFamily ||
|
|
183
|
+
style.androidFontFamily ||
|
|
184
|
+
style.letterSpacing !== undefined ||
|
|
185
|
+
style.iosLetterSpacing !== undefined ||
|
|
186
|
+
style.androidLetterSpacing !== undefined ||
|
|
187
|
+
style.textTransform ||
|
|
188
|
+
style.alignment)
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const hasActionStyle = (style?: Record<string, any>) =>
|
|
192
|
+
Boolean(
|
|
193
|
+
style &&
|
|
194
|
+
(style.backgroundColor ||
|
|
195
|
+
style.fontColor ||
|
|
196
|
+
style.fontSize !== undefined ||
|
|
197
|
+
style.lineHeight !== undefined ||
|
|
198
|
+
style.fontFamily ||
|
|
199
|
+
style.iosFontFamily ||
|
|
200
|
+
style.androidFontFamily ||
|
|
201
|
+
style.letterSpacing !== undefined ||
|
|
202
|
+
style.iosLetterSpacing !== undefined ||
|
|
203
|
+
style.androidLetterSpacing !== undefined ||
|
|
204
|
+
style.textTransform ||
|
|
205
|
+
style.borderRadius !== undefined ||
|
|
206
|
+
style.cornerRadius !== undefined ||
|
|
207
|
+
style.paddingTop !== undefined ||
|
|
208
|
+
style.paddingRight !== undefined ||
|
|
209
|
+
style.paddingBottom !== undefined ||
|
|
210
|
+
style.paddingLeft !== undefined)
|
|
211
|
+
);
|
|
163
212
|
|
|
164
|
-
|
|
213
|
+
const actionNode = hydratedSection?.actionButtons;
|
|
214
|
+
|
|
215
|
+
// An empty `default: {}` is truthy and would hide fields set on the flat
|
|
216
|
+
// object or on the focused bucket.
|
|
217
|
+
const hasHydratedSection =
|
|
218
|
+
hasTitleStyle(hydratedSection?.title?.default) ||
|
|
219
|
+
hasTitleStyle(hydratedSection?.title) ||
|
|
220
|
+
hasActionStyle(actionNode?.default) ||
|
|
221
|
+
hasActionStyle(actionNode?.focused) ||
|
|
222
|
+
hasActionStyle(actionNode?.focus) ||
|
|
223
|
+
hasActionStyle(actionNode);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
...parsed,
|
|
227
|
+
...propStyleOverrides,
|
|
228
|
+
sectionHeader: hasHydratedSection
|
|
229
|
+
? hydratedSection
|
|
230
|
+
: parsed?.sectionHeader,
|
|
231
|
+
};
|
|
165
232
|
}, [propStyleOverrides, plugin, themePluginId]);
|
|
166
233
|
|
|
167
234
|
const pluginLocalizations = useLocalizedStrings({
|
|
@@ -182,17 +249,26 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
182
249
|
const nextUpTitle =
|
|
183
250
|
pluginLocalizations?.next_up_title ||
|
|
184
251
|
(normalizedPluginId
|
|
185
|
-
? pluginLocalizations?.[`${normalizedPluginId}_next_up_title`]
|
|
252
|
+
? pluginLocalizations?.[`${normalizedPluginId}_next_up_title`] ||
|
|
253
|
+
pluginLocalizations?.[`${normalizedPluginId}_label_next_up`]
|
|
186
254
|
: undefined) ||
|
|
187
255
|
"Next Up";
|
|
188
256
|
|
|
189
257
|
const nowPlayingTitle =
|
|
190
258
|
pluginLocalizations?.now_playing_title ||
|
|
191
259
|
(normalizedPluginId
|
|
192
|
-
? pluginLocalizations?.[`${normalizedPluginId}_now_playing_title`]
|
|
260
|
+
? pluginLocalizations?.[`${normalizedPluginId}_now_playing_title`] ||
|
|
261
|
+
pluginLocalizations?.[`${normalizedPluginId}_label_now_playing`]
|
|
193
262
|
: undefined) ||
|
|
194
263
|
"Now Playing";
|
|
195
264
|
|
|
265
|
+
const clearLabel =
|
|
266
|
+
pluginLocalizations?.clear_label ||
|
|
267
|
+
(normalizedPluginId
|
|
268
|
+
? pluginLocalizations?.[`${normalizedPluginId}_label_clear`]
|
|
269
|
+
: undefined) ||
|
|
270
|
+
"Clear";
|
|
271
|
+
|
|
196
272
|
const resolvedTitle = props.title || localizedHeaderTitle || title;
|
|
197
273
|
|
|
198
274
|
const itemsToRender = viewModel ? fetchedItems : items;
|
|
@@ -316,6 +392,7 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
316
392
|
const hasNowPlaying = hasOperation(operations, OPERATIONS.NOW_PLAYING);
|
|
317
393
|
const hasClearAll = hasOperation(operations, OPERATIONS.CLEAR_ALL);
|
|
318
394
|
const hasAdd = hasOperation(operations, OPERATIONS.ADD);
|
|
395
|
+
const isQueueEmpty = hasNowPlaying && (itemsToRender?.length ?? 0) === 0;
|
|
319
396
|
|
|
320
397
|
const handleDeleteCollection = useCallback(() => {
|
|
321
398
|
editableCollection?.deleteCollection?.();
|
|
@@ -340,6 +417,9 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
340
417
|
onClear={hasClearAll ? handleDeleteCollection : undefined}
|
|
341
418
|
nextUpTitle={nextUpTitle}
|
|
342
419
|
nowPlayingTitle={nowPlayingTitle}
|
|
420
|
+
isEmpty={isQueueEmpty}
|
|
421
|
+
emptyMessage={emptyMessage}
|
|
422
|
+
clearLabel={clearLabel}
|
|
343
423
|
/>
|
|
344
424
|
),
|
|
345
425
|
});
|
|
@@ -369,6 +449,9 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
369
449
|
handleDeleteCollection,
|
|
370
450
|
nextUpTitle,
|
|
371
451
|
nowPlayingTitle,
|
|
452
|
+
isQueueEmpty,
|
|
453
|
+
emptyMessage,
|
|
454
|
+
clearLabel,
|
|
372
455
|
hasAdd,
|
|
373
456
|
handleCreatePlaylist,
|
|
374
457
|
createPlaylistTitle,
|
|
@@ -479,6 +562,9 @@ function _BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
479
562
|
onClear={hasClearAll ? handleDeleteCollection : undefined}
|
|
480
563
|
nextUpTitle={nextUpTitle}
|
|
481
564
|
nowPlayingTitle={nowPlayingTitle}
|
|
565
|
+
isEmpty={isQueueEmpty}
|
|
566
|
+
emptyMessage={emptyMessage}
|
|
567
|
+
clearLabel={clearLabel}
|
|
482
568
|
/>
|
|
483
569
|
) : null}
|
|
484
570
|
{hasAdd ? (
|
|
@@ -82,14 +82,23 @@ jest.mock("../AudioPlayer/Components/Item", () => ({
|
|
|
82
82
|
VARIANT_PLAYLIST_ITEM: {},
|
|
83
83
|
}));
|
|
84
84
|
|
|
85
|
+
const mockAudioPlayerButtonProps: Array<any> = [];
|
|
86
|
+
const mockNowPlayingHeaderProps: Array<any> = [];
|
|
87
|
+
|
|
85
88
|
jest.mock("../AudioPlayer/Components", () => ({
|
|
86
|
-
NowPlayingHeaderSection: () =>
|
|
87
|
-
|
|
89
|
+
NowPlayingHeaderSection: (props: any) => {
|
|
90
|
+
mockNowPlayingHeaderProps.push(props);
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
},
|
|
94
|
+
AudioPlayerButton: (props: any) => {
|
|
88
95
|
const { Text } = require("react-native");
|
|
89
96
|
|
|
97
|
+
mockAudioPlayerButtonProps.push(props);
|
|
98
|
+
|
|
90
99
|
return (
|
|
91
|
-
<Text testID="create-playlist-button" onPress={onPress}>
|
|
92
|
-
{data
|
|
100
|
+
<Text testID="create-playlist-button" onPress={props.onPress}>
|
|
101
|
+
{props.data?.title}
|
|
93
102
|
</Text>
|
|
94
103
|
);
|
|
95
104
|
},
|
|
@@ -201,6 +210,74 @@ describe("BottomSheetModalContent role/behavior cells", () => {
|
|
|
201
210
|
});
|
|
202
211
|
});
|
|
203
212
|
|
|
213
|
+
it("shows the empty Next Up message only when now playing is on and the list is empty", () => {
|
|
214
|
+
mockNowPlayingHeaderProps.length = 0;
|
|
215
|
+
const { BehaviorSubject } = require("rxjs");
|
|
216
|
+
|
|
217
|
+
const emptyQueue = new ContentViewModel({
|
|
218
|
+
title: "Queue",
|
|
219
|
+
items: [],
|
|
220
|
+
role: "dynamic_collection",
|
|
221
|
+
dynamicCollection: {
|
|
222
|
+
operations: ["nowPlaying", "remove", "reorder", "clearAll"],
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
emptyQueue.editableCollection = {
|
|
227
|
+
items$: new BehaviorSubject([]),
|
|
228
|
+
operations: ["nowPlaying", "remove", "reorder", "clearAll"],
|
|
229
|
+
move: jest.fn(),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const { rerender } = render(
|
|
233
|
+
<BottomSheetModalContent
|
|
234
|
+
{...baseProps}
|
|
235
|
+
items={[]}
|
|
236
|
+
emptyMessage="Queued items will be added here"
|
|
237
|
+
viewModel={emptyQueue}
|
|
238
|
+
/>
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
expect(mockNowPlayingHeaderProps.at(-1)).toMatchObject({
|
|
242
|
+
isEmpty: true,
|
|
243
|
+
emptyMessage: "Queued items will be added here",
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const withItem = new ContentViewModel({
|
|
247
|
+
title: "Queue",
|
|
248
|
+
items: [{ id: "t1", title: "Track 1" }],
|
|
249
|
+
role: "dynamic_collection",
|
|
250
|
+
dynamicCollection: {
|
|
251
|
+
operations: ["nowPlaying", "remove", "reorder", "clearAll"],
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
withItem.editableCollection = {
|
|
256
|
+
items$: new BehaviorSubject([{ id: "t1", title: "Track 1" }]),
|
|
257
|
+
operations: ["nowPlaying", "remove", "reorder", "clearAll"],
|
|
258
|
+
move: jest.fn(),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
rerender(
|
|
262
|
+
<BottomSheetModalContent
|
|
263
|
+
{...baseProps}
|
|
264
|
+
items={[{ id: "t1", title: "Track 1", label: "Track 1" }]}
|
|
265
|
+
emptyMessage="Queued items will be added here"
|
|
266
|
+
viewModel={withItem}
|
|
267
|
+
/>
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(mockNowPlayingHeaderProps.at(-1)).toMatchObject({
|
|
271
|
+
isEmpty: false,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
mockNowPlayingHeaderProps.length = 0;
|
|
275
|
+
|
|
276
|
+
render(<BottomSheetModalContent {...baseProps} />);
|
|
277
|
+
|
|
278
|
+
expect(mockNowPlayingHeaderProps).toHaveLength(0);
|
|
279
|
+
});
|
|
280
|
+
|
|
204
281
|
it("honors disableScrollViewWrap in contentComponentProps to bypass ScrollView wrapping", () => {
|
|
205
282
|
const CustomComponent = () => (
|
|
206
283
|
<React.Fragment>Custom Content</React.Fragment>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-ui-components",
|
|
3
|
-
"version": "15.1.
|
|
3
|
+
"version": "15.1.16-rc.0",
|
|
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": "15.1.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "15.1.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "15.1.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "15.1.
|
|
31
|
+
"@applicaster/applicaster-types": "15.1.16-rc.0",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "15.1.16-rc.0",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "15.1.16-rc.0",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "15.1.16-rc.0",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"react-native-sortables": "1.7.1",
|