@brightcove/components-embed-code-modal 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/CHANGELOG.md +21 -1
  3. package/__tests__/PrismLiveModal.test.tsx +85 -0
  4. package/__tests__/PrismPlayerControls.test.tsx +139 -0
  5. package/__tests__/VideoPlayer.test.tsx +68 -1
  6. package/build/__tests__/PrismLiveModal.test.d.ts +2 -0
  7. package/build/__tests__/PrismLiveModal.test.d.ts.map +1 -0
  8. package/build/__tests__/PrismPlayerControls.test.d.ts +2 -0
  9. package/build/__tests__/PrismPlayerControls.test.d.ts.map +1 -0
  10. package/build/index.js +2 -2
  11. package/build/index.js.map +1 -1
  12. package/build/package.json +2 -2
  13. package/build/src/js/components/prism/PrismLiveModal.d.ts.map +1 -1
  14. package/build/src/js/components/prism/PrismPlayerControls.d.ts +2 -1
  15. package/build/src/js/components/prism/PrismPlayerControls.d.ts.map +1 -1
  16. package/build/src/js/components/prism/PrismPlayerPreview.d.ts +2 -1
  17. package/build/src/js/components/prism/PrismPlayerPreview.d.ts.map +1 -1
  18. package/build/src/js/hooks/useIsMobile.d.ts +3 -0
  19. package/build/src/js/hooks/useIsMobile.d.ts.map +1 -0
  20. package/build/src/js/i18n.d.ts +24 -0
  21. package/build/src/js/i18n.d.ts.map +1 -1
  22. package/package.json +2 -2
  23. package/src/js/components/VideoPlayer.tsx +2 -2
  24. package/src/js/components/prism/PrismLiveModal.tsx +40 -6
  25. package/src/js/components/prism/PrismPlayerControls.tsx +56 -43
  26. package/src/js/components/prism/PrismPlayerPreview.tsx +19 -7
  27. package/src/js/hooks/useIsMobile.ts +33 -0
  28. package/src/styles/prism/PrismLiveModal.sass +35 -62
  29. package/src/translations/en.json +4 -0
  30. package/src/translations/es.json +4 -0
  31. package/src/translations/fr.json +4 -0
  32. package/src/translations/ja.json +4 -0
  33. package/src/translations/ko.json +4 -0
  34. package/src/translations/zh.json +4 -0
@@ -20,4 +20,4 @@ jobs:
20
20
  test:ci
21
21
  dry-run: false
22
22
  secrets:
23
- npm-token: ${{ secrets.NPM_PUBLISH_TOKEN }}
23
+ npm-token: ${{ secrets.NPM_TOKEN }}
package/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.3] - 2026-09-25
11
+ ### Added
12
+ - Mobile layout for the live embed modal: bottom sheet, Copy Code / Done footer, compact URL / Preview row and a hidden code card
13
+
14
+ ### Changed
15
+ - Use prism-core's native modal props for sizing, scrolling and the header border, with the columns scrolling independently
16
+ - Switch between the stacked and two-column layouts at a single 1024px breakpoint
17
+ - Fill the width/height sizing inputs and use Prism's default desktop size for the Copy URL / Preview buttons
18
+
19
+ ### Fixed
20
+ - Copy the recommended embed on mobile (JavaScript for Campaign, Viewer ID and MAP tracking players)
21
+
22
+ ## [1.1.2] - 2026-08-13
23
+ ### Fixed
24
+ - Allow DVR and Low Latency to be selected simultaneously in the live embed modal
25
+
10
26
  ## [1.1.1] - 2026-07-28
11
27
  ### Fixed
12
28
  - Point the root `types` at `./build/src/index.d.ts`, so consumers get declarations again. `publish-dir` moved from `build` to `.` in 1.0.8, which made the root `package.json` the published manifest; `main` was already root-relative but `types` still assumed `build/` was the package root, so 1.0.8 and 1.1.0 shipped a `types` path that does not exist and consumers hit `TS7016`
@@ -338,7 +354,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
338
354
  ### Added
339
355
  - Initial commit of embed code modal component
340
356
 
341
- [Unreleased]: https://bithub.brightcove.com/studio-ui-kit/studio-components-embed-code-modal/compare/v1.0.6...HEAD
357
+ [Unreleased]: https://bithub.brightcove.com/studio-ui-kit/studio-components-embed-code-modal/compare/v1.1.2...HEAD
358
+ [1.1.2]: https://bithub.brightcove.com/studio-ui-kit/studio-components-embed-code-modal/compare/v1.1.1...v1.1.2
359
+ [1.1.1]: https://bithub.brightcove.com/studio-ui-kit/studio-components-embed-code-modal/compare/v1.1.0...v1.1.1
360
+ [1.1.0]: https://bithub.brightcove.com/studio-ui-kit/studio-components-embed-code-modal/compare/v1.0.7...v1.1.0
361
+ [1.0.7]: https://bithub.brightcove.com/studio-ui-kit/studio-components-embed-code-modal/compare/v1.0.6...v1.0.7
342
362
  [1.0.6]: https://bithub.brightcove.com/studio-ui-kit/studio-components-embed-code-modal/compare/v1.0.5...v1.0.6
343
363
  [1.0.5]: https://bithub.brightcove.com/studio-ui-kit/studio-components-embed-code-modal/compare/v1.0.4...v1.0.5
344
364
  [1.0.4]: https://bithub.brightcove.com/studio-ui-kit/studio-components-embed-code-modal/compare/v1.0.3...v1.0.4
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { PrismLiveModal } from '../src/js/components/prism/PrismLiveModal';
4
+ import { LivePayload } from '../src/js/types/EmbedCodeModalProps';
5
+
6
+ jest.mock('@brightcove/prism-core', () => ({
7
+ PrismModal: ({
8
+ children,
9
+ ...props
10
+ }: {
11
+ children: React.ReactNode;
12
+ } & Record<string, unknown>) => (
13
+ <div data-testid='prism-modal' data-props={JSON.stringify(props)}>
14
+ {children}
15
+ </div>
16
+ ),
17
+ }));
18
+
19
+ jest.mock('../src/js/components/prism/PrismPlayerControls', () => ({
20
+ PrismPlayerControls: () => <div />,
21
+ }));
22
+
23
+ jest.mock('../src/js/components/prism/PrismPlayerPreview', () => ({
24
+ PrismPlayerPreview: () => <div />,
25
+ }));
26
+
27
+ jest.mock('js/utils/util', () => ({
28
+ getVideoPreviewLink: jest.fn(() => ''),
29
+ }));
30
+
31
+ const VIDEO: LivePayload = {
32
+ videoId: '123',
33
+ accountId: '456',
34
+ livePlaybackToken: 'token',
35
+ adConfigId: '',
36
+ };
37
+
38
+ const renderModal = () =>
39
+ render(
40
+ <PrismLiveModal
41
+ isOpen
42
+ onClose={jest.fn()}
43
+ video={VIDEO}
44
+ players={{}}
45
+ playerListForSelection={[]}
46
+ />,
47
+ );
48
+
49
+ const setViewportWidth = (width: number) =>
50
+ Object.defineProperty(window, 'innerWidth', {
51
+ configurable: true,
52
+ value: width,
53
+ });
54
+
55
+ describe('PrismLiveModal', () => {
56
+ afterEach(() => setViewportWidth(1024));
57
+
58
+ const renderProps = () =>
59
+ JSON.parse(
60
+ screen.getByTestId('prism-modal').getAttribute('data-props') as string,
61
+ );
62
+
63
+ test('configures the modal through native Prism props', () => {
64
+ renderModal();
65
+
66
+ expect(renderProps()).toMatchObject({
67
+ size: 'extraLarge',
68
+ forceMaxHeight: true,
69
+ contentScroll: 'none',
70
+ showHeaderBottomBorder: true,
71
+ });
72
+ });
73
+
74
+ test('docks as a bottom sheet and copies the code from the mobile footer', () => {
75
+ setViewportWidth(800);
76
+ renderModal();
77
+
78
+ expect(renderProps()).toMatchObject({
79
+ bottomSheet: true,
80
+ contentScroll: 'auto',
81
+ primaryButtonText: 'copyCode',
82
+ cancelButtonText: 'done',
83
+ });
84
+ });
85
+ });
@@ -0,0 +1,139 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import { PrismPlayerControls } from '../src/js/components/prism/PrismPlayerControls';
4
+ import {
5
+ EmbedCodeAspectRatio,
6
+ EmbedCodeUnits,
7
+ } from '../src/js/types/EmbedCodeModalProps';
8
+ import { PLAYER_LIST_FOR_SELECTION, PLAYERS } from '../src/js/constants/player';
9
+
10
+ jest.mock('@brightcove/prism-core', () => ({
11
+ PrismCheckbox: ({
12
+ label,
13
+ checked,
14
+ onChange,
15
+ disabled,
16
+ }: {
17
+ label: string;
18
+ checked: boolean;
19
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
20
+ disabled?: boolean;
21
+ }) => (
22
+ <input
23
+ type='checkbox'
24
+ aria-label={label}
25
+ checked={checked}
26
+ onChange={onChange}
27
+ disabled={disabled}
28
+ />
29
+ ),
30
+ PrismDropDown: () => <div />,
31
+ PrismInput: () => <div />,
32
+ PrismIconButton: () => <button />,
33
+ PrismCodeCard: () => <div />,
34
+ }));
35
+
36
+ const DEFAULT_VIDEO = {
37
+ videoId: '789',
38
+ accountId: '456',
39
+ livePlaybackToken: 'token123',
40
+ adConfigId: '',
41
+ };
42
+
43
+ const DEFAULT_EMBED_PROPERTIES = {
44
+ width: 960,
45
+ height: 540,
46
+ units: EmbedCodeUnits.PX,
47
+ responsive: false,
48
+ aspectRatio: EmbedCodeAspectRatio.CUSTOM,
49
+ };
50
+
51
+ const renderControls = (extraProps = {}) =>
52
+ render(
53
+ <PrismPlayerControls
54
+ video={DEFAULT_VIDEO}
55
+ players={PLAYERS}
56
+ playerListForSelection={PLAYER_LIST_FOR_SELECTION}
57
+ selectedPlayer='default'
58
+ onPlayerChange={jest.fn()}
59
+ embedProperties={DEFAULT_EMBED_PROPERTIES}
60
+ setEmbedProperties={jest.fn()}
61
+ showDVR
62
+ showLowLatency
63
+ dvrTime={300}
64
+ {...extraProps}
65
+ />,
66
+ );
67
+
68
+ describe('embed code reporting', () => {
69
+ test('reports the iframe embed for a plain player', () => {
70
+ const onEmbedCodeChange = jest.fn();
71
+ renderControls({ onEmbedCodeChange });
72
+
73
+ const code = onEmbedCodeChange.mock.calls.at(-1)?.[0] as string;
74
+ expect(code).toContain('<iframe');
75
+ });
76
+
77
+ test('reports the JavaScript embed for a player that requires it', () => {
78
+ const onEmbedCodeChange = jest.fn();
79
+ renderControls({
80
+ players: {
81
+ default: { ...PLAYERS.default, hasCampaignPlugin: true },
82
+ },
83
+ onEmbedCodeChange,
84
+ });
85
+
86
+ const code = onEmbedCodeChange.mock.calls.at(-1)?.[0] as string;
87
+ expect(code).toContain('<video-js');
88
+ });
89
+ });
90
+
91
+ describe('DVR and Low Latency checkboxes are independent', () => {
92
+ test('both checkboxes are enabled when asset supports DVR and Low Latency', () => {
93
+ renderControls();
94
+ expect(
95
+ screen.getByRole('checkbox', { name: 'dvr.allowDVR' }),
96
+ ).not.toBeDisabled();
97
+ expect(
98
+ screen.getByRole('checkbox', { name: 'lowLatency' }),
99
+ ).not.toBeDisabled();
100
+ });
101
+
102
+ test('DVR checkbox starts unchecked when Low Latency is also present', () => {
103
+ renderControls();
104
+ expect(
105
+ screen.getByRole('checkbox', { name: 'dvr.allowDVR' }),
106
+ ).not.toBeChecked();
107
+ expect(screen.getByRole('checkbox', { name: 'lowLatency' })).toBeChecked();
108
+ });
109
+
110
+ test('Low Latency checkbox stays enabled when DVR is checked', () => {
111
+ renderControls();
112
+ fireEvent.click(screen.getByRole('checkbox', { name: 'dvr.allowDVR' }));
113
+ expect(
114
+ screen.getByRole('checkbox', { name: 'lowLatency' }),
115
+ ).not.toBeDisabled();
116
+ });
117
+
118
+ test('DVR checkbox stays enabled when Low Latency is checked', () => {
119
+ renderControls();
120
+ fireEvent.click(screen.getByRole('checkbox', { name: 'lowLatency' }));
121
+ expect(
122
+ screen.getByRole('checkbox', { name: 'dvr.allowDVR' }),
123
+ ).not.toBeDisabled();
124
+ });
125
+
126
+ test('each callback fires independently without affecting the other', () => {
127
+ const onDVRClick = jest.fn();
128
+ const onLowLatencyClick = jest.fn();
129
+ renderControls({ onDVRClick, onLowLatencyClick });
130
+
131
+ fireEvent.click(screen.getByRole('checkbox', { name: 'dvr.allowDVR' }));
132
+ expect(onDVRClick).toHaveBeenCalledTimes(1);
133
+ expect(onLowLatencyClick).not.toHaveBeenCalled();
134
+
135
+ fireEvent.click(screen.getByRole('checkbox', { name: 'lowLatency' }));
136
+ expect(onLowLatencyClick).toHaveBeenCalledTimes(1);
137
+ expect(onDVRClick).toHaveBeenCalledTimes(1);
138
+ });
139
+ });
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { QueryClientProvider, QueryClient } from 'react-query';
3
- import { render, screen } from '@testing-library/react';
3
+ import { render, screen, fireEvent } from '@testing-library/react';
4
4
  import { VideoPlayer } from '../src/js/components/VideoPlayer';
5
5
  import { EmbedCodeModalTypes } from '../src/js/types/EmbedCodeModalProps';
6
6
  import { PLAYER_LIST_FOR_SELECTION, PLAYERS } from '../src/js/constants/player';
@@ -20,6 +20,13 @@ const DEFAULT_MODAL_COMP = {
20
20
  duration: 10,
21
21
  };
22
22
 
23
+ const LIVE_MODAL_COMP = {
24
+ videoId: '789',
25
+ accountId: '456',
26
+ livePlaybackToken: 'token123',
27
+ adConfigId: '',
28
+ };
29
+
23
30
  test('renders EmbedCodeModal component with right information', () => {
24
31
  render(
25
32
  wrappedComponent(
@@ -36,3 +43,63 @@ test('renders EmbedCodeModal component with right information', () => {
36
43
  expect(screen.getByRole('tab', { name: /javascript/i })).toBeInTheDocument();
37
44
  expect(screen.getByRole('tab', { name: /IFrame/i })).toBeInTheDocument();
38
45
  });
46
+
47
+ describe('DVR and Low Latency checkboxes are independent', () => {
48
+ const renderLivePlayer = (extraProps = {}) =>
49
+ render(
50
+ wrappedComponent(
51
+ <VideoPlayer
52
+ video={LIVE_MODAL_COMP}
53
+ embedCodeModalType={EmbedCodeModalTypes.LIVE}
54
+ playerListForSelection={PLAYER_LIST_FOR_SELECTION}
55
+ players={PLAYERS}
56
+ showDVR
57
+ showLowLatency
58
+ {...extraProps}
59
+ />,
60
+ ),
61
+ );
62
+
63
+ const getDVRCheckbox = () =>
64
+ screen.getByRole('checkbox', { name: /dvr\.allowDVR/i });
65
+ const getLowLatencyCheckbox = () =>
66
+ screen.getByRole('checkbox', { name: /lowLatency/i });
67
+
68
+ test('both checkboxes are enabled when asset supports DVR and Low Latency', () => {
69
+ renderLivePlayer();
70
+ expect(getDVRCheckbox()).not.toBeDisabled();
71
+ expect(getLowLatencyCheckbox()).not.toBeDisabled();
72
+ });
73
+
74
+ test('DVR checkbox starts unchecked when Low Latency is also present', () => {
75
+ renderLivePlayer();
76
+ expect(getDVRCheckbox()).not.toBeChecked();
77
+ expect(getLowLatencyCheckbox()).toBeChecked();
78
+ });
79
+
80
+ test('Low Latency checkbox stays enabled when DVR is checked', () => {
81
+ renderLivePlayer();
82
+ fireEvent.click(getDVRCheckbox());
83
+ expect(getLowLatencyCheckbox()).not.toBeDisabled();
84
+ });
85
+
86
+ test('DVR checkbox stays enabled when Low Latency is checked', () => {
87
+ renderLivePlayer();
88
+ fireEvent.click(getLowLatencyCheckbox());
89
+ expect(getDVRCheckbox()).not.toBeDisabled();
90
+ });
91
+
92
+ test('each callback fires independently without affecting the other', () => {
93
+ const onDVRClick = jest.fn();
94
+ const onLowLatencyClick = jest.fn();
95
+ renderLivePlayer({ onDVRClick, onLowLatencyClick });
96
+
97
+ fireEvent.click(getDVRCheckbox());
98
+ expect(onDVRClick).toHaveBeenCalledTimes(1);
99
+ expect(onLowLatencyClick).not.toHaveBeenCalled();
100
+
101
+ fireEvent.click(getLowLatencyCheckbox());
102
+ expect(onLowLatencyClick).toHaveBeenCalledTimes(1);
103
+ expect(onDVRClick).toHaveBeenCalledTimes(1);
104
+ });
105
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=PrismLiveModal.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PrismLiveModal.test.d.ts","sourceRoot":"","sources":["../../__tests__/PrismLiveModal.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=PrismPlayerControls.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PrismPlayerControls.test.d.ts","sourceRoot":"","sources":["../../__tests__/PrismPlayerControls.test.tsx"],"names":[],"mappings":""}