From 9116774758389e0ee1f2d60a4fbd854368e46edf Mon Sep 17 00:00:00 2001 From: adamlobler Date: Wed, 12 Aug 2026 18:05:30 +0200 Subject: [PATCH] feat(ui-banner): add Banner component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new Banner component for short, prominent messages (announcements or promotions) with an optional header, an icon, a dismiss control, and a fixed-style call-to-action button. Ships in violet (default) and sea color treatments, and relaxed (default) or compact density. Bump the instructure-design-tokens dependency pin in ui-scripts to v1.8.0, which already contains Banner's token registration for both canvas and rebrand brand families across all four themes. Wire Banner into the ui umbrella package's v11_7 export and TS project references, and add the regression-test page and Cypress entry. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/ui-banner/.gitignore | 4 + packages/ui-banner/babel.config.js | 36 ++++ packages/ui-banner/package.json | 87 +++++++++ .../src/Banner/__tests__/Banner.test.tsx | 169 ++++++++++++++++++ packages/ui-banner/src/Banner/v1/README.md | 126 +++++++++++++ packages/ui-banner/src/Banner/v1/index.tsx | 157 ++++++++++++++++ packages/ui-banner/src/Banner/v1/props.ts | 122 +++++++++++++ packages/ui-banner/src/Banner/v1/styles.ts | 127 +++++++++++++ packages/ui-banner/src/exports/a.ts | 27 +++ packages/ui-banner/src/exports/b.ts | 27 +++ packages/ui-banner/tsconfig.build.json | 21 +++ packages/ui-banner/tsconfig.json | 4 + packages/ui-scripts/package.json | 2 +- packages/ui/package.json | 1 + packages/ui/src/v11_7.ts | 2 + packages/ui/tsconfig.build.json | 3 + pnpm-lock.yaml | 70 +++++++- regression-test/cypress/e2e/spec.cy.ts | 1 + regression-test/src/app/banner/page.tsx | 71 ++++++++ 19 files changed, 1050 insertions(+), 7 deletions(-) create mode 100644 packages/ui-banner/.gitignore create mode 100644 packages/ui-banner/babel.config.js create mode 100644 packages/ui-banner/package.json create mode 100644 packages/ui-banner/src/Banner/__tests__/Banner.test.tsx create mode 100644 packages/ui-banner/src/Banner/v1/README.md create mode 100644 packages/ui-banner/src/Banner/v1/index.tsx create mode 100644 packages/ui-banner/src/Banner/v1/props.ts create mode 100644 packages/ui-banner/src/Banner/v1/styles.ts create mode 100644 packages/ui-banner/src/exports/a.ts create mode 100644 packages/ui-banner/src/exports/b.ts create mode 100644 packages/ui-banner/tsconfig.build.json create mode 100644 packages/ui-banner/tsconfig.json create mode 100644 regression-test/src/app/banner/page.tsx diff --git a/packages/ui-banner/.gitignore b/packages/ui-banner/.gitignore new file mode 100644 index 0000000000..02125f1a5b --- /dev/null +++ b/packages/ui-banner/.gitignore @@ -0,0 +1,4 @@ +lib/ +.babel-cache/ +es/ +types/ diff --git a/packages/ui-banner/babel.config.js b/packages/ui-banner/babel.config.js new file mode 100644 index 0000000000..647821c390 --- /dev/null +++ b/packages/ui-banner/babel.config.js @@ -0,0 +1,36 @@ +/* + * 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. + */ + +module.exports = { + presets: [ + [ + require('@instructure/ui-babel-preset'), + { + esModules: Boolean(process.env.ES_MODULES), + removeConsole: process.env.NODE_ENV === 'production', + transformImports: Boolean(process.env.TRANSFORM_IMPORTS) + } + ] + ] +} diff --git a/packages/ui-banner/package.json b/packages/ui-banner/package.json new file mode 100644 index 0000000000..088e4a8c2d --- /dev/null +++ b/packages/ui-banner/package.json @@ -0,0 +1,87 @@ +{ + "name": "@instructure/ui-banner", + "version": "11.7.4", + "description": "A UI component for displaying Banners", + "author": "Instructure, Inc. Engineering and Product Design", + "module": "./es/index.js", + "main": "./lib/index.js", + "types": "./types/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/instructure/instructure-ui.git" + }, + "homepage": "https://instructure.github.io/instructure-ui/", + "bugs": "https://github.com/instructure/instructure-ui/issues", + "scripts": { + "lint": "ui-scripts lint", + "lint:fix": "ui-scripts lint --fix", + "clean": "ui-scripts clean", + "build": "ui-scripts build --modules es,cjs", + "build:watch": "pnpm run ts:check -- --watch & ui-scripts build --watch", + "build:types": "tsc -p tsconfig.build.json", + "ts:check": "tsc -p tsconfig.build.json --noEmit --emitDeclarationOnly false" + }, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@instructure/console": "workspace:*", + "@instructure/emotion": "workspace:*", + "@instructure/shared-types": "workspace:*", + "@instructure/ui-buttons": "workspace:*", + "@instructure/ui-heading": "workspace:*", + "@instructure/ui-icons": "workspace:*", + "@instructure/ui-react-utils": "workspace:*", + "@instructure/ui-text": "workspace:*", + "@instructure/ui-themes": "workspace:*" + }, + "devDependencies": { + "@instructure/ui-axe-check": "workspace:*", + "@instructure/ui-babel-preset": "workspace:*", + "@instructure/ui-color-utils": "workspace:*", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "16.3.2", + "vitest": "^4.1.9" + }, + "peerDependencies": { + "react": ">=18 <=19" + }, + "publishConfig": { + "access": "public" + }, + "sideEffects": false, + "exports": { + "./lib/*": "./lib/*", + "./es/*": "./es/*", + "./types/*": "./types/*", + "./package.json": "./package.json", + "./src/*": "./src/*", + ".": { + "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" + }, + "./v11_6": { + "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" + }, + "./v11_7": { + "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/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-banner/src/Banner/__tests__/Banner.test.tsx b/packages/ui-banner/src/Banner/__tests__/Banner.test.tsx new file mode 100644 index 0000000000..e4ac70d957 --- /dev/null +++ b/packages/ui-banner/src/Banner/__tests__/Banner.test.tsx @@ -0,0 +1,169 @@ +/* + * 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 { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import { vi } from 'vitest' +import { runAxeCheck } from '@instructure/ui-axe-check' + +import { Banner } from '@instructure/ui-banner/latest' + +describe('', () => { + let consoleWarningMock: ReturnType + let consoleErrorMock: ReturnType + + beforeEach(() => { + consoleWarningMock = vi + .spyOn(console, 'warn') + .mockImplementation(() => {}) as any + consoleErrorMock = vi + .spyOn(console, 'error') + .mockImplementation(() => {}) as any + }) + + afterEach(() => { + consoleWarningMock.mockRestore() + consoleErrorMock.mockRestore() + }) + + it('should render the header', () => { + render(Body text) + + expect(screen.getByText('Heads up')).toBeInTheDocument() + }) + + it('should render children as body content', () => { + render(Your assignments were graded.) + + expect( + screen.getByText('Your assignments were graded.') + ).toBeInTheDocument() + }) + + it('should render a custom icon when provided', () => { + render(}>Body text) + + expect(screen.getByTestId('custom-icon')).toBeInTheDocument() + }) + + it('should render a call-to-action button when ctaText and onCtaClick are provided', () => { + const onCtaClick = vi.fn() + render( + + Body text + + ) + + const button = screen.getByRole('button', { name: 'Go' }) + expect(button).toBeInTheDocument() + + button.dispatchEvent( + new MouseEvent('click', { bubbles: true, button: 0, detail: 1 }) + ) + expect(onCtaClick).toHaveBeenCalledTimes(1) + }) + + it('should not render a call-to-action button when onCtaClick is missing', () => { + render(Body text) + + expect(screen.queryByRole('button', { name: 'Go' })).not.toBeInTheDocument() + }) + + it('should render a dismiss button and call onDismiss when clicked', async () => { + const onDismiss = vi.fn() + render( + + Body text + + ) + + const closeButton = screen.getByRole('button', { name: 'Close banner' }) + closeButton.dispatchEvent( + new MouseEvent('click', { bubbles: true, button: 0, detail: 1 }) + ) + + expect(onDismiss).toHaveBeenCalledTimes(1) + }) + + it('should not render a dismiss button when onDismiss is missing', () => { + render(Body text) + + expect(screen.queryByRole('button')).not.toBeInTheDocument() + }) + + it('should not render a dismiss button when isDismissible is false', () => { + render( + {}} closeButtonLabel="Close banner"> + Body text + + ) + + expect(screen.queryByRole('button')).not.toBeInTheDocument() + }) + + it('should warn when isDismissible is true without a closeButtonLabel', () => { + render( + {}}> + Body text + + ) + + expect(consoleWarningMock.mock.calls.flat()).toContainEqual( + expect.stringContaining('`closeButtonLabel` should be provided') + ) + }) + + it('should not warn when isDismissible is true with a closeButtonLabel', () => { + render( + {}} closeButtonLabel="Close"> + Body text + + ) + + expect(consoleWarningMock.mock.calls.flat()).not.toContainEqual( + expect.stringContaining('`closeButtonLabel` should be provided') + ) + }) + + it('should meet a11y standards', async () => { + const { container } = render( + {}} + closeButtonLabel="Close banner" + > + Body text + + ) + + const axeCheck = await runAxeCheck(container) + + expect(axeCheck).toBe(true) + }) +}) diff --git a/packages/ui-banner/src/Banner/v1/README.md b/packages/ui-banner/src/Banner/v1/README.md new file mode 100644 index 0000000000..d6198f51f4 --- /dev/null +++ b/packages/ui-banner/src/Banner/v1/README.md @@ -0,0 +1,126 @@ +--- +describes: Banner +--- + +The Banner component surfaces a short, prominent message -- an announcement +or a promotion -- with an optional header, an icon, a dismiss control, and +a slot for a call-to-action. + +All of Banner's text comes from props (`header`, `children`, `ctaText`, and +whatever you pass as `icon`) so it can be translated -- Banner never renders +hardcoded copy itself. + +```javascript +--- +type: example +--- +Your assignments were graded. +``` + +Add a `header` for a bold lead-in line above the body text. + +```javascript +--- +type: example +--- + + Try the redesigned gradebook filters. + +``` + +Banner ships in two colors: `violet` (default) and `sea`. + +```javascript +--- +type: example +--- +
+ + Violet banner. + + + Sea banner. + +
+``` + +`density` controls padding, gaps, and font sizes. `relaxed` (default) is +roomier; `compact` is tighter. + +```javascript +--- +type: example +--- +
+ + + More padding, larger text. + + + + + Less padding, smaller text. + + +
+``` + +Pass `isDismissible` along with `onDismiss` to show a close button in the +top-right corner. `closeButtonLabel` gives the button an accessible name -- +provide it whenever `isDismissible` is `true`, or a development warning is +logged. A close button only renders when both `isDismissible` and +`onDismiss` are set; a dismiss control with no handler is a dead end for +users, so Banner won't render one. + +```javascript +--- +type: example +--- + console.log('dismissed')} + closeButtonLabel="Close banner" +> + Dismiss me with the close button. + +``` + +Pass `ctaText` and `onCtaClick` for a follow-up action. Banner always +renders this as its own fixed-style button -- consumers control the label +and click behavior, not the button's size or color. + +```javascript +--- +type: example +--- + console.log('cta clicked')} +> + See how a saved study plan can keep you on track. + +``` + +Pass `icon` to override the default icon shown in the icon swatch. + +```javascript +--- +type: example +--- +}> + Custom icon example. + +``` + +### Accessibility + +- Banner does not assign itself a specific ARIA role; wrap it in a + region with an appropriate role (e.g. `role="status"` or + `role="alertdialog"`) at the call site when the message needs to be + announced to assistive technology. +- Always provide `closeButtonLabel` when `isDismissible` is `true` -- it is + the dismiss button's only accessible name. +- The `violet` and `sea` pastel backgrounds are chosen from the design + token set already tuned for sufficient contrast against Banner's text + and icon colors. diff --git a/packages/ui-banner/src/Banner/v1/index.tsx b/packages/ui-banner/src/Banner/v1/index.tsx new file mode 100644 index 0000000000..f431021b7a --- /dev/null +++ b/packages/ui-banner/src/Banner/v1/index.tsx @@ -0,0 +1,157 @@ +/* + * 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 { forwardRef } from 'react' + +import { useStyleNew } from '@instructure/emotion' +import { passthroughProps } from '@instructure/ui-react-utils' +import { warn } from '@instructure/console' +import { Button, CloseButton } from '@instructure/ui-buttons/latest' +import { Heading } from '@instructure/ui-heading/latest' +import { Text } from '@instructure/ui-text/latest' +import { DiamondInstUIIcon } from '@instructure/ui-icons' + +import generateStyle from './styles.js' + +import type { BannerProps } from './props' + +/** +--- +category: components +--- + +A Banner surfaces a short, prominent message -- typically an announcement +or a promotion -- with an optional header, an icon, a dismiss control, and +a call-to-action button. + +```js +--- +type: example +--- +Your assignments were graded. +``` +**/ +const Banner = forwardRef( + ( + { + color = 'violet', + density = 'relaxed', + header, + children, + icon, + isDismissible = false, + onDismiss, + closeButtonLabel, + ctaText, + onCtaClick, + themeOverride, + ...rest + }, + ref + ) => { + const styles = useStyleNew({ + generateStyle, + themeOverride, + params: { + color, + density + }, + componentId: 'Banner', + displayName: 'Banner' + }) + + warn( + !isDismissible || !!closeButtonLabel, + '[Banner] `closeButtonLabel` should be provided whenever `isDismissible` is `true` so the dismiss button has an accessible label.' + ) + + const showCloseButton = isDismissible && !!onDismiss + const showCta = !!ctaText && !!onCtaClick + + const iconSize = density === 'compact' ? 'sm' : 'md' + const defaultIcon = + const resolvedIcon = icon ?? defaultIcon + + return ( +
+ {showCloseButton && ( +
+ onDismiss?.()} + /> +
+ )} + + {resolvedIcon && ( +
+
{resolvedIcon}
+
+ )} + +
+ {header && ( + + {header} + + )} + + {children} + + {showCta && ( +
+ +
+ )} +
+
+ ) + } +) + +Banner.displayName = 'Banner' + +export default Banner +export { Banner } diff --git a/packages/ui-banner/src/Banner/v1/props.ts b/packages/ui-banner/src/Banner/v1/props.ts new file mode 100644 index 0000000000..99b5271f4c --- /dev/null +++ b/packages/ui-banner/src/Banner/v1/props.ts @@ -0,0 +1,122 @@ +/* + * 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 { ReactNode } from 'react' +import type { ComponentStyle, NewThemeOverrideProp } from '@instructure/emotion' +import type { NewComponentTypes } from '@instructure/ui-themes' + +type BannerOwnProps = { + /** + * The color treatment used for the banner's background and default icon. + */ + color?: 'violet' | 'sea' + + /** + * Controls the padding, gaps, and font sizes used by the banner. + * `'relaxed'` is larger, `'compact'` is smaller. + */ + density?: 'relaxed' | 'compact' + + /** + * An optional bold header line shown above the body content. + */ + header?: string + + /** + * The banner's body content. This is the main descriptive text (or other + * content) shown below the optional `header`. + */ + children: ReactNode + + /** + * Overrides the default icon shown in the icon swatch. If not provided, a + * decorative diamond icon is used. + */ + icon?: ReactNode + + /** + * Whether to show a dismiss (close) button in the top-right corner of the + * banner. The close button only renders when both `isDismissible` and + * `onDismiss` are provided -- a dismiss button with no handler is a + * dead end for users and is bad for accessibility, so it's intentionally + * omitted otherwise. When `isDismissible` is `true`, `closeButtonLabel` + * must also be provided or a warning is logged in development. + */ + isDismissible?: boolean + + /** + * Called when the dismiss button is clicked. Required (in practice) for + * the dismiss button to render -- see `isDismissible`. + */ + onDismiss?: () => void + + /** + * An accessible label for the dismiss button, read by screen readers. + * Required when `isDismissible` is `true`. + */ + closeButtonLabel?: string + + /** + * The label for an optional call-to-action button rendered below the + * body content. Banner always renders this as its own fixed-style + * `Button` (same size and color treatment everywhere) -- consumers + * control the label and click behavior, not the button's appearance. + * The button only renders when both `ctaText` and `onCtaClick` are + * provided. + */ + ctaText?: string + + /** + * Called when the call-to-action button is clicked. Required (in + * practice) for the button to render -- see `ctaText`. + */ + onCtaClick?: () => void +} + +type PropKeys = keyof BannerOwnProps + +type AllowedPropKeys = Readonly> + +type BannerProps = BannerOwnProps & + NewThemeOverrideProp> + +type BannerStyle = ComponentStyle< + 'banner' | 'closeButton' | 'iconSwatch' | 'icon' | 'content' | 'cta' +> + +const allowedProps: AllowedPropKeys = [ + 'color', + 'density', + 'header', + 'children', + 'icon', + 'isDismissible', + 'onDismiss', + 'closeButtonLabel', + 'ctaText', + 'onCtaClick' +] + +export type { BannerProps, BannerStyle } +export { allowedProps } diff --git a/packages/ui-banner/src/Banner/v1/styles.ts b/packages/ui-banner/src/Banner/v1/styles.ts new file mode 100644 index 0000000000..783862d6df --- /dev/null +++ b/packages/ui-banner/src/Banner/v1/styles.ts @@ -0,0 +1,127 @@ +/* + * 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, SharedTokens } from '@instructure/ui-themes' +import type { BannerProps, BannerStyle } from './props' + +type StyleParams = { + color: BannerProps['color'] + density: BannerProps['density'] +} + +/** + * --- + * private: true + * --- + * Generates the style object from the theme and provided additional information + * @param componentTheme The theme variable object. + * @param params Additional parameters to customize the style. + * @param sharedTokens Shared token object that stores common values for the theme. + * @return The final style object, which will be used in the component + */ +const generateStyle = ( + componentTheme: ReturnType, + params: StyleParams, + _sharedTokens: SharedTokens +): BannerStyle => { + const { color = 'violet', density = 'relaxed' } = params + const isCompact = density === 'compact' + + const background = + color === 'sea' + ? componentTheme.seaBackground + : componentTheme.violetBackground + + const iconSwatchBackground = + color === 'sea' + ? componentTheme.seaIconBackground + : componentTheme.violetIconBackground + + const paddingVertical = isCompact + ? componentTheme.compactPaddingVertical + : componentTheme.relaxedPaddingVertical + const paddingHorizontal = isCompact + ? componentTheme.compactPaddingHorizontal + : componentTheme.relaxedPaddingHorizontal + const contentGapHorizontal = isCompact + ? componentTheme.compactContentGapHorizontal + : componentTheme.relaxedContentGapHorizontal + const iconBorderRadius = isCompact + ? componentTheme.compactIconBorderRadius + : componentTheme.relaxedIconBorderRadius + + return { + banner: { + label: 'banner', + position: 'relative', + display: 'flex', + alignItems: 'flex-start', + gap: contentGapHorizontal, + background, + color: componentTheme.color, + borderStyle: componentTheme.borderStyle, + borderWidth: componentTheme.borderWidth, + borderColor: componentTheme.borderColor, + borderRadius: componentTheme.borderRadius, + padding: `${paddingVertical} ${paddingHorizontal}` + }, + closeButton: { + label: 'banner__closeButton', + position: 'absolute', + top: componentTheme.closeButtonMarginTop, + right: componentTheme.closeButtonMarginRight + }, + iconSwatch: { + label: 'banner__iconSwatch', + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: isCompact ? '2rem' : '2.5rem', + height: isCompact ? '2rem' : '2.5rem', + background: iconSwatchBackground, + borderRadius: iconBorderRadius + }, + icon: { + label: 'banner__icon', + color: componentTheme.iconColor, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + lineHeight: 0 + }, + content: { + label: 'banner__content', + display: 'flex', + flexDirection: 'column', + gap: componentTheme.contentGapVertical, + minWidth: 0 + }, + cta: { + label: 'banner__cta' + } + } +} + +export default generateStyle diff --git a/packages/ui-banner/src/exports/a.ts b/packages/ui-banner/src/exports/a.ts new file mode 100644 index 0000000000..a677efebea --- /dev/null +++ b/packages/ui-banner/src/exports/a.ts @@ -0,0 +1,27 @@ +/* + * 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 { default as Banner } from '../Banner/v1/index.js' + +export type { BannerProps } from '../Banner/v1/props' diff --git a/packages/ui-banner/src/exports/b.ts b/packages/ui-banner/src/exports/b.ts new file mode 100644 index 0000000000..a677efebea --- /dev/null +++ b/packages/ui-banner/src/exports/b.ts @@ -0,0 +1,27 @@ +/* + * 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 { default as Banner } from '../Banner/v1/index.js' + +export type { BannerProps } from '../Banner/v1/props' diff --git a/packages/ui-banner/tsconfig.build.json b/packages/ui-banner/tsconfig.build.json new file mode 100644 index 0000000000..de71c0f816 --- /dev/null +++ b/packages/ui-banner/tsconfig.build.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./types", + "rootDir": "./src", + "composite": true + }, + "include": ["src"], + "references": [ + { "path": "../console/tsconfig.build.json" }, + { "path": "../emotion/tsconfig.build.json" }, + { "path": "../shared-types/tsconfig.build.json" }, + { "path": "../ui-axe-check/tsconfig.build.json" }, + { "path": "../ui-react-utils/tsconfig.build.json" }, + { "path": "../ui-babel-preset/tsconfig.build.json" }, + { "path": "../ui-color-utils/tsconfig.build.json" }, + { "path": "../ui-themes/tsconfig.build.json" }, + { "path": "../ui-icons/tsconfig.build.json" }, + { "path": "../ui-buttons/tsconfig.build.json" } + ] +} diff --git a/packages/ui-banner/tsconfig.json b/packages/ui-banner/tsconfig.json new file mode 100644 index 0000000000..b29a7b46c4 --- /dev/null +++ b/packages/ui-banner/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": {} +} diff --git a/packages/ui-scripts/package.json b/packages/ui-scripts/package.json index 7f47b9cede..76b38ae061 100644 --- a/packages/ui-scripts/package.json +++ b/packages/ui-scripts/package.json @@ -23,7 +23,7 @@ "dependencies": { "@babel/cli": "^7.27.2", "@instructure/command-utils": "workspace:*", - "@instructure/instructure-design-tokens": "github:instructure/instructure-design-tokens#v1.5.0", + "@instructure/instructure-design-tokens": "github:instructure/instructure-design-tokens#v1.8.0", "dprint": "^0.55.1", "http-server": "^14.1.1", "inquirer": "^14.0.2", diff --git a/packages/ui/package.json b/packages/ui/package.json index 21f888c9e1..21c24eca9a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -29,6 +29,7 @@ "@instructure/ui-alerts": "workspace:*", "@instructure/ui-avatar": "workspace:*", "@instructure/ui-badge": "workspace:*", + "@instructure/ui-banner": "workspace:*", "@instructure/ui-billboard": "workspace:*", "@instructure/ui-breadcrumb": "workspace:*", "@instructure/ui-buttons": "workspace:*", diff --git a/packages/ui/src/v11_7.ts b/packages/ui/src/v11_7.ts index a0b7911323..8eab1d7a80 100644 --- a/packages/ui/src/v11_7.ts +++ b/packages/ui/src/v11_7.ts @@ -47,6 +47,8 @@ export { Avatar } from '@instructure/ui-avatar/v11_7' export type { AvatarProps } from '@instructure/ui-avatar/v11_7' export { Badge } from '@instructure/ui-badge/v11_7' export type { BadgeProps } from '@instructure/ui-badge/v11_7' +export { Banner } from '@instructure/ui-banner/v11_7' +export type { BannerProps } from '@instructure/ui-banner/v11_7' export { Billboard } from '@instructure/ui-billboard/v11_7' export type { BillboardProps } from '@instructure/ui-billboard/v11_7' export { Breadcrumb, BreadcrumbLink } from '@instructure/ui-breadcrumb/v11_7' diff --git a/packages/ui/tsconfig.build.json b/packages/ui/tsconfig.build.json index 85f3206f78..5fe7ee347d 100644 --- a/packages/ui/tsconfig.build.json +++ b/packages/ui/tsconfig.build.json @@ -22,6 +22,9 @@ { "path": "../ui-badge/tsconfig.build.json" }, + { + "path": "../ui-banner/tsconfig.build.json" + }, { "path": "../ui-billboard/tsconfig.build.json" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 025a3fe741..9d15426fa6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -662,6 +662,9 @@ importers: '@instructure/ui-badge': specifier: workspace:* version: link:../ui-badge + '@instructure/ui-banner': + specifier: workspace:* + version: link:../ui-banner '@instructure/ui-billboard': specifier: workspace:* version: link:../ui-billboard @@ -1171,6 +1174,61 @@ importers: specifier: ^4.1.9 version: 4.1.9(@types/node@22.19.15)(@vitest/browser-playwright@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1)(vite@7.3.1(@types/node@24.1.0)(jiti@2.6.1)(terser@5.48.0)(yaml@2.9.0)) + packages/ui-banner: + dependencies: + '@babel/runtime': + specifier: ^7.29.7 + version: 7.29.7 + '@instructure/console': + specifier: workspace:* + version: link:../console + '@instructure/emotion': + specifier: workspace:* + version: link:../emotion + '@instructure/shared-types': + specifier: workspace:* + version: link:../shared-types + '@instructure/ui-buttons': + specifier: workspace:* + version: link:../ui-buttons + '@instructure/ui-heading': + specifier: workspace:* + version: link:../ui-heading + '@instructure/ui-icons': + specifier: workspace:* + version: link:../ui-icons + '@instructure/ui-react-utils': + specifier: workspace:* + version: link:../ui-react-utils + '@instructure/ui-text': + specifier: workspace:* + version: link:../ui-text + '@instructure/ui-themes': + specifier: workspace:* + version: link:../ui-themes + react: + specifier: 18.3.1 + version: 18.3.1 + devDependencies: + '@instructure/ui-axe-check': + specifier: workspace:* + version: link:../ui-axe-check + '@instructure/ui-babel-preset': + specifier: workspace:* + version: link:../ui-babel-preset + '@instructure/ui-color-utils': + specifier: workspace:* + version: link:../ui-color-utils + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: 16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.1)(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + vitest: + specifier: ^4.1.9 + version: 4.1.9(@types/node@24.1.0)(@vitest/browser-playwright@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1)(vite@7.3.1(@types/node@24.1.0)(jiti@2.6.1)(terser@5.48.0)(yaml@2.9.0)) + packages/ui-billboard: dependencies: '@babel/runtime': @@ -3903,8 +3961,8 @@ importers: specifier: workspace:* version: link:../command-utils '@instructure/instructure-design-tokens': - specifier: github:instructure/instructure-design-tokens#v1.5.0 - version: https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/ae8f600e8ad4cbadddbaad857f0b6477c4a1e1d6 + specifier: github:instructure/instructure-design-tokens#v1.8.0 + version: https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/b85c96436131df6a23174a54bcbd3b03a5da1b8a dprint: specifier: ^0.55.1 version: 0.55.1 @@ -7065,8 +7123,8 @@ packages: '@types/node': optional: true - '@instructure/instructure-design-tokens@https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/ae8f600e8ad4cbadddbaad857f0b6477c4a1e1d6': - resolution: {gitHosted: true, tarball: https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/ae8f600e8ad4cbadddbaad857f0b6477c4a1e1d6} + '@instructure/instructure-design-tokens@https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/b85c96436131df6a23174a54bcbd3b03a5da1b8a': + resolution: {gitHosted: true, tarball: https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/b85c96436131df6a23174a54bcbd3b03a5da1b8a} version: 1.0.0 '@isaacs/cliui@8.0.2': @@ -14191,7 +14249,7 @@ snapshots: '@commitlint/top-level': 21.2.0 '@commitlint/types': 21.2.0 git-raw-commits: 5.0.1(conventional-commits-parser@6.4.0) - tinyexec: 1.0.4 + tinyexec: 1.2.4 transitivePeerDependencies: - conventional-commits-filter - conventional-commits-parser @@ -14865,7 +14923,7 @@ snapshots: optionalDependencies: '@types/node': 22.19.15 - '@instructure/instructure-design-tokens@https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/ae8f600e8ad4cbadddbaad857f0b6477c4a1e1d6': + '@instructure/instructure-design-tokens@https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/b85c96436131df6a23174a54bcbd3b03a5da1b8a': dependencies: glob: 13.0.6 diff --git a/regression-test/cypress/e2e/spec.cy.ts b/regression-test/cypress/e2e/spec.cy.ts index 62424e9fda..27baa54ded 100644 --- a/regression-test/cypress/e2e/spec.cy.ts +++ b/regression-test/cypress/e2e/spec.cy.ts @@ -113,6 +113,7 @@ const PAGES: PageSpec[] = [ { slug: 'alert', title: 'Alert' }, { slug: 'avatar', title: 'Avatar', wait: 300 }, { slug: 'badge', title: 'Badge' }, + { slug: 'banner', title: 'Banner' }, { slug: 'billboard', title: 'Billboard' }, { slug: 'breadcrumb', diff --git a/regression-test/src/app/banner/page.tsx b/regression-test/src/app/banner/page.tsx new file mode 100644 index 0000000000..ceaffca3b4 --- /dev/null +++ b/regression-test/src/app/banner/page.tsx @@ -0,0 +1,71 @@ +/* + * 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. + */ +'use client' +import React from 'react' +import { Banner as bn, View as vw } from '@instructure/ui/latest' + +// alias to avoid TS/SSR friction like other pages +const Banner = bn as any +const View = vw as any + +export default function BannerPage() { + return ( +
+ {/* Default usage */} + + + Your course has been published and is now visible to students. + + + + {/* Colors */} + + + This banner uses the sea color treatment. + + + + {/* Compact density */} + + + This banner uses the compact density. + + + + {/* Dismissible with CTA */} + + {}} + ctaText="Learn more" + onCtaClick={() => {}} + > + Try out the new grading workflow before it becomes the default. + + +
+ ) +}