@symbiote-native/screen-orientation 0.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.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/build/angular/index.d.ts +2 -0
- package/build/angular/index.js +4 -0
- package/build/angular/services/screen-orientation.service/index.d.ts +6 -0
- package/build/angular/services/screen-orientation.service/index.js +75 -0
- package/build/core/index.d.ts +3 -0
- package/build/core/index.js +2 -0
- package/build/core/native-module.d.ts +13 -0
- package/build/core/native-module.js +3 -0
- package/build/core/screen-orientation.d.ts +24 -0
- package/build/core/screen-orientation.js +152 -0
- package/build/core/types.d.ts +85 -0
- package/build/core/types.js +64 -0
- package/build/react/hooks/use-screen-orientation/index.d.ts +2 -0
- package/build/react/hooks/use-screen-orientation/index.js +20 -0
- package/build/react/index.d.ts +2 -0
- package/build/react/index.js +3 -0
- package/build/vue/composables/use-screen-orientation/index.d.ts +3 -0
- package/build/vue/composables/use-screen-orientation/index.js +27 -0
- package/build/vue/index.d.ts +2 -0
- package/build/vue/index.js +3 -0
- package/build-ngc/angular/index.d.ts +2 -0
- package/build-ngc/angular/index.js +5 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/angular/services/screen-orientation.service/index.d.ts +9 -0
- package/build-ngc/angular/services/screen-orientation.service/index.js +34 -0
- package/build-ngc/angular/services/screen-orientation.service/index.js.map +1 -0
- package/build-ngc/core/index.d.ts +3 -0
- package/build-ngc/core/index.js +3 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +13 -0
- package/build-ngc/core/native-module.js +4 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/screen-orientation.d.ts +24 -0
- package/build-ngc/core/screen-orientation.js +153 -0
- package/build-ngc/core/screen-orientation.js.map +1 -0
- package/build-ngc/core/types.d.ts +85 -0
- package/build-ngc/core/types.js +65 -0
- package/build-ngc/core/types.js.map +1 -0
- package/native-link.json +12 -0
- package/package.json +107 -0
- package/src/angular/index.ts +26 -0
- package/src/angular/services/screen-orientation.service/index.ts +45 -0
- package/src/angular/services/screen-orientation.service/screen-orientation.service.test.ts +101 -0
- package/src/core/index.ts +25 -0
- package/src/core/native-module.ts +36 -0
- package/src/core/screen-orientation.test.ts +307 -0
- package/src/core/screen-orientation.ts +186 -0
- package/src/core/types.ts +98 -0
- package/src/react/hooks/use-screen-orientation/index.ts +34 -0
- package/src/react/hooks/use-screen-orientation/use-screen-orientation.test.tsx +93 -0
- package/src/react/index.ts +26 -0
- package/src/vue/composables/use-screen-orientation/index.ts +42 -0
- package/src/vue/composables/use-screen-orientation/use-screen-orientation.test.ts +98 -0
- package/src/vue/index.ts +26 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Hand-ported from .vendors/expo/packages/expo-screen-orientation/src/ScreenOrientation.ts
|
|
2
|
+
// (sdk-57). Unlike battery, upstream ScreenOrientation throws UnavailabilityError when the
|
|
3
|
+
// native method is absent — same convention as packages/network/src/core/network.ts.
|
|
4
|
+
import { Platform, UnavailabilityError, type EventSubscription } from 'expo-modules-core';
|
|
5
|
+
import { Dimensions } from 'react-native';
|
|
6
|
+
import { expoScreenOrientation, type IPlatformOrientationParam } from './native-module';
|
|
7
|
+
import {
|
|
8
|
+
Orientation,
|
|
9
|
+
OrientationLock,
|
|
10
|
+
WebOrientationLock,
|
|
11
|
+
type OrientationChangeEvent,
|
|
12
|
+
type OrientationChangeListener,
|
|
13
|
+
type PlatformOrientationInfo,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
const NATIVE_MODULE_NAME = 'ScreenOrientation';
|
|
17
|
+
|
|
18
|
+
let _orientationChangeSubscribers: EventSubscription[] = [];
|
|
19
|
+
let _lastOrientationLock: OrientationLock = OrientationLock.UNKNOWN;
|
|
20
|
+
|
|
21
|
+
/** Locks the screen to the given orientation. */
|
|
22
|
+
export async function lockAsync(orientationLock: OrientationLock): Promise<void> {
|
|
23
|
+
if (!expoScreenOrientation.lockAsync) {
|
|
24
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'lockAsync');
|
|
25
|
+
}
|
|
26
|
+
const orientationLocks = Object.values(OrientationLock);
|
|
27
|
+
if (!orientationLocks.includes(orientationLock)) {
|
|
28
|
+
throw new TypeError(`Invalid Orientation Lock: ${orientationLock}`);
|
|
29
|
+
}
|
|
30
|
+
if (orientationLock === OrientationLock.OTHER) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
await expoScreenOrientation.lockAsync(orientationLock);
|
|
34
|
+
_lastOrientationLock = orientationLock;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Locks the screen to a platform-specific orientation param — Android constant, iOS
|
|
38
|
+
* orientation array, or web `WebOrientationLock`. */
|
|
39
|
+
export async function lockPlatformAsync(options: PlatformOrientationInfo): Promise<void> {
|
|
40
|
+
if (!expoScreenOrientation.lockPlatformAsync) {
|
|
41
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'lockPlatformAsync');
|
|
42
|
+
}
|
|
43
|
+
const { screenOrientationConstantAndroid, screenOrientationArrayIOS, screenOrientationLockWeb } =
|
|
44
|
+
options;
|
|
45
|
+
let platformOrientationParam: IPlatformOrientationParam | undefined;
|
|
46
|
+
if (Platform.OS === 'android' && screenOrientationConstantAndroid) {
|
|
47
|
+
if (isNaN(screenOrientationConstantAndroid)) {
|
|
48
|
+
throw new TypeError(
|
|
49
|
+
`lockPlatformAsync Android platform: screenOrientationConstantAndroid cannot be called with ${screenOrientationConstantAndroid}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
platformOrientationParam = screenOrientationConstantAndroid;
|
|
53
|
+
} else if (Platform.OS === 'ios' && screenOrientationArrayIOS) {
|
|
54
|
+
if (!Array.isArray(screenOrientationArrayIOS)) {
|
|
55
|
+
throw new TypeError(
|
|
56
|
+
`lockPlatformAsync iOS platform: screenOrientationArrayIOS cannot be called with ${screenOrientationArrayIOS}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const orientations = Object.values(Orientation);
|
|
60
|
+
for (const orientation of screenOrientationArrayIOS) {
|
|
61
|
+
if (!orientations.includes(orientation)) {
|
|
62
|
+
throw new TypeError(
|
|
63
|
+
`lockPlatformAsync iOS platform: ${orientation} is not a valid Orientation`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
platformOrientationParam = screenOrientationArrayIOS;
|
|
68
|
+
} else if (Platform.OS === 'web' && screenOrientationLockWeb) {
|
|
69
|
+
const webOrientationLocks = Object.values(WebOrientationLock);
|
|
70
|
+
if (!webOrientationLocks.includes(screenOrientationLockWeb)) {
|
|
71
|
+
throw new TypeError(`Invalid Web Orientation Lock: ${screenOrientationLockWeb}`);
|
|
72
|
+
}
|
|
73
|
+
platformOrientationParam = screenOrientationLockWeb;
|
|
74
|
+
}
|
|
75
|
+
if (!platformOrientationParam) {
|
|
76
|
+
throw new TypeError('lockPlatformAsync cannot be called with undefined option properties');
|
|
77
|
+
}
|
|
78
|
+
await expoScreenOrientation.lockPlatformAsync(platformOrientationParam);
|
|
79
|
+
_lastOrientationLock = OrientationLock.OTHER;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Unlocks the screen orientation back to the system default. */
|
|
83
|
+
export async function unlockAsync(): Promise<void> {
|
|
84
|
+
if (!expoScreenOrientation.lockAsync) {
|
|
85
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'lockAsync');
|
|
86
|
+
}
|
|
87
|
+
await expoScreenOrientation.lockAsync(OrientationLock.DEFAULT);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Gets the device's current screen orientation. */
|
|
91
|
+
export async function getOrientationAsync(): Promise<Orientation> {
|
|
92
|
+
if (!expoScreenOrientation.getOrientationAsync) {
|
|
93
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getOrientationAsync');
|
|
94
|
+
}
|
|
95
|
+
return expoScreenOrientation.getOrientationAsync();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Gets the current orientation lock, falling back to the last value set via `lockAsync()`/
|
|
99
|
+
* `lockPlatformAsync()` when the native method itself isn't available. */
|
|
100
|
+
export async function getOrientationLockAsync(): Promise<OrientationLock> {
|
|
101
|
+
if (!expoScreenOrientation.getOrientationLockAsync) {
|
|
102
|
+
return _lastOrientationLock;
|
|
103
|
+
}
|
|
104
|
+
return expoScreenOrientation.getOrientationLockAsync();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Gets the current orientation lock as a platform-specific value. */
|
|
108
|
+
export async function getPlatformOrientationLockAsync(): Promise<PlatformOrientationInfo> {
|
|
109
|
+
const platformOrientationLock = await expoScreenOrientation.getPlatformOrientationLockAsync?.();
|
|
110
|
+
if (Platform.OS === 'android' && typeof platformOrientationLock === 'number') {
|
|
111
|
+
return { screenOrientationConstantAndroid: platformOrientationLock };
|
|
112
|
+
}
|
|
113
|
+
if (Platform.OS === 'ios' && Array.isArray(platformOrientationLock)) {
|
|
114
|
+
return { screenOrientationArrayIOS: platformOrientationLock };
|
|
115
|
+
}
|
|
116
|
+
if (Platform.OS === 'web' && typeof platformOrientationLock === 'string') {
|
|
117
|
+
return { screenOrientationLockWeb: platformOrientationLock };
|
|
118
|
+
}
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Whether the given orientation lock is supported on this device. */
|
|
123
|
+
export async function supportsOrientationLockAsync(
|
|
124
|
+
orientationLock: OrientationLock,
|
|
125
|
+
): Promise<boolean> {
|
|
126
|
+
if (!expoScreenOrientation.supportsOrientationLockAsync) {
|
|
127
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'supportsOrientationLockAsync');
|
|
128
|
+
}
|
|
129
|
+
const orientationLocks = Object.values(OrientationLock);
|
|
130
|
+
if (!orientationLocks.includes(orientationLock)) {
|
|
131
|
+
throw new TypeError(`Invalid Orientation Lock: ${orientationLock}`);
|
|
132
|
+
}
|
|
133
|
+
return expoScreenOrientation.supportsOrientationLockAsync(orientationLock);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Subscribes to orientation-change events. */
|
|
137
|
+
export function addOrientationChangeListener(
|
|
138
|
+
listener: OrientationChangeListener,
|
|
139
|
+
): EventSubscription {
|
|
140
|
+
if (typeof listener !== 'function') {
|
|
141
|
+
throw new TypeError(`addOrientationChangeListener cannot be called with ${listener}`);
|
|
142
|
+
}
|
|
143
|
+
const subscription = createDidUpdateDimensionsSubscription(listener);
|
|
144
|
+
_orientationChangeSubscribers.push(subscription);
|
|
145
|
+
return subscription;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Removes every orientation-change listener registered via `addOrientationChangeListener()`. */
|
|
149
|
+
export function removeOrientationChangeListeners(): void {
|
|
150
|
+
let i = _orientationChangeSubscribers.length;
|
|
151
|
+
while (i--) {
|
|
152
|
+
_orientationChangeSubscribers[i]?.remove();
|
|
153
|
+
_orientationChangeSubscribers.pop();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Removes a single orientation-change listener's subscription. */
|
|
158
|
+
export function removeOrientationChangeListener(subscription: EventSubscription): void {
|
|
159
|
+
if (!subscription || !subscription.remove) {
|
|
160
|
+
throw new TypeError('Must pass in a valid subscription');
|
|
161
|
+
}
|
|
162
|
+
subscription.remove();
|
|
163
|
+
_orientationChangeSubscribers = _orientationChangeSubscribers.filter(sub => sub !== subscription);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// IMPORTANT: Android doesn't emit `expoDidUpdateDimensions` — RN's own Dimensions module does,
|
|
167
|
+
// so on Android we piggyback on RN's own `Dimensions.addEventListener('change', ...)` instead.
|
|
168
|
+
function createDidUpdateDimensionsSubscription(
|
|
169
|
+
listener: OrientationChangeListener,
|
|
170
|
+
): EventSubscription {
|
|
171
|
+
if (Platform.OS === 'web' || Platform.OS === 'ios') {
|
|
172
|
+
return expoScreenOrientation.addListener(
|
|
173
|
+
'expoDidUpdateDimensions',
|
|
174
|
+
async (update: OrientationChangeEvent) => {
|
|
175
|
+
listener(update);
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return Dimensions.addEventListener('change', async () => {
|
|
180
|
+
const [orientationLock, orientation] = await Promise.all([
|
|
181
|
+
getOrientationLockAsync(),
|
|
182
|
+
getOrientationAsync(),
|
|
183
|
+
]);
|
|
184
|
+
listener({ orientationInfo: { orientation }, orientationLock });
|
|
185
|
+
});
|
|
186
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Hand-ported from .vendors/expo/packages/expo-screen-orientation/src/ScreenOrientation.types.ts
|
|
2
|
+
// (sdk-57) — plain enums/data shapes, no `expo` meta-package import to swap out (same as
|
|
3
|
+
// packages/network's types.ts).
|
|
4
|
+
|
|
5
|
+
export enum Orientation {
|
|
6
|
+
UNKNOWN = 0,
|
|
7
|
+
PORTRAIT_UP = 1,
|
|
8
|
+
PORTRAIT_DOWN = 2,
|
|
9
|
+
LANDSCAPE_LEFT = 3,
|
|
10
|
+
LANDSCAPE_RIGHT = 4,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Values that can be passed to `lockAsync()`. `ALL`/`PORTRAIT` are invalid on devices that
|
|
15
|
+
* don't support `PORTRAIT_DOWN`.
|
|
16
|
+
*/
|
|
17
|
+
export enum OrientationLock {
|
|
18
|
+
/** iOS: all orientations except `PORTRAIT_DOWN`. Android: system decides. */
|
|
19
|
+
DEFAULT = 0,
|
|
20
|
+
/** All four possible orientations. */
|
|
21
|
+
ALL = 1,
|
|
22
|
+
/** Any portrait orientation. */
|
|
23
|
+
PORTRAIT = 2,
|
|
24
|
+
/** Right-side-up portrait only. */
|
|
25
|
+
PORTRAIT_UP = 3,
|
|
26
|
+
/** Upside-down portrait only. */
|
|
27
|
+
PORTRAIT_DOWN = 4,
|
|
28
|
+
/** Any landscape orientation. */
|
|
29
|
+
LANDSCAPE = 5,
|
|
30
|
+
/** Left landscape only. */
|
|
31
|
+
LANDSCAPE_LEFT = 6,
|
|
32
|
+
/** Right landscape only. */
|
|
33
|
+
LANDSCAPE_RIGHT = 7,
|
|
34
|
+
/** A platform-specific orientation — not a valid `lockAsync()` policy. */
|
|
35
|
+
OTHER = 8,
|
|
36
|
+
/** Unknown lock — not a valid `lockAsync()` policy. */
|
|
37
|
+
UNKNOWN = 9,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Each iOS device has a default set of UIKit size classes. */
|
|
41
|
+
export enum SizeClassIOS {
|
|
42
|
+
UNKNOWN = 0,
|
|
43
|
+
COMPACT = 1,
|
|
44
|
+
REGULAR = 2,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export enum WebOrientationLock {
|
|
48
|
+
PORTRAIT_PRIMARY = 'portrait-primary',
|
|
49
|
+
PORTRAIT_SECONDARY = 'portrait-secondary',
|
|
50
|
+
PORTRAIT = 'portrait',
|
|
51
|
+
LANDSCAPE_PRIMARY = 'landscape-primary',
|
|
52
|
+
LANDSCAPE_SECONDARY = 'landscape-secondary',
|
|
53
|
+
LANDSCAPE = 'landscape',
|
|
54
|
+
ANY = 'any',
|
|
55
|
+
NATURAL = 'natural',
|
|
56
|
+
UNKNOWN = 'unknown',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export enum WebOrientation {
|
|
60
|
+
PORTRAIT_PRIMARY = 'portrait-primary',
|
|
61
|
+
PORTRAIT_SECONDARY = 'portrait-secondary',
|
|
62
|
+
LANDSCAPE_PRIMARY = 'landscape-primary',
|
|
63
|
+
LANDSCAPE_SECONDARY = 'landscape-secondary',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type PlatformOrientationInfo = {
|
|
67
|
+
/** @platform android */
|
|
68
|
+
screenOrientationConstantAndroid?: number;
|
|
69
|
+
/** @platform ios */
|
|
70
|
+
screenOrientationArrayIOS?: Orientation[];
|
|
71
|
+
/** @platform web */
|
|
72
|
+
screenOrientationLockWeb?: WebOrientationLock;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type ScreenOrientationInfo = {
|
|
76
|
+
orientation: Orientation;
|
|
77
|
+
/** @platform ios */
|
|
78
|
+
verticalSizeClass?: SizeClassIOS;
|
|
79
|
+
/** @platform ios */
|
|
80
|
+
horizontalSizeClass?: SizeClassIOS;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type OrientationChangeEvent = {
|
|
84
|
+
orientationLock: OrientationLock;
|
|
85
|
+
orientationInfo: ScreenOrientationInfo;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type OrientationChangeListener = (event: OrientationChangeEvent) => void;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The shape shared by every adapter's `useScreenOrientation()` hook/composable/service — not an
|
|
92
|
+
* upstream expo-screen-orientation type (upstream ships no reactive hook of its own), defined once
|
|
93
|
+
* here so React/Vue/Angular all consume the identical shape instead of three ad-hoc declarations.
|
|
94
|
+
*/
|
|
95
|
+
export type ScreenOrientationState = {
|
|
96
|
+
orientation: Orientation;
|
|
97
|
+
orientationLock: OrientationLock;
|
|
98
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// React lifecycle wiring over the framework-agnostic core (core/screen-orientation.ts). Seeds
|
|
2
|
+
// the initial value with one-shot getOrientationAsync()/getOrientationLockAsync() calls before
|
|
3
|
+
// the first native event fires, mirroring packages/network's useNetworkState.
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
import {
|
|
6
|
+
addOrientationChangeListener,
|
|
7
|
+
getOrientationAsync,
|
|
8
|
+
getOrientationLockAsync,
|
|
9
|
+
Orientation,
|
|
10
|
+
OrientationLock,
|
|
11
|
+
type ScreenOrientationState,
|
|
12
|
+
} from '../../../core';
|
|
13
|
+
|
|
14
|
+
export function useScreenOrientation(): ScreenOrientationState {
|
|
15
|
+
const [screenOrientation, setScreenOrientation] = useState<ScreenOrientationState>({
|
|
16
|
+
orientation: Orientation.UNKNOWN,
|
|
17
|
+
orientationLock: OrientationLock.UNKNOWN,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
Promise.all([getOrientationAsync(), getOrientationLockAsync()]).then(
|
|
22
|
+
([orientation, orientationLock]) => setScreenOrientation({ orientation, orientationLock }),
|
|
23
|
+
);
|
|
24
|
+
const subscription = addOrientationChangeListener(event =>
|
|
25
|
+
setScreenOrientation({
|
|
26
|
+
orientation: event.orientationInfo.orientation,
|
|
27
|
+
orientationLock: event.orientationLock,
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
return () => subscription.remove();
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
return screenOrientation;
|
|
34
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Co-located React-driven test (ADR 0025) for useScreenOrientation. See network's
|
|
2
|
+
// use-network-state.test.tsx for the shared rationale (mocks `core`, not expo-modules-core
|
|
3
|
+
// internals).
|
|
4
|
+
|
|
5
|
+
import { createElement, type ReactElement } from 'react';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { mount, unmount, View } from '@symbiote-native/react';
|
|
8
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
9
|
+
import { useScreenOrientation } from './index';
|
|
10
|
+
|
|
11
|
+
type IScreenOrientationState = { orientation: number; orientationLock: number };
|
|
12
|
+
type IOrientationChangeEvent = {
|
|
13
|
+
orientationLock: number;
|
|
14
|
+
orientationInfo: { orientation: number };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const { addListener, getOrientationAsync, getOrientationLockAsync, remove } = vi.hoisted(() => {
|
|
18
|
+
const remove = vi.fn();
|
|
19
|
+
return {
|
|
20
|
+
addListener: vi.fn((_listener: (event: IOrientationChangeEvent) => void) => ({ remove })),
|
|
21
|
+
getOrientationAsync: vi.fn(async () => 1),
|
|
22
|
+
getOrientationLockAsync: vi.fn(async () => 0),
|
|
23
|
+
remove,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
vi.mock('../../../core', () => ({
|
|
28
|
+
addOrientationChangeListener: addListener,
|
|
29
|
+
getOrientationAsync,
|
|
30
|
+
getOrientationLockAsync,
|
|
31
|
+
Orientation: { UNKNOWN: 0, PORTRAIT_UP: 1 },
|
|
32
|
+
OrientationLock: { UNKNOWN: 9, DEFAULT: 0 },
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const ROOT_TAG = 954;
|
|
36
|
+
|
|
37
|
+
const results: IScreenOrientationState[] = [];
|
|
38
|
+
|
|
39
|
+
function Probe(): ReactElement {
|
|
40
|
+
results.push(useScreenOrientation());
|
|
41
|
+
return createElement(View);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const fabric = installFabric();
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
fabric.reset();
|
|
48
|
+
results.length = 0;
|
|
49
|
+
vi.clearAllMocks();
|
|
50
|
+
getOrientationAsync.mockResolvedValue(1);
|
|
51
|
+
getOrientationLockAsync.mockResolvedValue(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => unmount(ROOT_TAG));
|
|
55
|
+
|
|
56
|
+
describe('useScreenOrientation', () => {
|
|
57
|
+
it('reports OrientationLock/Orientation UNKNOWN before the initial fetch resolves', () => {
|
|
58
|
+
mount(ROOT_TAG, createElement(Probe));
|
|
59
|
+
|
|
60
|
+
expect(results[results.length - 1]).toEqual({ orientation: 0, orientationLock: 9 });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('reports the fetched state once the initial getOrientationAsync()/getOrientationLockAsync() resolve', async () => {
|
|
64
|
+
mount(ROOT_TAG, createElement(Probe));
|
|
65
|
+
|
|
66
|
+
await vi.waitFor(() =>
|
|
67
|
+
expect(results[results.length - 1]).toEqual({ orientation: 1, orientationLock: 0 }),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('updates when the native listener fires', async () => {
|
|
72
|
+
mount(ROOT_TAG, createElement(Probe));
|
|
73
|
+
await vi.waitFor(() => expect(results[results.length - 1].orientation).toBe(1));
|
|
74
|
+
|
|
75
|
+
const listener = addListener.mock.calls[0][0];
|
|
76
|
+
listener({ orientationLock: 5, orientationInfo: { orientation: 3 } });
|
|
77
|
+
|
|
78
|
+
await vi.waitFor(() =>
|
|
79
|
+
expect(results[results.length - 1]).toEqual({
|
|
80
|
+
orientation: 3,
|
|
81
|
+
orientationLock: 5,
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('unsubscribes from the native listener on unmount', () => {
|
|
87
|
+
mount(ROOT_TAG, createElement(Probe));
|
|
88
|
+
|
|
89
|
+
unmount(ROOT_TAG);
|
|
90
|
+
|
|
91
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// @symbiote-native/screen-orientation/react: the React entry over the framework-agnostic core.
|
|
2
|
+
|
|
3
|
+
export { useScreenOrientation } from './hooks/use-screen-orientation';
|
|
4
|
+
export {
|
|
5
|
+
lockAsync,
|
|
6
|
+
lockPlatformAsync,
|
|
7
|
+
unlockAsync,
|
|
8
|
+
getOrientationAsync,
|
|
9
|
+
getOrientationLockAsync,
|
|
10
|
+
getPlatformOrientationLockAsync,
|
|
11
|
+
supportsOrientationLockAsync,
|
|
12
|
+
addOrientationChangeListener,
|
|
13
|
+
removeOrientationChangeListeners,
|
|
14
|
+
removeOrientationChangeListener,
|
|
15
|
+
Orientation,
|
|
16
|
+
OrientationLock,
|
|
17
|
+
SizeClassIOS,
|
|
18
|
+
WebOrientationLock,
|
|
19
|
+
WebOrientation,
|
|
20
|
+
type PlatformOrientationInfo,
|
|
21
|
+
type ScreenOrientationInfo,
|
|
22
|
+
type OrientationChangeEvent,
|
|
23
|
+
type OrientationChangeListener,
|
|
24
|
+
type ScreenOrientationState,
|
|
25
|
+
type EventSubscription,
|
|
26
|
+
} from '../core';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Vue lifecycle wiring over the framework-agnostic core (core/screen-orientation.ts). Seeds the
|
|
2
|
+
// initial value with one-shot getOrientationAsync()/getOrientationLockAsync() calls before the
|
|
3
|
+
// first native event fires, mirroring packages/network's useNetworkState.
|
|
4
|
+
|
|
5
|
+
import { onMounted, onUnmounted, ref, type Ref } from '@vue/runtime-core';
|
|
6
|
+
import {
|
|
7
|
+
addOrientationChangeListener,
|
|
8
|
+
getOrientationAsync,
|
|
9
|
+
getOrientationLockAsync,
|
|
10
|
+
Orientation,
|
|
11
|
+
OrientationLock,
|
|
12
|
+
type EventSubscription,
|
|
13
|
+
type ScreenOrientationState,
|
|
14
|
+
} from '../../../core';
|
|
15
|
+
|
|
16
|
+
export function useScreenOrientation(): Ref<ScreenOrientationState> {
|
|
17
|
+
const screenOrientation = ref<ScreenOrientationState>({
|
|
18
|
+
orientation: Orientation.UNKNOWN,
|
|
19
|
+
orientationLock: OrientationLock.UNKNOWN,
|
|
20
|
+
});
|
|
21
|
+
let subscription: EventSubscription | undefined;
|
|
22
|
+
|
|
23
|
+
onMounted(() => {
|
|
24
|
+
Promise.all([getOrientationAsync(), getOrientationLockAsync()]).then(
|
|
25
|
+
([orientation, orientationLock]) => {
|
|
26
|
+
screenOrientation.value = { orientation, orientationLock };
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
subscription = addOrientationChangeListener(event => {
|
|
30
|
+
screenOrientation.value = {
|
|
31
|
+
orientation: event.orientationInfo.orientation,
|
|
32
|
+
orientationLock: event.orientationLock,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
onUnmounted(() => {
|
|
38
|
+
subscription?.remove();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return screenOrientation;
|
|
42
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Co-located Vue-driven test (ADR 0025) for useScreenOrientation. See network's
|
|
2
|
+
// use-network-state.test.ts for the shared rationale.
|
|
3
|
+
|
|
4
|
+
import { defineComponent, h, type Ref } from '@vue/runtime-core';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { mount, unmount } from '@symbiote-native/vue';
|
|
7
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
8
|
+
import { useScreenOrientation } from './index';
|
|
9
|
+
|
|
10
|
+
const ROOT_TAG = 9954;
|
|
11
|
+
|
|
12
|
+
type IScreenOrientationState = { orientation: number; orientationLock: number };
|
|
13
|
+
type IOrientationChangeEvent = {
|
|
14
|
+
orientationLock: number;
|
|
15
|
+
orientationInfo: { orientation: number };
|
|
16
|
+
};
|
|
17
|
+
type IListener = (event: IOrientationChangeEvent) => void;
|
|
18
|
+
|
|
19
|
+
let registeredListener: IListener | undefined;
|
|
20
|
+
const removeMock = vi.fn();
|
|
21
|
+
const addListenerMock = vi.fn((listener: IListener) => {
|
|
22
|
+
registeredListener = listener;
|
|
23
|
+
return { remove: removeMock };
|
|
24
|
+
});
|
|
25
|
+
const getOrientationAsyncMock = vi.fn(async () => 1);
|
|
26
|
+
const getOrientationLockAsyncMock = vi.fn(async () => 0);
|
|
27
|
+
|
|
28
|
+
vi.mock('../../../core', () => ({
|
|
29
|
+
addOrientationChangeListener: (listener: IListener) => addListenerMock(listener),
|
|
30
|
+
getOrientationAsync: () => getOrientationAsyncMock(),
|
|
31
|
+
getOrientationLockAsync: () => getOrientationLockAsyncMock(),
|
|
32
|
+
Orientation: { UNKNOWN: 0, PORTRAIT_UP: 1 },
|
|
33
|
+
OrientationLock: { UNKNOWN: 9, DEFAULT: 0 },
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const fabric = installFabric();
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
fabric.reset();
|
|
40
|
+
registeredListener = undefined;
|
|
41
|
+
addListenerMock.mockClear();
|
|
42
|
+
removeMock.mockClear();
|
|
43
|
+
getOrientationAsyncMock.mockClear();
|
|
44
|
+
getOrientationLockAsyncMock.mockClear();
|
|
45
|
+
getOrientationAsyncMock.mockResolvedValue(1);
|
|
46
|
+
getOrientationLockAsyncMock.mockResolvedValue(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => unmount(ROOT_TAG));
|
|
50
|
+
|
|
51
|
+
function mountScreenOrientation(): Ref<IScreenOrientationState> {
|
|
52
|
+
let screenOrientation: Ref<IScreenOrientationState> | undefined;
|
|
53
|
+
mount(
|
|
54
|
+
ROOT_TAG,
|
|
55
|
+
defineComponent({
|
|
56
|
+
setup: () => {
|
|
57
|
+
screenOrientation = useScreenOrientation();
|
|
58
|
+
return () => h('symbiote-text', {}, 'screen-orientation');
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
if (screenOrientation === undefined) {
|
|
63
|
+
throw new Error('setup() did not run');
|
|
64
|
+
}
|
|
65
|
+
return screenOrientation;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('useScreenOrientation (Vue)', () => {
|
|
69
|
+
it('starts at Orientation/OrientationLock UNKNOWN before the initial fetch resolves', () => {
|
|
70
|
+
const screenOrientation = mountScreenOrientation();
|
|
71
|
+
|
|
72
|
+
expect(screenOrientation.value).toEqual({ orientation: 0, orientationLock: 9 });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('updates to the fetched value once getOrientationAsync()/getOrientationLockAsync() resolve', async () => {
|
|
76
|
+
const screenOrientation = mountScreenOrientation();
|
|
77
|
+
|
|
78
|
+
await vi.waitFor(() =>
|
|
79
|
+
expect(screenOrientation.value).toEqual({ orientation: 1, orientationLock: 0 }),
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('updates the ref when the native listener fires', async () => {
|
|
84
|
+
const screenOrientation = mountScreenOrientation();
|
|
85
|
+
await vi.waitFor(() => expect(screenOrientation.value.orientation).toBe(1));
|
|
86
|
+
|
|
87
|
+
registeredListener?.({ orientationLock: 5, orientationInfo: { orientation: 3 } });
|
|
88
|
+
|
|
89
|
+
expect(screenOrientation.value).toEqual({ orientation: 3, orientationLock: 5 });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('removes the subscription on unmount', () => {
|
|
93
|
+
mountScreenOrientation();
|
|
94
|
+
unmount(ROOT_TAG);
|
|
95
|
+
|
|
96
|
+
expect(removeMock).toHaveBeenCalledTimes(1);
|
|
97
|
+
});
|
|
98
|
+
});
|
package/src/vue/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// @symbiote-native/screen-orientation/vue: the Vue entry over the framework-agnostic core.
|
|
2
|
+
|
|
3
|
+
export { useScreenOrientation } from './composables/use-screen-orientation';
|
|
4
|
+
export {
|
|
5
|
+
lockAsync,
|
|
6
|
+
lockPlatformAsync,
|
|
7
|
+
unlockAsync,
|
|
8
|
+
getOrientationAsync,
|
|
9
|
+
getOrientationLockAsync,
|
|
10
|
+
getPlatformOrientationLockAsync,
|
|
11
|
+
supportsOrientationLockAsync,
|
|
12
|
+
addOrientationChangeListener,
|
|
13
|
+
removeOrientationChangeListeners,
|
|
14
|
+
removeOrientationChangeListener,
|
|
15
|
+
Orientation,
|
|
16
|
+
OrientationLock,
|
|
17
|
+
SizeClassIOS,
|
|
18
|
+
WebOrientationLock,
|
|
19
|
+
WebOrientation,
|
|
20
|
+
type PlatformOrientationInfo,
|
|
21
|
+
type ScreenOrientationInfo,
|
|
22
|
+
type OrientationChangeEvent,
|
|
23
|
+
type OrientationChangeListener,
|
|
24
|
+
type ScreenOrientationState,
|
|
25
|
+
type EventSubscription,
|
|
26
|
+
} from '../core';
|