@applicaster/zapp-react-native-ui-components 16.0.0-alpha.1593888345 → 16.0.0-alpha.1701179921
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/LinkHandler/LinkHandler.tsx +2 -7
- package/Components/MasterCell/DefaultComponents/ActionButton.tsx +4 -1
- package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
- package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
- package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +2 -4
- package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +2 -3
- package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
- package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +1 -2
- package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +4 -2
- package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
- package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +5 -2
- package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +4 -1
- package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +58 -73
- package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +65 -56
- package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +140 -65
- package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +93 -46
- package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +148 -32
- package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +113 -0
- package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
- package/Components/ModalComponent/AudioPlayer/Components/index.ts +0 -2
- package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +132 -6
- package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +202 -13
- package/Components/ModalComponent/BottomSheetModalContent.tsx +187 -73
- package/Components/ModalComponent/SortableList.tsx +36 -22
- package/Components/ModalComponent/TextInputContent.tsx +42 -7
- package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +111 -6
- package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +50 -1
- package/Components/ModalComponent/index.tsx +7 -9
- package/Components/ModalComponent/presets/__tests__/getCell.test.ts +24 -0
- package/Components/ModalComponent/presets/selectableCell.tsx +1 -1
- package/Components/ModalComponent/showTextInput.ts +11 -2
- package/Components/PlayerContainer/PlayerContainer.tsx +8 -1
- package/Components/River/ComponentsMap/ComponentsMap.tsx +10 -12
- package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +138 -75
- package/Components/River/__tests__/componentsMap.test.js +8 -3
- package/Components/Screen/__tests__/Screen.test.tsx +24 -0
- package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -0
- package/Components/Screen/index.tsx +5 -0
- package/Components/ScreenRevealManager/withScreenRevealManager.tsx +1 -1
- package/Components/Transitioner/Transitioner.tsx +2 -3
- package/Components/Transitioner/__tests__/transitioner.test.js +83 -2
- package/Components/VideoLive/LiveImageManager.ts +8 -2
- package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
- package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
- package/Contexts/ModalNavigationContext/index.tsx +2 -2
- package/assets/empty-Info.png +0 -0
- package/package.json +5 -5
- package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -314
|
@@ -11,9 +11,14 @@ jest.mock(
|
|
|
11
11
|
() => () => null
|
|
12
12
|
);
|
|
13
13
|
|
|
14
|
-
jest.mock("react-native-safe-area-context", () =>
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
jest.mock("react-native-safe-area-context", () => {
|
|
15
|
+
const { View } = jest.requireActual("react-native");
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
SafeAreaView: View,
|
|
19
|
+
useSafeAreaInsets: () => ({ top: 50 }),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
17
22
|
|
|
18
23
|
jest.mock("react-native", () => {
|
|
19
24
|
const RN = jest.genMockFromModule("react-native");
|
|
@@ -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
|
<>
|
|
@@ -85,7 +85,7 @@ export const withScreenRevealManager = (Component) => {
|
|
|
85
85
|
|
|
86
86
|
if (isFirstComponentScreenPicker(componentsToRender)) {
|
|
87
87
|
// for screen-picker with have additional internal ComponentsMap, no need to add this wrapper
|
|
88
|
-
return <Component {...props} />;
|
|
88
|
+
return <Component {...props} initialNumberToLoad={1} />;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
return (
|
|
@@ -129,8 +129,7 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
|
|
|
129
129
|
* The last scene in the array is removed in the "backTransitionEnd" callback,
|
|
130
130
|
* only after the animation is complete.
|
|
131
131
|
*/
|
|
132
|
-
backTransitionStart() {
|
|
133
|
-
const previousRoute = this.props.navigator?.currentRoute;
|
|
132
|
+
backTransitionStart(previousRoute: string) {
|
|
134
133
|
const currentRoute = this.props.navigator?.currentRoute;
|
|
135
134
|
|
|
136
135
|
const diffLengthRoutes =
|
|
@@ -242,7 +241,7 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
|
|
|
242
241
|
break;
|
|
243
242
|
|
|
244
243
|
case NAV_ACTION_BACK: {
|
|
245
|
-
this.backTransitionStart();
|
|
244
|
+
this.backTransitionStart(previousRoute);
|
|
246
245
|
break;
|
|
247
246
|
}
|
|
248
247
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Text, View } from "react-native";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { AnimationManager } from "../AnimationManager";
|
|
5
|
+
import {
|
|
6
|
+
NAV_ACTION_BACK,
|
|
7
|
+
NAV_ACTION_PUSH,
|
|
8
|
+
TransitionerComponent,
|
|
9
|
+
} from "../Transitioner";
|
|
10
|
+
import { act, render } from "@testing-library/react-native";
|
|
6
11
|
|
|
7
12
|
jest.mock("../Scene", () => {
|
|
8
13
|
const View = jest.requireActual("react-native").View;
|
|
@@ -40,6 +45,27 @@ const transitionerComponentProps = {
|
|
|
40
45
|
|
|
41
46
|
jest.mock("uuid");
|
|
42
47
|
|
|
48
|
+
const sceneTransitionConfig = () => ({
|
|
49
|
+
push: {
|
|
50
|
+
duration: 0,
|
|
51
|
+
easing: jest.fn(),
|
|
52
|
+
from: { style: {} },
|
|
53
|
+
to: { style: {} },
|
|
54
|
+
},
|
|
55
|
+
back: {
|
|
56
|
+
duration: 0,
|
|
57
|
+
easing: jest.fn(),
|
|
58
|
+
from: { style: {} },
|
|
59
|
+
to: { style: {} },
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const sceneFor = (route) => (
|
|
64
|
+
<View pathname={route}>
|
|
65
|
+
<Text>{route}</Text>
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
|
|
43
69
|
describe("<TransitionerComponent />", () => {
|
|
44
70
|
it("renders correctly", () => {
|
|
45
71
|
const wrapper = render(
|
|
@@ -50,4 +76,59 @@ describe("<TransitionerComponent />", () => {
|
|
|
50
76
|
|
|
51
77
|
expect(wrapper.toJSON()).toMatchSnapshot();
|
|
52
78
|
});
|
|
79
|
+
|
|
80
|
+
it("removes both intermediate scenes when backing out to the tab root", () => {
|
|
81
|
+
jest
|
|
82
|
+
.spyOn(AnimationManager.prototype, "animate")
|
|
83
|
+
.mockImplementation((_, done) => done());
|
|
84
|
+
|
|
85
|
+
const ref = React.createRef();
|
|
86
|
+
const root = "/river/root";
|
|
87
|
+
const child = `${root}/river/child`;
|
|
88
|
+
const grandchild = `${child}/river/grandchild`;
|
|
89
|
+
|
|
90
|
+
const propsFor = (route, previousAction) => ({
|
|
91
|
+
navigator: {
|
|
92
|
+
previousAction,
|
|
93
|
+
currentRoute: route,
|
|
94
|
+
screenData: {},
|
|
95
|
+
},
|
|
96
|
+
transitionConfig: sceneTransitionConfig,
|
|
97
|
+
layoutData: {
|
|
98
|
+
isTablet: false,
|
|
99
|
+
isTabletPortrait: false,
|
|
100
|
+
},
|
|
101
|
+
children: sceneFor(route),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const { rerender } = render(
|
|
105
|
+
<TransitionerComponent ref={ref} {...propsFor(root, "REPLACE")} />
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
act(() => {
|
|
109
|
+
rerender(
|
|
110
|
+
<TransitionerComponent
|
|
111
|
+
ref={ref}
|
|
112
|
+
{...propsFor(child, NAV_ACTION_PUSH)}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
act(() => {
|
|
118
|
+
rerender(
|
|
119
|
+
<TransitionerComponent
|
|
120
|
+
ref={ref}
|
|
121
|
+
{...propsFor(grandchild, NAV_ACTION_PUSH)}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
act(() => {
|
|
127
|
+
rerender(
|
|
128
|
+
<TransitionerComponent ref={ref} {...propsFor(root, NAV_ACTION_BACK)} />
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(ref.current.state.scenes.map((scene) => scene.key)).toEqual([root]);
|
|
133
|
+
});
|
|
53
134
|
});
|
|
@@ -213,10 +213,16 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
213
213
|
log_debug(
|
|
214
214
|
`pauseItem: live-image not ready, ${item.playerId} - ${item.tag}`
|
|
215
215
|
);
|
|
216
|
-
} else {
|
|
217
|
-
item.player.pause();
|
|
218
216
|
}
|
|
219
217
|
|
|
218
|
+
// Pause even when the player is not ready yet: the underlying player may
|
|
219
|
+
// still be loading and would otherwise start playing on its own once it
|
|
220
|
+
// has buffered, leaving the live-image audible underneath the primary
|
|
221
|
+
// player. Note this is best effort - the web controller's pause() only
|
|
222
|
+
// logs while the player component is unmounted - so the player itself also
|
|
223
|
+
// refuses buffer-driven starts when it was never asked to play.
|
|
224
|
+
item.player.pause();
|
|
225
|
+
|
|
220
226
|
// Fake close event, because we unmount native view
|
|
221
227
|
item.player.onPlayerClose();
|
|
222
228
|
item.setMode?.(LiveImageType.Image);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const { LiveImageManager } = require("../LiveImageManager");
|
|
2
2
|
|
|
3
3
|
describe("LiveImageManager", () => {
|
|
4
|
+
const flushPromises = () => new Promise(setImmediate);
|
|
5
|
+
|
|
4
6
|
it("should subscribe to LiveImageManager events", () => {
|
|
5
7
|
const sampleListener = {
|
|
6
8
|
id: "1",
|
|
@@ -78,4 +80,61 @@ describe("LiveImageManager", () => {
|
|
|
78
80
|
expect(mockOnEnd1).toHaveBeenCalledWith(eventBody);
|
|
79
81
|
expect(mockOnEnd2).toHaveBeenCalledWith(eventBody);
|
|
80
82
|
});
|
|
83
|
+
|
|
84
|
+
it("should start a prepared live image when the mounted player loads", async () => {
|
|
85
|
+
const manager = new LiveImageManager();
|
|
86
|
+
|
|
87
|
+
const item = {
|
|
88
|
+
playerId: "live-image",
|
|
89
|
+
tag: "Live image",
|
|
90
|
+
player: {
|
|
91
|
+
playerId: "live-image",
|
|
92
|
+
playerState: { isReadyToPlay: false },
|
|
93
|
+
getEntry: jest.fn(),
|
|
94
|
+
play: jest.fn(),
|
|
95
|
+
},
|
|
96
|
+
getPlayer: jest.fn(),
|
|
97
|
+
prepareForPlayback: jest.fn().mockResolvedValue(true),
|
|
98
|
+
setMode: jest.fn(),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
manager.playLiveImage(item);
|
|
102
|
+
await flushPromises();
|
|
103
|
+
|
|
104
|
+
expect(item.setMode).toHaveBeenCalledWith("video");
|
|
105
|
+
expect(item.player.play).not.toHaveBeenCalled();
|
|
106
|
+
|
|
107
|
+
manager.onLiveImageVideoLoaded(item);
|
|
108
|
+
|
|
109
|
+
expect(item.player.play).toHaveBeenCalledTimes(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should not start a live image that was paused before the player loaded", async () => {
|
|
113
|
+
const manager = new LiveImageManager();
|
|
114
|
+
|
|
115
|
+
const item = {
|
|
116
|
+
playerId: "live-image",
|
|
117
|
+
tag: "Live image",
|
|
118
|
+
player: {
|
|
119
|
+
playerId: "live-image",
|
|
120
|
+
playerState: { isReadyToPlay: false },
|
|
121
|
+
getEntry: jest.fn(),
|
|
122
|
+
play: jest.fn(),
|
|
123
|
+
pause: jest.fn(),
|
|
124
|
+
onPlayerClose: jest.fn(),
|
|
125
|
+
},
|
|
126
|
+
getPlayer: jest.fn(),
|
|
127
|
+
prepareForPlayback: jest.fn().mockResolvedValue(true),
|
|
128
|
+
setMode: jest.fn(),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
manager.playLiveImage(item);
|
|
132
|
+
await flushPromises();
|
|
133
|
+
manager.pauseLiveImage(item);
|
|
134
|
+
manager.onLiveImageVideoLoaded(item);
|
|
135
|
+
|
|
136
|
+
expect(item.player.pause).toHaveBeenCalledTimes(1);
|
|
137
|
+
expect(item.player.play).not.toHaveBeenCalled();
|
|
138
|
+
expect(item.setMode).toHaveBeenLastCalledWith("image");
|
|
139
|
+
});
|
|
81
140
|
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react-native";
|
|
3
|
+
import { withModalNavigationContextProvider } from "../";
|
|
4
|
+
|
|
5
|
+
describe("withModalNavigationContextProvider", () => {
|
|
6
|
+
it("does not re-render the wrapped component when props are unchanged", () => {
|
|
7
|
+
const Component = jest.fn(() => null);
|
|
8
|
+
const WrappedComponent = withModalNavigationContextProvider(Component);
|
|
9
|
+
const { rerender } = render(<WrappedComponent title="Modal" />);
|
|
10
|
+
|
|
11
|
+
rerender(<WrappedComponent title="Modal" />);
|
|
12
|
+
|
|
13
|
+
expect(Component).toHaveBeenCalledTimes(1);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -6,11 +6,11 @@ export function useModalNavigationContext() {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function withModalNavigationContextProvider(Component) {
|
|
9
|
-
return function WrappedWithModalNavigationContextProvider(props) {
|
|
9
|
+
return React.memo(function WrappedWithModalNavigationContextProvider(props) {
|
|
10
10
|
return (
|
|
11
11
|
<ModalNavigationContext.Provider value={true}>
|
|
12
12
|
<Component {...props} />
|
|
13
13
|
</ModalNavigationContext.Provider>
|
|
14
14
|
);
|
|
15
|
-
};
|
|
15
|
+
});
|
|
16
16
|
}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-ui-components",
|
|
3
|
-
"version": "16.0.0-alpha.
|
|
3
|
+
"version": "16.0.0-alpha.1701179921",
|
|
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-alpha.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "16.0.0-alpha.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "16.0.0-alpha.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.
|
|
31
|
+
"@applicaster/applicaster-types": "16.0.0-alpha.1701179921",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "16.0.0-alpha.1701179921",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "16.0.0-alpha.1701179921",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.1701179921",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"react-native-sortables": "1.7.1",
|
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Modal Building Block — Action
|
|
3
|
-
*
|
|
4
|
-
* Single-file Expo Snack demo for the Action list item building block spec.
|
|
5
|
-
* Three forms (text only / leading icon + text / leading + text + trailing)
|
|
6
|
-
* are driven by data — no separate variants.
|
|
7
|
-
*
|
|
8
|
-
* Green (configurable): background, title, trailingIcon.chevronAsset
|
|
9
|
-
* White (hardcoded): container padding/gutter, icon size, leading icon assets,
|
|
10
|
-
* leading/trailing visibility logic from data
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import React from "react";
|
|
14
|
-
import {
|
|
15
|
-
Image,
|
|
16
|
-
Platform,
|
|
17
|
-
Pressable,
|
|
18
|
-
StyleSheet,
|
|
19
|
-
Text,
|
|
20
|
-
View,
|
|
21
|
-
} from "react-native";
|
|
22
|
-
|
|
23
|
-
const styles = StyleSheet.create({
|
|
24
|
-
row: {
|
|
25
|
-
flexDirection: "row",
|
|
26
|
-
alignItems: "center",
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Configuration (green / editable fields from spec)
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
export type TextTransform =
|
|
35
|
-
| "default"
|
|
36
|
-
| "lowercase"
|
|
37
|
-
| "uppercase"
|
|
38
|
-
| "capitalize";
|
|
39
|
-
|
|
40
|
-
export type BackgroundConfiguration = {
|
|
41
|
-
defaultColor: string;
|
|
42
|
-
focusedColor: string;
|
|
43
|
-
cornerRadius: number;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export type TitleConfiguration = {
|
|
47
|
-
fontColor: string;
|
|
48
|
-
focusedFontColor: string;
|
|
49
|
-
iosFontFamily: string;
|
|
50
|
-
androidFontFamily: string;
|
|
51
|
-
fontSize: number;
|
|
52
|
-
lineHeight: number;
|
|
53
|
-
iosLetterSpacing: number;
|
|
54
|
-
androidLetterSpacing: number;
|
|
55
|
-
textTransform: TextTransform;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export type TrailingIconConfiguration = {
|
|
59
|
-
chevronAsset?: string;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export type ActionConfiguration = {
|
|
63
|
-
background: BackgroundConfiguration;
|
|
64
|
-
title: TitleConfiguration;
|
|
65
|
-
trailingIcon: TrailingIconConfiguration;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export const DEFAULT_BACKGROUND_CONFIGURATION: BackgroundConfiguration = {
|
|
69
|
-
defaultColor: "transparent",
|
|
70
|
-
focusedColor: "rgba(62, 62, 62, 1)",
|
|
71
|
-
cornerRadius: 12,
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export const DEFAULT_TITLE_CONFIGURATION: TitleConfiguration = {
|
|
75
|
-
fontColor: "#FFFFFF",
|
|
76
|
-
focusedFontColor: "#FFFFFF",
|
|
77
|
-
iosFontFamily: "SFProText-Semibold",
|
|
78
|
-
androidFontFamily: "Roboto-Medium",
|
|
79
|
-
fontSize: 15,
|
|
80
|
-
lineHeight: 24,
|
|
81
|
-
iosLetterSpacing: -0.2,
|
|
82
|
-
androidLetterSpacing: 0,
|
|
83
|
-
textTransform: "default",
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export const DEFAULT_TRAILING_ICON_CONFIGURATION: TrailingIconConfiguration =
|
|
87
|
-
{};
|
|
88
|
-
|
|
89
|
-
export const DEFAULT_ACTION_CONFIGURATION: ActionConfiguration = {
|
|
90
|
-
background: DEFAULT_BACKGROUND_CONFIGURATION,
|
|
91
|
-
title: DEFAULT_TITLE_CONFIGURATION,
|
|
92
|
-
trailingIcon: DEFAULT_TRAILING_ICON_CONFIGURATION,
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
// Data (runtime — controls which elements are shown)
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
|
|
99
|
-
export type LeadingActionType =
|
|
100
|
-
| "addToQueue"
|
|
101
|
-
| "addToPlaylist"
|
|
102
|
-
| "favorite"
|
|
103
|
-
| "share"
|
|
104
|
-
| "sleepTimer"
|
|
105
|
-
| "playbackSpeed";
|
|
106
|
-
|
|
107
|
-
export type ActionData = {
|
|
108
|
-
title: string;
|
|
109
|
-
/** When set, leading icon is shown (Enable: On). */
|
|
110
|
-
leadingActionType?: LeadingActionType;
|
|
111
|
-
/** When true, trailing chevron is shown (Enable: On). */
|
|
112
|
-
showTrailingChevron?: boolean;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
export type AudioPlayerActionProps = {
|
|
116
|
-
configuration: ActionConfiguration;
|
|
117
|
-
data: ActionData;
|
|
118
|
-
focused?: boolean;
|
|
119
|
-
onPress?: () => void;
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
// Hardcoded spec values (white fields)
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
|
|
126
|
-
const CONTAINER_SPEC = {
|
|
127
|
-
paddingTop: 13,
|
|
128
|
-
paddingRight: 10,
|
|
129
|
-
paddingBottom: 13,
|
|
130
|
-
paddingLeft: 10,
|
|
131
|
-
horizontalGutter: 12,
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const ICON_SPEC = {
|
|
135
|
-
width: 24,
|
|
136
|
-
height: 24,
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const DEFAULT_CHEVRON_ASSET =
|
|
140
|
-
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAALElEQVR42mNgGAXUBlu2bPmPjIeWBeiGDy0Lhrbrh1/qGXqZaxSMglEw0gEA4c+IpUrpYX4AAAAASUVORK5CYII=";
|
|
141
|
-
|
|
142
|
-
/** Hardcoded: action type → leading icon asset (base64 PNG). */
|
|
143
|
-
const LEADING_ICON_ASSETS: Record<LeadingActionType, string> = {
|
|
144
|
-
addToQueue:
|
|
145
|
-
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAALElEQVR42mNgGAUjE/xHAzS3gFgweC0Y+nEwGslkBRFFwTj8LBjNyaNgmAMAJTf+EEEZcS0AAAAASUVORK5CYII=",
|
|
146
|
-
addToPlaylist:
|
|
147
|
-
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAKUlEQVR42mNgGAVDBvxHA6MWjFpAvEGkglELKLdgNJmOWkA7C0YBzQAAivu+UKRR1ZIAAAAASUVORK5CYII=",
|
|
148
|
-
favorite:
|
|
149
|
-
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAKklEQVR42mNgGAWjYBTgBP+xAGLkRi0YQhbgM4wqBo9aMHgsGAWjgCAAALyAZqh4yyZZAAAAAElFTkSuQmCC",
|
|
150
|
-
share:
|
|
151
|
-
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAK0lEQVR42mNgGAVDDvxHAqMWjFowUiz4TwQYgRYQYxBFlo1aMPAWjAK6AgD2g7pUUT7VeAAAAABJRU5ErkJggg==",
|
|
152
|
-
sleepTimer:
|
|
153
|
-
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAYUlEQVR42tWUSQ4AIAgD+f+n8eJRliIQ6MmEZTSxJZokvipdXgZpAbzOO17AglIXI7XULxmCoEMwRGuWam6AdRsN4IJYTRH4PIDkhxTA92zUPNBcFDDHaOVR0RJ2LXG9QgemFVK8tpo4PwAAAABJRU5ErkJggg==",
|
|
154
|
-
playbackSpeed:
|
|
155
|
-
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAK0lEQVR42mNgGAVDDvxHAqMWjFowUiz4TwQYgRYQYxBFlo1aMPAWjAK6AgD2g7pUUT7VeAAAAABJRU5ErkJggg==",
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
function getTextTransformStyle(
|
|
159
|
-
textTransform: TextTransform
|
|
160
|
-
): "none" | "lowercase" | "uppercase" | "capitalize" {
|
|
161
|
-
return textTransform === "default" ? "none" : textTransform;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function shouldShowLeadingIcon(leadingActionType?: LeadingActionType) {
|
|
165
|
-
return !!leadingActionType;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function shouldShowTrailingIcon(showTrailingChevron?: boolean) {
|
|
169
|
-
return !!showTrailingChevron;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
// Inner components
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
|
|
176
|
-
type ActionContainerProps = {
|
|
177
|
-
configuration: BackgroundConfiguration;
|
|
178
|
-
focused: boolean;
|
|
179
|
-
children: React.ReactNode;
|
|
180
|
-
onPress?: () => void;
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
function ActionContainer({
|
|
184
|
-
configuration,
|
|
185
|
-
focused,
|
|
186
|
-
children,
|
|
187
|
-
onPress,
|
|
188
|
-
}: ActionContainerProps) {
|
|
189
|
-
return (
|
|
190
|
-
<Pressable
|
|
191
|
-
onPress={onPress}
|
|
192
|
-
style={({ pressed }) => ({
|
|
193
|
-
backgroundColor:
|
|
194
|
-
pressed || focused
|
|
195
|
-
? configuration.focusedColor
|
|
196
|
-
: configuration.defaultColor,
|
|
197
|
-
borderRadius: configuration.cornerRadius,
|
|
198
|
-
paddingTop: CONTAINER_SPEC.paddingTop,
|
|
199
|
-
paddingRight: CONTAINER_SPEC.paddingRight,
|
|
200
|
-
paddingBottom: CONTAINER_SPEC.paddingBottom,
|
|
201
|
-
paddingLeft: CONTAINER_SPEC.paddingLeft,
|
|
202
|
-
})}
|
|
203
|
-
>
|
|
204
|
-
{children}
|
|
205
|
-
</Pressable>
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
type ActionTitleProps = {
|
|
210
|
-
title: string;
|
|
211
|
-
configuration: TitleConfiguration;
|
|
212
|
-
focused: boolean;
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
function ActionTitle({ title, configuration, focused }: ActionTitleProps) {
|
|
216
|
-
const letterSpacing =
|
|
217
|
-
Platform.OS === "ios"
|
|
218
|
-
? configuration.iosLetterSpacing
|
|
219
|
-
: configuration.androidLetterSpacing;
|
|
220
|
-
|
|
221
|
-
return (
|
|
222
|
-
<Text
|
|
223
|
-
style={{
|
|
224
|
-
flex: 1,
|
|
225
|
-
color: focused
|
|
226
|
-
? configuration.focusedFontColor
|
|
227
|
-
: configuration.fontColor,
|
|
228
|
-
fontSize: configuration.fontSize,
|
|
229
|
-
lineHeight: configuration.lineHeight,
|
|
230
|
-
letterSpacing,
|
|
231
|
-
fontFamily: Platform.select({
|
|
232
|
-
ios: configuration.iosFontFamily,
|
|
233
|
-
android: configuration.androidFontFamily,
|
|
234
|
-
default: configuration.androidFontFamily,
|
|
235
|
-
}),
|
|
236
|
-
textTransform: getTextTransformStyle(configuration.textTransform),
|
|
237
|
-
}}
|
|
238
|
-
numberOfLines={1}
|
|
239
|
-
>
|
|
240
|
-
{title}
|
|
241
|
-
</Text>
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
type ActionLeadingIconProps = {
|
|
246
|
-
actionType: LeadingActionType;
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
function ActionLeadingIcon({ actionType }: ActionLeadingIconProps) {
|
|
250
|
-
return (
|
|
251
|
-
<Image
|
|
252
|
-
source={{ uri: LEADING_ICON_ASSETS[actionType] }}
|
|
253
|
-
style={{ width: ICON_SPEC.width, height: ICON_SPEC.height }}
|
|
254
|
-
resizeMode="contain"
|
|
255
|
-
/>
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
type ActionTrailingIconProps = {
|
|
260
|
-
configuration: TrailingIconConfiguration;
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
function ActionTrailingIcon({ configuration }: ActionTrailingIconProps) {
|
|
264
|
-
const asset = configuration.chevronAsset ?? DEFAULT_CHEVRON_ASSET;
|
|
265
|
-
|
|
266
|
-
return (
|
|
267
|
-
<Image
|
|
268
|
-
source={{ uri: asset }}
|
|
269
|
-
style={{ width: ICON_SPEC.width, height: ICON_SPEC.height }}
|
|
270
|
-
resizeMode="contain"
|
|
271
|
-
/>
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// ---------------------------------------------------------------------------
|
|
276
|
-
// AudioPlayerAction
|
|
277
|
-
// ---------------------------------------------------------------------------
|
|
278
|
-
|
|
279
|
-
export function AudioPlayerAction({
|
|
280
|
-
configuration,
|
|
281
|
-
data,
|
|
282
|
-
focused = false,
|
|
283
|
-
onPress,
|
|
284
|
-
}: AudioPlayerActionProps) {
|
|
285
|
-
const { background, title, trailingIcon } = configuration;
|
|
286
|
-
const { title: label, leadingActionType, showTrailingChevron } = data;
|
|
287
|
-
|
|
288
|
-
const showLeading = shouldShowLeadingIcon(leadingActionType);
|
|
289
|
-
const showTrailing = shouldShowTrailingIcon(showTrailingChevron);
|
|
290
|
-
|
|
291
|
-
return (
|
|
292
|
-
<ActionContainer
|
|
293
|
-
configuration={background}
|
|
294
|
-
focused={focused}
|
|
295
|
-
onPress={onPress}
|
|
296
|
-
>
|
|
297
|
-
<View style={styles.row}>
|
|
298
|
-
{showLeading ? (
|
|
299
|
-
<>
|
|
300
|
-
<ActionLeadingIcon actionType={leadingActionType} />
|
|
301
|
-
<View style={{ width: CONTAINER_SPEC.horizontalGutter }} />
|
|
302
|
-
</>
|
|
303
|
-
) : null}
|
|
304
|
-
<ActionTitle title={label} configuration={title} focused={focused} />
|
|
305
|
-
{showTrailing ? (
|
|
306
|
-
<>
|
|
307
|
-
<View style={{ width: CONTAINER_SPEC.horizontalGutter }} />
|
|
308
|
-
<ActionTrailingIcon configuration={trailingIcon} />
|
|
309
|
-
</>
|
|
310
|
-
) : null}
|
|
311
|
-
</View>
|
|
312
|
-
</ActionContainer>
|
|
313
|
-
);
|
|
314
|
-
}
|