implement react router for all routes in sidenav

SideNav is now working for all routes
Instead ERB Html now is using React.Router

closes FOO-3886
closes FOO-3982
closes FOO-4326
closes FOO-4327
flag=instui_nav

test plan:
- Log in to Canvas as an Admin
- Go to RootAccount > Settings > Feature Options >
Enable New InstUI NavBar
- This should render the new SideNav
- Navbar should be displayed

Change-Id: Ic6d88dfb5284358846ee192cad6cbce7c8300da9
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/342010
QA-Review: Gustavo Bernardes <gustavo.bernardes@instructure.com>
Product-Review: Gustavo Bernardes <gustavo.bernardes@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Gustavo Bernardes <gustavo.bernardes@instructure.com>
Reviewed-by: Charley Kline <ckline@instructure.com>
This commit is contained in:
gustavo.bernardes 2024-03-04 13:00:47 -03:00 committed by Gustavo Bernardes
parent d4ca674c38
commit b1e6794484
8 changed files with 210 additions and 21 deletions

View File

@ -102,6 +102,12 @@ $ic-tooltip-arrow-size: 0.375rem;
}
}
.ic-svg-external-tool>div:first-child {
svg>path {
transform: scale(0.4);
}
}
.ic-sidenav-tray:active:hover,
.ic-user-tray:active:hover {
color: var(--ic-brand-primary);
@ -127,7 +133,6 @@ $ic-tooltip-arrow-size: 0.375rem;
.ic-user-avatar {
width: 1.875rem;
height: 1.875rem;
border: 2px solid var(--ic-brand-global-nav-avatar-border) !important;
}
.ic-collapse-div {
@ -438,8 +443,6 @@ body.primary-nav-expanded {
gap: 0.2rem !important;
width: auto !important;
height: 63.55px !important;
font-weight: 400;
transition: background-color 0.3s;
}
.ic-user-tray {
@ -463,6 +466,12 @@ body.primary-nav-expanded {
}
}
.ic-svg-external-tool>div:first-child {
svg>path {
transform: scale(0.4);
}
}
.ic-sidenav-tray:active:hover,
.ic-user-tray:active:hover {
color: var(--ic-brand-primary);
@ -488,7 +497,6 @@ body.primary-nav-expanded {
.ic-user-avatar {
width: 2.25rem;
height: 2.25rem;
border: 2px solid var(--ic-brand-global-nav-avatar-border) !important;
}
/** New SideNav CSS */

View File

@ -0,0 +1,109 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require_relative "../common"
require_relative "../../helpers/k5_common"
describe "New SideNav Navigation" do
include_context "in-process server selenium tests"
include K5Common
context "As a Teacher" do
before do
course_with_teacher_logged_in
@course.root_account.enable_feature!(:instui_nav)
end
it "minimizes and expand the side nav when clicked" do
get "/"
primary_nav_toggle = f("#sidenav-toggle")
primary_nav_toggle.click
wait_for_ajaximations
expect(f("body")).not_to have_class("primary-nav-expanded")
primary_nav_toggle.click
wait_for_ajaximations
expect(f("body")).to have_class("primary-nav-expanded")
end
describe "Profile Link" do
it "shows the profile tray upon clicking" do
get "/"
user_tray = f("#user-tray")
user_tray.click
wait_for_ajaximations
expect(f('[aria-label="User profile picture"]')).to be_displayed
end
end
describe "Courses Link" do
it "shows the courses tray upon clicking" do
get "/"
courses_tray = f("#courses-tray")
courses_tray.click
wait_for_ajaximations
expect(f("[aria-label='Courses tray']")).to be_displayed
end
end
describe "LTI Tools" do
it "shows a custom logo/link for LTI tools" do
@tool = Account.default.context_external_tools.new({
name: "Commons",
domain: "canvaslms.com",
consumer_key: "12345",
shared_secret: "secret"
})
@tool.set_extension_setting(:global_navigation, {
url: "canvaslms.com",
visibility: "admins",
display_type: "full_width",
text: "Commons",
icon_svg_path_64: "M100,37L70.1,10.5v17.6H38.6c-4.9,0-8.8,3.9-8.8,8.8s3.9,8.8,8.8,8.8h31.5v17.6L100,37z"
})
@tool.save!
get "/"
expect(f("#external-tool-tray")).to be_displayed
end
end
describe "Recent History" do
before do
Setting.set("enable_page_views", "db")
@assignment = @course.assignments.create(name: "another assessment")
@quiz = Quizzes::Quiz.create!(title: "quiz1", context: @course)
page_view_for url: app_url + "/courses/#{@course.id}/assignments/#{@assignment.id}",
context: @course,
created_at: 5.minutes.ago,
asset_category: "assignments",
asset_code: @assignment.asset_string
page_view_for url: app_url + "/courses/#{@course.id}/quizzes/#{@quiz.id}",
context: @course,
created_at: 1.minute.ago,
asset_category: "quizzes",
asset_code: @quiz.asset_string
end
it "shows the Recent History tray upon clicking" do
get "/"
wait_for_ajaximations
expect(f("#history-tray")).to be_displayed
end
end
end
end

View File

@ -49,7 +49,7 @@ const portalRouter = createBrowserRouter(
{accountGradingSettingsRoutes}
{(window.ENV.FEATURES.instui_nav || localStorage.instui_nav_dev) &&
['/', '/accounts/*', '/calendar/*', '/courses/*', '/conversations/*'].map(path => (
['/', '/*', '/*/*'].map(path => (
<Route
key={`key-to-${path}`}
path={path}

View File

@ -26,6 +26,7 @@ import MobileNavigation from './react/MobileNavigation'
import ready from '@instructure/ready'
import NewTabIndicator from './react/NewTabIndicator'
import {QueryProvider} from '@canvas/query'
import {getExternalTools} from './react/utils'
const I18n = useI18nScope('common')
@ -73,7 +74,7 @@ ready(() => {
const mobileContextNavContainer = document.getElementById('mobileContextNavContainer')
ReactDOM.render(
<QueryProvider>
<SideNav />
<SideNav externalTools={getExternalTools()} />
</QueryProvider>,
mobileContextNavContainer,
() => {

View File

@ -264,7 +264,7 @@ export default function MobileGlobalMenu(props: Props) {
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 0 64 64"
dangerouslySetInnerHTML={{__html: tool.svgPath}}
dangerouslySetInnerHTML={{__html: tool.svgPath ?? ''}}
width="1em"
height="1em"
aria-hidden="true"

View File

@ -16,11 +16,13 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react'
import React, {useMemo} from 'react'
import {Portal} from '@instructure/ui-portal'
import SideNav from './SideNav'
import {getExternalTools, type ExternalTool} from './utils'
export function Component() {
const externalTools = useMemo<ExternalTool[]>(() => getExternalTools(), [])
const mountPoint: HTMLElement | null = document.getElementById('header')
if (!mountPoint) {
return null
@ -28,7 +30,7 @@ export function Component() {
mountPoint.innerHTML = ''
return (
<Portal open={true} mountNode={mountPoint}>
<SideNav />
<SideNav externalTools={externalTools} />
</Portal>
)
}

View File

@ -29,6 +29,7 @@ import {
IconAdminLine,
IconCalendarMonthLine,
IconCanvasLogoSolid,
IconClockLine,
IconCoursesLine,
IconDashboardLine,
IconFolderLine,
@ -46,7 +47,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'
import {getUnreadCount} from './queries/unreadCountQuery'
import {getSetting, setSetting} from './queries/settingsQuery'
import {getActiveItem, getTrayLabel, getTrayPortal} from './utils'
import type {ActiveTray} from './utils'
import type {ActiveTray, ExternalTool} from './utils'
const I18n = useI18nScope('sidenav')
@ -66,25 +67,27 @@ export const InformationIconEnum = {
const defaultActiveItem = getActiveItem()
const SideNav = () => {
const SideNav = ({externalTools = []}: {externalTools?: ExternalTool[]}) => {
const [isTrayOpen, setIsTrayOpen] = useState(false)
const [activeTray, setActiveTray] = useState<ActiveTray | null>(null)
const [selectedNavItem, setSelectedNavItem] = useState<ActiveTray | ''>(defaultActiveItem)
const sideNavRef = useRef<HTMLDivElement | null>(null)
const logoRef = useRef<Element | null>(null)
const accountRef = useRef<Element | null>(null)
const adminRef = useRef<Element | null>(null)
const dashboardRef = useRef<Element | null>(null)
const coursesRef = useRef<Element | null>(null)
const adminRef = useRef<Element | null>(null)
const calendarRef = useRef<Element | null>(null)
const inboxRef = useRef<Element | null>(null)
const historyRef = useRef<Element | null>(null)
const externalTool = useRef<Element | null>(null)
const helpRef = useRef<Element | null>(null)
const logoRef = useRef<Element | null>(null)
// after tray is closed, eventually set activeTray to null
// we don't do this immediately in order to maintain animation of closing tray
useEffect(() => {
if (!isTrayOpen) {
setTimeout(() => setActiveTray(null), 150)
setTimeout(() => setActiveTray(null), 100)
}
}, [isTrayOpen])
@ -113,13 +116,11 @@ const SideNav = () => {
accountRef.current.dataset.selected = 'true'
}
break
case 'accounts':
if (adminRef.current instanceof HTMLElement) {
adminRef.current.dataset.selected = 'true'
}
break
case 'dashboard':
if (dashboardRef.current instanceof HTMLElement) {
dashboardRef.current.dataset.selected = 'true'
@ -157,6 +158,8 @@ const SideNav = () => {
contentPadding: '0.1rem',
backgroundColor: 'transparent',
hoverBackgroundColor: 'transparent',
fontWeight: 400,
linkTextDecoration: 'inherit',
}
const getHelpIcon = (): JSX.Element => {
@ -176,11 +179,11 @@ const SideNav = () => {
const countsEnabled = Boolean(
window.ENV.current_user_id && !window.ENV.current_user?.fake_student
)
const brandConfig =
(window.ENV.active_brand_config as {
variables: {'ic-brand-header-image': string}
}) ?? null
if (brandConfig) {
const variables = brandConfig.variables
logoUrl = variables['ic-brand-header-image']
@ -246,11 +249,20 @@ const SideNav = () => {
document.querySelector('#courses-tray'),
document.querySelector('#calendar-tray'),
document.querySelector('#inbox-tray'),
document.querySelector('#history-tray'),
document.querySelector('#external-tool-tray'),
document.querySelector('#help-tray'),
]
if (Array.isArray(sideNavTrays))
sideNavTrays.forEach(sideNavTray => sideNavTray?.classList.add('ic-sidenav-tray'))
const externalToolsSvgImg = ['ic-svg-external-tool', 'ic-img-external-tool']
if (Array.isArray(externalToolsSvgImg))
externalToolsSvgImg.forEach(svgImgClassName =>
document.querySelector('#external-tool-tray')?.classList.add(svgImgClassName)
)
document.querySelector('#user-tray')?.classList.add('ic-user-tray')
document.querySelector('#canvas-logo')?.classList.add('ic-canvas-logo')
document.querySelector('#brand-logo')?.classList.add('ic-brand-logo')
@ -261,6 +273,7 @@ const SideNav = () => {
const collapseButton = collapseDiv.childNodes[0] as HTMLElement
collapseDiv.classList.add('ic-collapse-div')
collapseButton.classList.add('ic-collapse-button')
collapseButton.id = 'sidenav-toggle'
if (collapseGlobalNav) document.body.classList.remove('primary-nav-expanded')
else document.body.classList.add('primary-nav-expanded')
@ -324,6 +337,7 @@ const SideNav = () => {
...navItemThemeOverride,
contentPadding: '0',
}}
minimized={collapseGlobalNav}
data-testid="sidenav-header-logo"
/>
<SideNavBar.Item
@ -356,9 +370,12 @@ const SideNav = () => {
src={window.ENV.current_user.avatar_image_url}
data-testid="sidenav-user-avatar"
showBorder="always"
frameBorder={2}
frameBorder={4}
themeOverride={{
background: 'transparent',
borderColor: '#ffffff',
borderWidthSmall: '0.2em',
borderWidthMedium: '0.2rem',
}}
/>
</Badge>
@ -372,6 +389,7 @@ const SideNav = () => {
}}
selected={selectedNavItem === 'profile'}
themeOverride={navItemThemeOverride}
minimized={collapseGlobalNav}
/>
<SideNavBar.Item
id="admin-tray"
@ -385,6 +403,7 @@ const SideNav = () => {
}}
selected={selectedNavItem === 'accounts'}
themeOverride={navItemThemeOverride}
minimized={collapseGlobalNav}
/>
<SideNavBar.Item
id="dashboard-tray"
@ -392,8 +411,9 @@ const SideNav = () => {
icon={isK5User ? <IconHomeLine data-testid="K5HomeIcon" /> : <IconDashboardLine />}
label={isK5User ? I18n.t('Home') : I18n.t('Dashboard')}
href="/"
themeOverride={navItemThemeOverride}
selected={selectedNavItem === 'dashboard'}
themeOverride={navItemThemeOverride}
minimized={collapseGlobalNav}
/>
<SideNavBar.Item
id="courses-tray"
@ -408,6 +428,7 @@ const SideNav = () => {
}}
selected={selectedNavItem === 'courses'}
themeOverride={navItemThemeOverride}
minimized={collapseGlobalNav}
/>
<SideNavBar.Item
id="calendar-tray"
@ -415,8 +436,9 @@ const SideNav = () => {
icon={<IconCalendarMonthLine />}
label={I18n.t('Calendar')}
href="/calendar"
themeOverride={navItemThemeOverride}
selected={selectedNavItem === 'calendar'}
themeOverride={navItemThemeOverride}
minimized={collapseGlobalNav}
/>
<SideNavBar.Item
id="inbox-tray"
@ -449,7 +471,51 @@ const SideNav = () => {
href="/conversations"
selected={selectedNavItem === 'conversations'}
themeOverride={navItemThemeOverride}
minimized={collapseGlobalNav}
/>
<SideNavBar.Item
id="history-tray"
elementRef={el => (historyRef.current = el)}
icon={<IconClockLine />}
label={I18n.t('History')}
href={window.ENV.page_view_update_url}
selected={selectedNavItem === 'history'}
themeOverride={navItemThemeOverride}
minimized={collapseGlobalNav}
/>
{externalTools &&
externalTools.map(tool => (
<SideNavBar.Item
key={tool.href}
id="external-tool-tray"
elementRef={el => (externalTool.current = el)}
icon={
'svgPath' in tool ? (
<svg
id="svg-external-tool"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 0 26 26"
dangerouslySetInnerHTML={{__html: tool.svgPath ?? ''}}
width="26px"
height="26px"
aria-hidden="true"
role="presentation"
focusable="false"
style={{fill: 'currentColor', fontSize: 26}}
/>
) : (
<img id="img-external-tool" width="26px" height="26px" src={tool.imgSrc} alt="" />
)
}
label={tool.label}
href={tool.href?.toString()}
selected={tool.isActive}
themeOverride={navItemThemeOverride}
minimized={collapseGlobalNav}
/>
))}
<SideNavBar.Item
id="help-tray"
icon={
@ -485,6 +551,7 @@ const SideNav = () => {
}}
selected={selectedNavItem === 'help'}
themeOverride={navItemThemeOverride}
minimized={collapseGlobalNav}
/>
</SideNavBar>
<Tray

View File

@ -20,10 +20,12 @@ import {useScope as useI18nScope} from '@canvas/i18n'
const I18n = useI18nScope('Navigation')
type CommonProperties = {
export type CommonProperties = {
href: string | null | undefined
isActive: boolean
label: string
svgPath?: string
imgSrc?: string
}
type SvgTool = CommonProperties & {svgPath: string}