From a9c55bc17bc9e73ebbf1cc74a850b3acc51e8635 Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Wed, 6 Jan 2021 00:25:23 +0100 Subject: [PATCH 1/3] fix(localize): folder restructure and Chrome corrections --- packages/localize/src/date/formatDate.js | 4 +-- .../src/date/getDateFormatBasedOnLocale.js | 4 +-- packages/localize/src/date/getLocale.js | 17 ----------- packages/localize/src/date/getMonthNames.js | 7 +++-- packages/localize/src/date/getWeekdayNames.js | 2 +- packages/localize/src/date/parseDate.js | 2 +- .../src/date/{ => utils}/addLeadingZero.js | 0 .../localize/src/date/{ => utils}/clean.js | 0 .../date/utils/forceShortMonthNamesForEnGb.js | 10 +++++++ .../src/date/{ => utils}/normalizeIntlDate.js | 0 packages/localize/src/date/{ => utils}/pad.js | 0 .../{ => utils}/sanitizedDateTimeFormat.js | 2 +- .../src/date/{ => utils}/splitDate.js | 0 .../localize/src/date/{ => utils}/trim.js | 0 packages/localize/src/number/formatNumber.js | 2 +- .../src/number/formatNumberToParts.js | 30 +++++++++++++++---- .../localize/src/number/getCurrencyName.js | 8 ++++- .../src/number/getDecimalSeparator.js | 2 +- .../localize/src/number/getGroupSeparator.js | 4 +-- packages/localize/src/number/roundNumber.js | 20 ------------- .../{ => utils}/emptyStringWhenNumberNan.js | 2 +- .../src/number/{ => utils}/normalSpaces.js | 0 .../forceAddGroupSeparators.js | 2 +- .../forceCurrencyToEnd.js | 2 +- .../forceENAUSymbols.js | 4 +-- .../forceNormalSpaces.js | 4 +-- .../forceSpaceBetweenCurrencyCodeAndNumber.js | 4 +-- .../forceSpaceInsteadOfZeroForGroup.js | 2 +- .../forceTryCurrencyCode.js | 4 +-- .../forceYenSymbol.js | 4 +-- .../normalizeIntl.js | 8 ++--- .../forceCurrencyNameForPHPEnGB.js | 10 +++++++ .../src/{number => utils}/getLocale.js | 0 33 files changed, 86 insertions(+), 74 deletions(-) delete mode 100644 packages/localize/src/date/getLocale.js rename packages/localize/src/date/{ => utils}/addLeadingZero.js (100%) rename packages/localize/src/date/{ => utils}/clean.js (100%) create mode 100644 packages/localize/src/date/utils/forceShortMonthNamesForEnGb.js rename packages/localize/src/date/{ => utils}/normalizeIntlDate.js (100%) rename packages/localize/src/date/{ => utils}/pad.js (100%) rename packages/localize/src/date/{ => utils}/sanitizedDateTimeFormat.js (83%) rename packages/localize/src/date/{ => utils}/splitDate.js (100%) rename packages/localize/src/date/{ => utils}/trim.js (100%) delete mode 100644 packages/localize/src/number/roundNumber.js rename packages/localize/src/number/{ => utils}/emptyStringWhenNumberNan.js (85%) rename packages/localize/src/number/{ => utils}/normalSpaces.js (100%) rename packages/localize/src/number/{ => utils/normalize-format-number-to-parts}/forceAddGroupSeparators.js (95%) rename packages/localize/src/number/{ => utils/normalize-format-number-to-parts}/forceCurrencyToEnd.js (88%) rename packages/localize/src/number/{ => utils/normalize-format-number-to-parts}/forceENAUSymbols.js (77%) rename packages/localize/src/number/{ => utils/normalize-format-number-to-parts}/forceNormalSpaces.js (72%) rename packages/localize/src/number/{ => utils/normalize-format-number-to-parts}/forceSpaceBetweenCurrencyCodeAndNumber.js (86%) rename packages/localize/src/number/{ => utils/normalize-format-number-to-parts}/forceSpaceInsteadOfZeroForGroup.js (82%) rename packages/localize/src/number/{ => utils/normalize-format-number-to-parts}/forceTryCurrencyCode.js (71%) rename packages/localize/src/number/{ => utils/normalize-format-number-to-parts}/forceYenSymbol.js (71%) rename packages/localize/src/number/{ => utils/normalize-format-number-to-parts}/normalizeIntl.js (87%) create mode 100644 packages/localize/src/number/utils/normalize-get-currency-name/forceCurrencyNameForPHPEnGB.js rename packages/localize/src/{number => utils}/getLocale.js (100%) diff --git a/packages/localize/src/date/formatDate.js b/packages/localize/src/date/formatDate.js index 9cee2a552..2bde7f856 100644 --- a/packages/localize/src/date/formatDate.js +++ b/packages/localize/src/date/formatDate.js @@ -1,6 +1,6 @@ import { localize } from '../localize.js'; -import { getLocale } from './getLocale.js'; -import { normalizeIntlDate } from './normalizeIntlDate.js'; +import { getLocale } from '../utils/getLocale.js'; +import { normalizeIntlDate } from './utils/normalizeIntlDate.js'; /** @typedef {import('../../types/LocalizeMixinTypes').DatePostProcessor} DatePostProcessor */ diff --git a/packages/localize/src/date/getDateFormatBasedOnLocale.js b/packages/localize/src/date/getDateFormatBasedOnLocale.js index 5336a7380..c5e976246 100644 --- a/packages/localize/src/date/getDateFormatBasedOnLocale.js +++ b/packages/localize/src/date/getDateFormatBasedOnLocale.js @@ -1,5 +1,5 @@ -import { sanitizedDateTimeFormat } from './sanitizedDateTimeFormat.js'; -import { splitDate } from './splitDate.js'; +import { sanitizedDateTimeFormat } from './utils/sanitizedDateTimeFormat.js'; +import { splitDate } from './utils/splitDate.js'; /** * To compute the localized date format diff --git a/packages/localize/src/date/getLocale.js b/packages/localize/src/date/getLocale.js deleted file mode 100644 index 88cba75bc..000000000 --- a/packages/localize/src/date/getLocale.js +++ /dev/null @@ -1,17 +0,0 @@ -import { localize } from '../localize.js'; - -/** - * Gets the locale to use - * - * @param {string|undefined} locale Locale to override browser locale - * @returns {string} - */ -export function getLocale(locale) { - if (locale) { - return locale; - } - if (localize && localize.locale) { - return localize.locale; - } - return 'en-GB'; -} diff --git a/packages/localize/src/date/getMonthNames.js b/packages/localize/src/date/getMonthNames.js index 716db16c7..72c2997c7 100644 --- a/packages/localize/src/date/getMonthNames.js +++ b/packages/localize/src/date/getMonthNames.js @@ -1,4 +1,5 @@ -import { normalizeIntlDate } from './normalizeIntlDate.js'; +import { normalizeIntlDate } from './utils/normalizeIntlDate.js'; +import { forceShortMonthNamesForEnGb } from './utils/forceShortMonthNamesForEnGb.js'; /** @type {Object.>} */ const monthsLocaleCache = {}; @@ -26,7 +27,9 @@ export function getMonthNames({ locale, style = 'long' } = {}) { const normalizedDate = normalizeIntlDate(formattedDate); months.push(normalizedDate); } - + if (locale === 'en-GB' && style === 'short') { + months = forceShortMonthNamesForEnGb(months); + } monthsLocaleCache[locale] = monthsLocaleCache[locale] || {}; monthsLocaleCache[locale][style] = months; diff --git a/packages/localize/src/date/getWeekdayNames.js b/packages/localize/src/date/getWeekdayNames.js index d96f8dd14..e4bb00c6a 100644 --- a/packages/localize/src/date/getWeekdayNames.js +++ b/packages/localize/src/date/getWeekdayNames.js @@ -1,4 +1,4 @@ -import { normalizeIntlDate } from './normalizeIntlDate.js'; +import { normalizeIntlDate } from './utils/normalizeIntlDate.js'; /** @type {Object.>} */ const weekdayNamesCache = {}; diff --git a/packages/localize/src/date/parseDate.js b/packages/localize/src/date/parseDate.js index 4dd5a59fb..228534733 100644 --- a/packages/localize/src/date/parseDate.js +++ b/packages/localize/src/date/parseDate.js @@ -1,6 +1,6 @@ import { localize } from '../localize.js'; import { getDateFormatBasedOnLocale } from './getDateFormatBasedOnLocale.js'; -import { addLeadingZero } from './addLeadingZero.js'; +import { addLeadingZero } from './utils/addLeadingZero.js'; /** * @param {function} fn diff --git a/packages/localize/src/date/addLeadingZero.js b/packages/localize/src/date/utils/addLeadingZero.js similarity index 100% rename from packages/localize/src/date/addLeadingZero.js rename to packages/localize/src/date/utils/addLeadingZero.js diff --git a/packages/localize/src/date/clean.js b/packages/localize/src/date/utils/clean.js similarity index 100% rename from packages/localize/src/date/clean.js rename to packages/localize/src/date/utils/clean.js diff --git a/packages/localize/src/date/utils/forceShortMonthNamesForEnGb.js b/packages/localize/src/date/utils/forceShortMonthNamesForEnGb.js new file mode 100644 index 000000000..d2be41d7b --- /dev/null +++ b/packages/localize/src/date/utils/forceShortMonthNamesForEnGb.js @@ -0,0 +1,10 @@ +/** + * @param {string[]} months + */ +export function forceShortMonthNamesForEnGb(months) { + if (months[8] === 'Sept') { + // eslint-disable-next-line no-param-reassign + months[8] = 'Sep'; + } + return months; +} diff --git a/packages/localize/src/date/normalizeIntlDate.js b/packages/localize/src/date/utils/normalizeIntlDate.js similarity index 100% rename from packages/localize/src/date/normalizeIntlDate.js rename to packages/localize/src/date/utils/normalizeIntlDate.js diff --git a/packages/localize/src/date/pad.js b/packages/localize/src/date/utils/pad.js similarity index 100% rename from packages/localize/src/date/pad.js rename to packages/localize/src/date/utils/pad.js diff --git a/packages/localize/src/date/sanitizedDateTimeFormat.js b/packages/localize/src/date/utils/sanitizedDateTimeFormat.js similarity index 83% rename from packages/localize/src/date/sanitizedDateTimeFormat.js rename to packages/localize/src/date/utils/sanitizedDateTimeFormat.js index c31a79b63..78e26df2d 100644 --- a/packages/localize/src/date/sanitizedDateTimeFormat.js +++ b/packages/localize/src/date/utils/sanitizedDateTimeFormat.js @@ -1,4 +1,4 @@ -import { formatDate } from './formatDate.js'; +import { formatDate } from '../formatDate.js'; import { clean } from './clean.js'; /** diff --git a/packages/localize/src/date/splitDate.js b/packages/localize/src/date/utils/splitDate.js similarity index 100% rename from packages/localize/src/date/splitDate.js rename to packages/localize/src/date/utils/splitDate.js diff --git a/packages/localize/src/date/trim.js b/packages/localize/src/date/utils/trim.js similarity index 100% rename from packages/localize/src/date/trim.js rename to packages/localize/src/date/utils/trim.js diff --git a/packages/localize/src/number/formatNumber.js b/packages/localize/src/number/formatNumber.js index 829873301..a80da0958 100644 --- a/packages/localize/src/number/formatNumber.js +++ b/packages/localize/src/number/formatNumber.js @@ -1,5 +1,5 @@ import { localize } from '../localize.js'; -import { getLocale } from './getLocale.js'; +import { getLocale } from '../utils/getLocale.js'; import { formatNumberToParts } from './formatNumberToParts.js'; /** @typedef {import('../../types/LocalizeMixinTypes').NumberPostProcessor} NumberPostProcessor */ diff --git a/packages/localize/src/number/formatNumberToParts.js b/packages/localize/src/number/formatNumberToParts.js index 1a5f4c1d3..d34dc7cab 100644 --- a/packages/localize/src/number/formatNumberToParts.js +++ b/packages/localize/src/number/formatNumberToParts.js @@ -1,10 +1,30 @@ -import { emptyStringWhenNumberNan } from './emptyStringWhenNumberNan.js'; +import { emptyStringWhenNumberNan } from './utils/emptyStringWhenNumberNan.js'; import { getDecimalSeparator } from './getDecimalSeparator.js'; import { getGroupSeparator } from './getGroupSeparator.js'; -import { getLocale } from './getLocale.js'; -import { normalizeIntl } from './normalizeIntl.js'; -import { normalSpaces } from './normalSpaces.js'; -import { roundNumber } from './roundNumber.js'; +import { getLocale } from '../utils/getLocale.js'; +import { normalizeIntl } from './utils/normalize-format-number-to-parts/normalizeIntl.js'; +import { normalSpaces } from './utils/normalSpaces.js'; + +/** + * Round the number based on the options + * + * @param {number} number + * @param {string} roundMode + * @throws {Error} roundMode can only be round|floor|ceiling + * @returns {number} + */ +export function roundNumber(number, roundMode) { + switch (roundMode) { + case 'floor': + return Math.floor(number); + case 'ceiling': + return Math.ceil(number); + case 'round': + return Math.round(number); + default: + throw new Error('roundMode can only be round|floor|ceiling'); + } +} /** * Splits a number up in parts for integer, fraction, group, literal, decimal and currency. diff --git a/packages/localize/src/number/getCurrencyName.js b/packages/localize/src/number/getCurrencyName.js index 14fe56e14..1cc14a162 100644 --- a/packages/localize/src/number/getCurrencyName.js +++ b/packages/localize/src/number/getCurrencyName.js @@ -1,4 +1,6 @@ import { formatNumberToParts } from './formatNumberToParts.js'; +import { localize } from '../localize.js'; +import { forceCurrencyNameForPHPEnGB } from './utils/normalize-get-currency-name/forceCurrencyNameForPHPEnGB.js'; /** * Based on number, returns currency name like 'US dollar' @@ -15,9 +17,13 @@ export function getCurrencyName(currencyIso, options) { currency: currencyIso, currencyDisplay: 'name', })); - const currencyName = parts + let currencyName = parts .filter(p => p.type === 'currency') .map(o => o.value) .join(' '); + const locale = options?.locale || localize.locale; + if (currencyIso === 'PHP' && locale === 'en-GB') { + currencyName = forceCurrencyNameForPHPEnGB(currencyName); + } return currencyName; } diff --git a/packages/localize/src/number/getDecimalSeparator.js b/packages/localize/src/number/getDecimalSeparator.js index 5df3b68eb..5dc9f2005 100644 --- a/packages/localize/src/number/getDecimalSeparator.js +++ b/packages/localize/src/number/getDecimalSeparator.js @@ -1,4 +1,4 @@ -import { getLocale } from './getLocale.js'; +import { getLocale } from '../utils/getLocale.js'; /** * To get the decimal separator diff --git a/packages/localize/src/number/getGroupSeparator.js b/packages/localize/src/number/getGroupSeparator.js index 5f97eca6d..6bfa21d0c 100644 --- a/packages/localize/src/number/getGroupSeparator.js +++ b/packages/localize/src/number/getGroupSeparator.js @@ -1,5 +1,5 @@ -import { getLocale } from './getLocale.js'; -import { normalSpaces } from './normalSpaces.js'; +import { getLocale } from '../utils/getLocale.js'; +import { normalSpaces } from './utils/normalSpaces.js'; /** * Gets the group separator diff --git a/packages/localize/src/number/roundNumber.js b/packages/localize/src/number/roundNumber.js deleted file mode 100644 index c7438e94a..000000000 --- a/packages/localize/src/number/roundNumber.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Round the number based on the options - * - * @param {number} number - * @param {string} roundMode - * @throws {Error} roundMode can only be round|floor|ceiling - * @returns {number} - */ -export function roundNumber(number, roundMode) { - switch (roundMode) { - case 'floor': - return Math.floor(number); - case 'ceiling': - return Math.ceil(number); - case 'round': - return Math.round(number); - default: - throw new Error('roundMode can only be round|floor|ceiling'); - } -} diff --git a/packages/localize/src/number/emptyStringWhenNumberNan.js b/packages/localize/src/number/utils/emptyStringWhenNumberNan.js similarity index 85% rename from packages/localize/src/number/emptyStringWhenNumberNan.js rename to packages/localize/src/number/utils/emptyStringWhenNumberNan.js index 6a4140f96..1d20fb4a1 100644 --- a/packages/localize/src/number/emptyStringWhenNumberNan.js +++ b/packages/localize/src/number/utils/emptyStringWhenNumberNan.js @@ -1,4 +1,4 @@ -import { localize } from '../localize.js'; +import { localize } from '../../localize.js'; /** * When number is NaN we should return an empty string or returnIfNaN param diff --git a/packages/localize/src/number/normalSpaces.js b/packages/localize/src/number/utils/normalSpaces.js similarity index 100% rename from packages/localize/src/number/normalSpaces.js rename to packages/localize/src/number/utils/normalSpaces.js diff --git a/packages/localize/src/number/forceAddGroupSeparators.js b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceAddGroupSeparators.js similarity index 95% rename from packages/localize/src/number/forceAddGroupSeparators.js rename to packages/localize/src/number/utils/normalize-format-number-to-parts/forceAddGroupSeparators.js index b3528d150..fb2c4b791 100644 --- a/packages/localize/src/number/forceAddGroupSeparators.js +++ b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceAddGroupSeparators.js @@ -1,7 +1,7 @@ /** * Add separators when they are not present * - * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart + * @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart * @param {FormatNumberPart[]} formattedParts * @param {string} groupSeparator * @returns {FormatNumberPart[]} diff --git a/packages/localize/src/number/forceCurrencyToEnd.js b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceCurrencyToEnd.js similarity index 88% rename from packages/localize/src/number/forceCurrencyToEnd.js rename to packages/localize/src/number/utils/normalize-format-number-to-parts/forceCurrencyToEnd.js index d4f02f5b4..4199e486a 100644 --- a/packages/localize/src/number/forceCurrencyToEnd.js +++ b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceCurrencyToEnd.js @@ -1,7 +1,7 @@ /** * For Dutch and Belgian amounts the currency should be at the end of the string * - * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart + * @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart * @param {FormatNumberPart[]} formattedParts * @returns {FormatNumberPart[]} */ diff --git a/packages/localize/src/number/forceENAUSymbols.js b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceENAUSymbols.js similarity index 77% rename from packages/localize/src/number/forceENAUSymbols.js rename to packages/localize/src/number/utils/normalize-format-number-to-parts/forceENAUSymbols.js index e477783fc..4d67caca0 100644 --- a/packages/localize/src/number/forceENAUSymbols.js +++ b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceENAUSymbols.js @@ -8,9 +8,9 @@ const CURRENCY_CODE_SYMBOL_MAP = { /** * Change the symbols for locale 'en-AU', due to bug in Chrome * - * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart + * @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart * @param {FormatNumberPart[]} formattedParts - * @param {import('../../types/LocalizeMixinTypes').FormatNumberOptions} [options] + * @param {import('../../../../types/LocalizeMixinTypes').FormatNumberOptions} [options] * @returns {FormatNumberPart[]} */ export function forceENAUSymbols(formattedParts, { currency, currencyDisplay } = {}) { diff --git a/packages/localize/src/number/forceNormalSpaces.js b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceNormalSpaces.js similarity index 72% rename from packages/localize/src/number/forceNormalSpaces.js rename to packages/localize/src/number/utils/normalize-format-number-to-parts/forceNormalSpaces.js index b8d083f47..cdaffa88b 100644 --- a/packages/localize/src/number/forceNormalSpaces.js +++ b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceNormalSpaces.js @@ -1,9 +1,9 @@ -import { normalSpaces } from './normalSpaces.js'; +import { normalSpaces } from '../normalSpaces.js'; /** * Parts with forced "normal" spaces * - * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart + * @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart * @param {FormatNumberPart[]} formattedParts * @returns {FormatNumberPart[]} */ diff --git a/packages/localize/src/number/forceSpaceBetweenCurrencyCodeAndNumber.js b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceSpaceBetweenCurrencyCodeAndNumber.js similarity index 86% rename from packages/localize/src/number/forceSpaceBetweenCurrencyCodeAndNumber.js rename to packages/localize/src/number/utils/normalize-format-number-to-parts/forceSpaceBetweenCurrencyCodeAndNumber.js index f43822c2f..883705fcc 100644 --- a/packages/localize/src/number/forceSpaceBetweenCurrencyCodeAndNumber.js +++ b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceSpaceBetweenCurrencyCodeAndNumber.js @@ -1,9 +1,9 @@ /** * When in some locales there is no space between currency and amount it is added * - * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart + * @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart * @param {FormatNumberPart[]} formattedParts - * @param {import('../../types/LocalizeMixinTypes').FormatNumberOptions} [options] + * @param {import('../../../../types/LocalizeMixinTypes').FormatNumberOptions} [options] * @returns {FormatNumberPart[]} */ export function forceSpaceBetweenCurrencyCodeAndNumber( diff --git a/packages/localize/src/number/forceSpaceInsteadOfZeroForGroup.js b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceSpaceInsteadOfZeroForGroup.js similarity index 82% rename from packages/localize/src/number/forceSpaceInsteadOfZeroForGroup.js rename to packages/localize/src/number/utils/normalize-format-number-to-parts/forceSpaceInsteadOfZeroForGroup.js index 18c2acc74..7206ab417 100644 --- a/packages/localize/src/number/forceSpaceInsteadOfZeroForGroup.js +++ b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceSpaceInsteadOfZeroForGroup.js @@ -2,7 +2,7 @@ * @desc Intl uses 0 as group separator for bg-BG locale. * This should be a ' ' * - * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart + * @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart * @param {FormatNumberPart[]} formattedParts * @returns {FormatNumberPart[]} corrected formatted parts */ diff --git a/packages/localize/src/number/forceTryCurrencyCode.js b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceTryCurrencyCode.js similarity index 71% rename from packages/localize/src/number/forceTryCurrencyCode.js rename to packages/localize/src/number/utils/normalize-format-number-to-parts/forceTryCurrencyCode.js index 3882a0273..e3c958e7d 100644 --- a/packages/localize/src/number/forceTryCurrencyCode.js +++ b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceTryCurrencyCode.js @@ -1,7 +1,7 @@ /** - * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart + * @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart * @param {FormatNumberPart[]} formattedParts - * @param {import('../../types/LocalizeMixinTypes').FormatNumberOptions} [options] + * @param {import('../../../../types/LocalizeMixinTypes').FormatNumberOptions} [options] * @returns {FormatNumberPart[]} */ diff --git a/packages/localize/src/number/forceYenSymbol.js b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceYenSymbol.js similarity index 71% rename from packages/localize/src/number/forceYenSymbol.js rename to packages/localize/src/number/utils/normalize-format-number-to-parts/forceYenSymbol.js index ccb9dfec1..2f39bb5f7 100644 --- a/packages/localize/src/number/forceYenSymbol.js +++ b/packages/localize/src/number/utils/normalize-format-number-to-parts/forceYenSymbol.js @@ -1,7 +1,7 @@ /** - * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart + * @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart * @param {FormatNumberPart[]} formattedParts - * @param {import('../../types/LocalizeMixinTypes').FormatNumberOptions} [options] + * @param {import('../../../../types/LocalizeMixinTypes').FormatNumberOptions} [options] * @returns {FormatNumberPart[]} */ export function forceYenSymbol(formattedParts, { currency, currencyDisplay } = {}) { diff --git a/packages/localize/src/number/normalizeIntl.js b/packages/localize/src/number/utils/normalize-format-number-to-parts/normalizeIntl.js similarity index 87% rename from packages/localize/src/number/normalizeIntl.js rename to packages/localize/src/number/utils/normalize-format-number-to-parts/normalizeIntl.js index b66ee582c..23b4f86aa 100644 --- a/packages/localize/src/number/normalizeIntl.js +++ b/packages/localize/src/number/utils/normalize-format-number-to-parts/normalizeIntl.js @@ -1,4 +1,4 @@ -import { getGroupSeparator } from './getGroupSeparator.js'; +import { getGroupSeparator } from '../../getGroupSeparator.js'; import { forceAddGroupSeparators } from './forceAddGroupSeparators.js'; import { forceCurrencyToEnd } from './forceCurrencyToEnd.js'; import { forceNormalSpaces } from './forceNormalSpaces.js'; @@ -9,11 +9,11 @@ import { forceTryCurrencyCode } from './forceTryCurrencyCode.js'; import { forceENAUSymbols } from './forceENAUSymbols.js'; /** - * Function with all fixes on localize + * Normalizes function "formatNumberToParts" * - * @typedef {import('../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart + * @typedef {import('../../../../types/LocalizeMixinTypes').FormatNumberPart} FormatNumberPart * @param {FormatNumberPart[]} formattedParts - * @param {import('../../types/LocalizeMixinTypes').FormatNumberOptions} options + * @param {import('../../../../types/LocalizeMixinTypes').FormatNumberOptions} options * @param {string} _locale * @returns {FormatNumberPart[]} */ diff --git a/packages/localize/src/number/utils/normalize-get-currency-name/forceCurrencyNameForPHPEnGB.js b/packages/localize/src/number/utils/normalize-get-currency-name/forceCurrencyNameForPHPEnGB.js new file mode 100644 index 000000000..2d1b5af15 --- /dev/null +++ b/packages/localize/src/number/utils/normalize-get-currency-name/forceCurrencyNameForPHPEnGB.js @@ -0,0 +1,10 @@ +/** + * @param {string} currencyName + */ +export function forceCurrencyNameForPHPEnGB(currencyName) { + if (currencyName === 'Philippine pesos') { + // eslint-disable-next-line no-param-reassign + currencyName = 'Philippine pisos'; + } + return currencyName; +} diff --git a/packages/localize/src/number/getLocale.js b/packages/localize/src/utils/getLocale.js similarity index 100% rename from packages/localize/src/number/getLocale.js rename to packages/localize/src/utils/getLocale.js From b88760d57809d49163033d24d04daf61a7bf0a09 Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Wed, 6 Jan 2021 00:44:30 +0100 Subject: [PATCH 2/3] feat(form-core): add details.isTriggeredByUser to model-value-changed --- packages/form-core/src/FormControlMixin.js | 29 ++- packages/form-core/src/FormatMixin.js | 6 +- .../src/choice-group/ChoiceInputMixin.js | 7 + .../test-suites/FormatMixin.suite.js | 17 +- .../choice-group/ChoiceInputMixin.suite.js | 15 ++ .../form-core/test/FormControlMixin.test.js | 39 ++++ .../types/FormControlMixinTypes.d.ts | 49 +++-- .../docs/17-validation-examples.md | 6 +- .../test/model-value-consistency.test.js | 177 ++++++++++++++++++ packages/listbox/src/LionOption.js | 2 + packages/listbox/src/ListboxMixin.js | 9 +- packages/listbox/test/lion-option.test.js | 32 ++++ 12 files changed, 357 insertions(+), 31 deletions(-) diff --git a/packages/form-core/src/FormControlMixin.js b/packages/form-core/src/FormControlMixin.js index 3a2609caa..0f7820599 100644 --- a/packages/form-core/src/FormControlMixin.js +++ b/packages/form-core/src/FormControlMixin.js @@ -4,6 +4,16 @@ import { FormRegisteringMixin } from './registration/FormRegisteringMixin.js'; import { getAriaElementsInRightDomOrder } from './utils/getAriaElementsInRightDomOrder.js'; import { Unparseable } from './validate/Unparseable.js'; +/** + * @typedef {import('@lion/core').TemplateResult} TemplateResult + * @typedef {import('@lion/core').CSSResult} CSSResult + * @typedef {import('@lion/core').nothing} nothing + * @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap + * @typedef {import('../types/FormControlMixinTypes.js').FormControlHost} FormControlHost + * @typedef {import('../types/FormControlMixinTypes.js').FormControlMixin} FormControlMixin + * @typedef {import('../types/FormControlMixinTypes.js').ModelValueEventDetails} ModelValueEventDetails + */ + /** * Generates random unique identifier (for dom elements) * @param {string} prefix @@ -18,11 +28,6 @@ function uuid(prefix) { * This Mixin is a shared fundament for all form components, it's applied on: * - LionField (which is extended to LionInput, LionTextarea, LionSelect etc. etc.) * - LionFieldset (which is extended to LionRadioGroup, LionCheckboxGroup, LionForm) - * @typedef {import('@lion/core').TemplateResult} TemplateResult - * @typedef {import('@lion/core').CSSResult} CSSResult - * @typedef {import('@lion/core').nothing} nothing - * @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap - * @typedef {import('../types/FormControlMixinTypes.js').FormControlMixin} FormControlMixin * @param {import('@open-wc/dedupe-mixin').Constructor} superclass * @type {FormControlMixin} */ @@ -750,7 +755,11 @@ const FormControlMixinImplementation = superclass => this.dispatchEvent( new CustomEvent('model-value-changed', { bubbles: true, - detail: { formPath: [this], initialize: true }, + detail: /** @type {ModelValueEventDetails} */ ({ + formPath: [this], + initialize: true, + isTriggeredByUser: false, + }), }), ); } @@ -822,7 +831,13 @@ const FormControlMixinImplementation = superclass => // // Since for a11y everything needs to be in lightdom, we don't add 'composed:true' this.dispatchEvent( - new CustomEvent('model-value-changed', { bubbles: true, detail: { formPath } }), + new CustomEvent('model-value-changed', { + bubbles: true, + detail: /** @type {ModelValueEventDetails} */ ({ + formPath, + isTriggeredByUser: Boolean(ev.detail?.isTriggeredByUser), + }), + }), ); } diff --git a/packages/form-core/src/FormatMixin.js b/packages/form-core/src/FormatMixin.js index 136132dc8..8ec33e3c4 100644 --- a/packages/form-core/src/FormatMixin.js +++ b/packages/form-core/src/FormatMixin.js @@ -8,6 +8,7 @@ import { ValidateMixin } from './validate/ValidateMixin.js'; /** * @typedef {import('../types/FormatMixinTypes').FormatMixin} FormatMixin * @typedef {import('@lion/localize/types/LocalizeMixinTypes').FormatNumberOptions} FormatOptions + * @typedef {import('../types/FormControlMixinTypes.js').ModelValueEventDetails} ModelValueEventDetails */ // For a future breaking release: @@ -316,7 +317,10 @@ const FormatMixinImplementation = superclass => this.dispatchEvent( new CustomEvent('model-value-changed', { bubbles: true, - detail: { formPath: [this] }, + detail: /** @type { ModelValueEventDetails } */ ({ + formPath: [this], + isTriggeredByUser: Boolean(this.__isHandlingUserInput), + }), }), ); } diff --git a/packages/form-core/src/choice-group/ChoiceInputMixin.js b/packages/form-core/src/choice-group/ChoiceInputMixin.js index 58cba4e73..dac659e9c 100644 --- a/packages/form-core/src/choice-group/ChoiceInputMixin.js +++ b/packages/form-core/src/choice-group/ChoiceInputMixin.js @@ -175,6 +175,7 @@ const ChoiceInputMixinImplementation = superclass => + ${this._afterTemplate()} `; } @@ -182,6 +183,10 @@ const ChoiceInputMixinImplementation = superclass => return nothing; } + _afterTemplate() { + return nothing; + } + connectedCallback() { super.connectedCallback(); this.addEventListener('user-input-changed', this.__toggleChecked); @@ -196,7 +201,9 @@ const ChoiceInputMixinImplementation = superclass => if (this.disabled) { return; } + this.__isHandlingUserInput = true; this.checked = !this.checked; + this.__isHandlingUserInput = false; } /** diff --git a/packages/form-core/test-suites/FormatMixin.suite.js b/packages/form-core/test-suites/FormatMixin.suite.js index 82c488dc6..6180c2d7a 100644 --- a/packages/form-core/test-suites/FormatMixin.suite.js +++ b/packages/form-core/test-suites/FormatMixin.suite.js @@ -123,18 +123,23 @@ export function runFormatMixinSuite(customConfig) { `); }); - it('fires `model-value-changed` for every change on the input', async () => { + it('fires `model-value-changed` for every input triggered by user', async () => { const formatEl = /** @type {FormatClass} */ (await fixture( html`<${elem}>`, )); let counter = 0; - formatEl.addEventListener('model-value-changed', () => { + let isTriggeredByUser = false; + formatEl.addEventListener('model-value-changed', ( + /** @param {CustomEvent} event */ event, + ) => { counter += 1; + isTriggeredByUser = /** @type {CustomEvent} */ (event).detail.isTriggeredByUser; }); mimicUserInput(formatEl, generateValueBasedOnType()); expect(counter).to.equal(1); + expect(isTriggeredByUser).to.be.true; // Counter offset +1 for Date because parseDate created a new Date object // when the user changes the value. @@ -150,17 +155,21 @@ export function runFormatMixinSuite(customConfig) { expect(counter).to.equal(2 + counterOffset); }); - it('fires `model-value-changed` for every modelValue change', async () => { + it('fires `model-value-changed` for every programmatic modelValue change', async () => { const el = /** @type {FormatClass} */ (await fixture( html`<${elem}>`, )); let counter = 0; - el.addEventListener('model-value-changed', () => { + let isTriggeredByUser = false; + + el.addEventListener('model-value-changed', event => { counter += 1; + isTriggeredByUser = /** @type {CustomEvent} */ (event).detail.isTriggeredByUser; }); el.modelValue = 'one'; expect(counter).to.equal(1); + expect(isTriggeredByUser).to.be.false; // no change means no event el.modelValue = 'one'; diff --git a/packages/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js b/packages/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js index 1b96336ee..b03fe21cb 100644 --- a/packages/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js +++ b/packages/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js @@ -95,6 +95,21 @@ export function runChoiceInputMixinSuite({ tagString } = {}) { expect(counter).to.equal(1); }); + it('adds "isTriggerByUser" flag on model-value-changed', async () => { + let isTriggeredByUser; + const el = /** @type {ChoiceInput} */ (await fixture(html` + <${tag} + @model-value-changed="${(/** @type {CustomEvent} */ event) => { + isTriggeredByUser = event.detail.isTriggeredByUser; + }}" + > + + + `)); + el._inputNode.dispatchEvent(new CustomEvent('change', { bubbles: true })); + expect(isTriggeredByUser).to.be.true; + }); + it('can be required', async () => { const el = /** @type {ChoiceInput} */ (await fixture(html` <${tag} .choiceValue=${'foo'} .validators=${[new Required()]}> diff --git a/packages/form-core/test/FormControlMixin.test.js b/packages/form-core/test/FormControlMixin.test.js index b2c36ae8e..36c7a3dbd 100644 --- a/packages/form-core/test/FormControlMixin.test.js +++ b/packages/form-core/test/FormControlMixin.test.js @@ -238,14 +238,19 @@ describe('FormControlMixin', () => { const fieldsetEv = fieldsetSpy.firstCall.args[0]; expect(fieldsetEv.target).to.equal(fieldsetEl); expect(fieldsetEv.detail.formPath).to.eql([fieldsetEl]); + expect(fieldsetEv.detail.initialize).to.be.true; expect(formSpy.callCount).to.equal(1); const formEv = formSpy.firstCall.args[0]; expect(formEv.target).to.equal(formEl); expect(formEv.detail.formPath).to.eql([formEl]); + expect(formEv.detail.initialize).to.be.true; }); }); + /** + * After initialization means: events triggered programmatically or by user actions + */ describe('After initialization', () => { it('redispatches one event from host and keeps formPath history', async () => { const formSpy = sinon.spy(); @@ -310,11 +315,45 @@ describe('FormControlMixin', () => { const choiceGroupEv = choiceGroupSpy.firstCall.args[0]; expect(choiceGroupEv.target).to.equal(choiceGroupEl); expect(choiceGroupEv.detail.formPath).to.eql([choiceGroupEl]); + expect(choiceGroupEv.detail.isTriggeredByUser).to.be.false; expect(formSpy.callCount).to.equal(1); const formEv = formSpy.firstCall.args[0]; expect(formEv.target).to.equal(formEl); expect(formEv.detail.formPath).to.eql([choiceGroupEl, formEl]); + expect(formEv.detail.isTriggeredByUser).to.be.false; + }); + + it('sets "isTriggeredByUser" event detail when event triggered by user', async () => { + const formSpy = sinon.spy(); + const fieldsetSpy = sinon.spy(); + const fieldSpy = sinon.spy(); + const formEl = await fixture(html` + <${groupTag} name="form"> + <${groupTag} name="fieldset"> + <${tag} name="field"> + + + `); + const fieldEl = formEl.querySelector('[name=field]'); + const fieldsetEl = formEl.querySelector('[name=fieldset]'); + + formEl.addEventListener('model-value-changed', formSpy); + fieldsetEl?.addEventListener('model-value-changed', fieldsetSpy); + fieldEl?.addEventListener('model-value-changed', fieldSpy); + + fieldEl?.dispatchEvent( + new CustomEvent('model-value-changed', { + bubbles: true, + detail: { isTriggeredByUser: true }, + }), + ); + + const fieldsetEv = fieldsetSpy.firstCall.args[0]; + expect(fieldsetEv.detail.isTriggeredByUser).to.be.true; + + const formEv = formSpy.firstCall.args[0]; + expect(formEv.detail.isTriggeredByUser).to.be.true; }); }); }); diff --git a/packages/form-core/types/FormControlMixinTypes.d.ts b/packages/form-core/types/FormControlMixinTypes.d.ts index 4a22aa04f..581186cdc 100644 --- a/packages/form-core/types/FormControlMixinTypes.d.ts +++ b/packages/form-core/types/FormControlMixinTypes.d.ts @@ -6,6 +6,29 @@ import { DisabledHost } from '@lion/core/types/DisabledMixinTypes'; import { LionValidationFeedback } from '../src/validate/LionValidationFeedback'; import { FormRegisteringHost } from './registration/FormRegisteringMixinTypes'; +export type ModelValueEventDetails { + /** + * A list that represents the path of FormControls the model-value-changed event + * 'traveled through'. + * (every FormControl stops propagation of its child and sends a new event, hereby adding + * itself to the beginning of formPath) + */ + formPath: HTMLElement[]; + /** + * Whether the model-value-changed event is triggered via user interaction. This information + * can be helpful for both Application Developers and Subclassers. + * This concept is related to the native isTrusted property: + * https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted + */ + isTriggeredByUser: boolean; + /** + * Whether it is the first event sent on initialization of the form (other + * model-value-changed events are triggered imperatively or via user input (in the latter + * case `isTriggeredByUser` is true)) + */ + initialize?: boolean; +} + declare interface HTMLElementWithValue extends HTMLElement { value: string; } @@ -40,12 +63,12 @@ export declare class FormControlHost { * controls until they are enabled. * (From: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly) */ - readOnly: boolean; + public readOnly: boolean; /** - * The name the element will be registered on to the .formElements collection + * The name the element will be registered with to the .formElements collection * of the parent. */ - name: string; + public name: string; /** * The model value is the result of the parser function(when available). * It should be considered as the internal value used for validation and reasoning/logic. @@ -58,25 +81,25 @@ export declare class FormControlHost { * - For a number input: a formatted String '1.234,56' will be converted to a Number: * 1234.56 */ - modelValue: unknown; + public modelValue: unknown; /** * The label text for the input node. * When no light dom defined via [slot=label], this value will be used */ - get label(): string; - set label(arg: string); + public get label(): string; + public set label(arg: string); __label: string | undefined; /** * The helpt text for the input node. * When no light dom defined via [slot=help-text], this value will be used */ - get helpText(): string; - set helpText(arg: string); + public get helpText(): string; + public set helpText(arg: string); __helpText: string | undefined; - set fieldName(arg: string); - get fieldName(): string; + public set fieldName(arg: string); + public get fieldName(): string; __fieldName: string | undefined; - get slots(): SlotsMap; + public get slots(): SlotsMap; get _inputNode(): HTMLElementWithValue; get _labelNode(): HTMLElement; get _helpTextNode(): HTMLElement; @@ -123,7 +146,7 @@ export declare class FormControlHost { __reflectAriaAttr(attrName: string, nodes: HTMLElement[], reorder: boolean | undefined): void; _isEmpty(modelValue?: unknown): boolean; _getAriaDescriptionElements(): HTMLElement[]; - addToAriaLabelledBy( + public addToAriaLabelledBy( element: HTMLElement, customConfig?: { idPrefix?: string | undefined; @@ -131,7 +154,7 @@ export declare class FormControlHost { }, ): void; __reorderAriaLabelledNodes: boolean | undefined; - addToAriaDescribedBy( + public addToAriaDescribedBy( element: HTMLElement, customConfig?: { idPrefix?: string | undefined; diff --git a/packages/form-integrations/docs/17-validation-examples.md b/packages/form-integrations/docs/17-validation-examples.md index 92b22ace3..1401d614d 100644 --- a/packages/form-integrations/docs/17-validation-examples.md +++ b/packages/form-integrations/docs/17-validation-examples.md @@ -58,11 +58,7 @@ Useful on input elements it allows to define how many characters can be entered. ```js preview-story export const stringValidators = () => html` - + { }); }); }); + +describe('detail.isTriggeredByUser', () => { + const allFormControls = [ + // 1) Fields + 'field', + // 1a) Input Fields + 'input', + 'input-amount', + 'input-date', + 'input-datepicker', + 'input-email', + 'input-iban', + 'input-range', + 'textarea', + // 1b) Choice Fields + 'option', + 'checkbox', + 'radio', + // 1c) Choice Group Fields + 'select', + 'listbox', + 'select-rich', + 'combobox', + // 2) FormGroups + // 2a) Choice FormGroups + 'checkbox-group', + 'radio-group', + // 2v) Fieldset + 'fieldset', + // 2c) Form + 'form', + ]; + + /** + * "isTriggeredByUser" for different types of fields: + * + * RegularField: + * - true: when change/input (c.q. user-input-changed) fired + * - false: when .modelValue set programmatically + * + * ChoiceField: + * - true: when 'change' event fired + * - false: when .modelValue (or checked) set programmatically + * + * OptionChoiceField: + * - true: when 'click' event fired + * - false: when .modelValue (or checked) set programmatically + * + * ChoiceGroupField (listbox, select-rich, combobox, radio-group, checkbox-group): + * - true: when child formElement condition for Choice Field is met + * - false: when child formElement condition for Choice Field is not met + * + * FormOrFieldset (fieldset, form): + * - true: when child formElement condition for Field is met + * - false: when child formElement condition for Field is not met + */ + + const featureDetectChoiceField = el => 'checked' in el && 'choiceValue' in el; + const featureDetectOptionChoiceField = el => 'active' in el; + + /** + * @param {FormControl} el + * @returns {'RegularField'|'ChoiceField'|'OptionChoiceField'|'ChoiceGroupField'|'FormOrFieldset'} + */ + function detectType(el) { + if (el._repropagationRole === 'child') { + if (featureDetectChoiceField(el)) { + return featureDetectOptionChoiceField(el) ? 'OptionChoiceField' : 'ChoiceField'; + } + return 'RegularField'; + } + return el._repropagationRole === 'choice-group' ? 'ChoiceGroupField' : 'FormOrFieldset'; + } + + /** + * @param {FormControl} el + * @param {string} newViewValue + * @returns {'RegularField'|'ChoiceField'|'OptionChoiceField'|'ChoiceGroupField'|'FormOrFieldset'} + */ + function mimicUserInput(el, newViewValue) { + const type = detectType(el); + let userInputEv; + if (type === 'RegularField') { + userInputEv = el._inputNode.tagName === 'SELECT' ? 'change' : 'input'; + el.value = newViewValue; // eslint-disable-line no-param-reassign + el._inputNode.dispatchEvent(new Event(userInputEv, { bubbles: true })); + } else if (type === 'ChoiceField') { + el._inputNode.dispatchEvent(new Event('change', { bubbles: true })); + } else if (type === 'OptionChoiceField') { + el.dispatchEvent(new Event('click', { bubbles: true })); + } + } + + allFormControls.forEach(controlName => { + it(`lion-${controlName} adds "detail.isTriggeredByUser" to model-value-changed event`, async () => { + const spy = sinon.spy(); + + const tagname = `lion-${controlName}`; + const tag = unsafeStatic(tagname); + let childrenEl; + if (controlName === 'select') { + childrenEl = await fixture( + html``, + ); + } else if (controlName === 'form') { + childrenEl = await fixture(html`
`); + } else if (controlName === 'field') { + childrenEl = await fixture(html``); + } + const el = await fixture(html`<${tag}>${childrenEl}`); + await el.registrationComplete; + el.addEventListener('model-value-changed', spy); + + function expectCorrectEventMetaRegularField(formControl) { + mimicUserInput(formControl, 'userValue', 'RegularField'); + expect(spy.firstCall.args[0].detail.isTriggeredByUser).to.be.true; + // eslint-disable-next-line no-param-reassign + formControl.modelValue = 'programmaticValue'; + expect(spy.secondCall.args[0].detail.isTriggeredByUser).to.be.false; + } + + function resetChoiceFieldToForceRepropagation(formControl) { + // eslint-disable-next-line no-param-reassign + formControl.checked = false; + spy.resetHistory(); + } + + function expectCorrectEventMetaChoiceField(formControl) { + resetChoiceFieldToForceRepropagation(formControl); + mimicUserInput(formControl, 'userValue', 'ChoiceField'); + expect(spy.firstCall.args[0].detail.isTriggeredByUser).to.be.true; + + resetChoiceFieldToForceRepropagation(formControl); + // eslint-disable-next-line no-param-reassign + formControl.checked = true; + expect(spy.firstCall.args[0].detail.isTriggeredByUser).to.be.false; + + // eslint-disable-next-line no-param-reassign + formControl.modelValue = { value: 'programmaticValue', checked: false }; + expect(spy.secondCall.args[0].detail.isTriggeredByUser).to.be.false; + } + + // 1. Derive the type of field we're dealing with + const type = detectType(el); + if (type === 'RegularField') { + expectCorrectEventMetaRegularField(el); + } else if (type === 'ChoiceField' || type === 'OptionChoiceField') { + expectCorrectEventMetaChoiceField(el); + } else if (type === 'ChoiceGroupField') { + let childName = 'option'; + if (controlName.endsWith('-group')) { + [childName] = controlName.split('-group'); + } + const childTagName = `lion-${childName}`; + const childTag = unsafeStatic(childTagName); + const childrenEls = await fixture( + html`
<${childTag}><${childTag}>
`, + ); + el.appendChild(childrenEls); + await el.registrationComplete; + expectCorrectEventMetaChoiceField(el.formElements[0]); + } else if (type === 'FormOrFieldset') { + const childrenEls = await fixture( + html`
`, + ); + el.appendChild(childrenEls); + await el.registrationComplete; + await el.updateComplete; + expectCorrectEventMetaRegularField(el.formElements[0]); + } + }); + }); +}); diff --git a/packages/listbox/src/LionOption.js b/packages/listbox/src/LionOption.js index 4ffa1adb1..8e70dd071 100644 --- a/packages/listbox/src/LionOption.js +++ b/packages/listbox/src/LionOption.js @@ -123,6 +123,7 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi return; } const parentForm = /** @type {unknown} */ (this.__parentFormGroup); + this.__isHandlingUserInput = true; if (parentForm && /** @type {ChoiceGroupHost} */ (parentForm).multipleChoice) { this.checked = !this.checked; this.active = !this.active; @@ -130,5 +131,6 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi this.checked = true; this.active = true; } + this.__isHandlingUserInput = false; } } diff --git a/packages/listbox/src/ListboxMixin.js b/packages/listbox/src/ListboxMixin.js index 3d9860662..0050e725f 100644 --- a/packages/listbox/src/ListboxMixin.js +++ b/packages/listbox/src/ListboxMixin.js @@ -14,6 +14,7 @@ import { LionOptions } from './LionOptions.js'; * @typedef {import('../types/ListboxMixinTypes').ListboxMixin} ListboxMixin * @typedef {import('../types/ListboxMixinTypes').ListboxHost} ListboxHost * @typedef {import('@lion/form-core/types/registration/FormRegistrarPortalMixinTypes').FormRegistrarPortalHost} FormRegistrarPortalHost + * @typedef {import('@lion/form-core/types/FormControlMixinTypes.js').ModelValueEventDetails} ModelValueEventDetails */ function uuid() { @@ -699,7 +700,13 @@ const ListboxMixinImplementation = superclass => // only send model-value-changed if the event is caused by one of its children if (ev.detail && ev.detail.formPath) { this.dispatchEvent( - new CustomEvent('model-value-changed', { detail: { element: ev.target } }), + new CustomEvent('model-value-changed', { + detail: /** @type {ModelValueEventDetails} */ ({ + formPath: ev.detail.formPath, + isTriggeredByUser: ev.detail.isTriggeredByUser, + element: ev.target, + }), + }), ); } this.__oldModelValue = this.modelValue; diff --git a/packages/listbox/test/lion-option.test.js b/packages/listbox/test/lion-option.test.js index 441482682..667a4cd34 100644 --- a/packages/listbox/test/lion-option.test.js +++ b/packages/listbox/test/lion-option.test.js @@ -13,6 +13,38 @@ describe('lion-option', () => { expect(el.modelValue).to.deep.equal({ value: 10, checked: false }); }); + it('fires model-value-changed on click', async () => { + let isTriggeredByUser; + const el = /** @type {LionOption} */ (await fixture(html` + + + `)); + el.dispatchEvent(new CustomEvent('click', { bubbles: true })); + expect(isTriggeredByUser).to.be.true; + }); + + it('fires model-value-changed on programmatic "checked" change', async () => { + let count = 0; + let isTriggeredByUser; + + const el = /** @type {LionOption} */ (await fixture(html` + + + `)); + el.checked = true; + expect(count).to.equal(1); + expect(isTriggeredByUser).to.be.false; + }); + it('can be checked', async () => { const el = /** @type {LionOption} */ (await fixture( html``, From a8cf4215366cf70d0d1c9854b1a924d6e9ba08c6 Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Wed, 6 Jan 2021 00:56:38 +0100 Subject: [PATCH 3/3] chore: add changeset localize and form-control --- .changeset/fast-years-search.md | 7 +++++++ .changeset/slow-forks-speak.md | 9 +++++++++ .../form-core/types/FormControlMixinTypes.d.ts | 14 ++++++++------ .../types/choice-group/ChoiceInputMixinTypes.d.ts | 3 +++ .../docs/17-validation-examples.md | 6 +++++- .../test/model-value-consistency.test.js | 8 ++++---- 6 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 .changeset/fast-years-search.md create mode 100644 .changeset/slow-forks-speak.md diff --git a/.changeset/fast-years-search.md b/.changeset/fast-years-search.md new file mode 100644 index 000000000..cdbe6f16c --- /dev/null +++ b/.changeset/fast-years-search.md @@ -0,0 +1,7 @@ +--- +'@lion/form-integrations': patch +'@lion/localize': patch +--- + +Improved localize DX by making it clear from source code structure what are main (exported) functions and what are util/helper functions consumed by those main functions. +Added Chrome Intl corrections for Philippine currency names and en-GB short month names. diff --git a/.changeset/slow-forks-speak.md b/.changeset/slow-forks-speak.md new file mode 100644 index 000000000..701cbe1e7 --- /dev/null +++ b/.changeset/slow-forks-speak.md @@ -0,0 +1,9 @@ +--- +'@lion/form-core': minor +'@lion/listbox': minor +--- + +Added `isTriggeredByUser` meta data in `model-value-changed` event + +Sometimes it can be helpful to detect whether a value change was caused by a user or via a programmatical change. +This feature acts as a normalization layer: since we use `model-value-changed` as a single source of truth event for all FormControls, there should be no use cases for (inconsistently implemented (cross browser)) events like `input`/`change`/`user-input-changed` etc. diff --git a/packages/form-core/types/FormControlMixinTypes.d.ts b/packages/form-core/types/FormControlMixinTypes.d.ts index 581186cdc..c0b01c739 100644 --- a/packages/form-core/types/FormControlMixinTypes.d.ts +++ b/packages/form-core/types/FormControlMixinTypes.d.ts @@ -6,7 +6,7 @@ import { DisabledHost } from '@lion/core/types/DisabledMixinTypes'; import { LionValidationFeedback } from '../src/validate/LionValidationFeedback'; import { FormRegisteringHost } from './registration/FormRegisteringMixinTypes'; -export type ModelValueEventDetails { +export type ModelValueEventDetails = { /** * A list that represents the path of FormControls the model-value-changed event * 'traveled through'. @@ -15,10 +15,12 @@ export type ModelValueEventDetails { */ formPath: HTMLElement[]; /** - * Whether the model-value-changed event is triggered via user interaction. This information - * can be helpful for both Application Developers and Subclassers. - * This concept is related to the native isTrusted property: - * https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted + * Sometimes it can be helpful to detect whether a value change was caused by a user or + * via a programmatical change. + * This feature acts as a normalization layer: since we use `model-value-changed` as a single + * source of truth event for all FormControls, there should be no use cases for + * (inconsistently implemented (cross browser)) events + * like 'input'/'change'/'user-input-changed' etc.) */ isTriggeredByUser: boolean; /** @@ -27,7 +29,7 @@ export type ModelValueEventDetails { * case `isTriggeredByUser` is true)) */ initialize?: boolean; -} +}; declare interface HTMLElementWithValue extends HTMLElement { value: string; diff --git a/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts b/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts index 622a58e3a..00326c14d 100644 --- a/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts +++ b/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts @@ -35,6 +35,7 @@ export declare class ChoiceInputHost { render(): TemplateResult; _choiceGraphicTemplate(): TemplateResult; + _afterTemplate(): TemplateResult; connectedCallback(): void; disconnectedCallback(): void; @@ -47,6 +48,8 @@ export declare class ChoiceInputHost { __syncCheckedToInputElement(): void; + __isHandlingUserInput: boolean; + _proxyInputEvent(): void; _onModelValueChanged( diff --git a/packages/form-integrations/docs/17-validation-examples.md b/packages/form-integrations/docs/17-validation-examples.md index 1401d614d..92b22ace3 100644 --- a/packages/form-integrations/docs/17-validation-examples.md +++ b/packages/form-integrations/docs/17-validation-examples.md @@ -58,7 +58,11 @@ Useful on input elements it allows to define how many characters can be entered. ```js preview-story export const stringValidators = () => html` - + { * - false: when .modelValue (or checked) set programmatically * * ChoiceGroupField (listbox, select-rich, combobox, radio-group, checkbox-group): - * - true: when child formElement condition for Choice Field is met - * - false: when child formElement condition for Choice Field is not met + * - true: when child formElement condition for ChoiceField(Option) is met + * - false: when child formElement condition for ChoiceField(Option) is not met * * FormOrFieldset (fieldset, form): - * - true: when child formElement condition for Field is met - * - false: when child formElement condition for Field is not met + * - true: when child formElement condition for RegularField is met + * - false: when child formElement condition for RegularField is not met */ const featureDetectChoiceField = el => 'checked' in el && 'choiceValue' in el;