From 90b16fc8cb9b2e66fb72ea2a20091e1fe7c399c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yasin=20=C3=87al=C4=B1=C5=9Fkan?= Date: Wed, 20 Oct 2021 16:00:12 +0300 Subject: [PATCH 1/3] fix(number-input): Turn off formatting for Android devices --- src/core/utils/device/deviceUtils.ts | 7 +++++++ src/core/utils/number/numberUtils.ts | 29 +++++++++++++--------------- src/form/input/Input.tsx | 10 ++++------ src/form/input/util/inputTypes.ts | 5 +++++ src/form/input/util/inputUtils.ts | 16 +++++++++++++++ 5 files changed, 45 insertions(+), 22 deletions(-) create mode 100644 src/core/utils/device/deviceUtils.ts create mode 100644 src/form/input/util/inputTypes.ts create mode 100644 src/form/input/util/inputUtils.ts diff --git a/src/core/utils/device/deviceUtils.ts b/src/core/utils/device/deviceUtils.ts new file mode 100644 index 00000000..0212f454 --- /dev/null +++ b/src/core/utils/device/deviceUtils.ts @@ -0,0 +1,7 @@ +function isAndroid() { + const userAgent = navigator.userAgent || navigator.vendor; + + return /android/i.test(userAgent); +} + +export {isAndroid}; diff --git a/src/core/utils/number/numberUtils.ts b/src/core/utils/number/numberUtils.ts index c52d0aa9..5d798bf8 100644 --- a/src/core/utils/number/numberUtils.ts +++ b/src/core/utils/number/numberUtils.ts @@ -1,3 +1,4 @@ +import {isAndroid} from "../device/deviceUtils"; import {FormatNumberOptions, ParseNumberOptions} from "./numberTypes"; function formatNumber(formatNumberOptions: FormatNumberOptions) { @@ -36,21 +37,20 @@ function formatNumber(formatNumberOptions: FormatNumberOptions) { } /** - * Coerces a number scientific notation. {@link https://observablehq.com/@mbostock/localized-number-parsing | Reference} + * Coerces a number to scientific notation. {@link https://observablehq.com/@mbostock/localized-number-parsing | Reference} * @param {object} options - An object includes parse number options * @param {string} options.locale - Default locale used is browser locale * @param {number} options.maximumFractionDigits - The maximum digit a decimal can have * @param {number} value - A number to convert to string * @returns {string} The value after coercing the given value to a scientific notation. */ -function parseNumber( - options: ParseNumberOptions = {locale: navigator.language, maximumFractionDigits: 0}, - value: string -) { - const {THOUSANDTHS_SEPARATOR, DECIMAL_NUMBER_SEPARATOR} = getNumberSeparators( - options.locale - ); - const numerals = getLocaleNumerals(options.locale); +function parseNumber(options: ParseNumberOptions, value: string) { + const { + locale = isAndroid() ? "en-US" : navigator.language, + maximumFractionDigits = 0 + } = options; + const {THOUSANDTHS_SEPARATOR, DECIMAL_NUMBER_SEPARATOR} = getNumberSeparators(locale); + const numerals = getLocaleNumerals(locale); const numeral = new RegExp(`[${numerals.join("")}]`, "g"); const digitMapper = getDigit(new Map(numerals.map((d, i) => [d, i]))); let parsedNumber = value @@ -59,12 +59,12 @@ function parseNumber( .replace(new RegExp(`[${DECIMAL_NUMBER_SEPARATOR}]`), ".") .replace(numeral, digitMapper); - if (typeof options.maximumFractionDigits === "number") { + if (typeof maximumFractionDigits === "number") { const [integerPart, decimalPart] = parsedNumber.split("."); - if (options.maximumFractionDigits === 0) { + if (maximumFractionDigits === 0) { parsedNumber = integerPart; - } else if (decimalPart && decimalPart.length === options.maximumFractionDigits + 1) { + } else if (decimalPart && decimalPart.length === maximumFractionDigits + 1) { return parsedNumber.slice(0, parsedNumber.length - 1); } } else { @@ -86,10 +86,7 @@ function mapDigitsToLocalVersion( {locale = navigator.language}: {locale?: string}, digits: string ) { - return digits - .split("") - .map(mapDigitToLocalVersion({locale})) - .join(""); + return digits.split("").map(mapDigitToLocalVersion({locale})).join(""); } function mapDigitToLocalVersion({locale = navigator.language}: {locale?: string}) { diff --git a/src/form/input/Input.tsx b/src/form/input/Input.tsx index cf10e5e5..072f5752 100644 --- a/src/form/input/Input.tsx +++ b/src/form/input/Input.tsx @@ -12,6 +12,8 @@ import { getNegativeZero, getThousandthSeparatorCount } from "../../core/utils/number/numberUtils"; +import {getLocalizationOptions} from "./util/inputUtils"; +import {InputLocalizationOptions} from "./util/inputTypes"; export type InputTypes = | "checkbox" @@ -50,11 +52,7 @@ export type InputProps = Omit< hasError?: boolean; customClassName?: string; onChange: React.ReactEventHandler; - localizationOptions?: { - shouldFormatToLocaleString?: boolean; - locale?: string; - maximumFractionDigits?: number; - }; + localizationOptions?: InputLocalizationOptions; }; const Input = React.forwardRef( @@ -80,7 +78,7 @@ const Input = React.forwardRef( shouldFormatToLocaleString = false, locale, maximumFractionDigits = 0 - } = localizationOptions; + } = getLocalizationOptions(localizationOptions); const [ { DECIMAL_NUMBER_SEPARATOR: decimalSeparatorForLocale, diff --git a/src/form/input/util/inputTypes.ts b/src/form/input/util/inputTypes.ts new file mode 100644 index 00000000..d04de7fe --- /dev/null +++ b/src/form/input/util/inputTypes.ts @@ -0,0 +1,5 @@ +export type InputLocalizationOptions = { + shouldFormatToLocaleString?: boolean; + locale?: string; + maximumFractionDigits?: number; +}; diff --git a/src/form/input/util/inputUtils.ts b/src/form/input/util/inputUtils.ts new file mode 100644 index 00000000..18013bff --- /dev/null +++ b/src/form/input/util/inputUtils.ts @@ -0,0 +1,16 @@ +import {isAndroid} from "../../../core/utils/device/deviceUtils"; +import {InputLocalizationOptions} from "./inputTypes"; + +function getLocalizationOptions( + localizationOptions: InputLocalizationOptions +): InputLocalizationOptions { + return isAndroid() + ? { + shouldFormatToLocaleString: false, + locale: undefined, + maximumFractionDigits: localizationOptions.maximumFractionDigits + } + : localizationOptions; +} + +export {getLocalizationOptions}; From a9099bd34175f94bb1f99067a24869331a3f900e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yasin=20=C3=87al=C4=B1=C5=9Fkan?= Date: Fri, 22 Oct 2021 17:59:21 +0300 Subject: [PATCH 2/3] feat(number-imput): Turn off formatting for iOS too --- src/core/utils/device/deviceUtils.ts | 7 ++++--- src/core/utils/number/numberUtils.ts | 4 ++-- src/form/input/util/inputUtils.ts | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/core/utils/device/deviceUtils.ts b/src/core/utils/device/deviceUtils.ts index 0212f454..6059b044 100644 --- a/src/core/utils/device/deviceUtils.ts +++ b/src/core/utils/device/deviceUtils.ts @@ -1,7 +1,8 @@ -function isAndroid() { +function isMobileDevice() { const userAgent = navigator.userAgent || navigator.vendor; + const devices = [/Android/i, /iPhone/i, /iPad/i]; - return /android/i.test(userAgent); + return devices.some((device) => userAgent.match(device)); } -export {isAndroid}; +export {isMobileDevice}; diff --git a/src/core/utils/number/numberUtils.ts b/src/core/utils/number/numberUtils.ts index 5d798bf8..14119efb 100644 --- a/src/core/utils/number/numberUtils.ts +++ b/src/core/utils/number/numberUtils.ts @@ -1,4 +1,4 @@ -import {isAndroid} from "../device/deviceUtils"; +import {isMobileDevice} from "../device/deviceUtils"; import {FormatNumberOptions, ParseNumberOptions} from "./numberTypes"; function formatNumber(formatNumberOptions: FormatNumberOptions) { @@ -46,7 +46,7 @@ function formatNumber(formatNumberOptions: FormatNumberOptions) { */ function parseNumber(options: ParseNumberOptions, value: string) { const { - locale = isAndroid() ? "en-US" : navigator.language, + locale = isMobileDevice() ? "en-US" : navigator.language, maximumFractionDigits = 0 } = options; const {THOUSANDTHS_SEPARATOR, DECIMAL_NUMBER_SEPARATOR} = getNumberSeparators(locale); diff --git a/src/form/input/util/inputUtils.ts b/src/form/input/util/inputUtils.ts index 18013bff..1cd05e2c 100644 --- a/src/form/input/util/inputUtils.ts +++ b/src/form/input/util/inputUtils.ts @@ -1,10 +1,10 @@ -import {isAndroid} from "../../../core/utils/device/deviceUtils"; +import {isMobileDevice} from "../../../core/utils/device/deviceUtils"; import {InputLocalizationOptions} from "./inputTypes"; function getLocalizationOptions( localizationOptions: InputLocalizationOptions ): InputLocalizationOptions { - return isAndroid() + return isMobileDevice() ? { shouldFormatToLocaleString: false, locale: undefined, From 0f4fa9a436b0061abf1021cbe002e27f8378f8de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yasin=20=C3=87al=C4=B1=C5=9Fkan?= Date: Tue, 26 Oct 2021 10:22:10 +0300 Subject: [PATCH 3/3] chore(input): Move input types to related module --- src/core/utils/number/numberUtils.ts | 6 +-- src/form/input/Input.tsx | 48 ++------------------- src/form/input/input.test.tsx | 3 +- src/form/input/typeahead/TypeaheadInput.tsx | 3 +- src/form/input/util/inputTypes.ts | 40 +++++++++++++++++ src/form/input/util/inputUtils.ts | 14 +++++- src/form/password-input/PasswordInput.tsx | 3 +- src/index.ts | 3 +- 8 files changed, 65 insertions(+), 55 deletions(-) diff --git a/src/core/utils/number/numberUtils.ts b/src/core/utils/number/numberUtils.ts index 14119efb..a6991a8a 100644 --- a/src/core/utils/number/numberUtils.ts +++ b/src/core/utils/number/numberUtils.ts @@ -1,4 +1,3 @@ -import {isMobileDevice} from "../device/deviceUtils"; import {FormatNumberOptions, ParseNumberOptions} from "./numberTypes"; function formatNumber(formatNumberOptions: FormatNumberOptions) { @@ -45,10 +44,7 @@ function formatNumber(formatNumberOptions: FormatNumberOptions) { * @returns {string} The value after coercing the given value to a scientific notation. */ function parseNumber(options: ParseNumberOptions, value: string) { - const { - locale = isMobileDevice() ? "en-US" : navigator.language, - maximumFractionDigits = 0 - } = options; + const {locale = navigator.language, maximumFractionDigits = 0} = options; const {THOUSANDTHS_SEPARATOR, DECIMAL_NUMBER_SEPARATOR} = getNumberSeparators(locale); const numerals = getLocaleNumerals(locale); const numeral = new RegExp(`[${numerals.join("")}]`, "g"); diff --git a/src/form/input/Input.tsx b/src/form/input/Input.tsx index 072f5752..aeed308c 100644 --- a/src/form/input/Input.tsx +++ b/src/form/input/Input.tsx @@ -12,48 +12,8 @@ import { getNegativeZero, getThousandthSeparatorCount } from "../../core/utils/number/numberUtils"; -import {getLocalizationOptions} from "./util/inputUtils"; -import {InputLocalizationOptions} from "./util/inputTypes"; - -export type InputTypes = - | "checkbox" - | "button" - | "color" - | "date" - | "datetime-local" - | "email" - | "file" - | "hidden" - | "image" - | "month" - | "number" - | "password" - | "radio" - | "range" - | "reset" - | "search" - | "submit" - | "tel" - | "text" - | "time" - | "url" - | "week"; - -export type InputProps = Omit< - React.InputHTMLAttributes, - "disabled" | "name" | "className" -> & { - name: string; - type?: InputTypes; - testid?: string; - leftIcon?: React.ReactNode; - rightIcon?: React.ReactNode; - isDisabled?: boolean; - hasError?: boolean; - customClassName?: string; - onChange: React.ReactEventHandler; - localizationOptions?: InputLocalizationOptions; -}; +import {getInputLocalizationOptions, getInputParseNumberOptions} from "./util/inputUtils"; +import {InputProps} from "./util/inputTypes"; const Input = React.forwardRef( /* eslint-disable complexity */ @@ -78,7 +38,7 @@ const Input = React.forwardRef( shouldFormatToLocaleString = false, locale, maximumFractionDigits = 0 - } = getLocalizationOptions(localizationOptions); + } = getInputLocalizationOptions(localizationOptions); const [ { DECIMAL_NUMBER_SEPARATOR: decimalSeparatorForLocale, @@ -179,7 +139,7 @@ const Input = React.forwardRef( if (newValue) { const formattedNewValue = parseNumber( - {locale, maximumFractionDigits}, + getInputParseNumberOptions({locale, maximumFractionDigits}), newValue ); // Number("-") returns NaN. Should allow minus sign as first character. diff --git a/src/form/input/input.test.tsx b/src/form/input/input.test.tsx index 95bdd7a1..dd2f73f6 100644 --- a/src/form/input/input.test.tsx +++ b/src/form/input/input.test.tsx @@ -3,7 +3,8 @@ import {render, screen} from "@testing-library/react"; import "@testing-library/jest-dom"; import userEvent from "@testing-library/user-event"; -import Input, {InputProps} from "./Input"; +import Input from "./Input"; +import {InputProps} from "./util/inputTypes"; import {testA11y} from "../../core/utils/test/testUtils"; describe("", () => { diff --git a/src/form/input/typeahead/TypeaheadInput.tsx b/src/form/input/typeahead/TypeaheadInput.tsx index 12706616..961b908d 100644 --- a/src/form/input/typeahead/TypeaheadInput.tsx +++ b/src/form/input/typeahead/TypeaheadInput.tsx @@ -1,7 +1,8 @@ import React, {useEffect} from "react"; import classNames from "classnames"; -import Input, {InputProps, InputTypes} from "../Input"; +import Input from "../Input"; +import {InputProps, InputTypes} from "../util/inputTypes"; import useDebounce from "../../../core/utils/hooks/useDebounce"; export type TypeaheadInputProps = Omit & { diff --git a/src/form/input/util/inputTypes.ts b/src/form/input/util/inputTypes.ts index d04de7fe..aa258175 100644 --- a/src/form/input/util/inputTypes.ts +++ b/src/form/input/util/inputTypes.ts @@ -1,3 +1,43 @@ +export type InputTypes = + | "checkbox" + | "button" + | "color" + | "date" + | "datetime-local" + | "email" + | "file" + | "hidden" + | "image" + | "month" + | "number" + | "password" + | "radio" + | "range" + | "reset" + | "search" + | "submit" + | "tel" + | "text" + | "time" + | "url" + | "week"; + +export type InputProps = Omit< + React.InputHTMLAttributes, + "disabled" | "name" | "className" +> & { + name: string; + type?: InputTypes; + testid?: string; + leftIcon?: React.ReactNode; + rightIcon?: React.ReactNode; + isDisabled?: boolean; + hasError?: boolean; + customClassName?: string; + onChange: React.ReactEventHandler; + localizationOptions?: InputLocalizationOptions; +}; + export type InputLocalizationOptions = { shouldFormatToLocaleString?: boolean; locale?: string; diff --git a/src/form/input/util/inputUtils.ts b/src/form/input/util/inputUtils.ts index 1cd05e2c..dd2b9230 100644 --- a/src/form/input/util/inputUtils.ts +++ b/src/form/input/util/inputUtils.ts @@ -1,7 +1,8 @@ import {isMobileDevice} from "../../../core/utils/device/deviceUtils"; +import {ParseNumberOptions} from "../../../core/utils/number/numberTypes"; import {InputLocalizationOptions} from "./inputTypes"; -function getLocalizationOptions( +function getInputLocalizationOptions( localizationOptions: InputLocalizationOptions ): InputLocalizationOptions { return isMobileDevice() @@ -13,4 +14,13 @@ function getLocalizationOptions( : localizationOptions; } -export {getLocalizationOptions}; +function getInputParseNumberOptions(options: ParseNumberOptions): ParseNumberOptions { + return isMobileDevice() + ? { + ...options, + locale: "en-US" + } + : options; +} + +export {getInputLocalizationOptions, getInputParseNumberOptions}; diff --git a/src/form/password-input/PasswordInput.tsx b/src/form/password-input/PasswordInput.tsx index 9cd2b2a3..2da8f5f1 100644 --- a/src/form/password-input/PasswordInput.tsx +++ b/src/form/password-input/PasswordInput.tsx @@ -3,7 +3,8 @@ import "./_password-input.scss"; import React, {useState} from "react"; import classNames from "classnames"; -import Input, {InputProps} from "../input/Input"; +import Input from "../input/Input"; +import {InputProps} from "../input/util/inputTypes"; import Button from "../../button/Button"; export interface PasswordInputProps extends Omit { diff --git a/src/index.ts b/src/index.ts index 3c9ffcc1..43674c76 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,8 @@ import "./ui/reference/_measurement.scss"; import FormField, { FormFieldProps as FormFieldComponentProps } from "./form/field/FormField"; -import Input, {InputProps as InputComponentProps} from "./form/input/Input"; +import Input from "./form/input/Input"; +import {InputProps as InputComponentProps} from "./form/input/util/inputTypes"; import PasswordInput, { PasswordInputProps as PasswordInputComponentProps } from "./form/password-input/PasswordInput";