@24i/bigscreen-sdk 1.0.7-alpha.2159 → 1.0.7-alpha.2161

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": "@24i/bigscreen-sdk",
3
- "version": "1.0.7-alpha.2159",
3
+ "version": "1.0.7-alpha.2161",
4
4
  "description": "SmartApps BIGscreen SDK monorepo",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -40,6 +40,7 @@
40
40
  "@24i/player-base": "6.10.0",
41
41
  "@24i/smartapps-datalayer": "3.0.3",
42
42
  "@sentry/browser": "^7.12.1",
43
+ "@types/gtag.js": "^0.0.12",
43
44
  "@types/prop-types": "^15.7.5",
44
45
  "@types/scheduler": "^0.16.2",
45
46
  "csstype": "^3.1.1",
@@ -80,6 +81,10 @@
80
81
  },
81
82
  "exports": {
82
83
  "./adobe-heartbeat": "./packages/adobe-heartbeat/src/index.ts",
84
+ "./analytics/interface": "./packages/analytics/src/interface.ts",
85
+ "./analytics/ConsoleAnalytics": "./packages/analytics/src/clients/ConsoleAnalytics/index.ts",
86
+ "./analytics/GoogleAnalytics": "./packages/analytics/src/clients/GoogleAnalytics/index.ts",
87
+ "./analytics": "./packages/analytics/src/index.ts",
83
88
  "./animations/mock": "./packages/animations/src/__mocks__/JSAnimations.ts",
84
89
  "./animations": "./packages/animations/src/index.ts",
85
90
  "./announcement-banner": "./packages/announcement-banner/src/index.ts",
@@ -0,0 +1,65 @@
1
+ ---
2
+ id: README
3
+ title: Analytics
4
+ hide_title: true
5
+ sidebar_label: Analytics
6
+ ---
7
+
8
+ # Analytics
9
+ This package contains general implementation of analytics and implementation for these clients:
10
+ - ConsoleAnalytics - debugging analytics that dump triggers to the console.
11
+
12
+ ## analytics
13
+ Single instance `analytics` is a general manager for analytics clients. It provides a unified
14
+ interface for use by the app without it needing to know any specific analytics client implementation.
15
+ It also adds some general app info to each trigger call (e.g. resolution, language, etc.)
16
+
17
+ ## A
18
+ `A` is a shorthand utility function to simplify sending analytics information. It allows setting
19
+ a basic payload that is sent with each trigger call and shallowly merged with trigger payload.
20
+ Trigger payload can override values inside this basic payload.
21
+
22
+ ### Usage
23
+ ```typescript
24
+ import { A } from '@24i/bigscreen-sdk/analytics';
25
+
26
+ class DetailScene {
27
+ a = A.of({ sceneType: SceneType.DETAIL });
28
+
29
+ activate(params: RouteParams) {
30
+ this.a.addToPayload({
31
+ sceneId: `/detail/${params.assetType}/${params.id}`,
32
+ sceneName: `Detail - ${data.type} - [unknown]`,
33
+ asset: undefined,
34
+ });
35
+ // ...
36
+ }
37
+
38
+ async loadAndRenderData() {
39
+ this.a('buffering_start');
40
+ const data = await dataProvider();
41
+ this.a.addToPayload({
42
+ sceneName: `Detail - ${data.type} - ${data.name}`,
43
+ asset: data,
44
+ });
45
+ this.a('buffering_stop');
46
+ // render
47
+ this.a('screen_view');
48
+ }
49
+ }
50
+ ```
51
+ It is better to store shorthand `a` function inside the component instance. Some scenes can be
52
+ instantiated multiple times by the router (e.g. Browse in WL). Therefore it cannot use a single
53
+ instance from a local `./analytics'` import for example.
54
+
55
+ ## Clients
56
+ Analytics client is the integration to the specific analytics provider (e.g. Google Analytics).
57
+ It needs to implement `AnalyticsClient` interface and register its instance to receive the events.
58
+
59
+ Clients have to opt-in to events they wish to listen to. This can be done by listing all events or by
60
+ using a regular expression.
61
+
62
+ ```typescript
63
+ analytics.addClient(new GoogleAnalytics(), ['screen_view', 'buffering_start', 'buffering_stop']);
64
+ analytics.addClient(new ConsoleAnalytics(), /.*/);
65
+ ```
@@ -0,0 +1,51 @@
1
+ import { analytics } from './Analytics';
2
+ import { AnalyticsEventsMap, AnalyticsEventsNames, AnyPayload, AFunction } from './interface';
3
+
4
+ interface PrivateAFunction extends AFunction {
5
+ payload: AnyPayload,
6
+ }
7
+
8
+ export class A {
9
+ /**
10
+ * Creates an analytics 'a' function for easy triggering of events.
11
+ * @param payload basic payload that is added to each payload of the returned
12
+ * function call.
13
+ * @returns a function for triggering analytics events
14
+ * @example
15
+ * ```typescript
16
+ * // Creates a function and sets the basic payload.
17
+ * const a = A.of({ sceneType: 'HOME', sceneId: 'home' });
18
+ *
19
+ * // Sends 'screen_view' event with the basic payload.
20
+ * a('screen_view');
21
+ *
22
+ * // Sends 'buffering_stop' with shallow merge of the basic payload and { asset } object.
23
+ * a('buffering_stop', { asset });
24
+ * ```
25
+ */
26
+ static of(payload: AnyPayload) {
27
+ const a: PrivateAFunction = (<
28
+ EventName extends AnalyticsEventsNames,
29
+ P extends Partial<Parameters<AnalyticsEventsMap[EventName]>[0]>,
30
+ >(eventName: EventName, eventPayload: P) => {
31
+ analytics.triggerEvent(eventName, {
32
+ ...a.payload,
33
+ ...eventPayload,
34
+ } as unknown as Parameters<AnalyticsEventsMap[EventName]>[0]);
35
+ }) as unknown as PrivateAFunction;
36
+ a.payload = payload;
37
+ a.addToPayload = (extendedPayload: AnyPayload) => {
38
+ a.payload = {
39
+ ...a.payload,
40
+ ...extendedPayload,
41
+ };
42
+ };
43
+ a.setPayload = (newPayload: AnyPayload) => {
44
+ a.payload = newPayload;
45
+ };
46
+ a.clearPayload = () => {
47
+ a.payload = {};
48
+ };
49
+ return a as AFunction;
50
+ }
51
+ }
@@ -0,0 +1,114 @@
1
+ import { EventsManager } from '@24i/bigscreen-sdk/events-manager';
2
+ import { i18n } from '@24i/bigscreen-sdk/i18n';
3
+ import type {
4
+ AnalyticsEventsMap,
5
+ AnalyticsEventsNames,
6
+ AnalyticsClient,
7
+ GeneralAppData,
8
+ AppInfo,
9
+ UserInfo,
10
+ } from './interface';
11
+ import { EVENT_NAMES, ONLY_ONCE_EVENTS, UNREPEATABLE_EVENTS } from './constants';
12
+
13
+ class Analytics {
14
+ private events = new EventsManager<AnalyticsEventsMap>();
15
+
16
+ private onlyOnceAlreadySent: string[] = [];
17
+
18
+ private lastPayloadForEvent: Partial<Record<AnalyticsEventsNames, string>> = {};
19
+
20
+ private clients: Record<string, {
21
+ client: AnalyticsClient
22
+ registeredEvents: Partial<Record<AnalyticsEventsNames, Function>>,
23
+ }> = {};
24
+
25
+ private generalAppData: Partial<GeneralAppData> = {
26
+ language: i18n.getLanguage(),
27
+ resolution: `${window.innerWidth}x${window.innerHeight}`,
28
+ };
29
+
30
+ private appInfo: Partial<AppInfo> = {};
31
+
32
+ private userInfo: Partial<UserInfo> = {};
33
+
34
+ constructor() {
35
+ i18n.addEventListener('languagechange', this.onLanguageChange);
36
+ }
37
+
38
+ addClient(client: AnalyticsClient, events: AnalyticsEventsNames[] | RegExp) {
39
+ if (this.clients[client.name]) {
40
+ throw new Error(`${client.name} analytics client is already registered.`);
41
+ }
42
+ const eventsToRegister = Array.isArray(events)
43
+ ? events
44
+ : EVENT_NAMES.filter(events.test.bind(events));
45
+ const registeredEvents: Partial<Record<AnalyticsEventsNames, Function>> = {};
46
+ eventsToRegister.forEach((eventName) => {
47
+ const listener = (payload: any) => {
48
+ client.onEvent(eventName, payload);
49
+ };
50
+ this.events.addEventListener(eventName, listener);
51
+ registeredEvents[eventName] = listener;
52
+ });
53
+ this.clients[client.name] = {
54
+ client: client,
55
+ registeredEvents,
56
+ };
57
+ }
58
+
59
+ removeClient(client: AnalyticsClient) {
60
+ if (this.clients[client.name]) {
61
+ const { registeredEvents } = this.clients[client.name];
62
+ Object.keys(registeredEvents).forEach((eventName) => {
63
+ // @ts-ignore - specific payload typing
64
+ this.events.removeEventListener(eventName, registeredEvents[eventName]!);
65
+ });
66
+ delete this.clients[client.name];
67
+ }
68
+ }
69
+
70
+ triggerEvent<Event extends AnalyticsEventsNames>(
71
+ event: Event,
72
+ triggerPayload: Parameters<AnalyticsEventsMap[Event]>[0] = undefined,
73
+ ) {
74
+ const fullPayload = {
75
+ ...this.generalAppData,
76
+ ...this.appInfo,
77
+ ...this.userInfo,
78
+ ...triggerPayload,
79
+ };
80
+ if (ONLY_ONCE_EVENTS.includes(event)) {
81
+ const serializedPayload = `${event} - ${JSON.stringify(fullPayload)}`;
82
+ if (this.onlyOnceAlreadySent.includes(serializedPayload)) return;
83
+ this.onlyOnceAlreadySent.push(serializedPayload);
84
+ }
85
+ if (UNREPEATABLE_EVENTS.includes(event)) {
86
+ const serializedPayload = JSON.stringify(fullPayload);
87
+ if (this.lastPayloadForEvent[event] === serializedPayload) return;
88
+ this.lastPayloadForEvent[event] = serializedPayload;
89
+ }
90
+ this.tryFillUpMissingData();
91
+ // @ts-ignore - payload typing
92
+ this.events.triggerEvent(event, fullPayload);
93
+ }
94
+
95
+ setAppInfo(data: Partial<AppInfo>) {
96
+ this.appInfo = data;
97
+ }
98
+
99
+ setUserInfo(data: Partial<UserInfo>) {
100
+ this.userInfo = data;
101
+ }
102
+
103
+ private onLanguageChange = () => {
104
+ this.generalAppData.language = i18n.getLanguage();
105
+ };
106
+
107
+ private tryFillUpMissingData() {
108
+ if (!this.generalAppData.language) {
109
+ this.generalAppData.language = i18n.getLanguage();
110
+ }
111
+ }
112
+ }
113
+
114
+ export const analytics = new Analytics();
@@ -0,0 +1,11 @@
1
+ import { AFunction } from './interface';
2
+
3
+ export const analyticsOnScroll = (a: AFunction, itemsTotal: number) => (_: any, index: number) => {
4
+ const percent = (index + 1) / itemsTotal;
5
+ /* eslint-disable no-magic-numbers */
6
+ if (percent >= 0.9) a('scroll_90');
7
+ else if (percent >= 0.75) a('scroll_75');
8
+ else if (percent >= 0.50) a('scroll_50');
9
+ else if (percent >= 0.25) a('scroll_25');
10
+ /* eslint-enable no-magic-numbers */
11
+ };
@@ -0,0 +1,12 @@
1
+ import { AnalyticsClient } from '../../interface';
2
+
3
+ /**
4
+ * Simple debugging client that dumps all triggers to the console.
5
+ */
6
+ export class ConsoleAnalytics implements AnalyticsClient {
7
+ name = 'ConsoleAnalytics';
8
+
9
+ onEvent(eventName: string, payload: any) {
10
+ console.info('CA: ', eventName, payload);
11
+ }
12
+ }
@@ -0,0 +1 @@
1
+ export { ConsoleAnalytics } from './ConsoleAnalytics';
@@ -0,0 +1,84 @@
1
+ import { Storage } from '@24i/bigscreen-sdk/storage';
2
+ import { debounce } from '@24i/bigscreen-sdk/utils/debounce';
3
+ import { generateUuid } from '@24i/bigscreen-sdk/utils/generateUuid';
4
+ import type { AnalyticsClient, AnalyticsEventsMap } from '../../interface';
5
+ import { mapPayload } from './mapPayload';
6
+ import { TRIGGER_NAME_TO_GA_EVENTS } from './constants';
7
+ import { GAEventsNames } from './interface';
8
+
9
+ const SEND_DEBOUNCE_MS = 5000;
10
+ const MAX_QUEUE_LENGTH = 10;
11
+
12
+ const GA_CLIENT_ID = 'GA_client_id';
13
+
14
+ const getGAUrl = (id: string, secret: string) => (
15
+ // You can use the following URL for debugging if the events are not getting collected.
16
+ // `https://www.google-analytics.com/debug/mp/collect?measurement_id=${id}&api_secret=${secret}`
17
+ `https://www.google-analytics.com/mp/collect?measurement_id=${id}&api_secret=${secret}`
18
+ );
19
+
20
+ export class GoogleAnalytics implements AnalyticsClient {
21
+ name = 'GoogleAnalytics';
22
+
23
+ clientId = '';
24
+
25
+ sessionId = generateUuid();
26
+
27
+ a24iOperational = false;
28
+
29
+ eventsQueue: { name: GAEventsNames, params: Record<string, any> }[] = [];
30
+
31
+ constructor(private id: string, private secret: string) {
32
+ this.init();
33
+ }
34
+
35
+ async init() {
36
+ this.clientId = await Storage.getItem(GA_CLIENT_ID) ?? '';
37
+ if (!this.clientId) {
38
+ this.clientId = generateUuid();
39
+ await Storage.setItem(GA_CLIENT_ID, this.clientId);
40
+ }
41
+ }
42
+
43
+ onEvent<Event extends keyof AnalyticsEventsMap>(
44
+ triggerName: Event,
45
+ payload: Parameters<AnalyticsEventsMap[Event]>,
46
+ ): void {
47
+ const events = TRIGGER_NAME_TO_GA_EVENTS[triggerName];
48
+ events.forEach(async (event) => {
49
+ if (event === 'a24i_operational' && !this.shouldContinueWithA24iOperational()) return;
50
+ const gtagPayload = mapPayload(event, triggerName, this.sessionId, payload);
51
+ this.send(event, gtagPayload);
52
+ });
53
+ }
54
+
55
+ private send(name: GAEventsNames, params: Record<string, any>) {
56
+ this.eventsQueue.push({ name, params });
57
+ if (this.eventsQueue.length >= MAX_QUEUE_LENGTH) {
58
+ this.sendQueue();
59
+ } else {
60
+ this.sendQueueWithDelay();
61
+ }
62
+ }
63
+
64
+ private sendQueue = () => {
65
+ const body = JSON.stringify({
66
+ client_id: this.clientId,
67
+ events: this.eventsQueue,
68
+ });
69
+ this.eventsQueue.length = 0;
70
+ try {
71
+ fetch(getGAUrl(this.id, this.secret), { method: 'POST', body });
72
+ } catch (e) {
73
+ // Effort to send was made.
74
+ }
75
+ };
76
+
77
+ private sendQueueWithDelay = debounce(this.sendQueue, SEND_DEBOUNCE_MS);
78
+
79
+ private shouldContinueWithA24iOperational() {
80
+ const { a24iOperational } = this;
81
+ if (!a24iOperational) this.a24iOperational = true;
82
+ return !a24iOperational;
83
+ }
84
+ }
@@ -0,0 +1,40 @@
1
+ import { AnalyticsEventsNames } from '../../interface';
2
+ import { GAEventsNames } from './interface';
3
+
4
+ export const TRIGGER_NAME_TO_GA_EVENTS: Record<AnalyticsEventsNames, GAEventsNames[]> = {
5
+ // App
6
+ 'app_close': ['app_close', 'video_progress'],
7
+
8
+ // Data loading / Player buffering
9
+ 'buffering_start': ['buffering_start'],
10
+ 'buffering_stop': ['buffering_start'],
11
+
12
+ // Heartbeat
13
+ 'heartbeat_5': ['a24i_content'],
14
+
15
+ // Playback
16
+ 'playback_10': ['video_progress'],
17
+ 'playback_25': ['video_progress'],
18
+ 'playback_50': ['video_progress'],
19
+ 'playback_75': ['video_progress'],
20
+ 'playback_90': ['video_progress'],
21
+ 'playback_pause': ['video_progress'],
22
+ 'playback_start': ['video_start'],
23
+ 'playback_stop': ['video_progress'],
24
+
25
+ // Player
26
+ 'player_close': ['a24i_content', 'video_progress'],
27
+ 'player_open': ['a24i_content'],
28
+
29
+ // Scenes
30
+ 'screen_view': [
31
+ 'page_view',
32
+ 'a24i_operational',
33
+ ],
34
+
35
+ // Scrolling in scene
36
+ 'scroll_25': ['scroll'],
37
+ 'scroll_50': ['scroll'],
38
+ 'scroll_75': ['scroll'],
39
+ 'scroll_90': ['scroll'],
40
+ };
@@ -0,0 +1 @@
1
+ export { GoogleAnalytics } from './GoogleAnalytics';
@@ -0,0 +1,10 @@
1
+ export type GAEventsNames = (
2
+ | 'a24i_content'
3
+ | 'a24i_operational'
4
+ | 'app_close'
5
+ | 'buffering_start'
6
+ | 'page_view'
7
+ | 'scroll'
8
+ | 'video_progress'
9
+ | 'video_start'
10
+ );
@@ -0,0 +1,76 @@
1
+ import type {
2
+ AnalyticsEventsNames,
3
+ GeneralAppData,
4
+ SceneInfo,
5
+ AppInfo,
6
+ UserInfo,
7
+ } from '../../interface';
8
+ import type { GAEventsNames } from './interface';
9
+
10
+ const MILI_TO_MICRO = 10;
11
+
12
+ const camelToSnake = (name: string) => (
13
+ name.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`)
14
+ );
15
+
16
+ const addKeyIfExists = <T extends string>(
17
+ keyName: T,
18
+ payload: { [key in typeof keyName]?: any },
19
+ gtagPayload: Record<string, any>,
20
+ ) => {
21
+ if (payload[keyName]) gtagPayload[camelToSnake(keyName)] = payload[keyName];
22
+ };
23
+
24
+ const mapGeneralData = (payload: Partial<GeneralAppData>) => {
25
+ const gtagPayloadGeneralData: Partial<GeneralAppData> = {};
26
+ addKeyIfExists('language', payload, gtagPayloadGeneralData);
27
+ addKeyIfExists('resolution', payload, gtagPayloadGeneralData);
28
+ return gtagPayloadGeneralData;
29
+ };
30
+
31
+ const mapSceneInfo = (payload: Partial<SceneInfo>) => {
32
+ const gtagPayloadSceneInfo: Partial<SceneInfo> = {};
33
+ addKeyIfExists('sceneId', payload, gtagPayloadSceneInfo);
34
+ addKeyIfExists('sceneType', payload, gtagPayloadSceneInfo);
35
+ addKeyIfExists('sceneName', payload, gtagPayloadSceneInfo);
36
+ return gtagPayloadSceneInfo;
37
+ };
38
+
39
+ const mapAppData = (payload: Partial<AppInfo>) => {
40
+ const gtagPayloadAppInfo: Partial<AppInfo> = {};
41
+ addKeyIfExists('applicationId', payload, gtagPayloadAppInfo);
42
+ addKeyIfExists('serviceId', payload, gtagPayloadAppInfo);
43
+ return gtagPayloadAppInfo;
44
+ };
45
+
46
+ const mapUserData = (payload: Partial<UserInfo>) => {
47
+ const gtagPayloadUserInfo: Partial<UserInfo> = {};
48
+ addKeyIfExists('userId', payload, gtagPayloadUserInfo);
49
+ return gtagPayloadUserInfo;
50
+ };
51
+
52
+ export const mapPayload = (
53
+ eventName: GAEventsNames,
54
+ triggerName: AnalyticsEventsNames,
55
+ sessionId: string,
56
+ payload: Record<string, any>,
57
+ ) => {
58
+ let gtagPayload: Record<string, any> = {
59
+ session_id: sessionId,
60
+ event_trigger: triggerName,
61
+ timestamp_micros: Date.now() * MILI_TO_MICRO,
62
+ ...mapGeneralData(payload),
63
+ ...mapAppData(payload),
64
+ ...mapUserData(payload),
65
+ };
66
+ if (eventName === 'page_view' || eventName === 'buffering_start') {
67
+ gtagPayload = {
68
+ ...gtagPayload,
69
+ ...mapSceneInfo(payload),
70
+ };
71
+ }
72
+ if (eventName === 'scroll') {
73
+ gtagPayload.percent_scroll = parseInt(triggerName.split('_')[1], 10);
74
+ }
75
+ return gtagPayload;
76
+ };
@@ -0,0 +1,56 @@
1
+ import { AnalyticsEventsNames } from './interface';
2
+
3
+ export const EVENT_NAMES = [
4
+ // App
5
+ 'app_close',
6
+
7
+ // Data loading / Player buffering
8
+ 'buffering_start',
9
+ 'buffering_stop',
10
+
11
+ // Heartbeat
12
+ 'heartbeat_5',
13
+
14
+ // Playback
15
+ 'playback_10',
16
+ 'playback_25',
17
+ 'playback_50',
18
+ 'playback_75',
19
+ 'playback_90',
20
+ 'playback_pause',
21
+ 'playback_start',
22
+ 'playback_stop',
23
+
24
+ // Player
25
+ 'player_close',
26
+ 'player_open',
27
+
28
+ // Scenes
29
+ 'screen_view',
30
+
31
+ // Scrolling in scene
32
+ 'scroll_25',
33
+ 'scroll_50',
34
+ 'scroll_75',
35
+ 'scroll_90',
36
+ ] as const;
37
+
38
+ export const ONLY_ONCE_EVENTS: AnalyticsEventsNames[] = [
39
+ // Playback
40
+ 'playback_10',
41
+ 'playback_25',
42
+ 'playback_50',
43
+ 'playback_75',
44
+ 'playback_90',
45
+
46
+ // Scrolling in scene
47
+ 'scroll_25',
48
+ 'scroll_50',
49
+ 'scroll_75',
50
+ 'scroll_90',
51
+ ];
52
+
53
+ export const UNREPEATABLE_EVENTS: AnalyticsEventsNames[] = [
54
+ // Scenes
55
+ 'screen_view',
56
+ ];
@@ -0,0 +1,3 @@
1
+ export { analytics } from './Analytics';
2
+ export { A } from './A';
3
+ export { analyticsOnScroll } from './analyticsOnScroll';
@@ -0,0 +1,82 @@
1
+ export type SceneInfo = {
2
+ sceneId: string,
3
+ sceneType: string,
4
+ sceneName: string,
5
+ };
6
+
7
+ export type AppInfo = {
8
+ applicationId: string,
9
+ serviceId: string,
10
+ };
11
+
12
+ export type UserInfo = {
13
+ userId: string,
14
+ };
15
+
16
+ export type GeneralAppData = {
17
+ language: string,
18
+ resolution: string,
19
+ };
20
+
21
+ /**
22
+ * List of all tracked events.
23
+ */
24
+ export type AnalyticsEventsMap = {
25
+ // App
26
+ 'app_close': (payload: void) => void,
27
+
28
+ // Data loading / Player buffering
29
+ 'buffering_start': (payload: SceneInfo) => void,
30
+ 'buffering_stop': (payload: SceneInfo) => void,
31
+
32
+ // Heartbeat
33
+ 'heartbeat_5': (payload: void) => void,
34
+
35
+ // Playback
36
+ 'playback_10': (payload: void) => void,
37
+ 'playback_25': (payload: void) => void,
38
+ 'playback_50': (payload: void) => void,
39
+ 'playback_75': (payload: void) => void,
40
+ 'playback_90': (payload: void) => void,
41
+ 'playback_pause': (payload: void) => void,
42
+ 'playback_start': (payload: void) => void,
43
+ 'playback_stop': (payload: void) => void,
44
+
45
+ // Player
46
+ 'player_close': (payload: void) => void,
47
+ 'player_open': (payload: void) => void,
48
+
49
+ // Scenes
50
+ 'screen_view': (payload: SceneInfo) => void,
51
+
52
+ // Scrolling in scene
53
+ 'scroll_25': (payload: SceneInfo) => void,
54
+ 'scroll_50': (payload: SceneInfo) => void,
55
+ 'scroll_75': (payload: SceneInfo) => void,
56
+ 'scroll_90': (payload: SceneInfo) => void,
57
+ };
58
+
59
+ export type AnalyticsEventsNames = keyof AnalyticsEventsMap;
60
+
61
+ export interface AnalyticsClient {
62
+ name: string,
63
+ onEvent<Event extends keyof AnalyticsEventsMap>(
64
+ eventName: Event,
65
+ payload: Parameters<AnalyticsEventsMap[Event]>,
66
+ ): void;
67
+ }
68
+
69
+ export type AnyPayload = Record<string, any>;
70
+
71
+ export interface AFunction {
72
+ <
73
+ EventName extends AnalyticsEventsNames,
74
+ P extends Partial<Parameters<AnalyticsEventsMap[EventName]>[0]>,
75
+ >(eventName: EventName, payload?: P): void,
76
+
77
+ addToPayload(payload: AnyPayload): void,
78
+
79
+ setPayload(payload: AnyPayload): void,
80
+
81
+ clearPayload(): void,
82
+ }
@@ -312,6 +312,11 @@ Method to focus between multiple focusable components. The first existing (non-n
312
312
  const focusFirstExisting = (...toFocus: Reference<IFocusable>[]) => () => void;
313
313
  ```
314
314
 
315
+ If you need to enable `preventScroll` in element focus options, wrap your reference with `preventScrollOnFocus` function like this:
316
+ ```ts
317
+ focusFirstExisting(this.navigation, preventScrollOnFocus(this.div))
318
+ ```
319
+
315
320
  ## refocusAfterHashChange
316
321
  When the hash is changed, the browser automatically blurs the currently focused element and focuses body.
317
322
  This can be problematic when the overlay component is active (development tools, modals). Focus is lost and
@@ -1,6 +1,12 @@
1
- import { Reference } from '@24i/bigscreen-sdk/jsx';
1
+ import { createRef, Reference } from '@24i/bigscreen-sdk/jsx';
2
2
  import { IFocusable } from './IFocusable';
3
3
 
4
+ export const preventScrollOnFocus = (ref: Reference<IFocusable>) => (
5
+ createRef<IFocusable>({
6
+ focus: () => ref.current?.focus({ preventScroll: true }),
7
+ })
8
+ );
9
+
4
10
  export const focusFirstExisting = (...toFocus: Reference<IFocusable>[]) => (
5
11
  () => {
6
12
  for (let i = 0; i < toFocus.length; i++) {
@@ -11,5 +11,5 @@ export {
11
11
  setNextFocusCandidate,
12
12
  focusCandidate,
13
13
  } from './candidate';
14
- export { focusFirstExisting } from './focusFirstExisting';
14
+ export { focusFirstExisting, preventScrollOnFocus } from './focusFirstExisting';
15
15
  export { refocusAfterHashChange } from './refocusAfterHashChange';
@@ -2,8 +2,6 @@ import { Reference, unwrapReference } from '@24i/bigscreen-sdk/jsx';
2
2
  import { isRtl } from '@24i/bigscreen-sdk/i18n';
3
3
  import { SCREEN_WIDTH } from './sizes';
4
4
 
5
- type Condition = (wrapperElement: HTMLElement) => boolean;
6
-
7
5
  const SAFE_TIMEOUT = 100;
8
6
 
9
7
  export const isElementVisible = (
@@ -38,15 +36,15 @@ export const isElementFullyVisible = (
38
36
  export const isElementWrappedBy = (
39
37
  elementOrReference: HTMLElement | Reference<HTMLElement>,
40
38
  wrapperElementOrReferenceOrCondition:
41
- HTMLElement | Reference<HTMLElement> | Condition,
39
+ HTMLElement | Reference<HTMLElement> | ((currentWrapper: HTMLElement) => boolean),
42
40
  includesElementItself = false,
43
41
  ) => {
44
42
  const element = unwrapReference(elementOrReference);
45
43
  let currentParent = includesElementItself ? element : element.parentElement;
46
44
  let wrapperElement: HTMLElement | null = null;
47
- let condition: Condition | null = null;
45
+ let condition: ((currentWrapper: HTMLElement) => boolean) | null = null;
48
46
  if (typeof wrapperElementOrReferenceOrCondition === 'function') {
49
- condition = wrapperElementOrReferenceOrCondition as Condition;
47
+ condition = wrapperElementOrReferenceOrCondition;
50
48
  } else {
51
49
  wrapperElement = unwrapReference(wrapperElementOrReferenceOrCondition);
52
50
  if (!wrapperElement) return false;