@pushwoosh/dumb-components 1.1.233 → 1.1.234
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/NativeRichMediaPreview/NativeRichMediaPreview.d.ts +33 -0
- package/NativeRichMediaPreview/NativeRichMediaPreview.js +31 -16
- package/NativeRichMediaPreview/__tests__/NativeRichMediaPreview.test.d.ts +1 -0
- package/NativeRichMediaPreview/__tests__/NativeRichMediaPreview.test.js +81 -0
- package/NativeRichMediaPreview/index.d.ts +1 -1
- package/NativeRichMediaPreview/types.d.ts +7 -1
- package/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,36 @@
|
|
|
1
1
|
import { type ReactElement } from 'react';
|
|
2
2
|
import type { NativeRichMediaPreviewProps } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Preview of a native rich media (modal, banner, stories, spinwheel, …)
|
|
5
|
+
* rendered from its JSON config; lays out with plain CSS in the real pixels
|
|
6
|
+
* of the given width/height box, with no fidelity to any phone's pt-metrics
|
|
7
|
+
* since this is a preview and devices vary. Content flows inside a scrollable
|
|
8
|
+
* layer above a static backdrop (the dim overlay or, for
|
|
9
|
+
* `fullscreen`/`video`/`stories`, the full-bleed background image or poster):
|
|
10
|
+
* content taller than the box scrolls, shorter content aligns per display
|
|
11
|
+
* type while the backdrop stays put. Pass `scale` to lay the content out on a
|
|
12
|
+
* larger (or smaller) reference viewport and scale the whole preview to fill
|
|
13
|
+
* the box — the way to cram a phone-sized layout into a small preview box
|
|
14
|
+
* while keeping its proportions. Shows an invalid-config placeholder when the
|
|
15
|
+
* config cannot be parsed or misses required contract fields.
|
|
16
|
+
* With `withThemeToggle` the preview carries a chip that flips it between the light
|
|
17
|
+
* values and the dark overlay merged over them; pass `theme` with `onThemeChange`
|
|
18
|
+
* to drive that from outside, when a surrounding editor has its own theme switch.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <NativeRichMediaPreview
|
|
22
|
+
* width={304}
|
|
23
|
+
* height={659}
|
|
24
|
+
* content={{
|
|
25
|
+
* displayType: 'modal',
|
|
26
|
+
* modal: {
|
|
27
|
+
* showClose: true,
|
|
28
|
+
* dimBackground: true,
|
|
29
|
+
* background: '#FFFFFFFF',
|
|
30
|
+
* title: { text: 'Hi', color: '#4B5057FF' },
|
|
31
|
+
* buttons: [],
|
|
32
|
+
* },
|
|
33
|
+
* }}
|
|
34
|
+
* />
|
|
35
|
+
*/
|
|
3
36
|
export declare function NativeRichMediaPreview(props: NativeRichMediaPreviewProps): ReactElement;
|
|
@@ -152,6 +152,17 @@ function renderConfig(config, activeIndex) {
|
|
|
152
152
|
return _jsx(NativeRichMediaInvalidView, {});
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
+
function parseDarkVariant(content, withThemeToggle) {
|
|
156
|
+
if (!withThemeToggle) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const merged = mergeNativeDarkOverlay(content);
|
|
160
|
+
// The merge hands back `content` itself, untouched, whenever there is nothing to apply.
|
|
161
|
+
if (merged === content) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return parseNativeRichMediaConfig(merged);
|
|
165
|
+
}
|
|
155
166
|
/**
|
|
156
167
|
* Preview of a native rich media (modal, banner, stories, spinwheel, …)
|
|
157
168
|
* rendered from its JSON config; lays out with plain CSS in the real pixels
|
|
@@ -165,6 +176,9 @@ function renderConfig(config, activeIndex) {
|
|
|
165
176
|
* the box — the way to cram a phone-sized layout into a small preview box
|
|
166
177
|
* while keeping its proportions. Shows an invalid-config placeholder when the
|
|
167
178
|
* config cannot be parsed or misses required contract fields.
|
|
179
|
+
* With `withThemeToggle` the preview carries a chip that flips it between the light
|
|
180
|
+
* values and the dark overlay merged over them; pass `theme` with `onThemeChange`
|
|
181
|
+
* to drive that from outside, when a surrounding editor has its own theme switch.
|
|
168
182
|
*
|
|
169
183
|
* @example
|
|
170
184
|
* <NativeRichMediaPreview
|
|
@@ -182,17 +196,6 @@ function renderConfig(config, activeIndex) {
|
|
|
182
196
|
* }}
|
|
183
197
|
* />
|
|
184
198
|
*/
|
|
185
|
-
function parseDarkVariant(content, withThemeToggle) {
|
|
186
|
-
if (!withThemeToggle) {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
const merged = mergeNativeDarkOverlay(content);
|
|
190
|
-
// The merge hands back `content` itself, untouched, whenever there is nothing to apply.
|
|
191
|
-
if (merged === content) {
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
return parseNativeRichMediaConfig(merged);
|
|
195
|
-
}
|
|
196
199
|
export function NativeRichMediaPreview(props) {
|
|
197
200
|
const {
|
|
198
201
|
content,
|
|
@@ -204,9 +207,20 @@ export function NativeRichMediaPreview(props) {
|
|
|
204
207
|
activeIndex = 0,
|
|
205
208
|
isLoading = false,
|
|
206
209
|
withBackground = true,
|
|
207
|
-
withThemeToggle = false
|
|
210
|
+
withThemeToggle = false,
|
|
211
|
+
theme: controlledTheme,
|
|
212
|
+
onThemeChange
|
|
208
213
|
} = props;
|
|
209
|
-
const [
|
|
214
|
+
const [ownTheme, setOwnTheme] = useState('light');
|
|
215
|
+
const isControlled = controlledTheme !== undefined;
|
|
216
|
+
const theme = controlledTheme ?? ownTheme;
|
|
217
|
+
const toggleTheme = () => {
|
|
218
|
+
const next = theme === 'dark' ? 'light' : 'dark';
|
|
219
|
+
if (!isControlled) {
|
|
220
|
+
setOwnTheme(next);
|
|
221
|
+
}
|
|
222
|
+
onThemeChange?.(next);
|
|
223
|
+
};
|
|
210
224
|
const {
|
|
211
225
|
config,
|
|
212
226
|
isDarkRendered,
|
|
@@ -222,6 +236,7 @@ export function NativeRichMediaPreview(props) {
|
|
|
222
236
|
canToggleDark: darkParsed !== null
|
|
223
237
|
};
|
|
224
238
|
}, [content, localization, language, theme, withThemeToggle]);
|
|
239
|
+
const renderedTheme = isDarkRendered ? 'dark' : 'light';
|
|
225
240
|
const shouldDim = !isLoading && dimsScreen(config);
|
|
226
241
|
return _jsxs(PreviewHost, {
|
|
227
242
|
...LIGHT_ISLAND_PROPS,
|
|
@@ -239,9 +254,9 @@ export function NativeRichMediaPreview(props) {
|
|
|
239
254
|
})
|
|
240
255
|
}, config.displayType), !isLoading && config && renderChrome(config)]
|
|
241
256
|
}), !isLoading && config && withThemeToggle && _jsx(NativeRichMediaThemeChip, {
|
|
242
|
-
theme:
|
|
243
|
-
disabled: !canToggleDark,
|
|
244
|
-
onToggle:
|
|
257
|
+
theme: isControlled ? theme : renderedTheme,
|
|
258
|
+
disabled: !isControlled && !canToggleDark,
|
|
259
|
+
onToggle: toggleTheme
|
|
245
260
|
}), isLoading && _jsx(SpinnerLayer, {
|
|
246
261
|
children: _jsx(Spinner, {
|
|
247
262
|
size: "medium",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { NativeRichMediaPreview } from '../NativeRichMediaPreview';
|
|
4
|
+
const lightContent = {
|
|
5
|
+
displayType: 'modal',
|
|
6
|
+
modal: {
|
|
7
|
+
showClose: true,
|
|
8
|
+
dimBackground: true,
|
|
9
|
+
background: '#FFFFFFFF',
|
|
10
|
+
title: {
|
|
11
|
+
text: 'Hi',
|
|
12
|
+
color: '#4B5057FF'
|
|
13
|
+
},
|
|
14
|
+
buttons: []
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const darkableContent = {
|
|
18
|
+
...lightContent,
|
|
19
|
+
modal: {
|
|
20
|
+
...lightContent.modal,
|
|
21
|
+
dark: {
|
|
22
|
+
background: '#14161AFF',
|
|
23
|
+
title: {
|
|
24
|
+
color: '#F2F4F7FF'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const themeChip = () => screen.getByLabelText('Switch light and dark');
|
|
30
|
+
const renderPreview = (props = {}) => render(_jsx(NativeRichMediaPreview, {
|
|
31
|
+
content: darkableContent,
|
|
32
|
+
width: 304,
|
|
33
|
+
height: 659,
|
|
34
|
+
withThemeToggle: true,
|
|
35
|
+
...props
|
|
36
|
+
}));
|
|
37
|
+
describe('NativeRichMediaPreview theme toggle', () => {
|
|
38
|
+
it('keeps its own theme when uncontrolled', () => {
|
|
39
|
+
renderPreview();
|
|
40
|
+
expect(themeChip()).toHaveAttribute('aria-pressed', 'false');
|
|
41
|
+
fireEvent.click(themeChip());
|
|
42
|
+
expect(themeChip()).toHaveAttribute('aria-pressed', 'true');
|
|
43
|
+
});
|
|
44
|
+
it('follows the theme prop instead of a click when controlled', () => {
|
|
45
|
+
const onThemeChange = jest.fn();
|
|
46
|
+
const {
|
|
47
|
+
rerender
|
|
48
|
+
} = renderPreview({
|
|
49
|
+
theme: 'dark',
|
|
50
|
+
onThemeChange
|
|
51
|
+
});
|
|
52
|
+
expect(themeChip()).toHaveAttribute('aria-pressed', 'true');
|
|
53
|
+
fireEvent.click(themeChip());
|
|
54
|
+
expect(onThemeChange).toHaveBeenCalledWith('light');
|
|
55
|
+
expect(themeChip()).toHaveAttribute('aria-pressed', 'true');
|
|
56
|
+
rerender(_jsx(NativeRichMediaPreview, {
|
|
57
|
+
content: darkableContent,
|
|
58
|
+
width: 304,
|
|
59
|
+
height: 659,
|
|
60
|
+
withThemeToggle: true,
|
|
61
|
+
theme: "light",
|
|
62
|
+
onThemeChange: onThemeChange
|
|
63
|
+
}));
|
|
64
|
+
expect(themeChip()).toHaveAttribute('aria-pressed', 'false');
|
|
65
|
+
});
|
|
66
|
+
it('reports the controlled theme even when the content has no dark overlay', () => {
|
|
67
|
+
renderPreview({
|
|
68
|
+
content: lightContent,
|
|
69
|
+
theme: 'dark',
|
|
70
|
+
onThemeChange: jest.fn()
|
|
71
|
+
});
|
|
72
|
+
expect(themeChip()).toHaveAttribute('aria-pressed', 'true');
|
|
73
|
+
expect(themeChip()).not.toBeDisabled();
|
|
74
|
+
});
|
|
75
|
+
it('disables the chip on content without a dark overlay when uncontrolled', () => {
|
|
76
|
+
renderPreview({
|
|
77
|
+
content: lightContent
|
|
78
|
+
});
|
|
79
|
+
expect(themeChip()).toBeDisabled();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { NativeRichMediaConfig, NativeRichMediaDisplayType, NativeRichMediaDarkOverlay, NativeRichMediaText, NativeRichMediaBorder, NativeRichMediaCover, NativeRichMediaAction, NativeRichMediaButton, NativeRichMediaReward, NativeRichMediaModalBlock, NativeRichMediaSheetBlock, NativeRichMediaCarouselBlock, NativeRichMediaStoriesBlock, NativeRichMediaBannerBlock, NativeRichMediaFullscreenBlock, NativeRichMediaVideoBlock, NativeRichMediaPipBlock, NativeRichMediaScratchcardBlock, NativeRichMediaSpinwheelBlock, NativeRichMediaPreviewProps, NativeRichMediaLocalization, } from './types';
|
|
1
|
+
export type { NativeRichMediaConfig, NativeRichMediaDisplayType, NativeRichMediaDarkOverlay, NativeRichMediaText, NativeRichMediaBorder, NativeRichMediaCover, NativeRichMediaAction, NativeRichMediaButton, NativeRichMediaReward, NativeRichMediaModalBlock, NativeRichMediaSheetBlock, NativeRichMediaCarouselBlock, NativeRichMediaStoriesBlock, NativeRichMediaBannerBlock, NativeRichMediaFullscreenBlock, NativeRichMediaVideoBlock, NativeRichMediaPipBlock, NativeRichMediaScratchcardBlock, NativeRichMediaSpinwheelBlock, NativeRichMediaPreviewProps, NativeRichMediaLocalization, NativeRichMediaTheme, } from './types';
|
|
2
2
|
export { NATIVE_RICH_MEDIA_DISPLAY_TYPES, EDITABLE_NATIVE_RICH_MEDIA_DISPLAY_TYPES } from './types';
|
|
3
3
|
export { parseNativeRichMediaConfig } from './parseNativeRichMediaConfig';
|
|
4
4
|
export { parseLocalizationParams } from './parseLocalizationParams';
|
|
@@ -319,6 +319,8 @@ export declare const EDITABLE_NATIVE_RICH_MEDIA_DISPLAY_TYPES: NativeRichMediaDi
|
|
|
319
319
|
* fallback. Text lives here rather than inline in `content`.
|
|
320
320
|
*/
|
|
321
321
|
export type NativeRichMediaLocalization = Record<string, Record<string, string>>;
|
|
322
|
+
/** Colour scheme the preview renders: the light values, or them merged with the dark overlay. */
|
|
323
|
+
export type NativeRichMediaTheme = 'light' | 'dark';
|
|
322
324
|
/** Props of {@link NativeRichMediaPreview}. */
|
|
323
325
|
export type NativeRichMediaPreviewProps = {
|
|
324
326
|
/** Rich media config, as an object or a JSON string. */
|
|
@@ -339,6 +341,10 @@ export type NativeRichMediaPreviewProps = {
|
|
|
339
341
|
isLoading?: boolean;
|
|
340
342
|
/** Render the container chrome — white backdrop and border (default `true`). Set `false` for a transparent preview over the consumer's own background. */
|
|
341
343
|
withBackground?: boolean;
|
|
342
|
-
/** Show a chip that switches the preview between the light and dark variants (default `false`).
|
|
344
|
+
/** Show a chip that switches the preview between the light and dark variants (default `false`). Uncontrolled, the preview opens light. */
|
|
343
345
|
withThemeToggle?: boolean;
|
|
346
|
+
/** Theme to render, making the chip controlled: the preview follows this value, the chip mirrors it (rather than the rendered result, since content without a dark overlay renders identically in both themes) and only reports clicks through `onThemeChange`. Omit to let the preview keep its own state. */
|
|
347
|
+
theme?: NativeRichMediaTheme;
|
|
348
|
+
/** Called with the other theme when the chip is clicked; the only way to react to the chip on a controlled preview. */
|
|
349
|
+
onThemeChange?: (theme: NativeRichMediaTheme) => void;
|
|
344
350
|
};
|
package/index.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export { Chip, type ChipProps, type ChipVariant } from './Chip';
|
|
|
50
50
|
export { RichmediaPreview, useRichmediaHtml } from './RichmediaPreview';
|
|
51
51
|
export { SandboxedHtmlFrame, type SandboxedHtmlFrameProps, type IsolatedSandboxedHtmlFrameProps, type InspectableSandboxedHtmlFrameProps, } from './SandboxedHtmlFrame';
|
|
52
52
|
export { NativeRichMediaPreview, parseNativeRichMediaConfig, parseLocalizationParams, NATIVE_RICH_MEDIA_DISPLAY_TYPES, EDITABLE_NATIVE_RICH_MEDIA_DISPLAY_TYPES, } from './NativeRichMediaPreview';
|
|
53
|
-
export type { NativeRichMediaConfig, NativeRichMediaDisplayType, NativeRichMediaText, NativeRichMediaBorder, NativeRichMediaCover, NativeRichMediaAction, NativeRichMediaButton, NativeRichMediaReward, NativeRichMediaDarkOverlay, NativeRichMediaModalBlock, NativeRichMediaSheetBlock, NativeRichMediaCarouselBlock, NativeRichMediaStoriesBlock, NativeRichMediaBannerBlock, NativeRichMediaFullscreenBlock, NativeRichMediaVideoBlock, NativeRichMediaPipBlock, NativeRichMediaScratchcardBlock, NativeRichMediaSpinwheelBlock, NativeRichMediaPreviewProps, NativeRichMediaLocalization, } from './NativeRichMediaPreview';
|
|
53
|
+
export type { NativeRichMediaConfig, NativeRichMediaDisplayType, NativeRichMediaText, NativeRichMediaBorder, NativeRichMediaCover, NativeRichMediaAction, NativeRichMediaButton, NativeRichMediaReward, NativeRichMediaDarkOverlay, NativeRichMediaModalBlock, NativeRichMediaSheetBlock, NativeRichMediaCarouselBlock, NativeRichMediaStoriesBlock, NativeRichMediaBannerBlock, NativeRichMediaFullscreenBlock, NativeRichMediaVideoBlock, NativeRichMediaPipBlock, NativeRichMediaScratchcardBlock, NativeRichMediaSpinwheelBlock, NativeRichMediaPreviewProps, NativeRichMediaLocalization, NativeRichMediaTheme, } from './NativeRichMediaPreview';
|
|
54
54
|
export { RichMessageEditor, RichMessageViewer, type DynamicContent, } from './RichMessageEditor';
|
|
55
55
|
export { FrequencyCappingEdit, FrequencyCappingReadonly, FrequencyCappingInApps, CappingCountingMode, CountingMode, type CountingModeProps, type LocalCapping, type CappingInAppBase, } from './FrequencyCapping';
|
|
56
56
|
export { SendRateEdit, SendRateReadonly, type SendRateBase, type SendRateEditProps, type SendRateReadonlyProps, } from './SendRate';
|