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
8813b8cf
Unverified
Commit
8813b8cf
authored
Jul 24, 2019
by
Robert Knight
Committed by
GitHub
Jul 24, 2019
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1187 from hypothesis/single-expanded-menu-item
Only allow one group's submenu to be expanded at a time
parents
398663fa
1660d3b0
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
618 additions
and
119 deletions
+618
-119
group-list-item.js
src/sidebar/components/group-list-item.js
+30
-26
group-list-section.js
src/sidebar/components/group-list-section.js
+21
-2
group-list.js
src/sidebar/components/group-list.js
+19
-2
menu-item.js
src/sidebar/components/menu-item.js
+63
-44
menu.js
src/sidebar/components/menu.js
+18
-0
slider.js
src/sidebar/components/slider.js
+103
-0
group-list-item-test.js
src/sidebar/components/test/group-list-item-test.js
+74
-24
group-list-section-test.js
src/sidebar/components/test/group-list-section-test.js
+39
-1
group-list-test.js
src/sidebar/components/test/group-list-test.js
+82
-14
menu-item-test.js
src/sidebar/components/test/menu-item-test.js
+45
-2
menu-test.js
src/sidebar/components/test/menu-test.js
+9
-0
slider-test.js
src/sidebar/components/test/slider-test.js
+104
-0
group-list-item.scss
src/styles/sidebar/components/group-list-item.scss
+0
-4
menu-item.scss
src/styles/sidebar/components/menu-item.scss
+11
-0
No files found.
src/sidebar/components/group-list-item.js
View file @
8813b8cf
...
@@ -2,7 +2,6 @@
...
@@ -2,7 +2,6 @@
const
propTypes
=
require
(
'prop-types'
);
const
propTypes
=
require
(
'prop-types'
);
const
{
Fragment
,
createElement
}
=
require
(
'preact'
);
const
{
Fragment
,
createElement
}
=
require
(
'preact'
);
const
{
useState
}
=
require
(
'preact/hooks'
);
const
useStore
=
require
(
'../store/use-store'
);
const
useStore
=
require
(
'../store/use-store'
);
const
{
orgName
}
=
require
(
'../util/group-list-item-common'
);
const
{
orgName
}
=
require
(
'../util/group-list-item-common'
);
...
@@ -19,19 +18,17 @@ const MenuItem = require('./menu-item');
...
@@ -19,19 +18,17 @@ const MenuItem = require('./menu-item');
*/
*/
function
GroupListItem
({
function
GroupListItem
({
analytics
,
analytics
,
defaultSubmenuOpen
=
false
,
isExpanded
,
flash
,
flash
,
group
,
group
,
groups
:
groupsService
,
groups
:
groupsService
,
onExpand
,
})
{
})
{
const
canLeaveGroup
=
group
.
type
===
'private'
;
const
canLeaveGroup
=
group
.
type
===
'private'
;
const
activityUrl
=
group
.
links
.
html
;
const
activityUrl
=
group
.
links
.
html
;
const
hasActionMenu
=
activityUrl
||
canLeaveGroup
;
const
hasActionMenu
=
activityUrl
||
canLeaveGroup
;
const
isSelectable
=
!
group
.
scopes
.
enforced
||
group
.
isScopedToUri
;
const
isSelectable
=
!
group
.
scopes
.
enforced
||
group
.
isScopedToUri
;
const
[
isExpanded
,
setExpanded
]
=
useState
(
hasActionMenu
?
defaultSubmenuOpen
:
undefined
);
const
focusedGroupId
=
useStore
(
store
=>
store
.
focusedGroupId
());
const
focusedGroupId
=
useStore
(
store
=>
store
.
focusedGroupId
());
const
isSelected
=
group
.
id
===
focusedGroupId
;
const
isSelected
=
group
.
id
===
focusedGroupId
;
...
@@ -63,7 +60,7 @@ function GroupListItem({
...
@@ -63,7 +60,7 @@ function GroupListItem({
// TODO - Fix this more cleanly in `MenuItem`.
// TODO - Fix this more cleanly in `MenuItem`.
event
.
preventDefault
();
event
.
preventDefault
();
setExpande
d
(
!
isExpanded
);
onExpan
d
(
!
isExpanded
);
};
};
const
copyLink
=
()
=>
{
const
copyLink
=
()
=>
{
...
@@ -79,23 +76,21 @@ function GroupListItem({
...
@@ -79,23 +76,21 @@ function GroupListItem({
group
.
type
===
'private'
?
'Copy invite link'
:
'Copy activity link'
;
group
.
type
===
'private'
?
'Copy invite link'
:
'Copy activity link'
;
// Close the submenu when any clicks happen which close the top-level menu.
// Close the submenu when any clicks happen which close the top-level menu.
const
collapseSubmenu
=
()
=>
setExpande
d
(
false
);
const
collapseSubmenu
=
()
=>
onExpan
d
(
false
);
return
(
return
(
<
Fragment
>
<
MenuItem
<
MenuItem
icon
=
{
group
.
logo
||
'blank'
}
icon
=
{
group
.
logo
||
'blank'
}
iconAlt
=
{
orgName
(
group
)}
iconAlt
=
{
orgName
(
group
)}
isDisabled
=
{
!
isSelectable
}
isDisabled
=
{
!
isSelectable
}
isExpanded
=
{
hasActionMenu
?
isExpanded
:
false
}
isExpanded
=
{
isExpanded
}
isSelected
=
{
isSelected
}
isSelected
=
{
isSelected
}
isSubmenuVisible
=
{
isExpanded
}
isSubmenuVisible
=
{
isExpanded
}
label
=
{
group
.
name
}
label
=
{
group
.
name
}
onClick
=
{
isSelectable
?
focusGroup
:
toggleSubmenu
}
onClick
=
{
isSelectable
?
focusGroup
:
toggleSubmenu
}
onToggleSubmenu
=
{
toggleSubmenu
}
onToggleSubmenu
=
{
toggleSubmenu
}
submenu
=
{
/
>
<
Fragment
>
{
isExpanded
&&
(
<
div
className
=
"group-list-item__submenu"
>
<
ul
onClick
=
{
collapseSubmenu
}
>
<
ul
onClick
=
{
collapseSubmenu
}
>
{
activityUrl
&&
(
{
activityUrl
&&
(
<
li
>
<
li
>
...
@@ -133,17 +128,26 @@ function GroupListItem({
...
@@ -133,17 +128,26 @@ function GroupListItem({
This
group
is
restricted
to
specific
URLs
.
This
group
is
restricted
to
specific
URLs
.
<
/p
>
<
/p
>
)}
)}
<
/
div
>
<
/
Fragment
>
)
}
}
<
/Fragment
>
/
>
);
);
}
}
GroupListItem
.
propTypes
=
{
GroupListItem
.
propTypes
=
{
group
:
propTypes
.
object
.
isRequired
,
group
:
propTypes
.
object
.
isRequired
,
/** Whether the submenu is open when the item is initially rendered. */
/**
defaultSubmenuOpen
:
propTypes
.
bool
,
* Whether the submenu for this group is expanded.
*/
isExpanded
:
propTypes
.
bool
,
/**
* Callback invoked to expand or collapse the current group.
*
* @type {(expand: boolean) => any}
*/
onExpand
:
propTypes
.
func
,
// Injected services.
// Injected services.
analytics
:
propTypes
.
object
.
isRequired
,
analytics
:
propTypes
.
object
.
isRequired
,
...
...
src/sidebar/components/group-list-section.js
View file @
8813b8cf
...
@@ -9,21 +9,40 @@ const MenuSection = require('./menu-section');
...
@@ -9,21 +9,40 @@ const MenuSection = require('./menu-section');
/**
/**
* A labeled section of the groups list.
* A labeled section of the groups list.
*/
*/
function
GroupListSection
({
groups
,
heading
})
{
function
GroupListSection
({
expandedGroup
,
onExpandGroup
,
groups
,
heading
})
{
return
(
return
(
<
MenuSection
heading
=
{
heading
}
>
<
MenuSection
heading
=
{
heading
}
>
{
groups
.
map
(
group
=>
(
{
groups
.
map
(
group
=>
(
<
GroupListItem
key
=
{
group
.
id
}
group
=
{
group
}
/
>
<
GroupListItem
key
=
{
group
.
id
}
isExpanded
=
{
group
===
expandedGroup
}
onExpand
=
{
expanded
=>
onExpandGroup
(
expanded
?
group
:
null
)}
group
=
{
group
}
/
>
))}
))}
<
/MenuSection
>
<
/MenuSection
>
);
);
}
}
GroupListSection
.
propTypes
=
{
GroupListSection
.
propTypes
=
{
/**
* The `Group` whose submenu is currently expanded, or `null` if no group
* is currently expanded.
*/
expandedGroup
:
propTypes
.
object
,
/* The list of groups to be displayed in the group list section. */
/* The list of groups to be displayed in the group list section. */
groups
:
propTypes
.
arrayOf
(
propTypes
.
object
),
groups
:
propTypes
.
arrayOf
(
propTypes
.
object
),
/* The string name of the group list section. */
/* The string name of the group list section. */
heading
:
propTypes
.
string
,
heading
:
propTypes
.
string
,
/**
* Callback invoked when a group is expanded or collapsed.
*
* The argument is the group being expanded, or `null` if the expanded group
* is being collapsed.
*
* @type {(group: Group|null) => any}
*/
onExpandGroup
:
propTypes
.
func
,
};
};
module
.
exports
=
GroupListSection
;
module
.
exports
=
GroupListSection
;
src/sidebar/components/group-list.js
View file @
8813b8cf
'use strict'
;
'use strict'
;
const
{
createElement
}
=
require
(
'preact'
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
useMemo
}
=
require
(
'preact/hooks'
);
const
{
useMemo
,
useState
}
=
require
(
'preact/hooks'
);
const
propTypes
=
require
(
'prop-types'
);
const
propTypes
=
require
(
'prop-types'
);
const
isThirdPartyService
=
require
(
'../util/is-third-party-service'
);
const
isThirdPartyService
=
require
(
'../util/is-third-party-service'
);
...
@@ -53,6 +53,13 @@ function GroupList({ serviceUrl, settings }) {
...
@@ -53,6 +53,13 @@ function GroupList({ serviceUrl, settings }) {
const
canCreateNewGroup
=
userid
&&
!
isThirdPartyUser
(
userid
,
authDomain
);
const
canCreateNewGroup
=
userid
&&
!
isThirdPartyUser
(
userid
,
authDomain
);
const
newGroupLink
=
serviceUrl
(
'groups.new'
);
const
newGroupLink
=
serviceUrl
(
'groups.new'
);
// The group whose submenu is currently open, or `null` if no group item is
// currently expanded.
//
// nb. If we create other menus that behave similarly in future, we may want
// to move this state to the `Menu` component.
const
[
expandedGroup
,
setExpandedGroup
]
=
useState
(
null
);
let
label
;
let
label
;
if
(
focusedGroup
)
{
if
(
focusedGroup
)
{
const
icon
=
focusedGroup
.
organization
.
logo
;
const
icon
=
focusedGroup
.
organization
.
logo
;
...
@@ -84,22 +91,32 @@ function GroupList({ serviceUrl, settings }) {
...
@@ -84,22 +91,32 @@ function GroupList({ serviceUrl, settings }) {
align
=
"left"
align
=
"left"
contentClass
=
"group-list__content"
contentClass
=
"group-list__content"
label
=
{
label
}
label
=
{
label
}
onOpenChanged
=
{()
=>
setExpandedGroup
(
null
)}
title
=
"Select group"
title
=
"Select group"
>
>
{
currentGroupsSorted
.
length
>
0
&&
(
{
currentGroupsSorted
.
length
>
0
&&
(
<
GroupListSection
<
GroupListSection
expandedGroup
=
{
expandedGroup
}
onExpandGroup
=
{
setExpandedGroup
}
heading
=
"Currently Viewing"
heading
=
"Currently Viewing"
groups
=
{
currentGroupsSorted
}
groups
=
{
currentGroupsSorted
}
/
>
/
>
)}
)}
{
featuredGroupsSorted
.
length
>
0
&&
(
{
featuredGroupsSorted
.
length
>
0
&&
(
<
GroupListSection
<
GroupListSection
expandedGroup
=
{
expandedGroup
}
onExpandGroup
=
{
setExpandedGroup
}
heading
=
"Featured Groups"
heading
=
"Featured Groups"
groups
=
{
featuredGroupsSorted
}
groups
=
{
featuredGroupsSorted
}
/
>
/
>
)}
)}
{
myGroupsSorted
.
length
>
0
&&
(
{
myGroupsSorted
.
length
>
0
&&
(
<
GroupListSection
heading
=
"My Groups"
groups
=
{
myGroupsSorted
}
/
>
<
GroupListSection
expandedGroup
=
{
expandedGroup
}
onExpandGroup
=
{
setExpandedGroup
}
heading
=
"My Groups"
groups
=
{
myGroupsSorted
}
/
>
)}
)}
{
canCreateNewGroup
&&
(
{
canCreateNewGroup
&&
(
...
...
src/sidebar/components/menu-item.js
View file @
8813b8cf
...
@@ -6,6 +6,7 @@ const propTypes = require('prop-types');
...
@@ -6,6 +6,7 @@ const propTypes = require('prop-types');
const
{
onActivate
}
=
require
(
'../util/on-activate'
);
const
{
onActivate
}
=
require
(
'../util/on-activate'
);
const
Slider
=
require
(
'./slider'
);
const
SvgIcon
=
require
(
'./svg-icon'
);
const
SvgIcon
=
require
(
'./svg-icon'
);
/**
/**
...
@@ -20,9 +21,8 @@ const SvgIcon = require('./svg-icon');
...
@@ -20,9 +21,8 @@ const SvgIcon = require('./svg-icon');
* The icon can either be an external SVG image, referenced by URL, or a named
* The icon can either be an external SVG image, referenced by URL, or a named
* icon rendered by an `SvgIcon`.
* icon rendered by an `SvgIcon`.
*
*
* For items that have submenus, the `MenuItem` only provides an indicator as
* For items that have submenus, the `MenuItem` will call the `renderSubmenu`
* to whether the submenu is open. The container is responsible for displaying
* prop to render the content of the submenu, when the submenu is visible.
* the submenu items beneath the current item.
*/
*/
function
MenuItem
({
function
MenuItem
({
href
,
href
,
...
@@ -36,6 +36,7 @@ function MenuItem({
...
@@ -36,6 +36,7 @@ function MenuItem({
label
,
label
,
onClick
,
onClick
,
onToggleSubmenu
,
onToggleSubmenu
,
submenu
,
})
{
})
{
const
iconClass
=
'menu-item__icon'
;
const
iconClass
=
'menu-item__icon'
;
const
iconIsUrl
=
icon
&&
icon
.
indexOf
(
'/'
)
!==
-
1
;
const
iconIsUrl
=
icon
&&
icon
.
indexOf
(
'/'
)
!==
-
1
;
...
@@ -58,51 +59,60 @@ function MenuItem({
...
@@ -58,51 +59,60 @@ function MenuItem({
const
rightIcon
=
isSubmenuItem
?
renderedIcon
:
null
;
const
rightIcon
=
isSubmenuItem
?
renderedIcon
:
null
;
return
(
return
(
<
div
// Wrapper element is a `<div>` rather than a `Fragment` to work around
aria
-
checked
=
{
isSelected
}
// limitations of Enzyme's shallow rendering.
className
=
{
classnames
(
'menu-item'
,
{
<
div
>
'menu-item--submenu'
:
isSubmenuItem
,
<
div
'is-disabled'
:
isDisabled
,
aria
-
checked
=
{
isSelected
}
'is-expanded'
:
isExpanded
,
className
=
{
classnames
(
'menu-item'
,
{
'is-selected'
:
isSelected
,
'menu-item--submenu'
:
isSubmenuItem
,
})}
'is-disabled'
:
isDisabled
,
role
=
"menuitem"
'is-expanded'
:
isExpanded
,
{...(
onClick
&&
onActivate
(
'menuitem'
,
onClick
))}
'is-selected'
:
isSelected
,
>
})}
<
div
className
=
"menu-item__action"
>
role
=
"menuitem"
{
hasLeftIcon
&&
(
{...(
onClick
&&
onActivate
(
'menuitem'
,
onClick
))}
<
div
className
=
"menu-item__icon-container"
>
{
leftIcon
}
<
/div
>
>
)}
<
div
className
=
"menu-item__action"
>
{
href
&&
(
{
hasLeftIcon
&&
(
<
a
<
div
className
=
"menu-item__icon-container"
>
{
leftIcon
}
<
/div
>
className
=
{
labelClass
}
)}
href
=
{
href
}
{
href
&&
(
target
=
"_blank"
<
a
rel
=
"noopener noreferrer"
className
=
{
labelClass
}
href
=
{
href
}
target
=
"_blank"
rel
=
"noopener noreferrer"
>
{
label
}
<
/a
>
)}
{
!
href
&&
<
span
className
=
{
labelClass
}
>
{
label
}
<
/span>
}
{
hasRightIcon
&&
(
<
div
className
=
"menu-item__icon-container"
>
{
rightIcon
}
<
/div
>
)}
<
/div
>
{
typeof
isSubmenuVisible
===
'boolean'
&&
(
<
div
className
=
"menu-item__toggle"
// We need to pass strings here rather than just the boolean attribute
// because otherwise the attribute will be omitted entirely when
// `isSubmenuVisible` is false.
aria
-
expanded
=
{
isSubmenuVisible
?
'true'
:
'false'
}
aria
-
label
=
{
`Show actions for
${
label
}
`
}
{...
onActivate
(
'button'
,
onToggleSubmenu
)}
>
>
{
label
}
<
SvgIcon
<
/a
>
name
=
{
isSubmenuVisible
?
'collapse-menu'
:
'expand-menu'
}
)}
className
=
"menu-item__toggle-icon"
{
!
href
&&
<
span
className
=
{
labelClass
}
>
{
label
}
<
/span>
}
/>
{
hasRightIcon
&&
(
<
/div
>
<
div
className
=
"menu-item__icon-container"
>
{
rightIcon
}
<
/div
>
)}
)}
<
/div
>
<
/div
>
{
typeof
isSubmenuVisible
===
'boolean'
&&
(
{
typeof
isSubmenuVisible
===
'boolean'
&&
(
<
div
<
Slider
visible
=
{
isSubmenuVisible
}
>
className
=
"menu-item__toggle"
<
div
className
=
"menu-item__submenu"
>
{
submenu
}
<
/div
>
// We need to pass strings here rather than just the boolean attribute
<
/Slider
>
// because otherwise the attribute will be omitted entirely when
// `isSubmenuVisible` is false.
aria
-
expanded
=
{
isSubmenuVisible
?
'true'
:
'false'
}
aria
-
label
=
{
`Show actions for
${
label
}
`
}
{...
onActivate
(
'button'
,
onToggleSubmenu
)}
>
<
SvgIcon
name
=
{
isSubmenuVisible
?
'collapse-menu'
:
'expand-menu'
}
className
=
"menu-item__toggle-icon"
/>
<
/div
>
)}
)}
<
/div
>
<
/div
>
);
);
...
@@ -171,6 +181,15 @@ MenuItem.propTypes = {
...
@@ -171,6 +181,15 @@ MenuItem.propTypes = {
* state of the menu.
* state of the menu.
*/
*/
onToggleSubmenu
:
propTypes
.
func
,
onToggleSubmenu
:
propTypes
.
func
,
/**
* Contents of the submenu for this item.
*
* This is typically a list of `MenuItem` components with the `isSubmenuItem`
* prop set to `true`, but can include other content as well.
* The submenu is only rendered if `isSubmenuVisible` is `true`.
*/
submenu
:
propTypes
.
any
,
};
};
module
.
exports
=
MenuItem
;
module
.
exports
=
MenuItem
;
src/sidebar/components/menu.js
View file @
8813b8cf
...
@@ -50,11 +50,21 @@ function Menu({
...
@@ -50,11 +50,21 @@ function Menu({
contentClass
,
contentClass
,
defaultOpen
=
false
,
defaultOpen
=
false
,
label
,
label
,
onOpenChanged
,
menuIndicator
=
true
,
menuIndicator
=
true
,
title
,
title
,
})
{
})
{
const
[
isOpen
,
setOpen
]
=
useState
(
defaultOpen
);
const
[
isOpen
,
setOpen
]
=
useState
(
defaultOpen
);
// Notify parent when menu is opened or closed.
const
wasOpen
=
useRef
(
isOpen
);
useEffect
(()
=>
{
if
(
typeof
onOpenChanged
===
'function'
&&
wasOpen
.
current
!==
isOpen
)
{
wasOpen
.
current
=
isOpen
;
onOpenChanged
(
isOpen
);
}
},
[
isOpen
,
onOpenChanged
]);
// Toggle menu when user presses toggle button. The menu is shown on mouse
// Toggle menu when user presses toggle button. The menu is shown on mouse
// press for a more responsive/native feel but also handles a click event for
// press for a more responsive/native feel but also handles a click event for
// activation via other input methods.
// activation via other input methods.
...
@@ -240,6 +250,14 @@ Menu.propTypes = {
...
@@ -240,6 +250,14 @@ Menu.propTypes = {
*/
*/
defaultOpen
:
propTypes
.
bool
,
defaultOpen
:
propTypes
.
bool
,
/**
* Callback invoked when the menu is opened or closed.
*
* This can be used, for example, to reset any ephemeral state that the
* menu content may have.
*/
onOpenChanged
:
propTypes
.
func
,
/**
/**
* A title for the menu. This is important for accessibility if the menu's
* A title for the menu. This is important for accessibility if the menu's
* toggle button has only an icon as a label.
* toggle button has only an icon as a label.
...
...
src/sidebar/components/slider.js
0 → 100644
View file @
8813b8cf
'use strict'
;
const
propTypes
=
require
(
'prop-types'
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
useCallback
,
useEffect
,
useRef
,
useState
}
=
require
(
'preact/hooks'
);
/**
* A container which reveals its content when `visible` is `true` using
* a sliding animation.
*
* When the content is not partially or wholly visible, it is removed from the
* DOM using `display: none` so it does not appear in the keyboard navigation
* order.
*
* Currently the only reveal/expand direction supported is top-down.
*/
function
Slider
({
children
,
visible
})
{
const
containerRef
=
useRef
(
null
);
const
[
containerHeight
,
setContainerHeight
]
=
useState
(
visible
?
'auto'
:
0
);
// Whether the content is currently partially or wholly visible. This is
// different from `visible` when collapsing as it is true until the collapse
// animation completes.
const
[
contentVisible
,
setContentVisible
]
=
useState
(
visible
);
// Adjust the container height when the `visible` prop changes.
useEffect
(()
=>
{
const
isVisible
=
containerHeight
!==
0
;
if
(
visible
===
isVisible
)
{
// Do nothing after the initial mount.
return
;
}
const
el
=
containerRef
.
current
;
if
(
visible
)
{
// Show the content synchronously so that we can measure it here.
el
.
style
.
display
=
''
;
// Make content visible in future renders.
setContentVisible
(
true
);
// When expanding, transition the container to the current fixed height
// of the content. After the transition completes, we'll reset to "auto"
// height to adapt to future content changes.
setContainerHeight
(
el
.
scrollHeight
);
}
else
{
// When collapsing, immediately change the current height to a fixed height
// (in case it is currently "auto"), force a synchronous layout,
// then transition to 0.
//
// These steps are needed because browsers will not animate transitions
// from "auto" => "0" and may not animate "auto" => fixed height => 0
// if the layout tree transitions directly from "auto" => 0.
el
.
style
.
height
=
`
${
el
.
scrollHeight
}
px`
;
// Force a sync layout.
el
.
getBoundingClientRect
();
setContainerHeight
(
0
);
}
},
[
containerHeight
,
visible
]);
const
handleTransitionEnd
=
useCallback
(()
=>
{
if
(
visible
)
{
setContainerHeight
(
'auto'
);
}
else
{
// When the collapse animation completes, stop rendering the content so
// that the browser has fewer nodes to render and the content is removed
// from keyboard navigation.
setContentVisible
(
false
);
}
},
[
setContainerHeight
,
visible
]);
return
(
<
div
// nb. Preact uses "ontransitionend" rather than "onTransitionEnd".
// See https://bugs.chromium.org/p/chromium/issues/detail?id=961193
//
// eslint-disable-next-line react/no-unknown-property
ontransitionend
=
{
handleTransitionEnd
}
ref
=
{
containerRef
}
style
=
{{
display
:
contentVisible
?
''
:
'none'
,
height
:
containerHeight
,
overflow
:
'hidden'
,
transition
:
`height 0.15s ease-in`
,
}}
>
{
children
}
<
/div
>
);
}
Slider
.
propTypes
=
{
children
:
propTypes
.
any
,
/**
* Whether the content should be visible or not.
*/
visible
:
propTypes
.
bool
,
};
module
.
exports
=
Slider
;
src/sidebar/components/test/group-list-item-test.js
View file @
8813b8cf
...
@@ -62,6 +62,11 @@ describe('GroupListItem', () => {
...
@@ -62,6 +62,11 @@ describe('GroupListItem', () => {
}
}
FakeMenuItem
.
displayName
=
'MenuItem'
;
FakeMenuItem
.
displayName
=
'MenuItem'
;
function
FakeSlider
({
children
,
visible
})
{
return
visible
?
children
:
null
;
}
FakeSlider
.
displayName
=
'Slider'
;
GroupListItem
.
$imports
.
$mock
({
GroupListItem
.
$imports
.
$mock
({
'./menu-item'
:
FakeMenuItem
,
'./menu-item'
:
FakeMenuItem
,
'../util/copy-to-clipboard'
:
{
'../util/copy-to-clipboard'
:
{
...
@@ -179,8 +184,29 @@ describe('GroupListItem', () => {
...
@@ -179,8 +184,29 @@ describe('GroupListItem', () => {
});
});
});
});
it
(
'expands submenu if `isExpanded` is `true`'
,
()
=>
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
isExpanded
:
true
});
assert
.
isTrue
(
wrapper
.
find
(
'MenuItem'
)
.
first
()
.
prop
(
'isExpanded'
)
);
});
it
(
'collapses submenu if `isExpanded` is `false`'
,
()
=>
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
isExpanded
:
false
});
assert
.
isFalse
(
wrapper
.
find
(
'MenuItem'
)
.
first
()
.
prop
(
'isExpanded'
)
);
});
it
(
'toggles submenu when toggle is clicked'
,
()
=>
{
it
(
'toggles submenu when toggle is clicked'
,
()
=>
{
const
wrapper
=
createGroupListItem
(
fakeGroup
);
const
onExpand
=
sinon
.
stub
();
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
onExpand
});
const
toggleSubmenu
=
()
=>
{
const
toggleSubmenu
=
()
=>
{
const
dummyEvent
=
new
Event
();
const
dummyEvent
=
new
Event
();
act
(()
=>
{
act
(()
=>
{
...
@@ -194,64 +220,85 @@ describe('GroupListItem', () => {
...
@@ -194,64 +220,85 @@ describe('GroupListItem', () => {
};
};
toggleSubmenu
();
toggleSubmenu
();
assert
.
isTrue
(
wrapper
.
exists
(
'ul'
));
assert
.
calledWith
(
onExpand
,
true
);
onExpand
.
resetHistory
();
wrapper
.
setProps
({
isExpanded
:
true
});
toggleSubmenu
();
toggleSubmenu
();
assert
.
isFalse
(
wrapper
.
exists
(
'ul'
)
);
assert
.
calledWith
(
onExpand
,
false
);
});
});
it
(
'does not show submenu toggle if there are no available actions'
,
()
=>
{
it
(
'does not show submenu toggle if there are no available actions'
,
()
=>
{
fakeGroup
.
links
.
html
=
null
;
fakeGroup
.
links
.
html
=
null
;
fakeGroup
.
type
=
'open'
;
fakeGroup
.
type
=
'open'
;
const
wrapper
=
createGroupListItem
(
fakeGroup
);
const
wrapper
=
createGroupListItem
(
fakeGroup
);
assert
.
is
Undefined
(
wrapper
.
find
(
'MenuItem'
).
prop
(
'isExpanded'
));
assert
.
is
False
(
wrapper
.
find
(
'MenuItem'
).
prop
(
'isExpanded'
));
});
});
function
getSubmenu
(
wrapper
)
{
const
submenu
=
wrapper
.
find
(
'MenuItem'
)
.
first
()
.
prop
(
'submenu'
);
return
mount
(
<
div
>
{
submenu
}
<
/div>
)
;
}
it
(
'does not show link to activity page if not available'
,
()
=>
{
it
(
'does not show link to activity page if not available'
,
()
=>
{
fakeGroup
.
links
.
html
=
null
;
fakeGroup
.
links
.
html
=
null
;
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
isExpanded
:
true
,
});
});
assert
.
isFalse
(
wrapper
.
exists
(
'MenuItem[label="View group activity"]'
));
const
submenu
=
getSubmenu
(
wrapper
);
assert
.
isFalse
(
submenu
.
exists
(
'MenuItem[label="View group activity"]'
));
});
});
it
(
'shows link to activity page if available'
,
()
=>
{
it
(
'shows link to activity page if available'
,
()
=>
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
isExpanded
:
true
,
});
});
assert
.
isTrue
(
wrapper
.
exists
(
'MenuItem[label="View group activity"]'
));
const
submenu
=
getSubmenu
(
wrapper
);
assert
.
isTrue
(
submenu
.
exists
(
'MenuItem[label="View group activity"]'
));
});
});
it
(
'does not show "Leave" action if user cannot leave'
,
()
=>
{
it
(
'does not show "Leave" action if user cannot leave'
,
()
=>
{
fakeGroup
.
type
=
'open'
;
fakeGroup
.
type
=
'open'
;
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
isExpanded
:
true
,
});
});
assert
.
isFalse
(
wrapper
.
exists
(
'MenuItem[label="Leave group"]'
));
const
submenu
=
getSubmenu
(
wrapper
);
assert
.
isFalse
(
submenu
.
exists
(
'MenuItem[label="Leave group"]'
));
});
});
it
(
'shows "Leave" action if user can leave'
,
()
=>
{
it
(
'shows "Leave" action if user can leave'
,
()
=>
{
fakeGroup
.
type
=
'private'
;
fakeGroup
.
type
=
'private'
;
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
isExpanded
:
true
,
});
});
assert
.
isTrue
(
wrapper
.
exists
(
'MenuItem[label="Leave group"]'
));
const
submenu
=
getSubmenu
(
wrapper
);
assert
.
isTrue
(
submenu
.
exists
(
'MenuItem[label="Leave group"]'
));
});
});
it
(
'prompts to leave group if "Leave" action is clicked'
,
()
=>
{
it
(
'prompts to leave group if "Leave" action is clicked'
,
()
=>
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
isExpanded
:
true
,
});
});
clickMenuItem
(
wrapper
,
'Leave group'
);
const
submenu
=
getSubmenu
(
wrapper
);
clickMenuItem
(
submenu
,
'Leave group'
);
assert
.
called
(
window
.
confirm
);
assert
.
called
(
window
.
confirm
);
assert
.
notCalled
(
fakeGroupsService
.
leave
);
assert
.
notCalled
(
fakeGroupsService
.
leave
);
});
});
it
(
'leaves group if "Leave" is clicked and user confirms'
,
()
=>
{
it
(
'leaves group if "Leave" is clicked and user confirms'
,
()
=>
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
isExpanded
:
true
,
});
});
window
.
confirm
.
returns
(
true
);
window
.
confirm
.
returns
(
true
);
clickMenuItem
(
wrapper
,
'Leave group'
);
const
submenu
=
getSubmenu
(
wrapper
);
clickMenuItem
(
submenu
,
'Leave group'
);
assert
.
called
(
window
.
confirm
);
assert
.
called
(
window
.
confirm
);
assert
.
calledWith
(
fakeGroupsService
.
leave
,
fakeGroup
.
id
);
assert
.
calledWith
(
fakeGroupsService
.
leave
,
fakeGroup
.
id
);
});
});
...
@@ -277,7 +324,7 @@ describe('GroupListItem', () => {
...
@@ -277,7 +324,7 @@ describe('GroupListItem', () => {
fakeGroup
.
scopes
.
enforced
=
enforced
;
fakeGroup
.
scopes
.
enforced
=
enforced
;
fakeGroup
.
isScopedToUri
=
isScopedToUri
;
fakeGroup
.
isScopedToUri
=
isScopedToUri
;
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
isExpanded
:
true
,
});
});
assert
.
equal
(
assert
.
equal
(
wrapper
wrapper
...
@@ -286,7 +333,9 @@ describe('GroupListItem', () => {
...
@@ -286,7 +333,9 @@ describe('GroupListItem', () => {
.
prop
(
'isDisabled'
),
.
prop
(
'isDisabled'
),
expectDisabled
expectDisabled
);
);
assert
.
equal
(
wrapper
.
exists
(
'.group-list-item__footer'
),
expectDisabled
);
const
submenu
=
getSubmenu
(
wrapper
);
assert
.
equal
(
submenu
.
exists
(
'.group-list-item__footer'
),
expectDisabled
);
});
});
});
});
...
@@ -316,9 +365,10 @@ describe('GroupListItem', () => {
...
@@ -316,9 +365,10 @@ describe('GroupListItem', () => {
fakeGroup
.
type
=
groupType
;
fakeGroup
.
type
=
groupType
;
fakeGroup
.
links
.
html
=
hasLink
?
'https://anno.co/groups/1'
:
null
;
fakeGroup
.
links
.
html
=
hasLink
?
'https://anno.co/groups/1'
:
null
;
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
isExpanded
:
true
,
});
});
const
copyAction
=
wrapper
const
submenu
=
getSubmenu
(
wrapper
);
const
copyAction
=
submenu
.
find
(
'MenuItem'
)
.
find
(
'MenuItem'
)
.
filterWhere
(
n
=>
n
.
prop
(
'label'
).
startsWith
(
'Copy'
));
.
filterWhere
(
n
=>
n
.
prop
(
'label'
).
startsWith
(
'Copy'
));
...
@@ -332,9 +382,9 @@ describe('GroupListItem', () => {
...
@@ -332,9 +382,9 @@ describe('GroupListItem', () => {
it
(
'copies activity URL if "Copy link" action is clicked'
,
()
=>
{
it
(
'copies activity URL if "Copy link" action is clicked'
,
()
=>
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
isExpanded
:
true
,
});
});
clickMenuItem
(
wrapper
,
'Copy invite link'
);
clickMenuItem
(
getSubmenu
(
wrapper
)
,
'Copy invite link'
);
assert
.
calledWith
(
fakeCopyText
,
'https://annotate.com/groups/groupid'
);
assert
.
calledWith
(
fakeCopyText
,
'https://annotate.com/groups/groupid'
);
assert
.
calledWith
(
fakeFlash
.
info
,
'Copied link for "Test"'
);
assert
.
calledWith
(
fakeFlash
.
info
,
'Copied link for "Test"'
);
});
});
...
@@ -342,9 +392,9 @@ describe('GroupListItem', () => {
...
@@ -342,9 +392,9 @@ describe('GroupListItem', () => {
it
(
'reports an error if "Copy link" action fails'
,
()
=>
{
it
(
'reports an error if "Copy link" action fails'
,
()
=>
{
fakeCopyText
.
throws
(
new
Error
(
'Something went wrong'
));
fakeCopyText
.
throws
(
new
Error
(
'Something went wrong'
));
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
isExpanded
:
true
,
});
});
clickMenuItem
(
wrapper
,
'Copy invite link'
);
clickMenuItem
(
getSubmenu
(
wrapper
)
,
'Copy invite link'
);
assert
.
calledWith
(
fakeCopyText
,
'https://annotate.com/groups/groupid'
);
assert
.
calledWith
(
fakeCopyText
,
'https://annotate.com/groups/groupid'
);
assert
.
calledWith
(
fakeFlash
.
error
,
'Unable to copy link'
);
assert
.
calledWith
(
fakeFlash
.
error
,
'Unable to copy link'
);
});
});
...
...
src/sidebar/components/test/group-list-section-test.js
View file @
8813b8cf
...
@@ -22,8 +22,11 @@ describe('GroupListSection', () => {
...
@@ -22,8 +22,11 @@ describe('GroupListSection', () => {
const
createGroupListSection
=
({
const
createGroupListSection
=
({
groups
=
testGroups
,
groups
=
testGroups
,
heading
=
'Test section'
,
heading
=
'Test section'
,
...
props
}
=
{})
=>
{
}
=
{})
=>
{
return
shallow
(
<
GroupListSection
groups
=
{
groups
}
heading
=
{
heading
}
/>
)
;
return
shallow
(
<
GroupListSection
groups
=
{
groups
}
heading
=
{
heading
}
{...
props
}
/
>
);
};
};
it
(
'renders heading'
,
()
=>
{
it
(
'renders heading'
,
()
=>
{
...
@@ -35,4 +38,39 @@ describe('GroupListSection', () => {
...
@@ -35,4 +38,39 @@ describe('GroupListSection', () => {
const
wrapper
=
createGroupListSection
();
const
wrapper
=
createGroupListSection
();
assert
.
equal
(
wrapper
.
find
(
GroupListItem
).
length
,
testGroups
.
length
);
assert
.
equal
(
wrapper
.
find
(
GroupListItem
).
length
,
testGroups
.
length
);
});
});
it
(
'expands group specified by `expandedGroup` prop'
,
()
=>
{
const
wrapper
=
createGroupListSection
();
for
(
let
i
=
0
;
i
<
testGroups
.
length
;
i
++
)
{
wrapper
.
setProps
({
expandedGroup
:
testGroups
[
i
]
});
wrapper
.
find
(
GroupListItem
).
forEach
((
n
,
idx
)
=>
{
assert
.
equal
(
n
.
prop
(
'isExpanded'
),
idx
===
i
);
});
}
});
it
(
"sets expanded group when a group's submenu is expanded"
,
()
=>
{
const
onExpandGroup
=
sinon
.
stub
();
const
wrapper
=
createGroupListSection
({
onExpandGroup
});
wrapper
.
find
(
GroupListItem
)
.
first
()
.
props
()
.
onExpand
(
true
);
assert
.
calledWith
(
onExpandGroup
,
testGroups
[
0
]);
});
it
(
"resets expanded group when group's submenu is collapsed"
,
()
=>
{
const
onExpandGroup
=
sinon
.
stub
();
const
wrapper
=
createGroupListSection
({
expandedGroup
:
testGroups
[
0
],
onExpandGroup
,
});
wrapper
.
find
(
GroupListItem
)
.
first
()
.
props
()
.
onExpand
(
false
);
assert
.
calledWith
(
onExpandGroup
,
null
);
});
});
});
src/sidebar/components/test/group-list-test.js
View file @
8813b8cf
...
@@ -2,6 +2,7 @@
...
@@ -2,6 +2,7 @@
const
{
shallow
}
=
require
(
'enzyme'
);
const
{
shallow
}
=
require
(
'enzyme'
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
act
}
=
require
(
'preact/test-utils'
);
const
GroupList
=
require
(
'../group-list'
);
const
GroupList
=
require
(
'../group-list'
);
...
@@ -23,6 +24,27 @@ describe('GroupList', () => {
...
@@ -23,6 +24,27 @@ describe('GroupList', () => {
).
dive
();
).
dive
();
}
}
/**
* Configure the store to populate all of the group sections.
* Must be called before group list is rendered.
*/
function
populateGroupSections
()
{
const
testGroups
=
[
{
...
testGroup
,
id
:
'zzz'
,
},
{
...
testGroup
,
id
:
'aaa'
,
},
];
fakeStore
.
getMyGroups
.
returns
(
testGroups
);
fakeStore
.
getCurrentlyViewingGroups
.
returns
(
testGroups
);
fakeStore
.
getFeaturedGroups
.
returns
(
testGroups
);
return
testGroups
;
}
beforeEach
(()
=>
{
beforeEach
(()
=>
{
fakeServiceUrl
=
sinon
.
stub
();
fakeServiceUrl
=
sinon
.
stub
();
fakeSettings
=
{
fakeSettings
=
{
...
@@ -75,20 +97,7 @@ describe('GroupList', () => {
...
@@ -75,20 +97,7 @@ describe('GroupList', () => {
});
});
it
(
'sorts groups within each section by organization'
,
()
=>
{
it
(
'sorts groups within each section by organization'
,
()
=>
{
const
testGroups
=
[
const
testGroups
=
populateGroupSections
();
{
...
testGroup
,
id
:
'zzz'
,
},
{
...
testGroup
,
id
:
'aaa'
,
},
];
fakeStore
.
getMyGroups
.
returns
(
testGroups
);
fakeStore
.
getCurrentlyViewingGroups
.
returns
(
testGroups
);
fakeStore
.
getFeaturedGroups
.
returns
(
testGroups
);
const
fakeGroupOrganizations
=
groups
=>
const
fakeGroupOrganizations
=
groups
=>
groups
.
sort
((
a
,
b
)
=>
a
.
id
.
localeCompare
(
b
.
id
));
groups
.
sort
((
a
,
b
)
=>
a
.
id
.
localeCompare
(
b
.
id
));
GroupList
.
$imports
.
$mock
({
GroupList
.
$imports
.
$mock
({
...
@@ -163,4 +172,63 @@ describe('GroupList', () => {
...
@@ -163,4 +172,63 @@ describe('GroupList', () => {
const
img
=
shallow
(
label
).
find
(
'img'
);
const
img
=
shallow
(
label
).
find
(
'img'
);
assert
.
equal
(
img
.
prop
(
'src'
),
'test-icon'
);
assert
.
equal
(
img
.
prop
(
'src'
),
'test-icon'
);
});
});
/**
* Assert that the submenu for a particular group is expanded (or none is
* if `group` is `null`).
*/
const
verifyGroupIsExpanded
=
(
wrapper
,
group
)
=>
wrapper
.
find
(
'GroupListSection'
).
forEach
(
section
=>
{
assert
.
equal
(
section
.
prop
(
'expandedGroup'
),
group
);
});
it
(
"sets or resets expanded group item when a group's submenu toggle is clicked"
,
()
=>
{
const
testGroups
=
populateGroupSections
();
// Render group list. Initially no submenu should be expanded.
const
wrapper
=
createGroupList
();
verifyGroupIsExpanded
(
wrapper
,
null
);
// Expand a group in one of the sections.
act
(()
=>
{
wrapper
.
find
(
'GroupListSection'
)
.
first
()
.
prop
(
'onExpandGroup'
)(
testGroups
[
0
]);
});
wrapper
.
update
();
verifyGroupIsExpanded
(
wrapper
,
testGroups
[
0
]);
// Reset expanded group.
act
(()
=>
{
wrapper
.
find
(
'GroupListSection'
)
.
first
()
.
prop
(
'onExpandGroup'
)(
null
);
});
wrapper
.
update
();
verifyGroupIsExpanded
(
wrapper
,
null
);
});
it
(
'resets expanded group when menu is closed'
,
()
=>
{
const
testGroups
=
populateGroupSections
();
const
wrapper
=
createGroupList
();
// Expand one of the submenus.
act
(()
=>
{
wrapper
.
find
(
'GroupListSection'
)
.
first
()
.
prop
(
'onExpandGroup'
)(
testGroups
[
0
]);
});
wrapper
.
update
();
verifyGroupIsExpanded
(
wrapper
,
testGroups
[
0
]);
// Close the menu
act
(()
=>
{
wrapper
.
find
(
'Menu'
).
prop
(
'onOpenChanged'
)(
false
);
});
wrapper
.
update
();
verifyGroupIsExpanded
(
wrapper
,
null
);
});
});
});
src/sidebar/components/test/menu-item-test.js
View file @
8813b8cf
...
@@ -56,7 +56,9 @@ describe('MenuItem', () => {
...
@@ -56,7 +56,9 @@ describe('MenuItem', () => {
});
});
it
(
'shows the submenu indicator if `isSubmenuVisible` is a boolean'
,
()
=>
{
it
(
'shows the submenu indicator if `isSubmenuVisible` is a boolean'
,
()
=>
{
const
wrapper
=
createMenuItem
({
isSubmenuVisible
:
true
});
const
wrapper
=
createMenuItem
({
isSubmenuVisible
:
true
,
});
assert
.
isTrue
(
wrapper
.
exists
(
'SvgIcon[name="collapse-menu"]'
));
assert
.
isTrue
(
wrapper
.
exists
(
'SvgIcon[name="collapse-menu"]'
));
wrapper
.
setProps
({
isSubmenuVisible
:
false
});
wrapper
.
setProps
({
isSubmenuVisible
:
false
});
...
@@ -70,7 +72,10 @@ describe('MenuItem', () => {
...
@@ -70,7 +72,10 @@ describe('MenuItem', () => {
it
(
'calls the `onToggleSubmenu` callback when the submenu toggle is clicked'
,
()
=>
{
it
(
'calls the `onToggleSubmenu` callback when the submenu toggle is clicked'
,
()
=>
{
const
onToggleSubmenu
=
sinon
.
stub
();
const
onToggleSubmenu
=
sinon
.
stub
();
const
wrapper
=
createMenuItem
({
isSubmenuVisible
:
true
,
onToggleSubmenu
});
const
wrapper
=
createMenuItem
({
isSubmenuVisible
:
true
,
onToggleSubmenu
,
});
wrapper
.
find
(
'.menu-item__toggle'
).
simulate
(
'click'
);
wrapper
.
find
(
'.menu-item__toggle'
).
simulate
(
'click'
);
assert
.
called
(
onToggleSubmenu
);
assert
.
called
(
onToggleSubmenu
);
});
});
...
@@ -98,4 +103,42 @@ describe('MenuItem', () => {
...
@@ -98,4 +103,42 @@ describe('MenuItem', () => {
// The actual icon for the submenu should be shown on the right.
// The actual icon for the submenu should be shown on the right.
assert
.
equal
(
iconSpaces
.
at
(
1
).
children
().
length
,
1
);
assert
.
equal
(
iconSpaces
.
at
(
1
).
children
().
length
,
1
);
});
});
it
(
'does not render submenu content if `isSubmenuVisible` is undefined'
,
()
=>
{
const
wrapper
=
createMenuItem
({});
assert
.
isFalse
(
wrapper
.
exists
(
'Slider'
));
});
it
(
'shows submenu content if `isSubmenuVisible` is true'
,
()
=>
{
const
wrapper
=
createMenuItem
({
isSubmenuVisible
:
true
,
submenu
:
<
div
>
Submenu
content
<
/div>
,
});
assert
.
equal
(
wrapper
.
find
(
'Slider'
).
prop
(
'visible'
),
true
);
assert
.
equal
(
wrapper
.
find
(
'Slider'
)
.
children
()
.
text
(),
'Submenu content'
);
});
it
(
'hides submenu content if `isSubmenuVisible` is false'
,
()
=>
{
const
wrapper
=
createMenuItem
({
isSubmenuVisible
:
false
,
submenu
:
<
div
>
Submenu
content
<
/div>
,
});
assert
.
equal
(
wrapper
.
find
(
'Slider'
).
prop
(
'visible'
),
false
);
// The submenu content may still be rendered if the submenu is currently
// collapsing.
assert
.
equal
(
wrapper
.
find
(
'Slider'
)
.
children
()
.
text
(),
'Submenu content'
);
});
});
});
src/sidebar/components/test/menu-test.js
View file @
8813b8cf
...
@@ -51,6 +51,15 @@ describe('Menu', () => {
...
@@ -51,6 +51,15 @@ describe('Menu', () => {
assert
.
isFalse
(
isOpen
(
wrapper
));
assert
.
isFalse
(
isOpen
(
wrapper
));
});
});
it
(
'calls `onOpenChanged` prop when menu is opened or closed'
,
()
=>
{
const
onOpenChanged
=
sinon
.
stub
();
const
wrapper
=
createMenu
({
onOpenChanged
});
wrapper
.
find
(
'button'
).
simulate
(
'click'
);
assert
.
calledWith
(
onOpenChanged
,
true
);
wrapper
.
find
(
'button'
).
simulate
(
'click'
);
assert
.
calledWith
(
onOpenChanged
,
false
);
});
it
(
'opens and closes when the toggle button is pressed'
,
()
=>
{
it
(
'opens and closes when the toggle button is pressed'
,
()
=>
{
const
wrapper
=
createMenu
();
const
wrapper
=
createMenu
();
assert
.
isFalse
(
isOpen
(
wrapper
));
assert
.
isFalse
(
isOpen
(
wrapper
));
...
...
src/sidebar/components/test/slider-test.js
0 → 100644
View file @
8813b8cf
'use strict'
;
const
{
mount
}
=
require
(
'enzyme'
);
const
{
createElement
}
=
require
(
'preact'
);
const
Slider
=
require
(
'../slider'
);
describe
(
'Slider'
,
()
=>
{
let
container
;
const
createSlider
=
(
props
=
{})
=>
{
return
mount
(
<
Slider
visible
=
{
false
}
{...
props
}
>
<
div
style
=
{{
width
:
100
,
height
:
200
}}
>
Test
content
<
/div
>
<
/Slider>
,
{
attachTo
:
container
}
);
};
beforeEach
(()
=>
{
container
=
document
.
createElement
(
'div'
);
document
.
body
.
appendChild
(
container
);
});
afterEach
(()
=>
{
container
.
remove
();
});
it
(
'should render collapsed if `visible` is false on mount'
,
()
=>
{
const
wrapper
=
createSlider
({
visible
:
false
});
const
{
height
}
=
wrapper
.
getDOMNode
().
getBoundingClientRect
();
assert
.
equal
(
height
,
0
);
// The content shouldn't be rendered, so it doesn't appear in the keyboard
// navigation order.
assert
.
equal
(
wrapper
.
getDOMNode
().
style
.
display
,
'none'
);
});
it
(
'should render expanded if `visible` is true on mount'
,
()
=>
{
const
wrapper
=
createSlider
({
visible
:
true
});
const
{
height
}
=
wrapper
.
getDOMNode
().
getBoundingClientRect
();
assert
.
equal
(
height
,
200
);
});
it
(
'should transition to expanded if `visible` changes to `true`'
,
()
=>
{
const
wrapper
=
createSlider
({
visible
:
false
});
wrapper
.
setProps
({
visible
:
true
});
const
containerStyle
=
wrapper
.
getDOMNode
().
style
;
assert
.
equal
(
containerStyle
.
height
,
'200px'
);
});
it
(
'should transition to collapsed if `visible` changes to `false`'
,
done
=>
{
const
wrapper
=
createSlider
({
visible
:
true
});
wrapper
.
setProps
({
visible
:
false
});
setTimeout
(()
=>
{
const
{
height
}
=
wrapper
.
getDOMNode
().
getBoundingClientRect
();
assert
.
equal
(
height
,
0
);
done
();
},
1
);
});
it
(
'should set the container height to "auto" when an expand transition finishes'
,
()
=>
{
const
wrapper
=
createSlider
({
visible
:
false
});
wrapper
.
setProps
({
visible
:
true
});
let
containerStyle
=
wrapper
.
getDOMNode
().
style
;
assert
.
equal
(
containerStyle
.
height
,
'200px'
);
wrapper
.
find
(
'div'
)
.
first
()
.
simulate
(
'transitionend'
);
containerStyle
=
wrapper
.
getDOMNode
().
style
;
assert
.
equal
(
containerStyle
.
height
,
'auto'
);
});
it
(
'should stop rendering content when a collapse transition finishes'
,
()
=>
{
const
wrapper
=
createSlider
({
visible
:
true
});
wrapper
.
setProps
({
visible
:
false
});
wrapper
.
find
(
'div'
)
.
first
()
.
simulate
(
'transitionend'
);
const
containerStyle
=
wrapper
.
getDOMNode
().
style
;
assert
.
equal
(
containerStyle
.
display
,
'none'
);
});
[
true
,
false
].
forEach
(
visible
=>
{
it
(
'should handle unmounting while expanding or collapsing'
,
()
=>
{
const
wrapper
=
createSlider
({
visible
});
wrapper
.
setProps
({
visible
:
!
visible
});
wrapper
.
unmount
();
});
});
});
src/styles/sidebar/components/group-list-item.scss
View file @
8813b8cf
.group-list-item__submenu
{
border-bottom
:
solid
1px
$grey-2
;
}
// Footer to display at the bottom of a menu item.
// Footer to display at the bottom of a menu item.
.group-list-item__footer
{
.group-list-item__footer
{
background-color
:
$grey-1
;
background-color
:
$grey-1
;
...
...
src/styles/sidebar/components/menu-item.scss
View file @
8813b8cf
...
@@ -28,7 +28,13 @@ $menu-item-padding: 10px;
...
@@ -28,7 +28,13 @@ $menu-item-padding: 10px;
font-weight
:
normal
;
font-weight
:
normal
;
}
}
// Animate the background color transition of menu items with expanding submenus.
// The timing should match the expansion of the submenu.
transition
:
background
0
.15s
ease-in
;
&
.is-expanded
{
&
.is-expanded
{
// Set the background color of menu items with submenus to match the
// submenu items.
background
:
$grey-1
;
background
:
$grey-1
;
}
}
}
}
...
@@ -142,3 +148,8 @@ $menu-item-padding: 10px;
...
@@ -142,3 +148,8 @@ $menu-item-padding: 10px;
height
:
12px
;
height
:
12px
;
}
}
}
}
// The container for open submenus
.menu-item__submenu
{
border-bottom
:
solid
1px
$grey-2
;
}
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