Skip to content

Commit

Permalink
Fixes #63: Adds scroll-to-focused-item to enable scrolling to an it…
Browse files Browse the repository at this point in the history
…em before focusing.
  • Loading branch information
bicknellr committed Jun 15, 2016
1 parent 574ca3e commit c3cb651
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 27 deletions.
47 changes: 47 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@
.row {
height: 120px;
}

.scrolling {
max-width: 200px;
max-height: 300px;
overflow: auto;
}

.very-tall-item {
min-height: 350px;
background-color: #bfb;
}
</style>
</head>
<body unresolved>
Expand Down Expand Up @@ -109,6 +120,42 @@ <h3>Multi-select menubar</h3>
</div>
</div>

<div>
<h3><code>scroll-to-focused-item</code></h3>
<div class="horizontal-section">
<simple-menu class="scrolling" scroll-to-focused-item>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem" disabled>Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem" class="very-tall-item">
Why is this item so big? All the blank space after this text and
before the next item is really confusing when scrolling down.
</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem" disabled>Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem" disabled>Item</a>
<a href="javascript:void(0)" role="menuitem" disabled>Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem" disabled>Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem">Item</a>
<a href="javascript:void(0)" role="menuitem" disabled>Item</a>
</simple-menu>
</div>
</div>
</div>
</body>
</html>
29 changes: 29 additions & 0 deletions iron-menu-behavior.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
type: Object
},

/**
* If true, before an item is focused, that item is scrolled into view and
* positioned at the nearest edge of the menu.
*/
scrollToFocusedItem: {
type: Boolean,
value: false
},

/**
* The attribute to use on menu items to look up the item title. Typing the first
* letter of an item when the menu is open focuses that item. If unset, `textContent`
Expand Down Expand Up @@ -197,6 +206,26 @@
old && old.setAttribute('tabindex', '-1');
if (focusedItem) {
focusedItem.setAttribute('tabindex', '0');

// TODO(bicknellr): Most of this can be removed once the
// ScrollIntoViewOptions argument type gets general support.
// https://drafts.csswg.org/cssom-view/#dom-element-scrollintoview
if (this.scrollToFocusedItem) {
var selfRect = this.getBoundingClientRect();
var focusedItemRect = focusedItem.getBoundingClientRect();
var topOutside = focusedItemRect.top < selfRect.top;
var bottomOutside =
(selfRect.top + selfRect.height) <
(focusedItemRect.top + focusedItemRect.height);
// If the item is already visible (topOutside === bottomOutside === false)
// or covering the entire menu area (... === true), don't scroll.
if (topOutside !== bottomOutside) {
// If the item is larger than the menu, always align to the top edge.
var itemTallerThanSelf = focusedItemRect.height > selfRect.height;
focusedItem.scrollIntoView(itemTallerThanSelf || topOutside);
}
}

focusedItem.focus();
}
},
Expand Down
142 changes: 142 additions & 0 deletions test/iron-menu-behavior.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,33 @@
</template>
</test-fixture>

<test-fixture id="scrolling">
<template>
<test-menu scroll-to-focused-item scrolling>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div large style="height: 500px;">item</div>
<div>item</div>
<div>item</div>
<div>item</div>
</test-menu>
</template>
</test-fixture>

<script>
suite('menu a11y tests', function() {
test('menu has role="menu"', function() {
Expand Down Expand Up @@ -379,6 +406,121 @@
done();
});
});

suite('`scroll-to-focused-item`', function() {
var menu;

setup(function() {
menu = fixture('scrolling');
});

test('does not scroll if the item is already visible', function() {
var menuRect = menu.getBoundingClientRect();
var firstItem = menu.items[0];
var firstItemRect = firstItem.getBoundingClientRect();

assert.isAtLeast(firstItemRect.top, menuRect.top,
'First item top is below menu top.');
assert.isAtMost(
firstItemRect.top + firstItemRect.height,
menuRect.top + menuRect.height,
'First item bottom is above menu bottom.');

var startMenuScrollTop = menu.scrollTop;
menu.select(0);
assert.equal(menu.scrollTop, startMenuScrollTop,
'Menu `scrollTop` is unchanged.');
});

test('scrolls to align the item to the bottom of the menu, if the ' +
'item is at least partially below the bounds of the menu',
function() {
var menuRect = menu.getBoundingClientRect();
var itemIndex = menu.items.length - 1;
var item = menu.items[itemIndex];
var itemRect = item.getBoundingClientRect();

assert.isAtLeast(
itemRect.top, menuRect.top,
'The item top is below the menu top.');
assert.isAtLeast(
itemRect.top + itemRect.height,
menuRect.top + menuRect.height,
'The item bottom is below the menu bottom.');
assert.isAtMost(itemRect.height, menuRect.height,
'The item is smaller than the menu.');

menu.select(itemIndex);

var menuRect = menu.getBoundingClientRect();
var itemRect = item.getBoundingClientRect();

assert.equal(
itemRect.top + itemRect.height,
menuRect.top + menuRect.height,
'The item bottom is aligned to the bottom of the menu.');
});

test('scrolls to align the item to the top of the menu, if the ' +
'item is at least partially above the bounds of the menu',
function() {
var menuRect = menu.getBoundingClientRect();
var itemIndex = 0;
var item = menu.items[itemIndex];
var itemRect = item.getBoundingClientRect();

menu.scrollTop = menu.scrollHeight;

assert.isAtLeast(
menuRect.top, itemRect.top,
'The item top is above the menu top.');
assert.isAtLeast(
menuRect.top + menuRect.height,
itemRect.top + itemRect.height,
'The item bottom is above the menu bottom.');
assert.isAtMost(itemRect.height, menuRect.height,
'The item is smaller than the menu.');

menu.select(itemIndex);

var menuRect = menu.getBoundingClientRect();
var itemRect = item.getBoundingClientRect();

assert.equal(
itemRect.top, menuRect.top,
'The item top is aligned to the top of the menu.');
});

test('scrolls to align the item to the top of the menu, if the ' +
'item is below the menu bounds but larger than the menu',
function() {
var menuRect = menu.getBoundingClientRect();
var item = Polymer.dom(menu).querySelector('[large]');
var itemIndex = menu.items.indexOf(item);
var itemRect = item.getBoundingClientRect();

menu.scrollTop = menu.scrollHeight;

assert.isAtLeast(
itemRect.top, menuRect.top,
'The item top is below the menu top.');
assert.isAtLeast(
itemRect.top + itemRect.height,
menuRect.top + menuRect.height,
'The item bottom is below the menu bottom.');
assert.isAtLeast(itemRect.height, menuRect.height,
'The item is larger than the menu.');

menu.select(itemIndex);

var menuRect = menu.getBoundingClientRect();
var itemRect = item.getBoundingClientRect();

assert.equal(
itemRect.top, menuRect.top,
'The item top is aligned to the top of the menu.');
});
});
});
</script>
</body>
Expand Down
47 changes: 20 additions & 27 deletions test/test-menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,28 @@
<link rel="import" href="../iron-menu-behavior.html">

<dom-module id="test-menu">

<template>

<style>
:host([scrolling]) {
display: block;
max-height: 100px;
overflow: auto;
}
</style>
<content></content>

<div id="extraContent" tabindex="-1">focusable extra content</div>

</template>

<script>
Polymer({
is: 'test-menu',

behaviors: [
Polymer.IronMenuBehavior
],

get extraContent() {
return this.$.extraContent;
}
});
</script>
</dom-module>

<script>

(function() {

Polymer({

is: 'test-menu',

behaviors: [
Polymer.IronMenuBehavior
],

get extraContent() {
return this.$.extraContent;
}

});

})();

</script>

0 comments on commit c3cb651

Please sign in to comment.