Skip to content

Commit

Permalink
a11y: impress: help screen reader to report shape selection
Browse files Browse the repository at this point in the history
Avoid screen reader to wrongly report text when a shape or image is
selected:
- Got editable area to be made empty when user is not editing text
- Got default for any input to be prevented (except for some special
cases) when user is not editing text so editable area is kept empty

The selection action and the selected object name (e.g. "Rectangle",
"Presentation Title", etc.) are sent to the client.
That allows screen reader to report: "Presentation Title selected" or
"Rectangle unselected", according to the action type.
Selection text content is reported too when available.

Something alike is reported on cell navigation in a spreadsheet.

Signed-off-by: Marco Cecchetti <[email protected]>
Change-Id: I75a8b66ef8cb7b24b28d749f0b24afe2587de45e
  • Loading branch information
mcecchetti committed Oct 26, 2023
1 parent 83fb699 commit afa31b2
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 66 deletions.
74 changes: 63 additions & 11 deletions browser/src/layer/marker/A11yTextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ L.A11yTextInput = L.TextInput.extend({
// Used for signaling when in a mobile device the user tapped the edit button
this._justSwitchedToEditMode = false;

// Used when editing a shape text content.
this._isEditingInSelection = false;
this._hasAnySelection = false;


// In core text selection exists even if it's empty and <backspace> deletes the empty selection
// instead of the previous character.
this._hasSelection = false;
Expand Down Expand Up @@ -200,17 +205,15 @@ L.A11yTextInput = L.TextInput.extend({
},

_updateTable: function(outCount, inList, row, col, rowSpan, colSpan) {
if (this._isDebugOn) {
this._log('_updateTable: '
+ '\n outCount: ' + outCount
+ '\n inList: ' + inList.toString()
+ '\n row: ' + row + ', rowSpan: ' + rowSpan
+ '\n col: ' + col + ', colSpan: ' + colSpan
);
}
this._log('_updateTable: '
+ '\n outCount: ' + outCount
+ '\n inList: ' + inList.toString()
+ '\n row: ' + row + ', rowSpan: ' + rowSpan
+ '\n col: ' + col + ', colSpan: ' + colSpan
);

if (this._timeoutForTableDescription)
clearTimeout(this._timeoutForTableDescription);
if (this._timeoutForA11yDescription)
clearTimeout(this._timeoutForA11yDescription);

var eventDescription = '';
if (outCount > 0 || inList.length > 0) {
Expand Down Expand Up @@ -251,7 +254,7 @@ L.A11yTextInput = L.TextInput.extend({
this._setDescription(eventDescription);

var that = this;
this._timeoutForTableDescription = setTimeout(function() {
this._timeoutForA11yDescription = setTimeout(function() {
that._setDescription('');
}, 1000);
},
Expand All @@ -261,6 +264,55 @@ L.A11yTextInput = L.TextInput.extend({
this._updateTable(outCount, inList, row + 1, col + 1, rowSpan, colSpan);
},

onAccessibilityEditingInSelectionState: function(cell, enabled, selectionDescr, paragraph) {
this._log('onAccessibilityEditingInSelectionState: cell: ' + cell + ', enabled: ' + enabled);
if (!cell) {
this._isEditingInSelection = enabled;
}
if (enabled) {
clearTimeout(this._timeoutForA11yDescription);
var eventDescription = '';
if (typeof selectionDescr === 'string' && selectionDescr.length > 0)
eventDescription += selectionDescr + '. ';
eventDescription += _('Editing activated. ');
if (typeof paragraph === 'string' && paragraph.length > 0)
eventDescription += paragraph;
this._setDescription(eventDescription);
this._timeoutForA11yDescription = setTimeout(function () {
this._setDescription('');
}.bind(this), 1000);
}
},

onAccessibilitySelectionChanged: function(cell, action, name, textContent) {
this._log('onAccessibilitySelectionChanged: cell: ' + cell + ', action: ' + action + ', name: ' + name);
if (this._timeoutForA11yDescription)
clearTimeout(this._timeoutForA11yDescription);
this._emptyArea();
var eventDescription = '';
if (action === 'create' || action === 'add') {
this._hasAnySelection = true;
eventDescription = name + ' ' + _('selected') + '. ';
if (typeof textContent === 'string' && textContent.length > 0) {
eventDescription += (cell ? '' : _('Has text: ')) + textContent;
}
}
else if (action === 'remove') {
this._hasAnySelection = false;
eventDescription = name + ' ' + _('unselected');
}
else if (action === 'delete') {
this._hasAnySelection = false;
eventDescription = name + ' ' + _('deleted');
}
this._setDescription(eventDescription);
if (action !== 'create' && action !== 'add') {
this._timeoutForA11yDescription = setTimeout(function () {
this._setDescription('');
}.bind(this), 1000);
}
},

// Check if a UTF-16 pair represents a Unicode code point
_isSurrogatePair: function(hi, lo) {
return hi >= 0xd800 && hi <= 0xdbff && lo >= 0xdc00 && lo <= 0xdfff;
Expand Down
122 changes: 69 additions & 53 deletions browser/src/layer/marker/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -981,68 +981,84 @@ L.TextInput = L.Layer.extend({
}
}

// In order to allow screen reader to track caret navigation properly even if there is some connection delay
// default behaviour for Left/Right arrow key press is no more prevented in Map.Keyboard._handleKeyEvent,
// Here we set _isLeftRightArrow flag and handle some special cases.
if (this.hasAccessibilitySupport()) {
this._isLeftRightArrow = 0;
if (ev.key === 'ArrowLeft') {
this._isLeftRightArrow = -1;
} else if (ev.key === 'ArrowRight') {
this._isLeftRightArrow = 1;
}

// If we are at paragraph begin/end and left/right arrow is pressed, we need to prevent default behaviour
if (this._isLeftRightArrow) {
clearTimeout(this._onNavigationEndTimeout);
if (!this._isSelectionValid() || this._isComposing ||
(this._isLeftRightArrow > 0 && this._isCursorAtEnd()) ||
(this._isLeftRightArrow < 0 && this._isCursorAtStart())) {
this._log('_onKeyDown: preventDefault');
ev.preventDefault();
if ((this._hasAnySelection && !this._isEditingInSelection && this._map.getDocType() !== 'spreadsheet') ||
(!this._hasAnySelection && this._map.getDocType() === 'presentation')) {
if (!L.browser.cypressTest) {
var allowedKeyEvent =
this._map.keyboard.allowedKeyCodeWhenNotEditing[ev.keyCode] ||
ev.ctrlKey ||
ev.altKey ||
(this._newlineHint && ev.shiftKey);
if (!allowedKeyEvent) {
window.console.log('TextInput._onKeyDown: any input default prevented since no shape editing is active.');
ev.preventDefault();
}
}
}
// In order to allow screen reader to track caret navigation properly even if there is some connection delay
// default behaviour for Left/Right arrow key press is no more prevented in Map.Keyboard._handleKeyEvent,
// Here we set _isLeftRightArrow flag and handle some special cases.
else {
this._isLeftRightArrow = 0;
if (ev.key === 'ArrowLeft') {
this._isLeftRightArrow = -1;
} else if (ev.key === 'ArrowRight') {
this._isLeftRightArrow = 1;
}

if (this._isLeftRightArrow) {
if (this._listPrefixLength > 0) {
var cursorPosition = this._getSelectionEnd();
if (cursorPosition === this._listPrefixLength && this._isLeftRightArrow < 0) {
// if caret is at begin of list entry content: "1. |Item 1" and shift+left arrow is pressed,
// then caret is moved at the end of previous paragraph, if any; or it's not moved at all
// if we are at the document beginning; so we only need to prevent default behaviour
// If we are at paragraph begin/end and left/right arrow is pressed, we need to prevent default behaviour
if (this._isLeftRightArrow) {
clearTimeout(this._onNavigationEndTimeout);
if (!this._isSelectionValid() || this._isComposing ||
(this._isLeftRightArrow > 0 && this._isCursorAtEnd()) ||
(this._isLeftRightArrow < 0 && this._isCursorAtStart())) {
this._log('_onKeyDown: preventDefault');
ev.preventDefault();
if (!ev.shiftKey) {
// when shift is not pressed, caret is moved ahead of list prefix: "|1. Item 1",
// selection is cleared, if any
this._updateCursorPosition(0);
}
}
// if caret is ahead of list prefix: "|1. Item 1" and right arrow is pressed, with or without shift,
// caret is moved at begin of list entry content: "1. |Item 1", nothing is selected
if (cursorPosition === 0 && this._isLeftRightArrow > 0) {
ev.preventDefault();
this._updateCursorPosition(this._listPrefixLength);
}
}

// When left/right arrow is pressed and text is selected, selection is cleared
// and caret needs to be moved by one char left/right.
// However, for an editable div the behaviour is different:
// - when left arrow is pressed caret moves at start of previously selected text
// - when right arrow is pressed caret moves at end of previously selected text
// So we need to prevent default behaviour and simulate the same behaviour that occurs in LibreOffice.
if (!ev.shiftKey) {
var selection = window.getSelection();
if (!selection.isCollapsed) {
// The case where a left arrow is pressed with caret at the beginning of a list entry content
// is already handled earlier.
if (!(this._listPrefixLength > 0 &&
this._lastCursorPosition === this._listPrefixLength && this._isLeftRightArrow < 0)) {
this._log('_onKeyDown: pressed left/right arrows with selected text');
if (this._isLeftRightArrow) {
if (this._listPrefixLength > 0) {
var cursorPosition = this._getSelectionEnd();
if (cursorPosition === this._listPrefixLength && this._isLeftRightArrow < 0) {
// if caret is at begin of list entry content: "1. |Item 1" and shift+left arrow is pressed,
// then caret is moved at the end of previous paragraph, if any; or it's not moved at all
// if we are at the document beginning; so we only need to prevent default behaviour
ev.preventDefault();
if (!ev.shiftKey) {
// when shift is not pressed, caret is moved ahead of list prefix: "|1. Item 1",
// selection is cleared, if any
this._updateCursorPosition(0);
}
}
// if caret is ahead of list prefix: "|1. Item 1" and right arrow is pressed, with or without shift,
// caret is moved at begin of list entry content: "1. |Item 1", nothing is selected
if (cursorPosition === 0 && this._isLeftRightArrow > 0) {
ev.preventDefault();
var pos = this._lastCursorPosition + this._isLeftRightArrow;
// _updateCursorPosition takes care to normalize pos value
this._updateCursorPosition(pos);
this._updateCursorPosition(this._listPrefixLength);
}
}

// When left/right arrow is pressed and text is selected, selection is cleared
// and caret needs to be moved by one char left/right.
// However, for an editable div the behaviour is different:
// - when left arrow is pressed caret moves at start of previously selected text
// - when right arrow is pressed caret moves at end of previously selected text
// So we need to prevent default behaviour and simulate the same behaviour that occurs in LibreOffice.
if (!ev.shiftKey) {
var selection = window.getSelection();
if (!selection.isCollapsed) {
// The case where a left arrow is pressed with caret at the beginning of a list entry content
// is already handled earlier.
if (!(this._listPrefixLength > 0 &&
this._lastCursorPosition === this._listPrefixLength && this._isLeftRightArrow < 0)) {
this._log('_onKeyDown: pressed left/right arrows with selected text');
ev.preventDefault();
var pos = this._lastCursorPosition + this._isLeftRightArrow;
// _updateCursorPosition takes care to normalize pos value
this._updateCursorPosition(pos);
}
}
}
}
Expand Down
16 changes: 14 additions & 2 deletions browser/src/layer/tile/CanvasTileLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1887,11 +1887,23 @@ L.CanvasTileLayer = L.Layer.extend({
var col = parseInt(obj.col);
var rowSpan = obj.rowSpan !== undefined ? parseInt(obj.rowSpan) : 1;
var colSpan = obj.colSpan !== undefined ? parseInt(obj.colSpan) : 1;
this._map._textInput.onAccessibilityFocusedCellChanged(outCount, inList, row, col, rowSpan, colSpan, obj.paragraph);
this._map._textInput.onAccessibilityFocusedCellChanged(
outCount, inList, row, col, rowSpan, colSpan, obj.paragraph);
}
else if (textMsg.startsWith('a11yeditinginselectionstate:')) {
obj = JSON.parse(textMsg.substring('a11yeditinginselectionstate:'.length + 1));
this._map._textInput.onAccessibilityEditingInSelectionState(
parseInt(obj.cell) > 0, parseInt(obj.enabled) > 0, obj.selection, obj.paragraph);
}
else if (textMsg.startsWith('a11yselectionchanged:')) {
obj = JSON.parse(textMsg.substring('a11yselectionchanged:'.length + 1));
this._map._textInput.onAccessibilitySelectionChanged(
parseInt(obj.cell) > 0, obj.action, obj.name, obj.text);
}
else if (textMsg.startsWith('a11yfocusedparagraph:')) {
obj = JSON.parse(textMsg.substring('a11yfocusedparagraph:'.length + 1));
this._map._textInput.setA11yFocusedParagraph(obj.content, parseInt(obj.position), parseInt(obj.start), parseInt(obj.end));
this._map._textInput.setA11yFocusedParagraph(
obj.content, parseInt(obj.position), parseInt(obj.start), parseInt(obj.end));
}
else if (textMsg.startsWith('a11ycaretposition:')) {
var pos = textMsg.substring('a11ycaretposition:'.length + 1);
Expand Down
33 changes: 33 additions & 0 deletions browser/src/map/handler/Map.Keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,39 @@ L.Map.Keyboard = L.Handler.extend({
zoomOut: [189, 109, 173]
},

allowedKeyCodeWhenNotEditing: {
27: true, // ESC
33: true, // pageUp
34: true, // pageDown
13: true, // enter
8: true, // BACKSPACE
16: true, // SHIFT
17: true, // CTRL
18: true, // ALT
19: true, // PAUSE
20: true, // CAPSLOCK
35: true, // END
36: true, // HOME
45: true, // INSERT
46: true, // DELETE
91: true, // LEFTWINDOWKEY
92: true, // RIGHTWINDOWKEY
112: true, //F1
113: true, //F2
114: true, //F3
115: true, //F4
116: true, //F5
117: true, //F6
118: true, //F7
119: true, //F8
120: true, //F9
121: true, //F10
122: true, //F11
123: true, //F12
144: true, //NUMLOCK
145: true, //SCROLLLOCK
},

initialize: function (map) {
this._map = map;
this._setPanOffset(map.options.keyboardPanOffset);
Expand Down
10 changes: 10 additions & 0 deletions kit/ChildSession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3159,6 +3159,16 @@ void ChildSession::loKitCallback(const int type, const std::string& payload)
case LOK_CALLBACK_COLOR_PALETTES:
sendTextFrame("colorpalettes: " + payload);
break;
case LOK_CALLBACK_A11Y_EDITING_IN_SELECTION_STATE:
{
sendTextFrame("a11yeditinginselectionstate: " + payload);
break;
}
case LOK_CALLBACK_A11Y_SELECTION_CHANGED:
{
sendTextFrame("a11yselectionchanged: " + payload);
break;
}
default:
LOG_ERR("Unknown callback event (" << lokCallbackTypeToString(type) << "): " << payload);
}
Expand Down

0 comments on commit afa31b2

Please sign in to comment.