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

STCOM-944 provide NumberField component #1740

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -19,6 +19,7 @@
* Button: Button link style has a min-height, which can offset it from text. Fixes STCOM-1039.
* The vertical scroll bar displays at the second pane when it doesn't need. Fixes STCOM-1044.
* Focus management and accessible labeling of confirmation modals. Confirmation modals announce in a way similart to Javascript alerts. Fixes STCOM-1041.
* Provide `NumberField` component. Refs STCOM-944.

## [10.2.0](https://github.com/folio-org/stripes-components/tree/v10.2.0) (2022-06-14)
[Full Changelog](https://github.com/folio-org/stripes-components/compare/v10.1.0...v10.2.0)
Expand Down
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -21,6 +21,7 @@ export { default as FormattedUTCDate } from './lib/FormattedUTCDate';
export { default as Label } from './lib/Label';
export { default as TextLink } from './lib/TextLink';
export { Loading, LoadingPane, LoadingView } from './lib/Loading';
export { default as NumberField } from './lib/NumberField';
export { default as RadioButton } from './lib/RadioButton';
export { default as RadioButtonGroup } from './lib/RadioButtonGroup';
export { default as Select } from './lib/Select';
Expand Down
59 changes: 59 additions & 0 deletions lib/NumberField/NumberField.js
@@ -0,0 +1,59 @@
import { useIntl } from 'react-intl';
import PropTypes from 'prop-types';
import TextField from '../TextField';

/**
* Number
* @param {*} param
* @returns
*/
const NumberField = ({ field, ...rest }) => {
const intl = useIntl();

/**
* The plot here is to take a number in native-js (12345.6),
* collect its parts when pushed through Intl.NumberFormat,
* and then use those parts to create a parser.
*
* It works for any locale, including those using non-Arabic
* numerals (i.e. other than 0-9).
*
* verbatim from https://observablehq.com/@mbostock/localized-number-parsing
*/
const parts = new Intl.NumberFormat(intl.locale).formatToParts(12345.6);
const numerals = [...new Intl.NumberFormat(intl.locale, { useGrouping: false }).format(9876543210)].reverse();
const index = new Map(numerals.map((d, i) => [d, i]));
const nfGroup = new RegExp(`[${parts.find(d => d.type === 'group').value}]`, 'g');
const nfDecimal = new RegExp(`[${parts.find(d => d.type === 'decimal').value}]`);
const nfNumeral = new RegExp(`[${numerals.join('')}]`, 'g');
const nfIndex = d => index.get(d);

const parse = (v) => {
return v.trim()
.replace(nfGroup, '')
.replace(nfDecimal, '.')
.replace(nfNumeral, nfIndex)
? +v : NaN;
};

const format = (v) => {
return intl.formatNumber(v);
};

const Field = field;

return (
<Field
{...rest}
component={TextField}
parse={parse}
format={format}
type="numberasdfasdffffffffff"
/>);
};

NumberField.propTypes = {
field: PropTypes.func
};

export default NumberField;
1 change: 1 addition & 0 deletions lib/NumberField/index.js
@@ -0,0 +1 @@
export { default } from './NumberField';
21 changes: 21 additions & 0 deletions lib/NumberField/readme.md
@@ -0,0 +1,21 @@
# NumberField

Input field for parsing numeric strings in any locale ("1,234.56", "1.234,56") into JS numbers (1234.56), i.e. `atof`.

## Basic Usage
```js
import { NumberField } from '@folio/stripes/components';

<NumberField
name="amount"
label={<FormattedMessage id="ui-users.charge.amount.label" />}
field={Field}
id="amount"
fullWidth
required
>
```

## Summary

Detail: Convert a numeric string in any locale to a JS float, i.e. `atof` for all you C programmers. In a Javascript number, the comma `,` is used for grouping, the decimal `.` for separating the whole and decimal portion of floating point numbers, and the numerals consist of 0-9. These values are the same in the `en-US` locale but are not shared by all locales, e.g. `de-DE` which uses `.` for grouping and `,` for decimal; of course, other locales may not use Arabic numerals. This component allows users to enter numeric strings in the format expected by their current locale and have them be correctly parsed into actual JS numbers.
65 changes: 65 additions & 0 deletions lib/NumberField/tests/NumberField-test.js
@@ -0,0 +1,65 @@
import React from 'react';
import { describe, beforeEach, it } from 'mocha';
import { expect } from 'chai';

import { NumberField as Interactor } from '@folio/stripes-testing';
import { mountWithContext } from '../../../tests/helpers';

import NumberField from '../NumberField';

const Field = ({ children, rest }) => <div {...rest}>{children}</div>;

describe.only('NumberField', () => {
const numberField = Interactor();

describe('Native JS locale, i.e. format matches JS (en-US)', () => {
beforeEach(async () => {
await mountWithContext(
<NumberField
field={Field}
id="nfTest"
name="nf"
label="number-field"
/>,
[],
'en-US'
);
});

it('renders an input type="number" by default', () => {
console.log()
numberField.has({ type: 'nuasdfasdfmber' });
});

it('applies the supplied id prop to the input', () => textField.has({ id: 'nfTest' }));

describe('entering numbers into the field', () => {
beforeEach(() => numberField.fillIn('1,234.56'));

it('updates the value', () => numberField.has({ value: 1234.56 }));
});
});

describe('Non-native JS locale, i.e. format does NOT match JS (de-DE)', () => {
beforeEach(async () => {
await mountWithContext(
<NumberField
id="nfTest"
field={Field}
/>,
[],
'de-DE'
);
});

it('renders an input type="number" by default', () => {
numberField.has({ type: 'number' });
});

describe('entering numbers into the field', () => {
beforeEach(() => numberField.fillIn('1.234,56'));

it('updates the value', () => numberField.has({ value: 1234.56 }));
});
});
});