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