diff --git a/packages/core/src/components/DateField/DateField.jsx b/packages/core/src/components/DateField/DateField.jsx index 4b5fb375a1..d89e57b278 100644 --- a/packages/core/src/components/DateField/DateField.jsx +++ b/packages/core/src/components/DateField/DateField.jsx @@ -82,7 +82,7 @@ export class DateField extends React.PureComponent { inversed: this.props.inversed, onBlur: (this.props.onBlur || this.props.onComponentBlur) && this.handleBlur, onChange: this.props.onChange && this.handleChange, - type: 'number' + numeric: true }; const labelId = this.labelId(); @@ -109,8 +109,6 @@ export class DateField extends React.PureComponent { this.monthInput = el; if (this.props.monthFieldRef) this.props.monthFieldRef(el); }} - max="12" - min="1" defaultValue={this.props.monthDefaultValue} label={this.props.monthLabel} name={this.props.monthName} @@ -127,8 +125,6 @@ export class DateField extends React.PureComponent { this.dayInput = el; if (this.props.dayFieldRef) this.props.dayFieldRef(el); }} - max="31" - min="1" defaultValue={this.props.dayDefaultValue} label={this.props.dayLabel} name={this.props.dayName} @@ -147,8 +143,6 @@ export class DateField extends React.PureComponent { }} defaultValue={this.props.yearDefaultValue} label={this.props.yearLabel} - min={this.props.yearMin} - max={this.props.yearMax} name={this.props.yearName} value={this.props.yearValue} aria-describedby={labelId} @@ -167,7 +161,6 @@ DateField.defaultProps = { monthLabel: 'Month', monthName: 'month', yearLabel: 'Year', - yearMin: 1900, yearName: 'year', dateFormatter: defaultDateFormatter }; @@ -282,14 +275,6 @@ DateField.propTypes = { * Label for the year `input` field */ yearLabel: PropTypes.node, - /** - * Max value for the year `input` field - */ - yearMax: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - /** - * Minimum value for the year `input` field - */ - yearMin: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** * `name` for the year field */ diff --git a/packages/core/src/components/DateField/__snapshots__/DateField.test.jsx.snap b/packages/core/src/components/DateField/__snapshots__/DateField.test.jsx.snap index 83f8cf0c4d..142055cc9e 100644 --- a/packages/core/src/components/DateField/__snapshots__/DateField.test.jsx.snap +++ b/packages/core/src/components/DateField/__snapshots__/DateField.test.jsx.snap @@ -43,13 +43,13 @@ exports[`DateField has custom yearMax and yearMin 1`] = ` className="ds-c-field ds-c-field--month" defaultValue={undefined} id="textfield_snapshot" - max="12" - min="1" + inputMode="numeric" name="month" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -78,13 +78,13 @@ exports[`DateField has custom yearMax and yearMin 1`] = ` className="ds-c-field ds-c-field--day" defaultValue={undefined} id="textfield_snapshot" - max="31" - min="1" + inputMode="numeric" name="day" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -113,13 +113,13 @@ exports[`DateField has custom yearMax and yearMin 1`] = ` className="ds-c-field ds-c-field--year" defaultValue={undefined} id="textfield_snapshot" - max={2000} - min="1990" + inputMode="numeric" name="year" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -177,13 +177,13 @@ exports[`DateField has errorMessage 1`] = ` className="ds-c-field ds-c-field--month" defaultValue={undefined} id="textfield_snapshot" - max="12" - min="1" + inputMode="numeric" name="month" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -212,13 +212,13 @@ exports[`DateField has errorMessage 1`] = ` className="ds-c-field ds-c-field--day" defaultValue={undefined} id="textfield_snapshot" - max="31" - min="1" + inputMode="numeric" name="day" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -247,13 +247,13 @@ exports[`DateField has errorMessage 1`] = ` className="ds-c-field ds-c-field--year" defaultValue={undefined} id="textfield_snapshot" - max={undefined} - min={1900} + inputMode="numeric" name="year" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -304,13 +304,13 @@ exports[`DateField has invalid day 1`] = ` className="ds-c-field ds-c-field--month" defaultValue={undefined} id="textfield_snapshot" - max="12" - min="1" + inputMode="numeric" name="month" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -339,13 +339,13 @@ exports[`DateField has invalid day 1`] = ` className="ds-c-field ds-c-field--day ds-c-field--error" defaultValue={undefined} id="textfield_snapshot" - max="31" - min="1" + inputMode="numeric" name="day" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -374,13 +374,13 @@ exports[`DateField has invalid day 1`] = ` className="ds-c-field ds-c-field--year" defaultValue={undefined} id="textfield_snapshot" - max={undefined} - min={1900} + inputMode="numeric" name="year" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -431,13 +431,13 @@ exports[`DateField has invalid month 1`] = ` className="ds-c-field ds-c-field--month ds-c-field--error" defaultValue={undefined} id="textfield_snapshot" - max="12" - min="1" + inputMode="numeric" name="month" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -466,13 +466,13 @@ exports[`DateField has invalid month 1`] = ` className="ds-c-field ds-c-field--day" defaultValue={undefined} id="textfield_snapshot" - max="31" - min="1" + inputMode="numeric" name="day" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -501,13 +501,13 @@ exports[`DateField has invalid month 1`] = ` className="ds-c-field ds-c-field--year" defaultValue={undefined} id="textfield_snapshot" - max={undefined} - min={1900} + inputMode="numeric" name="year" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -558,13 +558,13 @@ exports[`DateField has invalid year 1`] = ` className="ds-c-field ds-c-field--month" defaultValue={undefined} id="textfield_snapshot" - max="12" - min="1" + inputMode="numeric" name="month" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -593,13 +593,13 @@ exports[`DateField has invalid year 1`] = ` className="ds-c-field ds-c-field--day" defaultValue={undefined} id="textfield_snapshot" - max="31" - min="1" + inputMode="numeric" name="day" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -628,13 +628,13 @@ exports[`DateField has invalid year 1`] = ` className="ds-c-field ds-c-field--year ds-c-field--error" defaultValue={undefined} id="textfield_snapshot" - max={undefined} - min={1900} + inputMode="numeric" name="year" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -687,13 +687,13 @@ exports[`DateField has requirementLabel 1`] = ` className="ds-c-field ds-c-field--month" defaultValue={undefined} id="textfield_snapshot" - max="12" - min="1" + inputMode="numeric" name="month" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -722,13 +722,13 @@ exports[`DateField has requirementLabel 1`] = ` className="ds-c-field ds-c-field--day" defaultValue={undefined} id="textfield_snapshot" - max="31" - min="1" + inputMode="numeric" name="day" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -757,13 +757,13 @@ exports[`DateField has requirementLabel 1`] = ` className="ds-c-field ds-c-field--year" defaultValue={undefined} id="textfield_snapshot" - max={undefined} - min={1900} + inputMode="numeric" name="year" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -814,13 +814,13 @@ exports[`DateField is inversed 1`] = ` className="ds-c-field ds-c-field--inverse ds-c-field--month" defaultValue={undefined} id="textfield_snapshot" - max="12" - min="1" + inputMode="numeric" name="month" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -849,13 +849,13 @@ exports[`DateField is inversed 1`] = ` className="ds-c-field ds-c-field--inverse ds-c-field--day" defaultValue={undefined} id="textfield_snapshot" - max="31" - min="1" + inputMode="numeric" name="day" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -884,13 +884,13 @@ exports[`DateField is inversed 1`] = ` className="ds-c-field ds-c-field--inverse ds-c-field--year" defaultValue={undefined} id="textfield_snapshot" - max={undefined} - min={1900} + inputMode="numeric" name="year" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -941,13 +941,13 @@ exports[`DateField renders with all defaultProps 1`] = ` className="ds-c-field ds-c-field--month" defaultValue={undefined} id="textfield_snapshot" - max="12" - min="1" + inputMode="numeric" name="month" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -976,13 +976,13 @@ exports[`DateField renders with all defaultProps 1`] = ` className="ds-c-field ds-c-field--day" defaultValue={undefined} id="textfield_snapshot" - max="31" - min="1" + inputMode="numeric" name="day" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> @@ -1011,13 +1011,13 @@ exports[`DateField renders with all defaultProps 1`] = ` className="ds-c-field ds-c-field--year" defaultValue={undefined} id="textfield_snapshot" - max={undefined} - min={1900} + inputMode="numeric" name="year" onBlur={undefined} onChange={undefined} + pattern="[0-9]*" rows={undefined} - type="number" + type="text" value={undefined} /> diff --git a/packages/core/src/components/DateField/datefield.example.html b/packages/core/src/components/DateField/datefield.example.html index 46d64bc188..d13472b00a 100644 --- a/packages/core/src/components/DateField/datefield.example.html +++ b/packages/core/src/components/DateField/datefield.example.html @@ -10,9 +10,9 @@ > @@ -22,9 +22,9 @@ Day @@ -34,9 +34,9 @@ Year diff --git a/packages/core/src/components/FormLabel/_FormLabel.docs.scss b/packages/core/src/components/FormLabel/_FormLabel.docs.scss index 8ec195297a..a93d78fee9 100644 --- a/packages/core/src/components/FormLabel/_FormLabel.docs.scss +++ b/packages/core/src/components/FormLabel/_FormLabel.docs.scss @@ -25,7 +25,7 @@ Style guide: components.form-label.react #### Labels -- Each form field should have a . Never use a field's placeholder attribute as the primary way to label the field. +- Each form field should have a ``. Never use a field's placeholder attribute as the primary way to label the field. #### Legends diff --git a/packages/core/src/components/TextField/Mask.example.jsx b/packages/core/src/components/TextField/Mask.example.jsx index 99324fb243..6be0ba08d2 100644 --- a/packages/core/src/components/TextField/Mask.example.jsx +++ b/packages/core/src/components/TextField/Mask.example.jsx @@ -56,6 +56,9 @@ const Example = () => { ariaLabel="Enter monthly income amount in dollars." label="Currency" mask="currency" + inputMode="numeric" + pattern="[0-9]*" + type="text" name="currency_example" onBlur={evt => handleBlur(evt, 'currency')} defaultValue="2500" @@ -73,6 +76,9 @@ const Example = () => { handleBlur(evt, 'ssn')} defaultValue="123456789" @@ -81,6 +87,9 @@ const Example = () => { handleBlur(evt, 'zip')} defaultValue="123456789" diff --git a/packages/core/src/components/TextField/Mask.jsx b/packages/core/src/components/TextField/Mask.jsx index b1b7dafdf2..bfa9bd91f4 100644 --- a/packages/core/src/components/TextField/Mask.jsx +++ b/packages/core/src/components/TextField/Mask.jsx @@ -2,12 +2,23 @@ import PropTypes from 'prop-types'; import React from 'react'; // Deliminate chunks of integers -const deliminatedMaskRegex = { +const maskDeliminatedRegex = { phone: /(\d{3})(\d{1,3})?(\d+)?/, ssn: /([*\d]{3})([*\d]{1,2})?([*\d]+)?/, zip: /(\d{5})(\d*)/ }; +const maskPattern = { + phone: '[0-9-]*', + ssn: '[0-9-*]*', + zip: '[0-9-]*', + currency: '[0-9.-]*' +}; + +const maskOverlayContent = { + currency: '$' +}; + /** * Split value into groups and insert a hyphen deliminator between each * @param {String} value @@ -120,10 +131,10 @@ export function maskValue(value = '', mask) { if (number !== undefined) { value = stringWithFixedDigits(number.toLocaleString('en-US')); } - } else if (deliminatedMaskRegex[mask]) { + } else if (maskDeliminatedRegex[mask]) { // Use deliminator regex to mask value and remove unwanted characters // If the regex does not match, return the numeric digits. - value = deliminateRegexGroups(value, deliminatedMaskRegex[mask]); + value = deliminateRegexGroups(value, maskDeliminatedRegex[mask]); } } @@ -224,21 +235,41 @@ export class Mask extends React.PureComponent { } render() { + const { mask } = this.props; const field = this.field(); - return React.cloneElement(field, { + const modifiedTextField = React.cloneElement(field, { defaultValue: undefined, onBlur: evt => this.handleBlur(evt, field), onChange: evt => this.handleChange(evt, field), - value: this.state.value + value: this.state.value, + type: 'text', + inputMode: 'numeric', + pattern: maskPattern[this.props.mask] }); + + // UI overlayed on top of a field to support certain masks + const maskOverlay = maskOverlayContent[mask] ? ( + + {maskOverlayContent[mask]} + + ) : null; + + return ( + + {maskOverlay} + {modifiedTextField} + + ); } } Mask.propTypes = { - /** Pass the input as the child */ + /** + * Must contain a `TextField` component + */ children: PropTypes.node.isRequired, - mask: PropTypes.string.isRequired + mask: PropTypes.oneOf(['currency', 'phone', 'ssn', 'zip']) }; /** @@ -255,7 +286,7 @@ export function unmaskValue(value, mask) { if (matches) { value = matches.join(''); } - } else if (deliminatedMaskRegex[mask]) { + } else if (maskDeliminatedRegex[mask]) { // Remove the deliminators and revert to single ungrouped string value = toDigitsAndAsterisks(value); } diff --git a/packages/core/src/components/TextField/Mask.test.jsx b/packages/core/src/components/TextField/Mask.test.jsx index a8dd7c3ecb..fcbe993c57 100644 --- a/packages/core/src/components/TextField/Mask.test.jsx +++ b/packages/core/src/components/TextField/Mask.test.jsx @@ -3,7 +3,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; // Some tests are generated. When a new mask is added, add it here: -const masks = ['currency', 'ssn', 'zip']; +const masks = ['currency', 'ssn', 'zip', 'phone']; function render(customProps = {}, inputProps = {}, deep = false) { const component = ( @@ -44,11 +44,28 @@ describe('Mask', function() { }); }); + it('renders mask', () => { + const data = render({ + mask: 'ssn' + }); + + expect(data.wrapper).toMatchSnapshot(); + }); + + it('renders mask overlay', () => { + const data = render({ + mask: 'currency' + }); + + expect(data.wrapper).toMatchSnapshot(); + }); + it('calls onBlur when the value is the same', () => { const onBlur = jest.fn(); const wrapper = render({ mask: 'currency' }, { value: '123', onBlur: onBlur }).wrapper; + const input = wrapper.find('input'); - wrapper.simulate('blur', { target: { value: '123' }, persist: jest.fn() }); + input.simulate('blur', { target: { value: '123' }, persist: jest.fn() }); expect(onBlur.mock.calls.length).toBe(1); }); @@ -69,8 +86,9 @@ describe('Mask', function() { it('calls onChange', () => { const onChange = jest.fn(); const wrapper = render({ mask: 'currency' }, { value: '123', onChange: onChange }).wrapper; + const input = wrapper.find('input'); - wrapper.simulate('change', { target: { value: '123' } }); + input.simulate('change', { target: { value: '123' } }); expect(onChange.mock.calls.length).toBe(1); }); diff --git a/packages/core/src/components/TextField/TextField.example.jsx b/packages/core/src/components/TextField/TextField.example.jsx index edfb277db8..ffe0752a12 100644 --- a/packages/core/src/components/TextField/TextField.example.jsx +++ b/packages/core/src/components/TextField/TextField.example.jsx @@ -11,7 +11,7 @@ ReactDOM.render( name="single_example" requirementLabel="Optional" /> - + , use 'inputRef' instead. This prop has been renamed and will be removed in a future release.` ); } + if (props.type === 'number') { + console.warn( + `Please use the 'numeric' prop instead of 'type="number"' unless your user research suggests otherwise.` + ); + } } } @@ -42,33 +47,7 @@ export class TextField extends React.PureComponent { * markup if a mask is present */ renderFieldAndMask(field) { - const maskName = this.props.mask; - - return maskName ? ( - - {this.renderMaskOverlay()} - {field} - - ) : ( - field - ); - } - - /** - * UI overlayed on top of a field to support certain masks - */ - renderMaskOverlay() { - if (this.props.mask) { - const content = { - currency: '$' - }; - - return ( - - {content[this.props.mask]} - - ); - } + return this.props.mask ? {field} : field; } render() { @@ -88,10 +67,12 @@ export class TextField extends React.PureComponent { labelId, mask, multiline, + numeric, requirementLabel, rows, size, type, + pattern, ...fieldProps } = this.props; const FieldComponent = multiline ? 'textarea' : 'input'; @@ -113,6 +94,13 @@ export class TextField extends React.PureComponent { size && `ds-c-field--${size}` ); + let inputType = type; + if (numeric) { + inputType = 'text'; + } else if (multiline) { + inputType = undefined; + } + const field = ( ); @@ -151,7 +141,6 @@ export class TextField extends React.PureComponent { > {label} - {this.renderFieldAndMask(field, mask)} ); @@ -234,8 +223,16 @@ TextField.propTypes = { */ multiline: PropTypes.bool, name: PropTypes.string.isRequired, + /** + * Sets `inputMode`, `type`, and `pattern` to improve accessiblity and consistency for number fields. Use this prop instead of `type="number"`, see [here](https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/) for more information. + */ + numeric: PropTypes.bool, onBlur: PropTypes.func, onChange: PropTypes.func, + /** + * @hide-prop HTML `input` [pattern](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefpattern). + */ + pattern: PropTypes.string, /** * Optionally specify the number of visible text lines for the field. Only * applicable if this is a multiline field. @@ -246,7 +243,7 @@ TextField.propTypes = { */ size: PropTypes.oneOf(['small', 'medium']), /** - * Any valid `input` [type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). + * HTML `input` [type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#_types) attribute. If you are using `type=number` please use the numeric prop instead. */ type: PropTypes.string, /** diff --git a/packages/core/src/components/TextField/TextField.scss b/packages/core/src/components/TextField/TextField.scss index 58b044eb32..a570df0dc6 100644 --- a/packages/core/src/components/TextField/TextField.scss +++ b/packages/core/src/components/TextField/TextField.scss @@ -24,16 +24,6 @@ outline: 3px solid $focus-color; outline-offset: 0; } - - &[type='number'] { - appearance: textfield; - - &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { - appearance: none; - margin: 0; - } - } } .ds-c-field--small { diff --git a/packages/core/src/components/TextField/TextField.test.jsx b/packages/core/src/components/TextField/TextField.test.jsx index 36b8d51c53..752f6f9f0d 100644 --- a/packages/core/src/components/TextField/TextField.test.jsx +++ b/packages/core/src/components/TextField/TextField.test.jsx @@ -285,6 +285,12 @@ describe('TextField', function() { expect(typeof unmaskValue).toBe('function'); }); + it('renders TextField', () => { + const data = render(); + + expect(data.wrapper).toMatchSnapshot(); + }); + it('renders currency mask', () => { const data = render({ mask: 'currency' diff --git a/packages/core/src/components/TextField/__snapshots__/Mask.test.jsx.snap b/packages/core/src/components/TextField/__snapshots__/Mask.test.jsx.snap new file mode 100644 index 0000000000..c58d5b9bb9 --- /dev/null +++ b/packages/core/src/components/TextField/__snapshots__/Mask.test.jsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Mask renders mask 1`] = ` + + + +`; + +exports[`Mask renders mask overlay 1`] = ` + + + $ + + + +`; diff --git a/packages/core/src/components/TextField/__snapshots__/TextField.test.jsx.snap b/packages/core/src/components/TextField/__snapshots__/TextField.test.jsx.snap index 99780d60c9..54f102d7ee 100644 --- a/packages/core/src/components/TextField/__snapshots__/TextField.test.jsx.snap +++ b/packages/core/src/components/TextField/__snapshots__/TextField.test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TextField masks renders currency mask 1`] = ` +exports[`TextField masks renders TextField 1`] = ` @@ -11,25 +11,36 @@ exports[`TextField masks renders currency mask 1`] = ` > Foo - + +`; + +exports[`TextField masks renders currency mask 1`] = ` + + + Foo + + - - $ - - - - - + + `; diff --git a/packages/core/src/components/TextField/textfield.example.html b/packages/core/src/components/TextField/textfield.example.html index cceff46676..096454fa72 100644 --- a/packages/core/src/components/TextField/textfield.example.html +++ b/packages/core/src/components/TextField/textfield.example.html @@ -11,7 +11,15 @@ Number field - + Small size modifier Medium size modifier