🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

react-native-airship

Package Overview
Dependencies
Maintainers
2
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-native-airship - npm Package Compare versions

Comparing version
0.2.5
to
0.2.6
+5
lib/components/Airship.d.ts
import { Airship } from '../types';
/**
* Constructs an Airship component.
*/
export declare function makeAirship(): Airship;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import * as React from 'react';
import { View } from 'react-native';
import { makeEvent, makeEvents } from 'yavent';
import { sidesToOffset, sidesToPadding } from '../util/sides';
import { Barometer } from './Barometer';
const emptyLayout = {
offset: sidesToOffset([0, 0, 0, 0]),
padding: sidesToPadding([0, 0, 0, 0])
};
/**
* Constructs an Airship component.
*/
export function makeAirship() {
// Static state shared by all mounted containers:
const [onClear, emitClear] = makeEvent();
const [onGuestsChange, emitGuestsChange] = makeEvent();
let guests = [];
let nextKey = 0;
const AirshipHost = (props) => {
const { children } = props;
// Watch the common guest list:
const [ourGuests, setGuests] = React.useState(guests);
React.useEffect(() => onGuestsChange(setGuests), []);
// Track layout changes:
const [layout, setLayout] = React.useState(emptyLayout);
return (React.createElement(React.Fragment, null,
React.createElement(Barometer, { onLayout: setLayout }),
children,
ourGuests.map(guest => (React.createElement(View, { key: guest.key, pointerEvents: "box-none", style: Object.assign(Object.assign(Object.assign({}, layout.offset), layout.padding), { flexDirection: 'row', justifyContent: 'center', position: 'absolute' }) }, guest.element)))));
};
let clearing = false;
function clear() {
if (clearing)
return;
clearing = true;
emitClear(undefined);
clearing = false;
}
function show(render) {
return __awaiter(this, void 0, void 0, function* () {
const key = `airship${nextKey++}`;
function remove() {
unclear();
guests = guests.filter(guest => guest.key !== key);
emitGuestsChange(guests);
}
// Assemble the bridge:
const [on, emit] = makeEvents();
let bridge;
const promise = new Promise((resolve, reject) => {
bridge = {
on,
onResult: callback => on('result', callback),
reject,
remove,
resolve
};
});
// Hook up events:
promise.then(() => emit('result', undefined), () => emit('result', undefined));
const unclear = onClear(() => emit('clear', undefined));
// Save the guest element in the shared state:
guests = [...guests, { key, element: render(bridge) }];
emitGuestsChange(guests);
return promise;
});
}
return Object.assign(AirshipHost, { clear, show });
}
import * as React from 'react';
import { ViewStyle } from 'react-native';
import { AirshipBridge } from '../types';
export interface AirshipDropdownProps {
bridge: AirshipBridge<undefined>;
children?: React.ReactNode;
onPress?: () => void;
autoHideMs?: number;
backgroundColor?: string;
borderRadius?: number;
flexDirection?: ViewStyle['flexDirection'];
justifyContent?: ViewStyle['justifyContent'];
margin?: number | number[];
maxHeight?: number;
maxWidth?: number;
padding?: number | number[];
slideInMs?: number;
slideOutMs?: number;
}
/**
* A notification that slides down from the top of the screen.
*/
export declare function AirshipDropdown(props: AirshipDropdownProps): JSX.Element;
import * as React from 'react';
import { Animated, Dimensions, TouchableWithoutFeedback } from 'react-native';
import { fixSides, sidesToMargin, sidesToPadding } from '../util/sides';
const safeAreaGap = 64;
/**
* A notification that slides down from the top of the screen.
*/
export function AirshipDropdown(props) {
const { bridge, children, onPress = () => bridge.resolve(undefined), autoHideMs = 5000, backgroundColor = 'white', borderRadius = 4, flexDirection, justifyContent, maxHeight = defaultMaxHeight(), maxWidth = 512, slideInMs = 300, slideOutMs = 500 } = props;
const margin = sidesToMargin(fixSides(props.margin, 0));
const padding = sidesToPadding(fixSides(props.padding, 0));
const hiddenOffset = -(maxHeight + margin.marginBottom);
margin.marginTop = -safeAreaGap;
padding.paddingTop += safeAreaGap;
// Create the animation:
const offset = React.useRef(new Animated.Value(hiddenOffset)).current;
React.useEffect(() => {
let timeout;
// Animate in:
Animated.timing(offset, {
toValue: 0,
duration: slideInMs,
useNativeDriver: true
}).start(() => {
// Start the auto-hide timer:
if (autoHideMs > 0) {
timeout = setTimeout(() => {
timeout = undefined;
bridge.resolve(undefined);
}, autoHideMs);
}
});
// Animate out:
bridge.on('clear', () => bridge.resolve(undefined));
bridge.on('result', () => {
Animated.timing(offset, {
toValue: hiddenOffset,
duration: slideOutMs,
useNativeDriver: true
}).start(() => bridge.remove());
});
return () => {
if (timeout != null)
clearTimeout(timeout);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const bodyStyle = Object.assign(Object.assign(Object.assign({}, margin), padding), { alignSelf: 'flex-start', backgroundColor, borderBottomLeftRadius: borderRadius, borderBottomRightRadius: borderRadius, flexDirection, flexShrink: 1, justifyContent,
maxHeight, shadowOffset: { height: 0, width: 0 }, shadowOpacity: 1, shadowRadius: 4, transform: [{ translateY: offset }], width: maxWidth // This works because flexShrink is set
});
return (React.createElement(TouchableWithoutFeedback, { onPress: onPress },
React.createElement(Animated.View, { style: bodyStyle }, children)));
}
function defaultMaxHeight() {
const { width, height } = Dimensions.get('screen');
return 0.25 * Math.max(width, height);
}
import * as React from 'react';
import { ViewStyle } from 'react-native';
import { AirshipBridge } from '../types';
export interface AirshipModalProps<T = unknown> {
bridge: AirshipBridge<T>;
children?: React.ReactNode;
onCancel: () => void;
center?: boolean;
backgroundColor?: string;
borderRadius?: number;
flexDirection?: ViewStyle['flexDirection'];
justifyContent?: ViewStyle['justifyContent'];
margin?: number | number[];
maxHeight?: number;
maxWidth?: number;
padding?: number | number[];
shadowOffset?: {
height: number;
width: number;
};
shadowOpacity?: number;
shadowRadius?: number;
slideInMs?: number;
slideOutMs?: number;
underlay?: string | React.ReactElement;
}
/**
* A modal that slides a modal up from the bottom of the screen
* and dims the rest of the app.
*/
export declare function AirshipModal<T>(props: AirshipModalProps<T>): JSX.Element;
import * as React from 'react';
import { Animated, BackHandler, Dimensions, TouchableWithoutFeedback } from 'react-native';
import { fixSides, sidesToMargin, sidesToPadding } from '../util/sides';
const safeAreaGap = 64;
/**
* A modal that slides a modal up from the bottom of the screen
* and dims the rest of the app.
*/
export function AirshipModal(props) {
const { bridge, children, onCancel, backgroundColor = 'white', borderRadius = 10, center = false, flexDirection, justifyContent, maxHeight, maxWidth = 512, shadowOffset = { height: 0, width: 0 }, shadowOpacity = 1, shadowRadius = 10, slideInMs = 300, slideOutMs = 300, underlay = 'rgba(0, 0, 0, 0.75)' } = props;
const margin = sidesToMargin(fixSides(props.margin, 0));
const padding = sidesToPadding(fixSides(props.padding, 0));
React.useEffect(() => bridge.on('clear', onCancel), [bridge, onCancel]);
// Create the animations:
const offset = React.useRef(new Animated.Value(Dimensions.get('window').height)).current;
const opacity = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
// Animate in:
Animated.parallel([
Animated.timing(opacity, {
toValue: 1,
duration: slideInMs,
useNativeDriver: true
}),
Animated.timing(offset, {
toValue: 0,
duration: slideInMs,
useNativeDriver: true
})
]).start();
// Animate out:
bridge.on('result', () => {
Animated.parallel([
Animated.timing(opacity, {
toValue: 0,
duration: slideOutMs,
useNativeDriver: true
}),
Animated.timing(offset, {
toValue: Dimensions.get('window').height,
duration: slideOutMs,
useNativeDriver: true
})
]).start(bridge.remove);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Set up the back-button handler:
React.useEffect(() => {
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
onCancel();
return true;
});
return () => backHandler.remove();
}, [onCancel]);
const underlayStyle = {
backgroundColor: typeof underlay === 'string' ? underlay : 'transparent',
bottom: 0,
left: 0,
opacity: opacity,
position: 'absolute',
right: 0,
top: 0
};
const bodyCommon = Object.assign(Object.assign(Object.assign({}, margin), padding), { alignSelf: center ? 'center' : 'flex-end', backgroundColor,
flexDirection, flexShrink: 1, justifyContent,
maxHeight,
shadowOffset,
shadowOpacity,
shadowRadius, transform: [{ translateY: offset }], width: maxWidth // This works because flexShrink is set
});
const bodyStyle = center
? Object.assign(Object.assign({}, bodyCommon), { borderRadius }) : Object.assign(Object.assign({}, bodyCommon), { borderTopLeftRadius: borderRadius, borderTopRightRadius: borderRadius, marginBottom: -safeAreaGap, paddingBottom: padding.paddingBottom + safeAreaGap });
return (React.createElement(React.Fragment, null,
React.createElement(TouchableWithoutFeedback, { onPress: () => onCancel() },
React.createElement(Animated.View, { style: underlayStyle }, typeof underlay !== 'string' ? underlay : undefined)),
React.createElement(Animated.View, { style: bodyStyle }, children)));
}
import * as React from 'react';
import { AirshipBridge } from '../types';
export interface AirshipToastProps {
bridge: AirshipBridge<undefined>;
children?: React.ReactNode;
message?: string;
autoHideMs?: number;
backgroundColor?: string;
borderRadius?: number;
fadeInMs?: number;
fadeOutMs?: number;
margin?: number | number[];
maxWidth?: number;
opacity?: number;
padding?: number | number[];
textColor?: string;
textSize?: number;
}
/**
* A semi-transparent message overlay.
*/
export declare function AirshipToast(props: AirshipToastProps): JSX.Element;
import * as React from 'react';
import { Animated, Text } from 'react-native';
import { fixSides, sidesToMargin, sidesToPadding } from '../util/sides';
/**
* A semi-transparent message overlay.
*/
export function AirshipToast(props) {
const { textSize = 14 } = props;
const { autoHideMs = 3000, backgroundColor = 'white', borderRadius = 1.5 * textSize, bridge, children, fadeInMs = 300, fadeOutMs = 1000, maxWidth = 512, opacity: finalOpacity = 0.9, message, textColor = 'black' } = props;
const margin = sidesToMargin(fixSides(props.margin, 2 * textSize));
const padding = sidesToPadding(fixSides(props.padding, textSize));
// Create the animation:
const opacity = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
let timeout;
// Animate in:
Animated.timing(opacity, {
toValue: finalOpacity,
duration: fadeInMs,
useNativeDriver: true
}).start(() => {
// Start the auto-hide timer:
if (autoHideMs > 0) {
timeout = setTimeout(() => {
timeout = undefined;
bridge.resolve(undefined);
}, autoHideMs);
}
});
// Animate out:
bridge.on('clear', () => bridge.resolve(undefined));
bridge.on('result', () => {
Animated.timing(opacity, {
toValue: 0,
duration: fadeOutMs,
useNativeDriver: true
}).start(() => bridge.remove());
});
return () => {
if (timeout != null)
clearTimeout(timeout);
};
});
const bodyStyle = Object.assign(Object.assign(Object.assign({}, margin), padding), { alignItems: 'center', alignSelf: 'flex-end', backgroundColor,
borderRadius, flexDirection: 'row', justifyContent: 'flex-start', maxWidth, opacity: opacity });
const textStyle = {
color: textColor,
flexShrink: 1,
fontSize: textSize,
textAlign: 'center'
};
return (React.createElement(Animated.View, { style: bodyStyle },
message != null ? React.createElement(Text, { style: textStyle }, message) : null,
children));
}
import * as React from 'react';
import { Offset, Padding } from '../util/sides';
export interface BarometerLayout {
offset: Offset;
padding: Padding;
}
interface Props {
children?: React.ReactNode;
onLayout?: (layout: BarometerLayout) => void;
}
/**
* Measures various things about the Airship environment,
* so we know how to position our children.
*
* This component mounts a view with absolute positioning,
* and then measures that view relative to the window.
* If a side is inset from the window edge, we use a negative offset
* to expand it outward. If a side extends beyond the window edge,
* we use padding to push the content inward.
*
* On iOS, we also mount a child inside a SafeAreaView, to measure
* the safe area insets. We add these to the padding.
*
* Finally, we keep track of the keyboard, adding extra padding &
* scheduling animations as needed.
*/
export declare function Barometer(props: Props): JSX.Element;
export {};
import * as React from 'react';
import { Dimensions, Keyboard, Platform, SafeAreaView, StyleSheet, View } from 'react-native';
import { addSides, mapSides, sidesToOffset, sidesToPadding, subtractSides } from '../util/sides';
const emptySides = [0, 0, 0, 0];
/**
* Measures various things about the Airship environment,
* so we know how to position our children.
*
* This component mounts a view with absolute positioning,
* and then measures that view relative to the window.
* If a side is inset from the window edge, we use a negative offset
* to expand it outward. If a side extends beyond the window edge,
* we use padding to push the content inward.
*
* On iOS, we also mount a child inside a SafeAreaView, to measure
* the safe area insets. We add these to the padding.
*
* Finally, we keep track of the keyboard, adding extra padding &
* scheduling animations as needed.
*/
export function Barometer(props) {
const { children, onLayout = () => { } } = props;
// Mutable state:
const keyboardHeight = React.useRef(0);
const lastLayoutJson = React.useRef('');
const view = React.useRef(null);
const childView = React.useRef(null);
// Handle layout changes:
const handleLayout = React.useCallback(() => {
// Measure the view in the window:
const viewPromise = new Promise(resolve => {
if (view.current == null)
return resolve(emptySides);
view.current.measureInWindow((x, y, width, height) => {
const window = Dimensions.get('window');
resolve([y, window.width - width - x, window.height - height - y, x]);
});
});
// Measure the child view in the window:
const childPromise = new Promise(resolve => {
if (childView.current == null)
return resolve(viewPromise);
childView.current.measureInWindow((x, y, width, height) => {
const window = Dimensions.get('window');
resolve([y, window.width - width - x, window.height - height - y, x]);
});
});
// Measure the gap between the bottom of the screen and the view:
const bottomPromise = new Promise(resolve => {
if (view.current == null)
return 0;
view.current.measure((x, y, width, height, screenX, screenY) => {
const screen = Dimensions.get('screen');
resolve(screen.height - height - screenY);
});
});
// Combine the results, then call the callback:
Promise.all([viewPromise, childPromise, bottomPromise])
.then(([viewOffset, childOffset, bottomGap]) => {
// Cancel out any offset, so we cover the full window:
const offset = mapSides(viewOffset, side => -Math.max(side, 0));
// If the offset is negative, issue positive padding,
// plus any safe area:
const safePadding = subtractSides(childOffset, viewOffset);
const padding = addSides(safePadding, mapSides(viewOffset, side => Math.abs(side)));
// Use the keyboard padding, if needed:
const keyboardPadding = Math.max(keyboardHeight.current - bottomGap - offset[2], 0);
padding[2] = Math.max(padding[2], keyboardPadding);
// Send an update if we have changes:
const string = JSON.stringify([offset, padding]);
if (string !== lastLayoutJson.current) {
lastLayoutJson.current = string;
onLayout({
offset: sidesToOffset(offset),
padding: sidesToPadding(padding)
});
}
})
.catch(() => { });
}, [onLayout]);
// Subscribe to keyboard changes:
React.useEffect(() => {
const handleKeyboard = event => {
const screen = Dimensions.get('screen');
keyboardHeight.current = Math.min(
// These two give different results sometimes, so pick the smaller one:
screen.height - event.endCoordinates.screenY, event.endCoordinates.height);
if (event.duration > 0) {
Keyboard.scheduleLayoutAnimation(event);
}
handleLayout();
};
if (Platform.OS === 'android') {
Keyboard.addListener('keyboardDidShow', handleKeyboard);
Keyboard.addListener('keyboardDidHide', handleKeyboard);
}
else {
Keyboard.addListener('keyboardWillChangeFrame', handleKeyboard);
}
return () => {
if (Platform.OS === 'android') {
Keyboard.removeListener('keyboardDidShow', handleKeyboard);
Keyboard.removeListener('keyboardDidHide', handleKeyboard);
}
else {
Keyboard.removeListener('keyboardWillChangeFrame', handleKeyboard);
}
};
}, [handleLayout]);
if (Platform.OS === 'android') {
return (React.createElement(View, { ref: view, onLayout: handleLayout, pointerEvents: "none", style: StyleSheet.absoluteFill, testID: "AirshipBarometer" }, children));
}
return (React.createElement(SafeAreaView, { ref: view, onLayout: handleLayout, pointerEvents: "none", style: StyleSheet.absoluteFill, testID: "AirshipBarometer" },
React.createElement(View, { ref: childView, style: { flex: 1 }, testID: "AirshipBarometerChild" }, children)));
}
export { Unsubscribe } from 'yavent';
export { makeAirship } from './components/Airship';
export { AirshipDropdown, AirshipDropdownProps } from './components/AirshipDropdown';
export { AirshipModal, AirshipModalProps } from './components/AirshipModal';
export { AirshipToast, AirshipToastProps } from './components/AirshipToast';
export { Airship, AirshipBridge } from './types';
export { makeAirship } from './components/Airship';
export { AirshipDropdown } from './components/AirshipDropdown';
export { AirshipModal } from './components/AirshipModal';
export { AirshipToast } from './components/AirshipToast';

Sorry, the diff of this file is not supported yet

import * as React from 'react';
import { OnEvents } from 'yavent';
export interface AirshipEvents {
result: undefined;
clear: undefined;
}
/**
* Control panel for managing a component inside an airship.
*/
export interface AirshipBridge<T> {
resolve: (value: T | PromiseLike<T>) => void;
reject: (error: Error) => void;
remove: () => void;
on: OnEvents<AirshipEvents>;
onResult: (callback: () => unknown) => void;
}
/**
* Renders a component to place inside the airship.
*/
export declare type AirshipRender<T> = (bridge: AirshipBridge<T>) => React.ReactNode;
/**
* Props the Airship container component accepts.
*/
export interface AirshipProps {
children?: React.ReactNode;
}
/**
* The Airship itself is a component you should mount after your main
* scene or router.
*
* It has a static method anyone can call to display components.
* The method returns a promise, which the component can use to pass values
* to the outside world.
*/
export interface Airship extends React.FunctionComponent<AirshipProps> {
clear: () => void;
show: <T>(render: AirshipRender<T>) => Promise<T>;
}
export {};
/**
* The four sides (top, right, bottom, left) as a tuple.
*/
export declare type SideList = [number, number, number, number];
export interface Margin {
marginBottom: number;
marginLeft: number;
marginRight: number;
marginTop: number;
}
export interface Offset {
bottom: number;
left: number;
right: number;
top: number;
}
export interface Padding {
paddingBottom: number;
paddingLeft: number;
paddingRight: number;
paddingTop: number;
}
/**
* Interprets an array of 0-4 numbers as a web CSS sides shorthand
* (top, right, bottom, left).
*/
export declare function fixSides(sides: number[] | number | undefined, fallback: number): SideList;
export declare function addSides(a: SideList, b: SideList): SideList;
export declare function subtractSides(a: SideList, b: SideList): SideList;
export declare function mapSides(sides: SideList, f: (side: number) => number): SideList;
/**
* Turns a list of sides into CSS margin properties.
*/
export declare function sidesToMargin(sides: SideList): Margin;
/**
* Turns a list of sides into CSS positioning properties.
*/
export declare function sidesToOffset(sides: SideList): Offset;
/**
* Turns a list of sides into CSS padding properties.
*/
export declare function sidesToPadding(sides: SideList): Padding;
/**
* Interprets an array of 0-4 numbers as a web CSS sides shorthand
* (top, right, bottom, left).
*/
export function fixSides(sides, fallback) {
var _a, _b, _c, _d;
if (sides == null) {
return [fallback, fallback, fallback, fallback];
}
if (typeof sides === 'number') {
return [sides, sides, sides, sides];
}
const top = (_a = sides[0]) !== null && _a !== void 0 ? _a : fallback;
const right = (_b = sides[1]) !== null && _b !== void 0 ? _b : top;
const bottom = (_c = sides[2]) !== null && _c !== void 0 ? _c : top;
const left = (_d = sides[3]) !== null && _d !== void 0 ? _d : right;
return [top, right, bottom, left];
}
export function addSides(a, b) {
return [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]];
}
export function subtractSides(a, b) {
return [a[0] - b[0], a[1] - b[1], a[2] - b[2], a[3] - b[3]];
}
export function mapSides(sides, f) {
return [f(sides[0]), f(sides[1]), f(sides[2]), f(sides[3])];
}
/**
* Turns a list of sides into CSS margin properties.
*/
export function sidesToMargin(sides) {
return {
marginTop: sides[0],
marginRight: sides[1],
marginBottom: sides[2],
marginLeft: sides[3]
};
}
/**
* Turns a list of sides into CSS positioning properties.
*/
export function sidesToOffset(sides) {
return {
top: sides[0],
right: sides[1],
bottom: sides[2],
left: sides[3]
};
}
/**
* Turns a list of sides into CSS padding properties.
*/
export function sidesToPadding(sides) {
return {
paddingTop: sides[0],
paddingRight: sides[1],
paddingBottom: sides[2],
paddingLeft: sides[3]
};
}
+15
-8
# react-native-airship
## 0.2.5
## 0.2.6 (2021-07-15)
- fix: Make the Flow `Airship` type work like the Typescript version.
- feature: Add optional shadow properties to the `AirshipModal`:
- `shadowOffset`
- `shadowOpacity`
- `shadowRadius`
## 0.2.4
## 0.2.5 (2021-04-15)
- fix: Make the `Airship` Flow type work like the Typescript version.
## 0.2.4 (2021-03-25)
- fix: Do not crash when calling `Airship.clear` recursively.
## 0.2.3
## 0.2.3 (2021-01-23)

@@ -17,15 +24,15 @@ - fix: Measure the screen by mounting a test component and seeing where it lands, avoiding the need for various properties to control the layout. If the status bar is translucent, or if they keyboard is `adjustPan` mode on Android, we can automatically determine that now and do the right thing.

## 0.2.2
## 0.2.2 (2020-09-03)
- feature: Add an `Airship.clear` method, which calls any callbacks registered with `bridge.on('clear')`.
- feature: Add a `bridge.on('result')` method to replace `bridge.onResult`.
- fix: Add some missing Flow type defitions.
- fix: Add some missing Flow type definitions.
- fix: Make the Typescript definitions work better with strict mode.
- deprecated: `bridge.onResult`
## 0.2.1
## 0.2.1 (2020-08-11)
- Fix various Flow & documentation issues from the previous release.
## 0.2.0
## 0.2.0(2020-08-07)

@@ -32,0 +39,0 @@ With this version, the demo components become an official part of the library. The old `react-native-airship/demos` entry point has gone away, so you can import `AirshipDropdown`, `AirshipModal`, and `AirshipToast` directly from `react-native-airship` now.

{
"name": "react-native-airship",
"version": "0.2.5",
"version": "0.2.6",
"private": false,

@@ -15,4 +15,4 @@ "description": "Flexible toolkit for building modals & alerts",

"author": "William Swanson <swansontec@gmail.com>",
"main": "lib/src/index.js",
"types": "lib/src/index.d.ts",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [

@@ -22,18 +22,13 @@ "/CHANGELOG.md",

"/package.json",
"/README.md",
"/src/*"
"/README.md"
],
"scripts": {
"build.flow": "flow",
"build.lib": "tsc && cp src/index.flow.js lib/src/index.js.flow && rimraf lib/AirshipDemo",
"build.lib": "tsc && cp src/index.flow.js lib/index.js.flow",
"clean": "rimraf lib",
"fix": "npm run lint -- --fix",
"lint": "eslint .",
"prepare": "npm-run-all clean -p build.*"
"precommit": "lint-staged && run-p build.* && (cd AirshipDemo; tsc)",
"prepare": "husky install && npm-run-all clean -p build.*"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged && run-p build.*"
}
},
"lint-staged": {

@@ -46,3 +41,3 @@ "*.{js,jsx,ts,tsx}": "eslint"

"devDependencies": {
"@types/react": "^16.9.43",
"@types/react": "^17.0.14",
"@types/react-native": "^0.63.2",

@@ -62,3 +57,3 @@ "@typescript-eslint/eslint-plugin": "^4.8.2",

"flow-bin": "^0.132.0",
"husky": "^4.3.0",
"husky": "^7.0.0",
"lint-staged": "^10.5.3",

@@ -65,0 +60,0 @@ "npm-run-all": "^4.1.5",

import { Airship } from '../types';
/**
* Constructs an Airship component.
*/
export declare function makeAirship(): Airship;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import * as React from 'react';
import { View } from 'react-native';
import { makeEvent, makeEvents } from 'yavent';
import { sidesToOffset, sidesToPadding } from '../util/sides';
import { Barometer } from './Barometer';
const emptyLayout = {
offset: sidesToOffset([0, 0, 0, 0]),
padding: sidesToPadding([0, 0, 0, 0])
};
/**
* Constructs an Airship component.
*/
export function makeAirship() {
// Static state shared by all mounted containers:
const [onClear, emitClear] = makeEvent();
const [onGuestsChange, emitGuestsChange] = makeEvent();
let guests = [];
let nextKey = 0;
const AirshipHost = (props) => {
const { children } = props;
// Watch the common guest list:
const [ourGuests, setGuests] = React.useState(guests);
React.useEffect(() => onGuestsChange(setGuests), []);
// Track layout changes:
const [layout, setLayout] = React.useState(emptyLayout);
return (React.createElement(React.Fragment, null,
React.createElement(Barometer, { onLayout: setLayout }),
children,
ourGuests.map(guest => (React.createElement(View, { key: guest.key, pointerEvents: "box-none", style: Object.assign(Object.assign(Object.assign({}, layout.offset), layout.padding), { flexDirection: 'row', justifyContent: 'center', position: 'absolute' }) }, guest.element)))));
};
let clearing = false;
function clear() {
if (clearing)
return;
clearing = true;
emitClear(undefined);
clearing = false;
}
function show(render) {
return __awaiter(this, void 0, void 0, function* () {
const key = `airship${nextKey++}`;
function remove() {
unclear();
guests = guests.filter(guest => guest.key !== key);
emitGuestsChange(guests);
}
// Assemble the bridge:
const [on, emit] = makeEvents();
let bridge;
const promise = new Promise((resolve, reject) => {
bridge = {
on,
onResult: callback => on('result', callback),
reject,
remove,
resolve
};
});
// Hook up events:
promise.then(() => emit('result', undefined), () => emit('result', undefined));
const unclear = onClear(() => emit('clear', undefined));
// Save the guest element in the shared state:
guests = [...guests, { key, element: render(bridge) }];
emitGuestsChange(guests);
return promise;
});
}
return Object.assign(AirshipHost, { clear, show });
}
//# sourceMappingURL=Airship.js.map
{"version":3,"file":"Airship.js","sourceRoot":"","sources":["../../../src/components/Airship.tsx"],"names":[],"mappings":"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"}
import * as React from 'react';
import { ViewStyle } from 'react-native';
import { AirshipBridge } from '../types';
export interface AirshipDropdownProps {
bridge: AirshipBridge<undefined>;
children?: React.ReactNode;
onPress?: () => void;
autoHideMs?: number;
backgroundColor?: string;
borderRadius?: number;
flexDirection?: ViewStyle['flexDirection'];
justifyContent?: ViewStyle['justifyContent'];
margin?: number | number[];
maxHeight?: number;
maxWidth?: number;
padding?: number | number[];
slideInMs?: number;
slideOutMs?: number;
}
/**
* A notification that slides down from the top of the screen.
*/
export declare function AirshipDropdown(props: AirshipDropdownProps): JSX.Element;
import * as React from 'react';
import { Animated, Dimensions, TouchableWithoutFeedback } from 'react-native';
import { fixSides, sidesToMargin, sidesToPadding } from '../util/sides';
const safeAreaGap = 64;
/**
* A notification that slides down from the top of the screen.
*/
export function AirshipDropdown(props) {
const { bridge, children, onPress = () => bridge.resolve(undefined), autoHideMs = 5000, backgroundColor = 'white', borderRadius = 4, flexDirection, justifyContent, maxHeight = defaultMaxHeight(), maxWidth = 512, slideInMs = 300, slideOutMs = 500 } = props;
const margin = sidesToMargin(fixSides(props.margin, 0));
const padding = sidesToPadding(fixSides(props.padding, 0));
const hiddenOffset = -(maxHeight + margin.marginBottom);
margin.marginTop = -safeAreaGap;
padding.paddingTop += safeAreaGap;
// Create the animation:
const offset = React.useRef(new Animated.Value(hiddenOffset)).current;
React.useEffect(() => {
let timeout;
// Animate in:
Animated.timing(offset, {
toValue: 0,
duration: slideInMs,
useNativeDriver: true
}).start(() => {
// Start the auto-hide timer:
if (autoHideMs > 0) {
timeout = setTimeout(() => {
timeout = undefined;
bridge.resolve(undefined);
}, autoHideMs);
}
});
// Animate out:
bridge.on('clear', () => bridge.resolve(undefined));
bridge.on('result', () => {
Animated.timing(offset, {
toValue: hiddenOffset,
duration: slideOutMs,
useNativeDriver: true
}).start(() => bridge.remove());
});
return () => {
if (timeout != null)
clearTimeout(timeout);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const bodyStyle = Object.assign(Object.assign(Object.assign({}, margin), padding), { alignSelf: 'flex-start', backgroundColor, borderBottomLeftRadius: borderRadius, borderBottomRightRadius: borderRadius, flexDirection, flexShrink: 1, justifyContent,
maxHeight, shadowOffset: { height: 0, width: 0 }, shadowOpacity: 1, shadowRadius: 4, transform: [{ translateY: offset }], width: maxWidth // This works because flexShrink is set
});
return (React.createElement(TouchableWithoutFeedback, { onPress: onPress },
React.createElement(Animated.View, { style: bodyStyle }, children)));
}
function defaultMaxHeight() {
const { width, height } = Dimensions.get('screen');
return 0.25 * Math.max(width, height);
}
//# sourceMappingURL=AirshipDropdown.js.map
{"version":3,"file":"AirshipDropdown.js","sourceRoot":"","sources":["../../../src/components/AirshipDropdown.tsx"],"names":[],"mappings":"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"}
import * as React from 'react';
import { ViewStyle } from 'react-native';
import { AirshipBridge } from '../types';
export interface AirshipModalProps<T = unknown> {
bridge: AirshipBridge<T>;
children?: React.ReactNode;
onCancel: () => void;
center?: boolean;
backgroundColor?: string;
borderRadius?: number;
flexDirection?: ViewStyle['flexDirection'];
justifyContent?: ViewStyle['justifyContent'];
margin?: number | number[];
maxHeight?: number;
maxWidth?: number;
padding?: number | number[];
slideInMs?: number;
slideOutMs?: number;
underlay?: string | React.ReactElement;
}
/**
* A modal that slides a modal up from the bottom of the screen
* and dims the rest of the app.
*/
export declare function AirshipModal<T>(props: AirshipModalProps<T>): JSX.Element;
import * as React from 'react';
import { Animated, BackHandler, Dimensions, TouchableWithoutFeedback } from 'react-native';
import { fixSides, sidesToMargin, sidesToPadding } from '../util/sides';
const safeAreaGap = 64;
/**
* A modal that slides a modal up from the bottom of the screen
* and dims the rest of the app.
*/
export function AirshipModal(props) {
const { bridge, children, onCancel, backgroundColor = 'white', borderRadius = 10, center = false, flexDirection, justifyContent, maxHeight, maxWidth = 512, slideInMs = 300, slideOutMs = 300, underlay = 'rgba(0, 0, 0, 0.75)' } = props;
const margin = sidesToMargin(fixSides(props.margin, 0));
const padding = sidesToPadding(fixSides(props.padding, 0));
React.useEffect(() => bridge.on('clear', onCancel), [bridge, onCancel]);
// Create the animations:
const offset = React.useRef(new Animated.Value(Dimensions.get('window').height)).current;
const opacity = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
// Animate in:
Animated.parallel([
Animated.timing(opacity, {
toValue: 1,
duration: slideInMs,
useNativeDriver: true
}),
Animated.timing(offset, {
toValue: 0,
duration: slideInMs,
useNativeDriver: true
})
]).start();
// Animate out:
bridge.on('result', () => {
Animated.parallel([
Animated.timing(opacity, {
toValue: 0,
duration: slideOutMs,
useNativeDriver: true
}),
Animated.timing(offset, {
toValue: Dimensions.get('window').height,
duration: slideOutMs,
useNativeDriver: true
})
]).start(bridge.remove);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Set up the back-button handler:
React.useEffect(() => {
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
onCancel();
return true;
});
return () => backHandler.remove();
}, [onCancel]);
const underlayStyle = {
backgroundColor: typeof underlay === 'string' ? underlay : 'transparent',
bottom: 0,
left: 0,
opacity: opacity,
position: 'absolute',
right: 0,
top: 0
};
const bodyCommon = Object.assign(Object.assign(Object.assign({}, margin), padding), { alignSelf: center ? 'center' : 'flex-end', backgroundColor,
flexDirection, flexShrink: 1, justifyContent,
maxHeight, shadowOffset: { height: 0, width: 0 }, shadowOpacity: 1, shadowRadius: 10, transform: [{ translateY: offset }], width: maxWidth // This works because flexShrink is set
});
const bodyStyle = center
? Object.assign(Object.assign({}, bodyCommon), { borderRadius }) : Object.assign(Object.assign({}, bodyCommon), { borderTopLeftRadius: borderRadius, borderTopRightRadius: borderRadius, marginBottom: -safeAreaGap, paddingBottom: padding.paddingBottom + safeAreaGap });
return (React.createElement(React.Fragment, null,
React.createElement(TouchableWithoutFeedback, { onPress: () => onCancel() },
React.createElement(Animated.View, { style: underlayStyle }, typeof underlay !== 'string' ? underlay : undefined)),
React.createElement(Animated.View, { style: bodyStyle }, children)));
}
//# sourceMappingURL=AirshipModal.js.map
{"version":3,"file":"AirshipModal.js","sourceRoot":"","sources":["../../../src/components/AirshipModal.tsx"],"names":[],"mappings":"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"}
import * as React from 'react';
import { AirshipBridge } from '../types';
export interface AirshipToastProps {
bridge: AirshipBridge<undefined>;
children?: React.ReactNode;
message?: string;
autoHideMs?: number;
backgroundColor?: string;
borderRadius?: number;
fadeInMs?: number;
fadeOutMs?: number;
margin?: number | number[];
maxWidth?: number;
opacity?: number;
padding?: number | number[];
textColor?: string;
textSize?: number;
}
/**
* A semi-transparent message overlay.
*/
export declare function AirshipToast(props: AirshipToastProps): JSX.Element;
import * as React from 'react';
import { Animated, Text } from 'react-native';
import { fixSides, sidesToMargin, sidesToPadding } from '../util/sides';
/**
* A semi-transparent message overlay.
*/
export function AirshipToast(props) {
const { textSize = 14 } = props;
const { autoHideMs = 3000, backgroundColor = 'white', borderRadius = 1.5 * textSize, bridge, children, fadeInMs = 300, fadeOutMs = 1000, maxWidth = 512, opacity: finalOpacity = 0.9, message, textColor = 'black' } = props;
const margin = sidesToMargin(fixSides(props.margin, 2 * textSize));
const padding = sidesToPadding(fixSides(props.padding, textSize));
// Create the animation:
const opacity = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
let timeout;
// Animate in:
Animated.timing(opacity, {
toValue: finalOpacity,
duration: fadeInMs,
useNativeDriver: true
}).start(() => {
// Start the auto-hide timer:
if (autoHideMs > 0) {
timeout = setTimeout(() => {
timeout = undefined;
bridge.resolve(undefined);
}, autoHideMs);
}
});
// Animate out:
bridge.on('clear', () => bridge.resolve(undefined));
bridge.on('result', () => {
Animated.timing(opacity, {
toValue: 0,
duration: fadeOutMs,
useNativeDriver: true
}).start(() => bridge.remove());
});
return () => {
if (timeout != null)
clearTimeout(timeout);
};
});
const bodyStyle = Object.assign(Object.assign(Object.assign({}, margin), padding), { alignItems: 'center', alignSelf: 'flex-end', backgroundColor,
borderRadius, flexDirection: 'row', justifyContent: 'flex-start', maxWidth, opacity: opacity });
const textStyle = {
color: textColor,
flexShrink: 1,
fontSize: textSize,
textAlign: 'center'
};
return (React.createElement(Animated.View, { style: bodyStyle },
message != null ? React.createElement(Text, { style: textStyle }, message) : null,
children));
}
//# sourceMappingURL=AirshipToast.js.map
{"version":3,"file":"AirshipToast.js","sourceRoot":"","sources":["../../../src/components/AirshipToast.tsx"],"names":[],"mappings":"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"}
import * as React from 'react';
import { Offset, Padding } from '../util/sides';
export interface BarometerLayout {
offset: Offset;
padding: Padding;
}
interface Props {
children?: React.ReactNode;
onLayout?: (layout: BarometerLayout) => void;
}
/**
* Measures various things about the Airship environment,
* so we know how to position our children.
*
* This component mounts a view with absolute positioning,
* and then measures that view relative to the window.
* If a side is inset from the window edge, we use a negative offset
* to expand it outward. If a side extends beyond the window edge,
* we use padding to push the content inward.
*
* On iOS, we also mount a child inside a SafeAreaView, to measure
* the safe area insets. We add these to the padding.
*
* Finally, we keep track of the keyboard, adding extra padding &
* scheduling animations as needed.
*/
export declare function Barometer(props: Props): JSX.Element;
export {};
import * as React from 'react';
import { Dimensions, Keyboard, Platform, SafeAreaView, StyleSheet, View } from 'react-native';
import { addSides, mapSides, sidesToOffset, sidesToPadding, subtractSides } from '../util/sides';
const emptySides = [0, 0, 0, 0];
/**
* Measures various things about the Airship environment,
* so we know how to position our children.
*
* This component mounts a view with absolute positioning,
* and then measures that view relative to the window.
* If a side is inset from the window edge, we use a negative offset
* to expand it outward. If a side extends beyond the window edge,
* we use padding to push the content inward.
*
* On iOS, we also mount a child inside a SafeAreaView, to measure
* the safe area insets. We add these to the padding.
*
* Finally, we keep track of the keyboard, adding extra padding &
* scheduling animations as needed.
*/
export function Barometer(props) {
const { children, onLayout = () => { } } = props;
// Mutable state:
const keyboardHeight = React.useRef(0);
const lastLayoutJson = React.useRef('');
const view = React.useRef(null);
const childView = React.useRef(null);
// Handle layout changes:
const handleLayout = React.useCallback(() => {
// Measure the view in the window:
const viewPromise = new Promise(resolve => {
if (view.current == null)
return resolve(emptySides);
view.current.measureInWindow((x, y, width, height) => {
const window = Dimensions.get('window');
resolve([y, window.width - width - x, window.height - height - y, x]);
});
});
// Measure the child view in the window:
const childPromise = new Promise(resolve => {
if (childView.current == null)
return resolve(viewPromise);
childView.current.measureInWindow((x, y, width, height) => {
const window = Dimensions.get('window');
resolve([y, window.width - width - x, window.height - height - y, x]);
});
});
// Measure the gap between the bottom of the screen and the view:
const bottomPromise = new Promise(resolve => {
if (view.current == null)
return 0;
view.current.measure((x, y, width, height, screenX, screenY) => {
const screen = Dimensions.get('screen');
resolve(screen.height - height - screenY);
});
});
// Combine the results, then call the callback:
Promise.all([viewPromise, childPromise, bottomPromise])
.then(([viewOffset, childOffset, bottomGap]) => {
// Cancel out any offset, so we cover the full window:
const offset = mapSides(viewOffset, side => -Math.max(side, 0));
// If the offset is negative, issue positive padding,
// plus any safe area:
const safePadding = subtractSides(childOffset, viewOffset);
const padding = addSides(safePadding, mapSides(viewOffset, side => Math.abs(side)));
// Use the keyboard padding, if needed:
const keyboardPadding = Math.max(keyboardHeight.current - bottomGap - offset[2], 0);
padding[2] = Math.max(padding[2], keyboardPadding);
// Send an update if we have changes:
const string = JSON.stringify([offset, padding]);
if (string !== lastLayoutJson.current) {
lastLayoutJson.current = string;
onLayout({
offset: sidesToOffset(offset),
padding: sidesToPadding(padding)
});
}
})
.catch(() => { });
}, [onLayout]);
// Subscribe to keyboard changes:
React.useEffect(() => {
const handleKeyboard = event => {
const screen = Dimensions.get('screen');
keyboardHeight.current = Math.min(
// These two give different results sometimes, so pick the smaller one:
screen.height - event.endCoordinates.screenY, event.endCoordinates.height);
if (event.duration > 0) {
// @ts-expect-error
Keyboard.scheduleLayoutAnimation(event);
}
handleLayout();
};
if (Platform.OS === 'android') {
Keyboard.addListener('keyboardDidShow', handleKeyboard);
Keyboard.addListener('keyboardDidHide', handleKeyboard);
}
else {
Keyboard.addListener('keyboardWillChangeFrame', handleKeyboard);
}
return () => {
if (Platform.OS === 'android') {
Keyboard.removeListener('keyboardDidShow', handleKeyboard);
Keyboard.removeListener('keyboardDidHide', handleKeyboard);
}
else {
Keyboard.removeListener('keyboardWillChangeFrame', handleKeyboard);
}
};
}, [handleLayout]);
if (Platform.OS === 'android') {
return (React.createElement(View, { ref: view, onLayout: handleLayout, pointerEvents: "none", style: StyleSheet.absoluteFill, testID: "AirshipBarometer" }, children));
}
return (React.createElement(SafeAreaView, { ref: view, onLayout: handleLayout, pointerEvents: "none", style: StyleSheet.absoluteFill, testID: "AirshipBarometer" },
React.createElement(View, { ref: childView, style: { flex: 1 }, testID: "AirshipBarometerChild" }, children)));
}
//# sourceMappingURL=Barometer.js.map
{"version":3,"file":"Barometer.js","sourceRoot":"","sources":["../../../src/components/Barometer.tsx"],"names":[],"mappings":"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"}
export { Unsubscribe } from 'yavent';
export { makeAirship } from './components/Airship';
export { AirshipDropdown, AirshipDropdownProps } from './components/AirshipDropdown';
export { AirshipModal, AirshipModalProps } from './components/AirshipModal';
export { AirshipToast, AirshipToastProps } from './components/AirshipToast';
export { Airship, AirshipBridge } from './types';
export { makeAirship } from './components/Airship';
export { AirshipDropdown } from './components/AirshipDropdown';
export { AirshipModal } from './components/AirshipModal';
export { AirshipToast } from './components/AirshipToast';
//# sourceMappingURL=index.js.map

Sorry, the diff of this file is not supported yet

{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAClD,OAAO,EACL,eAAe,EAEhB,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAAE,YAAY,EAAqB,MAAM,2BAA2B,CAAA;AAC3E,OAAO,EAAE,YAAY,EAAqB,MAAM,2BAA2B,CAAA"}
import * as React from 'react';
import { OnEvents } from 'yavent';
export interface AirshipEvents {
result: undefined;
clear: undefined;
}
/**
* Control panel for managing a component inside an airship.
*/
export interface AirshipBridge<T> {
resolve: (value: T | PromiseLike<T>) => void;
reject: (error: Error) => void;
remove: () => void;
on: OnEvents<AirshipEvents>;
onResult: (callback: () => unknown) => void;
}
/**
* Renders a component to place inside the airship.
*/
export declare type AirshipRender<T> = (bridge: AirshipBridge<T>) => React.ReactNode;
/**
* Props the Airship container component accepts.
*/
export interface AirshipProps {
children?: React.ReactNode;
}
/**
* The Airship itself is a component you should mount after your main
* scene or router.
*
* It has a static method anyone can call to display components.
* The method returns a promise, which the component can use to pass values
* to the outside world.
*/
export interface Airship extends React.FunctionComponent<AirshipProps> {
clear: () => void;
show: <T>(render: AirshipRender<T>) => Promise<T>;
}
export {};
//# sourceMappingURL=types.js.map
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
/**
* The four sides (top, right, bottom, left) as a tuple.
*/
export declare type SideList = [number, number, number, number];
export interface Margin {
marginBottom: number;
marginLeft: number;
marginRight: number;
marginTop: number;
}
export interface Offset {
bottom: number;
left: number;
right: number;
top: number;
}
export interface Padding {
paddingBottom: number;
paddingLeft: number;
paddingRight: number;
paddingTop: number;
}
/**
* Interprets an array of 0-4 numbers as a web CSS sides shorthand
* (top, right, bottom, left).
*/
export declare function fixSides(sides: number[] | number | undefined, fallback: number): SideList;
export declare function addSides(a: SideList, b: SideList): SideList;
export declare function subtractSides(a: SideList, b: SideList): SideList;
export declare function mapSides(sides: SideList, f: (side: number) => number): SideList;
/**
* Turns a list of sides into CSS margin properties.
*/
export declare function sidesToMargin(sides: SideList): Margin;
/**
* Turns a list of sides into CSS positioning properties.
*/
export declare function sidesToOffset(sides: SideList): Offset;
/**
* Turns a list of sides into CSS padding properties.
*/
export declare function sidesToPadding(sides: SideList): Padding;
/**
* Interprets an array of 0-4 numbers as a web CSS sides shorthand
* (top, right, bottom, left).
*/
export function fixSides(sides, fallback) {
var _a, _b, _c, _d;
if (sides == null) {
return [fallback, fallback, fallback, fallback];
}
if (typeof sides === 'number') {
return [sides, sides, sides, sides];
}
const top = (_a = sides[0]) !== null && _a !== void 0 ? _a : fallback;
const right = (_b = sides[1]) !== null && _b !== void 0 ? _b : top;
const bottom = (_c = sides[2]) !== null && _c !== void 0 ? _c : top;
const left = (_d = sides[3]) !== null && _d !== void 0 ? _d : right;
return [top, right, bottom, left];
}
export function addSides(a, b) {
return [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]];
}
export function subtractSides(a, b) {
return [a[0] - b[0], a[1] - b[1], a[2] - b[2], a[3] - b[3]];
}
export function mapSides(sides, f) {
return [f(sides[0]), f(sides[1]), f(sides[2]), f(sides[3])];
}
/**
* Turns a list of sides into CSS margin properties.
*/
export function sidesToMargin(sides) {
return {
marginTop: sides[0],
marginRight: sides[1],
marginBottom: sides[2],
marginLeft: sides[3]
};
}
/**
* Turns a list of sides into CSS positioning properties.
*/
export function sidesToOffset(sides) {
return {
top: sides[0],
right: sides[1],
bottom: sides[2],
left: sides[3]
};
}
/**
* Turns a list of sides into CSS padding properties.
*/
export function sidesToPadding(sides) {
return {
paddingTop: sides[0],
paddingRight: sides[1],
paddingBottom: sides[2],
paddingLeft: sides[3]
};
}
//# sourceMappingURL=sides.js.map
{"version":3,"file":"sides.js","sourceRoot":"","sources":["../../../src/util/sides.ts"],"names":[],"mappings":"AA0BA;;;GAGG;AACH,MAAM,UAAU,QAAQ,CACtB,KAAoC,EACpC,QAAgB;;IAEhB,IAAI,KAAK,IAAI,IAAI,EAAE;QACjB,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;KAChD;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;QAC7B,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;KACpC;IAED,MAAM,GAAG,SAAG,KAAK,CAAC,CAAC,CAAC,mCAAI,QAAQ,CAAA;IAChC,MAAM,KAAK,SAAG,KAAK,CAAC,CAAC,CAAC,mCAAI,GAAG,CAAA;IAC7B,MAAM,MAAM,SAAG,KAAK,CAAC,CAAC,CAAC,mCAAI,GAAG,CAAA;IAC9B,MAAM,IAAI,SAAG,KAAK,CAAC,CAAC,CAAC,mCAAI,KAAK,CAAA;IAC9B,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,CAAA;AACnC,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,CAAW,EAAE,CAAW;IAC/C,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC7D,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,CAAW,EAAE,CAAW;IACpD,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC7D,CAAC;AAED,MAAM,UAAU,QAAQ,CACtB,KAAe,EACf,CAA2B;IAE3B,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC7D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,KAAe;IAC3C,OAAO;QACL,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;QACnB,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;QACrB,YAAY,EAAE,KAAK,CAAC,CAAC,CAAC;QACtB,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC;KACrB,CAAA;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,KAAe;IAC3C,OAAO;QACL,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;QACb,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QACf,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;QAChB,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;KACf,CAAA;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAe;IAC5C,OAAO;QACL,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC;QACpB,YAAY,EAAE,KAAK,CAAC,CAAC,CAAC;QACtB,aAAa,EAAE,KAAK,CAAC,CAAC,CAAC;QACvB,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;KACtB,CAAA;AACH,CAAC"}
import * as React from 'react'
import { View } from 'react-native'
import { Event, Events, makeEvent, makeEvents } from 'yavent'
import {
Airship,
AirshipBridge,
AirshipEvents,
AirshipProps,
AirshipRender
} from '../types'
import { sidesToOffset, sidesToPadding } from '../util/sides'
import { Barometer, BarometerLayout } from './Barometer'
interface Guest {
key: string
element: React.ReactNode
}
const emptyLayout: BarometerLayout = {
offset: sidesToOffset([0, 0, 0, 0]),
padding: sidesToPadding([0, 0, 0, 0])
}
/**
* Constructs an Airship component.
*/
export function makeAirship(): Airship {
// Static state shared by all mounted containers:
const [onClear, emitClear]: Event<void> = makeEvent()
const [onGuestsChange, emitGuestsChange]: Event<Guest[]> = makeEvent()
let guests: Guest[] = []
let nextKey: number = 0
const AirshipHost = (props: AirshipProps): JSX.Element => {
const { children } = props
// Watch the common guest list:
const [ourGuests, setGuests] = React.useState(guests)
React.useEffect(() => onGuestsChange(setGuests), [])
// Track layout changes:
const [layout, setLayout] = React.useState(emptyLayout)
return (
<>
<Barometer onLayout={setLayout} />
{children}
{ourGuests.map(guest => (
<View
key={guest.key}
pointerEvents="box-none"
style={{
...layout.offset,
...layout.padding,
flexDirection: 'row',
justifyContent: 'center',
position: 'absolute'
}}
>
{guest.element}
</View>
))}
</>
)
}
let clearing = false
function clear(): void {
if (clearing) return
clearing = true
emitClear(undefined)
clearing = false
}
async function show<T>(render: AirshipRender<T>): Promise<T> {
const key = `airship${nextKey++}`
function remove(): void {
unclear()
guests = guests.filter(guest => guest.key !== key)
emitGuestsChange(guests)
}
// Assemble the bridge:
const [on, emit]: Events<AirshipEvents> = makeEvents()
let bridge!: AirshipBridge<T>
const promise: Promise<T> = new Promise((resolve, reject) => {
bridge = {
on,
onResult: callback => on('result', callback),
reject,
remove,
resolve
}
})
// Hook up events:
promise.then(
() => emit('result', undefined),
() => emit('result', undefined)
)
const unclear = onClear(() => emit('clear', undefined))
// Save the guest element in the shared state:
guests = [...guests, { key, element: render(bridge) }]
emitGuestsChange(guests)
return promise
}
return Object.assign(AirshipHost, { clear, show })
}
import * as React from 'react'
import {
Animated,
Dimensions,
TouchableWithoutFeedback,
ViewStyle
} from 'react-native'
import { AirshipBridge } from '../types'
import { fixSides, sidesToMargin, sidesToPadding } from '../util/sides'
export interface AirshipDropdownProps {
bridge: AirshipBridge<undefined>
children?: React.ReactNode
// Called when the user taps anywhere in the dropdown.
// Defaults to hiding the dropdown.
onPress?: () => void
// Determines how long the dropdown remains visible,
// or 0 to disable auto-hide. Defaults to 5000ms.
autoHideMs?: number
// The component color. Defaults to white.
backgroundColor?: string
// The radius to use on the bottom corners. Defaults to 4.
borderRadius?: number
// The flex direction for the contents.
flexDirection?: ViewStyle['flexDirection']
// How to justify the contents along the flex direction.
justifyContent?: ViewStyle['justifyContent']
// The minimum gap between the component and the screen edges.
// Takes 0-4 numbers (top, right, bottom, left),
// using the same logic as the web `margin` property. Defaults to 0.
margin?: number | number[]
// The maximum height the component will be.
// Defaults to 25% of the longest screen dimension.
maxHeight?: number
// The maximum width the component will be.
// Defaults to 512.
maxWidth?: number
// Internal padding to place inside the component.
// Takes 0-4 numbers (top, right, bottom, left),
// using the same logic as the web `padding` property. Defaults to 0.
padding?: number | number[]
// How long the entry animation should be. Defaults to 300ms.
slideInMs?: number
// How long the exit animation should be. Defaults to 500ms.
slideOutMs?: number
}
const safeAreaGap = 64
/**
* A notification that slides down from the top of the screen.
*/
export function AirshipDropdown(props: AirshipDropdownProps): JSX.Element {
const {
bridge,
children,
onPress = () => bridge.resolve(undefined),
autoHideMs = 5000,
backgroundColor = 'white',
borderRadius = 4,
flexDirection,
justifyContent,
maxHeight = defaultMaxHeight(),
maxWidth = 512,
slideInMs = 300,
slideOutMs = 500
} = props
const margin = sidesToMargin(fixSides(props.margin, 0))
const padding = sidesToPadding(fixSides(props.padding, 0))
const hiddenOffset = -(maxHeight + margin.marginBottom)
margin.marginTop = -safeAreaGap
padding.paddingTop += safeAreaGap
// Create the animation:
const offset = React.useRef(new Animated.Value(hiddenOffset)).current
React.useEffect(() => {
let timeout: ReturnType<typeof setTimeout> | undefined
// Animate in:
Animated.timing(offset, {
toValue: 0,
duration: slideInMs,
useNativeDriver: true
}).start(() => {
// Start the auto-hide timer:
if (autoHideMs > 0) {
timeout = setTimeout(() => {
timeout = undefined
bridge.resolve(undefined)
}, autoHideMs)
}
})
// Animate out:
bridge.on('clear', () => bridge.resolve(undefined))
bridge.on('result', () => {
Animated.timing(offset, {
toValue: hiddenOffset,
duration: slideOutMs,
useNativeDriver: true
}).start(() => bridge.remove())
})
return () => {
if (timeout != null) clearTimeout(timeout)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const bodyStyle: ViewStyle = {
...margin,
...padding,
alignSelf: 'flex-start',
backgroundColor,
borderBottomLeftRadius: borderRadius,
borderBottomRightRadius: borderRadius,
flexDirection,
flexShrink: 1,
justifyContent,
maxHeight,
shadowOffset: { height: 0, width: 0 },
shadowOpacity: 1,
shadowRadius: 4,
transform: [{ translateY: offset as any }],
width: maxWidth // This works because flexShrink is set
}
return (
<TouchableWithoutFeedback onPress={onPress}>
<Animated.View style={bodyStyle}>{children}</Animated.View>
</TouchableWithoutFeedback>
)
}
function defaultMaxHeight(): number {
const { width, height } = Dimensions.get('screen')
return 0.25 * Math.max(width, height)
}
import * as React from 'react'
import {
Animated,
BackHandler,
Dimensions,
TouchableWithoutFeedback,
ViewStyle
} from 'react-native'
import { AirshipBridge } from '../types'
import { fixSides, sidesToMargin, sidesToPadding } from '../util/sides'
export interface AirshipModalProps<T = unknown> {
bridge: AirshipBridge<T>
children?: React.ReactNode
// Called when the user taps outside the modal or clicks the back button:
onCancel: () => void
// True to have the modal float in the center of the screen,
// or false for a bottom modal. Defaults to false.
center?: boolean
// The component color. Defaults to white.
backgroundColor?: string
// The radius to use on the corners. Defaults to 10.
borderRadius?: number
// The flex direction for the contents.
flexDirection?: ViewStyle['flexDirection']
// How to justify the contents along the flex direction.
justifyContent?: ViewStyle['justifyContent']
// The minimum gap between the component and the screen edges.
// Takes 0-4 numbers (top, right, bottom, left),
// using the same logic as the web `margin` property. Defaults to 0.
margin?: number | number[]
// The maximum height the component will be.
// Defaults to no limit.
maxHeight?: number
// The maximum width the component will be.
// Defaults to 512.
maxWidth?: number
// Internal padding to place inside the component.
// Takes 0-4 numbers (top, right, bottom, left),
// using the same logic as the web `padding` property. Defaults to 0.
padding?: number | number[]
// How long the entry animation should be. Defaults to 300ms.
slideInMs?: number
// How long the exit animation should be. Defaults to 300ms.
slideOutMs?: number
// The color of the window underlay,
// or a React element for a custom background.
// Defaults to rgba(0, 0, 0, 0.75).
underlay?: string | React.ReactElement
}
const safeAreaGap = 64
/**
* A modal that slides a modal up from the bottom of the screen
* and dims the rest of the app.
*/
export function AirshipModal<T>(props: AirshipModalProps<T>): JSX.Element {
const {
bridge,
children,
onCancel,
backgroundColor = 'white',
borderRadius = 10,
center = false,
flexDirection,
justifyContent,
maxHeight,
maxWidth = 512,
slideInMs = 300,
slideOutMs = 300,
underlay = 'rgba(0, 0, 0, 0.75)'
} = props
const margin = sidesToMargin(fixSides(props.margin, 0))
const padding = sidesToPadding(fixSides(props.padding, 0))
React.useEffect(() => bridge.on('clear', onCancel), [bridge, onCancel])
// Create the animations:
const offset = React.useRef(
new Animated.Value(Dimensions.get('window').height)
).current
const opacity = React.useRef(new Animated.Value(0)).current
React.useEffect(() => {
// Animate in:
Animated.parallel([
Animated.timing(opacity, {
toValue: 1,
duration: slideInMs,
useNativeDriver: true
}),
Animated.timing(offset, {
toValue: 0,
duration: slideInMs,
useNativeDriver: true
})
]).start()
// Animate out:
bridge.on('result', () => {
Animated.parallel([
Animated.timing(opacity, {
toValue: 0,
duration: slideOutMs,
useNativeDriver: true
}),
Animated.timing(offset, {
toValue: Dimensions.get('window').height,
duration: slideOutMs,
useNativeDriver: true
})
]).start(bridge.remove)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Set up the back-button handler:
React.useEffect(() => {
const backHandler = BackHandler.addEventListener(
'hardwareBackPress',
() => {
onCancel()
return true
}
)
return () => backHandler.remove()
}, [onCancel])
const underlayStyle: ViewStyle = {
backgroundColor: typeof underlay === 'string' ? underlay : 'transparent',
bottom: 0,
left: 0,
opacity: opacity as any,
position: 'absolute',
right: 0,
top: 0
}
const bodyCommon: ViewStyle = {
...margin,
...padding,
alignSelf: center ? 'center' : 'flex-end',
backgroundColor,
flexDirection,
flexShrink: 1,
justifyContent,
maxHeight,
shadowOffset: { height: 0, width: 0 },
shadowOpacity: 1,
shadowRadius: 10,
transform: [{ translateY: offset as any }],
width: maxWidth // This works because flexShrink is set
}
const bodyStyle = center
? {
...bodyCommon,
borderRadius
}
: {
...bodyCommon,
borderTopLeftRadius: borderRadius,
borderTopRightRadius: borderRadius,
marginBottom: -safeAreaGap,
paddingBottom: padding.paddingBottom + safeAreaGap
}
return (
<>
<TouchableWithoutFeedback onPress={() => onCancel()}>
<Animated.View style={underlayStyle}>
{typeof underlay !== 'string' ? underlay : undefined}
</Animated.View>
</TouchableWithoutFeedback>
<Animated.View style={bodyStyle}>{children}</Animated.View>
</>
)
}
import * as React from 'react'
import { Animated, Text, TextStyle, ViewStyle } from 'react-native'
import { AirshipBridge } from '../types'
import { fixSides, sidesToMargin, sidesToPadding } from '../util/sides'
export interface AirshipToastProps {
bridge: AirshipBridge<undefined>
children?: React.ReactNode
// A message to show inside the toast.
// This will come before any other children.
message?: string
// Determines how long the dropdown remains visible,
// or 0 to disable auto-hide. Defaults to 3000ms.
autoHideMs?: number
// The component color. Defaults to grey.
backgroundColor?: string
// The radius to use on the corners.
borderRadius?: number
// How long the entry animation should be. Defaults to 300ms.
fadeInMs?: number
// How long the exit animation should be. Defaults to 500ms.
fadeOutMs?: number
// The minimum gap between the component and the screen edges.
// Takes 0-4 numbers (top, right, bottom, left),
// using the same logic as the web `margin` property.
margin?: number | number[]
// The maximum width the component will be.
maxWidth?: number
// The opacity the component should fade to. Defaults to 0.9.
opacity?: number
// Internal padding to place inside the component.
// Takes 0-4 numbers (top, right, bottom, left),
// using the same logic as the web `padding` property.
padding?: number | number[]
// The color to use for the text. Defaults to black.
textColor?: string
// The size of the text.
textSize?: number
}
/**
* A semi-transparent message overlay.
*/
export function AirshipToast(props: AirshipToastProps): JSX.Element {
const { textSize = 14 } = props
const {
autoHideMs = 3000,
backgroundColor = 'white',
borderRadius = 1.5 * textSize,
bridge,
children,
fadeInMs = 300,
fadeOutMs = 1000,
maxWidth = 512,
opacity: finalOpacity = 0.9,
message,
textColor = 'black'
} = props
const margin = sidesToMargin(fixSides(props.margin, 2 * textSize))
const padding = sidesToPadding(fixSides(props.padding, textSize))
// Create the animation:
const opacity = React.useRef(new Animated.Value(0)).current
React.useEffect(() => {
let timeout: ReturnType<typeof setTimeout> | undefined
// Animate in:
Animated.timing(opacity, {
toValue: finalOpacity,
duration: fadeInMs,
useNativeDriver: true
}).start(() => {
// Start the auto-hide timer:
if (autoHideMs > 0) {
timeout = setTimeout(() => {
timeout = undefined
bridge.resolve(undefined)
}, autoHideMs)
}
})
// Animate out:
bridge.on('clear', () => bridge.resolve(undefined))
bridge.on('result', () => {
Animated.timing(opacity, {
toValue: 0,
duration: fadeOutMs,
useNativeDriver: true
}).start(() => bridge.remove())
})
return () => {
if (timeout != null) clearTimeout(timeout)
}
})
const bodyStyle: ViewStyle = {
...margin,
...padding,
alignItems: 'center',
alignSelf: 'flex-end',
backgroundColor,
borderRadius,
flexDirection: 'row',
justifyContent: 'flex-start',
maxWidth,
opacity: opacity as any
}
const textStyle: TextStyle = {
color: textColor,
flexShrink: 1,
fontSize: textSize,
textAlign: 'center'
}
return (
<Animated.View style={bodyStyle}>
{message != null ? <Text style={textStyle}>{message}</Text> : null}
{children}
</Animated.View>
)
}
import * as React from 'react'
import {
Dimensions,
Keyboard,
KeyboardEventListener,
Platform,
SafeAreaView,
StyleSheet,
View
} from 'react-native'
import {
addSides,
mapSides,
Offset,
Padding,
SideList,
sidesToOffset,
sidesToPadding,
subtractSides
} from '../util/sides'
export interface BarometerLayout {
offset: Offset
padding: Padding
}
interface Props {
children?: React.ReactNode
onLayout?: (layout: BarometerLayout) => void
}
const emptySides: SideList = [0, 0, 0, 0]
/**
* Measures various things about the Airship environment,
* so we know how to position our children.
*
* This component mounts a view with absolute positioning,
* and then measures that view relative to the window.
* If a side is inset from the window edge, we use a negative offset
* to expand it outward. If a side extends beyond the window edge,
* we use padding to push the content inward.
*
* On iOS, we also mount a child inside a SafeAreaView, to measure
* the safe area insets. We add these to the padding.
*
* Finally, we keep track of the keyboard, adding extra padding &
* scheduling animations as needed.
*/
export function Barometer(props: Props): JSX.Element {
const { children, onLayout = () => {} } = props
// Mutable state:
const keyboardHeight: React.MutableRefObject<number> = React.useRef(0)
const lastLayoutJson: React.MutableRefObject<string> = React.useRef('')
const view: React.RefObject<SafeAreaView | View> = React.useRef(null)
const childView: React.RefObject<View> = React.useRef(null)
// Handle layout changes:
const handleLayout = React.useCallback((): void => {
// Measure the view in the window:
const viewPromise: Promise<SideList> = new Promise(resolve => {
if (view.current == null) return resolve(emptySides)
view.current.measureInWindow((x, y, width, height) => {
const window = Dimensions.get('window')
resolve([y, window.width - width - x, window.height - height - y, x])
})
})
// Measure the child view in the window:
const childPromise: Promise<SideList> = new Promise(resolve => {
if (childView.current == null) return resolve(viewPromise)
childView.current.measureInWindow((x, y, width, height) => {
const window = Dimensions.get('window')
resolve([y, window.width - width - x, window.height - height - y, x])
})
})
// Measure the gap between the bottom of the screen and the view:
const bottomPromise: Promise<number> = new Promise(resolve => {
if (view.current == null) return 0
view.current.measure((x, y, width, height, screenX, screenY) => {
const screen = Dimensions.get('screen')
resolve(screen.height - height - screenY)
})
})
// Combine the results, then call the callback:
Promise.all([viewPromise, childPromise, bottomPromise])
.then(([viewOffset, childOffset, bottomGap]) => {
// Cancel out any offset, so we cover the full window:
const offset = mapSides(viewOffset, side => -Math.max(side, 0))
// If the offset is negative, issue positive padding,
// plus any safe area:
const safePadding = subtractSides(childOffset, viewOffset)
const padding = addSides(
safePadding,
mapSides(viewOffset, side => Math.abs(side))
)
// Use the keyboard padding, if needed:
const keyboardPadding = Math.max(
keyboardHeight.current - bottomGap - offset[2],
0
)
padding[2] = Math.max(padding[2], keyboardPadding)
// Send an update if we have changes:
const string = JSON.stringify([offset, padding])
if (string !== lastLayoutJson.current) {
lastLayoutJson.current = string
onLayout({
offset: sidesToOffset(offset),
padding: sidesToPadding(padding)
})
}
})
.catch(() => {})
}, [onLayout])
// Subscribe to keyboard changes:
React.useEffect(() => {
const handleKeyboard: KeyboardEventListener = event => {
const screen = Dimensions.get('screen')
keyboardHeight.current = Math.min(
// These two give different results sometimes, so pick the smaller one:
screen.height - event.endCoordinates.screenY,
event.endCoordinates.height
)
if (event.duration > 0) {
// @ts-expect-error
Keyboard.scheduleLayoutAnimation(event)
}
handleLayout()
}
if (Platform.OS === 'android') {
Keyboard.addListener('keyboardDidShow', handleKeyboard)
Keyboard.addListener('keyboardDidHide', handleKeyboard)
} else {
Keyboard.addListener('keyboardWillChangeFrame', handleKeyboard)
}
return () => {
if (Platform.OS === 'android') {
Keyboard.removeListener('keyboardDidShow', handleKeyboard)
Keyboard.removeListener('keyboardDidHide', handleKeyboard)
} else {
Keyboard.removeListener('keyboardWillChangeFrame', handleKeyboard)
}
}
}, [handleLayout])
if (Platform.OS === 'android') {
return (
<View
ref={view}
onLayout={handleLayout}
pointerEvents="none"
style={StyleSheet.absoluteFill}
testID="AirshipBarometer"
>
{children}
</View>
)
}
return (
<SafeAreaView
ref={view}
onLayout={handleLayout}
pointerEvents="none"
style={StyleSheet.absoluteFill}
testID="AirshipBarometer"
>
<View ref={childView} style={{ flex: 1 }} testID="AirshipBarometerChild">
{children}
</View>
</SafeAreaView>
)
}
// @flow
import * as React from 'react'
import { type OnEvents, type Unsubscribe } from 'yavent'
export type { Unsubscribe }
type AirshipEvents = {
result: void,
clear: void
}
/**
* Control panel for managing a component inside an airship.
*/
export type AirshipBridge<T> = {
// Use these to pass values to the outside world:
+resolve: (value: T | Promise<T>) => void,
+reject: (error: Error) => void,
// Unmounts the component:
+remove: () => void,
// Subscribes to events.
// Use `on('result', callback)` to subscribe to
// the promise being resolved or rejected.
// Use `on('clear', callback)` to subscribe to
// the `Airship.clear` method being called.
+on: OnEvents<AirshipEvents>,
// Runs a callback when the result promise settles.
// Deprecated in favor of `on('result')`.
+onResult: (callback: () => mixed) => void
}
/**
* Renders a component to place inside the airship.
*/
type AirshipRender<T> = (bridge: AirshipBridge<T>) => React.Node
/**
* Props the Airship container component accepts.
*/
export interface AirshipProps {
children?: React.Node;
}
/**
* The airship itself is a component you should mount after your main
* scene or router.
*
* It has a static method anyone can call to display components.
* The method returns a promise, which the component can use to pass values
* to the outside world.
*/
declare class AirshipClass extends React.Component<AirshipProps> {
static clear(): void;
static show<T>(render: AirshipRender<T>): Promise<T>;
}
export type Airship = typeof AirshipClass
/**
* Constructs an Airship component.
*/
declare export function makeAirship(): Airship
type FlexDirection = 'column-reverse' | 'column' | 'row-reverse' | 'row'
type JustifyContent =
| 'center'
| 'flex-end'
| 'flex-start'
| 'space-around'
| 'space-between'
| 'space-evenly'
/**
* A drop-down alert.
*/
export type AirshipDropdownProps = {
bridge: AirshipBridge<void>,
children?: React.Node,
onPress?: () => void,
autoHideMs?: number,
backgroundColor?: string,
borderRadius?: number,
flexDirection?: FlexDirection,
justifyContent?: JustifyContent,
margin?: number | number[],
maxHeight?: number,
maxWidth?: number,
padding?: number | number[],
slideInMs?: number,
slideOutMs?: number
}
declare export class AirshipDropdown
extends React.Component<AirshipDropdownProps> {}
/**
* A slide-up modal which dims the rest of the screen.
*/
export type AirshipModalProps<T> = {
bridge: AirshipBridge<T>,
children?: React.Node,
onCancel: () => void,
center?: boolean,
backgroundColor?: string,
borderRadius?: number,
flexDirection?: FlexDirection,
justifyContent?: JustifyContent,
margin?: number | number[],
maxHeight?: number,
maxWidth?: number,
padding?: number | number[],
slideInMs?: number,
slideOutMs?: number,
underlay?: string | React.Element<any>
}
declare export class AirshipModal<T>
extends React.Component<AirshipModalProps<T>> {}
/**
* Emulates the Android Toast component in a cross-platform way.
*/
export type AirshipToastProps = {
bridge: AirshipBridge<void>,
children?: React.Node,
message?: string,
autoHideMs?: number,
backgroundColor?: string,
borderRadius?: number,
fadeInMs?: number,
fadeOutMs?: number,
margin?: number | number[],
maxWidth?: number,
opacity?: number,
padding?: number | number[],
textColor?: string,
textSize?: number
}
declare export class AirshipToast extends React.Component<AirshipToastProps> {}
export { Unsubscribe } from 'yavent'
export { makeAirship } from './components/Airship'
export {
AirshipDropdown,
AirshipDropdownProps
} from './components/AirshipDropdown'
export { AirshipModal, AirshipModalProps } from './components/AirshipModal'
export { AirshipToast, AirshipToastProps } from './components/AirshipToast'
export { Airship, AirshipBridge } from './types'
import * as React from 'react'
import { OnEvents } from 'yavent'
export interface AirshipEvents {
result: undefined
clear: undefined
}
/**
* Control panel for managing a component inside an airship.
*/
export interface AirshipBridge<T> {
// Use these to pass values to the outside world:
resolve: (value: T | PromiseLike<T>) => void
reject: (error: Error) => void
// Unmounts the component:
remove: () => void
// Subscribes to events.
// Use `on('result', callback)` to subscribe to
// the promise being resolved or rejected.
// Use `on('clear', callback)` to subscribe to
// the `Airship.clear` method being called.
on: OnEvents<AirshipEvents>
// Runs a callback when the result promise settles.
// Deprecated in favor of `on('result')`.
onResult: (callback: () => unknown) => void
}
/**
* Renders a component to place inside the airship.
*/
export type AirshipRender<T> = (bridge: AirshipBridge<T>) => React.ReactNode
/**
* Props the Airship container component accepts.
*/
export interface AirshipProps {
children?: React.ReactNode
}
/**
* The Airship itself is a component you should mount after your main
* scene or router.
*
* It has a static method anyone can call to display components.
* The method returns a promise, which the component can use to pass values
* to the outside world.
*/
export interface Airship extends React.FunctionComponent<AirshipProps> {
clear: () => void
show: <T>(render: AirshipRender<T>) => Promise<T>
}
/**
* The four sides (top, right, bottom, left) as a tuple.
*/
export type SideList = [number, number, number, number]
export interface Margin {
marginBottom: number
marginLeft: number
marginRight: number
marginTop: number
}
export interface Offset {
bottom: number
left: number
right: number
top: number
}
export interface Padding {
paddingBottom: number
paddingLeft: number
paddingRight: number
paddingTop: number
}
/**
* Interprets an array of 0-4 numbers as a web CSS sides shorthand
* (top, right, bottom, left).
*/
export function fixSides(
sides: number[] | number | undefined,
fallback: number
): SideList {
if (sides == null) {
return [fallback, fallback, fallback, fallback]
}
if (typeof sides === 'number') {
return [sides, sides, sides, sides]
}
const top = sides[0] ?? fallback
const right = sides[1] ?? top
const bottom = sides[2] ?? top
const left = sides[3] ?? right
return [top, right, bottom, left]
}
export function addSides(a: SideList, b: SideList): SideList {
return [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]]
}
export function subtractSides(a: SideList, b: SideList): SideList {
return [a[0] - b[0], a[1] - b[1], a[2] - b[2], a[3] - b[3]]
}
export function mapSides(
sides: SideList,
f: (side: number) => number
): SideList {
return [f(sides[0]), f(sides[1]), f(sides[2]), f(sides[3])]
}
/**
* Turns a list of sides into CSS margin properties.
*/
export function sidesToMargin(sides: SideList): Margin {
return {
marginTop: sides[0],
marginRight: sides[1],
marginBottom: sides[2],
marginLeft: sides[3]
}
}
/**
* Turns a list of sides into CSS positioning properties.
*/
export function sidesToOffset(sides: SideList): Offset {
return {
top: sides[0],
right: sides[1],
bottom: sides[2],
left: sides[3]
}
}
/**
* Turns a list of sides into CSS padding properties.
*/
export function sidesToPadding(sides: SideList): Padding {
return {
paddingTop: sides[0],
paddingRight: sides[1],
paddingBottom: sides[2],
paddingLeft: sides[3]
}
}