summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordbeam@chromium.org <dbeam@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-08-14 23:47:37 +0000
committerdbeam@chromium.org <dbeam@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-08-14 23:48:31 +0000
commit3da41f9378f9d075a94cc278f99ce4344f9acc7b (patch)
tree9430f9e3b76dba93ea4efbe250da33c8a1da69d3
parentb0eca279214bf99ba2fd4cea4e5ae72429a8cdb8 (diff)
downloadchromium_src-3da41f9378f9d075a94cc278f99ce4344f9acc7b.zip
chromium_src-3da41f9378f9d075a94cc278f99ce4344f9acc7b.tar.gz
chromium_src-3da41f9378f9d075a94cc278f99ce4344f9acc7b.tar.bz2
history: rework focus management.
webui: add some resuable focus components (to be made more generic if desired). BUG=393489 R=dmazzoni@chromium.org Review URL: https://codereview.chromium.org/453783003 Cr-Commit-Position: refs/heads/master@{#289723} git-svn-id: svn://svn.chromium.org/chrome/trunk/src@289723 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/browser/resources/history/history.css30
-rw-r--r--chrome/browser/resources/history/history.html4
-rw-r--r--chrome/browser/resources/history/history.js369
-rw-r--r--ui/webui/resources/js/cr/ui/focus_grid.js103
-rw-r--r--ui/webui/resources/js/cr/ui/focus_row.js197
-rw-r--r--ui/webui/resources/webui_resources.grd6
6 files changed, 443 insertions, 266 deletions
diff --git a/chrome/browser/resources/history/history.css b/chrome/browser/resources/history/history.css
index 0b28a41..351c1e5 100644
--- a/chrome/browser/resources/history/history.css
+++ b/chrome/browser/resources/history/history.css
@@ -245,7 +245,7 @@ html[dir='rtl'] #display-filter-controls label span.first-button-component,
}
.entry-box,
-.site-domain-wrapper {
+.site-domain-row {
-webkit-align-items: center;
cursor: default;
display: -webkit-flex;
@@ -260,6 +260,7 @@ html[dir='rtl'] #display-filter-controls label span.first-button-component,
.site-domain-wrapper {
cursor: pointer;
+ display: -webkit-flex;
width: 100%;
}
@@ -344,7 +345,8 @@ html[dir='rtl'] .site-domain {
white-space: nowrap;
}
-.entry input[type='checkbox'] {
+.entry input[type='checkbox'],
+.site-domain-row input[type='checkbox'] {
-webkit-margin-end: 6px;
line-height: 1em;
min-width: 13px;
@@ -415,14 +417,14 @@ html[dir='rtl'] .site-domain {
}
</if>
-.entry-box {
+.entry-box,
+.site-domain-row {
+ -webkit-padding-end: 6px;
-webkit-padding-start: 6px;
border-radius: 2px;
}
-.entry-box:hover,
-.entry-box.lead,
-.entry-box.contains-focus {
+.active :-webkit-any(.entry-box, .site-domain-row) {
background-color: rgba(0, 0, 0, .05);
}
@@ -467,6 +469,7 @@ html[dir='rtl'] .site-domain {
}
.site-domain-arrow {
+ -webkit-transform: rotate(0);
-webkit-transition: -webkit-transform 300ms linear;
background: url(../disclosure_triangle_small.png) no-repeat;
background-position: 5px 5px;
@@ -478,22 +481,14 @@ html[dir='rtl'] .site-domain {
width: 21px;
}
-.site-domain-arrow.collapse {
- -webkit-transform: rotate(0);
-}
-
-.site-domain-arrow.expand {
- -webkit-transform: rotate(90deg);
+html[dir='rtl'] .site-domain-arrow {
+ -webkit-transform: rotate(180deg);
}
-html[dir='rtl'] .site-domain-arrow.collapse {
+html .expand .site-domain-arrow {
-webkit-transform: rotate(90deg);
}
-html[dir='rtl'] .site-domain-arrow.expand {
- -webkit-transform: rotate(180deg);
-}
-
.entry .bookmark-section {
-webkit-margin-end: 3px;
-webkit-margin-start: 8px;
@@ -528,6 +523,7 @@ html[dir='rtl'] .site-domain-arrow.expand {
}
button.menu-button.drop-down {
+ -webkit-margin-end: 0;
min-width: 12px;
top: 0;
}
diff --git a/chrome/browser/resources/history/history.html b/chrome/browser/resources/history/history.html
index bc6d787..2bcc1c4 100644
--- a/chrome/browser/resources/history/history.html
+++ b/chrome/browser/resources/history/history.html
@@ -27,11 +27,15 @@
<link rel="stylesheet" href="other_devices.css">
</if>
+<script src="chrome://resources/js/assert.js"></script>
<script src="chrome://resources/js/event_tracker.js"></script>
+<script src="chrome://resources/js/util.js"></script>
<script src="chrome://resources/js/cr.js"></script>
<script src="chrome://resources/js/cr/ui.js"></script>
<script src="chrome://resources/js/cr/ui/command.js"></script>
<script src="chrome://resources/js/cr/ui/focus_manager.js"></script>
+<script src="chrome://resources/js/cr/ui/focus_row.js"></script>
+<script src="chrome://resources/js/cr/ui/focus_grid.js"></script>
<script src="chrome://resources/js/cr/ui/menu_item.js"></script>
<script src="chrome://resources/js/cr/ui/menu.js"></script>
<if expr="not is_android and not is_ios">
diff --git a/chrome/browser/resources/history/history.js b/chrome/browser/resources/history/history.js
index a10cada..7b149c6 100644
--- a/chrome/browser/resources/history/history.js
+++ b/chrome/browser/resources/history/history.js
@@ -148,6 +148,7 @@ Visit.prototype.getResultDOM = function(propertyBag) {
var isSearchResult = propertyBag.isSearchResult || false;
var addTitleFavicon = propertyBag.addTitleFavicon || false;
var useMonthDate = propertyBag.useMonthDate || false;
+ var focusless = propertyBag.focusless || false;
var node = createElementWithClassName('li', 'entry');
var time = createElementWithClassName('div', 'time');
var entryBox = createElementWithClassName('label', 'entry-box');
@@ -163,18 +164,19 @@ Visit.prototype.getResultDOM = function(propertyBag) {
checkbox.type = 'checkbox';
checkbox.id = 'checkbox-' + this.id_;
checkbox.time = this.date.getTime();
- checkbox.tabIndex = -1;
checkbox.addEventListener('click', checkboxClicked);
entryBox.appendChild(checkbox);
+ if (focusless)
+ checkbox.tabIndex = -1;
+
// Clicking anywhere in the entryBox will check/uncheck the checkbox.
entryBox.setAttribute('for', checkbox.id);
entryBox.addEventListener('mousedown', entryBoxMousedown);
entryBox.addEventListener('click', entryBoxClick);
+ entryBox.addEventListener('keydown', this.handleKeydown_.bind(this));
}
- entryBox.addEventListener('keydown', this.handleKeydown_.bind(this));
-
// Keep track of the drop down that triggered the menu, so we know
// which element to apply the command to.
// TODO(dubroy): Ideally we'd use 'activate', but MenuButton swallows it.
@@ -209,9 +211,15 @@ Visit.prototype.getResultDOM = function(propertyBag) {
visitEntryWrapper.classList.add('blocked-indicator');
visitEntryWrapper.appendChild(this.getVisitAttemptDOM_());
} else {
- visitEntryWrapper.appendChild(this.getTitleDOM_(isSearchResult));
+ var title = visitEntryWrapper.appendChild(
+ this.getTitleDOM_(isSearchResult));
+
if (addTitleFavicon)
this.addFaviconToElement_(visitEntryWrapper);
+
+ if (focusless)
+ title.querySelector('a').tabIndex = -1;
+
visitEntryWrapper.appendChild(domain);
}
@@ -233,10 +241,12 @@ Visit.prototype.getResultDOM = function(propertyBag) {
var dropDown = createElementWithClassName('button', 'drop-down');
dropDown.value = 'Open action menu';
dropDown.title = loadTimeData.getString('actionMenuDescription');
- dropDown.tabIndex = -1;
dropDown.setAttribute('menu', '#action-menu');
dropDown.setAttribute('aria-haspopup', 'true');
+ if (focusless)
+ dropDown.tabIndex = -1;
+
cr.ui.decorate(dropDown, MenuButton);
dropDown.respondToArrowKeys = false;
@@ -289,34 +299,6 @@ Visit.prototype.removeFromHistory = function() {
}.bind(this));
};
-/**
- * @param {boolean} isLead Whether this visit is the "lead" visit, i.e. the one
- * that would be focused if the entry list is tabbed to.
- */
-Visit.prototype.setIsLead = function(isLead) {
- this.domNode_.querySelector('.entry-box').classList.toggle('lead', isLead);
- if (!isLead) {
- this.getFocusableControls_().forEach(function(control) {
- control.tabIndex = -1;
- });
- }
-};
-
-/**
- * @param {Element} control A control element to focus.
- */
-Visit.prototype.focusControl = function(control) {
- var controls = this.getFocusableControls_();
- assert(controls.indexOf(control) >= 0);
-
- for (var i = 0; i < controls.length; ++i) {
- controls[i].tabIndex = controls[i] == control ? 0 : -1;
- }
-
- control.focus();
- this.setIsLead(true);
-};
-
Object.defineProperty(Visit.prototype, 'checkBox', {
get: function() {
return this.domNode_.querySelector('input[type=checkbox]');
@@ -387,7 +369,6 @@ Visit.prototype.getTitleDOM_ = function(isSearchResult) {
link.href = this.url_;
link.id = 'id-' + this.id_;
link.target = '_top';
- link.tabIndex = -1;
var integerId = parseInt(this.id_, 10);
link.addEventListener('click', function() {
recordUmaAction('HistoryPage_EntryLinkClick');
@@ -459,52 +440,14 @@ Visit.prototype.showMoreFromSite_ = function() {
};
/**
- * @return {Array.<Element>} A list of focusable controls.
- * @private
- */
-Visit.prototype.getFocusableControls_ = function() {
- var controls = [];
-
- if (this.checkBox)
- controls.push(this.checkBox);
-
- if (this.bookmarkStar)
- controls.push(this.bookmarkStar);
-
- controls.push(this.titleLink);
-
- if (this.dropDown)
- controls.push(this.dropDown);
-
- return controls;
-};
-
-/**
* @param {Event} e A keydown event to handle.
* @private
*/
Visit.prototype.handleKeydown_ = function(e) {
- var key = e.keyIdentifier;
- if (key == 'U+0008' || key == 'U+007F') { // Delete or Backspace.
- if (!this.model_.isDeletingVisits())
- this.removeEntryFromHistory_(e);
- return;
- }
-
- var target = e.target;
- if (target != document.activeElement || !(key == 'Left' || key == 'Right'))
- return;
-
- var controls = this.getFocusableControls_();
- for (var i = 0; i < controls.length; ++i) {
- if (controls[i].contains(target)) {
- var toFocus = key == 'Left' ? controls[i - 1] : controls[i + 1];
- if (toFocus) {
- this.focusControl(toFocus);
- e.preventDefault();
- }
- break;
- }
+ // Delete or Backspace should delete the entry if allowed.
+ if ((e.keyIdentifier == 'U+0008' || e.keyIdentifier == 'U+007F') &&
+ !this.model_.isDeletingVisits()) {
+ this.removeEntryFromHistory_(e);
}
};
@@ -876,6 +819,34 @@ HistoryModel.prototype.getGroupByDomain = function() {
};
///////////////////////////////////////////////////////////////////////////////
+// HistoryFocusObserver:
+
+/** @implements {cr.ui.FocusRow.Observer} */
+function HistoryFocusObserver() {}
+
+HistoryFocusObserver.prototype = {
+ /** @override */
+ onActivate: function(row) {
+ this.getActiveRowElement_(row).classList.add('active');
+ },
+
+ /** @override */
+ onDeactivate: function(row, el) {
+ this.getActiveRowElement_(row).classList.remove('active');
+ },
+
+ /**
+ * @param {cr.ui.FocusRow} row The row to find an element for.
+ * @return {Element} |row|'s "active" element.
+ * @private
+ */
+ getActiveRowElement_: function(row) {
+ return findAncestorByClass(row.items[0], 'entry') ||
+ findAncestorByClass(row.items[0], 'site-domain-wrapper');
+ },
+};
+
+///////////////////////////////////////////////////////////////////////////////
// HistoryView:
/**
@@ -889,6 +860,8 @@ function HistoryView(model) {
this.editButtonTd_ = $('edit-button');
this.editingControlsDiv_ = $('editing-controls');
this.resultDiv_ = $('results-display');
+ this.focusGrid_ = new cr.ui.FocusGrid(this.resultDiv_,
+ new HistoryFocusObserver);
this.pageDiv_ = $('results-pagination');
this.model_ = model;
this.pageIndex_ = 0;
@@ -950,15 +923,6 @@ function HistoryView(model) {
else
self.setOffset(0);
});
-
- this.resultDiv_.addEventListener(
- 'keydown', this.handleKeydown_.bind(this));
- this.resultDiv_.addEventListener(
- 'mousedown', this.handleMousedown_.bind(this), true);
- this.resultDiv_.addEventListener(
- 'focusin', this.updateFocusableElements_.bind(this));
- this.resultDiv_.addEventListener(
- 'blur', this.updateFocusableElements_.bind(this));
}
// HistoryView, public: -------------------------------------------------------
@@ -1081,7 +1045,7 @@ HistoryView.prototype.onModelReady = function(doneLoading) {
var hasResults = this.model_.visits_.length > 0;
document.body.classList.toggle('has-results', hasResults);
- this.updateFocusableElements_();
+ this.updateFocusGrid_();
this.updateNavBar_();
if (isMobileVersion()) {
@@ -1132,8 +1096,15 @@ HistoryView.prototype.showNotification = function(innerHTML, isWarning) {
*/
HistoryView.prototype.onBeforeRemove = function(visit) {
assert(this.currentVisits_.indexOf(visit) >= 0);
- var toFocus = this.getVisitAfter_(visit) || this.getVisitBefore_(visit);
- this.swapFocusedVisit_(toFocus);
+
+ var pos = this.focusGrid_.getPositionForTarget(document.activeElement);
+ if (!pos)
+ return;
+
+ var row = this.focusGrid_.rows[pos.row + 1] ||
+ this.focusGrid_.rows[pos.row - 1];
+ if (row)
+ row.focusIndex(Math.min(pos.col, row.items.length - 1));
};
/**
@@ -1182,6 +1153,7 @@ HistoryView.prototype.removeVisit = function(visit) {
for (var i = 0; i < toRemove.length; ++i) {
removeNode(toRemove[i], onRemove, this);
}
+ this.updateFocusGrid_();
var index = this.currentVisits_.indexOf(visit);
if (index >= 0)
@@ -1265,9 +1237,11 @@ HistoryView.prototype.getGroupedVisitsDOM_ = function(
var siteResults = results.appendChild(
createElementWithClassName('li', 'site-entry'));
- // Make a wrapper that will contain the arrow, the favicon and the domain.
var siteDomainWrapper = siteResults.appendChild(
createElementWithClassName('div', 'site-domain-wrapper'));
+ // Make a row that will contain the arrow, the favicon and the domain.
+ var siteDomainRow = siteDomainWrapper.appendChild(
+ createElementWithClassName('div', 'site-domain-row'));
if (this.model_.editingEntriesAllowed) {
var siteDomainCheckbox =
@@ -1276,13 +1250,12 @@ HistoryView.prototype.getGroupedVisitsDOM_ = function(
siteDomainCheckbox.type = 'checkbox';
siteDomainCheckbox.addEventListener('click', domainCheckboxClicked);
siteDomainCheckbox.domain_ = domain;
-
- siteDomainWrapper.appendChild(siteDomainCheckbox);
+ siteDomainRow.appendChild(siteDomainCheckbox);
}
- var siteArrow = siteDomainWrapper.appendChild(
- createElementWithClassName('div', 'site-domain-arrow collapse'));
- var siteDomain = siteDomainWrapper.appendChild(
+ var siteArrow = siteDomainRow.appendChild(
+ createElementWithClassName('div', 'site-domain-arrow'));
+ var siteDomain = siteDomainRow.appendChild(
createElementWithClassName('div', 'site-domain'));
var siteDomainLink = siteDomain.appendChild(
createElementWithClassName('button', 'link-button'));
@@ -1297,10 +1270,11 @@ HistoryView.prototype.getGroupedVisitsDOM_ = function(
domainVisits[0].addFaviconToElement_(siteDomain);
- siteDomainWrapper.addEventListener('click', toggleHandler);
+ siteDomainWrapper.addEventListener(
+ 'click', this.toggleGroupedVisits_.bind(this));
if (this.model_.isSupervisedProfile) {
- siteDomainWrapper.appendChild(
+ siteDomainRow.appendChild(
getFilteringStatusDOM(domainVisits[0].hostFilteringBehavior));
}
@@ -1316,7 +1290,8 @@ HistoryView.prototype.getGroupedVisitsDOM_ = function(
var isMonthGroupedResult = this.getRangeInDays() == HistoryModel.Range.MONTH;
for (var j = 0, visit; visit = domainVisits[j]; j++) {
resultsList.appendChild(visit.getResultDOM({
- useMonthDate: isMonthGroupedResult
+ focusless: true,
+ useMonthDate: isMonthGroupedResult,
}));
this.setVisitRendered_(visit);
}
@@ -1563,6 +1538,34 @@ HistoryView.prototype.displayResults_ = function(doneLoading) {
this.setTimeColumnWidth_(this.resultDiv_);
};
+var focusGridRowSelector = [
+ '.day-results > .entry:not(.fade-out)',
+ '.expand .grouped .entry:not(.fade-out)',
+ '.site-domain-wrapper'
+].join(', ');
+
+var focusGridColumnSelector = [
+ '.entry-box input',
+ '.bookmark-section.starred',
+ '.title a',
+ '.drop-down',
+ '.domain-checkbox',
+ '.link-button',
+].join(', ');
+
+/** @private */
+HistoryView.prototype.updateFocusGrid_ = function() {
+ var rows = this.resultDiv_.querySelectorAll(focusGridRowSelector);
+ var grid = [];
+
+ for (var i = 0; i < rows.length; ++i) {
+ assert(rows[i].parentNode);
+ grid.push(rows[i].querySelectorAll(focusGridColumnSelector));
+ }
+
+ this.focusGrid_.setGrid(grid);
+};
+
/**
* Update the visibility of the page navigation buttons.
* @private
@@ -1625,134 +1628,32 @@ HistoryView.prototype.setTimeColumnWidth_ = function() {
};
/**
- * @param {Visit} visit The starting point when looking for a previous visit.
- * @return {Visit|undefined} The previous visit (if there is one) or undefined.
- * @private
- */
-HistoryView.prototype.getVisitBefore_ = function(visit) {
- var index = this.currentVisits_.indexOf(visit);
- return index < 0 ? undefined : this.currentVisits_[index - 1];
-};
-
-/**
- * @param {Visit} visit The starting point when looking for a previous visit.
- * @return {Visit|undefined} The next visit (if there is one) or undefined.
- * @private
- */
-HistoryView.prototype.getVisitAfter_ = function(visit) {
- var index = this.currentVisits_.indexOf(visit);
- return index < 0 ? undefined : this.currentVisits_[index + 1];
-};
-
-/**
- * Swaps focus to |toBeFocused|. Assumes the another visit is currently focused.
- * @param {Visit} visit A visit to focus.
- * @private
- */
-HistoryView.prototype.swapFocusedVisit_ = function(visit) {
- if (!visit)
- return;
-
- var activeVisit = findAncestorByClass(document.activeElement, 'entry').visit;
- var controls = activeVisit.getFocusableControls_();
-
- for (var i = 0; i < controls.length; ++i) {
- var control = controls[i];
- if (!control.contains(document.activeElement))
- continue;
-
- // Try to focus the same type of control if the new visit has it.
- if (control == activeVisit.checkBox && visit.checkBox) {
- visit.focusControl(visit.checkBox);
- } else if (control == activeVisit.bookmarkStar && visit.bookmarkStar) {
- visit.focusControl(visit.bookmarkStar);
- } else if (control == activeVisit.titleLink) {
- visit.focusControl(visit.titleLink);
- } else if (control == activeVisit.dropDown && visit.dropDown) {
- visit.focusControl(visit.dropDown);
- } else {
- // Otherwise, just focus something that might be in a similar column.
- var controlsToFocus = visit.getFocusableControls_();
- var indexToFocus = Math.min(i, controlsToFocus.length - 1);
- visit.focusControl(controlsToFocus[indexToFocus]);
- }
- break;
- }
-
- activeVisit.setIsLead(false);
-};
-
-/**
- * @param {Event} e A keydown event to handle.
- * @private
- */
-HistoryView.prototype.handleKeydown_ = function(e) {
- // Only handle up or down arrows on the focused element.
- var key = e.keyIdentifier, target = e.target;
- if (target != document.activeElement || !(key == 'Up' || key == 'Down'))
- return;
-
- var entry = findAncestorByClass(e.target, 'entry');
- var visit = entry && entry.visit;
- this.swapFocusedVisit_(key == 'Up' ? this.getVisitBefore_(visit) :
- this.getVisitAfter_(visit));
-};
-
-/**
- * @param {Event} e A mousedown event to handle.
- * @private
- */
-HistoryView.prototype.handleMousedown_ = function(e) {
- var target = e.target;
- var entry = findAncestorByClass(target, 'entry');
- if (!entry || !entry.contains(target))
- return;
-
- var visit = entry.visit;
- if (visit.bookmarkStar && visit.bookmarkStar.contains(target))
- return;
-
- if (visit.titleLink.contains(target))
- visit.focusControl(visit.titleLink);
- else if (visit.dropDown && visit.dropDown.contains(target))
- visit.focusControl(visit.dropDown);
- else // Focus the checkbox by default. If no checkbox, focus the title.
- visit.focusControl(visit.checkBox || visit.titleLink);
-
- e.preventDefault();
-};
-
-/**
- * Ensures there's only 1 focusable visit.
+ * Toggles an element in the grouped history.
+ * @param {Element} e The element which was clicked on.
* @private
*/
-HistoryView.prototype.updateFocusableElements_ = function() {
- var focusable = Array.prototype.slice.call(
- this.resultDiv_.querySelectorAll('[tabindex="0"]'));
-
- // Don't change the tabIndex of the first [tabindex=0] node or the active
- // element if either are in |focusable|.
- focusable.splice(Math.max(0, focusable.indexOf(document.activeElement)), 1);
+HistoryView.prototype.toggleGroupedVisits_ = function(e) {
+ var entry = findAncestorByClass(e.target, 'site-entry');
+ var innerResultList = entry.querySelector('.site-results');
- for (var i = 0; i < focusable.length; ++i) {
- var el = focusable[i];
- var entry = findAncestorByClass(el, 'entry');
- if (!entry.contains(document.activeElement))
- entry.visit.setIsLead(false);
- else
- el.tabIndex = -1;
+ if (entry.classList.contains('expand')) {
+ innerResultList.style.height = 0;
+ } else {
+ innerResultList.style.height = 'auto';
+ // -webkit-transition does not work on height:auto elements so first set
+ // the height to auto so that it is computed and then set it to the
+ // computed value in pixels so the transition works properly.
+ var height = innerResultList.clientHeight;
+ innerResultList.style.height = 0;
+ setTimeout(function() {
+ innerResultList.style.height = height + 'px';
+ }, 0);
}
- // If there's no focusable elements, allow the first visit to be focused.
- if (!this.resultDiv_.querySelector('[tabindex="0"]') &&
- this.currentVisits_.length > 0) {
- var firstVisit = this.currentVisits_[0];
- firstVisit.setIsLead(true);
- (firstVisit.checkBox || firstVisit.titleLink).tabIndex = 0;
- }
+ entry.classList.toggle('expand');
+ this.updateFocusGrid_();
};
-
///////////////////////////////////////////////////////////////////////////////
// State object:
/**
@@ -1940,8 +1841,6 @@ function load() {
window.addEventListener('resize',
historyView.positionNotificationBar.bind(historyView));
- cr.ui.FocusManager.disableMouseFocusOnButtons();
-
if (isMobileVersion()) {
// Move the search box out of the header.
var resultsDisplay = $('results-display');
@@ -2186,7 +2085,7 @@ function updateParentCheckbox(checkbox) {
var groupCheckbox = entry.querySelector('.site-domain-wrapper input');
if (groupCheckbox)
- groupCheckbox.checked = false;
+ groupCheckbox.checked = false;
}
function entryBoxMousedown(event) {
@@ -2253,34 +2152,6 @@ function removeNode(node, onRemove, opt_scope) {
}
/**
- * Toggles an element in the grouped history.
- * @param {Element} e The element which was clicked on.
- */
-function toggleHandler(e) {
- var innerResultList = e.currentTarget.parentElement.querySelector(
- '.site-results');
- var innerArrow = e.currentTarget.parentElement.querySelector(
- '.site-domain-arrow');
- if (innerArrow.classList.contains('collapse')) {
- innerResultList.style.height = 'auto';
- // -webkit-transition does not work on height:auto elements so first set
- // the height to auto so that it is computed and then set it to the
- // computed value in pixels so the transition works properly.
- var height = innerResultList.clientHeight;
- innerResultList.style.height = 0;
- setTimeout(function() {
- innerResultList.style.height = height + 'px';
- }, 0);
- innerArrow.classList.remove('collapse');
- innerArrow.classList.add('expand');
- } else {
- innerResultList.style.height = 0;
- innerArrow.classList.remove('expand');
- innerArrow.classList.add('collapse');
- }
-}
-
-/**
* Builds the DOM elements to show the filtering status of a domain/URL.
* @param {SupervisedUserFilteringBehavior} filteringBehavior The filter
* behavior for this item.
diff --git a/ui/webui/resources/js/cr/ui/focus_grid.js b/ui/webui/resources/js/cr/ui/focus_grid.js
new file mode 100644
index 0000000..713a838
--- /dev/null
+++ b/ui/webui/resources/js/cr/ui/focus_grid.js
@@ -0,0 +1,103 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('cr.ui', function() {
+ /**
+ * A class to manage grid of focusable elements in a 2D grid. For example,
+ * given this grid:
+ *
+ * focusable [focused] focusable (row: 0, col: 1)
+ * focusable focusable focusable
+ * focusable focusable focusable
+ *
+ * Pressing the down arrow would result in the focus moving down 1 row and
+ * keeping the same column:
+ *
+ * focusable focusable focusable
+ * focusable [focused] focusable (row: 1, col: 1)
+ * focusable focusable focusable
+ *
+ * And pressing right at this point would move the focus to:
+ *
+ * focusable focusable focusable
+ * focusable focusable [focused] (row: 1, col: 2)
+ * focusable focusable focusable
+ *
+ * @param {Node=} opt_boundary Ignore focus events outside this node.
+ * @param {cr.ui.FocusRow.Observer=} opt_observer An observer of rows.
+ * @implements {cr.ui.FocusRow.Delegate}
+ * @constructor
+ */
+ function FocusGrid(opt_boundary, opt_observer) {
+ /** @type {Node|undefined} */
+ this.boundary_ = opt_boundary;
+
+ /** @private {cr.ui.FocusRow.Observer|undefined} */
+ this.observer_ = opt_observer;
+
+ /** @type {!Array.<!cr.ui.FocusRow>} */
+ this.rows = [];
+ }
+
+ FocusGrid.prototype = {
+ /**
+ * @param {EventTarget} target A target item to find in this grid.
+ * @return {?{row: number, col: number}} A position or null if not found.
+ */
+ getPositionForTarget: function(target) {
+ for (var i = 0; i < this.rows.length; ++i) {
+ for (var j = 0; j < this.rows[i].items.length; ++j) {
+ if (target == this.rows[i].items[j])
+ return {row: i, col: j};
+ }
+ }
+ return null;
+ },
+
+ /** @override */
+ onKeydown: function(keyRow, e) {
+ var rowIndex = this.rows.indexOf(keyRow);
+ assert(rowIndex >= 0);
+
+ var row = -1;
+
+ if (e.keyIdentifier == 'Up')
+ row = rowIndex - 1;
+ else if (e.keyIdentifier == 'Down')
+ row = rowIndex + 1;
+ else if (e.keyIdentifier == 'PageUp')
+ row = 0;
+ else if (e.keyIdentifier == 'PageDown')
+ row = this.rows.length - 1;
+
+ if (!this.rows[row])
+ return;
+
+ var colIndex = keyRow.items.indexOf(e.target);
+ var col = Math.min(colIndex, this.rows[row].items.length - 1);
+
+ this.rows[row].focusIndex(col);
+
+ e.preventDefault();
+ },
+
+ /**
+ * @param {!Array.<!NodeList|!Array.<!Element>>} grid A 2D array of nodes.
+ */
+ setGrid: function(grid) {
+ this.rows.forEach(function(row) { row.destroy(); });
+
+ this.rows = grid.map(function(row) {
+ return new cr.ui.FocusRow(row, this.boundary_, this, this.observer_);
+ }, this);
+
+ if (!this.getPositionForTarget(document.activeElement) && this.rows[0])
+ this.rows[0].activeIndex = 0;
+ },
+ };
+
+ return {
+ FocusGrid: FocusGrid,
+ };
+});
diff --git a/ui/webui/resources/js/cr/ui/focus_row.js b/ui/webui/resources/js/cr/ui/focus_row.js
new file mode 100644
index 0000000..9f3495d
--- /dev/null
+++ b/ui/webui/resources/js/cr/ui/focus_row.js
@@ -0,0 +1,197 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('cr.ui', function() {
+ /**
+ * A class to manage focus between given horizontally arranged elements.
+ * For example, given the page:
+ *
+ * <input type="checkbox"> <label>Check me!</label> <button>X</button>
+ *
+ * One could create a FocusRow by doing:
+ *
+ * new cr.ui.FocusRow([checkboxEl, labelEl, buttonEl])
+ *
+ * if there are references to each node or querying them from the DOM like so:
+ *
+ * new cr.ui.FocusRow(dialog.querySelectorAll('list input[type=checkbox]'))
+ *
+ * Pressing left cycles backward and pressing right cycles forward in item
+ * order. Pressing Home goes to the beginning of the list and End goes to the
+ * end of the list.
+ *
+ * If an item in this row is focused, it'll stay active (accessible via tab).
+ * If no items in this row are focused, the row can stay active until focus
+ * changes to a node inside |this.boundary_|. If opt_boundary isn't
+ * specified, any focus change deactivates the row.
+ *
+ * @param {!Array.<!Element>|!NodeList} items Elements to track focus of.
+ * @param {Node=} opt_boundary Focus events are ignored outside of this node.
+ * @param {FocusRow.Delegate=} opt_delegate A delegate to handle key events.
+ * @param {FocusRow.Observer=} opt_observer An observer that's notified if
+ * this focus row is added to or removed from the focus order.
+ * @constructor
+ */
+ function FocusRow(items, opt_boundary, opt_delegate, opt_observer) {
+ /** @type {!Array.<!Element>} */
+ this.items = Array.prototype.slice.call(items);
+ assert(this.items.length > 0);
+
+ /** @type {!Node} */
+ this.boundary_ = opt_boundary || document;
+
+ /** @private {FocusRow.Delegate|undefined} */
+ this.delegate_ = opt_delegate;
+
+ /** @private {FocusRow.Observer|undefined} */
+ this.observer_ = opt_observer;
+
+ /** @private {!EventTracker} */
+ this.eventTracker_ = new EventTracker;
+ this.eventTracker_.add(cr.doc, 'focusin', this.onFocusin_.bind(this));
+ this.eventTracker_.add(cr.doc, 'keydown', this.onKeydown_.bind(this));
+
+ this.items.forEach(function(item) {
+ if (item != document.activeElement)
+ item.tabIndex = -1;
+
+ this.eventTracker_.add(item, 'click', this.onClick_.bind(this));
+ }, this);
+
+ /**
+ * The index that should be actively participating in the page tab order.
+ * @type {number}
+ * @private
+ */
+ this.activeIndex_ = this.items.indexOf(document.activeElement);
+ }
+
+ /** @interface */
+ FocusRow.Delegate = function() {};
+
+ FocusRow.Delegate.prototype = {
+ /**
+ * Called when a key is pressed while an item in |this.items| is focused. If
+ * |e|'s default is prevented, further processing is skipped.
+ * @param {cr.ui.FocusRow} row The row that detected a keydown.
+ * @param {Event} e The keydown event.
+ */
+ onKeydown: assertNotReached,
+ };
+
+ /** @interface */
+ FocusRow.Observer = function() {};
+
+ FocusRow.Observer.prototype = {
+ /**
+ * Called when the row is activated (added to the focus order).
+ * @param {cr.ui.FocusRow} row The row added to the focus order.
+ */
+ onActivate: assertNotReached,
+
+ /**
+ * Called when the row is deactivated (removed from the focus order).
+ * @param {cr.ui.FocusRow} row The row removed from the focus order.
+ */
+ onDeactivate: assertNotReached,
+ };
+
+ FocusRow.prototype = {
+ get activeIndex() {
+ return this.activeIndex_;
+ },
+ set activeIndex(index) {
+ var wasActive = this.items[this.activeIndex_];
+ if (wasActive)
+ wasActive.tabIndex = -1;
+
+ this.items.forEach(function(item) { assert(item.tabIndex == -1); });
+ this.activeIndex_ = index;
+
+ if (this.items[index])
+ this.items[index].tabIndex = 0;
+
+ if (!this.observer_)
+ return;
+
+ var isActive = index >= 0 && index < this.items.length;
+ if (isActive == !!wasActive)
+ return;
+
+ if (isActive)
+ this.observer_.onActivate(this);
+ else
+ this.observer_.onDeactivate(this);
+ },
+
+ /**
+ * Focuses the item at |index|.
+ * @param {number} index An index to focus. Must be between 0 and
+ * this.items.length - 1.
+ */
+ focusIndex: function(index) {
+ this.items[index].focus();
+ },
+
+ /** Call this to clean up event handling before dereferencing. */
+ destroy: function() {
+ this.eventTracker_.removeAll();
+ },
+
+ /**
+ * @param {Event} e The focusin event.
+ * @private
+ */
+ onFocusin_: function(e) {
+ if (this.boundary_.contains(e.target))
+ this.activeIndex = this.items.indexOf(e.target);
+ },
+
+ /**
+ * @param {Event} e A focus event.
+ * @private
+ */
+ onKeydown_: function(e) {
+ var item = this.items.indexOf(e.target);
+ if (item < 0)
+ return;
+
+ if (this.delegate_)
+ this.delegate_.onKeydown(this, e);
+
+ if (e.defaultPrevented)
+ return;
+
+ var index = -1;
+
+ if (e.keyIdentifier == 'Left')
+ index = item + (isRTL() ? 1 : -1);
+ else if (e.keyIdentifier == 'Right')
+ index = item + (isRTL() ? -1 : 1);
+ else if (e.keyIdentifier == 'Home')
+ index = 0;
+ else if (e.keyIdentifier == 'End')
+ index = this.items.length - 1;
+
+ if (!this.items[index])
+ return;
+
+ this.focusIndex(index);
+ e.preventDefault();
+ },
+
+ /**
+ * @param {Event} e A click event.
+ * @private
+ */
+ onClick_: function(e) {
+ if (!e.button)
+ this.activeIndex = this.items.indexOf(e.currentTarget);
+ },
+ };
+
+ return {
+ FocusRow: FocusRow,
+ };
+});
diff --git a/ui/webui/resources/webui_resources.grd b/ui/webui/resources/webui_resources.grd
index 19be267..da77c2d 100644
--- a/ui/webui/resources/webui_resources.grd
+++ b/ui/webui/resources/webui_resources.grd
@@ -247,6 +247,8 @@ without changes to the corresponding grd file. -->
<structure name="IDR_WEBUI_CSS_WIDGETS"
file="css/widgets.css" type="chrome_html"
flattenhtml="true" />
+ <structure name="IDR_WEBUI_JS_ASSERT"
+ file="js/assert.js" type="chrome_html" />
<structure name="IDR_WEBUI_JS_CR"
file="js/cr.js" type="chrome_html" />
<structure name="IDR_WEBUI_JS_CR_EVENT_TARGET"
@@ -281,11 +283,15 @@ without changes to the corresponding grd file. -->
file="js/cr/ui/dialogs.js" type="chrome_html" />
<structure name="IDR_WEBUI_JS_CR_UI_DRAG_WRAPPER"
file="js/cr/ui/drag_wrapper.js" type="chrome_html" />
+ <structure name="IDR_WEBUI_JS_CR_UI_FOCUS_GRID"
+ file="js/cr/ui/focus_grid.js" type="chrome_html" />
<structure name="IDR_WEBUI_JS_CR_UI_FOCUS_MANAGER"
file="js/cr/ui/focus_manager.js" type="chrome_html" />
<structure name="IDR_WEBUI_JS_CR_UI_FOCUS_OUTLINE_MANAGER"
file="js/cr/ui/focus_outline_manager.js"
type="chrome_html" />
+ <structure name="IDR_WEBUI_JS_CR_UI_FOCUS_ROW"
+ file="js/cr/ui/focus_row.js" type="chrome_html" />
<structure name="IDR_WEBUI_JS_CR_UI_LIST"
file="js/cr/ui/list.js" type="chrome_html" />
<structure name="IDR_WEBUI_JS_CR_UI_LIST_ITEM"