Last active
March 24, 2024 15:54
-
-
Save artyorsh/f9846c93e42a5ed45364ea747d0c4101 to your computer and use it in GitHub Desktop.
Time Picker example with UI Kitten and DateTimePicker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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" | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | |
}, | |
}); |
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!
@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
@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?