@24i/bigscreen-sdk 1.0.3-alpha.2091 → 1.0.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.
- package/package.json +1 -1
- package/packages/focus/README.md +24 -3
- package/packages/focus/src/IFocusable.ts +2 -1
- package/packages/focus/src/Layout/Base.tsx +5 -1
- package/packages/focus/src/Layout/Horizontal.ts +19 -6
- package/packages/focus/src/Layout/__test__/Base.spec.ts +19 -1
- package/packages/focus/src/Layout/__test__/Horizontal.spec.ts +49 -1
- package/packages/focus/src/Layout/__test__/shared.ts +4 -0
package/package.json
CHANGED
package/packages/focus/README.md
CHANGED
|
@@ -8,6 +8,18 @@ sidebar_label: Focus
|
|
|
8
8
|
# Focus
|
|
9
9
|
This package takes care of focus in SmartApps BIGscreen apps. It contains layout components and modules for handling focus candidates and focus limiting.
|
|
10
10
|
|
|
11
|
+
## IFocusable
|
|
12
|
+
IFocusable is an interface that should be implemented by anything that can be focused.
|
|
13
|
+
```ts
|
|
14
|
+
interface IFocusable {
|
|
15
|
+
focus(options?: FocusOptions): void,
|
|
16
|
+
hasDom?(element: HTMLElement): boolean,
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
- focus is the main function that is called when focusing component/element.
|
|
20
|
+
- hasDom is optional optimization for components to help Layout components locate the currently
|
|
21
|
+
focused child.
|
|
22
|
+
|
|
11
23
|
## Layout components
|
|
12
24
|
- Vertical: Use it to create focus between elements vertically (up/down)
|
|
13
25
|
- Horizontal: Use it to create focus between elements horizontally (left/right)
|
|
@@ -20,9 +32,18 @@ Both of these components share the same props;
|
|
|
20
32
|
### Props
|
|
21
33
|
- children: Children will be rendered as is.
|
|
22
34
|
- focusableChildren: Array of references between which the focus moves.
|
|
23
|
-
- domRef: Reference to a dom element that corresponds to closest dom wrapper.
|
|
24
|
-
|
|
25
|
-
-
|
|
35
|
+
- domRef: Reference to a dom element that corresponds to closest dom wrapper.
|
|
36
|
+
It will listen onKeyDown on this element.
|
|
37
|
+
- onFocusChanged: Optional callback after focus has changed. It receives a reference
|
|
38
|
+
to the focused element and its index. Ideal place to change other elements
|
|
39
|
+
(e.g. header changing with focused movie)
|
|
40
|
+
- onBeforeFocusChange: Optional callback just before focus change. It receives a reference
|
|
41
|
+
to the focused element and its index. Ideal place for preparing for the change
|
|
42
|
+
(e.g. moving the container to fit new element in the screen)
|
|
43
|
+
- dir: Optional forceful setting of the component direction. Defaults to `'auto'` which follows
|
|
44
|
+
the direction of the app.
|
|
45
|
+
`'ltr'` always forces left-to-right layout,
|
|
46
|
+
`'rtl'` always forces right-to-left.
|
|
26
47
|
|
|
27
48
|
### Usage
|
|
28
49
|
```jsx
|
|
@@ -9,6 +9,7 @@ export type Props = SharedProps & {
|
|
|
9
9
|
focusableChildren: Array<Reference>,
|
|
10
10
|
onFocusChanged?: (focused: Reference, index: number) => any,
|
|
11
11
|
onBeforeFocusChange?: (focused: Reference, index: number) => any,
|
|
12
|
+
dir?: 'auto' | 'ltr' | 'rtl',
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export interface ILayout {
|
|
@@ -29,6 +30,7 @@ export const getLayoutBase = (onKeyDown: (event: KeyboardEvent, layout: ILayout)
|
|
|
29
30
|
onFocusChanged: noop,
|
|
30
31
|
onBeforeFocusChange: noop,
|
|
31
32
|
focusOptions: { preventScroll: true },
|
|
33
|
+
dir: 'auto',
|
|
32
34
|
};
|
|
33
35
|
|
|
34
36
|
declare props: DeclareProps<Props, typeof Layout.defaultProps>;
|
|
@@ -61,7 +63,9 @@ export const getLayoutBase = (onKeyDown: (event: KeyboardEvent, layout: ILayout)
|
|
|
61
63
|
) {
|
|
62
64
|
return this.index;
|
|
63
65
|
}
|
|
64
|
-
const targetIndex = focusableChildren.findIndex((
|
|
66
|
+
const targetIndex = focusableChildren.findIndex(({ current }) => (
|
|
67
|
+
current === target || current?.hasDom?.(target)
|
|
68
|
+
));
|
|
65
69
|
return targetIndex !== -1 ? targetIndex : this.index;
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { LEFT, RIGHT } from '@24i/bigscreen-sdk/device/keymap';
|
|
2
|
+
import { isRtl } from '@24i/bigscreen-sdk/i18n';
|
|
3
|
+
import { getLayoutBase, ILayout, Props } from './Base';
|
|
4
|
+
|
|
5
|
+
const isHorizontalRtl = (dir: Props['dir']) => {
|
|
6
|
+
switch (dir) {
|
|
7
|
+
case 'rtl': return true;
|
|
8
|
+
case 'ltr': return false;
|
|
9
|
+
case 'auto':
|
|
10
|
+
default:
|
|
11
|
+
return isRtl();
|
|
12
|
+
}
|
|
13
|
+
};
|
|
3
14
|
|
|
4
15
|
const onKeyDown = (e: KeyboardEvent, layout: ILayout) => {
|
|
5
|
-
if (
|
|
6
|
-
layout.backward(e);
|
|
7
|
-
|
|
8
|
-
|
|
16
|
+
if (!isHorizontalRtl(layout.props.dir)) {
|
|
17
|
+
if (LEFT.is(e)) layout.backward(e);
|
|
18
|
+
if (RIGHT.is(e)) layout.forward(e);
|
|
19
|
+
} else {
|
|
20
|
+
if (LEFT.is(e)) layout.forward(e);
|
|
21
|
+
if (RIGHT.is(e)) layout.backward(e);
|
|
9
22
|
}
|
|
10
23
|
};
|
|
11
24
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { IFocusable } from '../../IFocusable';
|
|
1
2
|
import { getLayoutBase } from '../Base';
|
|
2
|
-
import { rootRef, refChildA, refChildB, refChildC } from './shared';
|
|
3
|
+
import { rootRef, refChildA, refChildB, refChildC, refChildD } from './shared';
|
|
3
4
|
|
|
4
5
|
const mockKeydown = jest.fn();
|
|
5
6
|
const mockEvent: any = {
|
|
@@ -159,4 +160,21 @@ describe('Focus Layout Base', () => {
|
|
|
159
160
|
base.index = 2;
|
|
160
161
|
expect(() => base.focusCurrentIndex()).toThrow();
|
|
161
162
|
});
|
|
163
|
+
|
|
164
|
+
it('should take result of hasDom into account of determining current focus position', () => {
|
|
165
|
+
base = new Base({
|
|
166
|
+
domRef: rootRef,
|
|
167
|
+
children: 'children',
|
|
168
|
+
focusableChildren: [refChildA, refChildB, refChildC, refChildD],
|
|
169
|
+
onFocusChanged: jest.fn(),
|
|
170
|
+
onBeforeFocusChange: jest.fn(),
|
|
171
|
+
});
|
|
172
|
+
((refChildD.current as IFocusable).hasDom as jest.MockedFn<
|
|
173
|
+
Exclude<IFocusable['hasDom'], undefined>
|
|
174
|
+
>).mockReturnValueOnce(true);
|
|
175
|
+
base.backward(mockEvent);
|
|
176
|
+
expect(base.index).toBe(2);
|
|
177
|
+
base.backward(mockEvent);
|
|
178
|
+
expect(base.index).toBe(1);
|
|
179
|
+
});
|
|
162
180
|
});
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import { isRtl } from '@24i/bigscreen-sdk/i18n';
|
|
1
2
|
import { Horizontal } from '../Horizontal';
|
|
2
3
|
import { rootRef, refChildA, refChildB, refChildC } from './shared';
|
|
3
4
|
|
|
5
|
+
jest.mock('@24i/bigscreen-sdk/i18n', () => ({
|
|
6
|
+
...jest.requireActual('@24i/bigscreen-sdk/i18n'),
|
|
7
|
+
isRtl: jest.fn().mockReturnValue(false),
|
|
8
|
+
}));
|
|
9
|
+
const isRtlMock = isRtl as jest.MockedFn<typeof isRtl>;
|
|
10
|
+
|
|
4
11
|
const mockEvent = {
|
|
5
12
|
target: false,
|
|
6
13
|
preventDefault: jest.fn(),
|
|
@@ -38,9 +45,50 @@ describe('Horizontal Focus Layout', () => {
|
|
|
38
45
|
expect(horizontal.forward).toHaveBeenCalled();
|
|
39
46
|
});
|
|
40
47
|
|
|
41
|
-
it('should call
|
|
48
|
+
it('should call backward on LEFT', () => {
|
|
49
|
+
horizontal.setIndex(1);
|
|
50
|
+
horizontal.onKeyDown(mockLEFTEvent);
|
|
51
|
+
expect(horizontal.backward).toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should call backward on RIGHT when RTL', () => {
|
|
55
|
+
isRtlMock.mockReturnValueOnce(true);
|
|
56
|
+
horizontal.setIndex(1);
|
|
57
|
+
horizontal.onKeyDown(mockRIGHTEvent);
|
|
58
|
+
expect(horizontal.backward).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should call forward on LEFT when RTL', () => {
|
|
62
|
+
isRtlMock.mockReturnValueOnce(true);
|
|
63
|
+
horizontal.onKeyDown(mockLEFTEvent);
|
|
64
|
+
expect(horizontal.forward).toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should call backward on RIGHT when forced RTL', () => {
|
|
68
|
+
horizontal.props.dir = 'rtl';
|
|
69
|
+
horizontal.setIndex(1);
|
|
70
|
+
horizontal.onKeyDown(mockRIGHTEvent);
|
|
71
|
+
expect(horizontal.backward).toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should call forward on LEFT when forced RTL', () => {
|
|
75
|
+
horizontal.props.dir = 'rtl';
|
|
76
|
+
horizontal.onKeyDown(mockLEFTEvent);
|
|
77
|
+
expect(horizontal.forward).toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should call backward on LEFT when RTL and forced LTR', () => {
|
|
81
|
+
isRtlMock.mockReturnValueOnce(true);
|
|
82
|
+
horizontal.props.dir = 'ltr';
|
|
42
83
|
horizontal.setIndex(1);
|
|
43
84
|
horizontal.onKeyDown(mockLEFTEvent);
|
|
44
85
|
expect(horizontal.backward).toHaveBeenCalled();
|
|
45
86
|
});
|
|
87
|
+
|
|
88
|
+
it('should call forward on RIGHT when RTL and forced LTR', () => {
|
|
89
|
+
isRtlMock.mockReturnValueOnce(true);
|
|
90
|
+
horizontal.props.dir = 'ltr';
|
|
91
|
+
horizontal.onKeyDown(mockRIGHTEvent);
|
|
92
|
+
expect(horizontal.forward).toHaveBeenCalled();
|
|
93
|
+
});
|
|
46
94
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRef } from '@24i/bigscreen-sdk/jsx';
|
|
2
|
+
import { IFocusable } from '../../IFocusable';
|
|
2
3
|
|
|
3
4
|
class ChildMock {
|
|
4
5
|
focus = jest.fn();
|
|
@@ -10,6 +11,9 @@ export const rootRef = createRef(root);
|
|
|
10
11
|
const childA = new ChildMock();
|
|
11
12
|
const childB = new ChildMock();
|
|
12
13
|
const childC = new ChildMock();
|
|
14
|
+
const childD = new ChildMock();
|
|
15
|
+
(childD as IFocusable).hasDom = jest.fn().mockReturnValue(false);
|
|
13
16
|
export const refChildA = createRef(childA);
|
|
14
17
|
export const refChildB = createRef(childB);
|
|
15
18
|
export const refChildC = createRef(childC);
|
|
19
|
+
export const refChildD = createRef(childD);
|