Skip to content
Draft
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
36 changes: 24 additions & 12 deletions src/components/hv-date-field/field/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Contexts from 'hyperview/src/contexts';
import React, { useState } from 'react';
import { StyleSheet, TouchableWithoutFeedback, View } from 'react-native';
import { createProps, createStyleProp } from 'hyperview/src/services';
import { useProps, useStyleProp } from 'hyperview/src/services';
import FieldLabel from '../field-label';
import type { Props } from './types';
import type { StyleSheet as StyleSheetType } from 'hyperview/src/types';
Expand All @@ -11,29 +11,41 @@ import type { StyleSheet as StyleSheetType } from 'hyperview/src/types';
* Tapping the box focuses the field and brings up the date picker.
*/
export default (props: Props) => {
// eslint-disable-next-line react/destructuring-assignment
const {
children,
element,
focused,
onPress,
options,
stylesheets,
value,
} = props;
const { pressedSelected, selected } = options;
// Styles selected based on pressed state of the field.
const [pressed, setPressed] = useState(false);

// Create the props (including styles) for the box of the input field.
const viewProps = createProps(props.element, props.stylesheets, {
...props.options,
focused: props.focused,
const viewProps = useProps(element, stylesheets, {
...options,
focused,
pressed,
styleAttr: 'field-style',
});

const labelStyle: StyleSheetType = StyleSheet.flatten(
createStyleProp(props.element, props.stylesheets, {
...props.options,
focused: props.focused,
useStyleProp(element, stylesheets, {
focused,
pressed,
pressedSelected,
selected,
styleAttr: 'field-text-style',
}),
);

return (
<TouchableWithoutFeedback
onPress={props.onPress}
onPress={onPress}
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}
>
Expand All @@ -42,16 +54,16 @@ export default (props: Props) => {
<Contexts.DateFormatContext.Consumer>
{formatter => (
<FieldLabel
element={props.element}
focused={props.focused}
element={element}
focused={focused}
formatter={formatter}
pressed={pressed}
style={labelStyle}
value={props.value}
value={value}
/>
)}
</Contexts.DateFormatContext.Consumer>
{props.children}
{children}
</View>
</TouchableWithoutFeedback>
);
Expand Down
46 changes: 0 additions & 46 deletions src/components/hv-image/index.ts

This file was deleted.

46 changes: 46 additions & 0 deletions src/components/hv-image/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as Namespaces from 'hyperview/src/services/namespaces';
import type {
HvComponentOnUpdate,
HvComponentProps,
} from 'hyperview/src/types';
import React, { useMemo } from 'react';
import { Image } from 'react-native';
import { LOCAL_NAME } from 'hyperview/src/types';
import { addHref } from 'hyperview/src/core/hyper-ref';
import urlParse from 'url-parse';
import { useProps } from 'hyperview/src/services';

const HvImage = (props: HvComponentProps) => {
// eslint-disable-next-line react/destructuring-assignment
const { element, onUpdate, options, stylesheets } = props;
const { screenUrl, skipHref } = options;
const baseProps = useProps(element, stylesheets, options);
const source = useMemo(() => element.getAttribute('source'), [element]);
const componentProps = useMemo(() => {
if (!source) {
return baseProps;
}
return {
...baseProps,
source: { uri: urlParse(source, screenUrl, true).toString() },
};
}, [baseProps, screenUrl, source]);

const component = useMemo(() => React.createElement(Image, componentProps), [
componentProps,
]);
Comment on lines +29 to +31

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

component is changing from the return value of React.createElement (previous implementation) to a functional component that returns the returned value of React.createElement (new implementation). I assume this is unintentional, as this changes the produced tree, and I wonder if we need this.

return skipHref
? component
: addHref(
component,
element,
stylesheets,
onUpdate as HvComponentOnUpdate,
options,
);
};

HvImage.namespaceURI = Namespaces.HYPERVIEW;
HvImage.localName = LOCAL_NAME.IMAGE;

export default HvImage;
167 changes: 94 additions & 73 deletions src/components/hv-option/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,97 +5,118 @@ import type {
HvComponentOnUpdate,
HvComponentProps,
} from 'hyperview/src/types';
import React, { PureComponent } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { TouchableWithoutFeedback, View } from 'react-native';
import { LOCAL_NAME } from 'hyperview/src/types';
import type { State } from './types';
import { createEventHandler } from 'hyperview/src/core/hyper-ref';
import { createProps } from 'hyperview/src/services';
import { useProps } from 'hyperview/src/services';

/**
* A component representing an option in a single-select or multiple-select list.
* Has a local pressed state. The selected state is read from the element attribute.
*/
export default class HvOption extends PureComponent<HvComponentProps, State> {
static namespaceURI = Namespaces.HYPERVIEW;
const HvOption = (props: HvComponentProps) => {
// eslint-disable-next-line react/destructuring-assignment
const { element, onUpdate, options, stylesheets } = props;
const [pressed, setPressed] = useState(false);
const { onSelect, onToggle } = options;
const value = useMemo(() => element.getAttribute('value'), [element]);
const selected = useMemo(() => element.getAttribute('selected') === 'true', [
element,
]);
const prevSelected = useRef(selected);

static localName = LOCAL_NAME.OPTION;
// Updates options with pressed/selected state, so that child element can render
// using the appropriate modifier styles.
const newOptions = useMemo(() => {
return {
...options,
pressed,
pressedSelected: pressed && selected,
selected,
} as const;
}, [options, pressed, selected]);

state: State = {
pressed: false,
};
const componentProps = useProps(element, stylesheets, newOptions);

componentDidUpdate(prevProps: HvComponentProps) {
const selected = this.props.element.getAttribute('selected') === 'true';
const prevSelected = prevProps.element.getAttribute('selected') === 'true';
if (selected && !prevSelected) {
Behaviors.trigger('select', this.props.element, this.props.onUpdate);
}
const flex = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO: fix this
return componentProps.style?.flex;
}, [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO: fix this
componentProps.style?.flex,
]);

if (!selected && prevSelected) {
Behaviors.trigger('deselect', this.props.element, this.props.onUpdate);
const handlePress = useCallback(() => {
if (onSelect) {
// Updates the DOM state, causing this element to re-render as selected.
// Used in select-single context.
onSelect(value);
}
}
if (onToggle) {
// Updates the DOM state, toggling this element.
// Used in select-multiple context.
onToggle(value);
}
}, [onSelect, onToggle, value]);

render() {
const { onSelect, onToggle } = this.props.options;
const handlePressIn = useCallback(() => setPressed(true), []);
const handlePressOut = useCallback(() => setPressed(false), []);

const value = this.props.element.getAttribute('value');
const selected = this.props.element.getAttribute('selected') === 'true';
// Option renders as an outer TouchableWithoutFeedback view and inner view.
// The outer view handles presses, the inner view handles styling.
const outerProps = useMemo(() => {
return {
onPress: createEventHandler(handlePress, true),
onPressIn: createEventHandler(handlePressIn),
onPressOut: createEventHandler(handlePressOut),

// Updates options with pressed/selected state, so that child element can render
// using the appropriate modifier styles.
const newOptions = {
...this.props.options,
pressed: this.state.pressed,
pressedSelected: this.state.pressed && selected,
selected,
} as const;
const props = createProps(
this.props.element,
this.props.stylesheets,
// Flex is a style that needs to be lifted from the inner component to the outer
// component to ensure proper layout.
style: flex ? { flex } : {},
};
}, [flex, handlePress, handlePressIn, handlePressOut]);

// TODO: Replace with <HvChildren>
const children = useMemo(() => {
return Render.renderChildren(
element,
stylesheets,
onUpdate as HvComponentOnUpdate,
newOptions,
);
}, [element, newOptions, onUpdate, stylesheets]);

// Option renders as an outer TouchableWithoutFeedback view and inner view.
// The outer view handles presses, the inner view handles styling.
const outerProps = {
onPress: createEventHandler(() => {
if (onSelect) {
// Updates the DOM state, causing this element to re-render as selected.
// Used in select-single context.
onSelect(value);
}
if (onToggle) {
// Updates the DOM state, toggling this element.
// Used in select-multiple context.
onToggle(value);
}
}, true),
onPressIn: createEventHandler(() => this.setState({ pressed: true })),
onPressOut: createEventHandler(() => this.setState({ pressed: false })),
style: {},
};
if (props.style && props.style.flex) {
// Flex is a style that needs to be lifted from the inner component to the outer
// component to ensure proper layout.
outerProps.style = { flex: props.style.flex };
useEffect(() => {
if (selected && !prevSelected.current) {
Behaviors.trigger('select', element, onUpdate);
}

// TODO: Replace with <HvChildren>
return React.createElement(
TouchableWithoutFeedback,
outerProps,
React.createElement(
View,
props,
...Render.renderChildren(
this.props.element,
this.props.stylesheets,
this.props.onUpdate as HvComponentOnUpdate,
newOptions,
),
),
);
}
}
if (!selected && prevSelected.current) {
Behaviors.trigger('deselect', element, onUpdate);
}
prevSelected.current = selected;
}, [element, onUpdate, selected]);

const view = useMemo(() => {
return React.createElement(View, componentProps, ...children);
}, [componentProps, children]);

return useMemo(
() => React.createElement(TouchableWithoutFeedback, outerProps, view),
[outerProps, view],
);
};

HvOption.namespaceURI = Namespaces.HYPERVIEW;
HvOption.localName = LOCAL_NAME.OPTION;

export default HvOption;
4 changes: 0 additions & 4 deletions src/components/hv-option/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
export type State = {
pressed: boolean;
};

// https://reactnative.dev/docs/flatlist#scrolltoindex
export type ScrollParams = {
animated?: boolean | undefined;
Expand Down
Loading