Implement fancy breadcrumbs in new files
closes: CNVS-15249 CNVS-15372 test plan: go to new files and make sure the breadcrumbs look like blakes mockups especially try it when you have a bunch of crumbs and a small screen it should have a '...' that opens in a drop down. Change-Id: I632e4278aec4ee2d7373a6864d7d7c50fd067abc Reviewed-on: https://gerrit.instructure.com/41052 Reviewed-by: Clay Diffrient <cdiffrient@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> Product-Review: Ryan Shaw <ryan@instructure.com> QA-Review: Ryan Shaw <ryan@instructure.com>
This commit is contained in:
parent
2f4acbb850
commit
f44dcfe1c2
|
@ -0,0 +1,52 @@
|
|||
define [
|
||||
'jquery'
|
||||
'react'
|
||||
'react-router'
|
||||
'compiled/react/shared/utils/withReactDOM',
|
||||
], ($, React, {Link}, withReactDOM) ->
|
||||
|
||||
BreadcrumbCollapsedContainer = React.createClass
|
||||
|
||||
|
||||
propTypes:
|
||||
foldersToContain: React.PropTypes.array.isRequired
|
||||
|
||||
getInitialState: ->
|
||||
open: false
|
||||
|
||||
open: ->
|
||||
clearTimeout @timeout
|
||||
@setState open: true
|
||||
|
||||
close: ->
|
||||
@timeout = setTimeout =>
|
||||
@setState open: false
|
||||
, 100
|
||||
|
||||
render: withReactDOM ->
|
||||
li {
|
||||
href: '#'
|
||||
onMouseEnter: @open
|
||||
onMouseLeave: @close
|
||||
onFocus: @open
|
||||
onBlur: @close
|
||||
style: {position: 'relative'}
|
||||
},
|
||||
a href: '#',
|
||||
'…',
|
||||
div className: "popover bottom ef-breadcrumb-popover #{'open' if @state.open}",
|
||||
div({className: 'arrow'}),
|
||||
div className: 'popover-content',
|
||||
ul {},
|
||||
@props.foldersToContain.map (folder) =>
|
||||
li {},
|
||||
Link {
|
||||
to: (if folder.urlPath() then 'folder' else 'rootFolder')
|
||||
contextType: @props.contextType
|
||||
contextId: @props.contextId
|
||||
splat: folder.urlPath()
|
||||
activeClassName: 'active'
|
||||
className: 'ellipsis'
|
||||
},
|
||||
i({className: 'ef-big-icon icon-folder'}),
|
||||
span {}, folder.get('name')
|
|
@ -1,15 +1,120 @@
|
|||
define [
|
||||
'jquery'
|
||||
'underscore'
|
||||
'react'
|
||||
'react-router'
|
||||
'./BreadcrumbCollapsedContainer'
|
||||
'compiled/react/shared/utils/withReactDOM'
|
||||
], (React, ReactRouter, withReactDOM) ->
|
||||
], ($, _, React, {Link}, BreadcrumbCollapsedContainer, withReactDOM ) ->
|
||||
|
||||
Breadcrumbs = React.createClass
|
||||
|
||||
propTypes:
|
||||
rootTillCurrentFolder: React.PropTypes.array.isRequired
|
||||
contextType: React.PropTypes.oneOf(['users', 'groups', 'accounts', 'courses']).isRequired
|
||||
contextId: React.PropTypes.string.isRequired
|
||||
|
||||
getInitialState: ->
|
||||
{
|
||||
minCrumbWidth: 40
|
||||
maxCrumbWidth: 500
|
||||
availableWidth: 200000
|
||||
}
|
||||
|
||||
componentWillMount: ->
|
||||
# Get the existing Canvas breadcrumbs, store them, and remove them
|
||||
@fixOldCrumbs()
|
||||
|
||||
componentDidMount: ->
|
||||
# Attach the resize listener to dynamically change the components
|
||||
# involved in the breadcrumb trail.
|
||||
$(window).on('resize', @handleResize)
|
||||
@handleResize()
|
||||
|
||||
|
||||
componentWillUnmount: ->
|
||||
$(window).off('resize', @handleResize)
|
||||
|
||||
fixOldCrumbs: ->
|
||||
$oldCrumbs = $('#breadcrumbs')
|
||||
heightOfOneBreadcrumb = $oldCrumbs.find('li:visible:first').height() * 1.5
|
||||
homeName = $oldCrumbs.find('.home').text()
|
||||
$a = $oldCrumbs.find('li').eq(1).find('a')
|
||||
contextUrl = $a.attr('href')
|
||||
contextName = $a.text()
|
||||
$oldCrumbs.remove()
|
||||
|
||||
@setState({homeName, contextUrl, contextName, heightOfOneBreadcrumb})
|
||||
|
||||
handleResize: ->
|
||||
@startRecalculating(window.innerWidth)
|
||||
|
||||
startRecalculating: (newAvailableWidth) ->
|
||||
@setState({
|
||||
availableWidth: newAvailableWidth
|
||||
maxCrumbWidth: 500
|
||||
}, @checkIfCrumbsFit)
|
||||
|
||||
componentWillReceiveProps: -> setTimeout(@startRecalculating)
|
||||
|
||||
checkIfCrumbsFit: ->
|
||||
return unless @state.heightOfOneBreadcrumb
|
||||
breadcrumbHeight = $(@refs.breadcrumbs.getDOMNode()).height()
|
||||
if (breadcrumbHeight > @state.heightOfOneBreadcrumb) and (@state.maxCrumbWidth > @state.minCrumbWidth)
|
||||
maxCrumbWidth = Math.max(@state.minCrumbWidth, @state.maxCrumbWidth - 20)
|
||||
@setState({maxCrumbWidth}, @checkIfCrumbsFit)
|
||||
|
||||
renderSingleCrumb: (folder, isLastCrumb) ->
|
||||
li {},
|
||||
Link {
|
||||
to: (if folder.urlPath() then 'folder' else 'rootFolder')
|
||||
contextType: @props.contextType
|
||||
contextId: @props.contextId
|
||||
splat: folder.urlPath()
|
||||
activeClassName: 'active'
|
||||
title: folder.get('name')
|
||||
},
|
||||
span {
|
||||
className: 'ellipsis'
|
||||
style:
|
||||
maxWidth: (@state.maxCrumbWidth if isLastCrumb)
|
||||
},
|
||||
folder.get('name')
|
||||
|
||||
renderDynamicCrumbs: ->
|
||||
return [] unless @props.rootTillCurrentFolder?.length
|
||||
[foldersInMiddle..., lastFolder] = @props.rootTillCurrentFolder
|
||||
if @state.maxCrumbWidth > @state.minCrumbWidth
|
||||
@props.rootTillCurrentFolder.map (folder) => @renderSingleCrumb(folder, folder isnt lastFolder)
|
||||
else
|
||||
[
|
||||
BreadcrumbCollapsedContainer({
|
||||
foldersToContain: foldersInMiddle
|
||||
contextType: @props.contextType,
|
||||
contextId: @props.contextId
|
||||
}),
|
||||
@renderSingleCrumb(lastFolder, false)
|
||||
]
|
||||
|
||||
render: withReactDOM ->
|
||||
nav 'aria-label':'breadcrumbs', role:'navigation', className:'ef-breadcrumbs',
|
||||
@props.rootTillCurrentFolder.map (folder) =>
|
||||
ReactRouter.Link to: (if folder.urlPath() then 'folder' else 'rootFolder'), contextType: @props.contextType, contextId: @props.contextId, splat: folder.urlPath(), activeClassName: 'active',
|
||||
span className:'ellipsible', folder.get('name')
|
||||
nav {
|
||||
'aria-label':'breadcrumbs'
|
||||
role: 'navigation'
|
||||
id: 'breadcrumbs'
|
||||
ref: 'breadcrumbs'
|
||||
},
|
||||
ul {},
|
||||
# The first link (house icon)
|
||||
li className: 'home',
|
||||
a href: '/',
|
||||
i className: 'icon-home standalone-icon', title: @state.homeName,
|
||||
span className: 'screenreader-only',
|
||||
@state.homeName
|
||||
# Context link
|
||||
li {},
|
||||
a href: @state.contextUrl,
|
||||
span className: 'ellipsible',
|
||||
@state.contextName
|
||||
@renderDynamicCrumbs()...
|
||||
|
||||
|
||||
|
|
|
@ -28,18 +28,18 @@ define [
|
|||
|
||||
render: withReactDOM ->
|
||||
div null,
|
||||
Breadcrumbs({
|
||||
rootTillCurrentFolder: @state.rootTillCurrentFolder,
|
||||
contextType: @props.params.contextType,
|
||||
contextId: @props.params.contextId
|
||||
})
|
||||
Toolbar({
|
||||
currentFolder: @state.currentFolder,
|
||||
query: @props.query,
|
||||
params: @props.params
|
||||
selectedItems: @state.selectedItems
|
||||
})
|
||||
if @state.rootTillCurrentFolder
|
||||
Breadcrumbs({
|
||||
rootTillCurrentFolder: @state.rootTillCurrentFolder,
|
||||
contextType: @props.params.contextType,
|
||||
contextId: @props.params.contextId
|
||||
})
|
||||
|
||||
div className: 'ef-main',
|
||||
aside className: 'visible-desktop ef-folder-content',
|
||||
if @state.rootTillCurrentFolder
|
||||
|
|
|
@ -319,9 +319,7 @@ class FilesController < ApplicationController
|
|||
|
||||
def ember_app
|
||||
raise ActiveRecord::RecordNotFound unless tab_enabled?(@context.class::TAB_FILES) && @context.feature_enabled?(:better_file_browsing)
|
||||
clear_crumbs
|
||||
@padless = true
|
||||
@body_classes << 'full-width'
|
||||
@body_classes << 'full-width padless-content'
|
||||
js_bundle :react_files
|
||||
jammit_css :ember_files
|
||||
render :text => "".html_safe, :layout => true
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
list-style: none;
|
||||
> li {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
+ li {
|
||||
&:before {
|
||||
@extend i[class*=icon-]:before;
|
||||
@extend .icon-arrow-open-right:before;
|
||||
font-size: 10px;
|
||||
font-size: 10px !important;
|
||||
color: hsl(0,0,61%);
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
@ -20,6 +21,16 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
// This is kinda hacky, but the label for the crumbs is in a
|
||||
// <span class="ellipsis"> so we have to give it a display other than inline
|
||||
// for it to actually do ellipsis. but when we do that, it is positioned weird;
|
||||
// hence the position, top and margin-top, to get it back to where it should be.
|
||||
.ellipsis {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.icon-home:before {
|
||||
font-size: 10px;
|
||||
color: $base-font-color--subdued;
|
||||
|
|
|
@ -291,6 +291,30 @@ i[class*=current_uploads__instructions__icon-upload] {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ef-breadcrumb-popover {
|
||||
display: block;
|
||||
left: -9999px;
|
||||
top: 15px;
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
width: auto;
|
||||
&.open {
|
||||
left: -23px;
|
||||
opacity: 1;
|
||||
}
|
||||
&.popover > .arrow {
|
||||
left: 53px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Hack, Hack, Hack!
|
||||
// to make sure that there is space for the ItemCog menu to appear below the bottom thing in the list of files
|
||||
|
|
|
@ -3,38 +3,28 @@ define [
|
|||
'jquery'
|
||||
'compiled/react_files/components/Breadcrumbs'
|
||||
'compiled/models/Folder'
|
||||
'react-router'
|
||||
], (React, $, Breadcrumbs, Folder, ReactRouter) ->
|
||||
'compiled/react_files/routes'
|
||||
], (React, $, Breadcrumbs, Folder, routes) ->
|
||||
|
||||
Simulate = React.addons.TestUtils.Simulate
|
||||
|
||||
module 'FolderChild',
|
||||
setup: ->
|
||||
React.addons.TestUtils.renderIntoDocument(routes)
|
||||
|
||||
# Need to pass in setup objects but doing the same test
|
||||
breadcrumbTest = (routerObject, test) =>
|
||||
sinon.stub(ReactRouter, 'Link').returns("/some_url")
|
||||
@breadcrumbs = React.renderComponent(Breadcrumbs(rootTillCurrentFolder: routerObject), $('<div>').appendTo('body')[0])
|
||||
sampleProps =
|
||||
rootTillCurrentFolder: [new Folder(), new Folder({name: 'test_folder_name', full_name: 'course_files/test_folder_name'})]
|
||||
contextId: 'sample_course_id'
|
||||
contextType: 'courses'
|
||||
|
||||
test()
|
||||
@component = React.renderComponent(Breadcrumbs(sampleProps), $('<div>').appendTo('body')[0])
|
||||
|
||||
ReactRouter.Link.restore()
|
||||
React.unmountComponentAtNode(@breadcrumbs.getDOMNode().parentNode)
|
||||
teardown: ->
|
||||
React.unmountComponentAtNode(@component.getDOMNode().parentNode)
|
||||
|
||||
module 'Breadcrumbs#render',
|
||||
test 'generates the rootFolder link', ->
|
||||
routerObject =
|
||||
[
|
||||
new Folder(name: 'folder')
|
||||
]
|
||||
|
||||
breadcrumbTest routerObject, ->
|
||||
ok ReactRouter.Link.calledWith(to: 'rootFolder', contextType: undefined, contextId: undefined, splat: "", activeClassName: 'active'), 'called with correct parameters for rootFolder'
|
||||
test 'generates a folder link', ->
|
||||
folder = new Folder(name: 'folder')
|
||||
folder.urlPath = -> "somePath"
|
||||
routerObject =
|
||||
[
|
||||
folder
|
||||
]
|
||||
|
||||
breadcrumbTest routerObject, ->
|
||||
ok ReactRouter.Link.calledWith(to: 'folder', contextType: undefined, contextId: undefined, splat: "somePath", activeClassName: 'active'), 'called with correct parameters for a folder link'
|
||||
|
||||
test 'generates the home, rootFolder, and other links', ->
|
||||
$breadcrumbs = $(this.component.getDOMNode())
|
||||
equal $breadcrumbs.find('.home a').attr('href'), '/', 'correct home url'
|
||||
equal $breadcrumbs.find('li:nth-child(3) a').attr('href'), '/courses/sample_course_id/files', 'rootFolder link has correct url'
|
||||
equal $breadcrumbs.find('li:nth-child(4) a').attr('href'), '/courses/sample_course_id/files/folder/test_folder_name', 'correct url for child'
|
||||
equal $breadcrumbs.find('li:nth-child(4) a').text(), 'test_folder_name', 'shows folder names'
|
||||
|
|
Loading…
Reference in New Issue