Commit 259d695e authored by Eduardo Sanz García's avatar Eduardo Sanz García Committed by Eduardo

Improve consistency on bucket hovering and focusing

On #4069, we introduced a small improvement when hovering a left-pointed
bucket: focus the corresponding annotation card, in addition to the
anchor's highlight.

In this PR, we introduce the same improvement to the up and down-pointed
buckets.

In addition, I have realised we handled `onBlur` but not `onFocus`
events. I have added more complete support for keyboard navigation.

I substituted `onMouseMove` for `onMouseEnter` because it is triggered
less frequently.
parent 6ed8c93b
...@@ -20,6 +20,7 @@ function BucketButton({ bucket, onFocusAnnotations, onSelectAnnotations }) { ...@@ -20,6 +20,7 @@ function BucketButton({ bucket, onFocusAnnotations, onSelectAnnotations }) {
onSelectAnnotations([...bucket.tags], event.metaKey || event.ctrlKey); onSelectAnnotations([...bucket.tags], event.metaKey || event.ctrlKey);
} }
/** @param {boolean} hasFocus */
function setFocus(hasFocus) { function setFocus(hasFocus) {
if (hasFocus) { if (hasFocus) {
onFocusAnnotations([...bucket.tags]); onFocusAnnotations([...bucket.tags]);
...@@ -32,9 +33,10 @@ function BucketButton({ bucket, onFocusAnnotations, onSelectAnnotations }) { ...@@ -32,9 +33,10 @@ function BucketButton({ bucket, onFocusAnnotations, onSelectAnnotations }) {
<button <button
className="Buckets__button Buckets__button--left" className="Buckets__button Buckets__button--left"
onClick={event => selectAnnotations(event)} onClick={event => selectAnnotations(event)}
onMouseMove={() => setFocus(true)}
onMouseOut={() => setFocus(false)}
onBlur={() => setFocus(false)} onBlur={() => setFocus(false)}
onFocus={() => setFocus(true)}
onMouseEnter={() => setFocus(true)}
onMouseOut={() => setFocus(false)}
title={buttonTitle} title={buttonTitle}
aria-label={buttonTitle} aria-label={buttonTitle}
> >
...@@ -50,21 +52,36 @@ function BucketButton({ bucket, onFocusAnnotations, onSelectAnnotations }) { ...@@ -50,21 +52,36 @@ function BucketButton({ bucket, onFocusAnnotations, onSelectAnnotations }) {
* @param {object} props * @param {object} props
* @param {Bucket} props.bucket * @param {Bucket} props.bucket
* @param {'down'|'up'} props.direction * @param {'down'|'up'} props.direction
* @param {(tags: string[]) => void} props.onFocusAnnotations
* @param {(tags: string[], direction: 'down'|'up') => void} props.onScrollToClosestOffScreenAnchor * @param {(tags: string[], direction: 'down'|'up') => void} props.onScrollToClosestOffScreenAnchor
*/ */
function NavigationBucketButton({ function NavigationBucketButton({
bucket, bucket,
direction, direction,
onFocusAnnotations,
onScrollToClosestOffScreenAnchor, onScrollToClosestOffScreenAnchor,
}) { }) {
const buttonTitle = `Go ${direction} to next annotations (${bucket.tags.size})`; const buttonTitle = `Go ${direction} to next annotations (${bucket.tags.size})`;
/** @param {boolean} hasFocus */
function setFocus(hasFocus) {
if (hasFocus) {
onFocusAnnotations([...bucket.tags]);
} else {
onFocusAnnotations([]);
}
}
return ( return (
<button <button
className={classnames('Buckets__button', `Buckets__button--${direction}`)} className={classnames('Buckets__button', `Buckets__button--${direction}`)}
onClick={() => onClick={() =>
onScrollToClosestOffScreenAnchor([...bucket.tags], direction) onScrollToClosestOffScreenAnchor([...bucket.tags], direction)
} }
onBlur={() => setFocus(false)}
onFocus={() => setFocus(true)}
onMouseEnter={() => setFocus(true)}
onMouseOut={() => setFocus(false)}
title={buttonTitle} title={buttonTitle}
aria-label={buttonTitle} aria-label={buttonTitle}
> >
...@@ -103,6 +120,7 @@ export default function Buckets({ ...@@ -103,6 +120,7 @@ export default function Buckets({
<NavigationBucketButton <NavigationBucketButton
bucket={above} bucket={above}
direction="up" direction="up"
onFocusAnnotations={onFocusAnnotations}
onScrollToClosestOffScreenAnchor={onScrollToClosestOffScreenAnchor} onScrollToClosestOffScreenAnchor={onScrollToClosestOffScreenAnchor}
/> />
</li> </li>
...@@ -125,6 +143,7 @@ export default function Buckets({ ...@@ -125,6 +143,7 @@ export default function Buckets({
<NavigationBucketButton <NavigationBucketButton
bucket={below} bucket={below}
direction="down" direction="down"
onFocusAnnotations={onFocusAnnotations}
onScrollToClosestOffScreenAnchor={onScrollToClosestOffScreenAnchor} onScrollToClosestOffScreenAnchor={onScrollToClosestOffScreenAnchor}
/> />
</li> </li>
......
...@@ -45,6 +45,42 @@ describe('Buckets', () => { ...@@ -45,6 +45,42 @@ describe('Buckets', () => {
); );
describe('up and down navigation', () => { describe('up and down navigation', () => {
it('focuses associated anchors when mouse enters the element', () => {
const wrapper = createComponent();
wrapper.find('.Buckets__button--up').first().simulate('mouseenter');
assert.calledOnce(fakeOnFocusAnnotations);
assert.calledWith(fakeOnFocusAnnotations, ['a1', 'a2']);
});
it('removes focus on associated anchors when mouse leaves the element', () => {
const wrapper = createComponent();
wrapper.find('.Buckets__button--up').first().simulate('mouseout');
assert.calledOnce(fakeOnFocusAnnotations);
assert.calledWith(fakeOnFocusAnnotations, []);
});
it('focuses associated anchors when the element is focused', () => {
const wrapper = createComponent();
wrapper.find('.Buckets__button--up').first().simulate('focus');
assert.calledOnce(fakeOnFocusAnnotations);
assert.calledWith(fakeOnFocusAnnotations, ['a1', 'a2']);
});
it('removes focus on associated anchors when element is blurred', () => {
const wrapper = createComponent();
wrapper.find('.Buckets__button--up').first().simulate('blur');
assert.calledOnce(fakeOnFocusAnnotations);
assert.calledWith(fakeOnFocusAnnotations, []);
});
it('renders an up navigation button if there are above-screen anchors', () => { it('renders an up navigation button if there are above-screen anchors', () => {
const wrapper = createComponent(); const wrapper = createComponent();
const upButton = wrapper.find('.Buckets__button--up'); const upButton = wrapper.find('.Buckets__button--up');
...@@ -121,25 +157,34 @@ describe('Buckets', () => { ...@@ -121,25 +157,34 @@ describe('Buckets', () => {
it('focuses on associated annotations when mouse enters the element', () => { it('focuses on associated annotations when mouse enters the element', () => {
const wrapper = createComponent(); const wrapper = createComponent();
wrapper.find('.Buckets__button--left').first().simulate('mousemove'); wrapper.find('.Buckets__button--left').first().simulate('mouseenter');
assert.calledOnce(fakeOnFocusAnnotations); assert.calledOnce(fakeOnFocusAnnotations);
assert.calledWith(fakeOnFocusAnnotations, ['t1', 't2']); assert.calledWith(fakeOnFocusAnnotations, ['t1', 't2']);
}); });
it('removes focus on associated annotations when element is blurred', () => { it('removes focus on associated anchors when mouse leaves the element', () => {
const wrapper = createComponent(); const wrapper = createComponent();
wrapper.find('.Buckets__button--left').first().simulate('blur'); wrapper.find('.Buckets__button--left').first().simulate('mouseout');
assert.calledOnce(fakeOnFocusAnnotations); assert.calledOnce(fakeOnFocusAnnotations);
assert.calledWith(fakeOnFocusAnnotations, []); assert.calledWith(fakeOnFocusAnnotations, []);
}); });
it('removes focus on associated annotations when mouse leaves the element', () => { it('focuses associated anchors when the element is focused', () => {
const wrapper = createComponent(); const wrapper = createComponent();
wrapper.find('.Buckets__button--left').first().simulate('mouseout'); wrapper.find('.Buckets__button--left').first().simulate('focus');
assert.calledOnce(fakeOnFocusAnnotations);
assert.calledWith(fakeOnFocusAnnotations, ['t1', 't2']);
});
it('removes focus on associated annotations when element is blurred', () => {
const wrapper = createComponent();
wrapper.find('.Buckets__button--left').first().simulate('blur');
assert.calledOnce(fakeOnFocusAnnotations); assert.calledOnce(fakeOnFocusAnnotations);
assert.calledWith(fakeOnFocusAnnotations, []); assert.calledWith(fakeOnFocusAnnotations, []);
......
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