From 6d213052120773f3c8bf701a61facb4dae2b7588 Mon Sep 17 00:00:00 2001 From: adamlobler Date: Tue, 8 Sep 2026 16:15:08 +0200 Subject: [PATCH 1/2] feat(ui-banner): add Banner component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new promotional Banner component built on tokens already registered in instructure-design-tokens across all four themes. Supports violet/sea colors, relaxed/compact density, an optional dismiss control, an optional header and decorative icon slot, and primary/secondary action slots. Wires the component into the ui umbrella package (v11_7 export) and adds a regression-test page and Cypress spec entry. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .claude/launch.json | 11 ++ packages/ui-banner/.gitignore | 4 + packages/ui-banner/babel.config.js | 36 +++++ packages/ui-banner/package.json | 87 ++++++++++ .../src/Banner/__tests__/Banner.test.tsx | 150 ++++++++++++++++++ packages/ui-banner/src/Banner/v1/README.md | 134 ++++++++++++++++ packages/ui-banner/src/Banner/v1/index.tsx | 147 +++++++++++++++++ packages/ui-banner/src/Banner/v1/props.ts | 132 +++++++++++++++ packages/ui-banner/src/Banner/v1/styles.ts | 139 ++++++++++++++++ packages/ui-banner/src/exports/a.ts | 27 ++++ packages/ui-banner/tsconfig.build.json | 23 +++ packages/ui-banner/tsconfig.json | 4 + packages/ui/package.json | 1 + packages/ui/src/v11_7.ts | 2 + packages/ui/tsconfig.build.json | 3 + pnpm-lock.yaml | 58 +++++++ regression-test/cypress/e2e/spec.cy.ts | 1 + regression-test/src/app/banner/page.tsx | 78 +++++++++ 18 files changed, 1037 insertions(+) create mode 100644 .claude/launch.json 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/tsconfig.build.json create mode 100644 packages/ui-banner/tsconfig.json create mode 100644 regression-test/src/app/banner/page.tsx diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000000..0bf9891086 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "instui-docs", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["run", "dev"], + "port": 9090 + } + ] +} 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..40fd6183ef --- /dev/null +++ b/packages/ui-banner/package.json @@ -0,0 +1,87 @@ +{ + "name": "@instructure/ui-banner", + "version": "11.7.5", + "description": "A UI component for displaying a promotional Banner message", + "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-a11y-content": "workspace:*", + "@instructure/ui-buttons": "workspace:*", + "@instructure/ui-heading": "workspace:*", + "@instructure/ui-react-utils": "workspace:*", + "@instructure/ui-themes": "workspace:*", + "@instructure/ui-view": "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/a.ts", + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.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" + } + } +} 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..302f9ac9c4 --- /dev/null +++ b/packages/ui-banner/src/Banner/__tests__/Banner.test.tsx @@ -0,0 +1,150 @@ +/* + * 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 } from 'vitest-browser-react' +import { page, userEvent } from 'vitest/browser' +import { describe, it, expect, vi } from 'vitest' +import { runAxeCheck } from '@instructure/ui-axe-check' + +import { Banner } from '@instructure/ui-banner/latest' + +describe('', () => { + it('should render the header and children', async () => { + const { container } = await render( + + Banner body copy + + ) + + expect(container).toHaveTextContent('Banner header') + expect(container).toHaveTextContent('Banner body copy') + }) + + it('should render a close button by default and fire onDismiss', async () => { + const onDismiss = vi.fn() + await render(Banner body copy) + + const closeButton = page.getByRole('button') + await closeButton.click() + + expect(onDismiss).toHaveBeenCalled() + }) + + it('should not render a close button when dismissible is false', async () => { + const { container } = await render( + Banner body copy + ) + + expect(container.querySelector('button')).toBeNull() + }) + + it('should resolve the close button accessible name to the default label', async () => { + await render( {}}>Banner body copy) + + const closeButton = page.getByRole('button', { name: 'Close banner' }) + await expect.element(closeButton).toBeInTheDocument() + }) + + it('should allow overriding the close button accessible label', async () => { + await render( + {}}> + Banner body copy + + ) + + const closeButton = page.getByRole('button', { name: 'Dismiss promo' }) + await expect.element(closeButton).toBeInTheDocument() + }) + + it('should render a decorative icon as aria-hidden', async () => { + const { container } = await render( + icon} + > + Banner body copy + + ) + + const iconWrapper = container.querySelector( + '[data-testid="icon"]' + )?.parentElement + + expect(iconWrapper).toHaveAttribute('aria-hidden', 'true') + }) + + it('should render primary and secondary actions', async () => { + const { container } = await render( + } + renderSecondaryAction={() => } + > + Banner body copy + + ) + + expect(container).toHaveTextContent('Primary') + expect(container).toHaveTextContent('Secondary') + }) + + it('should not render a secondary action without a primary action', async () => { + const { container } = await render( + } + > + Banner body copy + + ) + + expect(container).not.toHaveTextContent('Secondary') + }) + + it('should be keyboard operable', async () => { + const onDismiss = vi.fn() + await render(Banner body copy) + + const closeButton = page.getByRole('button').element() as HTMLElement + closeButton.focus() + await userEvent.keyboard('{Enter}') + + expect(onDismiss).toHaveBeenCalled() + }) + + it('should meet a11y standards', async () => { + const { container } = await render( + {}} + renderPrimaryAction={() => } + > + Banner body copy + + ) + + 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..faa5dad874 --- /dev/null +++ b/packages/ui-banner/src/Banner/v1/README.md @@ -0,0 +1,134 @@ +--- +describes: Banner +--- + +A `Banner` is a promotional message used to proactively surface a feature, +offer, event, or announcement. It communicates something proactive, not a +system or account status — use [Alert](#Alert) for status, warnings, and +confirmations that need to interrupt and be responded to. + +### Default + +By default, `Banner` is dismissible and shows an optional header, body copy, +and a single primary action. + +```js +--- +type: example +--- + {}}> + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. + +``` + +### Colors + +Use `color` to choose the visual treatment that best fits the surrounding +page. + +```js +--- +type: example +--- +
+ {}} + margin="0 0 medium 0" + > + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + + {}}> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +
+``` + +### Density + +Use `density="compact"` when there's less room for the banner, such as inside +a smaller container. + +```js +--- +type: example +--- + {}}> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +``` + +### Dismissible + +Reserve `dismissible={false}` for time-sensitive or recurring campaign +banners, where the user shouldn't lose track of the message. A `Banner` is a +controlled component — it does not manage its own visibility, so mount and +unmount it based on your own state in `onDismiss`. + +```js +--- +type: example +--- + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +``` + +### Actions + +Use `renderPrimaryAction` when there's a next step for the user to take, and +`renderSecondaryAction` for at most one additional action. Don't use actions +for page navigation — link the header or message text instead. + +```js +--- +type: example +--- + {}} + renderPrimaryAction={() => } + renderSecondaryAction={() => ( + + )} +> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +``` + +### Icon + +`renderIcon` accepts a function that returns a decorative icon or +illustration. It's treated as purely decorative, so don't rely on it to +convey information — put anything meaningful in the header or body copy. + +```js +--- +type: example +--- + {}} + renderIcon={() => } +> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +``` + +### Guidelines + +```js +--- +type: embed +--- + +
+ The icon is decorative; don't rely on it to convey meaning + The dismiss button always needs an accessible label +
+
+``` 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..c36647f6af --- /dev/null +++ b/packages/ui-banner/src/Banner/v1/index.tsx @@ -0,0 +1,147 @@ +/* + * 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 { CloseButton } from '@instructure/ui-buttons/latest' +import { Heading } from '@instructure/ui-heading/latest' +import { View } from '@instructure/ui-view/latest' + +import { useStyleNew } from '@instructure/emotion' +import generateStyle from './styles.js' + +import type { BannerProps } from './props' + +/** +--- +category: components +--- + +A `Banner` is a promotional message used to proactively surface a feature, +offer, event, or announcement. Unlike `Alert`, a `Banner` doesn't communicate +system or account status — use `Alert` for that instead. + +```js +--- +type: example +--- + {}}> + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. + +``` +**/ +const Banner = forwardRef( + ( + { + color = 'violet', + density = 'relaxed', + dismissible = true, + onDismiss, + closeButtonLabel = 'Close banner', + renderIcon, + header, + children, + renderPrimaryAction, + renderSecondaryAction, + elementRef, + margin, + themeOverride + }, + ref + ) => { + const styles = useStyleNew({ + generateStyle, + themeOverride, + params: { + color, + density + }, + componentId: 'Banner', + displayName: 'Banner' + }) + + const handleRef = (el: Element | null) => { + if (typeof elementRef === 'function') { + elementRef(el) + } + if (typeof ref === 'function') { + ref(el as HTMLDivElement) + } else if (ref) { + const refObject = ref + refObject.current = el as HTMLDivElement + } + } + + const hasPrimaryAction = typeof renderPrimaryAction === 'function' + const hasSecondaryAction = + hasPrimaryAction && typeof renderSecondaryAction === 'function' + + return ( + + {renderIcon && ( + + )} + + + {header && ( + + {header} + + )} + {children &&
{children}
} +
+ {hasPrimaryAction && ( + + {renderPrimaryAction!()} + {hasSecondaryAction && renderSecondaryAction!()} + + )} +
+ {dismissible && ( + + + + )} +
+ ) + } +) + +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..e9ee36f364 --- /dev/null +++ b/packages/ui-banner/src/Banner/v1/props.ts @@ -0,0 +1,132 @@ +/* + * 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, + Spacing +} from '@instructure/emotion' +import type { NewComponentTypes } from '@instructure/ui-themes' + +type BannerOwnProps = { + /** + * The visual color treatment of the `Banner`. + */ + color?: 'violet' | 'sea' + + /** + * Controls the padding and internal spacing of the `Banner`. Use `compact` + * when there's less real estate for the banner such as smaller containers. + */ + density?: 'relaxed' | 'compact' + + /** + * Whether the `Banner` can be dismissed by the user. Reserve `false` for + * time-sensitive or recurring campaign banners where the user shouldn't + * lose track of the message. + */ + dismissible?: boolean + + /** + * Callback fired when the user dismisses the `Banner`. Required when + * `dismissible` is `true` — the `Banner` does not manage its own visibility. + */ + onDismiss?: () => void + + /** + * An accessible label for the dismiss button. + */ + closeButtonLabel?: ReactNode + + /** + * A function returning a decorative icon or illustration to render at the + * start of the `Banner`. Treated as purely decorative — pass meaningful + * content via `children` instead. + */ + renderIcon?: () => ReactNode + + /** + * An optional title for the `Banner`. + */ + header?: ReactNode + + /** + * The body content of the `Banner`. + */ + children?: ReactNode + + /** + * A function returning the primary action element (for example, a `Button`). + * Don't use this for page navigation — link the title or message instead. + */ + renderPrimaryAction?: () => ReactNode + + /** + * A function returning a secondary action element. Only rendered alongside + * a primary action. + */ + renderSecondaryAction?: () => ReactNode + + /** + * Provides a reference to the `Banner`'s underlying html element. + */ + elementRef?: (element: Element | null) => void + + /** + * Valid values are `0`, `none`, `auto`, and Spacing token values, + * see https://instructure.design/layout-spacing. Apply these values via + * familiar CSS-like shorthand. For example, `margin="general.spaceMd auto"`. + */ + margin?: Spacing +} + +type PropKeys = keyof BannerOwnProps + +type AllowedPropKeys = Readonly> + +type BannerProps = BannerOwnProps & + NewThemeOverrideProp> + +type BannerStyle = ComponentStyle< + 'banner' | 'icon' | 'content' | 'contentInner' | 'actions' | 'closeButton' +> + +const allowedProps: AllowedPropKeys = [ + 'color', + 'density', + 'dismissible', + 'onDismiss', + 'closeButtonLabel', + 'renderIcon', + 'header', + 'children', + 'renderPrimaryAction', + 'renderSecondaryAction', + 'elementRef', + 'margin' +] + +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..bb5699a2a5 --- /dev/null +++ b/packages/ui-banner/src/Banner/v1/styles.ts @@ -0,0 +1,139 @@ +/* + * 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, density } = params + + const backgroundByColor = { + violet: componentTheme.violetBackground, + sea: componentTheme.seaBackground + } + + const iconBackgroundByColor = { + violet: componentTheme.violetIconBackground, + sea: componentTheme.seaIconBackground + } + + const paddingByDensity = { + relaxed: `${componentTheme.relaxedPaddingVertical} ${componentTheme.relaxedPaddingHorizontal}`, + compact: `${componentTheme.compactPaddingVertical} ${componentTheme.compactPaddingHorizontal}` + } + + const iconBorderRadiusByDensity = { + relaxed: componentTheme.relaxedIconBorderRadius, + compact: componentTheme.compactIconBorderRadius + } + + // Matches the Figma spec's Illustration frame (32px, both densities). + // Not part of the Banner token set, so kept as a literal, like closeButtonMargin*. + const iconSize = '2rem' + + const contentGapHorizontalByDensity = { + relaxed: componentTheme.relaxedContentGapHorizontal, + compact: componentTheme.compactContentGapHorizontal + } + + return { + banner: { + label: 'banner', + position: 'relative', + display: 'flex', + borderRadius: componentTheme.borderRadius, + borderStyle: componentTheme.borderStyle, + borderWidth: componentTheme.borderWidth, + borderColor: componentTheme.borderColor, + color: componentTheme.color, + background: backgroundByColor[color!], + padding: paddingByDensity[density!], + gap: contentGapHorizontalByDensity[density!] + }, + icon: { + label: 'banner__icon', + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: iconSize, + height: iconSize, + background: iconBackgroundByColor[color!], + color: componentTheme.iconColor, + borderRadius: iconBorderRadiusByDensity[density!], + fontSize: '1rem', + '& svg': { + width: '1rem', + height: '1rem' + } + }, + content: { + label: 'banner__content', + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + gap: componentTheme.contentGapVertical + }, + contentInner: { + label: 'banner__contentInner', + display: 'flex', + flexDirection: 'column' + }, + actions: { + label: 'banner__actions', + display: 'flex', + alignItems: 'center', + // Matches the Figma spec's button group itemSpacing (12px). Not part + // of the Banner token set, so kept as a literal. + gap: '0.75rem' + }, + closeButton: { + label: 'banner__closeButton', + position: 'absolute', + top: componentTheme.closeButtonMarginTop, + insetInlineEnd: componentTheme.closeButtonMarginRight + } + } +} + +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..fc58933519 --- /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 { 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..0e746efd28 --- /dev/null +++ b/packages/ui-banner/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "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-a11y-content/tsconfig.build.json" }, + { "path": "../ui-axe-check/tsconfig.build.json" }, + { "path": "../ui-babel-preset/tsconfig.build.json" }, + { "path": "../ui-buttons/tsconfig.build.json" }, + { "path": "../ui-color-utils/tsconfig.build.json" }, + { "path": "../ui-heading/tsconfig.build.json" }, + { "path": "../ui-react-utils/tsconfig.build.json" }, + { "path": "../ui-themes/tsconfig.build.json" }, + { "path": "../ui-view/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/package.json b/packages/ui/package.json index c2fc557ca8..f82017cc05 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 f821ddec79..950eb89517 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -668,6 +668,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 @@ -1177,6 +1180,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-a11y-content': + specifier: workspace:* + version: link:../ui-a11y-content + '@instructure/ui-buttons': + specifier: workspace:* + version: link:../ui-buttons + '@instructure/ui-heading': + specifier: workspace:* + version: link:../ui-heading + '@instructure/ui-react-utils': + specifier: workspace:* + version: link:../ui-react-utils + '@instructure/ui-themes': + specifier: workspace:* + version: link:../ui-themes + '@instructure/ui-view': + specifier: workspace:* + version: link:../ui-view + 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': diff --git a/regression-test/cypress/e2e/spec.cy.ts b/regression-test/cypress/e2e/spec.cy.ts index 2becb31ce8..9cf728b2de 100644 --- a/regression-test/cypress/e2e/spec.cy.ts +++ b/regression-test/cypress/e2e/spec.cy.ts @@ -140,6 +140,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..1bcea3c855 --- /dev/null +++ b/regression-test/src/app/banner/page.tsx @@ -0,0 +1,78 @@ +/* + * 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, + Button as btn, + SearchInstUIIcon as sii +} from '@instructure/ui/latest' + +// alias to avoid TS/SSR friction like other pages +const Banner = bn as any +const Button = btn as any +const SearchInstUIIcon = sii as any + +export default function BannerPage() { + return ( +
+
+ {}}> + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. + +
+ +
+ {}}> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +
+ +
+ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +
+ +
+ {}} + renderIcon={() => } + renderPrimaryAction={() => ( + + )} + renderSecondaryAction={() => ( + + )} + > + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +
+
+ ) +} From f8a630496a0de8c448e0b94e81fe3a9c205fe388 Mon Sep 17 00:00:00 2001 From: adamlobler Date: Tue, 8 Sep 2026 16:25:20 +0200 Subject: [PATCH 2/2] build(ui-scripts): bump instructure-design-tokens to v1.8.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.5.0 predates the Banner component's tokens, so ui-banner's build:types failed in CI with "Property 'Banner' does not exist on type 'ComponentTypes'". v1.8.0 includes Banner's tokens across all four themes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/ui-scripts/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/ui-scripts/package.json b/packages/ui-scripts/package.json index 141579a530..9cf3fe6d41 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/pnpm-lock.yaml b/pnpm-lock.yaml index 950eb89517..9b9a9eaeb9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3964,8 +3964,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 @@ -7123,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': @@ -15050,7 +15050,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