Commit 25082988 authored by Cecilia Vela Gurovic's avatar Cecilia Vela Gurovic Committed by Robert Lyon

Bug 1813987: Accessibility settings and layout

- Added an option to set the profile to accessible.
This can be set in account preferences and
enables one extra field in the settings
of a view, in the basic settings section, to make
the layout accessible.

- by default, if the user account is accessible then
the pages will be crated as accessible.
To have a non accessible page, the option un view settings
needs to be set as 'No'

- Accessible layout only allows to add blocks with width=12
that is the same with of the grid.
This makes the blocks show as a sequence one on top of the other
like a list. The user can only reorder the position
they have in the list.

- When a page is accessible, the adding a block
by drag & drop is disabled

- Reordering of blocks is accessible by keyborad

- The 'add block' button is reachable by keyboard

- add an intro to the page for the screen reader
to explain how the page works

- add identification to each block for screen reader,
blocktype and title (if it’s set)

TODO
* When adding a block to the bottom or the top of the page,
the page should be scrolled to that block

* Floating menu: should have a way to be recognized as a menu
by the screen reader. Check the access keys for the menu.

behatnotneeded

Change-Id: I08417f0f11d747a67900c88c2f675ef5f85b7499
parent 69916a3d
/*
* Creates the new dragon drop object and sets the events to move the blocks
* up and down the grid, and to include messages for the screen reader
*/
function accessibilityReorder() {
var list = $('.grid-stack')[0];
// creating dragon drop object and setting the screen reader event handlers
window.dragonDrop = new DragonDrop(list, {
item: '.grid-stack-item',
handle: '.access-drop-handle',
announcement: {
grabbed: function(el) {
var title = $(el).find('h3 .blockinstance-header')[0].innerText;
return get_string('itemgrabbed', 'view', title);
},
dropped: function(el) {
var title = $(el).find('h3 .blockinstance-header')[0].innerText;
return get_string('itemdropped', 'view', title);
},
reorder: function(el, items) {
const pos = items.indexOf(el) + 1;
var title = $(el).find('h3 .blockinstance-header')[0].innerText;
return get_string('itemreorder', 'view', title, pos, items.length);
},
cancel: get_string('reordercancelled', 'view'),
}
});
// setting event handlers to update gridstack values and save them on the db
window.dragonDrop.on('grabbed', function (container, item) {
var title = $(item).find('h3 .blockinstance-header')[0].innerText;
console.log(get_string('itemgrabbed', 'view', title));
})
.on('dropped', function (container, item) {
var title = $(item).find('h3 .blockinstance-header')[0].innerText;
console.log(get_string('itemdropped', 'view', title));
})
.on('reorder', function (container, item) {
// dragon drop will swap the nodes in the DOM,
// but we still need to update the gridstack values
var newpos = this.items.indexOf(item);
var prevEl, nextEl, prevY, nextY, itemY, prevHeight;
prevEl = this.items[newpos - 1];
nextEl = this.items[newpos +1];
itemY = item.getAttribute('data-gs-y');
if (typeof(prevEl) != 'undefined' || typeof(nextEl) !== 'undefined') {
// we have at least one more element in the list
if (typeof(prevEl) === 'undefined') {
// moving element up the layout, to the first position
nextY = nextEl.getAttribute('data-gs-y');
if (+itemY > +nextY) {
swapBlocks(item, nextEl, nextY);
}
}
else if (typeof(nextEl) === 'undefined') {
// moving the element down in the layout, to the last position
swapBlocks(prevEl, item, itemY);
}
else {
prevY = prevEl.getAttribute('data-gs-y');
if (+prevY > +itemY) {
// moving the element down in the layout
swapBlocks(prevEl, item, itemY);
}
else {
// moving the element up in the layout
nextY = nextEl.getAttribute('data-gs-y');
if (+itemY > +nextY) {
swapBlocks(item, nextEl, nextY);
}
}
}
}
});
}
/*
* Updates the gridstack dimensions in the DOM elements and saves the new dimensions to the DB
* @param topBlock node that will be on top of the other block after the swap
* @param bottomBlock node that will be below the other block after the swap
* @param topBlockNewY int is the new gridstack value y for the block that will be on the top
*/
function swapBlocks(topBlock, bottomBlock, topBlockNewY) {
var topHeight, bottomY;
topHeight = topBlock.getAttribute('data-gs-height');
var bottomY = +topBlockNewY + +topHeight;
$('.grid-stack').data('gridstack').move(topBlock, +topBlock.getAttribute('data-gs-x'), +topBlockNewY);
$('.grid-stack').data('gridstack').move(bottomBlock, +bottomBlock.getAttribute('data-gs-x'), bottomY);
// save to DB new dimension values
var id = topBlock.getAttribute('data-gs-id'),
dimensions = {
newx: "0",
newy: topBlock.getAttribute('data-gs-y'),
newwidth: "12",
newheight: topBlock.getAttribute('data-gs-height'),
}
moveBlock(id, dimensions);
id = bottomBlock.getAttribute('data-gs-id');
dimensions = {
newx: "0",
newy: bottomBlock.getAttribute('data-gs-y'),
newwidth: "12",
newheight: bottomBlock.getAttribute('data-gs-height'),
}
moveBlock(id, dimensions);
}
dragon-drop
-----
Website: https://github.com/schne324/dragon-drop
Version: 3.2.1
Modifications:
- none
This diff is collapsed.
......@@ -39,12 +39,12 @@ function loadGridTranslate(grid, blocks) {
}
function loadGrid(grid, blocks) {
var minWidth = grid.opts.minCellColumns;
$.each(blocks, function(index, block) {
var blockContent = $('<div id="block_' + block.id + '"><div class="grid-stack-item-content">'
+ block.content +
'<div/><div/>');
addNewWidget(blockContent, block.id, block, grid, block.class);
addNewWidget(blockContent, block.id, block, grid, block.class, minWidth);
});
jQuery(document).trigger('blocksloaded');
......@@ -127,33 +127,35 @@ function updateTranslatedGridRows(blocks) {
function updateBlockSizes() {
$.each($('.grid-stack').children(), function(index, element) {
if (!$(element).hasClass('staticblock')) {
$('.grid-stack').data('gridstack').resize(
$('.grid-stack-item')[index],
$($('.grid-stack-item')[index]).attr('data-gs-width'),
Math.ceil(
(
$('.grid-stack-item-content')[index].scrollHeight +
$('.grid-stack').data('gridstack').opts.verticalMargin
)
/
(
$('.grid-stack').data('gridstack').cellHeight() +
$('.grid-stack').data('gridstack').opts.verticalMargin
)
)
var width = $($('.grid-stack-item')[index]).attr('data-gs-width'),
prevHeight = $($('.grid-stack-item')[index]).attr('data-gs-height'),
height = Math.ceil(
(
$('.grid-stack-item-content')[index].scrollHeight +
$('.grid-stack').data('gridstack').opts.verticalMargin
)
/
(
$('.grid-stack').data('gridstack').cellHeight() +
$('.grid-stack').data('gridstack').opts.verticalMargin
)
);
//height = height.toString();
if (+prevHeight != height) {
$('.grid-stack').data('gridstack').resize($('.grid-stack-item')[index], +width, height);
}
}
});
}
function addNewWidget(blockContent, blockId, dimensions, grid, blocktypeclass) {
function addNewWidget(blockContent, blockId, dimensions, grid, blocktypeclass, minWidth=null) {
el = grid.addWidget(
blockContent,
dimensions.positionx,
dimensions.positiony,
dimensions.width,
dimensions.height,
null, null, null, null, null,
null, minWidth, null, null, null,
blockId
);
......
......@@ -120,6 +120,10 @@
else {
return newblock;
}
if (typeof(window.dragonDrop) != 'undefined') {
var list = $('.grid-stack')[0];
window.dragonDrop.initElements(list);
}
};
/**
......@@ -419,7 +423,7 @@
function makeNewBlocksDraggable() {
$('.blocktype-drag').draggable({
$('.blocktype-drag.not-accessible').draggable({
start: function(event, ui) {
$(this).attr('data-gs-width', 4);
$(this).attr('data-gs-height', 3);
......@@ -528,10 +532,11 @@
addBlockCss(data.css);
var grid = $('.grid-stack').data('gridstack');
var grid = $('.grid-stack').data('gridstack'),
minWidth = grid.opts.minCellColumns;
dimensions.width = 4;
dimensions.height = 3;
addNewWidget(blockinstance, blockId, dimensions, grid, null);
addNewWidget(blockinstance, blockId, dimensions, grid, 'placeholder', minWidth);
if (data.data.configure) {
showDock($('#configureblock'), true);
......@@ -541,6 +546,16 @@
rewriteDeleteButton(blockinstance.find('.deletebutton'));
blockinstance.find('.deletebutton').trigger("focus");
}
if (typeof(window.dragonDrop) != 'undefined') {
var list = $('.grid-stack')[0];
if (whereTo == 'top') {
// new block will show on top of the page but it's still as the last child in the DOM
// need to place it first of the list before dragon drop reset
var children = list.children;
var length = children.length;
list.insertBefore(children[length-1], children[0]);
}
}
},
function() {
// On error callback we need to reset the Dock
......@@ -635,6 +650,11 @@
});
self.prop('disabled', false);
if (typeof(window.dragonDrop) != 'undefined') {
var list = $('.grid-stack')[0];
window.dragonDrop.initElements(list);
}
}, function() {
......
......@@ -62,6 +62,9 @@ $string['showhomeinfodescription1'] = 'Display information about how to use %s o
$string['showlayouttranslatewarning'] = 'Confirm before changing pages layout';
$string['showlayouttranslatewarningdescription'] = 'Display a warning and request confirmation before changing the layout of a page to the new layout when editing the page';
$string['accessibilityprofile'] = 'Accessible profile';
$string['accessibilityprofiledescription'] = 'Enable accessibility options in your account';
$string['showprogressbar'] = 'Profile completion progress bar';
$string['showprogressbardescription'] = 'Display progress bar and tips on how to complete your %s profile.';
......
......@@ -523,3 +523,13 @@ $string['pleaseconfirmtranslate'] = 'Transform page layout';
$string['confirmtranslationmessage'] = 'As part of Mahara 19.10 we introduced a new way to create pages layout. To be able to edit this page we\'ll need to transform the old layout to the new grid layout.
If you wish to transform this page alone, click \'Accept\'. To transform all pages that you edit and not see this message again, click \'Accept and remember\', this option can be changed in <a href="%s">Settings</a>. To go back to the page click on \'Cancel\'.
';
$string['accessibleview'] = 'Accessible layout';
$string['accessibleviewdesc'] = 'Enable to have a one column layout for your page';
$string['itemgrabbed'] = 'Item grabbed: %s';
$string['itemdropped'] = 'Item dropped: %s';
$string['itemreorder'] = 'List has been reordered. Item %s is now in position %s of %s';
$string['reordercancelled'] = 'Reorder was cancelled';
$string['accessibilitymodedescription'] = 'This page has accessibility layout enabled.
In this mode, the page blocks will have full page width and will be displayed as a list that you can reorder.
To change a block position, navigate to it, grab it with the \'Enter\' key and move it up and down the list of blocks with the arrow keys.';
$string['blocktypeis'] = ' %s blocktype';
......@@ -790,6 +790,7 @@
<FIELD NAME="lockblocks" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" />
<FIELD NAME="instructions" TYPE="text" NOTNULL="false" />
<FIELD NAME="instructionscollapsed" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" />
<FIELD NAME="accessibleview" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" />
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" />
......
......@@ -1437,5 +1437,15 @@ function xmldb_core_upgrade($oldversion=0) {
}
}
if ($oldversion < 2019080802) {
log_debug('Adding accessible column to view table');
$table = new XMLDBTable('view');
$field = new XMLDBField('accessible');
if (!field_exists($table, $field)) {
$field->setAttributes(XMLDB_TYPE_INTEGER, 1, null, XMLDB_NOTNULL, null, null, null, 0);
add_field($table, $field);
}
}
return $status;
}
......@@ -247,6 +247,7 @@ function expected_account_preferences() {
'searchinfields' => 'titleanddescriptionandtags',
'view_details_active' => 0,
'showlayouttranslatewarning' => 1,
'accessibilityprofile' => false,
);
}
......@@ -459,6 +460,13 @@ function general_account_prefs_form_elements($prefs) {
'description' => get_string('showlayouttranslatewarningdescription', 'account', hsc(get_config('sitename'))),
);
$elements['accessibilityprofile'] = array(
'type' => 'switchbox',
'defaultvalue' => $prefs->accessibilityprofile,
'title' => get_string('accessibilityprofile', 'account'),
'description' => get_string('accessibilityprofiledescription', 'account'),
);
return $elements;
}
......
......@@ -16,7 +16,7 @@ $config = new stdClass();
// See https://wiki.mahara.org/wiki/Developer_Area/Version_Numbering_Policy
// For upgrades on stable branches, increment the version by one. On master, use the date.
$config->version = 2019080601;
$config->version = 2019080802;
$config->series = '19.10';
$config->release = '19.10dev';
$config->minupgradefrom = 2017031605;
......
......@@ -65,6 +65,7 @@ class View {
private $instructionscollapsed=0;
private $newlayout = 1;
private $grid;
private $accessible = 0;
const UNSUBMITTED = 0;
const SUBMITTED = 1;
......@@ -1978,6 +1979,7 @@ class View {
$smarty = smarty_core();
$smarty->assign('blocktypes', $blocktypes);
$smarty->assign('javascript', $javascript);
$smarty->assign('accessible', $this->get('accessible'));
return $smarty->fetch('view/blocktypelist.tpl');
}
......@@ -2174,7 +2176,7 @@ public function get_blocks($editing=false, $exporting=false, $versioning=false)
INNER JOIN {block_instance} bi
ON bd.block = bi.id
WHERE bi.view = ?
ORDER BY bi.row, bi.column, bi.order';
ORDER BY positiony, positionx';
$blocks = get_records_sql_array($sql, array($this->get('id')));
}
else {
......
{include file="header.tpl"}
{if $accessible}
<span class="sr-only">{str tag=accessibilitymodedescription section=view}</span>
{/if}
<div class="view-instructions">
<form action="{$formurl}" method="post" class="row">
<input type="submit" name="{$action_name}" id="action-dummy" class="d-none">
......
<div class="bt-{$blocktype}-editor js-blockinstance blockinstance gridstackblock card card-secondary clearfix {if $configure} configure{elseif $retractable} retractable{/if}" data-id="{$id}" id="blockinstance_{$id}{if $configure}_configure{/if}">
<h3 class="card-header js-heading drag-handle {if !$title}card-header-placeholder{/if}" title="{$strmovetitletexttooltip}">
<h3 class="card-header js-heading drag-handle {if !$title}card-header-placeholder{/if} access-drop-handle" title="{$strmovetitletexttooltip}">
<span class="icon icon-arrows-alt move-indicator" role="presentation" aria-hidden="true"></span>
<span class="blockinstance-header">
{if $configure}{$configtitle}: {str tag=Configure section=view}{else}{$title|default:"[$strnotitle]"}{/if}
......@@ -26,6 +26,7 @@
</span>
</span>
</h3>
<span class="sr-only">{str tag=blocktypeis section=view arg1=$blocktype}</span>
<div class="block blockinstance-content js-blockinstance-content">
{$content|safe}
</div>
......
......@@ -3,7 +3,7 @@
<div class='btn-group-vertical'>
{/if}
{foreach from=$blocktypes item=blocktype}{strip}
<a class="blocktype-drag blocktypelink btn btn-primary hide-title-collapsed text-left" href="#" title="{$blocktype.title}">
<a class="{if !$accessible} not-accessible{/if} blocktype-drag blocktypelink btn btn-primary hide-title-collapsed text-left" href="#" title="{$blocktype.title}">
<input type="radio" id="blocktype-list-radio-{$blocktype.name}" class="blocktype-radio" name="blocktype" value="{$blocktype.name}">
<span class="icon icon-{$blocktype.cssicon} {$blocktype.cssicontype} icon-lg" title="{$blocktype.title}" role="presentation" aria-hidden="true"></span>
<label for="blocktype-list-radio-{$blocktype.name}" class="blocktypetitle title">
......
......@@ -153,6 +153,10 @@ $javascript = array('views', 'tinymce', 'paginator', 'js/jquery/jquery-ui/js/jqu
'js/gridstack/gridstack.jQueryUI.js',
'js/gridlayout.js',
);
if ($view->get('accessible')) {
$javascript[] = 'js/dragondrop/dragon-drop.js';
$javascript[] = 'js/accessibilityreorder.js';
}
$blocktype_js = $view->get_all_blocktype_javascript();
$javascript = array_merge($javascript, $blocktype_js['jsfiles']);
if (is_plugin_active('externalvideo', 'blocktype')) {
......@@ -202,19 +206,31 @@ if (!$view->uses_new_layout()) {
$blocks = $view->get_blocks(true);
$blocksencode = json_encode($blocks);
if ( $view->get('accessible')) {
$float = 'false';
$mincolumns = '12';
$reorder = ' accessibilityReorder();';
}
else {
$float = 'true';
$mincolumns = 'null';
$reorder = ' ';
}
$inlinejs .="
$(function () {
var options = {
verticalMargin: 10,
float: true, //to place a block in any part of the page and the position will remain fixed
float: {$float},
resizable: false,
acceptWidgets: '.blocktype-drag',
draggable: {
scroll: true,
},
animate: true,
},
grid, translate;
minCellColumns: {$mincolumns},
},
grid, translate;
grid = $('.grid-stack');
grid.gridstack(options);
......@@ -229,6 +245,7 @@ $(function () {
else {
loadGrid(grid, blocks);
}
{$reorder}
});
";
......@@ -264,15 +281,23 @@ if ($placeholderblock) {
$smarty = smarty_core();
$smarty->assign('blocktypes', $placeholderblock);
$smarty->assign('javascript', false);
$smarty->assign('accessible', $view->get('accessible'));
$placeholderbutton = $smarty->fetch('view/blocktypelist.tpl');
}
$smarty = smarty($javascript, $stylesheets, array(
$strings = array(
'view' => array(
'addnewblock',
'moveblock',
),
), $extraconfig);
);
if ($view->get('accessible')) {
$strings['view'][] = 'itemgrabbed';
$strings['view'][] = 'itemdropped';
$strings['view'][] = 'itemreorder';
}
$smarty = smarty($javascript, $stylesheets, $strings, $extraconfig);
$smarty->assign('addform', $addform);
......@@ -343,5 +368,5 @@ $smarty->assign('instructionscollapsed', $view->get('instructionscollapsed'));
$returnto = $view->get_return_to_url_and_title();
$smarty->assign('url', $returnto['url']);
$smarty->assign('title', $returnto['title']);
$smarty->assign('accessible', $view->get('accessible'));
$smarty->display('view/blocks.tpl');
......@@ -207,7 +207,6 @@ function create_settings_pieform() {
'plugintype' => 'core',
'pluginname' => 'admin',
'elements' => $elements,
//'validatecallback' => 'layout_validate',
);
return array(pieform($settingsform), $inlinejavascript);
......@@ -291,6 +290,17 @@ function get_basic_elements() {
'help' => true,
);
}
$viewhasblocks = count_records('block_instance', 'view', $view->get('id'));
$accessibleviewdisabled = $viewhasblocks && !$view->get('accessible');
if (!($group || $institution) && $USER->get_account_preference('accessibilityprofile')) {
$elements['accessibleview'] = array(
'type' => 'switchbox',
'title' => get_string('accessibleview', 'view'),
'description' => get_string('accessibleviewdesc', 'view'),
'defaultvalue' => !$accessibleviewdisabled,
'disabled' => $accessibleviewdisabled,
);
}
return $elements;
}
......@@ -529,7 +539,7 @@ function settings_submit(Pieform $form, $values) {
$view->commit();
$SESSION->add_ok_msg(get_string('viewsavedsuccessfully', 'view'));
redirect('/view/blocks.php?id=' . $view->get('id'));
}
}
function create_block($bt, $configdata, $view, $blockinfo = null, $dimension=null) {
if ($bt == 'taggedposts') {
......@@ -828,6 +838,9 @@ function set_view_title_and_description(Pieform $form, $values) {
if (isset($values['anonymise'])) {
$view->set('anonymise', (int)$values['anonymise']);
}
if (isset($values['accessibleview'])) {
$view->set('accessible', (int)$values['accessibleview']);
}
}
function set_view_advanced(Pieform $form, $values) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment