@playkit-js/transcript 3.0.1-canary.44-4c445bb → 3.0.1

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,27 +1,8 @@
1
1
  import {h, Component} from 'preact';
2
- import {A11yWrapper, OnClickEvent} from '@playkit-js/common';
3
2
  import * as styles from './search.scss';
4
3
  import {debounce} from '../../utils';
5
4
  const DEBOUNCE_TIMEOUT = 300;
6
5
 
7
- const {withText, Text} = KalturaPlayer.ui.preacti18n;
8
- const translates = ({activeSearchIndex, totalSearchResults}: SearchProps) => ({
9
- searchLabel: <Text id="transcript.search">Search in Transcript</Text>,
10
- clearSearchLabel: <Text id="transcript.clear_search">Clear search</Text>,
11
- nextMatchLabel: <Text id="transcript.next_search_match">Next</Text>,
12
- prevMatchLabel: <Text id="transcript.prev_search_match">Previous</Text>,
13
- searchResultsLabel: (
14
- <Text
15
- id="transcript.prev_search_match"
16
- fields={{
17
- current: totalSearchResults > 0 ? activeSearchIndex : 0,
18
- total: totalSearchResults
19
- }}>
20
- {`Result ${totalSearchResults > 0 ? activeSearchIndex : 0} of ${totalSearchResults}`}
21
- </Text>
22
- )
23
- });
24
-
25
6
  export interface SearchProps {
26
7
  onChange(value: string): void;
27
8
  searchQuery: string;
@@ -32,12 +13,6 @@ export interface SearchProps {
32
13
  value: string;
33
14
  activeSearchIndex: number;
34
15
  totalSearchResults: number;
35
-
36
- searchLabel?: string;
37
- clearSearchLabel?: string;
38
- nextMatchLabel?: string;
39
- prevMatchLabel?: string;
40
- searchResultsLabel?: string;
41
16
  }
42
17
 
43
18
  interface SearchState {
@@ -45,7 +20,7 @@ interface SearchState {
45
20
  focused: boolean;
46
21
  }
47
22
 
48
- class SearchComponent extends Component<SearchProps, SearchState> {
23
+ export class Search extends Component<SearchProps, SearchState> {
49
24
  state: SearchState = {
50
25
  active: false,
51
26
  focused: false
@@ -86,8 +61,8 @@ class SearchComponent extends Component<SearchProps, SearchState> {
86
61
  this.props.onChange(e.target.value);
87
62
  };
88
63
 
89
- private _onClear = (event: OnClickEvent, byKeyboard?: boolean) => {
90
- if (!byKeyboard) {
64
+ private _onClear = (event: MouseEvent) => {
65
+ if (event.x !== 0 && event.y !== 0) {
91
66
  this._focusedByMouse = true;
92
67
  }
93
68
  this._inputRef?.focus();
@@ -160,8 +135,7 @@ class SearchComponent extends Component<SearchProps, SearchState> {
160
135
  </div>
161
136
  <input
162
137
  className={styles.searchInput}
163
- aria-label={this.props.searchLabel}
164
- placeholder={this.props.searchLabel}
138
+ placeholder={'Search in Transcript'}
165
139
  value={searchQuery}
166
140
  onInput={this._handleOnChange}
167
141
  onFocus={this._onFocus}
@@ -173,82 +147,72 @@ class SearchComponent extends Component<SearchProps, SearchState> {
173
147
  }}
174
148
  />
175
149
  {searchQuery && (
176
- <A11yWrapper onClick={this._onClear}>
177
- <button className={styles.clearIcon} tabIndex={1} aria-label={this.props.clearSearchLabel}>
150
+ <button className={styles.clearIcon} onClick={this._onClear} tabIndex={1}>
151
+ <svg
152
+ width="32px"
153
+ height="32px"
154
+ viewBox="0 0 32 32"
155
+ version="1.1"
156
+ xmlns="http://www.w3.org/2000/svg"
157
+ xmlnsXlink="http://www.w3.org/1999/xlink">
158
+ <g id="Icons/32/Clere" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
159
+ <path
160
+ d="M16,8 C20.418278,8 24,11.581722 24,16 C24,20.418278 20.418278,24 16,24 C11.581722,24 8,20.418278 8,16 C8,11.581722 11.581722,8 16,8 Z M19.8665357,12.1334643 C19.6885833,11.9555119 19.4000655,11.9555119 19.2221131,12.1334643 L16,15.356 L12.7778869,12.1334643 L12.7064039,12.0750737 C12.5295326,11.9582924 12.2891726,11.977756 12.1334643,12.1334643 L12.0750737,12.2049473 C11.9582924,12.3818186 11.977756,12.6221786 12.1334643,12.7778869 L15.356,16 L12.1334643,19.2221131 C11.9555119,19.4000655 11.9555119,19.6885833 12.1334643,19.8665357 C12.3114167,20.0444881 12.5999345,20.0444881 12.7778869,19.8665357 L16,16.644 L19.2221131,19.8665357 L19.2935961,19.9249263 C19.4704674,20.0417076 19.7108274,20.022244 19.8665357,19.8665357 L19.9249263,19.7950527 C20.0417076,19.6181814 20.022244,19.3778214 19.8665357,19.2221131 L16.644,16 L19.8665357,12.7778869 C20.0444881,12.5999345 20.0444881,12.3114167 19.8665357,12.1334643 Z"
161
+ id="Shape"
162
+ fill="#cccccc"></path>
163
+ </g>
164
+ </svg>
165
+ </button>
166
+ )}
167
+ {searchQuery && (
168
+ <div className={styles.searchResults}>{`${totalSearchResults > 0 ? `${activeSearchIndex}/${totalSearchResults}` : '0/0'}`}</div>
169
+ )}
170
+ <div className={styles.prevNextWrapper}>
171
+ {searchQuery && (
172
+ <button
173
+ tabIndex={1}
174
+ className={`${styles.prevNextButton} ${totalSearchResults === 0 ? styles.disabled : ''}`}
175
+ onClick={this._goToPrevSearchResult}>
178
176
  <svg
179
- width="32px"
180
- height="32px"
181
- viewBox="0 0 32 32"
177
+ width="14px"
178
+ height="12px"
179
+ viewBox="1 0 14 12"
182
180
  version="1.1"
183
181
  xmlns="http://www.w3.org/2000/svg"
184
182
  xmlnsXlink="http://www.w3.org/1999/xlink">
185
- <g id="Icons/32/Clere" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
183
+ <g id="Icons/16/Arrow/-up" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
186
184
  <path
187
- d="M16,8 C20.418278,8 24,11.581722 24,16 C24,20.418278 20.418278,24 16,24 C11.581722,24 8,20.418278 8,16 C8,11.581722 11.581722,8 16,8 Z M19.8665357,12.1334643 C19.6885833,11.9555119 19.4000655,11.9555119 19.2221131,12.1334643 L16,15.356 L12.7778869,12.1334643 L12.7064039,12.0750737 C12.5295326,11.9582924 12.2891726,11.977756 12.1334643,12.1334643 L12.0750737,12.2049473 C11.9582924,12.3818186 11.977756,12.6221786 12.1334643,12.7778869 L15.356,16 L12.1334643,19.2221131 C11.9555119,19.4000655 11.9555119,19.6885833 12.1334643,19.8665357 C12.3114167,20.0444881 12.5999345,20.0444881 12.7778869,19.8665357 L16,16.644 L19.2221131,19.8665357 L19.2935961,19.9249263 C19.4704674,20.0417076 19.7108274,20.022244 19.8665357,19.8665357 L19.9249263,19.7950527 C20.0417076,19.6181814 20.022244,19.3778214 19.8665357,19.2221131 L16.644,16 L19.8665357,12.7778869 C20.0444881,12.5999345 20.0444881,12.3114167 19.8665357,12.1334643 Z"
188
- id="Shape"
189
- fill="#cccccc"></path>
185
+ d="M4.78325732,5.37830235 C4.43990319,4.94572127 3.81088342,4.87338855 3.37830235,5.21674268 C2.94572127,5.56009681 2.87338855,6.18911658 3.21674268,6.62169765 L7.21674268,11.6611718 C7.61710439,12.165575 8.38289561,12.165575 8.78325732,11.6611718 L12.7832573,6.62169765 C13.1266115,6.18911658 13.0542787,5.56009681 12.6216977,5.21674268 C12.1891166,4.87338855 11.5600968,4.94572127 11.2167427,5.37830235 L8,9.43097528 L4.78325732,5.37830235 Z"
186
+ id="Path-2"
187
+ fill="#cccccc"
188
+ transform="translate(8.000000, 8.519717) scale(1, -1) translate(-8.000000, -8.519717) "></path>
190
189
  </g>
191
190
  </svg>
192
191
  </button>
193
- </A11yWrapper>
194
- )}
195
- {searchQuery && (
196
- <div className={styles.searchResults} aria-live="polite" aria-label={this.props.searchResultsLabel}>{`${
197
- totalSearchResults > 0 ? `${activeSearchIndex}/${totalSearchResults}` : '0/0'
198
- }`}</div>
199
- )}
200
- <div className={styles.prevNextWrapper}>
201
- {searchQuery && (
202
- <A11yWrapper onClick={this._goToPrevSearchResult}>
203
- <button
204
- tabIndex={1}
205
- className={`${styles.prevNextButton} ${totalSearchResults === 0 ? styles.disabled : ''}`}
206
- aria-label={this.props.prevMatchLabel}>
207
- <svg
208
- width="14px"
209
- height="12px"
210
- viewBox="1 0 14 12"
211
- version="1.1"
212
- xmlns="http://www.w3.org/2000/svg"
213
- xmlnsXlink="http://www.w3.org/1999/xlink">
214
- <g id="Icons/16/Arrow/-up" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
215
- <path
216
- d="M4.78325732,5.37830235 C4.43990319,4.94572127 3.81088342,4.87338855 3.37830235,5.21674268 C2.94572127,5.56009681 2.87338855,6.18911658 3.21674268,6.62169765 L7.21674268,11.6611718 C7.61710439,12.165575 8.38289561,12.165575 8.78325732,11.6611718 L12.7832573,6.62169765 C13.1266115,6.18911658 13.0542787,5.56009681 12.6216977,5.21674268 C12.1891166,4.87338855 11.5600968,4.94572127 11.2167427,5.37830235 L8,9.43097528 L4.78325732,5.37830235 Z"
217
- id="Path-2"
218
- fill="#cccccc"
219
- transform="translate(8.000000, 8.519717) scale(1, -1) translate(-8.000000, -8.519717) "></path>
220
- </g>
221
- </svg>
222
- </button>
223
- </A11yWrapper>
224
192
  )}
225
193
  {searchQuery && (
226
- <A11yWrapper onClick={this._goToNextSearchResult}>
227
- <button
228
- tabIndex={1}
229
- className={`${styles.prevNextButton} ${totalSearchResults === 0 ? styles.disabled : ''}`}
230
- aria-label={this.props.nextMatchLabel}>
231
- <svg
232
- width="14px"
233
- height="12px"
234
- viewBox="1 2 14 12"
235
- version="1.1"
236
- xmlns="http://www.w3.org/2000/svg"
237
- xmlnsXlink="http://www.w3.org/1999/xlink">
238
- <g id="Icons/16/Arrow/down" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
239
- <path
240
- d="M4.78325732,5.37830235 C4.43990319,4.94572127 3.81088342,4.87338855 3.37830235,5.21674268 C2.94572127,5.56009681 2.87338855,6.18911658 3.21674268,6.62169765 L7.21674268,11.6611718 C7.61710439,12.165575 8.38289561,12.165575 8.78325732,11.6611718 L12.7832573,6.62169765 C13.1266115,6.18911658 13.0542787,5.56009681 12.6216977,5.21674268 C12.1891166,4.87338855 11.5600968,4.94572127 11.2167427,5.37830235 L8,9.43097528 L4.78325732,5.37830235 Z"
241
- id="Path-2"
242
- fill="#cccccc"></path>
243
- </g>
244
- </svg>
245
- </button>
246
- </A11yWrapper>
194
+ <button
195
+ tabIndex={1}
196
+ className={`${styles.prevNextButton} ${totalSearchResults === 0 ? styles.disabled : ''}`}
197
+ onClick={this._goToNextSearchResult}>
198
+ <svg
199
+ width="14px"
200
+ height="12px"
201
+ viewBox="1 2 14 12"
202
+ version="1.1"
203
+ xmlns="http://www.w3.org/2000/svg"
204
+ xmlnsXlink="http://www.w3.org/1999/xlink">
205
+ <g id="Icons/16/Arrow/down" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
206
+ <path
207
+ d="M4.78325732,5.37830235 C4.43990319,4.94572127 3.81088342,4.87338855 3.37830235,5.21674268 C2.94572127,5.56009681 2.87338855,6.18911658 3.21674268,6.62169765 L7.21674268,11.6611718 C7.61710439,12.165575 8.38289561,12.165575 8.78325732,11.6611718 L12.7832573,6.62169765 C13.1266115,6.18911658 13.0542787,5.56009681 12.6216977,5.21674268 C12.1891166,4.87338855 11.5600968,4.94572127 11.2167427,5.37830235 L8,9.43097528 L4.78325732,5.37830235 Z"
208
+ id="Path-2"
209
+ fill="#cccccc"></path>
210
+ </g>
211
+ </svg>
212
+ </button>
247
213
  )}
248
214
  </div>
249
215
  </div>
250
216
  );
251
217
  }
252
218
  }
253
-
254
- export const Search = withText(translates)(SearchComponent);
@@ -8,13 +8,7 @@ import {CaptionList} from '../caption-list';
8
8
  import {HighlightedMap, CuePointData, PluginPositions} from '../../types';
9
9
  import {CloseButton} from '../close-button';
10
10
  import {ErrorIcon} from './error-icon';
11
-
12
- const {ENTER, SPACE, TAB, ESC} = KalturaPlayer.ui.utils.KeyMap;
13
- const {withText, Text} = KalturaPlayer.ui.preacti18n;
14
-
15
- const translates = {
16
- autoScrollLabel: <Text id="transcript.auto_scroll">Enable auto scroll</Text>
17
- };
11
+ const {ENTER, Space, Tab, Esc} = KalturaPlayer.ui.utils.KeyMap;
18
12
 
19
13
  export interface TranscriptProps {
20
14
  onSeek(time: number): void;
@@ -34,7 +28,6 @@ export interface TranscriptProps {
34
28
  highlightedMap: HighlightedMap;
35
29
  pluginMode: PluginPositions;
36
30
  onItemClicked: (n: number) => void;
37
- autoScrollLabel?: string;
38
31
  }
39
32
 
40
33
  interface TranscriptState {
@@ -57,7 +50,7 @@ const initialSearch = {
57
50
 
58
51
  const SEARCHBAR_HEIGHT = 38; // height of search bar with margins
59
52
 
60
- export class TranscriptComponent extends Component<TranscriptProps, TranscriptState> {
53
+ export class Transcript extends Component<TranscriptProps, TranscriptState> {
61
54
  private _transcriptListRef: HTMLElement | null = null;
62
55
  private _captionListRef: any = null;
63
56
  private _skipTranscriptButtonRef: HTMLDivElement | null = null;
@@ -113,7 +106,6 @@ export class TranscriptComponent extends Component<TranscriptProps, TranscriptSt
113
106
  role="button"
114
107
  className={`${styles.autoscrollButton} ${isAutoScrollEnabled ? '' : styles.autoscrollButtonVisible}`}
115
108
  tabIndex={isAutoScrollEnabled ? -1 : 1}
116
- aria-label={this.props.autoScrollLabel}
117
109
  ref={node => {
118
110
  this._autoscrollButtonRef = node;
119
111
  }}>
@@ -209,13 +201,8 @@ export class TranscriptComponent extends Component<TranscriptProps, TranscriptSt
209
201
  );
210
202
  };
211
203
 
212
- private _handleClick = (event: MouseEvent | KeyboardEvent) => {
213
- event.preventDefault();
214
- this._autoscrollButtonRef?.focus();
215
- };
216
-
217
204
  private _handleKeyDown = (event: KeyboardEvent) => {
218
- if (event.keyCode === TAB && !event.shiftKey) {
205
+ if (event.keyCode === Tab && !event.shiftKey) {
219
206
  this.setState({
220
207
  isAutoScrollEnabled: false
221
208
  });
@@ -224,8 +211,9 @@ export class TranscriptComponent extends Component<TranscriptProps, TranscriptSt
224
211
  event.preventDefault();
225
212
  captionRef.focus();
226
213
  }
227
- } else if (event.keyCode === ENTER || event.keyCode === SPACE) {
228
- this._handleClick(event);
214
+ } else if (event.keyCode === ENTER || event.keyCode === Space) {
215
+ event.preventDefault();
216
+ this._autoscrollButtonRef?.focus();
229
217
  }
230
218
  };
231
219
 
@@ -238,7 +226,6 @@ export class TranscriptComponent extends Component<TranscriptProps, TranscriptSt
238
226
  }}
239
227
  className={styles.skipTranscriptButton}
240
228
  onKeyDown={this._handleKeyDown}
241
- onClick={this._handleClick}
242
229
  tabIndex={1}>
243
230
  Skip transcript
244
231
  </div>
@@ -364,7 +351,7 @@ export class TranscriptComponent extends Component<TranscriptProps, TranscriptSt
364
351
  };
365
352
 
366
353
  private _handleEsc = (event: KeyboardEvent) => {
367
- if (event.keyCode === ESC) {
354
+ if (event.keyCode === Esc) {
368
355
  this.props.onClose();
369
356
  }
370
357
  };
@@ -399,5 +386,3 @@ export class TranscriptComponent extends Component<TranscriptProps, TranscriptSt
399
386
  );
400
387
  }
401
388
  }
402
-
403
- export const Transcript = withText(translates)(TranscriptComponent);
@@ -10,6 +10,14 @@ import {DownloadPrintMenu, downloadContent, printContent} from './components/dow
10
10
 
11
11
  const {SidePanelModes, SidePanelPositions, ReservedPresetNames, ReservedPresetAreas} = ui;
12
12
  const {get} = ObjectUtils;
13
+ const {Tooltip} = KalturaPlayer.ui.components;
14
+ const {withText, Text} = KalturaPlayer.ui.preacti18n;
15
+
16
+ const translates = () => ({
17
+ printDownloadAreaLabel: <Text id="transcript.print_download_area_label">Download or print current transcript</Text>,
18
+ printTranscript: <Text id="transcript.print_transcript">Print current transcript</Text>,
19
+ downloadTranscript: <Text id="transcript.download_transcript">Download current transcript</Text>
20
+ });
13
21
 
14
22
  interface TimedMetadataEvent {
15
23
  payload: {
@@ -112,15 +120,14 @@ export class TranscriptPlugin extends KalturaPlayer.core.BasePlugin {
112
120
  };
113
121
 
114
122
  private _onTimedMetadataChange = ({payload}: TimedMetadataEvent) => {
115
- const transcriptCuePoints: Array<CuePoint> = payload.cues
116
- .filter((cue: CuePoint) => {
117
- return cue.metadata.cuePointType === ItemTypes.Caption;
123
+ const transcriptCuePoints: Array<CuePoint> = payload.cues.filter((cue: CuePoint) => {
124
+ return cue.metadata.cuePointType === ItemTypes.Caption;
118
125
  })
119
126
  .filter((cue, index, array) => {
120
127
  // filter out captions that has endTime eq to next caption startTime
121
128
  const nextCue = array[index + 1];
122
129
  return !nextCue || cue.endTime !== nextCue.startTime;
123
- });
130
+ });
124
131
  this._activeCuePointsMap = {};
125
132
  transcriptCuePoints.forEach(cue => {
126
133
  this._activeCuePointsMap[cue.id] = true;
@@ -159,14 +166,17 @@ export class TranscriptPlugin extends KalturaPlayer.core.BasePlugin {
159
166
  label: 'Download or print transcript',
160
167
  area: ReservedPresetAreas.TopBarRightControls,
161
168
  presets: [ReservedPresetNames.Playback, ReservedPresetNames.Live],
162
- get: () => (
169
+ get: withText(translates)(({printDownloadAreaLabel, printTranscript, downloadTranscript}: Record<string, string>) => (
163
170
  <DownloadPrintMenu
164
171
  onDownload={this._handleDownload}
165
172
  onPrint={this._handlePrint}
166
173
  downloadDisabled={getConfigValue(downloadDisabled, isBoolean, false)}
167
174
  printDisabled={getConfigValue(printDisabled, isBoolean, false)}
175
+ dropdownAriaLabel={printDownloadAreaLabel}
176
+ printButtonAriaLabel={printTranscript}
177
+ downloadButtonAriaLabel={downloadTranscript}
168
178
  />
169
- )
179
+ ))
170
180
  });
171
181
  }
172
182
 
@@ -204,18 +214,21 @@ export class TranscriptPlugin extends KalturaPlayer.core.BasePlugin {
204
214
  },
205
215
  iconComponent: ({isActive}: {isActive: boolean}) => {
206
216
  return (
207
- <PluginButton
208
- isActive={isActive}
209
- onClick={(e: OnClickEvent, byKeyboard?: boolean) => {
210
- if (this.sidePanelsManager.isItemActive(this._transcriptPanel)) {
211
- this._triggeredByKeyboard = false;
212
- this._handleCloseClick();
213
- } else {
214
- this._triggeredByKeyboard = Boolean(byKeyboard);
215
- this.sidePanelsManager.activateItem(this._transcriptPanel);
216
- }
217
- }}
218
- />
217
+ <Tooltip label={buttonLabel} type="bottom">
218
+ <PluginButton
219
+ isActive={isActive}
220
+ label={buttonLabel}
221
+ onClick={(e: OnClickEvent, byKeyboard?: boolean) => {
222
+ if (this.sidePanelsManager.isItemActive(this._transcriptPanel)) {
223
+ this._triggeredByKeyboard = false;
224
+ this._handleCloseClick();
225
+ } else {
226
+ this._triggeredByKeyboard = Boolean(byKeyboard);
227
+ this.sidePanelsManager.activateItem(this._transcriptPanel);
228
+ }
229
+ }}
230
+ />
231
+ </Tooltip>
219
232
  );
220
233
  },
221
234
  presets: [ReservedPresetNames.Playback, ReservedPresetNames.Live, ReservedPresetNames.Ads],
@@ -1,3 +1,4 @@
1
1
  export * from './utils';
2
2
  export * from './object-utils';
3
3
  export * from './debounce';
4
+ export * from './popover/popover';
@@ -0,0 +1,30 @@
1
+ .popover-container {
2
+ position: relative;
3
+ .popover-component {
4
+ background-color: #222222;
5
+ border-radius: 4px;
6
+ position: absolute;
7
+ right: 0px;
8
+ font-size: 15px;
9
+ display: block;
10
+ &.visible {
11
+ visibility: visible;
12
+ opacity: 1;
13
+ z-index: 10;
14
+ }
15
+ &.top {
16
+ bottom: 100%;
17
+ margin-bottom: 6px;
18
+ }
19
+ &.bottom {
20
+ top: 100%;
21
+ margin-top: 6px;
22
+ }
23
+ &.right {
24
+ left: 0px;
25
+ }
26
+ &.left {
27
+ right: 0px;
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,178 @@
1
+ import {h, Component, ComponentChild} from 'preact';
2
+ import * as styles from './popover.scss';
3
+
4
+ const {ENTER, Esc} = KalturaPlayer.ui.utils.KeyMap;
5
+
6
+ export enum PopoverVerticalPositions {
7
+ Top = 'top',
8
+ Bottom = 'bottom'
9
+ }
10
+ export enum PopoverHorizontalPositions {
11
+ Left = 'left',
12
+ Right = 'right'
13
+ }
14
+ export enum PopoverTriggerMode {
15
+ Click = 'click',
16
+ Hover = 'hover'
17
+ }
18
+
19
+ const CLOSE_ON_HOVER_DELAY = 500;
20
+
21
+ const defaultProps = {
22
+ verticalPosition: PopoverVerticalPositions.Top,
23
+ horizontalPosition: PopoverHorizontalPositions.Left,
24
+ triggerMode: PopoverTriggerMode.Click,
25
+ className: 'popover',
26
+ closeOnEsc: true,
27
+ closeOnClick: true
28
+ };
29
+
30
+ interface PopoverProps {
31
+ onClose?: () => void;
32
+ onOpen?: () => void;
33
+ closeOnClick: boolean;
34
+ closeOnEsc: boolean;
35
+ verticalPosition: PopoverVerticalPositions;
36
+ horizontalPosition: PopoverHorizontalPositions;
37
+ className: string;
38
+ triggerMode: PopoverTriggerMode;
39
+ content: ComponentChild;
40
+ children: ComponentChild;
41
+ }
42
+
43
+ interface PopoverState {
44
+ open: boolean;
45
+ }
46
+
47
+ export class Popover extends Component<PopoverProps, PopoverState> {
48
+ private _closeTimeout: any = null;
49
+ private _controlElement: HTMLDivElement | null = null;
50
+ static defaultProps = {
51
+ ...defaultProps
52
+ };
53
+ state = {
54
+ open: false
55
+ };
56
+
57
+ componentWillUnmount() {
58
+ this._removeListeners();
59
+ }
60
+
61
+ private _clearTimeout = () => {
62
+ clearTimeout(this._closeTimeout);
63
+ this._closeTimeout = null;
64
+ };
65
+
66
+ private _handleMouseEvent = (event: MouseEvent) => {
67
+ if (!this._controlElement?.contains(event.target as Node | null) && this.props.closeOnClick) {
68
+ this._closePopover();
69
+ }
70
+ };
71
+
72
+ private _handleKeyboardEvent = (event: KeyboardEvent) => {
73
+ if (this._controlElement?.contains(event.target as Node | null) && event.keyCode === ENTER) {
74
+ // handle Enter key pressed on Target icon to prevent triggering of _closePopover twice
75
+ return;
76
+ }
77
+ if ((this.props.closeOnEsc && event.keyCode === Esc) || event.keyCode === ENTER) {
78
+ // handle if ESC or Enter button presesd
79
+ this._closePopover();
80
+ }
81
+ };
82
+
83
+ private _openPopover = () => {
84
+ const {onOpen} = this.props;
85
+ this._clearTimeout();
86
+ this.setState({open: true}, () => {
87
+ this._addListeners();
88
+ if (onOpen) {
89
+ onOpen();
90
+ }
91
+ });
92
+ };
93
+
94
+ private _closePopover = () => {
95
+ const {onClose} = this.props;
96
+ this._clearTimeout();
97
+ this.setState({open: false}, () => {
98
+ this._removeListeners();
99
+ if (onClose) {
100
+ onClose();
101
+ }
102
+ });
103
+ };
104
+
105
+ private _togglePopover = (e: MouseEvent | KeyboardEvent) => {
106
+ if (this.state.open) {
107
+ this._closePopover();
108
+ } else {
109
+ this._openPopover();
110
+ }
111
+ };
112
+ private _handleMouseEnter = () => {
113
+ if (!this.state.open) {
114
+ this._openPopover();
115
+ }
116
+ };
117
+ private _handleMouseLeave = () => {
118
+ this._closeTimeout = setTimeout(this._closePopover, CLOSE_ON_HOVER_DELAY);
119
+ };
120
+ private _handleHoverOnPopover = () => {
121
+ if (this.state.open && this._closeTimeout) {
122
+ this._clearTimeout();
123
+ } else {
124
+ this._closePopover();
125
+ }
126
+ };
127
+ private _addListeners = () => {
128
+ document.addEventListener('click', this._handleMouseEvent);
129
+ document.addEventListener('keydown', this._handleKeyboardEvent);
130
+ };
131
+ private _removeListeners = () => {
132
+ document.removeEventListener('click', this._handleMouseEvent);
133
+ document.removeEventListener('keydown', this._handleKeyboardEvent);
134
+ };
135
+ private _getHoverEvents = () => {
136
+ if (this.props.triggerMode === PopoverTriggerMode.Hover) {
137
+ return {
138
+ targetEvents: {
139
+ onMouseEnter: this._handleMouseEnter,
140
+ onMouseLeave: this._handleMouseLeave
141
+ },
142
+ popoverEvents: {
143
+ onMouseEnter: this._handleHoverOnPopover,
144
+ onMouseLeave: this._handleHoverOnPopover
145
+ }
146
+ };
147
+ }
148
+ return {targetEvents: {onClick: this._togglePopover}, popoverEvents: {}};
149
+ };
150
+ render(props: PopoverProps) {
151
+ if (!props.content || !props.children) {
152
+ return null;
153
+ }
154
+ const {targetEvents, popoverEvents} = this._getHoverEvents();
155
+ return (
156
+ <div className={styles.popoverContainer}>
157
+ <div
158
+ className="popover-anchor-container"
159
+ ref={node => {
160
+ this._controlElement = node;
161
+ }}
162
+ {...targetEvents}
163
+ >
164
+ {props.children}
165
+ </div>
166
+ {this.state.open && (
167
+ <div
168
+ aria-expanded="true"
169
+ className={[props.className, styles.popoverComponent, styles[props.verticalPosition], styles[props.horizontalPosition]].join(' ')}
170
+ {...popoverEvents}
171
+ >
172
+ {props.content}
173
+ </div>
174
+ )}
175
+ </div>
176
+ );
177
+ }
178
+ }
@@ -1 +0,0 @@
1
- export * from "./popover";
@@ -1,43 +0,0 @@
1
- @import '../../variables.scss';
2
-
3
- .popover-container {
4
- position: relative;
5
- .popover-component {
6
- background-color: #222222;
7
- border-radius: 4px;
8
- position: absolute;
9
- right: 0px;
10
- font-size: 15px;
11
- display: block;
12
- &.hidden {
13
- display: none;
14
- }
15
- &.bottom {
16
- top: 100%;
17
- margin-top: 6px;
18
- }
19
- &.left {
20
- right: 0px;
21
- }
22
- }
23
- }
24
- .popover-menu {
25
- padding-top: 6px;
26
- padding-bottom: 6px;
27
- }
28
- .popover-menu-item {
29
- display: flex;
30
- align-items: center;
31
- padding: 9px 24px 9px 16px;
32
- white-space: nowrap;
33
- min-height: 30px;
34
- line-height: 18px;
35
- font-size: 15px;
36
- &:hover {
37
- cursor: pointer;
38
- background-color: $semigray-color;
39
- }
40
- &:focus {
41
- outline: 1px solid $focus-color;
42
- }
43
- }