Skip to content

Commit

Permalink
feat(react-intl): introduce FormattedDateTimeRange, a stage-3 API
Browse files Browse the repository at this point in the history
  • Loading branch information
longlho committed Nov 5, 2020
1 parent b08ee67 commit ebff2a3
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 4 deletions.
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module.exports = {
testEnvironment: 'node',
globals: {
'ts-jest': {
tsConfig: 'tsconfig.json',
tsconfig: 'tsconfig.json',
},
},
reporters: ['default', './tools/jest-reporter'],
Expand Down
1 change: 1 addition & 0 deletions packages/react-intl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,4 @@ export {FormattedNumberParts} from './src/components/createFormattedComponent';
export {default as FormattedRelativeTime} from './src/components/relative';
export {default as FormattedPlural} from './src/components/plural';
export {default as FormattedMessage} from './src/components/message';
export {default as FormattedDateTimeRange} from './src/components/dateTimeRange';
31 changes: 31 additions & 0 deletions packages/react-intl/src/components/dateTimeRange.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import {Context} from './injectIntl';
import {FormatDateOptions} from '@formatjs/intl';
import {invariantIntlContext} from '../utils';
import {DateTimeFormat} from '@formatjs/ecma402-abstract';

interface Props extends FormatDateOptions {
from: Parameters<DateTimeFormat['formatRange']>[0];
to: Parameters<DateTimeFormat['formatRange']>[1];
children?(value: React.ReactNode): React.ReactElement | null;
}

const FormattedDateTimeRange: React.FC<Props> = props => (
<Context.Consumer>
{(intl): React.ReactElement | null => {
invariantIntlContext(intl);
const {from, to, children, ...formatProps} = props;
// TODO: fix TS type definition for localeMatcher upstream
const formattedValue = intl.formatDateTimeRange(from, to, formatProps);

if (typeof children === 'function') {
return children(formattedValue as any);
}
const Text = intl.textComponent || React.Fragment;
return <Text>{formattedValue}</Text>;
}}
</Context.Consumer>
);

FormattedDateTimeRange.displayName = 'FormattedDateTimeRange';
export default FormattedDateTimeRange;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<FormattedDateTimeRange> falls back and warns on invalid Intl.DateTimeFormat options 1`] = `"FORMAT_ERROR"`;
94 changes: 94 additions & 0 deletions packages/react-intl/tests/unit/components/dateTimeRange.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as React from 'react';
import {mount} from 'enzyme';
import {FormattedDateTimeRange} from '../../../';
import {mountFormattedComponentWithProvider} from '../testUtils';
import {createIntl} from '../../../src/components/provider';
import {IntlShape} from '../../../';

const mountWithProvider = mountFormattedComponentWithProvider(
FormattedDateTimeRange
);

describe('<FormattedDateTimeRange>', () => {
let intl: IntlShape;
beforeEach(() => {
intl = createIntl({
locale: 'en',
});
});

it('has a `displayName`', () => {
expect(typeof FormattedDateTimeRange.displayName).toBe('string');
});

it('throws when <IntlProvider> is missing from ancestry', () => {
expect(() =>
mount(<FormattedDateTimeRange from={Date.now()} to={Date.now()} />)
).toThrow(Error);
});

it('renders a formatted date in a <>', () => {
const from = new Date('2020-01-01');
const to = new Date('2020-01-15');

const rendered = mountWithProvider({from, to}, intl);

expect(rendered.text()).toBe(intl.formatDateTimeRange(from, to));
});
it('renders a formatted date w/o textComponent', () => {
const from = new Date('2020-01-01');
const to = new Date('2020-01-15');
const rendered = mountWithProvider(
{from, to},
{...intl, textComponent: '' as any}
);

expect(rendered.text()).toBe(intl.formatDateTimeRange(from, to));
});

it('accepts valid Intl.DateTimeFormat options as props', () => {
const from = new Date('2020-01-01');
const to = new Date('2020-01-15');
const options = {year: 'numeric'};

const rendered = mountWithProvider({from, to, ...options}, intl);

expect(rendered.text()).toBe(intl.formatDateTimeRange(from, to, options));
});

it('falls back and warns on invalid Intl.DateTimeFormat options', () => {
const from = new Date();
const onError = jest.fn();
const rendered = mountWithProvider(
// @ts-ignore
{from, to: undefined, year: 'invalid'},
{...intl, onError}
);

expect(rendered.text()).toBe(String(from));
expect(onError).toHaveBeenCalled();
expect(onError.mock.calls[0][0].code).toMatchSnapshot();
});

it('supports function-as-child pattern', () => {
const from = new Date('2020-01-01');
const to = new Date('2020-01-15');
const spyChildren = jest.fn().mockImplementation(() => <b>Jest</b>);
const rendered = mountWithProvider(
{
from,
to,
children: spyChildren,
},
intl
).find('b');

expect(spyChildren).toHaveBeenCalledTimes(1);
expect(spyChildren.mock.calls[0]).toEqual([
intl.formatDateTimeRange(from, to),
]);

expect(rendered.type()).toBe('b');
expect(rendered.text()).toBe('Jest');
});
});
7 changes: 6 additions & 1 deletion website/docs/polyfills/intl-datetimeformat.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
id: intl-datetimeformat
title: Intl.DateTimeFormat (ES2020)
title: Intl.DateTimeFormat (ESNext)
---

A spec-compliant polyfill for Intl.DateTimeFormat fully tested by the [official ECMAScript Conformance test suite](https://github.com/tc39/test262)
Expand All @@ -16,6 +16,11 @@ Right now we only support Gregorian calendar in this polyfill. Therefore we reco
Right now this polyfill supports daylight transition until 2038 due to [Year 2038 problem](https://en.wikipedia.org/wiki/Year_2038_problem).
:::

## Features

- [dateStyle/timeStyle](https://github.com/tc39/proposal-intl-datetime-style)
- [formatRange](https://github.com/tc39/proposal-intl-DateTimeFormat-formatRange)

## Installation

import Tabs from '@theme/Tabs'
Expand Down
4 changes: 2 additions & 2 deletions website/docs/polyfills/intl-numberformat.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
id: intl-numberformat
title: Intl.NumberFormat (ES2020)
title: Intl.NumberFormat (ESNext)
---

A polyfill for ES2020 [`Intl.NumberFormat`][numberformat] and [`Number.prototype.toLocaleString`][tolocalestring].
A polyfill for ESNext [`Intl.NumberFormat`][numberformat] and [`Number.prototype.toLocaleString`][tolocalestring].

[numberformat]: https://tc39.es/ecma402/#numberformat-objects
[tolocalestring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString
Expand Down
27 changes: 27 additions & 0 deletions website/docs/react-intl/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ interface IntlConfig {
interface IntlFormatters {
formatDate(value: number | Date, opts: FormatDateOptions): string
formatTime(value: number | Date, opts: FormatDateOptions): string
formatDateTimeRange(
from: number | Date,
to: number | Date,
opts: FormatDateOptions
): string
formatRelativeTime(
value: number,
unit: Unit,
Expand Down Expand Up @@ -225,6 +230,28 @@ It expects a `value` which can be parsed as a date (i.e., `isFinite(new Date(val
intl.formatTime(Date.now()) // "4:03 PM"
```

## formatDateTimeRange

:::caution browser support
This requires stage-3 API [Intl.RelativeTimeFormat.prototype.formatRange](https://github.com/tc39/proposal-intl-DateTimeFormat-formatRange) which has limited browser support. Please use our [polyfill](../polyfills/intl-datetimeformat.md) if you plan to support them.
:::

```tsx
function formatDateTimeRange(
from: number | Date,
to: number | Date,
options?: Intl.DateTimeFormatOptions & {format?: string}
): string
```

This function will return a formatted date/time range string

It expects 2 values (a `from` Date & a `to` Date) and accepts `options` that conform to `DateTimeFormatOptions`.

```tsx live
intl.formatDateTimeRange(new Date('2020-01-01'), new Date('2020-01-15')) // "Jan 1 - 15"
```

## formatRelativeTime

:::caution browser support
Expand Down
30 changes: 30 additions & 0 deletions website/docs/react-intl/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,36 @@ props: Intl.DateTimeFormatOptions &
</FormattedTimeParts>
```

## FormattedDateTimeRange

:::caution browser support
This requires stage-3 API [Intl.RelativeTimeFormat.prototype.formatRange](https://github.com/tc39/proposal-intl-DateTimeFormat-formatRange) which has limited browser support. Please use our [polyfill](../polyfills/intl-datetimeformat.md) if you plan to support them.
:::

This component uses the [`formatDateTimeRange`](api.md#formatdatetimerange) and [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat) APIs and has `props` that correspond to the `DateTimeFormatOptions` specified above

**Props:**

```tsx
props: DateTimeFormatOptions &
{
from: number | Date,
to: number | Date,
children: (formattedDate: string) => ReactElement,
}
```

By default `<FormattedDateTimeRange>` will render the formatted time into a `React.Fragment`. If you need to customize rendering, you can either wrap it with another React element (recommended), or pass a function as the child.

**Example:**

```tsx live
<FormattedDateTimeRange
from={new Date('2020-01-01')}
to={new Date('2020-01-15')}
/>
```

## FormattedRelativeTime

:::caution browser support
Expand Down

0 comments on commit ebff2a3

Please sign in to comment.