NEW250380
AX: Voiceover incorrectly changes value of input type="text" with role="spinbutton" when swipe up/swipe down gesture used, value change announces numeric value inserted between percentage value and percent itself
https://bugs.webkit.org/show_bug.cgi?id=250380
Summary AX: Voiceover incorrectly changes value of input type="text" with role="spinb...
jartik
Reported 2023-01-10 06:41:49 PST
Created attachment 464441 [details] Sample code Steps to Reproduce: 1). Please use the following code snippet: ============= Styles ============= h2 { margin-top: 0; } .spinbutton { display: flex; flex-direction: row; align-items: center; } .spinbutton button { width: 48px; height: 48px; cursor: pointer; } [role="spinbutton"] { width: 60px; text-align: center; } input[role="spinbutton"] { margin: 0 5px; } ============= END of Styles ============= ============= Code ============= <h2>Role "spinbutton" on div</h2> <div class="spinbutton"> <button class="decrease" type="button" tabindex="-1" aria-hidden>-</button> <div role="spinbutton" tabindex="0" aria-valuenow="1" aria-valuetext="1" aria-valuemin="0" aria-valuemax="200" aria-label="Quantity div spinbutton">1</div> <button class="increase" type="button" tabindex="-1">+</button> </div> <hr /> <h2>Role "spinbutton" on input type text</h2> <div class="spinbutton"> <button class="decrease" type="button" class="decrease" tabindex="-1" aria-hidden>-</button> <input type="text" role="spinbutton" aria-valuenow="1" aria-valuetext="1" aria-valuemin="0" aria-valuemax="200" value="1" aria-label="Quantity input type text spinbutton" /> <button class="increase" type="button" tabindex="-1">+</button> </div> <hr /> <h2>Role "spinbutton" on input type number</h2> <div class="spinbutton"> <button class="decrease" type="button" class="decrease" tabindex="-1" aria-hidden>-</button> <input type="number" step="1" min="0" max="200" role="spinbutton" aria-valuenow="1" aria-valuetext="1" aria-valuemin="0" aria-valuemax="200" value="1" aria-label="Quantity input type number spinbutton" /> <button class="increase" type="button" tabindex="-1">+</button> </div> <script> window.addEventListener('load', _ => { const containers = document.querySelectorAll('.spinbutton'); [...containers].forEach(c => { const spinButton = c.querySelector('[role="spinbutton"]'); const decreaseBtn = c.querySelector('.decrease'); const increaseBtn = c.querySelector('.increase'); const minValue = parseInt(spinButton.getAttribute('aria-valuemin'), 10); const maxValue = parseInt(spinButton.getAttribute('aria-valuemax'), 10); decreaseBtn.addEventListener('click', _ => decreaseValue(spinButton, minValue)); increaseBtn.addEventListener('click', _ => increaseValue(spinButton, maxValue)); spinButton.addEventListener('keydown', e => { console.log('::Keydown event:: key:', e.key); if (isInput(e.currentTarget) && e.currentTarget.type === 'number') return; if (e.key === 'ArrowUp') { increaseValue(e.currentTarget, maxValue); } else if (e.key === 'ArrowDown') { decreaseValue(e.currentTarget, minValue); } }); if (isInput(spinButton)) { spinButton.addEventListener('input', e => { console.log('::Input event:: inputType:', e.inputType); console.log('::Input event:: data:', e.data); console.log('::Input event:: value:', e.currentTarget.value); }); spinButton.addEventListener('change', e => { console.log('::Change event:: value:', e.currentTarget.value); e.currentTarget.setAttribute('aria-valuenow', e.currentTarget.value); e.currentTarget.setAttribute('aria-valuetext', e.currentTarget.value); const value = parseInt(e.currentTarget.value, 10); if (isNaN(value) || value < minValue || value > maxValue) { e.currentTarget.setAttribute('aria-invalid', 'true'); } else { e.currentTarget.removeAttribute('aria-invalid'); } }); } }); }); function getCurrentValue(spinButton) { return parseInt(spinButton.getAttribute('aria-valuenow'), 10) } function increaseValue(spinButton, maxValue) { const currentValue = getCurrentValue(spinButton); if (isNaN(currentValue)) { setNewValue(spinButton, 0); return; } const newValue = currentValue + 1; if (newValue > maxValue) return; setNewValue(spinButton, newValue); } function decreaseValue(spinButton, minValue) { const currentValue = getCurrentValue(spinButton); if (isNaN(currentValue)) { setNewValue(spinButton, 0); return; } const newValue = currentValue - 1; if (newValue < minValue) return; setNewValue(spinButton, newValue); } function setNewValue(spinButton, value) { spinButton.setAttribute('aria-valuenow', value); spinButton.setAttribute('aria-valuetext', value); if (isInput(spinButton)) { spinButton.removeAttribute('aria-invalid'); spinButton.value = value; } else { spinButton.innerHTML = value; } } function isInput(spinButton) { return spinButton.tagName === 'INPUT'; } </script> ============= END of Code ============= 2). Open page with provided code snippet on iPhone/iPad with Voiceover enabled. Swipe left/right or tap directly on input type="text" with role="spinbutton". Try to change value by swiping up and/or down. Actual Results: Instead of "keydown" event two input events are fired - first one with inputType of "deleteContent" which cleans input value, second one of "insertText" which adds text with value which seems like equal to 5% of the range between aria-valuemin and aria-valuemax values (this was determined by number of tests with different min/max values) is been added or substracted from the original value depending on operation type triggered (increment or decrement). Swiping up/down continuously changes applies added or substracted from the original value, change is applied only after input loses focus. On value change it seems like selected value as percentage from the range is been announced, mostly with some number, source of which I can not determine (but is seems like it should be somehow relative to the value change step), announced between percentage value and percentage itself. Expected Results: Keyboard "keydown" event is fired with "ArrowUp" as a key for increment and "ArrowDown" for decrement AT (assistive technology) event. Value is changed as per code in "keydown" event listener. Build Date & Hardware: iPhone 11, iOS 16.2 Additional Information: As per https://github.com/WICG/aom/blob/gh-pages/explainer.md#user-action-events-from-assistive-technology increment and decrement AT event should trigger Keyboard events simulating pressing of "ArrowUp" and "ArrowDown" keys respectively. This works for general "div" element with role="spinbutton" (present in the provided code snippet) - value changes correctly and been announced, but after that again percentage value, undetermined numeric value and 'percentage' word are announced just as in 'Actual Results' section. "keydown" listener does not run at all on input type="text", value change step as was mentioned seems like equal to 5% of the range between aria-valuemin and aria-valuemax values, but it is totally unclear why and if it is always has this value. If role="spinbutton" added to input type="number" with step attribute present, step value is been honored, but still "keydown" listener is not fired and general behavior is the same as for input type="text". There is also a bug https://bugs.webkit.org/show_bug.cgi?id=221102 already filled for percentage announcement which is still reproduced in iOS 16.2 (original bug was reported for iOS 14).
Attachments
Sample code (5.17 KB, text/html)
2023-01-10 06:41 PST, jartik
no flags
Radar WebKit Bug Importer
Comment 1 2023-01-10 06:42:01 PST
jeanne.waldman
Comment 2 2024-02-12 13:11:31 PST
I am seeing the same when I use contenteditable='true' on a div with role='spinbutton'. You can see that here in the codepen in the day field (modified from https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/examples/datepicker-spinbuttons/) https://codepen.io/boxergirl/pen/JjzeENM
jeanne.waldman
Comment 3 2024-12-19 10:51:21 PST
I have another codepen that shows this working on a div, but not on an input. https://codepen.io/Jeanne-Waldman/pen/ByBpYxj
Note You need to log in before you can comment on or make changes to this bug.