From ea6830f44bd8011c7efcff6c016795d91f977d90 Mon Sep 17 00:00:00 2001 From: Tamas Kovacs Date: Tue, 10 Mar 2026 11:40:11 +0100 Subject: [PATCH] feat(ui-navigation): rework AppNav INSTUI-4783 --- packages/ui-navigation/package.json | 20 +-- .../src/AppNav/v1/Item/index.tsx | 2 +- .../ui-navigation/src/AppNav/v1/index.tsx | 4 +- .../{v1 => v2}/Item/__tests__/Item.test.tsx | 0 .../src/AppNav/v2/Item/index.tsx | 144 +++++++++++++++ .../ui-navigation/src/AppNav/v2/Item/props.ts | 103 +++++++++++ .../src/AppNav/v2/Item/styles.ts | 97 ++++++++++ .../ui-navigation/src/AppNav/v2/README.md | 103 +++++++++++ .../{v1 => v2}/__tests__/AppNav.test.tsx | 0 .../ui-navigation/src/AppNav/v2/index.tsx | 166 ++++++++++++++++++ packages/ui-navigation/src/AppNav/v2/props.ts | 113 ++++++++++++ .../ui-navigation/src/AppNav/v2/styles.ts | 62 +++++++ packages/ui-navigation/src/exports/b.ts | 29 +++ 13 files changed, 830 insertions(+), 13 deletions(-) rename packages/ui-navigation/src/AppNav/{v1 => v2}/Item/__tests__/Item.test.tsx (100%) create mode 100644 packages/ui-navigation/src/AppNav/v2/Item/index.tsx create mode 100644 packages/ui-navigation/src/AppNav/v2/Item/props.ts create mode 100644 packages/ui-navigation/src/AppNav/v2/Item/styles.ts create mode 100644 packages/ui-navigation/src/AppNav/v2/README.md rename packages/ui-navigation/src/AppNav/{v1 => v2}/__tests__/AppNav.test.tsx (100%) create mode 100644 packages/ui-navigation/src/AppNav/v2/index.tsx create mode 100644 packages/ui-navigation/src/AppNav/v2/props.ts create mode 100644 packages/ui-navigation/src/AppNav/v2/styles.ts create mode 100644 packages/ui-navigation/src/exports/b.ts diff --git a/packages/ui-navigation/package.json b/packages/ui-navigation/package.json index e2df471b6b..aa5e12a68b 100644 --- a/packages/ui-navigation/package.json +++ b/packages/ui-navigation/package.json @@ -80,18 +80,18 @@ "default": "./es/exports/a.js" }, "./v11_7": { - "src": "./src/exports/a.ts", - "types": "./types/exports/a.d.ts", - "import": "./es/exports/a.js", - "require": "./lib/exports/a.js", - "default": "./es/exports/a.js" + "src": "./src/exports/b.ts", + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" }, "./latest": { - "src": "./src/exports/a.ts", - "types": "./types/exports/a.d.ts", - "import": "./es/exports/a.js", - "require": "./lib/exports/a.js", - "default": "./es/exports/a.js" + "src": "./src/exports/b.ts", + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" } } } diff --git a/packages/ui-navigation/src/AppNav/v1/Item/index.tsx b/packages/ui-navigation/src/AppNav/v1/Item/index.tsx index 58a47185ba..ca3b3c4ffa 100644 --- a/packages/ui-navigation/src/AppNav/v1/Item/index.tsx +++ b/packages/ui-navigation/src/AppNav/v1/Item/index.tsx @@ -32,7 +32,7 @@ import { passthroughProps } from '@instructure/ui-react-utils' -import { View } from '@instructure/ui-view' +import { View } from '@instructure/ui-view/v11_6' import { ScreenReaderContent } from '@instructure/ui-a11y-content' import type { ScreenReaderContentProps } from '@instructure/ui-a11y-content' diff --git a/packages/ui-navigation/src/AppNav/v1/index.tsx b/packages/ui-navigation/src/AppNav/v1/index.tsx index 2f03723124..29358bffcf 100644 --- a/packages/ui-navigation/src/AppNav/v1/index.tsx +++ b/packages/ui-navigation/src/AppNav/v1/index.tsx @@ -28,8 +28,8 @@ import { withStyleLegacy as withStyle } from '@instructure/emotion' import { callRenderProp, omitProps } from '@instructure/ui-react-utils' -import { View } from '@instructure/ui-view/latest' -import { Menu } from '@instructure/ui-menu/latest' +import { View } from '@instructure/ui-view/v11_6' +import { Menu } from '@instructure/ui-menu/v11_6' import { Item } from './Item' import generateStyle from './styles' diff --git a/packages/ui-navigation/src/AppNav/v1/Item/__tests__/Item.test.tsx b/packages/ui-navigation/src/AppNav/v2/Item/__tests__/Item.test.tsx similarity index 100% rename from packages/ui-navigation/src/AppNav/v1/Item/__tests__/Item.test.tsx rename to packages/ui-navigation/src/AppNav/v2/Item/__tests__/Item.test.tsx diff --git a/packages/ui-navigation/src/AppNav/v2/Item/index.tsx b/packages/ui-navigation/src/AppNav/v2/Item/index.tsx new file mode 100644 index 0000000000..56e2e94752 --- /dev/null +++ b/packages/ui-navigation/src/AppNav/v2/Item/index.tsx @@ -0,0 +1,144 @@ +/* + * 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 { ComponentElement, Component } from 'react' + +import { logError as error } from '@instructure/console' +import { + callRenderProp, + getElementType, + matchComponentTypes, + passthroughProps +} from '@instructure/ui-react-utils' + +import { View } from '@instructure/ui-view/latest' +import { ScreenReaderContent } from '@instructure/ui-a11y-content' +import type { ScreenReaderContentProps } from '@instructure/ui-a11y-content' + +import { withStyle } from '@instructure/emotion' + +import generateStyle from './styles' +import type { AppNavItemProps } from './props' +import { allowedProps } from './props' + +/** +--- +parent: AppNav +id: AppNav.Item +--- +@module Item +**/ +@withStyle(generateStyle) +class Item extends Component { + static readonly componentId = 'AppNav.Item' + + static allowedProps = allowedProps + + static defaultProps = { + children: null, + isSelected: false, + cursor: 'pointer', + isDisabled: false + } as const + + ref: Element | null = null + + componentDidMount() { + this.props.makeStyles?.() + } + + componentDidUpdate() { + this.props.makeStyles?.() + } + + handleRef = (el: Element | null) => { + const { elementRef } = this.props + + this.ref = el + + if (typeof elementRef === 'function') { + elementRef(el) + } + } + + handleClick = (e: React.MouseEvent) => { + const { isDisabled, onClick } = this.props + + if (isDisabled) { + e.preventDefault() + e.stopPropagation() + } else if (typeof onClick === 'function') { + onClick(e) + } + } + + render() { + const ElementType = getElementType(Item, this.props) + + const { renderIcon, renderLabel, href, renderAfter, cursor, isDisabled } = + this.props + + const icon = callRenderProp(renderIcon) + const label = callRenderProp(renderLabel) + + const labelIsForScreenReaders = matchComponentTypes< + ComponentElement + >(label, [ScreenReaderContent]) + + if (icon) { + error( + labelIsForScreenReaders, + '[AppNav] If an icon is used, the label text should be wrapped in .' + ) + } + + return ( + + {icon} + {labelIsForScreenReaders ? ( + label + ) : ( + {label} + )} + {renderAfter && callRenderProp(renderAfter)} + + ) + } +} + +export default Item +export { Item } diff --git a/packages/ui-navigation/src/AppNav/v2/Item/props.ts b/packages/ui-navigation/src/AppNav/v2/Item/props.ts new file mode 100644 index 0000000000..e65974acc8 --- /dev/null +++ b/packages/ui-navigation/src/AppNav/v2/Item/props.ts @@ -0,0 +1,103 @@ +/* + * 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 type { + AsElementType, + AppNavItemTheme, + OtherHTMLAttributes +} from '@instructure/shared-types' +import type { WithStyleProps, ComponentStyle } from '@instructure/emotion' +import type { Cursor } from '@instructure/shared-types' +import React from 'react' +import { Renderable } from '@instructure/shared-types' + +type AppNavItemOwnProps = { + /** + * The text to display. If the `icon` prop is used, label text must be wrapped + * in `ScreenReaderContent`. + */ + renderLabel: Renderable + /** + * Content to display after the renderLabel text, such as a badge + */ + renderAfter?: Renderable + /** + * The visual to display (ex. an Image, Logo, Avatar, or Icon) + */ + renderIcon?: Renderable + /** + * If the item goes to a new page, pass an href + */ + href?: string + /** + * If the item does not go to a new page, pass an onClick + */ + onClick?: (event: React.MouseEvent) => void + /** + * Denotes which item is currently selected + */ + isSelected?: boolean + /** + * provides a reference to the underlying focusable (`button` or `a`) element + */ + elementRef?: (element: Element | null) => void + /** + * The element type to render as (will default to `` if href is provided) + */ + as?: AsElementType + /** + * Specify the mouse cursor to use on :hover. + * The `pointer` cursor is used by default. + */ + cursor?: Cursor + /** + * Disables the link or button visually and functionally + */ + isDisabled?: boolean +} + +type PropKeys = keyof AppNavItemOwnProps + +type AllowedPropKeys = Readonly> + +type AppNavItemProps = AppNavItemOwnProps & + WithStyleProps & + OtherHTMLAttributes + +type AppNavItemStyle = ComponentStyle<'item' | 'label'> +const allowedProps: AllowedPropKeys = [ + 'renderLabel', + 'renderAfter', + 'renderIcon', + 'href', + 'onClick', + 'isSelected', + 'elementRef', + 'as', + 'cursor', + 'isDisabled' +] + +export type { AppNavItemProps, AppNavItemStyle } +export { allowedProps } diff --git a/packages/ui-navigation/src/AppNav/v2/Item/styles.ts b/packages/ui-navigation/src/AppNav/v2/Item/styles.ts new file mode 100644 index 0000000000..78528a88ac --- /dev/null +++ b/packages/ui-navigation/src/AppNav/v2/Item/styles.ts @@ -0,0 +1,97 @@ +/* + * 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 type { NewComponentTypes } from '@instructure/ui-themes' +import type { AppNavItemProps, AppNavItemStyle } from './props' + +/** + * --- + * private: true + * --- + * Generates the style object from the theme and provided additional information + * @param {Object} componentTheme The theme variable object. + * @param {Object} props the props of the component, the style is applied to + * @param {Object} state the state of the component, the style is applied to + * @return {Object} The final style object, which will be used in the component + */ +const generateStyle = ( + componentTheme: NewComponentTypes['AppNavItem'], + props: AppNavItemProps +): AppNavItemStyle => { + const { isSelected, isDisabled } = props + + const itemStyles = { + appearance: 'none', + overflow: 'visible', + direction: 'inherit', + margin: '0', + textDecoration: 'none', + userSelect: 'none', + touchAction: 'manipulation', + background: 'transparent', + border: 'none', + outline: 'none', + lineHeight: componentTheme.height, + padding: `0 ${componentTheme.padding}`, + alignItems: 'flex-start', + + '&:hover': { + textDecoration: 'underline', + textDecorationColor: componentTheme.textColor + }, + ...(isDisabled && { + pointerEvents: 'none', + opacity: 0.5 + }) + } + + return { + item: { + label: 'item', + ...itemStyles, + + // NOTE: needs separate groups for `:is()` and `:-webkit-any()` because of css selector group validation (see https://www.w3.org/TR/selectors-3/#grouping) + '&:is(a), &:is(button)': itemStyles, + '&:-webkit-any(a), &:-webkit-any(button)': itemStyles + }, + + label: { + label: 'item__label', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + fontFamily: componentTheme.fontFamily, + fontSize: componentTheme.fontSize, + fontWeight: componentTheme.fontWeight, + color: componentTheme.textColor, + + ...(isSelected && { + color: componentTheme.textColorSelected, + textDecoration: 'underline' + }) + } + } +} + +export default generateStyle diff --git a/packages/ui-navigation/src/AppNav/v2/README.md b/packages/ui-navigation/src/AppNav/v2/README.md new file mode 100644 index 0000000000..1da4994f2f --- /dev/null +++ b/packages/ui-navigation/src/AppNav/v2/README.md @@ -0,0 +1,103 @@ +--- +describes: AppNav +--- + +`AppNav` is a navigation component currently intended for use within LTI apps. AppNav +can be configured to adapt to small screen widths by truncating nav items that +don't fit within the available space. + +The `onUpdate` function prop returns the number of visible nav items, while the +`renderTruncateLabel` function prop allows you to customize the trigger for the Menu +that contains the truncated items. The example below shows how you can use both of +these props to create a hamburger menu when the number of visible nav items is less +than two. + +```js +--- +type: example +--- +const totalItemsCount = 5 + +const AppNavExample = () => { + const [visibleItemsCount, setVisibleItemsCount] = useState(totalItemsCount) + + const handleUpdate = ({ visibleItemsCount }) => { + setVisibleItemsCount(visibleItemsCount) + } + + return ( + = 2 ? visibleItemsCount : 0} + onUpdate={handleUpdate} + renderBeforeItems={ + Instructure} + renderIcon={ + + } + href="http://instructure.com" + /> + } + renderAfterItems={ + console.log('Add')} + renderIcon={} + margin="0 0 0 x-small" + screenReaderLabel="Add something" + withBorder={false} + withBackground={false} + /> + } + renderTruncateLabel={() => { + const hiddenItemsCount = totalItemsCount - visibleItemsCount + if (visibleItemsCount >= 2) { + return `${hiddenItemsCount} More` + } else { + return ( + + + {`${hiddenItemsCount} More`} + + ) + } + }} + > + { + return ( + + You have notifications from instructure-ui + + ) + }} + /> + } + /> + + + 'Canvas Community'} + href="https://community.canvaslms.com/" + /> + + + ) +} + +render() +``` diff --git a/packages/ui-navigation/src/AppNav/v1/__tests__/AppNav.test.tsx b/packages/ui-navigation/src/AppNav/v2/__tests__/AppNav.test.tsx similarity index 100% rename from packages/ui-navigation/src/AppNav/v1/__tests__/AppNav.test.tsx rename to packages/ui-navigation/src/AppNav/v2/__tests__/AppNav.test.tsx diff --git a/packages/ui-navigation/src/AppNav/v2/index.tsx b/packages/ui-navigation/src/AppNav/v2/index.tsx new file mode 100644 index 0000000000..9be34b41b0 --- /dev/null +++ b/packages/ui-navigation/src/AppNav/v2/index.tsx @@ -0,0 +1,166 @@ +/* + * 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 { ComponentElement, Component } from 'react' + +import { withStyle } from '@instructure/emotion' + +import { callRenderProp, omitProps } from '@instructure/ui-react-utils' + +import { View } from '@instructure/ui-view/latest' +import { Menu } from '@instructure/ui-menu/latest' +import { Item } from './Item' + +import generateStyle from './styles' +import type { AppNavProps } from './props' +import { allowedProps } from './props' +import { AppNavItemProps } from './Item/props' + +import { TruncateList } from '@instructure/ui-truncate-list' + +/** +--- +category: components +--- +**/ +@withStyle(generateStyle) +class AppNav extends Component { + static readonly componentId = 'AppNav' + + static allowedProps = allowedProps + + static defaultProps = { + children: null, + debounce: 300, + margin: '0', + renderTruncateLabel: () => 'More', + visibleItemsCount: 0 + } + + static Item = Item + + state = { + isMeasuring: false + } + + ref: Element | null = null + + componentDidMount() { + this.props.makeStyles?.() + } + + componentDidUpdate() { + this.props.makeStyles?.() + } + + handleRef = (el: Element | null) => { + const { elementRef } = this.props + + this.ref = el + + if (typeof elementRef === 'function') { + elementRef(el) + } + } + + renderMenu(items: ComponentElement[]) { + return ( + + } + > + {(items as ComponentElement[]).map( + (item, index) => { + return ( + + {callRenderProp(item.props.renderLabel)} + + ) + } + )} + + ) + } + + render() { + const { visibleItemsCount, screenReaderLabel, margin, debounce, styles } = + this.props + + const passthroughProps = View.omitViewProps( + omitProps(this.props, AppNav.allowedProps), + AppNav + ) + + const renderBeforeItems = callRenderProp(this.props.renderBeforeItems) + const renderAfterItems = callRenderProp(this.props.renderAfterItems) + const hasRenderedContent = renderBeforeItems || renderAfterItems + + return ( + + {renderBeforeItems && {renderBeforeItems}} + + + this.renderMenu( + hiddenChildren as ComponentElement[] + ) + } + itemSpacing={styles?.horizontalMargin} + fixMenuTriggerWidth={styles?.menuTriggerWidth} + css={styles?.list} + aria-label={callRenderProp(screenReaderLabel)} + > + {this.props.children} + + + {renderAfterItems && {renderAfterItems}} + + ) + } +} + +export { AppNav } +export default AppNav diff --git a/packages/ui-navigation/src/AppNav/v2/props.ts b/packages/ui-navigation/src/AppNav/v2/props.ts new file mode 100644 index 0000000000..14a1a70e35 --- /dev/null +++ b/packages/ui-navigation/src/AppNav/v2/props.ts @@ -0,0 +1,113 @@ +/* + * 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 type { + Spacing, + WithStyleProps, + ComponentStyle +} from '@instructure/emotion' +import type { + AppNavTheme, + OtherHTMLAttributes +} from '@instructure/shared-types' +import { Renderable } from '@instructure/shared-types' + +type AppNavOwnProps = { + /** + * Screenreader label for the overall navigation + */ + screenReaderLabel: string + /** + * The rate (in ms) the component responds to container resizing or + * an update to one of its child items + */ + debounce?: number + /** + * Content to display before the navigation items, such as a logo + */ + renderBeforeItems?: Renderable + /** + * Content to display after the navigation items, aligned to the far end + * of the navigation + */ + renderAfterItems?: Renderable + /** + * Valid values are `0`, `none`, `auto`, `xxx-small`, `xx-small`, `x-small`, + * `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via + * familiar CSS-like shorthand. For example: `margin="small auto large"`. + */ + margin?: Spacing + /** + * Provides a reference to the underlying nav element + */ + elementRef?: (element: Element | null) => void + /** + * Customize the text displayed in the menu trigger when links overflow + * the overall nav width. + */ + renderTruncateLabel?: Renderable + /** + * Called whenever the navigation items are updated or the size of + * the navigation changes. Passes in the `visibleItemsCount` as + * a parameter. + */ + onUpdate?: (visibleItemsCount: { visibleItemsCount: number }) => void + /** + * Sets the number of navigation items that are visible. + */ + visibleItemsCount?: number + /** + * Only accepts `AppNav.Item` as children + */ + children?: React.ReactNode +} + +type PropKeys = keyof AppNavOwnProps + +type AllowedPropKeys = Readonly> + +type AppNavProps = AppNavOwnProps & + WithStyleProps & + OtherHTMLAttributes + +type AppNavStyle = ComponentStyle<'appNav' | 'alignCenter' | 'list'> & { + horizontalMargin: string + menuTriggerWidth: string +} +const allowedProps: AllowedPropKeys = [ + 'screenReaderLabel', + 'children', + 'debounce', + 'renderBeforeItems', + 'renderAfterItems', + 'margin', + 'elementRef', + 'renderTruncateLabel', + 'onUpdate', + 'visibleItemsCount' +] + +export type { AppNavProps, AppNavStyle } +export { allowedProps } diff --git a/packages/ui-navigation/src/AppNav/v2/styles.ts b/packages/ui-navigation/src/AppNav/v2/styles.ts new file mode 100644 index 0000000000..49159cdba5 --- /dev/null +++ b/packages/ui-navigation/src/AppNav/v2/styles.ts @@ -0,0 +1,62 @@ +/* + * 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 type { NewComponentTypes } from '@instructure/ui-themes' +import type { AppNavStyle } from './props' + +/** + * --- + * private: true + * --- + * Generates the style object from the theme and provided additional information + * @param {Object} componentTheme The theme variable object. + * @param {Object} props the props of the component, the style is applied to + * @param {Object} state the state of the component, the style is applied to + * @return {Object} The final style object, which will be used in the component + */ +const generateStyle = (componentTheme: NewComponentTypes['AppNav']): AppNavStyle => { + return { + appNav: { + label: 'appNav', + fontFamily: componentTheme.fontFamily, + borderBottom: `${componentTheme.borderWidth} ${componentTheme.borderStyle} ${componentTheme.borderColor}` + }, + alignCenter: { + alignItems: 'center' + }, + list: { + label: 'appNav__list', + height: componentTheme.height, + flexGrow: 1, + flexShrink: 1, + flexBasis: '0', + minWidth: '0.0625rem', + paddingInlineStart: componentTheme.horizontalMargin + }, + horizontalMargin: componentTheme.horizontalMargin, + menuTriggerWidth: componentTheme.menuTriggerWidth + } +} + +export default generateStyle diff --git a/packages/ui-navigation/src/exports/b.ts b/packages/ui-navigation/src/exports/b.ts new file mode 100644 index 0000000000..bce04faf6b --- /dev/null +++ b/packages/ui-navigation/src/exports/b.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +export { AppNav } from '../AppNav/v2' +export { Item as AppNavItem } from '../AppNav/v2/Item' + +export type { AppNavProps } from '../AppNav/v2/props' +export type { AppNavItemProps } from '../AppNav/v2/Item/props'