diff --git a/.eslintrc b/.eslintrc index 587a4b9f..bc26b3e9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,7 +3,11 @@ "extends": [ "airbnb", "plugin:flowtype/recommended", - "plugin:react/recommended" + "plugin:react/recommended", + "prettier", + "prettier/babel", + "prettier/flowtype", + "prettier/react" ], "env": { "browser": true, @@ -31,16 +35,21 @@ }, "allowChildren": true }], + "lines-between-class-members": "off", + "react/destructuring-assignment": "off", "react/no-did-mount-set-state": "off", "react/jsx-no-bind": "off", "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }], "react/jsx-closing-bracket-location": 1, + "react/jsx-props-no-spreading": "off", "react/prefer-stateless-function": "off", "react/no-unused-prop-types": "off", "react/prop-types": 0, "react/require-default-props": "off", "react/sort-comp": 0, "react/display-name": ["off", { "ignoreTranspilerName": false }], + "react/default-props-match-prop-types": ["error", { "allowRequiredDefaults": true }], + "react/static-property-placement": 0, "class-methods-use-this": 0, "no-duplicate-imports": 0, "no-param-reassign": 0, diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..dd53e28c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,11 @@ +name: test +on: [push] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: install modules + run: yarn + - name: run tests + run: yarn test diff --git a/.gitignore b/.gitignore index 4abfade2..92b003a3 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ jspm_packages # Editors .idea + +# OSX +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 83115d1c..d1d82b32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,47 @@ The history of all changes to react-polymorph. vNext ===== +### Breaking Changes :boom: + +- `Input` now shows errors on hover and focus by default which can be configured via two new props: + +```ts + isShowingErrorOnFocus: boolean, + isShowingErrorOnHover: boolean, +``` + +### Features :sparkles: + +- `Input` now accepts a new prop `themeVariables` which can be used to override css variables ([PR 173](https://github.com/input-output-hk/react-polymorph/pull/173)) +- Implemented the search functionality to the Options component ([PR 165](https://github.com/input-output-hk/daedalus/pull/165)) +- Improved PIN entry component UX ([PR 166](https://github.com/input-output-hk/react-polymorph/pull/166)) +- Enabled pasting of multiple words into Autocomplete ([PR 163](https://github.com/input-output-hk/react-polymorph/pull/163)) + +### Fixes :muscle: + +- Fixed Select search issues([PR 179](https://github.com/input-output-hk/react-polymorph/pull/179)) +- Fixed Select Search styles and minor code issues ([PR 175](https://github.com/input-output-hk/react-polymorph/pull/175)) +- Fixed a wrong variable name for the select search highlight color ([PR 170](https://github.com/input-output-hk/react-polymorph/pull/170)) +- Fixed an issue related to Numeric Input when entering numbers after having selected the decimal separator ([PR 167](https://github.com/input-output-hk/react-polymorph/pull/167)) +- Fixed issues related to controlled/uncontrolled Tippy state ([PR 160](https://github.com/input-output-hk/react-polymorph/pull/160)) +- Fixed `NumericInput` to support DEBUG mode ([PR 159](https://github.com/input-output-hk/react-polymorph/pull/159)) + +0.9.7 +===== + +### Features + +- Improve `NumericInput` component to support big numbers ([PR 152](https://github.com/input-output-hk/react-polymorph/pull/152)) +- Improve autocomplete required selections ux ([PR 154](https://github.com/input-output-hk/react-polymorph/pull/154)) +- Improve `FormField` UX by using pop overs to show errors ([PR 151](https://github.com/input-output-hk/react-polymorph/pull/151) + +0.9.6 +===== + +### Features + +- Added `PopOver` component (aka Smart Tooltips) ([PR 150](https://github.com/input-output-hk/react-polymorph/pull/150) + 0.9.5 ===== diff --git a/README.md b/README.md index e9b793ef..e932ae48 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,56 @@ const SimpleFormApp = () => ( ); ``` -### Components and Skins +## Release Managament + +- Starting with `1.0.0` all future releases will follow semver semantics: + - `patch` (eg: 1.0.x) for **API compatible bug fixes** + - `minor` (eg.: 1.x.0) for **API compatible new features** + - `major` (eg: 2.0.0) for **API breaking changes** + +- For early integration of upcoming release changes we use the following conventions: + + - `[current version]-next.x` to tag changes for upcoming releases (as we cannot know the necessary + semver for the final release including all the changes). `x` in this case is simply a number that + is increased and can be thought of like "slots" for temporary releases + + - All temporary releases should be published with the `next` npm dist tag via: `npm publish --tag next` + so that they are not automatically tagged with the default `latest` npm tag. + +- The `master` branch only includes commits of final releases + +- `release/x.x.x` branches are created as soon as we cut a release and know the correct semver - they + are always targeting the `master` branch + should be well documented. They can include many release + candidates which should be tagged like `[next releaes]-rc.X` where you increment X per release candidate + until we are confident that the release is ready to be published under its normal version. + +### How to publish a temporary release + +Temporary releases are useful for testing specific changes in your project PRs without making public +releases that might confuse others and are not following semver. + +1. Create a dedicated branch for your bug/feature/chore +2. Run `npm view react-polymorph dist-tags.next` to see the latest release version the `next` npm dist-tag is currently pointing to + (this will look something like this: `1.0.0-next.1`) +3. Increase the `next.X` number by one (e.g: `npm version 1.0.0-next.2`) to create a new git tag via. +4. Publish the release candidate with `npm publish --tag next` (to assign the `next` dist-tag instead of `latest`) +5. Reference your release candidate version in your project PR + +### How to publish a stable release + +Stable releases are the next public version change of react-polymorph combining all previous temporary +releases into a semver based release: + +1. Create a new `release/x.x.x` branch based on `develop` (following semver based on changelog) +2. Update the version in `package.json` to the planned release version (do not tag it) +3. Update the `CHANGELOG.md` to assign the new release version to the last changes and upcoming changes +3. Setup a PR targetting `master` for the relase branch on Github and document the changes since last release +4. Publish a release candidate to npm (e.g: `1.0.1-rc.1`) +5. Integrate and test the release candidate +6. Iterate on the release via release candidates until its ready to be merged +7. Merge the release PR into `master` on Github and then `master` back into `develop` + +## Components and Skins React-polymorph comes with simple themes & skins out of the box, but anything is customizable. @@ -166,51 +215,39 @@ import { NumericInput } from "react-polymorph/lib/components"; import { InputSkin } from "react-polymorph/lib/skins/simple"; const MyNumericInput = () => ( - ); ``` - -_Side Note: this shows how you can make/use specialized versions of basic components by composition -(reusing the `InputSkin` with a specialized logic component) - a core idea of react-polymorph!_ - -##### Expected Behavior & Limitations: +##### Expected Behavior: Since there is no web standard on how to build numeric input components, here is the specification we came up with that serves our purposes in the best way: -- Only numeric inputs that are representable by Javascript numbers are valid. This is guarded by `Number.MIN_SAFE_INTEGER` - (-9007199254740991) and `Number.MAX_SAFE_INTEGER` (9007199254740991) but since also fractions need to - represented, the calculation for the maximum integer part goes like this: - `Number.MAX_SAFE_INTEGER / 10 ** (maximumFractionDigits + 1)` (which basically means that one integer digit is lost for - each supported fraction digit). For `maximumFractionDigits == 3` this results in - `9007199254740991 / 10 ** 4 == 900719925474.099` being the biggest number that can be entered. -- Only numeric digits `[0-9]` and dots `.` can be entered. +- Only numeric digits `[0-9]` and decimal separators (configurable via `bigNumberFormat` prop) can be entered. - When invalid characters are pasted as input, nothing happens -- When a second dot is entered it replaces the existing one and updates the fraction part accordingly -- Commas cannot be deleted but the cursor should jump over them when DEL or BACKSPACE keys are used -- The fraction dot can only be deleted if `minimumFractionDigits` is not defined or - if the resulting number does not exceed the numeric limits! -- It's possible to replace the whole number or parts of it (even the dot) by inserting another number. -- If the fraction dot is deleted but the resulting number is too big the cursor jumps over the dot without deletion -- If you insert a digit but the resulting number would exceed the numeric limit, nothing happens +- When a second decimal separators is entered it replaces the existing one and updates the fraction part accordingly +- Group separators cannot be deleted but the cursor should jump over them when DEL or BACKSPACE keys are used +- It's possible to replace the whole number or parts of it (even the decimal separator) by inserting another number. ##### Props: +The `NumericInput` is based on the `Input` component and extends it's functionality: + ```js type NumericInputProps = { + // Input props: autoFocus?: boolean, className?: string, context: ThemeContextProp, disabled?: boolean, error?: string, label?: string | Element, - numberLocaleOptions?: Number$LocaleOptions, onBlur?: Function, onChange?: Function, onFocus?: Function, @@ -220,26 +257,36 @@ type NumericInputProps = { theme: ?Object, themeId: string, themeOverrides: Object, - useDynamicDigitCalculation: boolean, - value: ?number, + // Numeric input specific props: + allowSigns?: boolean, + bigNumberFormat?: BigNumber.Format, + decimalPlaces?: number, + roundingMode?: BigNumber.RoundingMode, + value: ?BigNumber.Instance, }; ``` -###### `numberLocaleOptions` +###### `value` + +Must be an instance of [BigNumber](https://mikemcl.github.io/bignumber.js) +`onChange` also returns an instance of `BigNumber` after any user changes. + +###### `allowSigns` + +Is `true` by default, if `false` the user cannot enter negative numbers. + +###### `decimalPlaces` + +No restriction by default (any number of decimal places allowed). +Can be set to fix the decimal places to a specific amount. -`Number.toLocaleString()` is used internally to localize the given number value. This method takes options -explained in greater detail in the -[MDN web docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString) +###### `bigNumberFormat` -The most important parts are `maximumFractionDigits` (defaults to 3 as per web standard) and `minimumFractionDigits` -which dictate the handling of fraction digits. +You can configure the number format by passing in any valid [bignumber.js FORMAT option](https://mikemcl.github.io/bignumber.js/#format) -###### `useDynamicDigitCalculation` +###### `roundingMode` -This is an optional mode that "sacrifices" simple, clear UX in favor of being able to enter bigger numbers. -Basically it works the same way but it dynamically calculates how large the integer part of the number can -be based on the actual fraction digits entered. The less fraction digits, the more integer digits are possible -and vice versa. +You can configure the rounding mode by passing in any valid [bignumber.js ROUNDING_MODE option](https://mikemcl.github.io/bignumber.js/#rounding-mode) --- diff --git a/__tests__/FormField.test.js b/__tests__/FormField.test.js index 16abbbff..fa24bf95 100644 --- a/__tests__/FormField.test.js +++ b/__tests__/FormField.test.js @@ -6,42 +6,23 @@ import { renderInSimpleTheme } from './helpers/theming'; const renderFormField = () =>
; test('FormField renders correctly', () => { - expect(renderInSimpleTheme( - - )).toMatchSnapshot(); + expect( + renderInSimpleTheme() + ).toMatchSnapshot(); }); test('FormField renders with label', () => { - expect(renderInSimpleTheme( - - )).toMatchSnapshot(); -}); - -test('FormField renders with an error', () => { - expect(renderInSimpleTheme( - - )).toMatchSnapshot(); -}); - -test('FormField is disabled', () => { - expect(renderInSimpleTheme( - {disabled.toString()}} - /> - )).toMatchSnapshot(); + expect( + renderInSimpleTheme( + + ) + ).toMatchSnapshot(); }); test('FormField renders an input element', () => { - expect(renderInSimpleTheme( - } - /> - )).toMatchSnapshot(); + expect( + renderInSimpleTheme( + } /> + ) + ).toMatchSnapshot(); }); diff --git a/__tests__/NumericInput.behavior.test.js b/__tests__/NumericInput.behavior.test.js index a606f973..31af0336 100644 --- a/__tests__/NumericInput.behavior.test.js +++ b/__tests__/NumericInput.behavior.test.js @@ -1,13 +1,15 @@ +import BigNumber from 'bignumber.js'; import React from 'react'; import { NumericInput } from '../source/components/NumericInput'; import { mountInSimpleTheme } from './helpers/theming'; describe('NumericInput onChange simulations', () => { - const mountNumericInputWithProps = (props) => { const onChangeMock = jest.fn(); - const wrapper = mountInSimpleTheme(); + const wrapper = mountInSimpleTheme( + + ); const input = wrapper.find('input'); return { input, @@ -20,7 +22,8 @@ describe('NumericInput onChange simulations', () => { test('valid input triggers onChange listener', () => { const { input, onChangeMock } = mountNumericInputWithProps(); input.simulate('change', { nativeEvent: { target: { value: '19.00' } } }); - expect(onChangeMock.mock.calls[0][0]).toBe(19.00); + const onChangeValue = onChangeMock.mock.calls[0][0]; + expect(onChangeValue).toEqual('19'); }); test('invalid input does not trigger onChange listener', () => { const { input, onChangeMock } = mountNumericInputWithProps(); @@ -30,47 +33,37 @@ describe('NumericInput onChange simulations', () => { }); describe('configurable number formats', () => { - test('handles commas as thousand separators by default', () => { + test('handles commas as group separators by default', () => { const { input, onChangeMock } = mountNumericInputWithProps(); - input.simulate('change', { nativeEvent: { target: { value: '9,999,999.00' } } }); - expect(onChangeMock.mock.calls[0][0]).toBe(9999999.00); + input.simulate('change', { + nativeEvent: { target: { value: '9,999,999.00' } }, + }); + const onChangeValue = onChangeMock.mock.calls[0][0]; + expect(onChangeValue).toBe('9999999'); }); test('can be configured to handle dots as thousand separators', () => { const { input, onChangeMock } = mountNumericInputWithProps({ - numberFormat: { + bigNumberFormat: { groupSeparator: '.', decimalSeparator: ',', - } + }, + }); + input.simulate('change', { + nativeEvent: { target: { value: '9.999.999,00' } }, }); - input.simulate('change', { nativeEvent: { target: { value: '9.999.999,00' } } }); - expect(onChangeMock.mock.calls[0][0]).toBe(9999999.00); + expect(onChangeMock.mock.calls[0][0]).toBe('9999999'); }); test('can be configured to handle spaces as thousand separators', () => { const { input, onChangeMock } = mountNumericInputWithProps({ - numberFormat: { + bigNumberFormat: { groupSeparator: ' ', decimalSeparator: '.', - } + }, }); - input.simulate('change', { nativeEvent: { target: { value: '9 999 999.00' } } }); - expect(onChangeMock.mock.calls[0][0]).toBe(9999999.00); - }); - }); - - test('enforces given minimumFractionDigits', () => { - const { input } = mountNumericInputWithProps({ - numberLocaleOptions: { minimumFractionDigits: 6 }, - value: 0, - }); - expect(input.getDOMNode().value).toBe('0.000000'); - }); - - test('enforces given maximumFractionDigits', () => { - const { input } = mountNumericInputWithProps({ - numberLocaleOptions: { maximumFractionDigits: 2 }, - value: 0.123, + input.simulate('change', { + nativeEvent: { target: { value: '9 999 999.00' } }, + }); + expect(onChangeMock.mock.calls[0][0]).toBe('9999999'); }); - expect(input.getDOMNode().value).toBe('0.12'); }); - }); diff --git a/__tests__/NumericInput.test.js b/__tests__/NumericInput.test.js index ef835c0c..b05f2c06 100644 --- a/__tests__/NumericInput.test.js +++ b/__tests__/NumericInput.test.js @@ -1,40 +1,35 @@ +import BigNumber from 'bignumber.js'; import React from 'react'; import { NumericInput } from '../source/components/NumericInput'; import { renderInSimpleTheme } from './helpers/theming'; test('NumericInput renders correctly', () => { - expect(renderInSimpleTheme( - - )).toMatchSnapshot(); + expect(renderInSimpleTheme()).toMatchSnapshot(); }); test('NumericInput renders with placeholder', () => { - expect(renderInSimpleTheme( - - )).toMatchSnapshot(); + expect( + renderInSimpleTheme() + ).toMatchSnapshot(); }); test('NumericInput is disabled', () => { - expect(renderInSimpleTheme( - - )).toMatchSnapshot(); + expect(renderInSimpleTheme()).toMatchSnapshot(); }); test('NumericInput is readOnly', () => { - expect(renderInSimpleTheme( - - )).toMatchSnapshot(); + expect(renderInSimpleTheme()).toMatchSnapshot(); }); test('NumericInput renders with an error', () => { - expect(renderInSimpleTheme( - - )).toMatchSnapshot(); + expect( + renderInSimpleTheme() + ).toMatchSnapshot(); }); test('NumericInput renders with a value', () => { - expect(renderInSimpleTheme( - - )).toMatchSnapshot(); + expect( + renderInSimpleTheme() + ).toMatchSnapshot(); }); diff --git a/__tests__/TextArea.test.js b/__tests__/TextArea.test.js index d2dee8d5..3e1760da 100644 --- a/__tests__/TextArea.test.js +++ b/__tests__/TextArea.test.js @@ -4,31 +4,21 @@ import { TextArea } from '../source/components/TextArea'; import { renderInSimpleTheme } from './helpers/theming'; test('TextArea renders correctly', () => { - expect(renderInSimpleTheme( - -
- -`; - -exports[`TextArea renders with an error 1`] = ` -
-
- Please enter valid input -
-
- +
+
`; @@ -68,13 +55,15 @@ exports[`TextArea renders with placeholder 1`] = `
-
-