Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 32 additions & 24 deletions packages/react-strict-dom/src/native/modules/usePseudoStates.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,29 @@ type Interaction = {
handlers: ?InteractionHandlers
};

type PseudoState = {
+active: boolean,
+focus: boolean,
+mouseHover: boolean,
+pointerHover: boolean
};

const defaultState: PseudoState = {
active: false,
focus: false,
mouseHover: false,
pointerHover: false
};

export function usePseudoStates(style: Style): Interaction {
const [focus, setFocus] = React.useState(false);
const [mouseHover, setMouseHover] = React.useState(false);
const [pointerHover, setPointerHover] = React.useState(false);
const [active, setActive] = React.useState(false);
const [state, setState] = React.useState<PseudoState>(defaultState);

let isHoverStyledElement = false;
let isFocusStyledElement = false;
let isActiveStyledElement = false;

for (const styleValue of Object.values(style)) {
for (const key in style) {
const styleValue = style[key];
if (styleValue != null && typeof styleValue === 'object') {
if (styleValue.hasOwnProperty(':hover')) {
isHoverStyledElement = true;
Expand All @@ -64,39 +76,35 @@ export function usePseudoStates(style: Style): Interaction {
const handlers = React.useMemo(() => {
let value = null;
if (isHoverStyledElement || isFocusStyledElement || isActiveStyledElement) {
const set = (changes: Partial<PseudoState>) =>
setState((prev) => ({ ...prev, ...changes }));
value = {} as InteractionHandlers;
if (isHoverStyledElement) {
value.onMouseEnter = () => setMouseHover(true);
value.onMouseLeave = () => setMouseHover(false);
value.onPointerEnter = () => setPointerHover(true);
value.onMouseEnter = () => set({ mouseHover: true });
value.onMouseLeave = () => set({ mouseHover: false });
value.onPointerEnter = () => set({ pointerHover: true });
}
if (isFocusStyledElement) {
value.onBlur = () => setFocus(false);
value.onFocus = () => setFocus(true);
value.onBlur = () => set({ focus: false });
value.onFocus = () => set({ focus: true });
}
if (isActiveStyledElement) {
value.onPointerCancel = () => setActive(false);
value.onPointerDown = () => setActive(true);
value.onPointerUp = () => setActive(false);
value.onPointerCancel = () => set({ active: false });
value.onPointerDown = () => set({ active: true });
value.onPointerUp = () => set({ active: false });
}
if (isHoverStyledElement || isActiveStyledElement) {
value.onPointerLeave = () => {
if (isHoverStyledElement) {
setPointerHover(false);
}
if (isActiveStyledElement) {
setActive(false);
}
};
value.onPointerLeave = () =>
set({ active: false, pointerHover: false });
}
}
return value;
}, [isHoverStyledElement, isFocusStyledElement, isActiveStyledElement]);

return {
active,
focus,
hover: mouseHover || pointerHover,
active: state.active,
focus: state.focus,
hover: state.mouseHover || state.pointerHover,
handlers
};
}
80 changes: 40 additions & 40 deletions packages/react-strict-dom/src/native/modules/useStyleTransition.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,20 @@ type AnimatedStyle = {
[string]: ?ReactNativeStyleValue | ReadonlyArray<unknown>
};

type TransitionMetadata = Readonly<{
type Transition = Readonly<{
animatedValue: ReactNative.Animated.Value,
delay: number,
duration: number,
timingFunction: string | null,
shouldUseNativeDriver: boolean
}>;

type TransitionState = Readonly<{
currentStyle: ReactNativeStyle | void,
previousStyle: ReactNativeStyle | void,
transition: Transition | void
}>;

const INPUT_RANGE: ReadonlyArray<number> = [0, 1];

function isNumber(num: unknown): num is number {
Expand Down Expand Up @@ -290,43 +297,28 @@ export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {
return output;
}, {});

const [currentStyle, setCurrentStyle] =
React.useState<ReactNativeStyle | void>(style);
const [previousStyle, setPreviousStyle] =
React.useState<ReactNativeStyle | void>(undefined);
const [animatedValue, setAnimatedValue] =
React.useState<ReactNative.Animated.Value | void>(undefined);

// This ref is utilized as a performance optimization so that the effect that contains the
// animation trigger only is called when the animated value's identity changes. As far as the effect
// is concerned it just needs the most up to date version of these transition properties;
const transitionMetadataRef = React.useRef<TransitionMetadata>({
delay: transitionDelay,
duration: transitionDuration,
timingFunction: transitionTimingFunction,
shouldUseNativeDriver: canUseNativeDriver(transitionStyle)
});
// effect to sync the transition metadata
React.useEffect(() => {
transitionMetadataRef.current = {
delay: transitionDelay,
duration: transitionDuration,
timingFunction: transitionTimingFunction,
shouldUseNativeDriver: canUseNativeDriver(transitionStyle)
};
}, [
transitionDelay,
transitionDuration,
transitionStyle,
transitionTimingFunction
]);
const [{ currentStyle, previousStyle, transition }, setTransitionState] =
React.useState<TransitionState>(() => ({
currentStyle: style,
previousStyle: undefined,
transition: undefined
}));

const animatedValue = transition?.animatedValue;

// effect to trigger a transition
// REMEMBER: it is super important that this effect's dependency array **only** contains the animated value
// The transition object gets a new identity only when a new transition
// starts, so this effect runs exactly once per transition, using the timing
// values captured at that moment.
React.useEffect(() => {
if (animatedValue !== undefined) {
const { delay, duration, timingFunction, shouldUseNativeDriver } =
transitionMetadataRef.current;
if (transition !== undefined) {
const {
animatedValue,
delay,
duration,
timingFunction,
shouldUseNativeDriver
} = transition;

const animation = getAnimation(
animatedValue,
Expand All @@ -341,7 +333,7 @@ export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {
animation.stop();
};
}
}, [animatedValue]);
}, [transition]);

if (
_delay == null &&
Expand All @@ -354,10 +346,18 @@ export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {
}

if (transitionStyleHasChanged(transitionStyle, currentStyle)) {
setCurrentStyle(style);
setPreviousStyle(currentStyle);
setAnimatedValue(new ReactNative.Animated.Value(0));
// This commit will be thrown away due to the above state setters so we can bail out early
setTransitionState({
currentStyle: style,
previousStyle: currentStyle,
transition: {
animatedValue: new ReactNative.Animated.Value(0),
delay: transitionDelay,
duration: transitionDuration,
timingFunction: transitionTimingFunction,
shouldUseNativeDriver: canUseNativeDriver(transitionStyle)
}
});
// This commit will be thrown away due to the above state setter so we can bail out early
return style;
}

Expand Down