diff --git a/package-lock.json b/package-lock.json index abd96645e8..001b8c4584 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30210,6 +30210,61 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.0.2.tgz", + "integrity": "sha512-m5AcPfTRUcjwmhBzOJGEl6Y7+Crqyju0+TgTQxoS4SO+BkWbhOrcfZNq6wSWdl2BBbJbsAoBUb8ZacOFT+/JlA==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.0.2.tgz", + "integrity": "sha512-VJOQ+CDWFDGaWdrG12Nl+d7yHtLaurNgAQZVgaIy7/Xd+DojgmYLosFfZdGz1wpxmjJIAkAMVTKWcvkx1oggAw==", + "license": "MIT", + "dependencies": { + "react-router": "7.0.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/read": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/read/-/read-3.0.1.tgz", @@ -31435,6 +31490,12 @@ "version": "2.0.0", "license": "ISC" }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -34471,6 +34532,12 @@ "node": "*" } }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -37256,6 +37323,7 @@ "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^7.0.2", "semver": "^7.6.3", "uuid": "^11.0.3", "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 8d0764908b..90af1716ce 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": "^7.0.2", "semver": "^7.6.3", "uuid": "^11.0.3", "webpack-merge": "^6.0.1" diff --git a/packages/__docs__/src/App/index.tsx b/packages/__docs__/src/App/index.tsx index 594d375d4b..58f20661b1 100644 --- a/packages/__docs__/src/App/index.tsx +++ b/packages/__docs__/src/App/index.tsx @@ -23,725 +23,266 @@ */ /** @jsx jsx */ -import { - Component, - createContext, - LegacyRef, - ReactElement, - SyntheticEvent -} from 'react' - -import { Alert } from '@instructure/ui-alerts' -import { - InstUISettingsProvider, - withStyle, - jsx, - 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 { Component } from 'react' +import { Route, Routes, useNavigate } from 'react-router-dom' +import { withStyle, jsx } from '@instructure/emotion' +import { CanvasTopNav } from '@instructure/ui-top-nav-bar' 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 + IconAlertsLine, + IconAnalyticsLine, + IconArrowOpenEndSolid, + 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 { Img } from '@instructure/ui-img' -type AppContextType = { - themeKey: keyof MainDocsData['themes'] - themes: MainDocsData['themes'] - library?: LibraryOptions +type AppProps = { + navigate: (path: string, options?: { replace: boolean }) => void } -export const AppContext = createContext({ - themes: {}, - themeKey: '', - library: undefined -}) +type AppState = { + menuStack: string[] +} -@withStyle(generateStyle, generateComponentTheme) -class App extends Component { - static propTypes = propTypes - static allowedProps = allowedProps +type MenuItem = { + label: string + renderBeforeLabel?: JSX.Element + onClick?: () => void + renderAfterLabel?: JSX.Element +} - static defaultProps = { - trayWidth: 300 +type Menu = { + items: MenuItem[] + backNavigation: { + href?: string + label: string + onClick?: () => void } - _content?: HTMLDivElement | null - _menuTrigger?: HTMLButtonElement - _mediaQueryListener?: ReturnType - _defaultDocumentTitle?: string - _controller?: AbortController + title: string + renderBeforeMobileMenuItems?: JSX.Element + renderAfterMobileMenuItems?: JSX.Element +} +type MenuCollection = { + [key: string]: Menu +} + +@withStyle(generateStyle, generateComponentTheme) +class App extends Component { constructor(props: AppProps) { super(props) - // determine what page we're loading - const [page] = this.getPathInfo() - - // if there's room for the tray + 700px, load with the tray open (unless it's the homepage) - const smallerScreen = window.matchMedia( - `(max-width: ${props.trayWidth + 700}px)` - ).matches - const isHomepage = page === 'index' || typeof page === 'undefined' - const showTrayOnPageLoad = !smallerScreen && !isHomepage this.state = { - showMenu: showTrayOnPageLoad, - themeKey: undefined, - layout: 'large', - docsData: null, - versionsData: undefined, - iconsData: null - } - } - - fetchDocumentData = async (docId: string) => { - const result = await fetch('docs/' + docId + '.json', { - signal: this._controller?.signal - }) - const docData: DocData = await result.json() - if (docId.includes('.')) { - // e.g. 'Calendar.Day', first get 'Calendar' then 'Day' - const components = docId.split('.') - const everyComp = EveryComponent as Record - docData.componentInstance = everyComp[components[0]][components[1]] - } else { - docData.componentInstance = - EveryComponent[docId as keyof typeof EveryComponent] - } - return docData - } - - fetchVersionData = async (signal: AbortController['signal']) => { - const versionsData = await fetchVersionData(signal) - return this.setState({ versionsData }) - } - - scrollToElement() { - const [_page, id] = this.getPathInfo() - - if (id) { - // If we have an id and it corresponds to an element - // that exists, scroll it into view - const linkedSection = document.getElementById(id) - linkedSection && linkedSection.scrollIntoView() - } else if (this._content) { - // If we don't have an id, scroll the content back to the top - this._content.scrollTop = 0 - } - } - - componentDidMount() { - this._defaultDocumentTitle = document.title - this.updateKey() - - window.addEventListener('hashchange', this.updateKey, false) - - // TODO: Replace with the Responsive component later - // Using this method directly for now instead to avoid a call to findDOMNode - this._mediaQueryListener = addMediaQueryMatchListener( - { - medium: { minWidth: 700 }, - large: { minWidth: 1100 }, - 'x-large': { minWidth: 1300 } - }, - this._content!, - this.updateLayout - ) - this.props.makeStyles?.() - - this._controller = new AbortController() - const signal = this._controller.signal - - this.fetchVersionData(signal) - - const errorHandler = (error: Error) => { - logError(error.name === 'AbortError', error.message) + menuStack: [ + 'default', + ...(window.location.pathname.substring(1) + ? [window.location.pathname.substring(1)] + : []) + ] } - - fetch('icons-data.json', { signal }) - .then((response) => response.json()) - .then((iconsData) => { - this.setState({ iconsData: iconsData }) - }) - .catch(errorHandler) - fetch('markdown-and-sources-data.json', { signal }) - .then((response) => response.json()) - .then((docsData) => { - this.setState({ - docsData, - themeKey: Object.keys(docsData.themes)[0] - }) - }) - .catch(errorHandler) } - componentDidUpdate() { - this.props.makeStyles?.() + pushMenu = (menuKey: string) => { + this.setState((prevState) => ({ + menuStack: [...prevState.menuStack, menuKey] + })) } - componentWillUnmount() { - //cancel ongoing requests - this._controller?.abort() - window.removeEventListener('hashchange', this.updateKey, false) - - if (this._mediaQueryListener) { - this._mediaQueryListener.remove() - } + popMenu = () => { + this.setState((prevState) => ({ + menuStack: prevState.menuStack.slice(0, -1) + })) } - trackPage(page: string) { - let title = this._defaultDocumentTitle - if (page !== 'index') { - title = `${page} - ${this._defaultDocumentTitle}` - } - - document.title = title! + getCurrentMenu = () => { + const { menuStack } = this.state + return menuStack[menuStack.length - 1] } - getPathInfo = () => { - const { hash } = window.location - - const path = hash && hash.split('/') - - if (path) { - const [page, id] = path.map((entry) => decodeURI(entry.replace('#', ''))) - return [page, id] - } - return [] - } - - updateLayout = (matches: QueriesMatching) => { - let layout: LayoutSize = 'small' - - if (matches.length > 0) { - if (matches.includes('medium') && matches.length === 1) { - layout = 'medium' - } else if (matches.includes('large') && matches.length === 2) { - layout = 'large' - } else if (matches.includes('x-large')) { - layout = 'x-large' - } - } - this.setState({ layout }) - } - - updateKey = () => { - const [page, _id] = this.getPathInfo() - - if (page) { - this.setState( - ({ key, showMenu }) => ({ - key: page || 'index', - showMenu: this.handleShowTrayOnURLChange(key, showMenu) - }), - this.scrollToElement - ) - } else { - this.trackPage('index') - } - } - - handleContentRef: LegacyRef = (el) => { - this._content = el - } - - handleMenuTriggerRef = (el: Element | null) => { - this._menuTrigger = el as HTMLButtonElement - } - - handleMenuOpen = () => { - this.setState({ showMenu: true }) - } - - handleMenuClose = () => { - this.setState({ showMenu: false }, () => { - this._menuTrigger && this._menuTrigger.focus() - }) - } - - handleThemeChange = (_event: SyntheticEvent, option: { value: string }) => { - this.setState({ - themeKey: option.value - }) - } - - handleShowTrayOnURLChange = (key: string | undefined, showMenu: boolean) => { - const userIsComingFromHomepage = - key === 'index' || typeof key === 'undefined' - - const { layout } = this.state - - // if the user is coming from the homepage, make the tray show if the layout is large enough - if (layout === 'small') { - return false - } else if (userIsComingFromHomepage) { - return true - } else { - return showMenu - } - } - - renderThemeSelect() { - const themeKeys = Object.keys(this.state.docsData!.themes) - const smallScreen = this.state.layout === 'small' - - return themeKeys.length > 1 ? ( - - - - - - ) : null - } - - renderTheme(themeKey: string) { - const theme = this.state.docsData!.themes[themeKey] - - const { layout } = this.state - const smallerScreens = layout === 'small' || layout === 'medium' - - const themeContent = ( - - - Theme: {themeKey} - - - - ) - return ( -
{this.renderWrappedContent(themeContent)}
- ) - } - - renderIcons(key: string) { - const { iconsData } = this.state - const { layout } = this.state - const smallerScreens = layout === 'small' || layout === 'medium' - - const iconContent = ( - - - Icons - - - + + + + ) - return
{this.renderWrappedContent(iconContent)}
- } - - renderDocument(docId: string, repository: string) { - const { parents } = this.state.docsData! - const children: any[] = [] - const currentData = this.state.currentDocData - if (!currentData || currentData.id !== docId) { - // load all children and the main doc - this.fetchDocumentData(docId).then(async (data) => { - if (parents[docId]) { - for (const childId of parents[docId].children) { - children.push(await this.fetchDocumentData(childId)) + const menu: MenuCollection = { + default: { + items: [ + { + label: 'Account', + renderBeforeLabel: , + onClick: () => { + this.pushMenu('account') + }, + renderAfterLabel: + }, + { + label: 'Courses', + renderBeforeLabel: , + onClick: () => { + this.pushMenu('courses') + }, + renderAfterLabel: + }, + { + label: 'Dashboard', + renderBeforeLabel: , + onClick: () => { + this.props.navigate('/dashboard', { replace: true }) + window.location.reload() + } + }, + { + label: 'Help', + renderBeforeLabel: , + onClick: () => alert('Help clicked') } - } - // eslint-disable-next-line no-param-reassign - data.children = children - this.setState( + ], + backNavigation: { + href: undefined, + label: '', + onClick: undefined + }, + title: '' + }, + account: { + renderBeforeMobileMenuItems: ( +
+ +
+ ), + renderAfterMobileMenuItems: ( +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi + aliquet erat in orci semper fringilla. Nullam suscipit mollis mi, at + vehicula magna vulputate eu. Cras mattis felis id quam vehicula + euismod. Nulla dolor enim, ornare in odio a, molestie dictum ligula. + Nullam maximus et dolor eget porttitor. Vestibulum faucibus viverra + pellentesque. Duis lorem lectus, porta vitae aliquam vitae, vehicula + sagittis nulla. Aenean sagittis congue rhoncus. Cras laoreet eu + nulla eu dignissim. Maecenas sed massa nisi. Suspendisse + pellentesque, metus sed ultricies porta, justo tellus pulvinar diam, + ac ornare massa nibh quis purus. Duis erat ipsum, pellentesque in + diam non, luctus accumsan metus. In ipsum tellus, ullamcorper a + faucibus a, venenatis ut urna. Sed at rutrum turpis. +

+ ), + items: [ { - currentDocData: data + label: 'AccountInfo1' }, - this.scrollToElement - ) - }) - return ( - - - - ) - } - const { themes } = this.state.docsData! - const { layout, themeKey, versionsData } = this.state - const { olderVersionsGitBranchMap } = versionsData || {} - let legacyGitBranch: string | undefined = undefined - - if (olderVersionsGitBranchMap && versionInPath) { - legacyGitBranch = olderVersionsGitBranchMap[versionInPath] - } - - const themeVariables = themes[themeKey!].resource - const heading = currentData.extension !== '.md' ? currentData.title : '' - const documentContent = ( - { + this.popMenu() + } + }, + title: 'Account' + }, + courses: { + items: [{ label: 'Courses1' }, { label: 'Courses2' }], + backNavigation: { + label: 'Back', + onClick: () => { + this.popMenu() + } + }, + title: 'Courses' + }, + dashboard: { + title: 'Dashboard', + items: [{ label: 'Courses1' }, { label: 'Courses2' }], + backNavigation: { + label: 'Back', + onClick: () => { + this.popMenu() + this.props.navigate('/', { replace: true }) + window.location.reload() + } } - > - {this.renderThemeSelect()} -
- -
-
- ) - return this.renderWrappedContent(documentContent) - } - - renderWrappedContent( - content: ReactElement[] | ReactElement, - padding: any = 'large' - ) { - return {content} - } - - renderHero() { - const { library, docs, themes } = this.state.docsData! - const { layout } = this.state - - const themeDocs: ParsedDocSummary = {} - - Object.keys(themes).forEach((key) => { - themeDocs[key] = { - title: key, - category: 'themes' } - }) - return ( - - - - ) - } - - renderChangeLog() { - if (!this.state.changelogData) { - this.fetchDocumentData('CHANGELOG').then((data) => { - this.setState({ changelogData: data }) - }) - return ( - - - - ) - } - const CHANGELOG = this.state.changelogData - let content: string - - const { description } = CHANGELOG - const currentMajorVersion = this.state.docsData!.library.version.slice(0, 1) - - // we want to cut the docs below the last 2 major versions, - // so find the next title after it - const versionCutoffPoint = parseInt(currentMajorVersion, 10) - 2 - const breakpointIndex = description.indexOf(`# [${versionCutoffPoint}`) - 1 - - if (breakpointIndex < 0) { - content = description - } else { - content = - description.slice(0, breakpointIndex) + - '\n...\n' + - `# Version ${versionCutoffPoint} and below\n` + - `For older releases (v${versionCutoffPoint} and below), check the [GitHub CHANGELOG](https://github.com/instructure/instructure-ui/blob/master/CHANGELOG.md).` } return ( -
- {this.renderWrappedContent(compileMarkdown(content))} -
- ) - } - - renderError() { - const errorContent = ( - - Document not found. Please use the search in - the navigation to find any page in this documentation. - - ) - return ( -
{this.renderWrappedContent(errorContent)}
- ) - } - - renderContent(key?: string) { - const doc = this.state.docsData!.docs[key!] - const theme = this.state.docsData!.themes[key!] - const { repository } = this.state.docsData!.library - - if (!key || key === 'index') { - return this.renderHero() - } - if (key === 'CHANGELOG') { - return this.renderChangeLog() - } else if (key === 'icons') { - return this.renderIcons(key) - } else if (theme) { - return this.renderTheme(key) - } else if (doc) { - return this.renderDocument(key!, repository) - } else { - return this.renderError() - } - } - - renderFooter() { - const { author, repository } = this.state.docsData!.library - - return author || repository ? ( - - {author && ( - - - Made with by {author} - . - - - )} - - ) : null - } - - renderNavigation() { - const { name, version } = this.state.docsData!.library - const { key, layout, showMenu, versionsData } = this.state - // Render nothing when the menu is not shown and the layout isn't small - // When the layout is small, we still render the tray so that it can properly - // finish the exit transition - if (!showMenu && layout !== 'small') return - - const navContent = ( - - - - { - if (button) { - button.focus() - } - }} - /> - - - -
- -