@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.
@@ -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 [theme, setTheme] = useState('light');
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: isDarkRendered ? 'dark' : 'light',
243
- disabled: !canToggleDark,
244
- onToggle: () => setTheme(theme === 'dark' ? 'light' : 'dark')
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,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`). The preview always opens light. */
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/dumb-components",
3
- "version": "1.1.233",
3
+ "version": "1.1.234",
4
4
  "description": "React components to build Pushwoosh products",
5
5
  "main": "index.js",
6
6
  "module": "index.js",