@playkit-js/transcript 3.6.0-canary.0-a940bfb → 3.6.0-canary.0-baa8e71

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playkit-js/transcript",
3
- "version": "3.6.0-canary.0-a940bfb",
3
+ "version": "3.6.0-canary.0-baa8e71",
4
4
  "main": "dist/playkit-transcript.js",
5
5
  "license": "AGPL-3.0",
6
6
  "private": false,
@@ -14,14 +14,14 @@
14
14
  },
15
15
  "devDependencies": {
16
16
  "@playkit-js/kaltura-player-js": "3.17.9",
17
- "@playkit-js/playkit-js-ui": "0.78.0",
18
- "@playkit-js/ui-managers": "1.5.4-canary.0-9261ea6",
17
+ "@playkit-js/playkit-js-ui": "0.79.13",
18
+ "@playkit-js/ui-managers": "1.6.2-canary.0-9634d47",
19
19
  "@types/sanitize-html": "^2.9.3",
20
20
  "conventional-github-releaser": "3.1.3",
21
21
  "copyfiles": "^2.4.1",
22
22
  "cross-env": "^7.0.3",
23
23
  "css-loader": "^6.7.1",
24
- "cypress": "^12.12.0",
24
+ "cypress": "13.13.1",
25
25
  "playwright-webkit": "^1.33.0",
26
26
  "prettier": "^2.6.2",
27
27
  "rimraf": "^5.0.5",
@@ -42,7 +42,7 @@
42
42
  "clean": "rimraf ./dist",
43
43
  "prebuild": "npm run clean",
44
44
  "build": "webpack --mode production",
45
- "dev": "webpack-dev-server --mode development",
45
+ "dev": "webpack serve --open --mode development",
46
46
  "watch": "webpack --progress --color --watch --mode development",
47
47
  "cy:open": "cypress open",
48
48
  "cy:run": "yarn run cy:run:chrome && yarn run cy:run:firefox && yarn run cy:run:edge && yarn run cy:run:safari",
@@ -77,15 +77,16 @@
77
77
  "html5 player"
78
78
  ],
79
79
  "dependencies": {
80
- "@playkit-js/common": "1.5.13",
80
+ "@playkit-js/common": "1.5.23",
81
+ "@playkit-js/webpack-common": "^1.0.1-canary.0-dfd24a9",
81
82
  "sanitize-html": "^2.11.0",
82
83
  "stream-browserify": "^3.0.0"
83
84
  },
84
85
  "kaltura": {
85
86
  "name": "playkit-js-transcript",
86
87
  "dependencies": {
87
- "playkit-kaltura-cuepoints": "3.0.13",
88
- "playkit-ui-managers": "1.5.3"
88
+ "playkit-kaltura-cuepoints": "3.0.16",
89
+ "playkit-ui-managers": "1.7.0"
89
90
  }
90
91
  }
91
92
  }
@@ -22,10 +22,10 @@ export const AttachPlaceholder = ({onAttach, onClose}: AttachPlaceholderProps) =
22
22
  </div>
23
23
  <div className={styles.attachContent}>
24
24
  <div className={styles.attachText}>
25
- <Text id="transcript.attachTranscriptText">Transcript popped out</Text>
25
+ <Text id="transcript.attach_transcript_text">Transcript popped out</Text>
26
26
  </div>
27
27
  <Button type={ButtonType.primary} size={ButtonSize.medium} className={styles.attachButton} onClick={onAttach} testId="transcriptAttachButton">
28
- <Text id="transcript.attachTranscriptButton">Bring it back</Text>
28
+ <Text id="transcript.attach_transcript_button">Bring it back</Text>
29
29
  </Button>
30
30
  </div>
31
31
  </div>
@@ -58,7 +58,7 @@
58
58
  }
59
59
  }
60
60
  .highlight-search {
61
- color: $secondary-text-contrast-color;
61
+ color: $tone-8-color;
62
62
  background-color: $secondary-color;
63
63
  border-radius: 2px;
64
64
  &::selection {
@@ -4,18 +4,33 @@ import {secondsToTime} from '../../utils';
4
4
  import {CuePointData} from '../../types';
5
5
  import * as styles from './caption.scss';
6
6
 
7
+ import {TranscriptEvents} from '../../events/events';
8
+
9
+ //@ts-ignore
10
+ const {getDurationAsText} = KalturaPlayer.ui.utils;
11
+ const {withText, Text} = KalturaPlayer.ui.preacti18n;
12
+ const {withEventManager} = KalturaPlayer.ui.Event;
13
+ const {withPlayer} = KalturaPlayer.ui.components;
14
+
7
15
  export interface CaptionProps {
8
16
  showTime: boolean;
9
17
  searchLength: number;
10
18
  scrollTo(el: HTMLElement): void;
11
19
  scrollToSearchMatch(el: HTMLElement): void;
12
20
  videoDuration: number;
21
+ eventManager?: any;
22
+ player?: any;
23
+ captionLabel?: string;
24
+ moveToSearch?: string;
25
+ navigationInstruction?: string;
26
+ timeLabel?: string;
27
+ setTextToRead: (textToRead: string, delay?: number) => void;
13
28
  }
14
29
 
15
30
  interface ExtendedCaptionProps extends CaptionProps {
16
31
  caption: CuePointData;
17
32
  onClick: () => void;
18
- highlighted: boolean;
33
+ isHighlighted: boolean;
19
34
  shouldScroll: boolean;
20
35
  shouldScrollToSearchMatch: boolean;
21
36
  indexMap: Record<string, number> | undefined;
@@ -24,26 +39,67 @@ interface ExtendedCaptionProps extends CaptionProps {
24
39
  isAutoScrollEnabled: boolean;
25
40
  }
26
41
 
42
+ const translates = {
43
+ captionLabel: <Text id="transcript.caption_label">Jump to this point in video</Text>,
44
+ moveToSearch: <Text id="transcript.move_to_search">Click to jump to search result</Text>,
45
+ navigationInstruction: (
46
+ <Text id="transcript.navigation_instruction">
47
+ Press Home to navigate to the beginning of the transcript. Press End to jump to the end of the transcript.
48
+ </Text>
49
+ ),
50
+ timeLabel: <Text id="transcript.time_label">Timestamp</Text>
51
+ };
52
+
53
+ @withText(translates)
54
+ @withEventManager
55
+ @withPlayer
27
56
  export class Caption extends Component<ExtendedCaptionProps> {
28
- private _hotspotRef: HTMLElement | null = null;
57
+ private _captionRef: HTMLElement | null = null;
29
58
 
30
- componentDidUpdate() {
31
- if (this._hotspotRef && this.props.shouldScroll) {
32
- this.props.scrollTo(this._hotspotRef);
33
- } else if (this._hotspotRef && this.props.shouldScrollToSearchMatch) {
34
- this.props.scrollToSearchMatch(this._hotspotRef);
59
+ get indexArray() {
60
+ if (!this.props.indexMap) {
61
+ return [];
62
+ }
63
+ return Object.keys(this.props.indexMap).sort((a, b) => Number(a) - Number(b));
64
+ }
65
+
66
+ componentDidUpdate(previousProps: Readonly<ExtendedCaptionProps>) {
67
+ if (this._captionRef && this.props.shouldScroll) {
68
+ this.props.scrollTo(this._captionRef);
69
+ } else if (this._captionRef && this.props.shouldScrollToSearchMatch) {
70
+ this.props.scrollToSearchMatch(this._captionRef);
71
+ }
72
+ if (this.props.indexMap && previousProps.activeSearchIndex !== this.props.activeSearchIndex) {
73
+ if (this._hasSearchMatch()) {
74
+ this.props.setTextToRead(this.props.caption.text);
75
+ }
35
76
  }
36
77
  }
37
78
 
79
+ componentDidMount(): void {
80
+ const {eventManager, player} = this.props;
81
+ eventManager?.listen(player, TranscriptEvents.TRANSCRIPT_TO_SEARCH_MATCH, () => {
82
+ if (this._hasSearchMatch()) {
83
+ this._captionRef?.focus();
84
+ }
85
+ });
86
+
87
+ eventManager?.listen(player, TranscriptEvents.TRANSCRIPT_SCROLLING, () => {
88
+ if (this._captionRef && this._hasSearchMatch()) {
89
+ this.props.scrollTo(this._captionRef);
90
+ }
91
+ });
92
+ }
93
+
38
94
  shouldComponentUpdate(nextProps: ExtendedCaptionProps) {
39
- const {indexMap, highlighted, isAutoScrollEnabled, activeSearchIndex, longerThanHour} = this.props;
95
+ const {indexMap, isHighlighted, isAutoScrollEnabled, activeSearchIndex, longerThanHour} = this.props;
40
96
  if (longerThanHour !== nextProps.longerThanHour) {
41
97
  return true;
42
98
  }
43
- if (highlighted !== nextProps.highlighted) {
99
+ if (isHighlighted !== nextProps.isHighlighted) {
44
100
  return true;
45
101
  }
46
- if (highlighted && isAutoScrollEnabled !== nextProps.isAutoScrollEnabled) {
102
+ if (isHighlighted && isAutoScrollEnabled !== nextProps.isAutoScrollEnabled) {
47
103
  return true;
48
104
  }
49
105
  if (indexMap !== nextProps.indexMap) {
@@ -66,12 +122,16 @@ export class Caption extends Component<ExtendedCaptionProps> {
66
122
  }
67
123
  };
68
124
 
125
+ private _hasSearchMatch = () => {
126
+ if (!this.props.indexMap) {
127
+ return false;
128
+ }
129
+ return Boolean(this.indexArray.find((el: string) => Number(el) === this.props.activeSearchIndex));
130
+ };
131
+
69
132
  private _renderText = (text: string) => {
70
133
  const {activeSearchIndex, searchLength, indexMap} = this.props;
71
- let indexArray: string[] = [];
72
- if (indexMap) {
73
- indexArray = Object.keys(indexMap).sort((a, b) => Number(a) - Number(b));
74
- }
134
+ const indexArray = this.indexArray;
75
135
  if (text?.length === 0) {
76
136
  return null;
77
137
  }
@@ -99,15 +159,17 @@ export class Caption extends Component<ExtendedCaptionProps> {
99
159
  };
100
160
 
101
161
  render() {
102
- const {caption, highlighted, showTime, longerThanHour} = this.props;
103
- const {startTime, id} = caption;
104
- const isHighlighted = Object.keys(highlighted).some(c => c === id) ;
162
+ const {caption, isHighlighted, showTime, longerThanHour, indexMap, captionLabel, moveToSearch, navigationInstruction, timeLabel, player} =
163
+ this.props;
164
+ const {startTime} = caption;
105
165
  const time = showTime ? secondsToTime(startTime, longerThanHour) : '';
106
166
 
107
167
  const captionA11yProps: Record<string, any> = {
108
168
  ariaCurrent: isHighlighted,
109
169
  tabIndex: 0,
110
- ariaLabel: `${time}${showTime ? ' ' : ''}${caption.text}`,
170
+ ariaLabel: `${showTime ? `${timeLabel} ${getDurationAsText(Math.floor(startTime), player?.config.ui.locale, true)} ` : ''}${caption.text} ${
171
+ indexMap ? moveToSearch : captionLabel
172
+ }. ${navigationInstruction}`,
111
173
  role: 'button'
112
174
  };
113
175
 
@@ -116,7 +178,7 @@ export class Caption extends Component<ExtendedCaptionProps> {
116
178
  <div
117
179
  className={styles.caption}
118
180
  ref={node => {
119
- this._hotspotRef = node;
181
+ this._captionRef = node;
120
182
  }}
121
183
  {...captionA11yProps}>
122
184
  {showTime && (
@@ -1,4 +1,5 @@
1
1
  import {h, Component} from 'preact';
2
+ import {ScreenReaderContext} from '@playkit-js/common/dist/hoc/sr-wrapper';
2
3
  import {HOUR} from '../../utils';
3
4
  import {Caption} from '../caption';
4
5
  import * as styles from './captionList.scss';
@@ -19,7 +20,6 @@ export interface Props {
19
20
  onSeek: (n: CuePointData) => void;
20
21
  autoScroll?: boolean;
21
22
  onScroll: (n: number) => void;
22
- widgetWidth: number;
23
23
  highlightedMap: HighlightedMap;
24
24
  showItemsIcons: boolean;
25
25
  listDataContainCaptions?: boolean;
@@ -84,12 +84,12 @@ export class CaptionList extends Component<Props> {
84
84
  key: id,
85
85
  onClick: this._handleClick(captionData),
86
86
  caption: captionData,
87
- highlighted: highlightedMap,
87
+ isHighlighted: Boolean(highlightedMap[id]),
88
88
  longerThanHour: captionProps.videoDuration >= HOUR,
89
89
  shouldScroll: this._getShouldScroll(id),
90
90
  shouldScrollToSearchMatch: this._getShouldScrollToSearchMatch(id),
91
91
  isAutoScrollEnabled,
92
- searchCaption: this.props.searchMap[captionData.id],
92
+ searchCaption: this.props.searchMap[id],
93
93
  ...this._getSearchProps(id)
94
94
  };
95
95
  return newCaptionProps;
@@ -97,41 +97,45 @@ export class CaptionList extends Component<Props> {
97
97
 
98
98
  private _handleKeyUp = (event: KeyboardEvent) => {
99
99
  if (event.keyCode === END) {
100
- this._lastCaptionRef?._hotspotRef?.focus();
100
+ this._lastCaptionRef?.base.focus();
101
101
  } else if (event.keyCode === HOME) {
102
- this._firstCaptionRef?._hotspotRef?.focus();
102
+ this._firstCaptionRef?.base.focus();
103
103
  }
104
104
  };
105
105
 
106
106
  render() {
107
- const {data} = this.props;
108
- let isSearchCaption = false;
107
+ const {data, highlightedMap} = this.props;
108
+ let isSearchCaptionInList = false;
109
109
  return (
110
110
  <div className={styles.transcriptWrapper} onKeyUp={this._handleKeyUp}>
111
111
  {data.map((captionData, index) => {
112
112
  const captionProps = this._getCaptionProps(captionData);
113
+ let isSearchCaption = false;
114
+ if (captionProps.searchCaption && Object.keys(captionProps.searchCaption).some(key => parseInt(key) === this.props.activeSearchIndex)) {
115
+ isSearchCaptionInList = true;
116
+ isSearchCaption = true;
117
+ }
113
118
  return (
114
- <Caption
115
- ref={node => {
116
- if (index === 0) {
117
- this._firstCaptionRef = node;
118
- } else if (index === data.length - 1) {
119
- this._lastCaptionRef = node;
120
- }
121
- if (captionProps.searchCaption){
122
- Object.keys(captionProps.searchCaption).forEach(key => {
123
- if (parseInt(key) === this.props.activeSearchIndex) {
124
- this._currentCaptionRef = node
125
- isSearchCaption = true;
126
- }
127
- });
128
- }
129
- if (!isSearchCaption && captionProps.highlighted[captionData.id]) {
130
- this._currentCaptionRef = node;
131
- }
119
+ <ScreenReaderContext.Consumer key={captionData.id}>
120
+ {(setTextToRead: (textToRead: string, delay?: number | undefined) => void) => {
121
+ return (
122
+ <Caption
123
+ setTextToRead={setTextToRead}
124
+ ref={node => {
125
+ if (index === 0) {
126
+ this._firstCaptionRef = node;
127
+ } else if (index === data.length - 1) {
128
+ this._lastCaptionRef = node;
129
+ }
130
+ if (isSearchCaption || (!isSearchCaptionInList && captionProps.isHighlighted)) {
131
+ this._currentCaptionRef = node;
132
+ }
133
+ }}
134
+ {...captionProps}
135
+ />
136
+ );
132
137
  }}
133
- {...captionProps}
134
- />
138
+ </ScreenReaderContext.Consumer>
135
139
  );
136
140
  })}
137
141
  </div>
@@ -24,6 +24,7 @@ export const PluginButton = ({isActive, label, id, icon, dataTestId, setRef}: Pl
24
24
  setRef(node);
25
25
  }
26
26
  }}
27
+ tabIndex={0}
27
28
  type="button"
28
29
  aria-label={label}
29
30
  className={[ui.style.upperBarIcon, styles.pluginButton, isActive ? styles.active : ''].join(' ')}
@@ -0,0 +1,108 @@
1
+ import {A11yWrapper, OnClickEvent} from '@playkit-js/common/dist/hoc/a11y-wrapper';
2
+ import {h} from 'preact';
3
+ import {useState} from 'preact/hooks';
4
+ import {Icon} from '@playkit-js/common/dist/icon';
5
+
6
+ import * as styles from './popover-menu.scss';
7
+
8
+ export interface PopoverMenuItemData {
9
+ testId: string;
10
+ label: string;
11
+ onClick?: () => void;
12
+ isDisabled?: boolean;
13
+ isSelected?: boolean;
14
+ items?: Array<PopoverMenuItemData>;
15
+ }
16
+
17
+ interface PopoverMenuItemProps {
18
+ item: PopoverMenuItemData;
19
+ index: number;
20
+ setRef?: (index: number, ref: HTMLDivElement | null) => void;
21
+ onKeyUp: (index: number) => void;
22
+ onKeyDown: (index: number) => void;
23
+ onClick?: () => void;
24
+ }
25
+
26
+ export const PopoverMenuItem = (props: PopoverMenuItemProps) => {
27
+ const {item, index, setRef, onKeyUp, onKeyDown, onClick} = props;
28
+ const {isDisabled, isSelected, items, testId, label} = item;
29
+ const [isChildOpen, setIsChildOpen] = useState(false);
30
+
31
+ const getAddonAfter = () => {
32
+ if (items) {
33
+ return <Icon name="chevronRight" />;
34
+ }
35
+ if (isSelected) {
36
+ return <Icon name="check" />;
37
+ }
38
+ return null;
39
+ };
40
+
41
+ const handleOnClick = (event: OnClickEvent) => {
42
+ if (isDisabled) {
43
+ return;
44
+ }
45
+ if (items) {
46
+ event.stopPropagation();
47
+ setIsChildOpen(!isChildOpen);
48
+ return;
49
+ }
50
+ onClick?.();
51
+ };
52
+
53
+ const renderChildItems = () => {
54
+ if (!items) {
55
+ return null;
56
+ }
57
+ return (
58
+ <div className={[styles.popoverComponent, styles.childItem].join(' ')} role="menu" aria-expanded={isChildOpen} id="popoverContent">
59
+ {isChildOpen
60
+ ? items.map((item, index) => (
61
+ <PopoverMenuItem
62
+ key={index}
63
+ item={item}
64
+ index={index}
65
+ onKeyDown={() => {}}
66
+ onKeyUp={() => {}}
67
+ onClick={() => {
68
+ item.onClick?.();
69
+ onClick?.();
70
+ setIsChildOpen(false);
71
+ }}
72
+ />
73
+ ))
74
+ : null}
75
+ </div>
76
+ );
77
+ };
78
+
79
+ return (
80
+ <A11yWrapper
81
+ role="menuitem"
82
+ onClick={handleOnClick}
83
+ onDownKeyPressed={() => {
84
+ if (!isDisabled) {
85
+ onKeyDown(index);
86
+ }
87
+ }}
88
+ onUpKeyPressed={() => {
89
+ if (!isDisabled) {
90
+ onKeyUp(index);
91
+ }
92
+ }}>
93
+ <div
94
+ tabIndex={isDisabled ? -1 : 0}
95
+ role="menuitem"
96
+ aria-selected={isSelected}
97
+ className={`${styles.popoverMenuItem} ${isDisabled ? styles.popoverMenuItemDisabled : ''}`}
98
+ data-testid={testId}
99
+ ref={node => {
100
+ setRef?.(index, node);
101
+ }}>
102
+ {label}
103
+ {getAddonAfter()}
104
+ {renderChildItems()}
105
+ </div>
106
+ </A11yWrapper>
107
+ );
108
+ };
@@ -1,17 +1,19 @@
1
1
  @import './variables.scss';
2
2
 
3
+ $child-item-width: 170px;
4
+
3
5
  .popover-anchor-container {
4
6
  cursor: pointer;
5
7
  border-radius: $roundness-1;
6
-
8
+
7
9
  &:hover {
8
10
  background-color: $tone-4-color;
9
11
  }
10
-
12
+
11
13
  &.active {
12
14
  background-color: $tone-6-color;
13
15
  }
14
-
16
+
15
17
  .popover-anchor {
16
18
  pointer-events: none;
17
19
  }
@@ -19,7 +21,7 @@
19
21
 
20
22
  .popover-container {
21
23
  position: relative;
22
-
24
+
23
25
  .popover-component {
24
26
  background-color: $tone-7-color;
25
27
  border-radius: $roundness-1;
@@ -29,11 +31,21 @@
29
31
  position: absolute;
30
32
  right: 0px;
31
33
  z-index: 1;
34
+ &.child-item {
35
+ margin-top: 0;
36
+ top: 0;
37
+ width: $child-item-width;
38
+ left: calc(-7px - #{$child-item-width});
39
+ max-height: calc(100% * 5.8); // height - almost 6 menu items like in player caption menu
40
+ overflow-y: auto;
41
+ }
32
42
  }
33
43
  }
34
44
  .popover-menu-item {
45
+ position: relative;
35
46
  align-items: center;
36
47
  display: flex;
48
+ justify-content: space-between;
37
49
  font-size: 15px;
38
50
  line-height: 18px;
39
51
  min-height: 30px;
@@ -44,13 +56,13 @@
44
56
  &.popover-menu-item-disabled {
45
57
  color: $tone-4-color;
46
58
  }
47
-
59
+
48
60
  &:hover:not(.popover-menu-item-disabled) {
49
61
  background-color: $tone-6-color;
50
62
  border-radius: $roundness-1;
51
63
  cursor: pointer;
52
64
  }
53
-
65
+
54
66
  &:focus {
55
67
  outline: 1px solid #222;
56
68
  }
@@ -60,4 +72,3 @@
60
72
  padding-top: 6px;
61
73
  padding-bottom: 6px;
62
74
  }
63
-