Visitar URL original
fix(input-mask): keep the caret in place while formatting by Raydman · Pull Request #6977 · uswds/uswds · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion packages/usa-input-mask/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,46 @@ const handleCurrentValue = (el) => {
return newValue;
};

// maps the caret to the same position among accepted characters in the new
// value. A caret that sat after a separator stays after the separator.
const getCaretPosition = (el, newValue) => {
const isCharsetPresent = el.dataset.charset;
const isAccepted = (char) => strippedValue(isCharsetPresent, char) !== "";
const before = el.value.slice(0, el.selectionStart);
const count = strippedValue(isCharsetPresent, before).length;
const afterSeparator = before !== "" && !isAccepted(before.slice(-1));
let position = 0;
let seen = 0;

while (seen < count && position < newValue.length) {
if (isAccepted(newValue[position])) {
seen += 1;
}
position += 1;
}

if (afterSeparator) {
while (position < newValue.length && !isAccepted(newValue[position])) {
position += 1;
}
}

return position;
};

const handleValueChange = (el) => {
const inputEl = el;
const id = inputEl.getAttribute("id");
inputEl.value = handleCurrentValue(inputEl);
const newValue = handleCurrentValue(inputEl);

if (newValue !== inputEl.value) {
const hasSelection = inputEl.selectionStart !== null;
const position = hasSelection ? getCaretPosition(inputEl, newValue) : 0;
inputEl.value = newValue;
if (hasSelection && document.activeElement === inputEl) {
inputEl.setSelectionRange(position, position);
}
}

const maskVal = setValueOfMask(el);
const maskEl = document.getElementById(`${id}Mask`);
Expand Down
28 changes: 28 additions & 0 deletions packages/usa-input-mask/src/test/input-mask-alphanumeric.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,31 @@ tests.forEach(({ name, selector: containerSelector }) => {
});
});
});

describe("alphanumeric input mask caret position", () => {
const { body } = document;

let input;

beforeEach(() => {
body.innerHTML = TEMPLATE;
InputMask.on(document.body);
input = document.querySelector(".usa-masked");
});

afterEach(() => {
InputMask.off(document.body);
body.textContent = "";
});

it("keeps the caret after a character typed over a selection", () => {
// "A1B [2]C3" then "5"
input.focus();
input.value = "A1B 5C3";
input.setSelectionRange(5, 5);
EVENTS.input(input);

assert.strictEqual(input.value, "A1B 5C3");
assert.strictEqual(input.selectionStart, 5);
});
});
97 changes: 97 additions & 0 deletions packages/usa-input-mask/src/test/input-mask.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,100 @@ tests.forEach(({ name, selector: containerSelector }) => {
});
});
});

/**
* put the input in the state the browser leaves it in after an edit and
* before keyup fires
*/
const edit = (input, value, caret) => {
input.focus();
input.value = value;
input.setSelectionRange(caret, caret);
EVENTS.input(input);
};

describe("input mask caret position", () => {
const { body } = document;

let input;

beforeEach(() => {
body.innerHTML = TEMPLATE;
InputMask.on(document.body);
input = document.querySelector(".usa-masked");
});

afterEach(() => {
InputMask.off(document.body);
body.textContent = "";
});

it("keeps the caret in place after deleting a digit in the middle", () => {
// "123 4|5 6789" then Backspace
edit(input, "123 5 6789", 4);

assert.strictEqual(input.value, "123 56 789");
assert.strictEqual(input.selectionStart, 4);
});

it("keeps the caret after a digit typed in the middle", () => {
// "123 |45 678" then "9"
edit(input, "123 945 678", 5);

assert.strictEqual(input.value, "123 94 5678");
assert.strictEqual(input.selectionStart, 5);
});

it("keeps the caret after a digit typed over a selection", () => {
// "123 [45] 6789" then "9"
edit(input, "123 9 6789", 5);

assert.strictEqual(input.value, "123 96 789");
assert.strictEqual(input.selectionStart, 5);
});

it("moves the caret before a separator deleted with Backspace", () => {
// "123 |45 6789" then Backspace
edit(input, "12345 6789", 3);

assert.strictEqual(input.value, "123 45 6789");
assert.strictEqual(input.selectionStart, 3);
});

it("keeps the caret in place when a character is rejected", () => {
// "123 |45 6789" then "a"
edit(input, "123 a45 6789", 5);

assert.strictEqual(input.value, "123 45 6789");
assert.strictEqual(input.selectionStart, 4);
});

it("places the caret after a separator inserted at the end", () => {
// "123 45|" then "6"
edit(input, "123 456", 7);

assert.strictEqual(input.value, "123 45 6");
assert.strictEqual(input.selectionStart, 8);
});

it("leaves the caret alone when the value does not change", () => {
// "123 |45 6789" then an arrow key
edit(input, "123 45 6789", 4);

assert.strictEqual(input.value, "123 45 6789");
assert.strictEqual(input.selectionStart, 4);
});

it("does not steal focus from another element", () => {
const other = document.createElement("input");
body.appendChild(other);
other.focus();

input.value = "123 5 6789";
input.setSelectionRange(4, 4);
EVENTS.input(input);

assert.strictEqual(input.value, "123 56 789");
assert.strictEqual(document.activeElement, other);
});
});
2 changes: 1 addition & 1 deletion packages/usa-input-mask/src/usa-input-mask.twig
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
type="{{ type }}"
inputmode="{{ inputmode }}"
name="{{ id }}"
placeholder="{{ placeholder }}"
data-placeholder="{{ placeholder }}"
pattern="{{ pattern }}"
class="usa-input usa-masked"
aria-describedby="{{ id }}Hint"
Expand Down
Loading