diff --git a/packages/ui-card/package.json b/packages/ui-card/package.json index da9cef2579..a7fc4f30dd 100644 --- a/packages/ui-card/package.json +++ b/packages/ui-card/package.json @@ -27,7 +27,8 @@ "@instructure/emotion": "workspace:*", "@instructure/shared-types": "workspace:*", "@instructure/ui-react-utils": "workspace:*", - "@instructure/ui-themes": "workspace:*" + "@instructure/ui-themes": "workspace:*", + "@instructure/ui-heading": "workspace:*" }, "devDependencies": { "@instructure/ui-axe-check": "workspace:*", diff --git a/packages/ui-card/src/ImageCard/__tests__/ImageCard.test.tsx b/packages/ui-card/src/ImageCard/__tests__/ImageCard.test.tsx new file mode 100644 index 0000000000..4bb1d21294 --- /dev/null +++ b/packages/ui-card/src/ImageCard/__tests__/ImageCard.test.tsx @@ -0,0 +1,348 @@ +/* + * 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 } from 'vitest/browser' +import { describe, it, expect } from 'vitest' +import { InstUISettingsProvider } from '@instructure/emotion' +import { runAxeCheck } from '@instructure/ui-axe-check' + +import { ImageCard } from '@instructure/ui-card/latest' +import type { ImageCardProps } from '@instructure/ui-card/latest' + +// Every theme puts breakpointMd at 20rem and breakpointLg at 40rem, so these +// widths pin an ImageCard to one size each. +const BELOW_MD = '200px' +const BETWEEN = '400px' +const ABOVE_LG = '800px' + +// a 1x1 transparent gif keeps the tests off the network +const SRC = + 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' + +// ImageCard renders an outer container-query element, a padded surface inside +// it, and a wrapper around the children inside the surface. +const partsOf = (root: HTMLElement) => { + const card = root.firstElementChild as HTMLElement + const container = card.firstElementChild as HTMLElement + return { + card, + container, + img: container.querySelector('img') as HTMLImageElement, + heading: container.querySelector('h1, h2, h3, h4, h5, h6') as HTMLElement, + content: container.lastElementChild as HTMLElement + } +} + +const renderImageCard = async ( + width: string, + props: Partial = {} +) => { + const { container } = await render( +
+ + Card content + +
+ ) + return partsOf(container.firstElementChild as HTMLElement) +} + +// the canvas theme gives every card title the same font size, so the title +// sizes are overridden to tell the steps apart +const headingSizeOverride = { + components: { + Heading: { + titleCardMini: { fontSize: '10px' }, + titleCardRegular: { fontSize: '20px' }, + titleCardLarge: { fontSize: '30px' } + } + } +} + +// every step gets a distinct value, because the canvas theme repeats values +// across steps +const spacingOverride = { + paddingBaseSm: '3px', + paddingBaseMd: '5px', + paddingBaseLg: '7px', + borderRadiusBaseSm: '2px', + borderRadiusBaseMd: '4px', + borderRadiusBaseLg: '6px', + imgBorderRadiusSm: '8px', + imgBorderRadiusMd: '10px', + imgBorderRadiusLg: '12px', + imgTextGapSm: '9px', + imgTextGapMd: '11px', + imgTextGapLg: '13px', + titleContentGapSm: '15px', + titleContentGapMd: '17px', + titleContentGapLg: '19px' +} + +const paddingOf = (element: HTMLElement) => + parseFloat(getComputedStyle(element).paddingTop) + +const fontSizeOf = (element: HTMLElement) => + parseFloat(getComputedStyle(element).fontSize) + +describe('', () => { + describe('for a11y', () => { + it('should be accessible', async () => { + const { container } = await render( + + Card content + + ) + const axeCheck = await runAxeCheck(container) + expect(axeCheck).toBe(true) + }) + + it('should expose the image to assistive technology by its alt text', async () => { + await render() + await expect + .element(page.getByRole('img', { name: 'A cat' })) + .toBeVisible() + }) + }) + + describe('with the default props', () => { + it('should render the title as a heading', async () => { + await render() + await expect + .element(page.getByRole('heading', { name: 'Card title' })) + .toBeVisible() + }) + + it('should render its children', async () => { + const { content } = await renderImageCard(BETWEEN) + expect(content.textContent).toBe('Card content') + }) + + it('should render without children', async () => { + const { container } = await render( +
+ +
+ ) + const { content } = partsOf(container.firstElementChild as HTMLElement) + expect(content.textContent).toBe('') + }) + + it('should render the image with the given src', async () => { + const { img } = await renderImageCard(BETWEEN) + expect(img.getAttribute('src')).toBe(SRC) + }) + + it('should render the image, then the title, then the children', async () => { + const { container, img, heading, content } = await renderImageCard( + BETWEEN + ) + + expect(Array.from(container.children)).toEqual([img, heading, content]) + }) + + it('should fill the width of its container', async () => { + const { card } = await renderImageCard(BETWEEN) + expect(card.getBoundingClientRect().width).toBe(400) + }) + + it('should draw a background color and a shadow', async () => { + const { container } = await renderImageCard(BETWEEN) + const style = getComputedStyle(container) + expect(style.backgroundColor).not.toBe('rgba(0, 0, 0, 0)') + expect(style.boxShadow).not.toBe('none') + }) + + it('should wrap content that has no break opportunity', async () => { + const { container } = await renderImageCard(BELOW_MD) + expect(getComputedStyle(container).overflowWrap).toBe('anywhere') + }) + }) + + describe('for the image', () => { + it('should fill the width inside the padding', async () => { + const { img, container } = await renderImageCard(BETWEEN) + const style = getComputedStyle(container) + + expect(img.getBoundingClientRect().width).toBe( + container.getBoundingClientRect().width - + parseFloat(style.paddingLeft) - + parseFloat(style.paddingRight) + ) + }) + + it('should keep the aspect ratio from the theme', async () => { + const { img } = await renderImageCard(BETWEEN, { + themeOverride: { imgAspectRatio: 2 } + }) + const { width, height } = img.getBoundingClientRect() + expect(width / height).toBeCloseTo(2, 2) + }) + + it('should set the alt text on the image', async () => { + const { img } = await renderImageCard(BETWEEN, { alt: 'A cat' }) + expect(img.getAttribute('alt')).toBe('A cat') + }) + + it('should crop the image instead of stretching it', async () => { + const { img } = await renderImageCard(BETWEEN) + expect(getComputedStyle(img).objectFit).toBe('cover') + }) + }) + + describe('for responsive spacing', () => { + it('should step padding up as the card gets wider', async () => { + const { container: small } = await renderImageCard(BELOW_MD) + const { container: medium } = await renderImageCard(BETWEEN) + const { container: large } = await renderImageCard(ABOVE_LG) + + expect(paddingOf(small)).toBeLessThan(paddingOf(medium)) + expect(paddingOf(medium)).toBeLessThan(paddingOf(large)) + }) + + it.each([ + [BELOW_MD, '3px', '2px', '8px'], + [BETWEEN, '5px', '4px', '10px'], + [ABOVE_LG, '7px', '6px', '12px'] + ])( + 'should take padding and radii from the tokens for a %s wide card', + async (width, padding, radius, imgRadius) => { + const { container, img } = await renderImageCard(width, { + themeOverride: spacingOverride + }) + const style = getComputedStyle(container) + + expect(style.paddingTop).toBe(padding) + expect(style.borderTopLeftRadius).toBe(radius) + expect(getComputedStyle(img).borderTopLeftRadius).toBe(imgRadius) + } + ) + + it.each([ + [BELOW_MD, '9px', '15px'], + [BETWEEN, '11px', '17px'], + [ABOVE_LG, '13px', '19px'] + ])( + 'should take the image and title gaps from the tokens for a %s wide card', + async (width, imgGap, titleGap) => { + const { img, content } = await renderImageCard(width, { + themeOverride: spacingOverride + }) + + expect(getComputedStyle(img).marginBottom).toBe(imgGap) + expect(getComputedStyle(content).marginTop).toBe(titleGap) + } + ) + + it('should read its own width, not the viewport width', async () => { + const { container } = await render( +
+
+ +
+
+ +
+
+ ) + const narrow = partsOf( + container.querySelector('[data-testid="narrow"]') as HTMLElement + ) + const wide = partsOf( + container.querySelector('[data-testid="wide"]') as HTMLElement + ) + + expect(paddingOf(narrow.container)).toBeLessThan( + paddingOf(wide.container) + ) + }) + }) + + describe('for the title size', () => { + const renderWithTitleSizes = (width: string) => + render( + +
+ +
+
+ ) + + const headingOf = (container: HTMLElement) => + container.querySelector('h1, h2, h3, h4, h5, h6') as HTMLElement + + it.each([ + [BELOW_MD, 10], + [BETWEEN, 20], + [ABOVE_LG, 30] + ])('should size the title for a %s wide card', async (width, size) => { + const { container } = await renderWithTitleSizes(width) + await expect.poll(() => fontSizeOf(headingOf(container))).toBe(size) + }) + + it('should resize the title when the card is resized', async () => { + const { container } = await renderWithTitleSizes(BELOW_MD) + const wrapper = container.querySelector( + '[data-testid="wrapper"]' + ) as HTMLElement + + await expect.poll(() => fontSizeOf(headingOf(container))).toBe(10) + wrapper.style.width = ABOVE_LG + await expect.poll(() => fontSizeOf(headingOf(container))).toBe(30) + wrapper.style.width = BETWEEN + await expect.poll(() => fontSizeOf(headingOf(container))).toBe(20) + }) + + it('should switch size at the token breakpoints', async () => { + const { container } = await render( + +
+ +
+
+ ) + await expect.poll(() => fontSizeOf(headingOf(container))).toBe(30) + }) + }) + + describe('for the passthrough props', () => { + it('should pass through allowed HTML attributes to the outer element', async () => { + const { card } = await renderImageCard(BETWEEN, { + 'data-testid': 'image-card' + } as Partial) + expect(card.getAttribute('data-testid')).toBe('image-card') + }) + + it('should not render the title as an attribute of the outer element', async () => { + const { card } = await renderImageCard(BETWEEN) + expect(card.hasAttribute('title')).toBe(false) + }) + }) +}) diff --git a/packages/ui-card/src/ImageCard/v1/README.md b/packages/ui-card/src/ImageCard/v1/README.md new file mode 100644 index 0000000000..ca8adf36c6 --- /dev/null +++ b/packages/ui-card/src/ImageCard/v1/README.md @@ -0,0 +1,64 @@ +--- +describes: ImageCard +--- + +Use `` to present a card with an image on top, a title and +supporting content below. + +```js +--- +type: example +--- + + Content goes here. + +``` + +### Breakpoints + +Card has 3 visual designs based on its size, these are applied automatically + +```js +--- +type: example +--- + +
+ + + Narrower than the medium breakpoint. + + +
+
+ + + Between the two breakpoints. + + +
+
+ + + Wider than the large breakpoint. + + +
+
+``` diff --git a/packages/ui-card/src/ImageCard/v1/index.tsx b/packages/ui-card/src/ImageCard/v1/index.tsx new file mode 100644 index 0000000000..8cf01c72a0 --- /dev/null +++ b/packages/ui-card/src/ImageCard/v1/index.tsx @@ -0,0 +1,118 @@ +/* + * 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 { useEffect, useRef, useState } from 'react' +import { useStyleNew } from '@instructure/emotion' +import { passthroughProps } from '@instructure/ui-react-utils' +import { Heading } from '@instructure/ui-heading/latest' + +import type { ImageCardProps } from './props.js' +import generateStyle from './styles.js' + +const headingVariantsBySize: { + sm: 'titleCardMini' + md: 'titleCardRegular' + lg: 'titleCardLarge' +} = { + sm: 'titleCardMini', + md: 'titleCardRegular', + lg: 'titleCardLarge' +} + +/** +--- +category: components/Cards +--- +**/ +const ImageCard = ({ + children, + src, + alt, + title, + titleLevel = 'h3', + themeOverride, + ...rest +}: ImageCardProps) => { + const styles = useStyleNew({ + generateStyle, + themeOverride, + componentId: 'ImageCard', + displayName: 'ImageCard' + }) + + const [size, setSize] = useState<'sm' | 'md' | 'lg'>('md') + + const ref = useRef(null) + + const getBreakpointsInPx = (breakpointMd: any, breakpointLg: any) => { + const fontSize = Number( + getComputedStyle(document.documentElement).fontSize.slice(0, -2) + ) + + return { + // assume rem format every time + breakpointMd: Number(breakpointMd.slice(0, -3)) * fontSize, + breakpointLg: Number(breakpointLg.slice(0, -3)) * fontSize + } + } + + useEffect(() => { + const element = ref.current + if (!element) return + + const observer = new ResizeObserver(([entry]) => { + const { width } = entry.contentRect + const { breakpointMd, breakpointLg } = getBreakpointsInPx( + styles.breakpointMd, + styles.breakpointLg + ) + + if (width < breakpointMd) { + setSize('sm') + } else if (width >= breakpointLg) { + setSize('lg') + } else { + setSize('md') + } + }) + + observer.observe(element) + + return () => observer.disconnect() + }, [styles.breakpointMd, styles.breakpointLg]) + + return ( +
+
+ {alt} + + {title} + +
{children}
+
+
+ ) +} + +export default ImageCard +export { ImageCard } diff --git a/packages/ui-card/src/ImageCard/v1/props.ts b/packages/ui-card/src/ImageCard/v1/props.ts new file mode 100644 index 0000000000..e49b041a45 --- /dev/null +++ b/packages/ui-card/src/ImageCard/v1/props.ts @@ -0,0 +1,76 @@ +/* + * 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 { OtherHTMLAttributes } from '@instructure/shared-types' +import type { NewComponentTypes } from '@instructure/ui-themes' + +type HeadingLevel = U + +type ImageCardOwnProps = { + /** + * The content to be rendered inside the Card + */ + children?: ReactNode + /** + * The title string of the card. Renders as InstUI's + */ + title: string + /** + * Heading is rendered as h3 by default. Rewrite this if necessary. + */ + titleLevel?: HeadingLevel<'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'> | 'reset' + /** + * The src string of the img. + */ + src: string + /** + * The alt string of the img. + */ + alt: string +} + +type PropKeys = keyof ImageCardOwnProps + +type AllowedPropKeys = Readonly> + +type ImageCardProps = ImageCardOwnProps & + NewThemeOverrideProp> & + OtherHTMLAttributes + +type ImageCardStyle = ComponentStyle< + 'card' | 'container' | 'content' | 'img' | 'breakpointMd' | 'breakpointLg' +> + +const allowedProps: AllowedPropKeys = [ + 'children', + 'src', + 'alt', + 'title', + 'titleLevel' +] + +export type { ImageCardProps, ImageCardStyle } +export { allowedProps } diff --git a/packages/ui-card/src/ImageCard/v1/styles.ts b/packages/ui-card/src/ImageCard/v1/styles.ts new file mode 100644 index 0000000000..9e2e94f16b --- /dev/null +++ b/packages/ui-card/src/ImageCard/v1/styles.ts @@ -0,0 +1,130 @@ +/* + * 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 { boxShadowObjectsToCSSString } from '@instructure/ui-themes' +import type { NewComponentTypes } from '@instructure/ui-themes' +import type { ImageCardStyle } from './props' + +/** + * --- + * private: true + * --- + * Generates the style object from the theme and provided additional information + * @param componentTheme The theme variable object. + * @param props the props of the component, the style is applied to + * @return The final style object, which will be used in the component + */ +const generateStyle = ( + componentTheme: ReturnType +): ImageCardStyle => { + const variantSizeStyles = { + sm: { + borderRadius: componentTheme.borderRadiusBaseSm, + padding: componentTheme.paddingBaseSm + }, + md: { + borderRadius: componentTheme.borderRadiusBaseMd, + padding: componentTheme.paddingBaseMd + }, + lg: { + borderRadius: componentTheme.borderRadiusBaseLg, + padding: componentTheme.paddingBaseLg + } + } + + const imageSizeStyles = { + sm: { + marginBottom: componentTheme.imgTextGapSm, + borderRadius: componentTheme.imgBorderRadiusSm + }, + md: { + marginBottom: componentTheme.imgTextGapMd, + borderRadius: componentTheme.imgBorderRadiusMd + }, + lg: { + marginBottom: componentTheme.imgTextGapLg, + borderRadius: componentTheme.imgBorderRadiusLg + } + } + + const sizeQueries = { + sm: `@container (width < ${componentTheme.breakpointMd})`, + md: `@container ((width >= ${componentTheme.breakpointMd}) and (width < ${componentTheme.breakpointLg}))`, + lg: `@container (width >= ${componentTheme.breakpointLg})` + } + + return { + card: { + label: 'card', + containerType: 'inline-size', + width: '100%' + }, + container: { + label: 'card__content', + overflowWrap: 'anywhere', + background: componentTheme.backgroundColor, + boxShadow: boxShadowObjectsToCSSString(componentTheme.boxShadow), + [sizeQueries.sm]: { + ...variantSizeStyles.sm + }, + [sizeQueries.md]: { + ...variantSizeStyles.md + }, + [sizeQueries.lg]: { + ...variantSizeStyles.lg + } + }, + content: { + [sizeQueries.sm]: { + marginTop: componentTheme.titleContentGapSm + }, + [sizeQueries.md]: { + marginTop: componentTheme.titleContentGapMd + }, + [sizeQueries.lg]: { + marginTop: componentTheme.titleContentGapLg + } + }, + img: { + label: 'card__img', + display: 'block', + width: '100%', + aspectRatio: componentTheme.imgAspectRatio, + objectFit: 'cover', + [sizeQueries.sm]: { + ...imageSizeStyles.sm + }, + [sizeQueries.md]: { + ...imageSizeStyles.md + }, + [sizeQueries.lg]: { + ...imageSizeStyles.lg + } + }, + breakpointMd: componentTheme.breakpointMd, + breakpointLg: componentTheme.breakpointLg + } +} + +export default generateStyle diff --git a/packages/ui-card/src/exports/a.ts b/packages/ui-card/src/exports/a.ts index d64ba77528..af07cd376c 100644 --- a/packages/ui-card/src/exports/a.ts +++ b/packages/ui-card/src/exports/a.ts @@ -23,3 +23,5 @@ */ export { Card } from '../Card/v1/index.js' export type { CardProps } from '../Card/v1/props' +export { ImageCard } from '../ImageCard/v1/index.js' +export type { ImageCardProps } from '../ImageCard/v1/props' diff --git a/packages/ui-card/tsconfig.build.json b/packages/ui-card/tsconfig.build.json index 9684ded0d6..198e74f082 100644 --- a/packages/ui-card/tsconfig.build.json +++ b/packages/ui-card/tsconfig.build.json @@ -19,6 +19,9 @@ { "path": "../ui-babel-preset/tsconfig.build.json" }, + { + "path": "../ui-heading/tsconfig.build.json" + }, { "path": "../ui-react-utils/tsconfig.build.json" }, diff --git a/packages/ui/src/v11_7.ts b/packages/ui/src/v11_7.ts index 0ea08ae022..fca52c466f 100644 --- a/packages/ui/src/v11_7.ts +++ b/packages/ui/src/v11_7.ts @@ -83,8 +83,8 @@ export type { CalendarDayProps, CalendarProps } from '@instructure/ui-calendar/v11_7' -export { Card } from '@instructure/ui-card/v11_7' -export type { CardProps } from '@instructure/ui-card/v11_7' +export { Card, ImageCard } from '@instructure/ui-card/v11_7' +export type { CardProps, ImageCardProps } from '@instructure/ui-card/v11_7' export { Checkbox, CheckboxGroup, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a98714d5d..cb2ff8eb19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1534,6 +1534,9 @@ importers: '@instructure/shared-types': specifier: workspace:* version: link:../shared-types + '@instructure/ui-heading': + specifier: workspace:* + version: link:../ui-heading '@instructure/ui-react-utils': specifier: workspace:* version: link:../ui-react-utils diff --git a/regression-test/cypress/e2e/spec.cy.ts b/regression-test/cypress/e2e/spec.cy.ts index e260d93bcb..7a990e648e 100644 --- a/regression-test/cypress/e2e/spec.cy.ts +++ b/regression-test/cypress/e2e/spec.cy.ts @@ -164,6 +164,7 @@ const PAGES: PageSpec[] = [ { slug: 'form-errors', title: 'Form errors', wait: 300 }, { slug: 'global-nav', title: 'GlobalNav' }, { slug: 'heading', title: 'Heading' }, + { slug: 'imagecard', title: 'ImageCard', wait: 100 }, { slug: 'img', title: 'Img', wait: 100 }, { slug: 'link', diff --git a/regression-test/src/app/imagecard/page.tsx b/regression-test/src/app/imagecard/page.tsx new file mode 100644 index 0000000000..b6db67edfb --- /dev/null +++ b/regression-test/src/app/imagecard/page.tsx @@ -0,0 +1,57 @@ +/* + * 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 { ImageCard } from '@instructure/ui/latest' + +const SRC = + 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' + +// ImageCard picks its title size, padding and border radius from its own +// width, so each wrapper width below pins the card inside it to one step. +const STEPS = [ + { width: '200px', label: 'below breakpointMd' }, + { width: '400px', label: 'between breakpointMd and breakpointLg' }, + { width: '800px', label: 'above breakpointLg' } +] + +export default function ImageCardPage() { + return ( +
+
+ {STEPS.map(({ width, label }) => ( +
+ + Image card, {label} + +
+ ))} +
+
+ ) +} diff --git a/regression-test/src/app/page.tsx b/regression-test/src/app/page.tsx index 11602937f8..5d4e6299e7 100644 --- a/regression-test/src/app/page.tsx +++ b/regression-test/src/app/page.tsx @@ -49,6 +49,7 @@ const components = [ 'form-errors', 'global-nav', 'heading', + 'imagecard', 'img', 'link', 'menu',