Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add components to show only time #7917

Merged
merged 4 commits into from
Aug 23, 2022
Merged
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
15 changes: 12 additions & 3 deletions docs/DateField.md
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ title: "The DateField Component"

# `<DateField>`

Displays a date or datetime using the browser locale (thanks to `Date.toLocaleDateString()` and `Date.toLocaleString()`).
Displays a date, datetime or time using the browser locale (thanks to `Date.toLocaleDateString()`, `Date.toLocaleString()` and `Date.toLocaleTimeString()`).

```jsx
import { DateField } from 'react-admin';
@@ -19,13 +19,15 @@ import { DateField } from 'react-admin';
| ---------- | -------- | ------- | ------- | -------------------------------------------------------------------------------------------------------- |
| `locales` | Optional | string | '' | Override the browser locale in the date formatting. Passed as first argument to `Intl.DateTimeFormat()`. |
| `options` | Optional | Object | - | Date formatting options. Passed as second argument to `Intl.DateTimeFormat()`. |
| `showTime` | Optional | boolean | `false` | If true, show date and time. If false, show only date |
| `showTime` | Optional | boolean | `false` | If true, show the time |
| `showDate` | Optional | boolean | `true` | If true, show the date |


`<DateField>` also accepts the [common field props](./Fields.md#common-field-props).

## Usage

This component accepts a `showTime` attribute (`false` by default) to force the display of time in addition to date. It uses `Intl.DateTimeFormat()` if available, passing the `locales` and `options` props as arguments. If Intl is not available, it ignores the `locales` and `options` props.
This component accepts `showTime` and `showDate` props to decide whether to display a date (`showTime=false` and `showDate=true`), a datetime (`showTime=true` and `showDate=true`) or time (`showTime=true` and `showDate=false`). Setting `showTime` and `showDate` to false at the same time will throw and error. It uses `Intl.DateTimeFormat()` if available, passing the `locales` and `options` props as arguments. If Intl is not available, it ignores the `locales` and `options` props.

{% raw %}
```jsx
@@ -37,6 +39,13 @@ This component accepts a `showTime` attribute (`false` by default) to force the
// renders the record { id: 1234, publication_date: new Date('2017-04-23 23:05') } as
<span>4/23/2017, 11:05:00 PM</span>

<DateField source="publication_date" showTime showDate={false} />
// renders the record { id: 1234, publication_date: new Date('2017-04-23 23:05') } as
<span>11:05:00 PM</span>

<DateField source="publication_date" showTime={false} showDate={false} />
// throws an error as nothing would be displayed

<DateField source="publication_date" options={{ weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }} />
// renders the record { id: 1234, publication_date: new Date('2017-04-23') } as
<span>Sunday, April 23, 2017</span>
25 changes: 25 additions & 0 deletions docs/TimeInput.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
layout: default
title: "The TimeInput Component"
---

# `<TimeInput>`

An input for editing time. `<TimeInput>` renders a standard browser [Time Picker](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/time), so the appearance depends on the browser.

| Firefox | Edge |
| ------- | ---- |
| ![TimeInput Firefox](./img/time-input-firefox.gif) | ![TimeInput Edge](./img/time-input-edge.gif) |

This component works with Date objects to handle the timezone using the browser locale.
You can still pass string values as long as those can be converted to a JavaScript Date object.

```jsx
import { TimeInput } from 'react-admin';

<TimeInput source="published_at" />
```

`<TimeInput>` also accepts the [common input props](./Inputs.md#common-input-props).

**Tip**: For a MUI styled `<TimeInput>` component, check out [vascofg/react-admin-date-inputs](https://github.com/vascofg/react-admin-date-inputs).
Binary file added docs/img/time-input-edge.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/time-input-firefox.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/navigation.html
Original file line number Diff line number Diff line change
@@ -161,6 +161,7 @@
<li {% if page.path == 'SelectArrayInput.md' %} class="active" {% endif %}><a class="nav-link" href="./SelectArrayInput.html"><code>&lt;SelectArrayInput&gt;</code></a></li>
<li {% if page.path == 'SelectInput.md' %} class="active" {% endif %}><a class="nav-link" href="./SelectInput.html"><code>&lt;SelectInput&gt;</code></a></li>
<li {% if page.path == 'TextInput.md' %} class="active" {% endif %}><a class="nav-link" href="./TextInput.html"><code>&lt;TextInput&gt;</code></a></li>
<li {% if page.path == 'TimeInput.md' %} class="active" {% endif %}><a class="nav-link" href="./TimeInput.html"><code>&lt;TimeInput&gt;</code></a></li>
<li {% if page.path == 'TranslatableInputs.md' %} class="active" {% endif %}><a class="nav-link" href="./TranslatableInputs.html"><code>&lt;TranslatableInputs&gt;</code></a></li>
<li {% if page.path == 'PasswordInput.md' %} class="active" {% endif %}><a class="nav-link" href="./PasswordInput.html"><code>&lt;PasswordInput&gt;</code></a></li>
<li {% if page.path == 'ReferenceArrayInput.md' %} class="active" {% endif %}><a class="nav-link" href="./ReferenceArrayInput.html"><code>&lt;ReferenceArrayInput&gt;</code></a></li>
15 changes: 15 additions & 0 deletions packages/ra-ui-materialui/src/field/DateField.spec.tsx
Original file line number Diff line number Diff line change
@@ -71,6 +71,21 @@ describe('<DateField />', () => {
expect(queryByText(date)).not.toBeNull();
});

it('should render only a time when the showtime prop is true and showdate is false', () => {
const { queryByText } = render(
<DateField
record={{ id: 123, foo: new Date('2017-04-23 23:05') }}
showTime
showDate={false}
source="foo"
locales="en-US"
/>
);

const date = new Date('2017-04-23 23:05').toLocaleTimeString('en-US');
expect(queryByText(date)).not.toBeNull();
});

it('should pass the options prop to toLocaleString', () => {
const date = new Date('2017-04-23');
const options = {
29 changes: 23 additions & 6 deletions packages/ra-ui-materialui/src/field/DateField.tsx
Original file line number Diff line number Diff line change
@@ -39,9 +39,17 @@ export const DateField: FC<DateFieldProps> = memo(props => {
locales,
options,
showTime = false,
showDate = true,
source,
...rest
} = props;

if (!showTime && !showDate) {
throw new Error(
'<DateField> cannot have showTime and showDate false at the same time'
);
}

const record = useRecordContext(props);
if (!record) {
return null;
@@ -73,13 +81,20 @@ export const DateField: FC<DateFieldProps> = memo(props => {
// who may see a different date when calling toLocaleDateString().
dateOptions = { timeZone: 'UTC' };
}
const dateString = showTime
? toLocaleStringSupportsLocales
let dateString = '';
if (showTime && showDate) {
dateString = toLocaleStringSupportsLocales
? date.toLocaleString(locales, options)
: date.toLocaleString()
: toLocaleStringSupportsLocales
? date.toLocaleDateString(locales, dateOptions)
: date.toLocaleDateString();
: date.toLocaleString();
} else if (showDate) {
dateString = toLocaleStringSupportsLocales
? date.toLocaleDateString(locales, dateOptions)
: date.toLocaleDateString();
} else if (showTime) {
dateString = toLocaleStringSupportsLocales
? date.toLocaleTimeString(locales, options)
: date.toLocaleTimeString();
}

return (
<Typography
@@ -103,6 +118,7 @@ DateField.propTypes = {
]),
options: PropTypes.object,
showTime: PropTypes.bool,
showDate: PropTypes.bool,
};

DateField.displayName = 'DateField';
@@ -114,6 +130,7 @@ export interface DateFieldProps
locales?: string | string[];
options?: object;
showTime?: boolean;
showDate?: boolean;
}

const toLocaleStringSupportsLocales = (() => {
200 changes: 200 additions & 0 deletions packages/ra-ui-materialui/src/input/TimeInput.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import * as React from 'react';
import expect from 'expect';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { required, testDataProvider } from 'ra-core';
import { format } from 'date-fns';
import { useFormState } from 'react-hook-form';

import { AdminContext } from '../AdminContext';
import { SimpleForm, Toolbar } from '../form';
import { TimeInput } from './TimeInput';
import { ArrayInput, SimpleFormIterator } from './ArrayInput';
import { SaveButton } from '../button';

describe('<TimeInput />', () => {
const defaultProps = {
resource: 'posts',
source: 'publishedAt',
};

it('should render a time input', () => {
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm onSubmit={jest.fn()}>
<TimeInput {...defaultProps} />
</SimpleForm>
</AdminContext>
);
const input = screen.getByLabelText(
'resources.posts.fields.publishedAt'
) as HTMLInputElement;
expect(input.type).toBe('time');
});

it('should not make the form dirty on initialization', () => {
const publishedAt = new Date();
const FormState = () => {
const { isDirty } = useFormState();

return <p>Dirty: {isDirty.toString()}</p>;
};
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm
onSubmit={jest.fn()}
record={{ id: 1, publishedAt: publishedAt.toISOString() }}
>
<TimeInput {...defaultProps} />
<FormState />
</SimpleForm>
</AdminContext>
);
expect(screen.getByDisplayValue(format(publishedAt, 'HH:mm')));
expect(screen.queryByText('Dirty: false')).not.toBeNull();
});

it('should display a default value inside an ArrayInput', () => {
const date = new Date('Wed Oct 05 2011 16:48:00 GMT+0200');
const backlinksDefaultValue = [
{
date,
},
];
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm onSubmit={jest.fn()}>
<ArrayInput
defaultValue={backlinksDefaultValue}
source="backlinks"
>
<SimpleFormIterator>
<TimeInput source="date" />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</AdminContext>
);

expect(screen.getByDisplayValue(format(date, 'HH:mm')));
});

it('should submit the form default value with its timezone', async () => {
const publishedAt = new Date('Wed Oct 05 2011 16:48:00 GMT+0200');
const onSubmit = jest.fn();
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm
onSubmit={onSubmit}
defaultValues={{ publishedAt }}
toolbar={
<Toolbar>
<SaveButton alwaysEnable />
</Toolbar>
}
>
<TimeInput {...defaultProps} />
</SimpleForm>
</AdminContext>
);
expect(
screen.queryByDisplayValue(format(publishedAt, 'HH:mm'))
).not.toBeNull();
fireEvent.click(screen.getByLabelText('ra.action.save'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
publishedAt,
});
});
});

it('should submit the input default value with its timezone', async () => {
const publishedAt = new Date('Wed Oct 05 2011 16:48:00 GMT+0200');
const onSubmit = jest.fn();
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm
onSubmit={onSubmit}
toolbar={
<Toolbar>
<SaveButton alwaysEnable />
</Toolbar>
}
>
<TimeInput {...defaultProps} defaultValue={publishedAt} />
</SimpleForm>
</AdminContext>
);
expect(
screen.queryByDisplayValue(format(publishedAt, 'HH:mm'))
).not.toBeNull();
fireEvent.click(screen.getByLabelText('ra.action.save'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
publishedAt,
});
});
});

describe('error message', () => {
it('should not be displayed if field is pristine', () => {
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm onSubmit={jest.fn()}>
<TimeInput {...defaultProps} validate={required()} />
</SimpleForm>
</AdminContext>
);
expect(screen.queryByText('ra.validation.required')).toBeNull();
});

it('should be displayed if field has been touched and is invalid', async () => {
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm onSubmit={jest.fn()} mode="onBlur">
<TimeInput {...defaultProps} validate={required()} />
</SimpleForm>
</AdminContext>
);
const input = screen.getByLabelText(
'resources.posts.fields.publishedAt *'
);
fireEvent.blur(input);
await waitFor(() => {
expect(
screen.queryByText('ra.validation.required')
).not.toBeNull();
});
});

it('should be displayed if field has been touched multiple times and is invalid', async () => {
const onSubmit = jest.fn();
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm onSubmit={onSubmit}>
<TimeInput {...defaultProps} validate={required()} />
</SimpleForm>
</AdminContext>
);
const input = screen.getByLabelText(
'resources.posts.fields.publishedAt *'
);
fireEvent.change(input, {
target: { value: new Date().toISOString() },
});
fireEvent.blur(input);
await waitFor(() => {
expect(screen.queryByText('ra.validation.required')).toBeNull();
});
fireEvent.change(input, {
target: { value: '' },
});
fireEvent.blur(input);
fireEvent.click(screen.getByText('ra.action.save'));
await waitFor(() => {
expect(
screen.queryByText('ra.validation.required')
).not.toBeNull();
});
});
});
});
38 changes: 38 additions & 0 deletions packages/ra-ui-materialui/src/input/TimeInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as React from 'react';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';

import { AdminContext } from '../AdminContext';
import { Create } from '../detail';
import { SimpleForm } from '../form';
import { TimeInput } from './TimeInput';

export default { title: 'ra-ui-materialui/input/TimeInput' };

export const Basic = () => (
<Wrapper>
<TimeInput source="published" />
</Wrapper>
);

export const FullWidth = () => (
<Wrapper>
<TimeInput source="published" fullWidth />
</Wrapper>
);

export const Disabled = () => (
<Wrapper>
<TimeInput source="published" disabled />
</Wrapper>
);

const i18nProvider = polyglotI18nProvider(() => englishMessages);

const Wrapper = ({ children }) => (
<AdminContext i18nProvider={i18nProvider}>
<Create resource="posts">
<SimpleForm>{children}</SimpleForm>
</Create>
</AdminContext>
);
Loading