Skip to content

Commit

Permalink
Fix Date Input on Mobile Devices (#196)
Browse files Browse the repository at this point in the history
* Fix Date Input on Mobile Devices

* functions are pure

* Allow user to input date

* Fix SSR Issues

* Pure comment

* Add documentation
  • Loading branch information
azizhk authored and ritz078 committed Feb 5, 2019
1 parent 7b6af86 commit 36d388b
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 54 deletions.
10 changes: 7 additions & 3 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ Make sure the below code is executed at the very beginning.
The easier way is to use the cdn:

```html
<link rel="stylesheet" href="https://unpkg.com/@anarock/pebble@[version]/dist/pebble.css"/>
<link
rel="stylesheet"
href="https://unpkg.com/@anarock/pebble@[version]/dist/pebble.css"
/>
```

:boom: Warning: Pebble adds `box-sizing: border-box` by default to every element by using [`inherit`](https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/).
Expand Down Expand Up @@ -47,11 +50,12 @@ Then run `react-native link`.
and then it can be used by importing the Icon component.

```jsx
import { Icon } from "@anarock/pebble/native"
import { Icon } from "@anarock/pebble/native";

// Usage
<Icon name="iconName" size={20} color="#000000" />
<Icon name="iconName" size={20} color="#000000" />;
```

## Acknowledgements

We use [Chromaticqa](https://www.chromaticqa.com/) for visual regression testing and it is awesome.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"react-popper": "^1.3.2",
"react-spring": "^7.2.1",
"rheostat": "2.2.0",
"rifm": "^0.5.1",
"scroll-into-view-if-needed": "^2.2.20",
"utility-types": "^2.1.0"
},
Expand Down
147 changes: 118 additions & 29 deletions src/components/DateInput.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,112 @@
import * as React from "react";
import { DateInputProps } from "./typings/DateInput";
import { DateInputProps, DateInputState } from "./typings/DateInput";
import DropDown from "./DropDown";
import { dateClass, dropDownClassName } from "./styles/Date.styles";
import { format } from "date-fns";
import Calendar from "./Calendar";
import Input from "./Input";
// import { cx, css } from "emotion";
// import { isDesktop } from "../utils";
import { Rifm } from "rifm";
import { startOfDay, format } from "date-fns";
import NativeDateInput from "./NativeDateInput";
import { UserAgentInfoContext } from "../utils/useragent";

class DateInput extends React.PureComponent<DateInputProps> {
private onChange = (date: Date | string) => {
this.props.onChange(date ? new Date(date).getTime() : undefined);
const noop = () => {};

function dateFormat(str: string) {
const clean = str.replace(/[^\d]+/gi, "");
const chars = clean.split("");
return chars.reduce(
(r, v, index) =>
`${r}${v}${index === 1 || index === 3 ? "/" : ""}`.substr(0, 10),
""
);
}

export default class DateInput extends React.PureComponent<
DateInputProps,
DateInputState
> {
state: Readonly<DateInputState> = {
stringInput: ""
};

render() {
const { value, calendarProps } = this.props;
static getDerivedStateFromProps(
props: DateInputProps,
state: DateInputState
): Partial<DateInputState> | null {
let newState: Partial<DateInputState> | null = null;
if (props.value && props.value !== state.propsValue) {
newState = {
propsValue: props.value,
stringInput: (props.value && format(props.value, "DD/MM/YYYY")) || ""
};
}
return newState;
}

private onCalendarDateChange = (date: Date) => {
this.props.onChange(date.getTime());
};

private onInputChange = (input: string) => {
this.setState({ stringInput: input });
// TODO: Modify when close to year 9999
if (input.length === 10) {
// RIFM will ensure the length of the input.
const date = startOfDay(new Date());
date.setFullYear(
parseInt(input.substr(6, 4), 10),
parseInt(input.substr(3, 5), 10) - 1,
parseInt(input.substr(0, 2), 10)
);
this.props.onChange(date.getTime());
}
};

// const dropDownClass = cx(dropDownClassName, {
// [css({
// display: "none"
// })]: !isDesktop
// });
render() {
const {
calendarProps,
inputProps,
placeholder,
value: propsValue
} = this.props;

return (
<DropDown
// dropDownClassName={dropDownClass}
dropDownClassName={dropDownClassName}
labelComponent={({ toggleDropdown }) => (
<Input
onChange={this.onChange}
type="text"
value={(value && format(value, "DD/MM/YYYY")) || ""}
placeholder={this.props.placeholder}
fixLabelAtTop
inputProps={{
placeholder: "DD/MM/YYYY"
}}
onClick={toggleDropdown}
/>
<Rifm
value={this.state.stringInput}
onChange={this.onInputChange}
format={dateFormat}
>
{({ value, onChange }) => (
<Input
onChange={noop}
type={"tel"}
value={value}
placeholder={`${placeholder} DD/MM/YYYY`}
onClick={toggleDropdown}
fixLabelAtTop
{...inputProps}
inputProps={{
placeholder: "DD/MM/YYYY",
...(inputProps && inputProps.inputProps),
onChange
}}
/>
)}
</Rifm>
)}
>
{({ toggle }) => (
<Calendar
hideShadow
className={dateClass}
selected={propsValue ? new Date(propsValue) : undefined}
{...calendarProps}
range={false}
selected={value ? new Date(value) : undefined}
onChange={date => {
this.onChange(date);
this.onCalendarDateChange(date);
toggle();
}}
/>
Expand All @@ -58,4 +116,35 @@ class DateInput extends React.PureComponent<DateInputProps> {
}
}

export default DateInput;
function checkDateInputSupport(): boolean {
try {
const input = document.createElement("input");
const type = "date";
input.setAttribute("type", "date");
input.value = "\x01";
return (
input.type === type && (input.value !== "\x01" || !input.checkValidity())
);
} catch (e) {
return true;
}
}

const hasDateInputSupport = /*@__PURE__*/ checkDateInputSupport();

// tslint:disable-next-line max-classes-per-file
export class BrowserBasedDateInput extends React.PureComponent<DateInputProps> {
static contextType = UserAgentInfoContext;
render() {
return (
<UserAgentInfoContext.Consumer>
{({ userAgent }) => {
if (/Android|iPhone|iPad/i.test(userAgent) && hasDateInputSupport) {
return <NativeDateInput {...this.props} />;
}
return <DateInput {...this.props} />;
}}
</UserAgentInfoContext.Consumer>
);
}
}
31 changes: 31 additions & 0 deletions src/components/NativeDateInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from "react";
import { format, parse } from "date-fns";
import { DateInputProps } from "./typings/DateInput";
import Input from "./Input";

export default class NativeDateInput extends React.PureComponent<
DateInputProps
> {
private onDateInputChange = (value: string) => {
const date = parse(value);
this.props.onChange(date && date.getTime());
};

render() {
const { inputProps, placeholder, value } = this.props;

return (
<Input
onChange={this.onDateInputChange}
type="date"
// This format does not define the presentation format.
// value will always be in YYYY-MM-DD format.
// https://developers.google.com/web/updates/2012/08/Quick-FAQs-on-input-type-date-in-Google-Chrome
value={value && format(value, "YYYY-MM-DD")}
placeholder={placeholder}
fixLabelAtTop
{...inputProps}
/>
);
}
}
16 changes: 7 additions & 9 deletions src/components/styles/Input.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,14 @@ export const inputStyle = css({
"&:disabled": {
backgroundColor: colors.white.base
},
// ...(isDesktop && {
// "&[type='date']": {
// "-webkit-appearance": "textfield"
// },
// "&[type='date']::-webkit-inner-spin-button, &[type='date']::-webkit-calendar-picker-indicator": {
// webkitAppearance: "none",
// display: "none"
// },
// }),
.../*#__PURE__*/ mixins.getPlaceholderStyle(colors.gray.light)
// "&[type='date']": {
// "-webkit-appearance": "textfield"
// },
// "&[type='date']::-webkit-inner-spin-button, &[type='date']::-webkit-calendar-picker-indicator": {
// webkitAppearance: "none",
// display: "none"
// }
});

export const inputReadOnlyStyle = css({
Expand Down
8 changes: 8 additions & 0 deletions src/components/typings/DateInput.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { SimpleInputProps } from "./Input";
import { Omit } from "utility-types";
import { DateSingle } from "./Calendar";

export interface DateInputProps {
onChange: (date?: number) => void;
value?: number | Date;
placeholder: string;
inputProps?: Omit<SimpleInputProps, "value" | "onChange" | "placeholder">;
calendarProps?: DateSingle;
}

export interface DateInputState {
stringInput: string;
propsValue?: number | Date;
}
7 changes: 4 additions & 3 deletions src/components/typings/Input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ interface CommonInputProps {
successMessage?: string;
}

interface SimpleInputProps extends CommonInputProps {
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
export interface SimpleInputProps extends CommonInputProps {
inputProps?: React.InputHTMLAttributes<HTMLInputElement> &
React.RefAttributes<HTMLInputElement>;
textArea?: false;
type?: "text" | "date" | "password" | "number" | "email";
type?: "text" | "date" | "password" | "number" | "email" | "tel";
}

interface TextAreaInputProps extends CommonInputProps {
Expand Down
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./strings/capitalize";
export * from "./numbers/format";
export * from "./analytics/ga";
export * from "./dimensions";
export * from "./useragent";
70 changes: 70 additions & 0 deletions src/utils/useragent/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# UserAgentInfoContext Documentation

A React Context Provider specifically meant for userAgent and its derived attributes.
Set userAgent from top level component so that it is accessible within components on server and client.
Similar to [@quentin-sommer/react-useragent](https://github.com/quentin-sommer/react-useragent) but with type definitions and reduced scope.

```tsx
import { utils } from "@anarock/pebble";
const { UserAgentInfoContext } = utils;

class Component extends React.PureComponent {
static context = UserAgentInfoContext;
render () {
return (
<UserAgentInfoContext.Consumer>
{({ userAgent, isMobile }) => (
// Can use userAgent & isMobile here.
)}
</UserAgentInfoContext.Consumer>
)
}
}
```

## Usage with next.js

```tsx
import App, { Container, NextAppContext } from "next/app";
import * as React from "react";
import { utils } from "@anarock/pebble";

interface RootProps {
Component: React.ComponentClass;
userAgent: string;
// tslint:disable-next-line no-any
pageProps: any;
}

const { UserAgentInfoProvider } = utils;

export default class MyApp extends App<RootProps> {
static async getInitialProps({ Component, ctx }: NextAppContext) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}

const userAgent =
(ctx.req
? ctx.req.headers["user-agent"]
: typeof navigator !== "undefined" && navigator.userAgent) || "";

return {
userAgent,
pageProps
};
}

render() {
const { Component, pageProps, userAgent } = this.props;
return (
<Container>
<UserAgentInfoProvider userAgent={userAgent}>
<Component {...pageProps} />
</UserAgentInfoProvider>
</Container>
);
}
}
```
Loading

0 comments on commit 36d388b

Please sign in to comment.