Skip to content

Instantly share code, notes, and snippets.

@artyorsh
Last active March 24, 2024 15:54
Show Gist options
  • Save artyorsh/f9846c93e42a5ed45364ea747d0c4101 to your computer and use it in GitHub Desktop.
Save artyorsh/f9846c93e42a5ed45364ea747d0c4101 to your computer and use it in GitHub Desktop.
Time Picker example with UI Kitten and DateTimePicker
{
"components": {
"Timepicker": {
"meta": {
"scope": "all",
"parameters": {
"minHeight": {
"type": "number"
},
"paddingHorizontal": {
"type": "number"
},
"paddingVertical": {
"type": "number"
},
"borderRadius": {
"type": "number"
},
"borderColor": {
"type": "string"
},
"borderWidth": {
"type": "number"
},
"backgroundColor": {
"type": "string"
},
"textMarginHorizontal": {
"type": "number"
},
"textFontSize": {
"type": "number"
},
"textFontWeight": {
"type": "string"
},
"textFontFamily": {
"type": "string"
},
"textColor": {
"type": "string"
},
"placeholderColor": {
"type": "string"
},
"iconWidth": {
"type": "number"
},
"iconHeight": {
"type": "number"
},
"iconMarginHorizontal": {
"type": "number"
},
"iconTintColor": {
"type": "string"
},
"labelColor": {
"type": "string"
},
"labelFontFamily": {
"type": "string"
},
"labelFontSize": {
"type": "number"
},
"labelFontWeight": {
"type": "string"
},
"labelMarginBottom": {
"type": "number"
},
"captionMarginTop": {
"type": "number"
},
"captionColor": {
"type": "string"
},
"captionFontFamily": {
"type": "string"
},
"captionFontSize": {
"type": "number"
},
"captionFontWeight": {
"type": "string"
},
"captionIconWidth": {
"type": "number"
},
"captionIconHeight": {
"type": "number"
},
"captionIconMarginRight": {
"type": "number"
},
"captionIconTintColor": {
"type": "string"
}
},
"appearances": {
"default": {
"default": true
}
},
"variantGroups": {
"status": {
"basic": {
"default": true
},
"primary": {
"default": false
},
"success": {
"default": false
},
"info": {
"default": false
},
"warning": {
"default": false
},
"danger": {
"default": false
},
"control": {
"default": false
}
},
"size": {
"small": {
"default": false
},
"medium": {
"default": true
},
"large": {
"default": false
}
}
},
"states": {
"disabled": {
"default": false,
"priority": 0,
"scope": "all"
},
"active": {
"default": false,
"priority": 1,
"scope": "all"
}
}
},
"appearances": {
"default": {
"mapping": {
"paddingHorizontal": 8,
"textMarginHorizontal": 8,
"textFontFamily": "text-font-family",
"iconWidth": 24,
"iconHeight": 24,
"iconMarginHorizontal": 8,
"labelMarginBottom": 4,
"labelFontSize": "text-label-font-size",
"labelFontWeight": "text-label-font-weight",
"labelFontFamily": "text-label-font-family",
"captionMarginTop": 4,
"captionFontSize": "text-caption-1-font-size",
"captionFontWeight": "text-caption-1-font-weight",
"captionFontFamily": "text-caption-1-font-family",
"captionIconWidth": 10,
"captionIconHeight": 10,
"captionIconMarginRight": 8
},
"variantGroups": {
"status": {
"basic": {
"borderColor": "border-basic-color-4",
"backgroundColor": "background-basic-color-2",
"textColor": "text-basic-color",
"labelColor": "text-hint-color",
"captionColor": "text-hint-color",
"placeholderColor": "text-hint-color",
"iconTintColor": "text-hint-color",
"captionIconTintColor": "text-hint-color",
"state": {
"active": {
"borderColor": "color-primary-default",
"backgroundColor": "background-basic-color-1",
"textColor": "text-basic-color"
},
"disabled": {
"borderColor": "border-basic-color-4",
"backgroundColor": "background-basic-color-2",
"textColor": "text-disabled-color",
"iconTintColor": "text-disabled-color"
}
}
},
"primary": {
"borderColor": "color-primary-default",
"backgroundColor": "background-basic-color-2",
"textColor": "text-basic-color",
"labelColor": "text-hint-color",
"captionColor": "text-primary-color",
"placeholderColor": "text-hint-color",
"iconTintColor": "text-primary-color",
"captionIconTintColor": "text-primary-color",
"state": {
"active": {
"borderColor": "color-primary-active",
"backgroundColor": "background-basic-color-1",
"textColor": "text-basic-color"
},
"disabled": {
"borderColor": "border-basic-color-4",
"backgroundColor": "background-basic-color-2",
"textColor": "text-disabled-color",
"iconTintColor": "text-disabled-color"
}
}
},
"success": {
"borderColor": "color-success-default",
"backgroundColor": "background-basic-color-2",
"textColor": "text-basic-color",
"labelColor": "text-hint-color",
"captionColor": "text-success-color",
"placeholderColor": "text-hint-color",
"iconTintColor": "text-success-color",
"captionIconTintColor": "text-success-color",
"state": {
"active": {
"borderColor": "color-success-active",
"backgroundColor": "background-basic-color-1",
"textColor": "text-basic-color"
},
"disabled": {
"borderColor": "border-basic-color-4",
"backgroundColor": "background-basic-color-2",
"textColor": "text-disabled-color",
"iconTintColor": "text-disabled-color"
}
}
},
"info": {
"borderColor": "color-info-default",
"backgroundColor": "background-basic-color-2",
"textColor": "text-basic-color",
"labelColor": "text-hint-color",
"captionColor": "text-info-color",
"placeholderColor": "text-hint-color",
"iconTintColor": "text-info-color",
"captionIconTintColor": "text-info-color",
"state": {
"active": {
"borderColor": "color-info-active",
"backgroundColor": "background-basic-color-1",
"textColor": "text-basic-color"
},
"disabled": {
"borderColor": "border-basic-color-4",
"backgroundColor": "background-basic-color-2",
"textColor": "text-disabled-color",
"iconTintColor": "text-disabled-color"
}
}
},
"warning": {
"borderColor": "color-warning-default",
"backgroundColor": "background-basic-color-2",
"textColor": "text-basic-color",
"labelColor": "text-hint-color",
"captionColor": "text-warning-color",
"placeholderColor": "text-hint-color",
"iconTintColor": "text-warning-color",
"captionIconTintColor": "text-warning-color",
"state": {
"active": {
"borderColor": "color-warning-active",
"backgroundColor": "background-basic-color-1",
"textColor": "text-basic-color"
},
"disabled": {
"borderColor": "border-basic-color-4",
"backgroundColor": "background-basic-color-2",
"textColor": "text-disabled-color",
"iconTintColor": "text-disabled-color"
}
}
},
"danger": {
"borderColor": "color-danger-default",
"backgroundColor": "background-basic-color-2",
"textColor": "text-basic-color",
"labelColor": "text-hint-color",
"captionColor": "text-danger-color",
"placeholderColor": "text-hint-color",
"iconTintColor": "text-danger-color",
"captionIconTintColor": "text-danger-color",
"state": {
"active": {
"borderColor": "color-danger-active",
"backgroundColor": "background-basic-color-1",
"textColor": "text-basic-color"
},
"disabled": {
"borderColor": "border-basic-color-4",
"backgroundColor": "background-basic-color-2",
"textColor": "text-disabled-color",
"iconTintColor": "text-disabled-color"
}
}
},
"control": {
"borderColor": "color-basic-control-transparent-500",
"backgroundColor": "color-basic-control-transparent-300",
"textColor": "text-control-color",
"labelColor": "text-control-color",
"captionColor": "text-control-color",
"placeholderColor": "text-control-color",
"iconTintColor": "text-control-color",
"captionIconTintColor": "text-control-color",
"state": {
"active": {
"borderColor": "color-control-transparent-active-border",
"backgroundColor": "background-basic-color-1",
"textColor": "text-basic-color"
},
"disabled": {
"borderColor": "color-control-transparent-disabled-border",
"backgroundColor": "color-control-transparent-disabled",
"textColor": "text-control-color",
"iconTintColor": "text-control-color"
}
}
}
},
"size": {
"small": {
"minHeight": "size-small",
"borderRadius": "border-radius",
"borderWidth": "border-width",
"paddingVertical": 3,
"textFontSize": "text-subtitle-2-font-size",
"textFontWeight": "normal"
},
"medium": {
"minHeight": "size-medium",
"borderRadius": "border-radius",
"borderWidth": "border-width",
"paddingVertical": 7,
"textFontSize": "text-subtitle-1-font-size",
"textFontWeight": "normal"
},
"large": {
"minHeight": "size-large",
"borderRadius": "border-radius",
"borderWidth": "border-width",
"paddingVertical": 11,
"textFontSize": "text-subtitle-1-font-size",
"textFontWeight": "normal"
}
}
}
}
}
}
}
}
import React from 'react';
import {
GestureResponderEvent,
ImageProps,
StyleProp,
StyleSheet,
TouchableOpacityProps,
View,
ViewProps,
ViewStyle,
Platform,
} from 'react-native';
import DateTimePicker, { TimePickerOptions } from '@react-native-community/datetimepicker';
import {
EvaInputSize,
EvaStatus,
FalsyFC,
FalsyText,
RenderProp,
TouchableWithoutFeedback,
} from '@ui-kitten/components/devsupport';
import { PopoverPlacement } from '@ui-kitten/components/ui/popover/type';
import {
Interaction,
StyledComponentProps,
StyleType,
TextProps,
styled,
Card,
Popover,
PopoverPlacements,
DateService,
NativeDateService,
} from '@ui-kitten/components';
type TimePickerOmitChangeProps = Omit<TimePickerOptions, 'onChange'>;
type TimePickerOmitProps = Omit<TimePickerOmitChangeProps, 'value'>;
export interface TimepickerProps<D = Date> extends StyledComponentProps, TouchableOpacityProps, TimePickerOmitProps {
controlStyle?: StyleProp<ViewStyle>;
date?: D;
label?: RenderProp<TextProps> | React.ReactText;
caption?: RenderProp<TextProps> | React.ReactText;
captionIcon?: RenderProp<Partial<ImageProps>>;
accessoryLeft?: RenderProp<Partial<ImageProps>>;
accessoryRight?: RenderProp<Partial<ImageProps>>;
status?: EvaStatus;
size?: EvaInputSize;
placeholder?: RenderProp<TextProps> | React.ReactText;
placement?: PopoverPlacement | string;
backdropStyle?: StyleProp<ViewStyle>;
onSelect?: (date: D) => void;
onFocus?: () => void;
onBlur?: () => void;
dateService?: DateService<D>;
}
interface State {
visible: boolean;
}
@styled('Timepicker')
export class Timepicker<P> extends React.Component<TimepickerProps & P, State> {
static defaultProps: Partial<TimepickerProps> = {
placement: PopoverPlacements.BOTTOM_START,
dateService: new NativeDateService(),
};
public state: State = {
visible: false,
};
private get title(): RenderProp<TextProps> | React.ReactText | undefined {
const formattedDate = this.props.date && this.props.dateService?.format(this.props.date as Date, 'HH:mm');
return formattedDate || this.props.placeholder;
}
private getComponentStyle = (style: StyleType) => {
const {
textMarginHorizontal,
textFontFamily,
textFontSize,
textFontWeight,
textColor,
placeholderColor,
iconWidth,
iconHeight,
iconMarginHorizontal,
iconTintColor,
labelColor,
labelFontSize,
labelMarginBottom,
labelFontWeight,
labelFontFamily,
captionMarginTop,
captionColor,
captionFontSize,
captionFontWeight,
captionFontFamily,
captionIconWidth,
captionIconHeight,
captionIconMarginRight,
captionIconTintColor,
popoverWidth,
...controlParameters
} = style;
return {
control: controlParameters,
captionContainer: {
marginTop: captionMarginTop,
},
text: {
marginHorizontal: textMarginHorizontal,
fontFamily: textFontFamily,
fontSize: textFontSize,
fontWeight: textFontWeight,
color: textColor,
},
placeholder: {
marginHorizontal: textMarginHorizontal,
color: placeholderColor,
},
icon: {
width: iconWidth,
height: iconHeight,
marginHorizontal: iconMarginHorizontal,
tintColor: iconTintColor,
},
label: {
color: labelColor,
fontSize: labelFontSize,
fontFamily: labelFontFamily,
marginBottom: labelMarginBottom,
fontWeight: labelFontWeight,
},
captionIcon: {
width: captionIconWidth,
height: captionIconHeight,
tintColor: captionIconTintColor,
marginRight: captionIconMarginRight,
},
captionLabel: {
fontSize: captionFontSize,
fontWeight: captionFontWeight,
fontFamily: captionFontFamily,
color: captionColor,
},
popover: {
width: popoverWidth,
marginBottom: captionMarginTop,
},
};
};
private onPress = (event: GestureResponderEvent): void => {
this.setPickerVisible();
this.props.onPress && this.props.onPress(event);
};
private onPressIn = (event: GestureResponderEvent): void => {
// @ts-ignore
this.props.eva.dispatch([Interaction.ACTIVE]);
this.props.onPressIn && this.props.onPressIn(event);
};
private onPressOut = (event: GestureResponderEvent): void => {
// @ts-ignore
this.props.eva.dispatch([]);
this.props.onPressOut && this.props.onPressOut(event);
};
private onValueChange = (event: React.SyntheticEvent, value?: Date): void => {
Platform.OS !== 'ios' && this.setPickerInvisible();
value && this.props.onSelect && this.props.onSelect(value);
};
private onPickerVisible = (): void => {
// @ts-ignore
this.props.eva.dispatch([Interaction.ACTIVE]);
this.props.onFocus && this.props.onFocus();
};
private onPickerInvisible = (): void => {
// @ts-ignore
this.props.eva.dispatch([]);
this.props.onBlur && this.props.onBlur();
};
private setPickerVisible = (): void => {
this.setState({ visible: true }, this.onPickerVisible);
};
private setPickerInvisible = (): void => {
this.setState({ visible: false }, this.onPickerInvisible);
};
private renderPickerDefault = (): React.ReactElement => {
return (
<DateTimePicker
mode='time'
display={this.props.display}
is24Hour={this.props.is24Hour}
value={this.props.date as Date || new Date()}
onChange={this.onValueChange}
/>
);
};
private renderPickerIOS = (): React.ReactElement => {
return (
<Card disabled={true}>
{this.renderPickerDefault()}
</Card>
);
};
private renderInputDefault = (props: Partial<TimepickerProps>, evaStyle): React.ReactElement => {
return (
<TouchableWithoutFeedback
{...props}
style={[evaStyle.control, styles.control, props.controlStyle]}
onPress={this.onPress}
onPressIn={this.onPressIn}
onPressOut={this.onPressOut}>
<FalsyFC
style={evaStyle.icon}
component={props.accessoryLeft}
/>
<FalsyText
style={evaStyle.text}
numberOfLines={1}
ellipsizeMode='tail'
component={this.title}
/>
<FalsyFC
style={evaStyle.icon}
component={this.props.accessoryRight}
/>
</TouchableWithoutFeedback>
);
};
private renderInputIOS = (props: Partial<TimepickerProps>, evaStyle): React.ReactElement => {
return (
<Popover
style={[evaStyle.popover, styles.popover]}
backdropStyle={props.backdropStyle}
fullWidth={true}
placement={props.placement}
visible={this.state.visible}
anchor={() => this.renderInputDefault(props, evaStyle)}
onBackdropPress={this.setPickerInvisible}>
{this.renderPickerIOS()}
</Popover>
);
};
private renderInput = (props: Partial<TimepickerProps>, evaStyle): React.ReactElement => {
return Platform.select({
ios: this.renderInputIOS(props, evaStyle),
default: this.renderInputDefault(props, evaStyle),
});
};
public render(): React.ReactElement<ViewProps> {
const { eva, style, label, caption, captionIcon, ...inputProps } = this.props;
const evaStyle = this.getComponentStyle(this.props.eva?.style as StyleType);
return (
<React.Fragment>
<View style={style}>
<FalsyText
style={[evaStyle.label, styles.label]}
component={label}
/>
{this.renderInput(inputProps, evaStyle)}
<View style={[evaStyle.captionContainer, styles.captionContainer]}>
<FalsyFC
style={evaStyle.captionIcon}
component={captionIcon}
/>
<FalsyText
style={[evaStyle.captionLabel, styles.captionLabel]}
component={caption}
/>
</View>
</View>
{Platform.OS !== 'ios' && this.state.visible && this.renderPickerDefault()}
</React.Fragment>
);
}
}
const styles = StyleSheet.create({
popover: {
borderWidth: 0,
},
control: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
label: {
textAlign: 'left',
},
captionContainer: {
flexDirection: 'row',
alignItems: 'center',
},
captionLabel: {
textAlign: 'left',
},
closeButton: {
alignSelf: 'flex-end',
},
});
@artyorsh
Copy link
Author

artyorsh commented Jun 4, 2020

This component relies on the native time picker implementation, which should be installed into your project, see the guide.

There are two files: mapping.json and time-picker.component.tsx, which is the component itself.
The component uses Eva styling engine. Please see this guide to make it work.

The usage of the component is pretty similar to Datepicker component usage, e.g:

<Timepicker onSelect={date => console.log(date) } />

The rest of the properties are similar to Datepicker, please see the API tab of that component for the reference.

@berrylands
Copy link

Great! Thank you!

@psalm987
Copy link

Thanks for the amazing library, really loving how easy it is to use... how can I theme the time picker pop up window though? it pops up with a blue theme, but my theme color is red and it really clashes with my UI... I used this exact code to create the timepicker component... Thanks for your help... @artyorsh

@jailsonpaca
Copy link

Thank you!

@scrapecoder
Copy link

@psalm987 did you find any solution?

@scrapecoder
Copy link

@artyorsh thanks for your work... but the time picker pop-up color is not changing based on the mapping or eva design. Is it possible to change the color of the picker dialog if it allows us?

@lyqht
Copy link

lyqht commented Apr 14, 2022

Tried using this and got an error

ERROR  TypeError: undefined is not an object (evaluating 'this.meta.appearances')

Not sure what is causing it, any way to resolve this?

EDIT: after modifying the metro.config.js following the docs here, it works!

image

@m-vujicic
Copy link

@artyorsh thanks for amazing library and work on this component.

Since there is still no time-picker component I just wanted to warn anyone using this code - time display in input is not honoring is24Hour property.
In order to correct it, just replace title() function (lines 78-84) in time-picker.component.tsx with:

  private get title(): RenderProp<TextProps> | string | undefined {
    const tformat = this.props.is24Hour ? 'HH:mm' : 'hh:mm a';
    const formattedDate =
      this.props.date &&
      this.props.dateService?.format(this.props.date as Date, tformat);
    return formattedDate || this.props.placeholder;
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment