diff --git a/cypress.config.ts b/cypress.config.ts index cc357cc740..acd0399f03 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -21,12 +21,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import { defineConfig } from 'cypress' +import { defineConfig } from "cypress"; // TODO figure out why this isn't working: // import webpackConfig from '@instructure/ui-webpack-config' // eslint-disable-next-line @instructure/no-relative-imports -import webpackConfig from './packages/ui-karma-config/lib/legacyBaseWebpackConfig' +import webpackConfig from "./packages/ui-karma-config/lib/legacyBaseWebpackConfig"; export default defineConfig({ numTestsKeptInMemory: 1, @@ -37,21 +37,29 @@ export default defineConfig({ execTimeout: 120000, taskTimeout: 60000, retries: { - experimentalStrategy: 'detect-flake-and-pass-on-threshold', + experimentalStrategy: "detect-flake-and-pass-on-threshold", experimentalOptions: { - maxRetries: 10, - passesRequired: 1 + maxRetries: 2, + passesRequired: 1, }, openMode: true, - runMode: true + runMode: true, }, + screenshotOnRunFailure: false, + component: { - excludeSpecPattern: 'regression-test/**', + excludeSpecPattern: "regression-test/**", devServer: { - framework: 'react', - bundler: 'webpack', - webpackConfig - } - } -}) + framework: "react", + bundler: "webpack", + webpackConfig, + }, + }, + + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, +}); diff --git a/cypress/component/SubNav.cy.tsx b/cypress/component/SubNav.cy.tsx new file mode 100644 index 0000000000..7fc116bf9a --- /dev/null +++ b/cypress/component/SubNav.cy.tsx @@ -0,0 +1,67 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React from 'react' +import { SubNav } from '../../packages/ui-top-nav-bar' +import '../support/component' +import 'cypress-real-events' + + +describe('', () => { + it('should call onClick and redirect when item is clicked', async () => { + const onClickSpy = cy.spy().as('onClickSpy') + + cy.mount( + + ) + cy.contains('Assignments').realClick() + + cy.get('@onClickSpy').should('have.been.called') + cy.url().should('contain', '#-test') + }) + + it('should be able to select with keyboard', async () => { + const onClickSpy = cy.spy().as('onClickSpy') + cy.mount( + + ) + cy.contains('Home').focus() + cy.realPress(['Tab']).wait(100) + cy.realPress(['Enter']).wait(100) + + cy.get('@onClickSpy').should('have.been.called') + cy.url().should('contain', '#-test2') + }) +}) diff --git a/cypress/e2e/CanvasTopNav.cy.ts b/cypress/e2e/CanvasTopNav.cy.ts new file mode 100644 index 0000000000..9d44058504 --- /dev/null +++ b/cypress/e2e/CanvasTopNav.cy.ts @@ -0,0 +1,424 @@ +describe('New TopNavBar', () => { + describe('SideNavBar', () => { + it('should change URL on SideNavBar item selection', () => { + cy.visit('http://localhost:9090/') + + cy.url().should('not.contain', 'studio') + cy.contains('Studio').should('be.visible') + + cy.contains('Studio') + .click().wait(100) + + cy.url().should('contain', '/studio') + + cy.contains('Account') + .click().wait(100) + + cy.url().should('contain', '/account') + + cy.contains('Courses') + .click().wait(100) + cy.contains('Course 1') + .click().wait(100) + + cy.url().should('contain', '/course1') + }) + + it('should trigger onclick callback on SideNavBar item selection', () => { + cy.visit('http://localhost:9090/') + + cy.on('window:alert', cy.stub().as('alertStub')) + const expectedAlertText = 'Help clicked' + + cy.contains('Help') + .click().wait(100) + + cy.get('@alertStub') + .should('have.been.calledOnceWith', expectedAlertText) + }) + + it('should show main navigation as SideNavBar with labelled items', async () => { + cy.visit('http://localhost:9090/') + + cy.contains('This is home').should('exist') + cy.get('nav[data-cid="SideNavBar"]').as('SideNavBar').should('exist') + + cy.get('@SideNavBar').find('li').should('have.length', 6) + + cy.contains('Account').should('be.visible') + cy.get('svg[name="IconUser"]').should('be.visible') + + cy.contains('Courses').should('be.visible') + cy.get('svg[name="IconCourses"]').should('be.visible') + + cy.contains('Help').should('be.visible') + cy.get('svg[name="IconQuestion"]').should('be.visible') + + cy.contains('button', 'Minimize SideNavBar').should('be.visible') + }) + + it('should minimize SideNavBar and show only icons as items', async () => { + cy.visit('http://localhost:9090/') + + cy.contains('This is home').should('exist') + cy.get('li[class$="-navigation__list"]').should('have.length', 6) + + cy.contains('Account').should('be.visible') + cy.get('svg[name="IconUser"]').should('be.visible') + + cy.contains('Courses').should('be.visible') + cy.get('svg[name="IconCourses"]').should('be.visible') + + cy.contains('Help').should('be.visible') + cy.get('svg[name="IconQuestion"]').should('be.visible') + + cy.contains('button', 'Minimize SideNavBar') + .click().wait(100) + + cy.get('li[class$="-navigation__list"]').should('have.length', 6) + + cy.contains('Account').should('not.be.visible') + cy.get('svg[name="IconUser"]').should('be.visible') + + cy.contains('Courses').should('not.be.visible') + cy.get('svg[name="IconCourses"]').should('be.visible') + + cy.contains('Help').should('not.be.visible') + cy.get('svg[name="IconQuestion"]').should('be.visible') + }) + + it('should show main navigation as topNavBar and burger menu in mobile view', () => { + cy.viewport(300, 400) + cy.visit('http://localhost:9090') + + cy.contains('This is home').should('exist') + cy.get('div[class$="-mobileTopNavTopBar"]').as('TopNav').should('exist') + cy.get('@TopNav').find('svg[name="IconAnalytics"]').should('be.visible') + cy.get('@TopNav').find('svg[name="IconAlerts"]').should('be.visible') + cy.contains('button', 'burger').should('be.visible') + + cy.contains('Account').should('not.be.visible') + cy.get('svg[name="IconUser"]').should('not.be.visible') + cy.contains('Courses').should('not.be.visible') + cy.get('svg[name="IconCourses"]').should('not.be.visible') + cy.contains('Help').should('not.be.visible') + cy.get('svg[name="IconQuestion"]').should('not.be.visible') + + cy.contains('button', 'burger') + .click().wait(100) + + cy.contains('Account').should('be.visible') + cy.get('svg[name="IconUser"]').should('be.visible') + cy.contains('Courses').should('be.visible') + cy.get('svg[name="IconCourses"]').should('be.visible') + cy.contains('Help').should('be.visible') + cy.get('svg[name="IconQuestion"]').should('be.visible') + + cy.contains('Courses') + .click().wait(100) + + cy.contains('Courses1').should('be.visible') + + cy.contains('Back') + .click().wait(100) + + cy.get('Courses1').should('not.exist') + + cy.get('svg[name="IconX"]').closest('button') + .click().wait(100) + + cy.contains('Courses').should('not.be.visible') + }) + + it('should close burger menu after item selection in mobile view', async () => { + cy.viewport(300, 400) + cy.visit('http://localhost:9090/') + + cy.contains('This is the dashboard').should('not.exist') + cy.get('ul[class$="-options__list"]').should('not.be.visible') + + cy.contains('button', 'burger') + .click().wait(100) + + cy.contains('Dashboard').should('be.visible') + cy.get('ul[class$="-options__list"]').should('be.visible') + + cy.contains('Dashboard') + .click().wait(100) + + cy.contains('This is the dashboard').should('be.visible') + cy.contains('Dashboard').should('not.be.visible') + cy.get('ul[class$="-options__list"]').should('not.be.visible') + }) + }) + + describe('Tray', () => { + it('should open and close SideNav Tray properly', async () => { + cy.visit('http://localhost:9090/') + + cy.contains('button', 'Courses') + .click().wait(100) + + cy.contains('Course 1').should('be.visible') + cy.contains('Course 2').should('be.visible') + + cy.get('svg[name="IconX"]').closest('button') + .click().wait(100) + + cy.contains('Course 1').should('not.be.visible') + cy.contains('Course 2').should('not.be.visible') + }) + + it('should close SideNav Tray menu after item selection', async () => { + cy.visit('http://localhost:9090/') + + cy.contains('This is the first course home page').should('not.exist') + cy.get('span[data-cid="Tray"]').should('not.exist') + cy.contains('Course 2').should('not.exist') + + cy.contains('Courses') + .click().wait(100) + + cy.get('span[data-cid="Tray"]').should('be.visible') + cy.contains('Course 2').should('be.visible') + + cy.contains('Course 1') + .click().wait(100) + + cy.contains('This is the first course home page').should('be.visible') + cy.get('span[data-cid="Tray"]').should('not.be.visible') + cy.contains('Course 2').should('not.be.visible') + }) + }) + + describe('LTI', () => { + it('should display the LTI topNavBar only on specific pages', () => { + cy.visit('http://localhost:9090/') + + cy.get('div[class$="-desktopTopNavContainer"]') + .should('not.exist') + + cy.contains('Studio') + .click().wait(100) + + cy.get('div[class$="-desktopTopNavContainer"]') + .should('exist') + }) + + it('should show LTI topNavBar menu items', () => { + cy.visit('http://localhost:9090/studio') + + cy.get('div[class$="-desktopTopNavStart"]').as('menu') + .should('exist') + + cy.get('@menu') + .find('li').should('have.length', 4) + }) + + it('should highlight selected LTI topNavBar menu item', () => { + cy.visit('http://localhost:9090/studio') + + cy.get('div[class$="-desktopTopNavStart"]') + .find('li').eq(0).as('firstMenuItem') + + cy.get('@firstMenuItem').find('button') + .should('have.attr', 'aria-current', 'page') + + cy.get('@firstMenuItem').find('div[class$="-topNavBarItem__container"]') + .then(($container) => { + cy.window().then((win) => { + const afterStyles = win.getComputedStyle($container[0], '::after') + + expect(parseFloat(afterStyles.getPropertyValue('height'))).to.be.greaterThan(0) + }) + }) + + cy.get('div[class$="-desktopTopNavStart"]') + .find('li').eq(1).as('secondMenuItem') + + cy.get('@secondMenuItem') + .find('button').should('not.have.attr', 'aria-current') + }) + + it('should highlight focused LTI topNavBar menu item', () => { + cy.visit('http://localhost:9090/studio') + + cy.contains('Settings').closest('button').as('button') + + cy.get('@button').focus() + .then(($button) => { + cy.window().then((win) => { + const beforeStyles = win.getComputedStyle($button[0], '::before') + const border = beforeStyles.getPropertyValue('border') + + expect(border).to.contains('2px solid') + }) + }) + }) + + it('should show LTI topNavBar SubMenu as menuitem', () => { + cy.visit('http://localhost:9090/studio') + + cy.get('div[class$="-desktopTopNavStart"]') + .find('li').eq(3).as('subMenu') + + cy.get('@subMenu').click().wait(100) + + cy.contains('Link One').should('be.visible') + + cy.contains('Link One').click().wait(100) + + cy.contains('Level 2 Option One').should('be.visible') + cy.get('Link One').should('not.exist') + + cy.contains('Back').click().wait(100) + + cy.contains('Link One').should('be.visible') + + cy.contains('Link Two').click().wait(100) + + cy.url().should('contain', '/#TopNavBar') + cy.get('Link Two').should('not.exist') + }) + + it('should show LTI topNavBar buttons', () => { + cy.visit('http://localhost:9090/studio') + + cy.get('div[class$="-desktopTopNavEnd"]') + .find('button').should('have.length', 2) + + cy.get('div[class$="-desktopTopNavEnd"]') + .find('button').eq(0).as('AddLineButton') + + cy.get('@AddLineButton') + .should('have.text', 'AddLine') + + cy.get('@AddLineButton').find('svg[name="IconAdd"]') + .should('exist') + }) + + it('should trigger onclick callback on LTI topNavBar button click', () => { + cy.visit('http://localhost:9090/studio') + + cy.on('window:alert', cy.stub().as('alertStub')) + + cy.contains('AddLine') + .click().wait(100) + + cy.get('@alertStub') + .should('have.been.calledWith', 'Button 1') + + cy.contains('AdminLine') + .click().wait(100) + + cy.get('@alertStub') + .should('have.been.calledWith', 'Button 2') + }) + + it('should not show LTI topNavBar menu items and buttons in mobile view', () => { + cy.viewport(300, 400) + cy.visit('http://localhost:9090/studio') + + cy.wait(100) + cy.contains('LTI VIEW TEST').should('exist') + + cy.get('AddLine').should('not.exist') + cy.get('svg[name="IconAdd"]').should('not.exist') + + cy.get('div[class$="-topNavBarMenuItems"]').should('not.exist') + cy.get('Settings').should('not.exist') + cy.get('Submenu').should('not.exist') + }) + + it('should close LTI TopNav SubMenu after item selection', async () => { + cy.visit('http://localhost:9090/studio') + + cy.contains('LTI VIEW TEST').should('be.visible') + cy.get('ul[class$="-options__list"]').should('not.exist') + + cy.contains('Submenu') + .click().wait(100) + + cy.get('ul[class$="-options__list"]').should('be.visible') + cy.contains('Link One').should('be.visible') + + cy.contains('Link Two') + .click().wait(100) + + cy.get('ul[class$="-options__list"]').should('not.exist') + cy.contains('Link One').should('not.exist') + }) + }) + + describe('SubNav', () => { + it('should show subNav on specific page', () => { + cy.visit('http://localhost:9090/') + + cy.contains('This is home').should('exist') + cy.get('div[class$="-subNavContainer"]').should('not.exist') + cy.contains('Announcements').should('not.exist') + + cy.contains('Courses').click().wait(100) + cy.contains('Course 1').click().wait(100) + + cy.contains('This is the first course home page').should('be.visible') + cy.get('div[class$="-subNavContainer"]').should('exist') + cy.contains('Announcements').should('be.visible') + }) + + it('should show subNav in mobile view', () => { + cy.viewport(300, 400) + cy.visit('http://localhost:9090/') + + cy.contains('This is home').should('exist') + cy.get('div[class$="-subNavContainer"]').should('not.exist') + cy.contains('Announcements').should('not.exist') + + cy.contains('button', 'burger').click().wait(100) + cy.contains('Courses').click().wait(100) + cy.contains('Courses1').click().wait(100) + + cy.contains('This is the first course home page').should('be.visible') + cy.get('div[class$="-subNavContainer"]').should('exist') + cy.contains('Announcements').should('be.visible') + }) + }) + + describe('Breadcrumbs', () => { + it('should show Breadcrumbs', () => { + cy.visit('http://localhost:9090/') + + cy.contains('This is home').should('exist') + cy.get('ol[class$="-breadcrumb"]').should('not.be.visible') + cy.contains('Crumb 1').should('not.be.visible') + cy.url().should('not.contain', '#crumb1') + + cy.contains('Courses').click().wait(100) + cy.contains('Course 1').click().wait(100) + + cy.contains('This is the first course home page').should('be.visible') + cy.get('ol[class$="-breadcrumb"]').should('be.visible') + cy.contains('Crumb 1').should('be.visible') + + cy.contains('Crumb 1').click().wait(100) + cy.url().should('contain', '#crumb1') + }) + + it('should not show Breadcrumbs in mobile view', () => { + cy.viewport(300, 400) + cy.visit('http://localhost:9090/') + + cy.contains('This is home').should('exist') + cy.get('ol[class$="-breadcrumb"]').should('not.exist') + cy.contains('Crumb 1').should('not.exist') + + cy.contains('button', 'burger').click().wait(100) + cy.contains('Courses').click().wait(100) + cy.contains('Courses1').click().wait(100) + + cy.contains('This is the first course home page').should('be.visible') + cy.get('ol[class$="-breadcrumb"]').should('not.exist') + cy.contains('Crumb 1').should('not.exist') + }) + }) +}) \ No newline at end of file diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 0000000000..f80f74f8e1 --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f0ab1f0f01..e4ca2c3fd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6592,6 +6592,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.9", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", @@ -24311,6 +24320,38 @@ "dev": true, "license": "MIT" }, + "node_modules/react-router": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/read": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/read/-/read-3.0.1.tgz", @@ -30485,6 +30526,7 @@ "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^6.18.0", "semver": "^7.7.2", "uuid": "^11.1.0", "webpack-merge": "^6.0.1" diff --git a/packages/__docs__/components.ts b/packages/__docs__/components.ts index 5cb71d0db0..c88d7c0309 100644 --- a/packages/__docs__/components.ts +++ b/packages/__docs__/components.ts @@ -128,7 +128,7 @@ export { ToggleBlockquote } from './src/ToggleBlockquote' export { InstUISettingsProvider } from '@instructure/emotion' export { Drilldown } from '@instructure/ui-drilldown' export { SourceCodeEditor } from '@instructure/ui-source-code-editor' -export { TopNavBar } from '@instructure/ui-top-nav-bar' +export { TopNavBar, MobileTopNav } from '@instructure/ui-top-nav-bar' export { TruncateList } from '@instructure/ui-truncate-list' export { canvas, diff --git a/packages/__docs__/package.json b/packages/__docs__/package.json index 29aa9dcadb..7de02be351 100644 --- a/packages/__docs__/package.json +++ b/packages/__docs__/package.json @@ -122,6 +122,7 @@ "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^6.18.0", "semver": "^7.7.2", "uuid": "^11.1.0", "webpack-merge": "^6.0.1" diff --git a/packages/__docs__/src/App/index.tsx b/packages/__docs__/src/App/index.tsx index 05010c9974..e60510159e 100644 --- a/packages/__docs__/src/App/index.tsx +++ b/packages/__docs__/src/App/index.tsx @@ -22,821 +22,561 @@ * SOFTWARE. */ +import { Component } from 'react' +import { Route, Routes, useNavigate } from 'react-router-dom' +import { withStyle, InstUISettingsProvider } from '@instructure/emotion' +import { CanvasTopNav, SubNav } from '@instructure/ui-top-nav-bar' import { - Component, - createContext, - LegacyRef, - ReactElement, - SyntheticEvent, - createRef -} from 'react' - -import { Alert } from '@instructure/ui-alerts' -import { InstUISettingsProvider, withStyle, Global } from '@instructure/emotion' -import { Flex } from '@instructure/ui-flex' -import { Text } from '@instructure/ui-text' -import { View } from '@instructure/ui-view' -import { AccessibleContent } from '@instructure/ui-a11y-content' -import { Mask } from '@instructure/ui-overlays' -import { IconButton } from '@instructure/ui-buttons' -import { Tray } from '@instructure/ui-tray' -import { Link } from '@instructure/ui-link' -import { addMediaQueryMatchListener } from '@instructure/ui-responsive' -import type { QueriesMatching } from '@instructure/ui-responsive' -import { - IconHamburgerSolid, - IconHeartLine, - IconXSolid + IconAddLine, + IconAdminLine, + IconAlertsLine, + IconAnalyticsLine, + IconArcLine, + IconCoursesLine, + IconDashboardLine, + IconQuestionLine, + IconUserLine } from '@instructure/ui-icons' - -import { ContentWrap } from '../ContentWrap' -import { Document } from '../Document' -import { Header } from '../Header' -import { Heading } from '../Heading' -import { Hero } from '../Hero' -import { Nav } from '../Nav' -import { Theme } from '../Theme' -import { Select } from '../Select' -import { Section } from '../Section' -import IconsPage from '../Icons' -import { compileMarkdown } from '../compileMarkdown' - -import { fetchVersionData, versionInPath } from '../versionData' - import generateStyle from './styles' import generateComponentTheme from './theme' -import { LoadingScreen } from '../LoadingScreen' -import * as EveryComponent from '../../components' -import type { AppProps, AppState, DocData, LayoutSize } from './props' -import { propTypes, allowedProps } from './props' -import type { - LibraryOptions, - MainDocsData, - ParsedDocSummary -} from '../../buildScripts/DataTypes.mjs' -import { logError } from '@instructure/console' -import type { Spacing } from '@instructure/emotion' - -type AppContextType = { - themeKey: keyof MainDocsData['themes'] - themes: MainDocsData['themes'] - library?: LibraryOptions +import { Flex } from '@instructure/ui-flex' +import { Heading } from '@instructure/ui-heading' +import { CloseButton } from '@instructure/ui-buttons' +import { Tray } from '@instructure/ui-tray' +import { Img } from '@instructure/ui-img' +import { View } from '@instructure/ui-view' +import { SideNavBar } from '@instructure/ui-side-nav-bar' +import { TopNavBar } from '@instructure/ui-top-nav-bar' +import { ScreenReaderContent } from '@instructure/ui-a11y-content' +import Link from '../Link' +import { Drilldown } from '@instructure/ui-drilldown' + +type AppProps = { + navigate: (path: string, options?: { replace: boolean }) => void } -export const AppContext = createContext({ - themes: {}, - themeKey: '', - library: undefined -}) +type AppState = { + windowWidth: number + open: boolean + openMobile: boolean +} @withStyle(generateStyle, generateComponentTheme) class App extends Component { - static propTypes = propTypes - static allowedProps = allowedProps - - static defaultProps = { - trayWidth: 300 - } - _content?: HTMLDivElement | null - _menuTrigger?: HTMLButtonElement - _mediaQueryListener?: ReturnType - _defaultDocumentTitle?: string - _controller?: AbortController - _heroRef: React.RefObject - _navRef: React.RefObject