Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
a284417b
Unverified
Commit
a284417b
authored
Jun 25, 2019
by
Robert Knight
Committed by
GitHub
Jun 25, 2019
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1188 from hypothesis/menu-usability-fixes
Menu usability improvements
parents
b2c1318c
807fb607
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
182 additions
and
21 deletions
+182
-21
menu.js
src/sidebar/components/menu.js
+59
-17
menu-test.js
src/sidebar/components/test/menu-test.js
+46
-0
dom.js
src/sidebar/util/dom.js
+13
-4
dom-test.js
src/sidebar/util/test/dom-test.js
+62
-0
menu-item.scss
src/styles/sidebar/components/menu-item.scss
+2
-0
No files found.
src/sidebar/components/menu.js
View file @
a284417b
...
@@ -82,32 +82,72 @@ function Menu({
...
@@ -82,32 +82,72 @@ function Menu({
return
()
=>
{};
return
()
=>
{};
}
}
const
removeListeners
=
listen
(
// Close menu when user presses Escape key, regardless of focus.
const
removeKeypressListener
=
listen
(
document
.
body
,
document
.
body
,
[
'keypress'
,
'click'
,
'mousedown'
],
[
'keypress'
],
event
=>
{
event
=>
{
if
(
event
.
type
===
'keypress'
&&
event
.
key
!
==
'Escape'
)
{
if
(
event
.
key
=
==
'Escape'
)
{
return
;
closeMenu
()
;
}
}
if
(
event
.
type
===
'mousedown'
&&
menuRef
.
current
&&
menuRef
.
current
.
contains
(
event
.
target
)
)
{
// Close the menu as soon as the user _presses_ the mouse outside the
// menu, but only when they _release_ the mouse if they click inside
// the menu.
return
;
}
closeMenu
();
}
}
);
);
return
removeListeners
;
// Close menu if user focuses an element outside the menu via any means
// (key press, programmatic focus change).
const
removeFocusListener
=
listen
(
document
.
body
,
'focus'
,
event
=>
{
if
(
!
menuRef
.
current
.
contains
(
event
.
target
))
{
closeMenu
();
}
},
{
useCapture
:
true
}
);
// Close menu if user clicks outside menu, even if on an element which
// does not accept focus.
const
removeClickListener
=
listen
(
document
.
body
,
[
'mousedown'
,
'click'
],
event
=>
{
// nb. Mouse events inside the current menu are handled elsewhere.
if
(
!
menuRef
.
current
.
contains
(
event
.
target
))
{
closeMenu
();
}
},
{
useCapture
:
true
}
);
return
()
=>
{
removeKeypressListener
();
removeClickListener
();
removeFocusListener
();
};
},
[
closeMenu
,
isOpen
]);
},
[
closeMenu
,
isOpen
]);
const
stopPropagation
=
e
=>
e
.
stopPropagation
();
// Close menu if user presses a key which activates menu items.
const
handleMenuKeyPress
=
event
=>
{
if
(
event
.
key
===
'Enter'
||
event
.
key
===
' '
)
{
closeMenu
();
}
};
return
(
return
(
<
div
className
=
"menu"
ref
=
{
menuRef
}
>
<
div
className
=
"menu"
ref
=
{
menuRef
}
// Don't close the menu if the mouse is released over one of the menu
// elements outside the content area (eg. the arrow at the top of the
// content).
onClick
=
{
stopPropagation
}
// Don't close the menu if the user presses the mouse down on menu elements
// except for the toggle button.
onMouseDown
=
{
stopPropagation
}
>
<
button
<
button
aria
-
expanded
=
{
isOpen
?
'true'
:
'false'
}
aria
-
expanded
=
{
isOpen
?
'true'
:
'false'
}
aria
-
haspopup
=
{
true
}
aria
-
haspopup
=
{
true
}
...
@@ -135,6 +175,8 @@ function Menu({
...
@@ -135,6 +175,8 @@ function Menu({
contentClass
contentClass
)}
)}
role
=
"menu"
role
=
"menu"
onClick
=
{
closeMenu
}
onKeyPress
=
{
handleMenuKeyPress
}
>
>
{
children
}
{
children
}
<
/div
>
<
/div
>
...
...
src/sidebar/components/test/menu-test.js
View file @
a284417b
...
@@ -83,6 +83,7 @@ describe('Menu', () => {
...
@@ -83,6 +83,7 @@ describe('Menu', () => {
new
Event
(
'mousedown'
),
new
Event
(
'mousedown'
),
new
Event
(
'click'
),
new
Event
(
'click'
),
((
e
=
new
Event
(
'keypress'
)),
(
e
.
key
=
'Escape'
),
e
),
((
e
=
new
Event
(
'keypress'
)),
(
e
.
key
=
'Escape'
),
e
),
new
Event
(
'focus'
),
].
forEach
(
event
=>
{
].
forEach
(
event
=>
{
it
(
`closes when the user clicks or presses the mouse outside (
${
event
.
type
}
)`
,
()
=>
{
it
(
`closes when the user clicks or presses the mouse outside (
${
event
.
type
}
)`
,
()
=>
{
const
wrapper
=
createMenu
({
defaultOpen
:
true
});
const
wrapper
=
createMenu
({
defaultOpen
:
true
});
...
@@ -122,6 +123,51 @@ describe('Menu', () => {
...
@@ -122,6 +123,51 @@ describe('Menu', () => {
assert
.
isTrue
(
isOpen
(
wrapper
));
assert
.
isTrue
(
isOpen
(
wrapper
));
});
});
[
{
eventType
:
'click'
,
key
:
null
,
shouldClose
:
true
,
},
{
eventType
:
'keypress'
,
key
:
'Enter'
,
shouldClose
:
true
,
},
{
eventType
:
'keypress'
,
key
:
' '
,
shouldClose
:
true
,
},
{
eventType
:
'keypress'
,
key
:
'a'
,
shouldClose
:
false
,
},
{
eventType
:
'focus'
,
key
:
null
,
shouldClose
:
false
,
},
].
forEach
(({
eventType
,
key
,
shouldClose
})
=>
{
it
(
`
${
shouldClose
?
'closes'
:
"doesn't close"
}
when user performs a "
${
eventType
}
" (key: "
${
key
}
") on menu content`
,
()
=>
{
const
wrapper
=
createMenu
({
defaultOpen
:
true
});
wrapper
.
find
(
'.menu__content'
).
simulate
(
eventType
,
{
key
});
assert
.
equal
(
isOpen
(
wrapper
),
!
shouldClose
);
});
});
it
(
"doesn't close when user presses on a menu element outside the toggle button or content"
,
()
=>
{
const
wrapper
=
createMenu
({
defaultOpen
:
true
});
// The event may be received either by the top `<div>` or the arrow element
// itself.
wrapper
.
find
(
'.menu'
).
simulate
(
'mousedown'
);
wrapper
.
find
(
'.menu__arrow'
).
simulate
(
'mousedown'
);
});
it
(
'aligns menu content depending on `align` prop'
,
()
=>
{
it
(
'aligns menu content depending on `align` prop'
,
()
=>
{
const
wrapper
=
createMenu
({
defaultOpen
:
true
});
const
wrapper
=
createMenu
({
defaultOpen
:
true
});
assert
.
isTrue
(
wrapper
.
exists
(
'.menu__content--align-left'
));
assert
.
isTrue
(
wrapper
.
exists
(
'.menu__content--align-left'
));
...
...
src/sidebar/util/dom.js
View file @
a284417b
'use strict'
;
'use strict'
;
/**
/**
* Attach `handler` as an event listener for `events` on `element`.
* Attach listeners for one or multiple events to an element and return a
* function that removes the listeners.
*
*
* @param {Element}
* @param {string[]} events
* @param {(event: Event) => any} listener
* @param {boolean} [options.useCapture]
* @return {function} Function which removes the event listeners.
* @return {function} Function which removes the event listeners.
*/
*/
function
listen
(
element
,
events
,
handler
)
{
function
listen
(
element
,
events
,
listener
,
{
useCapture
=
false
}
=
{}
)
{
if
(
!
Array
.
isArray
(
events
))
{
if
(
!
Array
.
isArray
(
events
))
{
events
=
[
events
];
events
=
[
events
];
}
}
events
.
forEach
(
event
=>
element
.
addEventListener
(
event
,
handler
));
events
.
forEach
(
event
=>
element
.
addEventListener
(
event
,
listener
,
useCapture
)
);
return
()
=>
{
return
()
=>
{
events
.
forEach
(
event
=>
element
.
removeEventListener
(
event
,
handler
));
events
.
forEach
(
event
=>
element
.
removeEventListener
(
event
,
listener
,
useCapture
)
);
};
};
}
}
...
...
src/sidebar/util/test/dom-test.js
0 → 100644
View file @
a284417b
'use strict'
;
const
{
listen
}
=
require
(
'../dom'
);
describe
(
'sidebar/util/dom'
,
()
=>
{
const
createElement
=
()
=>
({
addEventListener
:
sinon
.
stub
(),
removeEventListener
:
sinon
.
stub
(),
});
describe
(
'listen'
,
()
=>
{
[
true
,
false
].
forEach
(
useCapture
=>
{
it
(
'adds listeners for specified events'
,
()
=>
{
const
element
=
createElement
();
const
handler
=
sinon
.
stub
();
listen
(
element
,
[
'click'
,
'mousedown'
],
handler
,
{
useCapture
});
assert
.
calledWith
(
element
.
addEventListener
,
'click'
,
handler
,
useCapture
);
assert
.
calledWith
(
element
.
addEventListener
,
'mousedown'
,
handler
,
useCapture
);
});
});
[
true
,
false
].
forEach
(
useCapture
=>
{
it
(
'removes listeners when returned function is invoked'
,
()
=>
{
const
element
=
createElement
();
const
handler
=
sinon
.
stub
();
const
removeListeners
=
listen
(
element
,
[
'click'
,
'mousedown'
],
handler
,
{
useCapture
}
);
removeListeners
();
assert
.
calledWith
(
element
.
removeEventListener
,
'click'
,
handler
,
useCapture
);
assert
.
calledWith
(
element
.
removeEventListener
,
'mousedown'
,
handler
,
useCapture
);
});
});
});
});
src/styles/sidebar/components/menu-item.scss
View file @
a284417b
$menu-item-padding
:
10px
;
$menu-item-padding
:
10px
;
.menu-item
{
.menu-item
{
@include
outline-on-keyboard-focus
;
$item-padding
:
$menu-item-padding
;
$item-padding
:
$menu-item-padding
;
$item-height
:
40px
;
$item-height
:
40px
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment