@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.
@@ -1,26 +1,23 @@
1
1
  import {A11yWrapper} from '@playkit-js/common/dist/hoc/a11y-wrapper';
2
2
  import {h, Component, VNode} from 'preact';
3
+ import {PopoverMenuItem, PopoverMenuItemData} from './popover-menu-item';
4
+ import {ui} from '@playkit-js/kaltura-player-js';
5
+ const {Tooltip} = ui.components;
6
+ const {withText, Text} = ui.preacti18n;
3
7
 
4
- const {Tooltip} = KalturaPlayer.ui.components;
5
- const {withText, Text} = KalturaPlayer.ui.preacti18n;
6
-
7
- const {withEventManager} = KalturaPlayer.ui.Event;
8
- const {TAB} = KalturaPlayer.ui.utils.KeyMap;
8
+ const {withEventManager} = ui.Event;
9
+ const {TAB} = ui.utils.KeyMap;
9
10
 
10
11
  import * as styles from './popover-menu.scss';
11
12
 
12
- interface PopoverMenuItemData {
13
- testId: string;
14
- label: string;
15
- onClick: () => void;
16
- isDisabled?: boolean;
17
- }
18
-
19
13
  interface PopoverMenuProps {
20
14
  moreOptionsLabel?: string;
21
15
  eventManager?: any;
22
16
  children?: VNode;
23
17
  items: Array<PopoverMenuItemData>;
18
+ kitchenSinkDetached: boolean;
19
+ popOverMenuHeight: number;
20
+ shouldUseCalculatedHeight: boolean;
24
21
  }
25
22
 
26
23
  interface PopoverMenuState {
@@ -41,23 +38,25 @@ class PopoverMenu extends Component<PopoverMenuProps, PopoverMenuState> {
41
38
 
42
39
  constructor(props: PopoverMenuProps) {
43
40
  super(props);
44
- this.state = {isOpen: false};
41
+ this.state = {
42
+ isOpen: false
43
+ };
45
44
 
46
- this.props.eventManager?.listen(document, 'click', this.handleMouseEvent);
47
- this.props.eventManager?.listen(document, 'keydown', this.handleKeydownEvent);
45
+ this.props.eventManager?.listen(document, 'click', this._handleMouseEvent);
46
+ this.props.eventManager?.listen(document, 'keydown', this._handleKeydownEvent);
48
47
  }
49
48
 
50
49
  componentWillUnmount() {
51
50
  this._itemsRefMap = new Map();
52
51
  }
53
52
 
54
- private handleMouseEvent = (event: MouseEvent) => {
53
+ private _handleMouseEvent = (event: MouseEvent) => {
55
54
  if (!this._controlElementRef?.contains(event.target as Node | null)) {
56
- this.closePopover();
55
+ this._closePopover();
57
56
  }
58
57
  };
59
58
 
60
- private handleKeydownEvent = (event: KeyboardEvent) => {
59
+ private _handleKeydownEvent = (event: KeyboardEvent) => {
61
60
  const eventTarget = event.target as Node | null;
62
61
  if (
63
62
  this.state.isOpen &&
@@ -66,7 +65,7 @@ class PopoverMenu extends Component<PopoverMenuProps, PopoverMenuState> {
66
65
  !this._popoverElementRef?.contains(eventTarget) &&
67
66
  eventTarget !== this._controlElementRef
68
67
  ) {
69
- this.closePopover();
68
+ this._closePopover();
70
69
  }
71
70
  };
72
71
 
@@ -78,26 +77,26 @@ class PopoverMenu extends Component<PopoverMenuProps, PopoverMenuState> {
78
77
  this._getItemRef(currentIndex + 1)?.focus();
79
78
  };
80
79
 
81
- private closePopover() {
80
+ private _closePopover() {
82
81
  this.setState({isOpen: false});
83
82
  }
84
83
 
85
- private togglePopover = () => {
84
+ private _togglePopover = () => {
86
85
  const isOpen = !this.state.isOpen;
87
86
 
88
87
  this.setState({isOpen}, () => {
89
- if (isOpen){
88
+ if (isOpen) {
90
89
  this._controlElementRef?.focus();
91
90
  this.props.eventManager?.listen(this._controlElementRef, 'keydown', (event: KeyboardEvent) => {
92
- if (event.keyCode === TAB){
91
+ if (event.keyCode === TAB) {
93
92
  const firstNonDisabledItem = this.props.items.findIndex((item: PopoverMenuItemData) => !item.isDisabled);
94
93
  if (firstNonDisabledItem !== -1) {
95
- this._getItemRef(firstNonDisabledItem -1)?.focus();
94
+ this._getItemRef(firstNonDisabledItem - 1)?.focus();
96
95
  }
97
96
  }
98
- })
97
+ });
99
98
  }
100
- })
99
+ });
101
100
  };
102
101
 
103
102
  private _getItemRef = (index: number) => {
@@ -109,23 +108,30 @@ class PopoverMenu extends Component<PopoverMenuProps, PopoverMenuState> {
109
108
  };
110
109
 
111
110
  render() {
112
- const {children, items} = this.props;
111
+ const {children, items, kitchenSinkDetached, popOverMenuHeight, shouldUseCalculatedHeight} = this.props;
112
+
113
+ let popOverHeight = 0;
114
+ if (shouldUseCalculatedHeight) {
115
+ const neededHeight = 48 * items.length;
116
+ const padding = 14;
117
+ popOverHeight = popOverMenuHeight - neededHeight <= 0 ? popOverMenuHeight - padding : neededHeight - padding;
118
+ }
113
119
 
114
120
  const popoverMenuContent = (
115
121
  <div className={styles.popoverContainer}>
116
- <A11yWrapper onClick={(e) => {
117
- e.stopPropagation();
118
- this.togglePopover();
119
- }}>
122
+ <A11yWrapper
123
+ onClick={e => {
124
+ e.stopPropagation();
125
+ this._togglePopover();
126
+ }}>
120
127
  <div
121
- aria-label={this.props.moreOptionsLabel!}
122
128
  tabIndex={0}
123
129
  data-testid="popover-anchor-container"
124
130
  className={`${styles.popoverAnchorContainer} ${this.state.isOpen ? styles.active : ''}`}
125
131
  aria-expanded={this.state.isOpen}
126
132
  aria-controls="popoverContent"
127
133
  ref={node => {
128
- if (node){
134
+ if (node) {
129
135
  this._controlElementRef = node;
130
136
  }
131
137
  }}>
@@ -135,6 +141,7 @@ class PopoverMenu extends Component<PopoverMenuProps, PopoverMenuState> {
135
141
 
136
142
  <div
137
143
  className={styles.popoverComponent}
144
+ style={shouldUseCalculatedHeight ? {height: `${popOverHeight}px`, overflowY: 'auto'} : {}}
138
145
  role="menu"
139
146
  aria-expanded={this.state.isOpen}
140
147
  id="popoverContent"
@@ -142,41 +149,22 @@ class PopoverMenu extends Component<PopoverMenuProps, PopoverMenuState> {
142
149
  this._popoverElementRef = node;
143
150
  }}>
144
151
  {this.state.isOpen
145
- ? items.map(({label, onClick, testId, isDisabled}, index) => {
146
- return (
147
- <A11yWrapper
148
- role="menuitem"
149
- onClick={() => {
150
- if (!isDisabled) {
151
- this.closePopover();
152
- onClick();
153
- }
154
- }}
155
- onDownKeyPressed={() => {
156
- if (!isDisabled) {
157
- this._handleDownKeyPressed(index);
158
- }
159
- }}
160
- onUpKeyPressed={() => {
161
- if (!isDisabled) {
162
- this._handleUpKeyPressed(index);
163
- }
164
- }}>
165
- {
166
- <div
167
- tabIndex={isDisabled ? -1 : 0}
168
- role="menuitem"
169
- className={`${styles.popoverMenuItem} ${isDisabled ? styles.popoverMenuItemDisabled : ''}`}
170
- data-testid={testId}
171
- ref={node => {
172
- this._setItemRef(index, node);
173
- }}>
174
- {label}
175
- </div>
152
+ ? items.map((item, index) => (
153
+ <PopoverMenuItem
154
+ key={index}
155
+ item={item}
156
+ index={index}
157
+ onKeyDown={this._handleDownKeyPressed}
158
+ onKeyUp={this._handleUpKeyPressed}
159
+ setRef={this._setItemRef}
160
+ onClick={() => {
161
+ this._closePopover();
162
+ if (!item.items) {
163
+ item.onClick?.();
176
164
  }
177
- </A11yWrapper>
178
- );
179
- })
165
+ }}
166
+ />
167
+ ))
180
168
  : null}
181
169
  </div>
182
170
  </div>
@@ -186,7 +174,9 @@ class PopoverMenu extends Component<PopoverMenuProps, PopoverMenuState> {
186
174
  popoverMenuContent
187
175
  ) : (
188
176
  <div>
189
- <Tooltip label={this.props.moreOptionsLabel!}>{popoverMenuContent}</Tooltip>
177
+ <Tooltip label={this.props.moreOptionsLabel!} {...(kitchenSinkDetached ? {type: 'bottom-left', strictPosition: true} : {})}>
178
+ {popoverMenuContent}
179
+ </Tooltip>
190
180
  </div>
191
181
  );
192
182
  }
@@ -1,7 +1,13 @@
1
1
  import {h, Component} from 'preact';
2
2
  import {InputField} from '@playkit-js/common/dist/components/input-field';
3
+ import {core, ui} from '@playkit-js/kaltura-player-js';
3
4
 
5
+ const {Utils} = core;
6
+ const {withEventManager} = KalturaPlayer.ui.Event;
7
+ const {TAB} = KalturaPlayer.ui.utils.KeyMap;
4
8
  const {withText, Text} = KalturaPlayer.ui.preacti18n;
9
+ const {withPlayer} = ui.Components;
10
+
5
11
  const translates = ({activeSearchIndex, totalSearchResults}: SearchProps) => ({
6
12
  searchLabel: <Text id="transcript.search">Search in Transcript</Text>,
7
13
  clearSearchLabel: <Text id="transcript.clear_search">Clear search</Text>,
@@ -34,11 +40,34 @@ export interface SearchProps {
34
40
  nextMatchLabel?: string;
35
41
  prevMatchLabel?: string;
36
42
  searchResultsLabel?: string;
43
+ eventManager?: any
44
+ focusPluginButton: (event: KeyboardEvent) => void;
45
+ player?: any;
37
46
  }
38
47
 
48
+ @withPlayer
49
+ @withEventManager
39
50
  class SearchComponent extends Component<SearchProps> {
40
51
  private _inputField: InputField | null = null;
41
52
 
53
+ constructor(props: SearchProps) {
54
+ super(props);
55
+ if (this.props.player._firstPlay){
56
+ this.props.eventManager?.listen(this.props.player, this.props.player.Event.FIRST_PLAY, () => {
57
+ this.props.eventManager?.listen(document, 'keydown', this.handleKeydownEvent);
58
+ })
59
+ } else {
60
+ this.props.eventManager?.listen(document, 'keydown', this.handleKeydownEvent);
61
+ }
62
+ }
63
+
64
+ private handleKeydownEvent = (event: KeyboardEvent) => {
65
+ if (event.keyCode === TAB && event.shiftKey && document.activeElement === this._inputField?.base?.childNodes[0]){
66
+ //@ts-ignore
67
+ this.props.focusPluginButton(event);
68
+ }
69
+ };
70
+
42
71
  shouldComponentUpdate(nextProps: Readonly<SearchProps>) {
43
72
  const {value, activeSearchIndex, totalSearchResults, kitchenSinkActive} = this.props;
44
73
  if (
@@ -23,6 +23,7 @@
23
23
  font-style: normal;
24
24
  font-weight: 700;
25
25
  line-height: 20px;
26
+ text-align: center;
26
27
  }
27
28
  }
28
29
  }
@@ -1,5 +1,7 @@
1
1
  @import '../../variables.scss';
2
2
 
3
+ $button-height: 32px;
4
+
3
5
  .hidden {
4
6
  visibility: hidden;
5
7
  }
@@ -25,7 +27,7 @@
25
27
  align-items: center;
26
28
  justify-content: center;
27
29
  width: 120px;
28
- height: 32px;
30
+ height: $button-height;
29
31
  border-radius: $roundness-3;
30
32
  box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.3);
31
33
  border: solid 1px $primary-color;
@@ -44,6 +46,10 @@
44
46
  font-family: sans-serif;
45
47
  font-style: normal;
46
48
  }
49
+
50
+ *:focus-visible:not(input) {
51
+ outline: 1px solid $tab-focus-color;
52
+ }
47
53
  }
48
54
 
49
55
  .global-container {
@@ -56,16 +62,31 @@
56
62
  }
57
63
 
58
64
  .header {
65
+ $header-margin-bottom: 8px;
66
+
67
+ position: relative;
59
68
  display: flex;
60
69
  align-items: center;
61
70
  justify-content: space-between;
62
71
  width: 100%;
63
- margin-bottom: 8px;
72
+ margin-bottom: $header-margin-bottom;
64
73
  padding-left: 16px;
65
74
  font-size: 16px;
66
75
  padding-right: 16px;
67
76
  gap: 8px;
68
77
  z-index: 2;
78
+ .to-search-button {
79
+ position: absolute;
80
+ right: 16px;
81
+ bottom: calc((#{$button-height + $header-margin-bottom}) * -1); // button height + margin
82
+ opacity: 0;
83
+ &:focus {
84
+ opacity: 1;
85
+ }
86
+ &[class^="Button__button"]:focus:not([class^="Button__disabled"]) {
87
+ background-color: $tone-6-color
88
+ }
89
+ }
69
90
  }
70
91
 
71
92
  .body {
@@ -1,6 +1,6 @@
1
1
  import {h, Component} from 'preact';
2
- import {ui} from '@playkit-js/kaltura-player-js';
3
- import {debounce} from '../../utils';
2
+ import {ui, core} from '@playkit-js/kaltura-player-js';
3
+ import {debounce} from '@playkit-js/common/dist/utils-common';
4
4
  import * as styles from './transcript.scss';
5
5
  import {Spinner} from '../spinner';
6
6
  import {Search} from '../search';
@@ -11,24 +11,27 @@ import {AutoscrollButton} from '../autoscroll-button';
11
11
  import {TranscriptMenu} from '../transcript-menu';
12
12
  import {SmallScreenSlate} from '../small-screen-slate';
13
13
  import {Button, ButtonType, ButtonSize} from '@playkit-js/common/dist/components/button';
14
+ import {ScreenReaderProvider} from '@playkit-js/common/dist/hoc/sr-wrapper';
14
15
  import {OnClickEvent, OnClick} from '@playkit-js/common/dist/hoc/a11y-wrapper';
15
16
  import {TranscriptEvents} from '../../events/events';
16
17
 
17
- const {ENTER, SPACE, TAB} = ui.utils.KeyMap;
18
+ const {ENTER, SPACE, TAB, ESC} = ui.utils.KeyMap;
18
19
  const {withText, Text} = ui.preacti18n;
19
20
 
20
- const {SidePanelModes} = ui;
21
+ const {SidePanelModes, SidePanelPositions} = ui;
21
22
  const {PLAYER_BREAK_POINTS} = ui.Components;
22
23
  const {connect} = ui.redux;
23
24
 
24
25
  const SEARCH_EVENT_DISPATCH_TIMEOUT = 2000;
26
+ const WIDGET_RESIZE_DEBOUNCE_TIMEOUT = 250;
25
27
 
26
28
  const translates = {
27
29
  skipTranscript: <Text id="transcript.skip_transcript">Skip transcript</Text>,
28
30
  errorTitle: <Text id="transcript.whoops">Whoops!</Text>,
29
31
  errorDescripton: <Text id="transcript.load_failed">Failed to load transcript</Text>,
30
32
  attachTranscript: <Text id="transcript.attach_transcript">Bring Transcript back</Text>,
31
- detachTranscript: <Text id="transcript.detach_transcript">Popout Transcript</Text>
33
+ detachTranscript: <Text id="transcript.detach_transcript">Popout transcript</Text>,
34
+ toSearchResult: <Text id="transcript.to_search_result">Go to result</Text>
32
35
  };
33
36
 
34
37
  export interface TranscriptProps {
@@ -53,6 +56,7 @@ export interface TranscriptProps {
53
56
  errorDescripton?: string;
54
57
  attachTranscript?: string;
55
58
  detachTranscript?: string;
59
+ toSearchResult?: string;
56
60
  downloadDisabled: boolean;
57
61
  onDownload: () => void;
58
62
  printDisabled: boolean;
@@ -65,6 +69,13 @@ export interface TranscriptProps {
65
69
  onAttach: () => void;
66
70
  kitchenSinkDetached: boolean;
67
71
  isMobile?: boolean;
72
+ playerWidth?: number;
73
+ onJumpToSearchMatch: () => void;
74
+ onScrollToSearchMatch: () => void;
75
+ focusPluginButton: (event: KeyboardEvent) => void;
76
+ textTracks: Array<core.TextTrack>;
77
+ changeLanguage: (textTrack: core.TextTrack) => void;
78
+ sidePanelPosition: string;
68
79
  }
69
80
 
70
81
  interface TranscriptState {
@@ -85,12 +96,13 @@ const initialSearch = {
85
96
  searchLength: 0
86
97
  };
87
98
 
99
+ const SMALL_WIDGET_WIDTH = 240;
88
100
  const SEARCHBAR_HEIGHT = 38; // height of search bar with margins
89
- const smallScreen = PLAYER_BREAK_POINTS?.SMALL || 480;
90
101
 
91
102
  const mapStateToProps = (state: any, ownProps: Pick<TranscriptProps, 'expandMode'>) => ({
92
- smallScreen: ownProps.expandMode === SidePanelModes.ALONGSIDE && state.shell.playerClientRect?.width < smallScreen,
93
- isMobile: state.shell.isMobile
103
+ smallScreen: ownProps.expandMode === SidePanelModes.ALONGSIDE && state.shell.playerClientRect?.width <= PLAYER_BREAK_POINTS.TINY,
104
+ isMobile: state.shell.isMobile,
105
+ playerWidth: state.shell.playerClientRect?.width
94
106
  });
95
107
 
96
108
  // @ts-ignore
@@ -104,20 +116,36 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
104
116
  private _preventScrollEvent: boolean = false;
105
117
  private _scrollToSearchMatchEnabled: boolean = false;
106
118
  private _widgetRootRef: HTMLElement | null = null;
119
+ private _transcriptMenuRef: HTMLElement | null = null;
107
120
 
108
121
  private _widgetHeight: number = 0;
109
122
  private _topAutoscrollEdge: number = 0;
110
123
  private _bottomAutoscrollEdge: number = 0;
111
124
  private _thirdOfWidgetHeight: number = 0;
112
125
 
126
+ private _resizeObserver: ResizeObserver | null = null;
127
+
113
128
  state: TranscriptState = {
114
129
  isAutoScrollEnabled: true,
115
130
  widgetWidth: 0,
116
131
  ...initialSearch
117
132
  };
118
133
 
134
+ componentDidMount(): void {
135
+ if (window.ResizeObserver) {
136
+ // observe transcript root element size changes
137
+ this._resizeObserver = new ResizeObserver(() => {
138
+ this._debounced.setWidgetSize();
139
+ });
140
+ this._resizeObserver.observe(this._widgetRootRef!);
141
+ } else {
142
+ // use player size to define transcript root element size
143
+ this._debounced.setWidgetSize();
144
+ }
145
+ }
146
+
119
147
  componentDidUpdate(previousProps: Readonly<TranscriptProps>, previousState: Readonly<TranscriptState>): void {
120
- const {captions, activeCaptionLanguage} = this.props;
148
+ const {captions, activeCaptionLanguage, playerWidth} = this.props;
121
149
  const {search} = this.state;
122
150
  if (previousProps.captions !== captions) {
123
151
  // clear search value only if active caption language was switched, otherwise keep previous value
@@ -129,9 +157,25 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
129
157
  this._debounced.findSearchMatches();
130
158
  }
131
159
 
132
- this._setWidgetSize();
160
+ if (!this._resizeObserver && previousProps.playerWidth !== playerWidth) {
161
+ // re-calculate wiget size if player size changed
162
+ this._debounced.setWidgetSize();
163
+ }
164
+ }
165
+
166
+ componentWillUnmount(): void {
167
+ if (this._resizeObserver) {
168
+ this._resizeObserver?.disconnect();
169
+ this._resizeObserver = null;
170
+ }
133
171
  }
134
172
 
173
+ private _handleClose = (event: KeyboardEvent) => {
174
+ if (event.keyCode === ESC) {
175
+ this.props.onClose(event, true);
176
+ }
177
+ };
178
+
135
179
  private _enableAutoScroll = (event: OnClickEvent, byKeyboard?: boolean) => {
136
180
  event.preventDefault();
137
181
  if (this.state.isAutoScrollEnabled) {
@@ -172,7 +216,10 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
172
216
  onClick={kitchenSinkDetached ? onAttach : onDetach}
173
217
  icon={kitchenSinkDetached ? 'attach' : 'detach'}
174
218
  ariaLabel={kitchenSinkDetached ? attachTranscript : detachTranscript}
175
- tooltip={{label: kitchenSinkDetached ? attachTranscript! : detachTranscript!}}
219
+ tooltip={{
220
+ label: kitchenSinkDetached ? attachTranscript! : detachTranscript!,
221
+ ...(kitchenSinkDetached ? {type: 'bottom-left', strictPosition: kitchenSinkDetached} : {})
222
+ }}
176
223
  />
177
224
  </div>
178
225
  );
@@ -180,6 +227,7 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
180
227
 
181
228
  private _onSearch = (search: string) => {
182
229
  this.setState({search});
230
+ this.props.onScrollToSearchMatch()
183
231
  };
184
232
 
185
233
  private _findSearchMatches = () => {
@@ -237,11 +285,64 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
237
285
  return styles.smallWidth;
238
286
  };
239
287
 
288
+ private _renderJumpToSearchButton = () => {
289
+ const {toSearchResult, onJumpToSearchMatch} = this.props;
290
+ const {search, activeSearchIndex, totalSearchResults} = this.state;
291
+ if (!search || totalSearchResults === 0 || activeSearchIndex === 0) {
292
+ return null;
293
+ }
294
+ return (
295
+ <Button
296
+ type={ButtonType.secondary}
297
+ className={styles.toSearchButton}
298
+ onClick={onJumpToSearchMatch}
299
+ ariaLabel={toSearchResult}
300
+ testId="transcript_jumpToSearchMatch">
301
+ {toSearchResult}
302
+ </Button>
303
+ );
304
+ };
305
+
240
306
  private _renderHeader = () => {
241
- const {toggledWithEnter, kitchenSinkActive, kitchenSinkDetached, downloadDisabled, onDownload, printDisabled, onPrint, isLoading} = this.props;
307
+ const {
308
+ sidePanelPosition,
309
+ toggledWithEnter,
310
+ kitchenSinkActive,
311
+ kitchenSinkDetached,
312
+ downloadDisabled,
313
+ onDownload,
314
+ printDisabled,
315
+ onPrint,
316
+ isLoading,
317
+ attachTranscript,
318
+ detachTranscript,
319
+ onAttach,
320
+ onDetach,
321
+ textTracks,
322
+ changeLanguage
323
+ } = this.props;
242
324
  const {search, activeSearchIndex, totalSearchResults} = this.state;
325
+ const widgetHeight = this._widgetRootRef?.getBoundingClientRect().height;
326
+ const transcriptHeight = this._transcriptMenuRef?.getBoundingClientRect().height;
327
+ const popOverHeight = (widgetHeight ?? 0) - (transcriptHeight ?? 0) - 16;
328
+ const shouldUseCalculatedHeight = sidePanelPosition === SidePanelPositions.TOP || sidePanelPosition === SidePanelPositions.BOTTOM;
329
+
330
+ let detachMenuItem: null | any = null;
331
+ if (this.state.widgetWidth <= SMALL_WIDGET_WIDTH && !kitchenSinkDetached) {
332
+ detachMenuItem = {
333
+ label: kitchenSinkDetached ? attachTranscript : detachTranscript,
334
+ onClick: kitchenSinkDetached ? onAttach : onDetach,
335
+ testId: 'transcript-detach-attach-button',
336
+ disabled: isLoading
337
+ };
338
+ }
243
339
  return (
244
- <div className={[styles.header, this._getHeaderStyles()].join(' ')} data-testid="transcript_header">
340
+ <div
341
+ className={[styles.header, this._getHeaderStyles()].join(' ')}
342
+ data-testid="transcript_header"
343
+ ref={node => {
344
+ this._transcriptMenuRef = node;
345
+ }}>
245
346
  <Search
246
347
  onChange={this._onSearch}
247
348
  onSearchIndexChange={this._debounced.onActiveSearchIndexChange}
@@ -250,9 +351,25 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
250
351
  totalSearchResults={totalSearchResults}
251
352
  toggledWithEnter={toggledWithEnter}
252
353
  kitchenSinkActive={kitchenSinkActive}
354
+ focusPluginButton={this.props.focusPluginButton}
355
+ />
356
+ {this._renderJumpToSearchButton()}
357
+ <TranscriptMenu
358
+ {...{
359
+ shouldUseCalculatedHeight,
360
+ popOverHeight,
361
+ downloadDisabled,
362
+ onDownload,
363
+ printDisabled,
364
+ onPrint,
365
+ isLoading,
366
+ detachMenuItem,
367
+ kitchenSinkDetached,
368
+ textTracks,
369
+ changeLanguage
370
+ }}
253
371
  />
254
- <TranscriptMenu {...{downloadDisabled, onDownload, printDisabled, onPrint, isLoading}} />
255
- {this._renderDetachButton()}
372
+ {!detachMenuItem && this._renderDetachButton()}
256
373
  {!kitchenSinkDetached && (
257
374
  <div data-testid="transcriptCloseButton">
258
375
  <Button
@@ -348,7 +465,6 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
348
465
  activeSearchIndex={activeSearchIndex}
349
466
  captionProps={captionProps}
350
467
  onScroll={this._onScroll}
351
- widgetWidth={this.state.widgetWidth}
352
468
  showItemsIcons={true}
353
469
  searchActive={false}
354
470
  />
@@ -415,39 +531,44 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
415
531
  private _debounced = {
416
532
  findSearchMatches: debounce(this._findSearchMatches, this.props.searchDebounceTimeout),
417
533
  onActiveSearchIndexChange: debounce(this._setActiveSearchIndex, this.props.searchNextPrevDebounceTimeout),
418
- searchEventDispatcher: debounce(this._dispatchSearchEvent, SEARCH_EVENT_DISPATCH_TIMEOUT)
534
+ searchEventDispatcher: debounce(this._dispatchSearchEvent, SEARCH_EVENT_DISPATCH_TIMEOUT),
535
+ setWidgetSize: debounce(this._setWidgetSize, WIDGET_RESIZE_DEBOUNCE_TIMEOUT)
419
536
  };
420
537
 
421
538
  render(props: TranscriptProps) {
422
539
  const {isLoading, kitchenSinkActive, kitchenSinkDetached, hasError, smallScreen, toggledWithEnter} = props;
423
540
  const renderTranscriptButtons = !(isLoading || hasError);
541
+
424
542
  return (
425
- <div
426
- className={`${styles.root} ${kitchenSinkActive || kitchenSinkDetached ? '' : styles.hidden}`}
427
- ref={node => {
428
- this._widgetRootRef = node;
429
- }}
430
- data-testid="transcript_root">
431
- {smallScreen && !kitchenSinkDetached ? (
432
- <SmallScreenSlate onClose={this.props.onClose} toggledWithEnter={toggledWithEnter} />
433
- ) : (
434
- <div className={styles.globalContainer}>
435
- {this._renderHeader()}
436
-
437
- {renderTranscriptButtons && this._renderSkipTranscriptButton()}
438
- <div
439
- className={styles.body}
440
- onScroll={this._onScroll}
441
- ref={node => {
442
- this._transcriptListRef = node;
443
- }}
444
- data-testid="transcript_list">
445
- {isLoading ? this._renderLoading() : this._renderTranscript()}
543
+ <ScreenReaderProvider>
544
+ <div
545
+ className={`${styles.root} ${kitchenSinkActive || kitchenSinkDetached ? '' : styles.hidden}`}
546
+ ref={node => {
547
+ this._widgetRootRef = node;
548
+ }}
549
+ onKeyUp={this._handleClose}
550
+ data-testid="transcript_root">
551
+ {smallScreen && !kitchenSinkDetached ? (
552
+ <SmallScreenSlate onClose={this.props.onClose} toggledWithEnter={toggledWithEnter} />
553
+ ) : (
554
+ <div className={styles.globalContainer}>
555
+ {this._renderHeader()}
556
+
557
+ {renderTranscriptButtons && this._renderSkipTranscriptButton()}
558
+ <div
559
+ className={styles.body}
560
+ onScroll={this._onScroll}
561
+ ref={node => {
562
+ this._transcriptListRef = node;
563
+ }}
564
+ data-testid="transcript_list">
565
+ {isLoading ? this._renderLoading() : this._renderTranscript()}
566
+ </div>
567
+ {renderTranscriptButtons && this._renderScrollToButton()}
446
568
  </div>
447
- {renderTranscriptButtons && this._renderScrollToButton()}
448
- </div>
449
- )}
450
- </div>
569
+ )}
570
+ </div>
571
+ </ScreenReaderProvider>
451
572
  );
452
573
  }
453
574
  }