Make K-5 Dashboard tabs sticky
Update the K-5 Dashboard tabs so they will stay fixed to the top of the viewport when the user scrolls. Also added some logic to shrink the "Welcome, <student>!" message at the top of the tabs when scrolling so as to not take up too much vertical space. closes LS-1862 flag = canvas_for_elementary Test plan: - Load the K-5 dashboard as a student - Scroll down on the Homeroom tab (either enroll in a bunch of courses or make the window small so that scrolling is possible) - Expect the tabs to be fixed to the top of the screen and the welcome message to shrink to what looks like an h2 - Scroll back to the top of the screen and expect the welcome message to enlarge back to the look of an h1 - Switch to the Schedule tab - Scroll back in time to load more events - Expect the tabs to be fixed to the top of the screen and more events to be loaded - Also expect the planner view to still be centered on "Today", rather than the events from the past Change-Id: If8b2297b0d1b05135a9b635588a6e8f7ac275c21 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/258657 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Product-Review: Peyton Craighill <pcraighill@instructure.com> Reviewed-by: Nate Armstrong <narmstrong@instructure.com> QA-Review: Nate Armstrong <narmstrong@instructure.com>
This commit is contained in:
parent
680ac696f4
commit
a2a795a823
|
@ -16,17 +16,19 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import React, {useEffect, useRef, useState} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import I18n from 'i18n!dashboard'
|
||||
import {Tabs} from '@instructure/ui-tabs'
|
||||
import {Heading} from '@instructure/ui-heading'
|
||||
import {
|
||||
IconBankLine,
|
||||
IconCalendarMonthLine,
|
||||
IconHomeLine,
|
||||
IconStarLightLine
|
||||
} from '@instructure/ui-icons'
|
||||
import {Tabs} from '@instructure/ui-tabs'
|
||||
import {View} from '@instructure/ui-view'
|
||||
|
||||
export const TAB_IDS = {
|
||||
HOMEROOM: 'tab-homeroom',
|
||||
|
@ -67,33 +69,57 @@ DashboardIconTab.propTypes = {
|
|||
selected: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
const DashboardTabs = ({currentTab, onRequestTabChange, tabsRef}) => {
|
||||
const DashboardTabs = ({currentTab, name, onRequestTabChange, tabsRef}) => {
|
||||
const [sticky, setSticky] = useState(false)
|
||||
const containerRef = useRef(null)
|
||||
useEffect(() => {
|
||||
// Need to copy the value of containerRef on mount so it will still be
|
||||
// available when the cleanup function runs.
|
||||
const cachedRef = containerRef.current
|
||||
// This IntersectionObserver will let us know when position: sticky has kicked in
|
||||
// on the tabs. See https://developers.google.com/web/updates/2017/09/sticky-headers
|
||||
const observer = new IntersectionObserver(
|
||||
([e]) => {
|
||||
setSticky(e.intersectionRatio < 1)
|
||||
},
|
||||
{threshold: [1]}
|
||||
)
|
||||
observer.observe(cachedRef)
|
||||
return () => observer.unobserve(cachedRef)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="ic-Dashboard-tabs">
|
||||
<Tabs elementRef={tabsRef} onRequestTabChange={onRequestTabChange} tabOverflow="scroll">
|
||||
{Object.keys(TABS).map(id => (
|
||||
<Tabs.Panel
|
||||
id={id}
|
||||
key={id}
|
||||
renderTitle={
|
||||
<DashboardIconTab
|
||||
icon={TABS[id].icon}
|
||||
label={TABS[id].label}
|
||||
selected={currentTab === id}
|
||||
/>
|
||||
}
|
||||
selected={currentTab === id}
|
||||
>
|
||||
<span />
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
<div className="ic-Dashboard-tabs" ref={containerRef}>
|
||||
<View as="div" padding="medium 0 0 0" background="primary">
|
||||
<Heading as="h1" level={sticky ? 'h2' : 'h1'} margin="0 0 small 0">
|
||||
{I18n.t('Welcome, %{name}!', {name})}
|
||||
</Heading>
|
||||
<Tabs elementRef={tabsRef} onRequestTabChange={onRequestTabChange} tabOverflow="scroll">
|
||||
{Object.keys(TABS).map(id => (
|
||||
<Tabs.Panel
|
||||
id={id}
|
||||
key={id}
|
||||
renderTitle={
|
||||
<DashboardIconTab
|
||||
icon={TABS[id].icon}
|
||||
label={TABS[id].label}
|
||||
selected={currentTab === id}
|
||||
/>
|
||||
}
|
||||
selected={currentTab === id}
|
||||
>
|
||||
<span />
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
</View>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
DashboardTabs.propTypes = {
|
||||
currentTab: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onRequestTabChange: PropTypes.func.isRequired,
|
||||
tabsRef: PropTypes.func
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ import PropTypes from 'prop-types'
|
|||
import $ from 'jquery'
|
||||
import {initializePlanner, responsiviser, store} from '@instructure/canvas-planner'
|
||||
import {ApplyTheme} from '@instructure/ui-themeable'
|
||||
import {Heading} from '@instructure/ui-heading'
|
||||
import {View} from '@instructure/ui-view'
|
||||
|
||||
import apiUserContent from 'compiled/str/apiUserContent'
|
||||
|
@ -117,11 +116,9 @@ export const K5Dashboard = ({
|
|||
<ApplyTheme theme={theme}>
|
||||
<Provider store={store}>
|
||||
<View as="section">
|
||||
<Heading level="h1" margin="medium 0 small 0">
|
||||
{I18n.t('Welcome, %{name}!', {name: display_name})}
|
||||
</Heading>
|
||||
<DashboardTabs
|
||||
currentTab={currentTab}
|
||||
name={display_name}
|
||||
onRequestTabChange={(_, {id}) => handleRequestTabChange(id)}
|
||||
tabsRef={setTabsRef}
|
||||
/>
|
||||
|
|
|
@ -152,7 +152,7 @@ const AssignmentLinks = ({color, requestTabChange, numDueToday = 0, numMissing =
|
|||
)
|
||||
}
|
||||
|
||||
AssignmentLinks.displayName = 'AssignmentsLinks'
|
||||
AssignmentLinks.displayName = 'AssignmentLinks'
|
||||
AssignmentLinks.propTypes = {
|
||||
color: PropTypes.string.isRequired,
|
||||
requestTabChange: PropTypes.func.isRequired,
|
||||
|
@ -198,7 +198,7 @@ const K5DashboardCard = ({
|
|||
const dashboardCard = (
|
||||
<div
|
||||
className="ic-DashboardCard"
|
||||
style={{opacity: isDragging ? 1 : 1, transform: 'translate3d(0,0,0)', width: responsiveWidth}}
|
||||
style={{opacity: isDragging ? 0 : 1, transform: 'translate3d(0,0,0)', width: responsiveWidth}}
|
||||
aria-label={originalName}
|
||||
data-testid="k5-dashboard-card"
|
||||
>
|
||||
|
|
|
@ -84,7 +84,9 @@ export const HomeroomPage = props => {
|
|||
)}
|
||||
{cards?.length > 0 && (
|
||||
<View as="section">
|
||||
<Heading level="h2">{I18n.t('My Subjects')}</Heading>
|
||||
<Heading level="h2" margin="medium 0 0 0">
|
||||
{I18n.t('My Subjects')}
|
||||
</Heading>
|
||||
<K5DashboardContext.Provider
|
||||
value={{assignmentsDueToday, assignmentsMissing, isStudent, responsiveSize}}
|
||||
>
|
||||
|
|
|
@ -41,12 +41,30 @@
|
|||
}
|
||||
|
||||
// These overrides are required because InstUI Tabs don't natively support icons or
|
||||
// themeable "selected" colors.
|
||||
// themeable "selected" colors. There are also some properties required for the
|
||||
// animated sticky header transitions that can't be accomplished easily via InstUI.
|
||||
.ic-Dashboard-tabs {
|
||||
position: sticky;
|
||||
// This is a trick to let our JS code know when position: sticky has kicked in.
|
||||
// See https://developers.google.com/web/updates/2017/09/sticky-headers.
|
||||
top: -1px;
|
||||
padding-top: 1px;
|
||||
border-bottom: $ic-border-color 1px solid;
|
||||
// We need a higher z-index than the planner items, some of which are at 1.
|
||||
z-index: 2;
|
||||
|
||||
& h1 {
|
||||
transition: font-size 0.1s ease;
|
||||
}
|
||||
|
||||
& [role="tab"] {
|
||||
padding: 0.75rem 1.25rem;
|
||||
}
|
||||
|
||||
& [role="tabpanel"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .ic-Dashboard-tabs__tab {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
|
|
@ -39,7 +39,7 @@ if (!Array.prototype.flat) {
|
|||
configurable: true,
|
||||
value: function flat(depth = 1) {
|
||||
if (depth === 0) return this.slice()
|
||||
return this.reduce(function(acc, cur) {
|
||||
return this.reduce(function (acc, cur) {
|
||||
if (Array.isArray(cur)) {
|
||||
acc.push(...flat.call(cur, depth - 1))
|
||||
} else {
|
||||
|
@ -87,6 +87,30 @@ if (!('MutationObserver' in window)) {
|
|||
})
|
||||
}
|
||||
|
||||
if (!('IntersectionObserver' in window)) {
|
||||
Object.defineProperty(window, 'IntersectionObserver', {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: class IntersectionObserver {
|
||||
disconnect() {
|
||||
return null
|
||||
}
|
||||
|
||||
observe() {
|
||||
return null
|
||||
}
|
||||
|
||||
takeRecords() {
|
||||
return null
|
||||
}
|
||||
|
||||
unobserve() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!('matchMedia' in window)) {
|
||||
window.matchMedia = () => ({
|
||||
matches: false,
|
||||
|
|
Loading…
Reference in New Issue