Skip to content
This repository has been archived by the owner on Nov 29, 2023. It is now read-only.

Commit

Permalink
Fix: localized numeral entry by keyboard in number input widgets (#973)
Browse files Browse the repository at this point in the history
  • Loading branch information
eyelidlessness authored May 6, 2023
1 parent c91625d commit 2330399
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 56 deletions.
30 changes: 15 additions & 15 deletions src/widget/number-input/decimal-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,28 @@ const getDecimalCharacters = (languages) => {
return characters;
};

/** @type {Map<string, RegExp>} */
let characterPatternsByLanguage = new Map();
/** @type {Map<string, Set<string>>} */
let validCharactersByLanguage = new Map();

/**
* @param {string[]} languages
*/
const getCharacterPattern = (languages) => {
const getValidCharacters = (languages) => {
const [language] = languages;
let characterPattern = characterPatternsByLanguage.get(language);
let validCharacters = validCharactersByLanguage.get(language);

if (characterPattern != null) {
return characterPattern;
if (validCharacters != null) {
return validCharacters;
}

const decimalCharacters = getDecimalCharacters(language);
const locales = Intl.getCanonicalLocales(languages);
const formatter = Intl.NumberFormat(locales);
const number = -9.012345678;

characterPattern = new RegExp(
`[-0-9${Array.from(decimalCharacters).join('')}]`
);
characterPatternsByLanguage.set(language, characterPattern);
validCharacters = new Set(`${number}${formatter.format(number)}`.split(''));
validCharactersByLanguage.set(language, validCharacters);

return characterPattern;
return validCharacters;
};

/** @type {Map<string, RegExp>} */
Expand Down Expand Up @@ -92,7 +92,7 @@ export default class DecimalInput extends NumberInput {
*/
static globalReset(form, rootElement) {
decimalCharactersByLanguage = new Map();
characterPatternsByLanguage = new Map();
validCharactersByLanguage = new Map();
validityPatternsByLanguage = new Map();
super.globalReset(form, rootElement);
}
Expand All @@ -103,8 +103,8 @@ export default class DecimalInput extends NumberInput {
return getDecimalCharacters(this.languages);
}

static get characterPattern() {
return getCharacterPattern(this.languages);
static get validCharacters() {
return getValidCharacters(this.languages);
}

static get pattern() {
Expand Down
39 changes: 37 additions & 2 deletions src/widget/number-input/integer-input.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,53 @@
import NumberInput from './number-input';

/** @type {Map<string, Set<string>>} */
let validCharactersByLanguage = new Map();

/**
* @param {string[]} languages
*/
const getValidCharacters = (languages) => {
const [language] = languages;
let validCharacters = validCharactersByLanguage.get(language);

if (validCharacters != null) {
return validCharacters;
}

const locales = Intl.getCanonicalLocales(languages);
const formatter = Intl.NumberFormat(locales);
const number = -9012345678;

validCharacters = new Set(`${number}${formatter.format(number)}`.split(''));
validCharactersByLanguage.set(language, validCharacters);

return validCharacters;
};

export default class IntegerInput extends NumberInput {
/**
* @param {import('./form').Form} form
* @param {import('../../js/form').Form} form
* @param {HTMLFormElement} rootElement
*/
static globalInit(form, rootElement) {
this.languageChanged = this.languageChanged.bind(this);
super.globalInit(form, rootElement);
}

/**
* @param {import('./form').Form} form
* @param {HTMLFormElement} rootElement
*/
static globalReset(form, rootElement) {
validCharactersByLanguage = new Map();
super.globalReset(form, rootElement);
}

static selector = '.question input[type="number"][data-type-xml="int"]';

static characterPattern = /[-0-9]/;
static get validCharacters() {
return getValidCharacters(this.languages);
}

static pattern = /^-?[0-9]+$/;

Expand Down
6 changes: 3 additions & 3 deletions src/widget/number-input/number-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,9 @@ class NumberInput extends Widget {

/**
* @abstract
* @type {RegExp}
* @type {Set<string>}
*/
static get characterPattern() {
static get validCharacters() {
throw new Error('Not implemented');
}

Expand Down Expand Up @@ -210,7 +210,7 @@ class NumberInput extends Widget {
metaKey ||
(key.length > 1 && key !== 'Spacebar') ||
(!isComposing &&
this.constructor.characterPattern.test(event.key))
this.constructor.validCharacters.has(event.key))
) {
return true;
}
Expand Down
137 changes: 101 additions & 36 deletions test/spec/widget.number-input.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ describe('Number inputs', () => {
/** @type {Form} */
let form;

/** @type {HTMLElement | null} */
let formHeader;

/** @type {HTMLSelectElement | null} */
let languageSelect;

/** @type {HTMLFormElement} */
let formElement;

Expand All @@ -38,6 +44,8 @@ describe('Number inputs', () => {
});

beforeEach(() => {
formHeader = null;
languageSelect = null;
decimalInput = null;
integerInput = null;

Expand Down Expand Up @@ -65,6 +73,7 @@ describe('Number inputs', () => {
});

afterEach(() => {
formHeader?.remove();
formElement.remove();
sandbox.restore();
form.resetView();
Expand Down Expand Up @@ -371,6 +380,96 @@ describe('Number inputs', () => {
expect(question.classList.contains('invalid-value')).to.equal(true);
});

/**
* @param {string[]} languages
*/
const setLanguageOptions = (languages) => {
const languageOptions = languages.map((language) => {
const option = document.createElement('option');

option.value = language;

return option;
});

formHeader = document.createElement('header');

formHeader.classList.add('form-header');

const languageSelectContainer = document.createElement('span');

languageSelectContainer.classList.add('form-language-selector');
formHeader.append(languageSelectContainer);
formElement.insertAdjacentElement('beforebegin', formHeader);

languageSelect = document.createElement('select');
languageSelect.id = 'form-languages';
languageSelectContainer.append(languageSelect);
languageSelect.append(...languageOptions);
formElement.append(languageSelect);
};

const isLocalizedNumeralInputSupported = () => {
const label = document.createElement('label');
const input = document.createElement('input');

label.append(input);

label.lang = 'ar-EG';
input.type = 'number';
input.value = '١';

return input.value !== '' && input.checkValidity();
};

if (isLocalizedNumeralInputSupported()) {
describe('localized integer input', () => {
before(() => {
preInit = false;
});

beforeEach(() => {
setLanguageOptions(['ar-EG']);
form.init();
formElement.dispatchEvent(events.ChangeLanguage());
});

it('does not prevent entering localized numerals in an integer input by keyboard', () => {
const control = formElement.querySelector(
IntegerInput.selector
);
const event = new KeyboardEvent('keydown', {
key: '١',
});
const preventDefaultStub = sandbox.stub(
event,
'preventDefault'
);

control.dispatchEvent(event);

expect(preventDefaultStub).not.to.have.been.called;
});

it('does not prevent entering localized numerals in a decimal input by keyboard', () => {
const control = formElement.querySelector(
IntegerInput.selector
);
const event = new KeyboardEvent('keydown', {
key: '٢',
});
const preventDefaultStub = sandbox.stub(
event,
'preventDefault'
);

control.dispatchEvent(event);

expect(preventDefaultStub).not.to.have.been.called;
});
});
}

const isLocalizedDecimalInputSupported = () => {
const label = document.createElement('label');
const input = document.createElement('input');
Expand Down Expand Up @@ -441,51 +540,17 @@ describe('Number inputs', () => {
'zh-HK',
];

/** @type {HTMLElement} */
let formHeader;

/** @type {HTMLSelectElement} */
let languageSelect;

before(() => {
preInit = false;
});

beforeEach(() => {
formElement = form.view.html;

const languageOptions = [
setLanguageOptions([
unsupportedLanguage,
...supportedLanguages,
].map((language) => {
const option = document.createElement('option');

option.value = language;

return option;
});

formHeader = document.createElement('header');

formHeader.classList.add('form-header');

const languageSelectContainer =
document.createElement('span');

languageSelectContainer.classList.add(
'form-language-selector'
);
formHeader.append(languageSelectContainer);
formElement.insertAdjacentElement(
'beforebegin',
formHeader
);

languageSelect = document.createElement('select');
languageSelect.id = 'form-languages';
languageSelectContainer.append(languageSelect);
languageSelect.append(...languageOptions);
formElement.append(languageSelect);
]);
});

afterEach(() => {
Expand Down

0 comments on commit 2330399

Please sign in to comment.