Skip to content

Commit

Permalink
Small tweak to toggle interface, clean up some more components
Browse files Browse the repository at this point in the history
  • Loading branch information
floatplane committed Nov 6, 2023
1 parent 60a6575 commit 45207ed
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 60 deletions.
5 changes: 3 additions & 2 deletions components/content/AvalancheCenterList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {useCallback} from 'react';

import {AvalancheCenterListData} from 'components/avalancheCenterList';
import {AvalancheCenterLogo} from 'components/AvalancheCenterLogo';
Expand All @@ -15,8 +15,9 @@ interface AvalancheCenterListItemProps {
}
const AvalancheCenterListItem: React.FC<AvalancheCenterListItemProps> = ({data, selected, setSelected}) => {
const center_id: AvalancheCenterID = data.center.id as AvalancheCenterID;
const onPress = useCallback(() => setSelected(center_id), [setSelected, center_id]);
return (
<TouchableOpacity onPress={() => setSelected(center_id)} activeOpacity={0.8}>
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
<HStack
justifyContent="space-between"
alignItems="flex-start"
Expand Down
7 changes: 5 additions & 2 deletions components/content/ButtonBar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Button} from 'components/content/Button';
import {HStack, ViewProps} from 'components/core';
import {Body, BodyBlack, BodyXSm, BodyXSmBlack} from 'components/text';
import React from 'react';
import React, {useMemo} from 'react';

interface ButtonBarProps<T> extends ViewProps {
items: {label: string; value: T}[];
Expand All @@ -25,6 +25,9 @@ const LabelFonts = {

export function ButtonBar<T>({items, selectedItem, onSelectionChange, disabled = false, size = 'normal', fullWidth = false, ...props}: ButtonBarProps<T>) {
const buttonCount = items.length;
const pressHandlers = useMemo(() => {
return items.map(item => () => onSelectionChange(item.value));
}, [items, onSelectionChange]);
return (
<HStack space={0} {...props}>
{items.map((item, index) => {
Expand All @@ -35,7 +38,7 @@ export function ButtonBar<T>({items, selectedItem, onSelectionChange, disabled =
return (
<Button
key={item.label}
onPress={() => onSelectionChange(item.value)}
onPress={pressHandlers[index]}
buttonStyle={selected ? 'primary' : 'normal'}
minWidth={fullWidth ? `${100.0 / items.length}%` : undefined}
px={24}
Expand Down
2 changes: 1 addition & 1 deletion components/observations/ObservationsListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const ObservationsListView: React.FunctionComponent<ObservationsListViewP
const endDate = requestedTimeToUTCDate(requestedTime);
const originalFilterConfig: ObservationFilterConfig = useMemo(() => createDefaultFilterConfig(requestedTime, additionalFilters), [requestedTime, additionalFilters]);
const [filterConfig, setFilterConfig] = useState<ObservationFilterConfig>(originalFilterConfig);
const [filterModalVisible, setFilterModalVisible, {on: showFilterModal}] = useToggle(false);
const [filterModalVisible, {set: setFilterModalVisible, on: showFilterModal}] = useToggle(false);
const mapResult = useMapLayer(center_id);
const mapLayer = mapResult.data;

Expand Down
2 changes: 1 addition & 1 deletion components/text/HTML.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const HTML: React.FunctionComponent<RenderHTMLSourceProps> = props => {

if (__DEV__) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [showSource, _setShowSource, {toggle: toggleSource}] = useToggle(false);
const [showSource, {toggle: toggleSource}] = useToggle(false);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const html = _.get(props, ['source', 'html'], undefined);
return (
Expand Down
4 changes: 1 addition & 3 deletions components/weather_data/WeatherStationDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,7 @@ export const WeatherStationDetail: React.FC<Props> = ({center_id, stationId, sou
{label: '7 day', value: 7},
]}
selectedItem={days}
onSelectionChange={(item: number) => {
setDays(item);
}}
onSelectionChange={setDays}
size="small"
paddingTop={6}
/>
Expand Down
94 changes: 51 additions & 43 deletions components/weather_data/WeatherStationFilterForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {useCallback} from 'react';

import {AntDesign} from '@expo/vector-icons';
import {zodResolver} from '@hookform/resolvers/zod';
Expand Down Expand Up @@ -163,46 +163,67 @@ export const WeatherStationFilterForm: React.FunctionComponent<WeatherStationFil
formContext.reset(currentFilterConfig);
}, [formContext, currentFilterConfig]);

const onCloseHandler = React.useCallback(() => {
const closeWithoutSaving = useCallback(() => {
formContext.reset(initialFilterConfig);
setVisible(false);
}, [initialFilterConfig, formContext, setVisible]);
const saveAndClose = useCallback(() => setVisible(false), [setVisible]);

useBackHandler(() => {
onCloseHandler();
closeWithoutSaving();
// Returning true marks the event as processed
return true;
});

const onSubmitHandler = (data: WeatherStationFilterConfig) => {
setFilterConfig(data);
};
const onSubmitHandler = useCallback(
(data: WeatherStationFilterConfig) => {
setFilterConfig(data);
},
[setFilterConfig],
);

const fieldRefs = React.useRef<{ref: RNView; field: keyof WeatherStationFilterConfig}[]>([]);
const scrollViewRef = React.useRef<ScrollView>(null);

const onSubmitErrorHandler = (errors: FieldErrors<WeatherStationFilterConfig>) => {
logger.error({errors: errors, formValues: formContext.getValues()}, 'filters error');
// scroll to the first field with an error
fieldRefs.current.some(({ref, field}) => {
if (errors[field] && scrollViewRef.current) {
const handle = findNodeHandle(scrollViewRef.current);
if (handle) {
ref.measureLayout(
handle,
(_left, top) => {
if (scrollViewRef.current) {
scrollViewRef.current.scrollTo({y: top});
}
},
() => undefined,
);
return true;
const onSubmitErrorHandler = useCallback(
(errors: FieldErrors<WeatherStationFilterConfig>) => {
logger.error({errors: errors, formValues: formContext.getValues()}, 'filters error');
// scroll to the first field with an error
fieldRefs.current.some(({ref, field}) => {
if (errors[field] && scrollViewRef.current) {
const handle = findNodeHandle(scrollViewRef.current);
if (handle) {
ref.measureLayout(
handle,
(_left, top) => {
if (scrollViewRef.current) {
scrollViewRef.current.scrollTo({y: top});
}
},
() => undefined,
);
return true;
}
}
}
return false;
});
};
return false;
});
},
[logger, formContext],
);
const onResetHandler = useCallback(() => formContext.reset(initialFilterConfig), [formContext, initialFilterConfig]);

const applyChanges = useCallback(
() =>
void (async () => {
// Force validation errors to show up on fields that haven't been visited yet
await formContext.trigger();
// Then try to submit the form
await formContext.handleSubmit(onSubmitHandler, onSubmitErrorHandler)();
// Finally, close the modal
setVisible(false);
})(),
[formContext, onSubmitHandler, onSubmitErrorHandler, setVisible],
);

return (
<FormProvider {...formContext}>
Expand All @@ -216,11 +237,11 @@ export const WeatherStationFilterForm: React.FunctionComponent<WeatherStationFil
borderColor="white"
header={
<HStack justifyContent={'space-between'} alignItems={'center'}>
<TouchableOpacity onPress={() => setVisible(false)}>
<TouchableOpacity onPress={saveAndClose}>
<AntDesign name="close" size={24} color="black" />
</TouchableOpacity>
<Title3Semibold>Filters</Title3Semibold>
<TouchableOpacity onPress={() => formContext.reset(initialFilterConfig)}>
<TouchableOpacity onPress={onResetHandler}>
<BodyBlack color={colorLookup('blue2')}>Reset</BodyBlack>
</TouchableOpacity>
</HStack>
Expand All @@ -247,20 +268,7 @@ export const WeatherStationFilterForm: React.FunctionComponent<WeatherStationFil
</Card>
</VStack>
</ScrollView>
<Button
mx={16}
mt={16}
buttonStyle="primary"
onPress={() =>
void (async () => {
// Force validation errors to show up on fields that haven't been visited yet
await formContext.trigger();
// Then try to submit the form
await formContext.handleSubmit(onSubmitHandler, onSubmitErrorHandler)();
// Finally, close the modal
setVisible(false);
})()
}>
<Button mx={16} mt={16} buttonStyle="primary" onPress={applyChanges}>
<BodySemibold>Apply Filters</BodySemibold>
</Button>
</KeyboardAvoidingView>
Expand Down
7 changes: 4 additions & 3 deletions components/weather_data/WeatherStationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {WeatherStationList} from 'components/weather_data/WeatherStationList';
import {WeatherStationMap} from 'components/weather_data/WeatherStationMap';
import {useAvalancheCenterMetadata} from 'hooks/useAvalancheCenterMetadata';
import {useMapLayer} from 'hooks/useMapLayer';
import {useToggle} from 'hooks/useToggle';
import {useWeatherStationsMetadata} from 'hooks/useWeatherStationsMetadata';
import React from 'react';
import {AvalancheCenterID} from 'types/nationalAvalancheCenter';
Expand Down Expand Up @@ -38,7 +39,7 @@ export const WeatherStations: React.FunctionComponent<{
token: string;
requestedTime: RequestedTimeString;
}> = ({center_id, token, requestedTime}) => {
const [list, setList] = React.useState<boolean>(false);
const [list, {toggle: toggleList}] = useToggle(false);
const mapLayerResult = useMapLayer(center_id);
const mapLayer = mapLayerResult.data;
const weatherStationsResult = useWeatherStationsMetadata(center_id, token);
Expand All @@ -49,8 +50,8 @@ export const WeatherStations: React.FunctionComponent<{
}

if (list) {
return <WeatherStationList center_id={center_id} requestedTime={requestedTime} mapLayer={mapLayer} weatherStations={weatherStations} toggleMap={() => setList(false)} />;
return <WeatherStationList center_id={center_id} requestedTime={requestedTime} mapLayer={mapLayer} weatherStations={weatherStations} toggleMap={toggleList} />;
} else {
return <WeatherStationMap center_id={center_id} requestedTime={requestedTime} mapLayer={mapLayer} weatherStations={weatherStations} toggleList={() => setList(true)} />;
return <WeatherStationMap center_id={center_id} requestedTime={requestedTime} mapLayer={mapLayer} weatherStations={weatherStations} toggleList={toggleList} />;
}
};
4 changes: 1 addition & 3 deletions components/weather_data/WeatherStationsDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,7 @@ export const WeatherStationsDetail: React.FC<Props> = ({center_id, name, station
{label: '7 day', value: 7},
]}
selectedItem={days}
onSelectionChange={(item: number) => {
setDays(item);
}}
onSelectionChange={setDays}
size="small"
paddingTop={6}
/>
Expand Down
4 changes: 2 additions & 2 deletions hooks/useToggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import {useCallback, useState} from 'react';

export const useToggle = (
initialState: boolean,
): [get: boolean, set: React.Dispatch<React.SetStateAction<boolean>>, helpers: {toggle: () => void; on: () => void; off: () => void}] => {
): [get: boolean, helpers: {set: React.Dispatch<React.SetStateAction<boolean>>; toggle: () => void; on: () => void; off: () => void}] => {
const [state, setState] = useState<boolean>(initialState);
const toggle = useCallback(() => setState(state => !state), [setState]);
const on = useCallback(() => setState(true), [setState]);
const off = useCallback(() => setState(false), [setState]);

return [state, setState, {toggle, on, off}];
return [state, {set: setState, toggle, on, off}];
};

0 comments on commit 45207ed

Please sign in to comment.