diff --git a/docs/contributor-docs/migrating-to-new-tokens.md b/docs/contributor-docs/migrating-to-new-tokens.md index 5853f869ac..7e295e7bae 100644 --- a/docs/contributor-docs/migrating-to-new-tokens.md +++ b/docs/contributor-docs/migrating-to-new-tokens.md @@ -17,7 +17,7 @@ Changes needed: - Import token types from `@instructure/ui-themes` instead of `@instructure/shared-types` - Update `generateStyle` function to use `NewComponentTypes['ComponentName']` for the theme parameter - Replace old theme tokens with new token names from the design system -- Replace `@withStyleRework` decorator with `@withStyle` and remove `generateComponentTheme` +- Replace `@withStyleLegacy` decorator with `@withStyle` and remove `generateComponentTheme` - delete `theme.ts` If tokens are from a different (usually parent) components, add the `componentID` of that component as second paramater of `@withStyle` and use that name in the `generateStyle` function in `style.ts`: `NewComponentTypes['ParentComponentNameWithTheTokens']` diff --git a/packages/__docs__/buildScripts/DataTypes.mts b/packages/__docs__/buildScripts/DataTypes.mts index ec2d7ffe52..9eddf07f87 100644 --- a/packages/__docs__/buildScripts/DataTypes.mts +++ b/packages/__docs__/buildScripts/DataTypes.mts @@ -155,5 +155,6 @@ export type { Glyph, MainDocsData, MainIconsData, - JsDocResult + JsDocResult, + Section } diff --git a/packages/__docs__/buildScripts/build-docs.mts b/packages/__docs__/buildScripts/build-docs.mts index 400893a881..94fdc5427e 100644 --- a/packages/__docs__/buildScripts/build-docs.mts +++ b/packages/__docs__/buildScripts/build-docs.mts @@ -48,7 +48,7 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const require = createRequire(import.meta.url) -// This needs to be required otherwise TSC will mess up the directory structure +// This needs to be required, otherwise TSC will mess up the directory structure // in the output directory // eslint-disable-next-line @instructure/no-relative-imports const rootPackage = require('../../../package.json') // root package.json @@ -65,21 +65,23 @@ const library: LibraryOptions = { scope: '@instructure' } +// TODO this misses ui-react-utils/src/DeterministicIDContext/index.ts and maybe some others const pathsToProcess = [ // these can be commented out for faster debugging 'CHANGELOG.md', - '**/packages/**/*.md', // package READMEs + 'CODE_OF_CONDUCT.md', + 'LICENSE.md', '**/docs/**/*.md', // general docs '**/src/*.{ts,tsx}', // util src files - '**/src/*/*.{ts,tsx}', // component src files - '**/src/*/*/*.{ts,tsx}', // child component src files - 'CODE_OF_CONDUCT.md', - 'LICENSE.md' + // TODO expand this to support new components + '**/src/*/v1/*.md', // package READMEs + '**/src/*/v1/*.{ts,tsx}', // component src files + '**/src/*/v1/*/*.{ts,tsx}' // child component src files ] const pathsToIgnore = [ - '**/macro.{js,ts}', '**/svg/**', + 'packages/**/props.ts', 'packages/*/README.md', // main package READMEs '**/packages/**/CHANGELOG.md', '**/config/**', @@ -138,18 +140,30 @@ function buildDocs() { 'Parsing markdown and source files... (' + matches.length + ' files)' ) let docs = matches.map((relativePath) => { - // loop trough every source and Readme file + // loop through every source and Readme file return processSingleFile(path.resolve(relativePath)) }) - docs = docs.filter(Boolean) // filter out undefined + // filter out undefined and duplicates (because a component is parsed twice, + // by its index.ts and by its README) + const seen = new Set() + docs = docs.filter(item => { + if (!item) { + return false + } else if (seen.has(item.id)) { + return false + } + seen.add(item.id) + return true + }) + const themes = parseThemes() const clientProps = getClientProps(docs as ProcessedFile[], library) - const props: MainDocsData = { + const mainDocsData: MainDocsData = { ...clientProps, themes: themes, library } - const markdownsAndSources = JSON.stringify(props) + const markdownsAndSources = JSON.stringify(mainDocsData) fs.writeFileSync( buildDir + 'markdown-and-sources-data.json', markdownsAndSources @@ -210,7 +224,7 @@ function processSingleFile(fullPath: string) { let docObject const dirName = path.dirname(fullPath) const fileName = path.parse(fullPath).name - if (fileName === 'index') { + if (fileName === 'index') { // e.g. ui/alerts/src/Alert/v1/index.tsx docObject = processFile(fullPath, projectRoot, library) if (!docObject) { return @@ -221,8 +235,9 @@ function processSingleFile(fullPath: string) { docObject.description = readmeDesc ? docObject.description + readmeDesc : docObject.description - } else if (fileName === 'README') { - // if we edit a README, we'll need to add the changes to the components JSON + } else if (fileName === 'README') { // e.g. ui/alerts/src/Alert/v1/README.md + // If there is an index file in the same folder, it's a component, so + // the README will be the description and the props will be in the index file. let componentIndexFile: string | undefined if (fs.existsSync(path.join(dirName, 'index.tsx'))) { componentIndexFile = path.join(dirName, 'index.tsx') @@ -244,6 +259,8 @@ function processSingleFile(fullPath: string) { if (!docObject) { return } + // eslint-disable-next-line no-console + console.info(`id: ${docObject.id} at ${fullPath}`) const docJSON = JSON.stringify(docObject) fs.writeFileSync(buildDir + 'docs/' + docObject.id + '.json', docJSON) return docObject diff --git a/packages/__docs__/buildScripts/processFile.mts b/packages/__docs__/buildScripts/processFile.mts index 6d9cd6c415..f8d546d2e4 100644 --- a/packages/__docs__/buildScripts/processFile.mts +++ b/packages/__docs__/buildScripts/processFile.mts @@ -33,8 +33,6 @@ export function processFile( projectRoot: string, library: LibraryOptions ): ProcessedFile | undefined { - // eslint-disable-next-line no-console - console.info(`Processing ${fullPath}`) const source = fs.readFileSync(fullPath) const dirName = path.dirname(fullPath) || process.cwd() const pathInfo = getPathInfo(fullPath, projectRoot, library) @@ -49,13 +47,10 @@ export function processFile( let docId: string const lowerPath = docData.relativePath.toLowerCase() if (docData.id) { - // exist if it was in the description at the top + // exist if it was in the YAML description at the top docId = docData.id - } else if ( - lowerPath.includes(path.sep + 'index.js') || - lowerPath.includes(path.sep + 'index.tsx') - ) { - docId = path.basename(dirName) // return its folder name + } else if (lowerPath.includes(path.sep + 'index.tsx')) { + docId = docData.displayName! } else if (lowerPath.includes('readme.md')) { const folder = path.basename(dirName) docId = docData.describes ? folder + '__README' : folder diff --git a/packages/__docs__/buildScripts/utils/getClientProps.mts b/packages/__docs__/buildScripts/utils/getClientProps.mts index 6e79c05be7..e5ee817f80 100644 --- a/packages/__docs__/buildScripts/utils/getClientProps.mts +++ b/packages/__docs__/buildScripts/utils/getClientProps.mts @@ -63,7 +63,7 @@ export function getClientProps(docs: ProcessedFile[], library: LibraryOptions) { tags: doc.tags } docIDs[id] = doc.srcPath - if (describes) { + if (describes) { // TODO unused parsed.descriptions[describes] = doc.description } diff --git a/packages/__docs__/resolve.mjs b/packages/__docs__/resolve.mjs index 7461d047f5..c3a7f4e95f 100644 --- a/packages/__docs__/resolve.mjs +++ b/packages/__docs__/resolve.mjs @@ -31,32 +31,32 @@ const alias = { import.meta.dirname, '../ui-a11y-content/src/' ), - '@instructure/ui-alerts$': path.resolve(import.meta.dirname, '../ui-alerts/src/'), - '@instructure/ui-avatar$': path.resolve(import.meta.dirname, '../ui-avatar/src/'), - '@instructure/ui-badge$': path.resolve(import.meta.dirname, '../ui-badge/src/'), + '@instructure/ui-alerts$': path.resolve(import.meta.dirname, '../ui-alerts/src/exports/a'), + '@instructure/ui-avatar$': path.resolve(import.meta.dirname, '../ui-avatar/src/exports/a'), + '@instructure/ui-badge$': path.resolve(import.meta.dirname, '../ui-badge/src/exports/a'), '@instructure/ui-billboard$': path.resolve( import.meta.dirname, - '../ui-billboard/src/' + '../ui-billboard/src/exports/a' ), '@instructure/ui-breadcrumb$': path.resolve( import.meta.dirname, - '../ui-breadcrumb/src/' + '../ui-breadcrumb/src/exports/a' ), - '@instructure/ui-buttons$': path.resolve(import.meta.dirname, '../ui-buttons/src/'), - '@instructure/ui-byline$': path.resolve(import.meta.dirname, '../ui-byline/src/'), - '@instructure/ui-calendar$': path.resolve(import.meta.dirname, '../ui-calendar/src/'), - '@instructure/ui-checkbox$': path.resolve(import.meta.dirname, '../ui-checkbox/src/'), + '@instructure/ui-buttons$': path.resolve(import.meta.dirname, '../ui-buttons/src/exports/a'), + '@instructure/ui-byline$': path.resolve(import.meta.dirname, '../ui-byline/src/exports/a'), + '@instructure/ui-calendar$': path.resolve(import.meta.dirname, '../ui-calendar/src/exports/a'), + '@instructure/ui-checkbox$': path.resolve(import.meta.dirname, '../ui-checkbox/src/exports/a'), '@instructure/ui-color-picker$': path.resolve( import.meta.dirname, - '../ui-color-picker/src/' + '../ui-color-picker/src/exports/a' ), '@instructure/ui-date-input$': path.resolve( import.meta.dirname, - '../ui-date-input/src/' + '../ui-date-input/src/exports/a' ), '@instructure/ui-date-time-input$': path.resolve( import.meta.dirname, - '../ui-date-time-input/src/' + '../ui-date-time-input/src/exports/a' ), '@instructure/ui-decorator$': path.resolve(import.meta.dirname, '../ui-decorator/src/'), '@instructure/ui-dialog$': path.resolve(import.meta.dirname, '../ui-dialog/src/'), @@ -77,7 +77,7 @@ const alias = { import.meta.dirname, '../ui-expandable/src/' ), - '@instructure/ui-flex$': path.resolve(import.meta.dirname, '../ui-flex/src'), + '@instructure/ui-flex$': path.resolve(import.meta.dirname, '../ui-flex/src/exports/a'), '@instructure/ui-focusable$': path.resolve( import.meta.dirname, '../ui-focusable/src/' @@ -88,22 +88,22 @@ const alias = { ), '@instructure/ui-form-field$': path.resolve( import.meta.dirname, - '../ui-form-field/src/' + '../ui-form-field/src/exports/a' ), - '@instructure/ui-grid$': path.resolve(import.meta.dirname, '../ui-grid/src/'), + '@instructure/ui-grid$': path.resolve(import.meta.dirname, '../ui-grid/src/exports/a'), '@instructure/ui-i18n$': path.resolve(import.meta.dirname, '../ui-i18n/src/'), '@instructure/ui-icons$': path.resolve(import.meta.dirname, '../ui-icons/src/'), '@instructure/ui-img$': path.resolve(import.meta.dirname, '../ui-img/src/'), '@instructure/ui-instructure$': path.resolve(import.meta.dirname, '../ui-instructure/src/'), - '@instructure/ui-link$': path.resolve(import.meta.dirname, '../ui-link/src/'), - '@instructure/ui-list$': path.resolve(import.meta.dirname, '../ui-list/src/'), + '@instructure/ui-link$': path.resolve(import.meta.dirname, '../ui-link/src/exports/a'), + '@instructure/ui-list$': path.resolve(import.meta.dirname, '../ui-list/src/exports/a'), '@instructure/ui-menu$': path.resolve(import.meta.dirname, '../ui-menu/src/'), - '@instructure/ui-metric$': path.resolve(import.meta.dirname, '../ui-metric/src/'), + '@instructure/ui-metric$': path.resolve(import.meta.dirname, '../ui-metric/src/exports/a'), '@instructure/ui-modal$': path.resolve(import.meta.dirname, '../ui-modal/src/'), '@instructure/ui-motion$': path.resolve(import.meta.dirname, '../ui-motion/src/'), '@instructure/ui-navigation$': path.resolve( import.meta.dirname, - '../ui-navigation/src/' + '../ui-navigation/src/exports/a' ), '@instructure/ui-number-input$': path.resolve( import.meta.dirname, @@ -111,11 +111,11 @@ const alias = { ), '@instructure/ui-text-area$': path.resolve( import.meta.dirname, - '../ui-text-area/src/' + '../ui-text-area/src/exports/a' ), '@instructure/ui-text-input$': path.resolve( import.meta.dirname, - '../ui-text-input/src/' + '../ui-text-input/src/exports/a' ), '@instructure/ui-options$': path.resolve(import.meta.dirname, '../ui-options/src/'), '@instructure/ui-overlays$': path.resolve(import.meta.dirname, '../ui-overlays/src/'), @@ -124,18 +124,18 @@ const alias = { '../ui-pagination/src/' ), '@instructure/ui-pages$': path.resolve(import.meta.dirname, '../ui-pages/src/'), - '@instructure/ui-pill$': path.resolve(import.meta.dirname, '../ui-pill/src/'), - '@instructure/ui-popover$': path.resolve(import.meta.dirname, '../ui-popover/src/'), + '@instructure/ui-pill$': path.resolve(import.meta.dirname, '../ui-pill/src/exports/a/'), + '@instructure/ui-popover$': path.resolve(import.meta.dirname, '../ui-popover/src/exports/a'), '@instructure/ui-position$': path.resolve(import.meta.dirname, '../ui-position/src/'), '@instructure/ui-portal$': path.resolve(import.meta.dirname, '../ui-portal/src/'), - '@instructure/ui-progress$': path.resolve(import.meta.dirname, '../ui-progress/src'), + '@instructure/ui-progress$': path.resolve(import.meta.dirname, '../ui-progress/src/exports/a'), '@instructure/ui-radio-input$': path.resolve( import.meta.dirname, '../ui-radio-input/src/' ), '@instructure/ui-range-input$': path.resolve( import.meta.dirname, - '../ui-range-input/src/' + '../ui-range-input/src/exports/a' ), '@instructure/ui-rating$': path.resolve(import.meta.dirname, '../ui-rating/src/'), '@instructure/ui-responsive$': path.resolve( @@ -157,17 +157,17 @@ const alias = { ), '@instructure/ui-source-code-editor$': path.resolve( import.meta.dirname, - '../ui-source-code-editor/src/' + '../ui-source-code-editor/src/exports/a/' ), - '@instructure/ui-spinner$': path.resolve(import.meta.dirname, '../ui-spinner/src/'), + '@instructure/ui-spinner$': path.resolve(import.meta.dirname, '../ui-spinner/src/exports/a'), '@instructure/ui-svg-images$': path.resolve( import.meta.dirname, '../ui-svg-images/src/' ), - '@instructure/ui-table$': path.resolve(import.meta.dirname, '../ui-table/src/'), - '@instructure/ui-tabs$': path.resolve(import.meta.dirname, '../ui-tabs/src/'), - '@instructure/ui-tag$': path.resolve(import.meta.dirname, '../ui-tag/src/'), - '@instructure/ui-text$': path.resolve(import.meta.dirname, '../ui-text/src/'), + '@instructure/ui-table$': path.resolve(import.meta.dirname, '../ui-table/src//exports/a'), + '@instructure/ui-tabs$': path.resolve(import.meta.dirname, '../ui-tabs/src/exports/a'), + '@instructure/ui-tag$': path.resolve(import.meta.dirname, '../ui-tag/src/exports/a'), + '@instructure/ui-text$': path.resolve(import.meta.dirname, '../ui-text/src/exports/a'), '@instructure/ui-time-select$': path.resolve( import.meta.dirname, '../ui-time-select/src/' @@ -176,26 +176,26 @@ const alias = { import.meta.dirname, '../ui-toggle-details/src/' ), - '@instructure/ui-tooltip$': path.resolve(import.meta.dirname, '../ui-tooltip/src/'), + '@instructure/ui-tooltip$': path.resolve(import.meta.dirname, '../ui-tooltip/src/exports/a'), '@instructure/ui-top-nav-bar$': path.resolve( import.meta.dirname, - '../ui-top-nav-bar/src/' + '../ui-top-nav-bar/src/exports/a' ), - '@instructure/ui-tray$': path.resolve(import.meta.dirname, '../ui-tray/src/'), + '@instructure/ui-tray$': path.resolve(import.meta.dirname, '../ui-tray/src/exports/a'), '@instructure/ui-tree-browser$': path.resolve( import.meta.dirname, - '../ui-tree-browser/src/' + '../ui-tree-browser/src/exports/a' ), '@instructure/ui-truncate-list$': path.resolve( import.meta.dirname, - '../ui-truncate-list/src/' + '../ui-truncate-list/src' ), '@instructure/ui-truncate-text$': path.resolve( import.meta.dirname, '../ui-truncate-text/src/' ), '@instructure/ui-utils$': path.resolve(import.meta.dirname, '../ui-utils/src/'), - '@instructure/ui-view$': path.resolve(import.meta.dirname, '../ui-view/src/'), + '@instructure/ui-view$': path.resolve(import.meta.dirname, '../ui-view/src/exports/a'), '@instructure/canvas-theme$': path.resolve( import.meta.dirname, '../canvas-theme/src/' @@ -218,9 +218,9 @@ const alias = { ), '@instructure/ui-file-drop$': path.resolve( import.meta.dirname, - '../ui-file-drop/src/' + '../ui-file-drop/src/exports/a' ), - '@instructure/ui-heading$': path.resolve(import.meta.dirname, '../ui-heading/src/'), + '@instructure/ui-heading$': path.resolve(import.meta.dirname, '../ui-heading/src/exports/a'), '@instructure/emotion$': path.resolve(import.meta.dirname, '../emotion/src/') } diff --git a/packages/__docs__/src/Nav/index.tsx b/packages/__docs__/src/Nav/index.tsx index 3b3d9180f7..6084d2afc8 100644 --- a/packages/__docs__/src/Nav/index.tsx +++ b/packages/__docs__/src/Nav/index.tsx @@ -35,6 +35,7 @@ import { capitalizeFirstLetter } from '@instructure/ui-utils' import { NavToggle } from '../NavToggle' import type { NavProps, NavState } from './props' import { Alert } from '@instructure/ui-alerts' +import type { Section } from '../../buildScripts/DataTypes.mjs' class Nav extends Component { _themeId: string @@ -63,9 +64,9 @@ class Nav extends Component { setExpandedSections = ( expanded: boolean, - sections: NavProps['sections'] - ): NavProps['sections'] => { - const expandedSections: NavProps['sections'] = {} + sections: Record + ) => { + const expandedSections: Record = {} Object.keys(sections).forEach((sectionId) => { expandedSections[sectionId] = expanded }) @@ -154,7 +155,9 @@ class Nav extends Component { matchQuery(str: string): boolean { const { query } = this.state - return query && typeof query.test === 'function' ? query.test(str) : true + return query && typeof (query as RegExp).test === 'function' + ? (query as RegExp).test(str) + : true } createNavToggle({ @@ -396,7 +399,7 @@ class Nav extends Component { } else { return this.createNavToggle({ id: sectionId, - title: this.props.sections[sectionId].title, + title: this.props.sections[sectionId].title || '[no title]', children: this.renderSectionChildren(sectionId, markExpanded), variant }) diff --git a/packages/__docs__/src/Nav/props.ts b/packages/__docs__/src/Nav/props.ts index ec86194cc2..addedfe883 100644 --- a/packages/__docs__/src/Nav/props.ts +++ b/packages/__docs__/src/Nav/props.ts @@ -23,12 +23,12 @@ */ import type { OtherHTMLAttributes } from '@instructure/shared-types' +import type { MainDocsData } from '../../buildScripts/DataTypes.mjs' type NavOwnProps = { - docs: Record - sections: Record - themes?: Record - icons?: Record | null + docs: MainDocsData['docs'] + sections: MainDocsData['sections'] + themes?: MainDocsData['themes'] selected?: string } type PropKeys = keyof NavOwnProps @@ -37,16 +37,10 @@ type AllowedPropKeys = Readonly> type NavProps = OtherHTMLAttributes & NavOwnProps -const allowedProps: AllowedPropKeys = [ - 'docs', - 'icons', - 'sections', - 'selected', - 'themes' -] +const allowedProps: AllowedPropKeys = ['docs', 'sections', 'selected', 'themes'] type NavState = { - query: any - expandedSections: Record + query: string | RegExp | null + expandedSections: Record userToggling: boolean queryStr?: string announcement: string | null diff --git a/packages/__docs__/src/withStyleForDocs.tsx b/packages/__docs__/src/withStyleForDocs.tsx index ac9715cf3e..17115ed226 100644 --- a/packages/__docs__/src/withStyleForDocs.tsx +++ b/packages/__docs__/src/withStyleForDocs.tsx @@ -114,7 +114,7 @@ const defaultValues = { * category: utilities/themes * --- * - * Same as `withStyleRework`, used only for the docs app. + * Same as `withStyleLegacy`, used only for the docs app. * * A decorator or higher order component that makes a component themeable. * diff --git a/packages/__docs__/tsconfig.node.build.json b/packages/__docs__/tsconfig.node.build.json index 9450054b40..c7ff563ba4 100644 --- a/packages/__docs__/tsconfig.node.build.json +++ b/packages/__docs__/tsconfig.node.build.json @@ -2,8 +2,8 @@ "extends": "../../tsconfig.node.json", "compilerOptions": { "declarationDir": "./types", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "Node16", + "moduleResolution": "Node16", "esModuleInterop": true, "outDir": "./lib", "resolveJsonModule": true diff --git a/packages/emotion/src/__tests__/useStyleRework.test.tsx b/packages/emotion/src/__tests__/useStyleRework.test.tsx index fbeae0bd06..8e9af90d37 100644 --- a/packages/emotion/src/__tests__/useStyleRework.test.tsx +++ b/packages/emotion/src/__tests__/useStyleRework.test.tsx @@ -31,7 +31,7 @@ import '@testing-library/jest-dom' import { InstUISettingsProvider, WithStyleProps, - useStyleRework + useStyleLegacy } from '../index' type Props = { @@ -106,7 +106,7 @@ describe('useStyle', () => { const ThemedComponent = ({ inverse = false, themeOverride }: Props) => { const [clearBackground, setClearBackground] = useState(false) - const styles = useStyleRework({ + const styles = useStyleLegacy({ generateStyle, generateComponentTheme, componentId: 'ThemedComponent', diff --git a/packages/emotion/src/__tests__/withStyle.test.tsx b/packages/emotion/src/__tests__/withStyle.test.tsx index 16df14408c..7037bad127 100644 --- a/packages/emotion/src/__tests__/withStyle.test.tsx +++ b/packages/emotion/src/__tests__/withStyle.test.tsx @@ -30,7 +30,7 @@ import userEvent from '@testing-library/user-event' import '@testing-library/jest-dom' import { - withStyleRework as withStyle, + withStyleLegacy as withStyle, InstUISettingsProvider, WithStyleProps } from '../index' diff --git a/packages/emotion/src/index.ts b/packages/emotion/src/index.ts index 14de7d1b58..52a912f135 100644 --- a/packages/emotion/src/index.ts +++ b/packages/emotion/src/index.ts @@ -26,7 +26,7 @@ export * from '@emotion/react' export { InstUISettingsProvider } from './InstUISettingsProvider' -export { withStyleRework } from './withStyleRework' +export { withStyleLegacy } from './withStyleLegacy' export { getComponentThemeOverride } from './getComponentThemeOverride' export { withStyle } from './withStyle' export { @@ -39,12 +39,12 @@ export { calcFocusOutlineStyles } from './styleUtils' -export { useStyleRework } from './useStyleRework' +export { useStyleLegacy } from './useStyleLegacy' export { useStyle } from './useStyle' export { useTheme } from './useTheme' export type { ComponentStyle, StyleObject, Overrides } from './EmotionTypes' -export type { WithStyleProps } from './withStyleRework' +export type { WithStyleProps } from './withStyleLegacy' export type { ThemeOverrideValue } from './useStyle' export type { SpacingValues, diff --git a/packages/emotion/src/useStyleRework.ts b/packages/emotion/src/useStyleLegacy.ts similarity index 97% rename from packages/emotion/src/useStyleRework.ts rename to packages/emotion/src/useStyleLegacy.ts index 4f99574174..15ca142785 100644 --- a/packages/emotion/src/useStyleRework.ts +++ b/packages/emotion/src/useStyleLegacy.ts @@ -59,7 +59,7 @@ type UseStyleParamsWithoutTheme< * This is only used by the **old themes**, remove when everything uses the new * theming system (InstUI v12) */ -const useStyleRework = < +const useStyleLegacy = < P extends (componentTheme: any, params: any, theme: any) => any >( useStyleParams: UseStyleParamsWithTheme

| UseStyleParamsWithoutTheme

@@ -88,5 +88,5 @@ const useStyleRework = < return generateStyle(componentTheme, params ? params : {}, theme) } -export default useStyleRework -export { useStyleRework } +export default useStyleLegacy +export { useStyleLegacy } diff --git a/packages/emotion/src/withStyleRework.tsx b/packages/emotion/src/withStyleLegacy.tsx similarity index 98% rename from packages/emotion/src/withStyleRework.tsx rename to packages/emotion/src/withStyleLegacy.tsx index 354beb0cc1..2c05861c0a 100644 --- a/packages/emotion/src/withStyleRework.tsx +++ b/packages/emotion/src/withStyleLegacy.tsx @@ -95,7 +95,7 @@ const defaultValues = { * used for old (v11 and eariler) theming system * TODO delete when the theme migration is complete */ -const withStyleRework = decorator( +const withStyleLegacy = decorator( ( ComposedComponent: WithStyleComponent, generateStyle: GenerateStyle, @@ -207,6 +207,6 @@ const withStyleRework = decorator( } ) -export default withStyleRework -export { withStyleRework } +export default withStyleLegacy +export { withStyleLegacy } export type { WithStyleProps } diff --git a/packages/ui-a11y-content/src/ScreenReaderContent/index.tsx b/packages/ui-a11y-content/src/ScreenReaderContent/index.tsx index 52a187ab72..f8b58589ed 100644 --- a/packages/ui-a11y-content/src/ScreenReaderContent/index.tsx +++ b/packages/ui-a11y-content/src/ScreenReaderContent/index.tsx @@ -26,7 +26,7 @@ import { Component } from 'react' import { passthroughProps, getElementType } from '@instructure/ui-react-utils' -import { withStyleRework as withStyle } from '@instructure/emotion' +import { withStyleLegacy as withStyle } from '@instructure/emotion' import generateStyle from './styles' diff --git a/packages/ui-alerts/package.json b/packages/ui-alerts/package.json index a9254451f5..584d03d401 100644 --- a/packages/ui-alerts/package.json +++ b/packages/ui-alerts/package.json @@ -56,10 +56,28 @@ "sideEffects": false, "exports": { ".": { - "types": "./types/index.d.ts", - "import": "./es/index.js", - "require": "./lib/index.js", - "default": "./es/index.js" + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_5": { + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_6": { + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./latest": { + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" }, "./lib/*": "./lib/*", "./es/*": "./es/*", diff --git a/packages/ui-alerts/src/Alert/README.md b/packages/ui-alerts/src/Alert/v1/README.md similarity index 100% rename from packages/ui-alerts/src/Alert/README.md rename to packages/ui-alerts/src/Alert/v1/README.md diff --git a/packages/ui-alerts/src/Alert/__tests__/Alert.test.tsx b/packages/ui-alerts/src/Alert/v1/__tests__/Alert.test.tsx similarity index 100% rename from packages/ui-alerts/src/Alert/__tests__/Alert.test.tsx rename to packages/ui-alerts/src/Alert/v1/__tests__/Alert.test.tsx diff --git a/packages/ui-alerts/src/Alert/index.tsx b/packages/ui-alerts/src/Alert/v1/index.tsx similarity index 99% rename from packages/ui-alerts/src/Alert/index.tsx rename to packages/ui-alerts/src/Alert/v1/index.tsx index 23a101354c..6fbe24c068 100644 --- a/packages/ui-alerts/src/Alert/index.tsx +++ b/packages/ui-alerts/src/Alert/v1/index.tsx @@ -43,7 +43,7 @@ import { } from '@instructure/ui-icons' import { Transition } from '@instructure/ui-motion' import { logError as error } from '@instructure/console' -import { withStyleRework as withStyle } from '@instructure/emotion' +import { withStyleLegacy as withStyle } from '@instructure/emotion' import generateStyle from './styles' import generateComponentTheme from './theme' diff --git a/packages/ui-alerts/src/Alert/props.ts b/packages/ui-alerts/src/Alert/v1/props.ts similarity index 100% rename from packages/ui-alerts/src/Alert/props.ts rename to packages/ui-alerts/src/Alert/v1/props.ts diff --git a/packages/ui-alerts/src/Alert/styles.ts b/packages/ui-alerts/src/Alert/v1/styles.ts similarity index 100% rename from packages/ui-alerts/src/Alert/styles.ts rename to packages/ui-alerts/src/Alert/v1/styles.ts diff --git a/packages/ui-alerts/src/Alert/theme.ts b/packages/ui-alerts/src/Alert/v1/theme.ts similarity index 100% rename from packages/ui-alerts/src/Alert/theme.ts rename to packages/ui-alerts/src/Alert/v1/theme.ts diff --git a/packages/ui-alerts/src/exports/a.ts b/packages/ui-alerts/src/exports/a.ts new file mode 100644 index 0000000000..85624be531 --- /dev/null +++ b/packages/ui-alerts/src/exports/a.ts @@ -0,0 +1,25 @@ +/* + * 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 { Alert } from '../Alert/v1' +export type { AlertProps } from '../Alert/v1/props' diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index c7e20e35ab..96f96b118b 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -48,10 +48,28 @@ "sideEffects": false, "exports": { ".": { - "types": "./types/index.d.ts", - "import": "./es/index.js", - "require": "./lib/index.js", - "default": "./es/index.js" + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_5": { + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_6": { + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" + }, + "./latest": { + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" }, "./lib/*": "./lib/*", "./es/*": "./es/*", diff --git a/packages/ui-avatar/src/Avatar/v1/README.md b/packages/ui-avatar/src/Avatar/v1/README.md new file mode 100644 index 0000000000..4039632155 --- /dev/null +++ b/packages/ui-avatar/src/Avatar/v1/README.md @@ -0,0 +1,193 @@ +--- +describes: Avatar +--- + +The avatar component can be used to display a user's avatar. When an image src is not supplied the user's initials will display. + +Instead of the initials, an SVG icon can be displayed with the `renderIcon` property. + +The avatar can be `circle` _(default)_ or `rectangle`. Use the `margin` prop to add space between Avatar and other content. + +```js +--- +type: example +readonly: true +--- +

+ + + } margin="0 space8 0 0" /> + + + } shape="rectangle" /> +
+``` + +### AI Avatar + +There is a need for special, `ai avatars`. These have a specific look. You can achieve it the following way + +```js +--- +type: example +readonly: true +--- + + + + + + + + + +``` + +### Size + +The `size` prop allows you to select from `xx-small`, `x-small`, `small`, `medium`, `large`, `x-large`, and `xx-large`. If the `auto` prop is set, the avatar size will adjust according to the font-size +of its container. + +```js +--- +type: example +--- +
+ + + + + + + + + + + + + + + + + + + + } size="xx-small" margin="0 space8 0 0" /> + } size="x-small" margin="0 space8 0 0" /> + } size="small" margin="0 space8 0 0" /> + } size="medium" margin="0 space8 0 0" /> + } size="large" margin="0 space8 0 0" /> + } size="x-large" margin="0 space8 0 0" /> + } size="xx-large" /> + +
+``` + +### Colors + +The color of the initials and icons can be set with the `color` prop, and it allows you to select from `default`, `shamrock`, `barney`, `crimson`, `fire`, `licorice` and `ash`. + +```js +--- +type: example +--- +
+ + + + + + + + + + + } name="Arthur C. Clarke" margin="0 space8 0 0" /> + } name="James Arias" color="shamrock" margin="0 space8 0 0" /> + } name="Charles Kimball" color="barney" margin="0 space8 0 0" /> + } name="Melissa Reed" color="crimson" margin="0 space8 0 0" /> + } name="Heather Wheeler" color="fire" margin="0 space8 0 0" /> + } name="David Herbert" color="licorice" margin="0 space8 0 0" /> + } name="Isaac Asimov" color="ash" /> + +
+``` + +The `hasInverseColor` prop inverts the background color and the text/icon color. + +Inverted Avatars have **no border**. + +```js +--- +type: example +--- +
+ + + + + + + + + + + } name="Arthur C. Clarke" hasInverseColor margin="0 space8 0 0" /> + } name="James Arias" color="shamrock" hasInverseColor margin="0 space8 0 0" /> + } name="Charles Kimball" color="barney" hasInverseColor margin="0 space8 0 0" /> + } name="Melissa Reed" color="crimson" hasInverseColor margin="0 space8 0 0" /> + } name="Heather Wheeler" color="fire" hasInverseColor margin="0 space8 0 0" /> + } name="David Herbert" color="licorice" hasInverseColor margin="0 space8 0 0" /> + } name="Isaac Asimov" color="ash" hasInverseColor /> + +
+``` + +In case you need more control over the color, feel free to use the `themeOverride` prop, and override the default theme variables. + +```js +--- +type: example +--- +
+ } themeOverride={{ color: '#efb410' }} margin="0 space8 0 0" /> + + } hasInverseColor themeOverride={{ color: 'lightblue', background: 'black' }} margin="0 space8 0 0" /> + +
+``` + +### Border + +By default only avatars without an image have borders but you can force it to `always` or `never` show with the `showBorder` prop however you should only use it rarely in very specific occasions (e.g. displaying an avatar in the [SideNavBar](/#SideNavBar)) + +```js +--- +type: example +--- +
+ + } margin="0 space8 0 0" showBorder="never"/> +
+``` + +### Accessibility + +Avatars use the `aria-hidden="true"` property and therefore are hidden from screenreaders. Make sure if you are using them stand-alone it's accompanied with [ScreenReaderContent](ScreenReaderContent). + +### Guidelines + +```js +--- +type: embed +--- + +
+ Ensure the appropriate size is being used for its placement (in a table, stand-alone, etc…) + Use circle variant in Canvas +
+
+ Use inline in sentence +
+
+``` diff --git a/packages/ui-avatar/src/Avatar/v1/index.tsx b/packages/ui-avatar/src/Avatar/v1/index.tsx new file mode 100644 index 0000000000..66c0648af7 --- /dev/null +++ b/packages/ui-avatar/src/Avatar/v1/index.tsx @@ -0,0 +1,184 @@ +/* + * 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 { useStyleLegacy as useStyle } from '@instructure/emotion' +import { + useState, + SyntheticEvent, + useEffect, + forwardRef, + ForwardedRef, + useRef +} from 'react' + +import { View } from '@instructure/ui-view' +import { callRenderProp, passthroughProps } from '@instructure/ui-react-utils' +import type { AvatarProps } from './props' + +import generateStyle from './styles' +import generateComponentTheme from './theme' + +/** +--- +category: components +--- +**/ +const Avatar = forwardRef( + ( + { + size = 'medium', + color = 'default', + hasInverseColor = false, + showBorder = 'auto', + shape = 'circle', + display = 'inline-block', + onImageLoaded, + src, + name, + renderIcon, + alt, + as, + margin, + themeOverride, + elementRef, + ...rest + }: AvatarProps, + ref: ForwardedRef + ) => { + const imgRef = useRef(null) + const [loaded, setLoaded] = useState(false) + + const styles = useStyle({ + generateStyle, + generateComponentTheme, + params: { + loaded, + size, + color, + hasInverseColor, + shape, + src, + showBorder, + themeOverride + }, + componentId: 'Avatar', + displayName: 'Avatar' + }) + + useEffect(() => { + // in case the image is unset in an update, show icons/initials again + if (loaded && !src) { + setLoaded(false) + } + // Image already loaded (common in SSR) + if (src && !loaded && imgRef.current && imgRef.current.complete) { + setLoaded(true) + onImageLoaded?.() + } + }, [loaded, src]) + + const makeInitialsFromName = () => { + if (!name || typeof name !== 'string') { + return + } + const currentName = name.trim() + if (currentName.length === 0) { + return + } + + if (currentName.match(/\s+/)) { + const names = currentName.split(/\s+/) + return (names[0][0] + names[names.length - 1][0]).toUpperCase() + } else { + return currentName[0].toUpperCase() + } + } + + const handleImageLoaded = (event: SyntheticEvent) => { + setLoaded(true) + onImageLoaded?.(event) + } + + const renderInitials = () => { + return ( + + ) + } + + const renderContent = () => { + if (!renderIcon) { + return renderInitials() + } + + return {callRenderProp(renderIcon)} + } + + return ( + + + {!loaded && renderContent()} + + ) + } +) +Avatar.displayName = 'Avatar' + +// TODO - why is this needed? +Avatar.displayName = 'Avatar' + +export default Avatar +export { Avatar } diff --git a/packages/ui-avatar/src/Avatar/v1/props.ts b/packages/ui-avatar/src/Avatar/v1/props.ts new file mode 100644 index 0000000000..d76ca80062 --- /dev/null +++ b/packages/ui-avatar/src/Avatar/v1/props.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 { SyntheticEvent } from 'react' + +import type { + Spacing, + WithStyleProps, + ComponentStyle +} from '@instructure/emotion' +import type { + AsElementType, + AvatarTheme, + OtherHTMLAttributes +} from '@instructure/shared-types' +import { Renderable } from '@instructure/shared-types' + +type AvatarOwnProps = { + /** + * The name to display. It will be automatically converted to initials. + */ + name: string + /** + * URL of the image to display as the background image + */ + src?: string + /** + * Accessible label + */ + alt?: string + size?: + | 'auto' + | 'xx-small' + | 'x-small' + | 'small' + | 'medium' + | 'large' + | 'x-large' + | 'xx-large' + color?: + | 'default' // = brand + | 'shamrock' + | 'barney' + | 'crimson' + | 'fire' + | 'licorice' + | 'ash' + | 'ai' + /** + * In inverse color mode the background and text/icon colors are inverted + */ + hasInverseColor?: boolean + /** + * `auto` only shows a border when there is no source image. This prop can force to always or never show that border. + */ + showBorder?: 'auto' | 'always' | 'never' + shape?: 'circle' | 'rectangle' + display?: 'inline-block' | 'block' + /** + * Valid values are `0`, `none`, `auto`, `xxx-small`, `xx-small`, `x-small`, + * `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via + * familiar CSS-like shorthand. For example: `margin="small auto large"`. + */ + margin?: Spacing + /** + * Callback fired when the avatar image has loaded. + * `event` can be `undefined`, if its already loaded when the page renders + * (can happen in SSR) + */ + onImageLoaded?: (event?: SyntheticEvent) => void + /** + * The element type to render as + */ + as?: AsElementType + /** + * provides a reference to the underlying html root element + */ + elementRef?: (element: Element | null) => void + /** + * An icon, or function that returns an icon that gets displayed. If the `src` prop is provided, `src` will have priority. + */ + renderIcon?: Renderable +} + +export type AvatarState = { + loaded: boolean +} + +type PropKeys = keyof AvatarOwnProps + +type AllowedPropKeys = Readonly> + +type AvatarProps = AvatarOwnProps & + WithStyleProps & + OtherHTMLAttributes + +type AvatarStyle = ComponentStyle< + 'avatar' | 'initials' | 'loadImage' | 'iconSVG' +> +const allowedProps: AllowedPropKeys = [ + 'name', + 'src', + 'alt', + 'size', + 'color', + 'hasInverseColor', + 'shape', + 'margin', + 'display', + 'onImageLoaded', + 'as', + 'elementRef', + 'renderIcon', + 'showBorder' +] + +export type { AvatarProps, AvatarStyle } +export { allowedProps } diff --git a/packages/ui-avatar/src/Avatar/v1/styles.ts b/packages/ui-avatar/src/Avatar/v1/styles.ts new file mode 100644 index 0000000000..277786006f --- /dev/null +++ b/packages/ui-avatar/src/Avatar/v1/styles.ts @@ -0,0 +1,260 @@ +/* + * 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 { AvatarTheme } from '@instructure/shared-types' +import { AvatarProps, AvatarStyle } from './props' + +type StyleParams = { + loaded: boolean + size: AvatarProps['size'] + color: AvatarProps['color'] + hasInverseColor: AvatarProps['hasInverseColor'] + shape: AvatarProps['shape'] + src: AvatarProps['src'] + showBorder: AvatarProps['showBorder'] + themeOverride: AvatarProps['themeOverride'] +} +/** + * --- + * 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. + * @return The final style object, which will be used in the component + */ +const generateStyle = ( + componentTheme: AvatarTheme, + params: StyleParams +): AvatarStyle => { + const { loaded, size, color, hasInverseColor, shape, src, showBorder } = + params + + // TODO: this is a temporary solution and should be revised on component update + // NOTE: this is needed due to design changes. The size of the component is calculated from "em" which means it is + // tied to the fontSize. The font sizes changed for the icons, which meant that the container (component) size would've + // changed too without additional calculations + const calcNewScaler = ( + originalFontSize: number, + newFontSize: number, + originalScaler: number + ) => { + return `${(originalFontSize * originalScaler) / newFontSize}em` + } + + const sizeStyles = { + auto: { + fontSize: 'inherit', + borderWidth: componentTheme.borderWidthSmall, + width: '2.5em', + height: '2.5em' + }, + 'xx-small': { + fontSize: '0.625rem', + borderWidth: componentTheme.borderWidthSmall, + width: calcNewScaler(0.5, 0.625, shape === 'circle' ? 2.5 : 3), + height: calcNewScaler(0.5, 0.625, 2.5) + }, + 'x-small': { + fontSize: '0.875rem', + borderWidth: componentTheme.borderWidthSmall, + width: calcNewScaler(0.75, 0.875, shape === 'circle' ? 2.5 : 3), + height: calcNewScaler(0.75, 0.875, 2.5) + }, + small: { + fontSize: '1.25rem', + borderWidth: componentTheme.borderWidthSmall, + width: calcNewScaler(1, 1.25, shape === 'circle' ? 2.5 : 3), + height: calcNewScaler(1, 1.25, 2.5) + }, + medium: { + fontSize: '1.5rem', + borderWidth: componentTheme.borderWidthMedium, + width: calcNewScaler(1.25, 1.5, shape === 'circle' ? 2.5 : 3), + height: calcNewScaler(1.25, 1.5, 2.5) + }, + large: { + fontSize: '1.75rem', + borderWidth: componentTheme.borderWidthMedium, + width: calcNewScaler(1.5, 1.75, shape === 'circle' ? 2.5 : 3), + height: calcNewScaler(1.5, 1.75, 2.5) + }, + 'x-large': { + fontSize: '2rem', + borderWidth: componentTheme.borderWidthMedium, + width: calcNewScaler(1.75, 2, shape === 'circle' ? 2.5 : 3), + height: calcNewScaler(1.75, 2, 2.5) + }, + 'xx-large': { + fontSize: '2.25rem', + borderWidth: componentTheme.borderWidthMedium, + width: calcNewScaler(2, 2.25, shape === 'circle' ? 2.5 : 3), + height: calcNewScaler(2, 2.25, 2.5) + } + } + + const initialSizeStyles = { + auto: { + fontSize: 'inherit' + }, + 'xx-small': { + fontSize: '0.5rem' + }, + 'x-small': { + fontSize: '0.75rem' + }, + small: { + fontSize: '1rem' + }, + medium: { + fontSize: '1.25rem' + }, + large: { + fontSize: '1.5rem' + }, + 'x-large': { + fontSize: '1.75rem' + }, + 'xx-large': { + fontSize: '2rem' + } + } + + const shapeStyles = { + circle: { + position: 'relative', + borderRadius: '100%', + overflow: 'hidden' + }, + rectangle: { + width: '3em' + } + } + + const colorVariants = { + default: componentTheme.color, // = brand + shamrock: componentTheme.colorShamrock, + barney: componentTheme.colorBarney, + crimson: componentTheme.colorCrimson, + fire: componentTheme.colorFire, + licorice: componentTheme.colorLicorice, + ash: componentTheme.colorAsh, + ai: ` + linear-gradient(to bottom, ${componentTheme.aiTopGradientColor} 0%, ${componentTheme.aiBottomGradientColor} 100%) padding-box, + linear-gradient(to bottom right, ${componentTheme.aiTopGradientColor} 0%, ${componentTheme.aiBottomGradientColor} 100%) border-box` + } + + const background = () => { + if (color === 'ai') { + return { + background: ` + linear-gradient(to bottom, ${componentTheme.aiTopGradientColor} 0%, ${componentTheme.aiBottomGradientColor} 100%) padding-box, + linear-gradient(to bottom right, ${componentTheme.aiTopGradientColor} 0%, ${componentTheme.aiBottomGradientColor} 100%) border-box`, + border: 'solid transparent' + } + } + return hasInverseColor + ? { + backgroundColor: colorVariants[color!], + backgroundClip: 'content-box' + } + : { + backgroundColor: componentTheme.background, + backgroundClip: 'content-box' + } + } + + const contentColor = () => { + if (color === 'ai') { + return componentTheme.aiFontColor + } + return hasInverseColor ? componentTheme.background : colorVariants[color!] + } + + return { + avatar: { + label: 'avatar', + boxSizing: 'border-box', + borderStyle: 'solid', + borderColor: componentTheme.borderColor, + ...background(), + backgroundPosition: 'center', + backgroundSize: 'cover', + backgroundRepeat: 'no-repeat', + overflow: 'hidden', + lineHeight: 0, + textAlign: 'center', + ...sizeStyles[size!], + ...shapeStyles[shape!], + ...(loaded + ? { + backgroundImage: `url('${src}')`, + ...(showBorder !== 'always' && { + border: 0 + }), + boxShadow: `inset 0 0 ${componentTheme.boxShadowBlur} 0 ${componentTheme.boxShadowColor}` + } + : { + backgroundImage: undefined, + ...(hasInverseColor && { + border: 0, + padding: sizeStyles[size!].borderWidth, + backgroundClip: 'border-box' + }) + }), + ...(showBorder === 'never' && { + border: 0 + }) + }, + initials: { + label: 'avatar__initials', + color: contentColor(), + lineHeight: '2.375em', + fontFamily: componentTheme.fontFamily, + fontWeight: componentTheme.fontWeight, + letterSpacing: '0.0313em', + ...initialSizeStyles[size!] + }, + loadImage: { + label: 'avatar__loadImage', + display: 'none' + }, + iconSVG: { + label: 'avatar__iconSVG', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + width: '100%', + + svg: { + fill: contentColor(), + height: '1em', + width: '1em' + } + } + } +} + +export default generateStyle diff --git a/packages/ui-avatar/src/Avatar/v1/theme.ts b/packages/ui-avatar/src/Avatar/v1/theme.ts new file mode 100644 index 0000000000..54a55ac9af --- /dev/null +++ b/packages/ui-avatar/src/Avatar/v1/theme.ts @@ -0,0 +1,67 @@ +/* + * 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 { alpha } from '@instructure/ui-color-utils' +import type { Theme } from '@instructure/ui-themes' +import { AvatarTheme } from '@instructure/shared-types' + +/** + * Generates the theme object for the component from the theme and provided additional information + * @param theme The current theme object. + * @return The final theme object with the overrides and component variables + */ +const generateComponentTheme = (theme: Theme): AvatarTheme => { + const { colors, borders, typography } = theme + + const componentVariables: AvatarTheme = { + background: colors?.contrasts?.white1010, + borderWidthSmall: borders?.widthSmall, + borderWidthMedium: borders?.widthMedium, + borderColor: colors?.contrasts?.grey3045, + boxShadowColor: alpha('#2d3b45', 12), + boxShadowBlur: '1rem', + fontFamily: typography?.fontFamily, + fontWeight: typography?.fontWeightBold, + + // these colors have sufficient contrast with the white background + // in the normal and high contrast themes + color: colors?.contrasts?.blue4570, + colorShamrock: colors?.contrasts?.green4570, + colorBarney: colors?.contrasts?.blue4570, + colorCrimson: colors?.contrasts?.red4570, + colorFire: colors?.contrasts?.orange4570, + colorLicorice: colors?.contrasts?.grey125125, + colorAsh: colors?.contrasts?.grey4570, + + aiTopGradientColor: colors?.contrasts?.violet4570, + aiBottomGradientColor: colors?.contrasts?.sea4570, + aiFontColor: colors?.contrasts?.white1010 + } + + return { + ...componentVariables + } +} + +export default generateComponentTheme diff --git a/packages/ui-avatar/src/Avatar/README.md b/packages/ui-avatar/src/Avatar/v2/README.md similarity index 100% rename from packages/ui-avatar/src/Avatar/README.md rename to packages/ui-avatar/src/Avatar/v2/README.md diff --git a/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx b/packages/ui-avatar/src/Avatar/v2/__tests__/Avatar.test.tsx similarity index 100% rename from packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx rename to packages/ui-avatar/src/Avatar/v2/__tests__/Avatar.test.tsx diff --git a/packages/ui-avatar/src/Avatar/index.tsx b/packages/ui-avatar/src/Avatar/v2/index.tsx similarity index 100% rename from packages/ui-avatar/src/Avatar/index.tsx rename to packages/ui-avatar/src/Avatar/v2/index.tsx diff --git a/packages/ui-avatar/src/Avatar/props.ts b/packages/ui-avatar/src/Avatar/v2/props.ts similarity index 100% rename from packages/ui-avatar/src/Avatar/props.ts rename to packages/ui-avatar/src/Avatar/v2/props.ts diff --git a/packages/ui-avatar/src/Avatar/styles.ts b/packages/ui-avatar/src/Avatar/v2/styles.ts similarity index 100% rename from packages/ui-avatar/src/Avatar/styles.ts rename to packages/ui-avatar/src/Avatar/v2/styles.ts diff --git a/packages/ui-avatar/src/exports/a.ts b/packages/ui-avatar/src/exports/a.ts new file mode 100644 index 0000000000..e8e2f9592f --- /dev/null +++ b/packages/ui-avatar/src/exports/a.ts @@ -0,0 +1,25 @@ +/* + * 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 { Avatar } from '../Avatar/v1' +export type { AvatarProps } from '../Avatar/v1/props' diff --git a/packages/ui-avatar/src/exports/b.ts b/packages/ui-avatar/src/exports/b.ts new file mode 100644 index 0000000000..00cdb98008 --- /dev/null +++ b/packages/ui-avatar/src/exports/b.ts @@ -0,0 +1,25 @@ +/* + * 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 { Avatar } from '../Avatar/v2' +export type { AvatarProps } from '../Avatar/v2/props' diff --git a/packages/ui-badge/package.json b/packages/ui-badge/package.json index 979579b258..6bbe9e981d 100644 --- a/packages/ui-badge/package.json +++ b/packages/ui-badge/package.json @@ -48,10 +48,28 @@ "sideEffects": false, "exports": { ".": { - "types": "./types/index.d.ts", - "import": "./es/index.js", - "require": "./lib/index.js", - "default": "./es/index.js" + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_5": { + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_6": { + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" + }, + "./latest": { + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" }, "./lib/*": "./lib/*", "./es/*": "./es/*", diff --git a/packages/ui-badge/src/Badge/v1/README.md b/packages/ui-badge/src/Badge/v1/README.md new file mode 100644 index 0000000000..00f0da42c3 --- /dev/null +++ b/packages/ui-badge/src/Badge/v1/README.md @@ -0,0 +1,277 @@ +--- +describes: Badge +--- + +### Making badges accessible + +Badge counts are automatically fed to screenreaders through the `aria-describedby` +attribute. Often a stand alone number doesn't give a screenreader user enough context (_"3" vs. "You have 3 unread emails"_). +The examples below use the `formatOutput` prop to make the badge more screenreader-friendly. + +```js +--- +type: example +--- +
+ + {formattedCount} + + ) + }} + > + + + You have new edits to review + }} + > + + +
+``` + +> Note the use of the `pulse` prop in the first example to make the Badge slowly pulse twice on mount. + +### Limit the count + +Use the `countUntil` prop to set a limit for the count. The default for `formatOverflowText` is a "+" symbol. + +```js +--- +type: example +--- +
+ + + + + + +
+``` + +### Standalone, notification and color variants + +Setting the `standalone` prop to `true` renders Badge as a standalone +element that is not positioned relative to a child and displays inline-block. +Setting `type="notification"` will render small circles that should not contain any visible text. + +```js +--- +type: example +--- +
+ + + This is a notification + }} + /> + + + + This is a success notification + }} + /> + + + + This is a danger notification + }} + /> + + + + + This is a danger notification + }} + /> + + +
+``` + +### Placement + +Default is `top end`. **Note that standalone badges can't be placed.** + +```js +--- +type: example +--- +const EditButton = () => ( + +) + +const Example = () => ( +
+ + + + + + + + + + + + + + + + + + + + + + + Updates are available for your account + + ) + }} + > + + + + Updates are available for your account + + ) + }} + > + + + + Updates are available for your account + + ) + }} + > + + + + Updates are available for your account + + ) + }} + > + + + + Updates are available for your account + + ) + }} + > + + + + Updates are available for your account + + ) + }} + > + + + +
+) + +render() +``` + +### Guidelines + +```js +--- +type: embed +--- + +
+ Use count for up to 2 digits of numbers + Use "+" symbol for more than 2 digits (99+) + Use for numeric count (like unread messages) + Provide accessible text for what the number represents +
+
+ Use as a status indicator refer to Pill + Use for text strings +
+
+``` diff --git a/packages/ui-badge/src/Badge/v1/index.tsx b/packages/ui-badge/src/Badge/v1/index.tsx new file mode 100644 index 0000000000..cc245ed661 --- /dev/null +++ b/packages/ui-badge/src/Badge/v1/index.tsx @@ -0,0 +1,171 @@ +/* + * 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 { Component, Children, ReactElement } from 'react' + +import { View } from '@instructure/ui-view' +import { + safeCloneElement, + withDeterministicId +} from '@instructure/ui-react-utils' + +import { withStyleLegacy as withStyle } from '@instructure/emotion' + +import generateStyle from './styles' +import generateComponentTheme from './theme' + +import { allowedProps } from './props' +import type { BadgeProps } from './props' + +/** +--- +category: components +--- +**/ +@withDeterministicId() +@withStyle(generateStyle, generateComponentTheme) +class Badge extends Component { + static readonly componentId = 'Badge' + + static allowedProps = allowedProps + static defaultProps = { + standalone: false, + type: 'count', + variant: 'primary', + display: 'inline-block', + pulse: false, + placement: 'top end', + elementRef: () => {}, + formatOverflowText: (_count: number, countUntil: number) => + `${countUntil - 1} +` + } + + constructor(props: BadgeProps) { + super(props) + this._defaultId = this.props.deterministicId!() + } + + _defaultId: string + + ref: Element | null = null + + handleRef = (el: Element | null) => { + const { elementRef } = this.props + + this.ref = el + + if (typeof elementRef === 'function') { + elementRef(el) + } + } + + componentDidMount() { + this.props.makeStyles?.() + } + componentDidUpdate() { + this.props.makeStyles?.() + } + + countOverflow() { + const { count, countUntil } = this.props + + return countUntil && count && countUntil > 1 && count >= countUntil + } + + renderOutput() { + const { count, countUntil, formatOverflowText, formatOutput, type } = + this.props + + // If the badge count is >= than the countUntil limit, format the badge text + // via the formatOverflowText function prop + let formattedCount = (count || '').toString() + if ( + count && + countUntil && + formatOverflowText && + type === 'count' && + this.countOverflow() + ) { + formattedCount = formatOverflowText(count, countUntil) + } + + if (typeof formatOutput === 'function') { + return formatOutput(formattedCount) + } else { + return type === 'count' ? formattedCount : null + } + } + + renderBadge() { + const { count, margin, standalone, type, styles } = this.props + + return ( + + {this.renderOutput()} + + ) + } + + renderChildren() { + return Children.map(this.props.children, (child) => { + return safeCloneElement(child as ReactElement, { + 'aria-describedby': this._defaultId + }) + }) + } + + render() { + const { margin, standalone, display, as, styles } = this.props + + if (standalone) { + return this.renderBadge() + } else { + return ( + + {this.renderChildren()} + {this.renderBadge()} + + ) + } + } +} + +export default Badge +export { Badge } diff --git a/packages/ui-badge/src/Badge/props.ts b/packages/ui-badge/src/Badge/v1/props.ts similarity index 100% rename from packages/ui-badge/src/Badge/props.ts rename to packages/ui-badge/src/Badge/v1/props.ts diff --git a/packages/ui-badge/src/Badge/v1/styles.ts b/packages/ui-badge/src/Badge/v1/styles.ts new file mode 100644 index 0000000000..0982dba1e4 --- /dev/null +++ b/packages/ui-badge/src/Badge/v1/styles.ts @@ -0,0 +1,226 @@ +/* + * 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 { keyframes } from '@instructure/emotion' + +import type { BadgeTheme } from '@instructure/shared-types' +import type { BadgeProps, BadgeStyle } from './props' + +// keyframes have to be outside of 'generateStyle', +// since it is causing problems in style recalculation +const pulseAnimation = keyframes` + to { + transform: scale(1); + opacity: 0.9; + }` + +/** + * --- + * private: true + * --- + * Generates the style object from the theme and provided additional information + * @param {Object} componentTheme The theme variable object. + * @param {Object} props the props of the component, the style is applied to + * @param {Object} state the state of the component, the style is applied to + * @return {Object} The final style object, which will be used in the component + */ +const generateStyle = ( + componentTheme: BadgeTheme, + props: BadgeProps +): BadgeStyle => { + const { type, variant, placement = '', standalone, pulse } = props + + const top = placement.indexOf('top') > -1 + const bottom = placement.indexOf('bottom') > -1 + const start = placement.indexOf('start') > -1 + const end = placement.indexOf('end') > -1 + const center = placement.indexOf('center') > -1 + + const variants = { + danger: { + badge: { + color: componentTheme.color, + backgroundColor: componentTheme.colorDanger + }, + pulseBorder: { + borderColor: componentTheme.colorDanger + } + }, + success: { + badge: { + color: componentTheme.color, + backgroundColor: componentTheme.colorSuccess + }, + pulseBorder: { + borderColor: componentTheme.colorSuccess + } + }, + primary: { + badge: { + color: componentTheme.color, + backgroundColor: componentTheme.colorPrimary + }, + pulseBorder: { + borderColor: componentTheme.colorPrimary + } + }, + inverse: { + // text and background colors are swapped + badge: { + color: componentTheme.colorInverse, + backgroundColor: componentTheme.color + }, + pulseBorder: { + borderColor: componentTheme.color + } + } + } + + const countPositions = { + ...(top && { top: `calc(-1 * ${componentTheme.countOffset})` }), + ...(bottom && { bottom: `calc(-1 * ${componentTheme.countOffset})` }), + ...(start && { + insetInlineStart: `calc(-1 * ${componentTheme.countOffset})`, + insetInlineEnd: 'auto' + }), + ...(end && { + insetInlineEnd: `calc(-1 * ${componentTheme.countOffset})`, + insetInlineStart: 'auto' + }), + ...(center && { + ...((end || start) && { + top: `calc(50% - (${componentTheme.size} / 2))` + }), + ...(start && { + insetInlineStart: 'auto', + insetInlineEnd: `calc(100% - ${componentTheme.countOffset})` + }), + ...(end && { + insetInlineEnd: 'auto', + insetInlineStart: `calc(100% - ${componentTheme.countOffset})` + }) + }) + } + + const notificationPositions = { + ...(top && { top: componentTheme.notificationOffset }), + ...(bottom && { bottom: componentTheme.notificationOffset }), + ...(start && { + insetInlineStart: componentTheme.notificationOffset, + insetInlineEnd: 'auto' + }), + ...(end && { + insetInlineEnd: componentTheme.notificationOffset, + insetInlineStart: 'auto' + }), + ...(center && { + ...((end || start) && { + top: `calc(50% - (${componentTheme.sizeNotification} / 2))` + }), + ...(start && { + insetInlineStart: `calc(-1 * ${componentTheme.sizeNotification} / 2)`, + insetInlineEnd: 'auto' + }), + ...(end && { + insetInlineEnd: `calc(-1 * ${componentTheme.sizeNotification} / 2)`, + insetInlineStart: 'auto' + }) + }) + } + + const notStandaloneTypeVariant = { + count: countPositions, + notification: notificationPositions + } + + const typeVariant = { + count: { + lineHeight: componentTheme.size, + minWidth: componentTheme.size, + paddingInlineStart: componentTheme.padding, + paddingInlineEnd: componentTheme.padding + }, + notification: { + width: componentTheme.sizeNotification, + height: componentTheme.sizeNotification + } + } + + return { + badge: { + label: 'badge', + fontFamily: componentTheme.fontFamily, + fontWeight: componentTheme.fontWeight, + boxSizing: 'border-box', + pointerEvents: 'none', + textAlign: 'center', + fontSize: componentTheme.fontSize, + whiteSpace: 'nowrap', + borderRadius: componentTheme.borderRadius, + + ...variants[variant!].badge, + + ...(pulse && { + position: 'relative', + + '&::before': { + content: '""', + width: 'calc(100% + 0.5rem)', + height: 'calc(100% + 0.5rem)', + borderRadius: componentTheme.borderRadius, + position: 'absolute', + top: '-0.25rem', + insetInlineEnd: 'auto', + insetInlineStart: '-0.25rem', + boxSizing: 'border-box', + border: `${componentTheme.pulseBorderThickness} solid`, + opacity: 0, + transform: 'scale(0.75)', + animationName: pulseAnimation, + animationDuration: '1s', + animationIterationCount: '4', + animationDirection: 'alternate', + ...variants[variant!].pulseBorder + } + }), + + ...typeVariant[type!], + + ...(!standalone && { + position: 'absolute', + zIndex: componentTheme.notificationZIndex, + ...notStandaloneTypeVariant[type!] + }) + }, + wrapper: { + label: 'badge__wrapper', + position: 'relative', + boxSizing: 'border-box', + + svg: { display: 'block' } + } + } +} + +export default generateStyle diff --git a/packages/ui-badge/src/Badge/v1/theme.ts b/packages/ui-badge/src/Badge/v1/theme.ts new file mode 100644 index 0000000000..c6aaa6a061 --- /dev/null +++ b/packages/ui-badge/src/Badge/v1/theme.ts @@ -0,0 +1,74 @@ +/* + * 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 { Theme, ThemeSpecificStyle } from '@instructure/ui-themes' +import { BadgeTheme } from '@instructure/shared-types' + +/** + * Generates the theme object for the component from the theme and provided additional information + * @param {Object} theme The actual theme object. + * @return {Object} The final theme object with the overrides and component variables + */ +const generateComponentTheme = (theme: Theme): BadgeTheme => { + const { + borders, + colors, + spacing, + typography, + stacking, + key: themeName + } = theme + + const themeSpecificStyle: ThemeSpecificStyle = { + canvas: { + colorPrimary: theme['ic-brand-primary'] + } + } + + const componentVariables: BadgeTheme = { + fontFamily: typography?.fontFamily, + fontWeight: typography?.fontWeightNormal, + color: colors?.contrasts?.white1010, + fontSize: typography?.fontSizeXSmall, + colorDanger: colors?.contrasts?.red4570, + colorSuccess: colors?.contrasts?.green4570, + colorPrimary: colors?.contrasts?.blue4570, + colorInverse: colors?.contrasts?.grey4570, + size: '1.25rem', + countOffset: '0.5rem', + notificationOffset: '0.125rem', + notificationZIndex: stacking?.above, + sizeNotification: spacing?.small, + borderRadius: '999rem', + padding: spacing?.xxSmall, + pulseBorderThickness: borders?.widthMedium + } + + return { + ...componentVariables, + ...themeSpecificStyle[themeName] + } +} + +export default generateComponentTheme diff --git a/packages/ui-badge/src/Badge/README.md b/packages/ui-badge/src/Badge/v2/README.md similarity index 100% rename from packages/ui-badge/src/Badge/README.md rename to packages/ui-badge/src/Badge/v2/README.md diff --git a/packages/ui-badge/src/Badge/__tests__/Badge.test.tsx b/packages/ui-badge/src/Badge/v2/__tests__/Badge.test.tsx similarity index 100% rename from packages/ui-badge/src/Badge/__tests__/Badge.test.tsx rename to packages/ui-badge/src/Badge/v2/__tests__/Badge.test.tsx diff --git a/packages/ui-badge/src/Badge/index.tsx b/packages/ui-badge/src/Badge/v2/index.tsx similarity index 98% rename from packages/ui-badge/src/Badge/index.tsx rename to packages/ui-badge/src/Badge/v2/index.tsx index 3d8f3017a8..26ddc12f2c 100644 --- a/packages/ui-badge/src/Badge/index.tsx +++ b/packages/ui-badge/src/Badge/v2/index.tsx @@ -30,7 +30,7 @@ import { withDeterministicId } from '@instructure/ui-react-utils' -import { withStyle } from '@instructure/emotion' +import { withStyleLegacy as withStyle } from '@instructure/emotion' import generateStyle from './styles' diff --git a/packages/ui-badge/src/Badge/v2/props.ts b/packages/ui-badge/src/Badge/v2/props.ts new file mode 100644 index 0000000000..a8812c69fc --- /dev/null +++ b/packages/ui-badge/src/Badge/v2/props.ts @@ -0,0 +1,112 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import type { AsElementType, BadgeTheme } from '@instructure/shared-types' +import type { + Spacing, + WithStyleProps, + ComponentStyle +} from '@instructure/emotion' +import type { PlacementPropValues } from '@instructure/ui-position' +import type { WithDeterministicIdProps } from '@instructure/ui-react-utils' +import type { PropsWithChildren } from 'react' + +type BadgeOwnProps = { + count?: number + /** + * The number at which the count gets truncated by + * formatOverflowText. For example, a countUntil of 100 + * would stop the count at 99. + */ + countUntil?: number + /** + * Render Badge as a counter (`count`) or as a smaller dot (`notification`) with + * no count number displayed. + */ + type?: 'count' | 'notification' + /** + * Render Badge as an inline html element that is not positioned relative + * to a child. + */ + standalone?: boolean + /** + * Make the Badge slowly pulse twice to get the user's attention. + */ + pulse?: boolean + variant?: 'primary' | 'success' | 'danger' | 'inverse' + /** + * provides a reference to the underlying html root element + */ + elementRef?: (element: Element | null) => void + formatOverflowText?: (count: number, countUntil: number) => string + formatOutput?: (formattedCount: string) => React.JSX.Element | string | number + as?: AsElementType + /** + * Specifies the display property of the container. + * + * __Use "block" only when the content inside the Badge also has "block" display.__ + */ + display?: 'inline-block' | 'block' + /** + * Valid values are `0`, `none`, `auto`, `xxx-small`, `xx-small`, `x-small`, + * `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via + * familiar CSS-like shorthand. For example: `margin="small auto large"`. + */ + margin?: Spacing + /** + * Supported values are `top start`, `top end`, `end center`, `bottom end`, + * `bottom start`, and `start center` + */ + placement?: PlacementPropValues +} & PropsWithChildren // > + +type BadgeProps = BadgeOwnProps & + WithStyleProps & + WithDeterministicIdProps + +type BadgeStyle = ComponentStyle<'badge' | 'wrapper'> +const allowedProps: AllowedPropKeys = [ + 'count', + 'countUntil', + 'children', + 'type', + 'standalone', + 'pulse', + 'variant', + 'placement', + 'display', + 'margin', + 'elementRef', + 'formatOverflowText', + 'formatOutput', + 'as' +] + +export type { BadgeProps, BadgeStyle } +export { allowedProps } diff --git a/packages/ui-badge/src/Badge/styles.ts b/packages/ui-badge/src/Badge/v2/styles.ts similarity index 100% rename from packages/ui-badge/src/Badge/styles.ts rename to packages/ui-badge/src/Badge/v2/styles.ts diff --git a/packages/ui-badge/src/exports/a.ts b/packages/ui-badge/src/exports/a.ts new file mode 100644 index 0000000000..2f5e24a367 --- /dev/null +++ b/packages/ui-badge/src/exports/a.ts @@ -0,0 +1,26 @@ +/* + * 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 { Badge } from '../Badge/v1' +export type { BadgeProps } from '../Badge/v1/props' diff --git a/packages/ui-badge/src/exports/b.ts b/packages/ui-badge/src/exports/b.ts new file mode 100644 index 0000000000..b346855ce7 --- /dev/null +++ b/packages/ui-badge/src/exports/b.ts @@ -0,0 +1,26 @@ +/* + * 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 { Badge } from '../Badge/v2' +export type { BadgeProps } from '../Badge/v2/props' diff --git a/packages/ui-billboard/package.json b/packages/ui-billboard/package.json index c992821558..ffbe70fed2 100644 --- a/packages/ui-billboard/package.json +++ b/packages/ui-billboard/package.json @@ -51,10 +51,28 @@ "sideEffects": false, "exports": { ".": { - "types": "./types/index.d.ts", - "import": "./es/index.js", - "require": "./lib/index.js", - "default": "./es/index.js" + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_5": { + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_6": { + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" + }, + "./latest": { + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" }, "./lib/*": "./lib/*", "./es/*": "./es/*", diff --git a/packages/ui-billboard/src/Billboard/v1/README.md b/packages/ui-billboard/src/Billboard/v1/README.md new file mode 100644 index 0000000000..06f20b5c31 --- /dev/null +++ b/packages/ui-billboard/src/Billboard/v1/README.md @@ -0,0 +1,95 @@ +--- +describes: Billboard +--- + +### Static Billboard + +Used for empty states, 404 pages, redirects, etc. + +```js +--- +type: example +--- +} +/> +``` + +### Structure + +- If Billboard has an `href` prop set, it will render as a link; + if an `onClick` prop is set, the component will render as a button. +- Use the `message` prop for your link or button text/call to action (Note: + don't pass interactive content to the `message` prop if you have set the `href` + or `onClick` props). +- Use the `size` prop to adjust the size of the icon and text. +- Pass [Instructure icons](icons-react) to the `hero` property via a function + _(see examples)_, and they will be sized correctly based on the Billboard's + `size`. + +```js +--- +type: example +--- + + } + /> + +``` + +```js +--- +type: example +--- + + } + /> + +``` + +```js +--- +type: example +--- + } +/> +``` + +### Disabled Billboard + +```js +--- +type: example +--- + } + disabled +/> +``` diff --git a/packages/ui-billboard/src/Billboard/v1/index.tsx b/packages/ui-billboard/src/Billboard/v1/index.tsx new file mode 100644 index 0000000000..0a76d639b4 --- /dev/null +++ b/packages/ui-billboard/src/Billboard/v1/index.tsx @@ -0,0 +1,171 @@ +/* + * 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 { Component, MouseEvent } from 'react' + +import { Heading } from '@instructure/ui-heading' +import { View } from '@instructure/ui-view' +import { + omitProps, + callRenderProp, + getElementType +} from '@instructure/ui-react-utils' + +import { withStyleLegacy as withStyle } from '@instructure/emotion' + +import generateStyle from './styles' +import generateComponentTheme from './theme' + +import { allowedProps } from './props' +import type { BillboardProps, HeroIconSize } from './props' +import type { ViewProps } from '@instructure/ui-view' + +/** +--- +category: components +--- +**/ +@withStyle(generateStyle, generateComponentTheme) +class Billboard extends Component { + static readonly componentId = 'Billboard' + + static allowedProps = allowedProps + static defaultProps = { + disabled: false, + readOnly: false, + size: 'medium', + headingAs: 'span', + headingLevel: 'h1', + as: 'span', + elementRef: () => {} + } as const + + ref: Element | null = null + + handleRef = (el: Element | null) => { + const { elementRef } = this.props + + this.ref = el + + if (typeof elementRef === 'function') { + elementRef(el) + } + } + + componentDidMount() { + this.props.makeStyles?.() + } + + componentDidUpdate() { + this.props.makeStyles?.() + } + + renderHeading() { + const { headingLevel, headingAs, heading, styles } = this.props + + return ( + + + {heading} + + + ) + } + + get SVGIconSize(): HeroIconSize { + const size = this.props.size + + // serve up appropriate SVGIcon size for each Billboard size + if (size === 'small') { + return 'medium' + } else if (size === 'large') { + return 'x-large' + } else { + return 'large' + } + } + + renderHero() { + if (typeof this.props.hero === 'function') { + return this.props.hero(this.SVGIconSize) + } else { + return this.props.hero + } + } + + renderContent() { + const { heading, message, hero, styles } = this.props + + return ( + + {hero && {this.renderHero()}} + {heading && this.renderHeading()} + {message && ( + {callRenderProp(message)} + )} + + ) + } + + handleClick = (e: MouseEvent): void => { + const { readOnly, onClick } = this.props + + if (readOnly) { + e.preventDefault() + e.stopPropagation() + } else if (typeof onClick === 'function') { + onClick(e) + } + } + + render() { + const { href, disabled, readOnly, margin, styles } = this.props + + const Element = getElementType(Billboard, this.props) + + return ( + + + {this.renderContent()} + + + ) + } +} + +export default Billboard +export { Billboard } diff --git a/packages/ui-billboard/src/Billboard/v1/props.ts b/packages/ui-billboard/src/Billboard/v1/props.ts new file mode 100644 index 0000000000..f1a6755902 --- /dev/null +++ b/packages/ui-billboard/src/Billboard/v1/props.ts @@ -0,0 +1,128 @@ +/* + * 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 { + Spacing, + WithStyleProps, + ComponentStyle +} from '@instructure/emotion' +import type { + AsElementType, + BillboardTheme, + OtherHTMLAttributes +} from '@instructure/shared-types' +import type { ViewProps } from '@instructure/ui-view' +import { MouseEvent } from 'react' +import { Renderable } from '@instructure/shared-types' +type HeroIconSize = 'medium' | 'x-large' | 'large' +type BillboardOwnProps = { + /** + * Provide an component or Instructure Icon for the hero image + */ + hero?: React.ReactElement | ((iconSize: HeroIconSize) => React.ReactElement) + /** + * If you're using an icon, this prop will size it. Also sets the font-size + * of the headline and message. + */ + size?: 'small' | 'medium' | 'large' + /** + * the element type to render as + */ + as?: AsElementType + /** + * provides a reference to the underlying html root element + */ + elementRef?: (element: Element | null) => void + /** + * The headline for the Billboard. Is styled as an h1 element by default + */ + heading?: string + /** + * Choose the appropriately semantic tag for the heading + */ + headingAs?: 'h1' | 'h2' | 'h3' | 'span' + /** + * Choose the font-size for the heading (see the Heading component) + */ + headingLevel?: 'h1' | 'h2' | 'h3' | 'h4' + /** + * Instructions or information for the Billboard. Note: you should not pass + * interactive content to this prop if you are also providing an `href` or + * `onClick`. That would cause the Billboard to render as a button or link + * and would result in nested interactive content. + */ + message?: Renderable + /** + * If you add an onClick prop, the Billboard renders as a clickable button + */ + onClick?: (e: MouseEvent) => void + /** + * If `href` is provided, Billboard will render as a link + */ + href?: string + /** + * Whether or not to disable the billboard + */ + disabled?: boolean + /** + * Works just like disabled but keeps the same styles as if it were active + */ + readOnly?: boolean + /** + * Valid values are `0`, `none`, `auto`, `xxx-small`, `xx-small`, `x-small`, + * `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via + * familiar CSS-like shorthand. For example: `margin="small auto large"`. + */ + margin?: Spacing +} + +type PropKeys = keyof BillboardOwnProps + +type AllowedPropKeys = Readonly> + +type BillboardProps = BillboardOwnProps & + WithStyleProps & + OtherHTMLAttributes + +type BillboardStyle = ComponentStyle< + 'billboard' | 'content' | 'hero' | 'heading' | 'message' +> +const allowedProps: AllowedPropKeys = [ + 'hero', + 'size', + 'as', + 'elementRef', + 'heading', + 'headingAs', + 'headingLevel', + 'message', + 'onClick', + 'href', + 'disabled', + 'readOnly', + 'margin' +] + +export type { BillboardProps, BillboardStyle, HeroIconSize } +export { allowedProps } diff --git a/packages/ui-billboard/src/Billboard/v1/styles.ts b/packages/ui-billboard/src/Billboard/v1/styles.ts new file mode 100644 index 0000000000..3ba329f795 --- /dev/null +++ b/packages/ui-billboard/src/Billboard/v1/styles.ts @@ -0,0 +1,163 @@ +/* + * 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 { BillboardTheme } from '@instructure/shared-types' +import type { BillboardProps, BillboardStyle } from './props' + +/** + * --- + * private: true + * --- + * Generates the style object from the theme and provided additional information + * @param {Object} componentTheme The theme variable object. + * @param {Object} props the props of the component, the style is applied to + * @param {Object} state the state of the component, the style is applied to + * @return {Object} The final style object, which will be used in the component + */ +const generateStyle = ( + componentTheme: BillboardTheme, + props: BillboardProps +): BillboardStyle => { + const { size, href, onClick, disabled, hero, heading } = props + + const clickable = href || onClick + + const sizeVariants = { + small: { + billboard: { padding: componentTheme.paddingSmall }, + hero: { fontSize: '3rem' }, + message: { fontSize: componentTheme.messageFontSizeSmall }, + heading: { ...(hero && { margin: `${componentTheme.mediumMargin} 0 0` }) } + }, + medium: { + billboard: { padding: componentTheme.paddingMedium }, + hero: { fontSize: '5rem' }, + message: { fontSize: componentTheme.messageFontSizeMedium }, + heading: {} + }, + large: { + billboard: { padding: componentTheme.paddingLarge }, + hero: { fontSize: '10rem' }, + message: { fontSize: componentTheme.messageFontSizeLarge }, + heading: {} + } + } + + const clickableVariants = clickable + ? { + appearance: 'none', + boxSizing: 'border-box', + cursor: 'pointer', + userSelect: 'none', + touchAction: 'manipulation', + width: '100%', + margin: '0', + border: `${componentTheme.buttonBorderWidth} ${componentTheme.buttonBorderStyle} transparent`, + borderRadius: componentTheme.buttonBorderRadius, + background: componentTheme.backgroundColor, + textDecoration: 'none', + + '&:hover': { borderStyle: componentTheme.buttonHoverBorderStyle }, + + '&:hover, &:focus': { + textDecoration: 'none', + outline: 'none', + borderColor: componentTheme.iconHoverColor, + + '& [class$=-billboard__hero]': { + color: componentTheme.iconHoverColor + } + }, + '&:active': { + background: componentTheme.clickableActiveBg, + borderColor: componentTheme.iconHoverColor, + + '& [class$=-billboard__hero], & [class$=-billboard__message]': { + color: componentTheme.clickableActiveText + } + } + } + : { + backgroundColor: componentTheme.backgroundColor + } + + return { + billboard: { + label: 'billboard', + boxSizing: 'border-box', + position: 'relative', + fontFamily: componentTheme.fontFamily, + marginLeft: 'auto', + marginRight: 'auto', + textAlign: 'center', + display: 'block', + ...sizeVariants[size!].billboard, + ...clickableVariants, + + ...(disabled && { + cursor: 'not-allowed', + pointerEvents: 'none', + opacity: 0.5 + }) + }, + content: { + label: 'billboard__content', + display: 'block' + }, + hero: { + label: 'billboard__hero', + display: 'block', + color: componentTheme.iconColor, + ...sizeVariants[size!].hero, + + '& > img, & > svg': { + maxWidth: '100%', + display: 'block', + margin: '0 auto' + }, + + '& > img': { height: 'auto' } + }, + heading: { + label: 'billboard__heading', + display: 'block', + ...(hero && { margin: `${componentTheme.largeMargin} 0 0` }), + ...sizeVariants[size!].heading + }, + message: { + label: 'billboard__message', + display: 'block', + color: clickable + ? componentTheme.messageColorClickable + : componentTheme.messageColor, + + ...((hero || heading) && { + margin: `${componentTheme.mediumMargin} 0 0` + }), + ...sizeVariants[size!].message + } + } +} + +export default generateStyle diff --git a/packages/ui-billboard/src/Billboard/v1/theme.ts b/packages/ui-billboard/src/Billboard/v1/theme.ts new file mode 100644 index 0000000000..49a8212c7b --- /dev/null +++ b/packages/ui-billboard/src/Billboard/v1/theme.ts @@ -0,0 +1,77 @@ +/* + * 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. + */ + +/* Global variables (colors, typography, spacing, etc.) are defined in lib/themes */ + +import type { Theme, ThemeSpecificStyle } from '@instructure/ui-themes' +import { BillboardTheme } from '@instructure/shared-types' + +/** + * Generates the theme object for the component from the theme and provided additional information + * @param {Object} theme The actual theme object. + * @return {Object} The final theme object with the overrides and component variables + */ +const generateComponentTheme = (theme: Theme): BillboardTheme => { + const { borders, colors, spacing, typography, key: themeName } = theme + + const themeSpecificStyle: ThemeSpecificStyle = { + canvas: { + iconHoverColor: theme['ic-link-color'], + messageColorClickable: theme['ic-link-color'], + clickableActiveBg: theme['ic-brand-primary'] + } + } + + const componentVariables: BillboardTheme = { + fontFamily: typography?.fontFamily, + paddingSmall: spacing?.small, + paddingMedium: spacing?.medium, + paddingLarge: spacing?.medium, + iconColor: colors?.contrasts?.grey4570, + mediumMargin: spacing?.small, + largeMargin: spacing?.medium, + iconHoverColor: colors?.contrasts?.blue4570, + backgroundColor: colors?.contrasts?.white1010, + iconHoverColorInverse: colors?.contrasts?.white1010, + buttonBorderWidth: borders?.widthMedium, + buttonBorderRadius: borders?.radiusLarge, + messageColor: colors?.contrasts?.blue4570, + messageColorClickable: colors?.contrasts?.blue4570, + messageColorInverse: colors?.contrasts?.grey1111, + messageFontSizeSmall: typography?.fontSizeSmall, + messageFontSizeMedium: typography?.fontSizeMedium, + messageFontSizeLarge: typography?.fontSizeLarge, + clickableActiveBg: colors?.contrasts?.blue4570, + clickableActiveText: colors?.contrasts?.white1010, + buttonBorderStyle: borders?.style, + buttonHoverBorderStyle: 'dashed' + } + + return { + ...componentVariables, + ...themeSpecificStyle[themeName] + } +} + +export default generateComponentTheme diff --git a/packages/ui-billboard/src/Billboard/README.md b/packages/ui-billboard/src/Billboard/v2/README.md similarity index 100% rename from packages/ui-billboard/src/Billboard/README.md rename to packages/ui-billboard/src/Billboard/v2/README.md diff --git a/packages/ui-billboard/src/Billboard/__tests__/Billboard.test.tsx b/packages/ui-billboard/src/Billboard/v2/__tests__/Billboard.test.tsx similarity index 100% rename from packages/ui-billboard/src/Billboard/__tests__/Billboard.test.tsx rename to packages/ui-billboard/src/Billboard/v2/__tests__/Billboard.test.tsx diff --git a/packages/ui-billboard/src/Billboard/index.tsx b/packages/ui-billboard/src/Billboard/v2/index.tsx similarity index 100% rename from packages/ui-billboard/src/Billboard/index.tsx rename to packages/ui-billboard/src/Billboard/v2/index.tsx diff --git a/packages/ui-billboard/src/Billboard/props.ts b/packages/ui-billboard/src/Billboard/v2/props.ts similarity index 100% rename from packages/ui-billboard/src/Billboard/props.ts rename to packages/ui-billboard/src/Billboard/v2/props.ts diff --git a/packages/ui-billboard/src/Billboard/styles.ts b/packages/ui-billboard/src/Billboard/v2/styles.ts similarity index 100% rename from packages/ui-billboard/src/Billboard/styles.ts rename to packages/ui-billboard/src/Billboard/v2/styles.ts diff --git a/packages/ui-billboard/src/exports/a.ts b/packages/ui-billboard/src/exports/a.ts new file mode 100644 index 0000000000..26f0bf4e0e --- /dev/null +++ b/packages/ui-billboard/src/exports/a.ts @@ -0,0 +1,25 @@ +/* + * 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 { Billboard } from '../Billboard/v1' +export type { BillboardProps } from '../Billboard/v1/props' diff --git a/packages/ui-billboard/src/exports/b.ts b/packages/ui-billboard/src/exports/b.ts new file mode 100644 index 0000000000..0fe8046b69 --- /dev/null +++ b/packages/ui-billboard/src/exports/b.ts @@ -0,0 +1,25 @@ +/* + * 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 { Billboard } from '../Billboard/v2' +export type { BillboardProps } from '../Billboard/v2/props' diff --git a/packages/ui-breadcrumb/package.json b/packages/ui-breadcrumb/package.json index 451c6a225f..8d7261955d 100644 --- a/packages/ui-breadcrumb/package.json +++ b/packages/ui-breadcrumb/package.json @@ -52,10 +52,28 @@ "sideEffects": false, "exports": { ".": { - "types": "./types/index.d.ts", - "import": "./es/index.js", - "require": "./lib/index.js", - "default": "./es/index.js" + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_5": { + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_6": { + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" + }, + "./latest": { + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" }, "./lib/*": "./lib/*", "./es/*": "./es/*", diff --git a/packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/__tests__/BreadcrumbLink.test.tsx b/packages/ui-breadcrumb/src/Breadcrumb/v1/BreadcrumbLink/__tests__/BreadcrumbLink.test.tsx similarity index 100% rename from packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/__tests__/BreadcrumbLink.test.tsx rename to packages/ui-breadcrumb/src/Breadcrumb/v1/BreadcrumbLink/__tests__/BreadcrumbLink.test.tsx diff --git a/packages/ui-breadcrumb/src/Breadcrumb/v1/BreadcrumbLink/index.tsx b/packages/ui-breadcrumb/src/Breadcrumb/v1/BreadcrumbLink/index.tsx new file mode 100644 index 0000000000..937e361006 --- /dev/null +++ b/packages/ui-breadcrumb/src/Breadcrumb/v1/BreadcrumbLink/index.tsx @@ -0,0 +1,121 @@ +/* + * 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 { Component } from 'react' + +import { TruncateText } from '@instructure/ui-truncate-text' +import { Link } from '@instructure/ui-link' +import { omitProps } from '@instructure/ui-react-utils' +import { Tooltip } from '@instructure/ui-tooltip' + +import { allowedProps } from './props' +import type { BreadcrumbLinkProps, BreadcrumbLinkState } from './props' + +/** +--- +parent: Breadcrumb +id: Breadcrumb.Link +--- +**/ + +class BreadcrumbLink extends Component< + BreadcrumbLinkProps, + BreadcrumbLinkState +> { + static readonly componentId = 'Breadcrumb.Link' + + static allowedProps = allowedProps + static defaultProps = {} + + ref: Element | null = null + + handleRef = (el: Element | null) => { + this.ref = el + } + constructor(props: BreadcrumbLinkProps) { + super(props) + + this.state = { + isTruncated: false + } + } + handleTruncation(isTruncated: boolean) { + if (isTruncated !== this.state.isTruncated) { + this.setState({ isTruncated }) + } + } + + render() { + const { + children, + href, + renderIcon, + iconPlacement, + onClick, + onMouseEnter, + isCurrentPage + } = this.props + + const { isTruncated } = this.state + const props = omitProps(this.props, BreadcrumbLink.allowedProps) + + const isInteractive = onClick || href + return ( + + + this.handleTruncation(isTruncated)} + > + {children} + + + + ) + } +} + +export default BreadcrumbLink +export { BreadcrumbLink } diff --git a/packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/props.ts b/packages/ui-breadcrumb/src/Breadcrumb/v1/BreadcrumbLink/props.ts similarity index 99% rename from packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/props.ts rename to packages/ui-breadcrumb/src/Breadcrumb/v1/BreadcrumbLink/props.ts index 46f89d2374..bf4d5bba2a 100644 --- a/packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/props.ts +++ b/packages/ui-breadcrumb/src/Breadcrumb/v1/BreadcrumbLink/props.ts @@ -78,6 +78,7 @@ type BreadcrumbLinkProps = PickPropsWithExceptions< | 'onClick' | 'renderIcon' | 'iconPlacement' + | 'isWithinText' | 'elementRef' > & BreadcrumbLinkOwnProps & diff --git a/packages/ui-breadcrumb/src/Breadcrumb/v1/README.md b/packages/ui-breadcrumb/src/Breadcrumb/v1/README.md new file mode 100644 index 0000000000..4ed586119e --- /dev/null +++ b/packages/ui-breadcrumb/src/Breadcrumb/v1/README.md @@ -0,0 +1,141 @@ +--- +describes: Breadcrumb +--- + +Breadcrumbs enable users to quickly see their location within a path of navigation. +Long breadcrumb text will be automatically truncated, ensuring the list always +remains on a single line. + +**Breadcrumbs are best suited for tablet-sized (~768px) screens and larger.** +For smaller screens, use a [Link](Link) that returns the user to the previous page or view. +The example below is implemented with [Responsive](Responsive). Resize the browser window to see +Breadcrumb become a Link at under 768px. + +```js +--- +type: example +--- + + {(props, matches) => { + if (matches.includes('tablet')) { + return ( + + Student Forecast + University of Utah + University of Utah Colleges + College of Life Sciences + + ) + } else { + return ( + + University of Utah Colleges + + ) + } + }} + +``` + +Change the `size` prop to control the font-size of the breadcrumbs (default is `medium`). + +```js +--- +type: example +--- +
+ + English 204 + + Exploring John Updike + + The Rabbit Novels + Rabbit Is Rich + + + + English 204 + + Exploring John Updike + + The Rabbit Novels + Rabbit Is Rich + + + + English 204 + + Exploring John Updike + + The Rabbit Novels + Rabbit Is Rich + +
+``` + +### Icons + +You can include icons in `Breadcrumb.Link`: + +```js +--- +type: example +--- + + } href="#Breadcrumb">Item Bank + } onClick={() => {}}>History + New Question + +``` + +### Guidelines + +```js +--- +type: embed +--- + +
+ Place Breadcrumb near the top of the page + Show hierarchy, not history + Keep Breadcrumb titles short but descriptive +
+
+ Use Breadcrumb if you are taking users through a multi-step process + Use Breadcrumb in mobile layouts: use a Link to the previous page/view instead +
+
+``` + +```js +--- +type: embed +--- + +
+ + To indicate the current element within a breadcrumb, the aria-current attribute is used. In this component, aria-current="page" will automatically be applied to the last element, and we recommend that the current page always be the last element in the breadcrumb. If the last element is not the current page, the isCurrentPage property should be applied to the relevant Breadcrumb.Link to ensure compatibility with screen readers. + +
+
+``` diff --git a/packages/ui-breadcrumb/src/Breadcrumb/v1/index.tsx b/packages/ui-breadcrumb/src/Breadcrumb/v1/index.tsx new file mode 100644 index 0000000000..95d609f92a --- /dev/null +++ b/packages/ui-breadcrumb/src/Breadcrumb/v1/index.tsx @@ -0,0 +1,142 @@ +/* + * 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 { + isValidElement, + cloneElement, + Children, + Component, + ReactElement +} from 'react' + +import { View } from '@instructure/ui-view' + +import { withStyleLegacy as withStyle } from '@instructure/emotion' +import { IconArrowOpenEndSolid } from '@instructure/ui-icons' +import { BreadcrumbLink } from './BreadcrumbLink' + +import generateStyle from './styles' +import generateComponentTheme from './theme' + +import { allowedProps } from './props' +import type { BreadcrumbProps } from './props' + +/** +--- +category: components +--- +**/ + +@withStyle(generateStyle, generateComponentTheme) +class Breadcrumb extends Component { + static readonly componentId = 'Breadcrumb' + + static allowedProps = allowedProps + static defaultProps = { + size: 'medium', + children: null + } + + ref: Element | null = null + + handleRef = (el: Element | null) => { + this.ref = el + } + + addAriaCurrent = (child: React.ReactNode) => { + const updatedChild = cloneElement( + child as React.ReactElement<{ 'aria-current'?: string }>, + { + 'aria-current': 'page' + } + ) + return updatedChild + } + + componentDidMount() { + this.props.makeStyles?.() + } + componentDidUpdate() { + this.props.makeStyles?.() + } + + static Link = BreadcrumbLink + + renderChildren() { + const { styles, children } = this.props + const numChildren = Children.count(children) + const inlineStyle = { + maxWidth: `${Math.floor(100 / numChildren)}%` + } + let isAriaCurrentSet = false + + return Children.map(children, (child, index) => { + const isLastElement = index === numChildren - 1 + if (isValidElement(child)) { + const isCurrentPage = + (child as ReactElement).props.isCurrentPage || false + if (isAriaCurrentSet && isCurrentPage) { + console.warn( + `Warning: Multiple elements with isCurrentPage=true found. Only one element should be set to current.` + ) + } + if (isCurrentPage) { + isAriaCurrentSet = true + } + } + return ( +
  • + {!isAriaCurrentSet && + isLastElement && + (child as React.ReactElement).props.isCurrentPage !== false + ? this.addAriaCurrent(child) + : child} + {index < numChildren - 1 && ( + + )} +
  • + ) + }) + } + + render() { + const { styles } = this.props + + return ( + +
      {this.renderChildren()}
    +
    + ) + } +} + +export default Breadcrumb +export { Breadcrumb, BreadcrumbLink } diff --git a/packages/ui-breadcrumb/src/Breadcrumb/v1/props.ts b/packages/ui-breadcrumb/src/Breadcrumb/v1/props.ts new file mode 100644 index 0000000000..30030bf542 --- /dev/null +++ b/packages/ui-breadcrumb/src/Breadcrumb/v1/props.ts @@ -0,0 +1,63 @@ +/* + * 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 { + Spacing, + WithStyleProps, + ComponentStyle +} from '@instructure/emotion' +import type { BreadcrumbTheme } from '@instructure/shared-types' + +type BreadcrumbOwnProps = { + /** + * children of type Breadcrumb.Link + */ + children?: React.ReactNode // TODO: oneOf([BreadcrumbLink]) + /** + * An accessible label for the navigation + */ + label: string + /** + * Sets the font-size of the breadcrumb text + */ + size?: 'small' | 'medium' | 'large' + /** + * Valid values are `0`, `none`, `auto`, `xxx-small`, `xx-small`, `x-small`, + * `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via + * familiar CSS-like shorthand. For example: `margin="small auto large"`. + */ + margin?: Spacing +} + +type PropKeys = keyof BreadcrumbOwnProps + +type AllowedPropKeys = Readonly> + +type BreadcrumbProps = BreadcrumbOwnProps & + WithStyleProps + +type BreadcrumbStyle = ComponentStyle<'breadcrumb' | 'crumb' | 'separator'> +const allowedProps: AllowedPropKeys = ['children', 'label', 'margin', 'size'] + +export type { BreadcrumbProps, BreadcrumbStyle } +export { allowedProps } diff --git a/packages/ui-breadcrumb/src/Breadcrumb/v1/styles.ts b/packages/ui-breadcrumb/src/Breadcrumb/v1/styles.ts new file mode 100644 index 0000000000..263dfa7954 --- /dev/null +++ b/packages/ui-breadcrumb/src/Breadcrumb/v1/styles.ts @@ -0,0 +1,116 @@ +/* + * 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 { BreadcrumbTheme } from '@instructure/shared-types' +import type { BreadcrumbProps, BreadcrumbStyle } from './props' + +/** + * --- + * private: true + * --- + * Generates the style object from the theme and provided additional information + * @param {Object} componentTheme The theme variable object. + * @param {Object} props the props of the component, the style is applied to + * @param {Object} state the state of the component, the style is applied to + * @return {Object} The final style object, which will be used in the component + */ +const generateStyle = ( + componentTheme: BreadcrumbTheme, + props: BreadcrumbProps +): BreadcrumbStyle => { + const { size } = props + + const crumbSizeVariants = { + small: { + fontSize: componentTheme.smallFontSize, + paddingInlineEnd: `calc(${componentTheme.smallSeparatorFontSize} * 2)`, + paddingInlineStart: 0 + }, + medium: { + fontSize: componentTheme.mediumFontSize, + paddingInlineEnd: `calc(${componentTheme.mediumSeparatorFontSize} * 2)`, + paddingInlineStart: 0 + }, + large: { + fontSize: componentTheme.largeFontSize, + paddingInlineEnd: `calc(${componentTheme.largeSeparatorFontSize} * 2)`, + paddingInlineStart: 0 + } + } + + const separatorSizeVariants = { + small: { + fontSize: componentTheme.smallSeparatorFontSize, + insetInlineEnd: `calc(${componentTheme.smallSeparatorFontSize} / 2)`, + insetInlineStart: 'auto', + marginTop: `calc(-1 * (${componentTheme.smallSeparatorFontSize} / 2))` + }, + medium: { + fontSize: componentTheme.mediumSeparatorFontSize, + insetInlineEnd: `calc(${componentTheme.mediumSeparatorFontSize} / 2)`, + insetInlineStart: 'auto', + marginTop: `calc(-1 * (${componentTheme.mediumSeparatorFontSize} / 2))` + }, + large: { + fontSize: componentTheme.largeSeparatorFontSize, + insetInlineEnd: `calc(${componentTheme.largeSeparatorFontSize} / 2)`, + insetInlineStart: 'auto', + marginTop: `calc(-1 * (${componentTheme.largeSeparatorFontSize} / 2))` + } + } + + return { + breadcrumb: { + label: 'breadcrumb', + fontFamily: componentTheme.fontFamily, + margin: 0, + padding: 0, + listStyleType: 'none', + overflow: 'visible', + display: 'flex', + alignItems: 'center' + }, + crumb: { + label: 'breadcrumb__crumb', + boxSizing: 'border-box', + position: 'relative', + ...crumbSizeVariants[size!], + + '&:last-child': { + paddingInlineEnd: 0 + } + }, + separator: { + label: 'breadcrumb__separator', + + boxSizing: 'border-box', + position: 'absolute', + top: '50%', + color: componentTheme.separatorColor, + ...separatorSizeVariants[size!] + } + } +} + +export default generateStyle diff --git a/packages/ui-breadcrumb/src/Breadcrumb/v1/theme.ts b/packages/ui-breadcrumb/src/Breadcrumb/v1/theme.ts new file mode 100644 index 0000000000..c2ce41b02f --- /dev/null +++ b/packages/ui-breadcrumb/src/Breadcrumb/v1/theme.ts @@ -0,0 +1,55 @@ +/* + * 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 { Theme } from '@instructure/ui-themes' +import { BreadcrumbTheme } from '@instructure/shared-types' + +/** + * Generates the theme object for the component from the theme and provided additional information + * @param {Object} theme The actual theme object. + * @return {Object} The final theme object with the overrides and component variables + */ +const generateComponentTheme = (theme: Theme): BreadcrumbTheme => { + const { colors, typography } = theme + + const componentVariables: BreadcrumbTheme = { + fontFamily: typography?.fontFamily, + separatorColor: colors?.contrasts?.grey4570, + + smallSeparatorFontSize: '0.5rem', + smallFontSize: typography?.fontSizeSmall, + + mediumSeparatorFontSize: '0.75rem', + mediumFontSize: typography?.fontSizeMedium, + + largeSeparatorFontSize: '1rem', + largeFontSize: typography?.fontSizeLarge + } + + return { + ...componentVariables + } +} + +export default generateComponentTheme diff --git a/packages/ui-breadcrumb/src/Breadcrumb/v2/BreadcrumbLink/__tests__/BreadcrumbLink.test.tsx b/packages/ui-breadcrumb/src/Breadcrumb/v2/BreadcrumbLink/__tests__/BreadcrumbLink.test.tsx new file mode 100644 index 0000000000..d201934442 --- /dev/null +++ b/packages/ui-breadcrumb/src/Breadcrumb/v2/BreadcrumbLink/__tests__/BreadcrumbLink.test.tsx @@ -0,0 +1,140 @@ +/* + * 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 { fireEvent, render, screen } from '@testing-library/react' +import { vi } from 'vitest' +import '@testing-library/jest-dom' + +import { runAxeCheck } from '@instructure/ui-axe-check' +import { BreadcrumbLink } from '../index' + +const TEST_TEXT_01 = 'Account' +const TEST_LINK = 'http://instructure-test.com' +const TEST_TO = '/example' + +describe('', () => { + let consoleWarningMock: ReturnType + let consoleErrorMock: ReturnType + + beforeEach(() => { + // Mocking console to prevent test output pollution + consoleWarningMock = vi + .spyOn(console, 'warn') + .mockImplementation(() => {}) as any + consoleErrorMock = vi + .spyOn(console, 'error') + .mockImplementation(() => {}) as any + }) + + afterEach(() => { + consoleWarningMock.mockRestore() + consoleErrorMock.mockRestore() + }) + + it('should render an anchor tag when given a href prop', () => { + render({TEST_TEXT_01}) + const anchor = screen.getByRole('link') + + expect(anchor).toHaveAttribute('href', TEST_LINK) + }) + + it('should render as a button and respond to onClick event', () => { + const onClick = vi.fn() + + render({TEST_TEXT_01}) + const button = screen.getByRole('button') + + fireEvent.click(button) + + expect(onClick).toHaveBeenCalledTimes(1) + }) + + it('should respond to mouseEnter event when provided with onMouseEnter prop', () => { + const onMouseEnter = vi.fn() + + render( + + {TEST_TEXT_01} + + ) + const link = screen.getByRole('link') + fireEvent.mouseEnter(link) + + expect(onMouseEnter).toHaveBeenCalledTimes(1) + }) + + it('should allow to prop to pass through', () => { + const { container } = render( + {TEST_TEXT_01} + ) + const link = container.querySelector('a') + + expect(link).toBeInTheDocument() + expect(link).toHaveAttribute('to', TEST_TO) + }) + + it('should not render a link when not given an href prop', () => { + const { container } = render( + {TEST_TEXT_01} + ) + const elementWithHref = container.querySelector('[href]') + const anchor = container.querySelector('a') + const span = container.querySelector('span') + + expect(elementWithHref).toBeNull() + expect(anchor).toBeNull() + expect(span).toBeInTheDocument() + expect(span).toHaveTextContent(TEST_TEXT_01) + }) + + it('should not render a button when not given an onClick prop', () => { + const { container } = render( + {TEST_TEXT_01} + ) + const button = container.querySelector('button') + const span = container.querySelector('span') + + expect(button).toBeNull() + expect(span).toBeInTheDocument() + expect(span).toHaveTextContent(TEST_TEXT_01) + }) + + it('should meet a11y standards as a link', async () => { + const { container } = render( + {TEST_TEXT_01} + ) + const axeCheck = await runAxeCheck(container) + + expect(axeCheck).toBe(true) + }) + + it('should meet a11y standards as a span', async () => { + const { container } = render( + {TEST_TEXT_01} + ) + const axeCheck = await runAxeCheck(container) + + expect(axeCheck).toBe(true) + }) +}) diff --git a/packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/index.tsx b/packages/ui-breadcrumb/src/Breadcrumb/v2/BreadcrumbLink/index.tsx similarity index 98% rename from packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/index.tsx rename to packages/ui-breadcrumb/src/Breadcrumb/v2/BreadcrumbLink/index.tsx index 4d16eabbc8..982f03c169 100644 --- a/packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/index.tsx +++ b/packages/ui-breadcrumb/src/Breadcrumb/v2/BreadcrumbLink/index.tsx @@ -25,7 +25,7 @@ import { Component } from 'react' import { TruncateText } from '@instructure/ui-truncate-text' -import { Link } from '@instructure/ui-link' +import { Link } from '@instructure/ui-link/latest' import { omitProps } from '@instructure/ui-react-utils' import { Tooltip } from '@instructure/ui-tooltip' diff --git a/packages/ui-breadcrumb/src/Breadcrumb/v2/BreadcrumbLink/props.ts b/packages/ui-breadcrumb/src/Breadcrumb/v2/BreadcrumbLink/props.ts new file mode 100644 index 0000000000..9a4b0b3305 --- /dev/null +++ b/packages/ui-breadcrumb/src/Breadcrumb/v2/BreadcrumbLink/props.ts @@ -0,0 +1,101 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import type { + PickPropsWithExceptions, + OtherHTMLAttributes, + Renderable +} from '@instructure/shared-types' +import type { ViewOwnProps } from '@instructure/ui-view' +import type { LinkProps } from '@instructure/ui-link/latest' + +type BreadcrumbLinkOwnProps = { + /** + * Content to render as the crumb, generally should be text. + */ + children: React.ReactNode + /** + * Link the crumb should direct to; if an href is provided, the crumb will render as a link + */ + href?: string + /** + * If the Breadcrumb.Link has an onClick prop (and no href), it will render as a button + */ + onClick?: (event: React.MouseEvent) => void + /** + * Fires when the Link is hovered + */ + onMouseEnter?: (event: React.MouseEvent) => void + /** + * Sets the font-size of the breadcrumb text + */ + size?: 'small' | 'medium' | 'large' + /** + * Add an icon to the Breadcrumb.Link + */ + renderIcon?: Renderable + /** + * Place the icon before or after the text in the Breadcrumb.Link + */ + iconPlacement?: 'start' | 'end' + /** + * Whether the page this breadcrumb points to is the current one. If true, it sets aria-current="page". + * If this prop is not set to true on any breadcrumb element, the one recieving the aria-current="page" will always be the last element, unless the last element's isCurrentPage prop is explicity set to false. + */ + isCurrentPage?: boolean +} + +type PropKeys = keyof BreadcrumbLinkOwnProps + +type AllowedPropKeys = Readonly> + +type BreadcrumbLinkProps = PickPropsWithExceptions< + LinkProps, + | 'children' + | 'href' + | 'onClick' + | 'renderIcon' + | 'iconPlacement' + | 'elementRef' +> & + BreadcrumbLinkOwnProps & + OtherHTMLAttributes +const allowedProps: AllowedPropKeys = [ + 'children', + 'href', + 'iconPlacement', + 'onClick', + 'onMouseEnter', + 'renderIcon', + 'size', + 'isCurrentPage' +] + +type BreadcrumbLinkState = { + isTruncated: boolean +} + +export type { BreadcrumbLinkProps, BreadcrumbLinkState } +export { allowedProps } diff --git a/packages/ui-breadcrumb/src/Breadcrumb/README.md b/packages/ui-breadcrumb/src/Breadcrumb/v2/README.md similarity index 100% rename from packages/ui-breadcrumb/src/Breadcrumb/README.md rename to packages/ui-breadcrumb/src/Breadcrumb/v2/README.md diff --git a/packages/ui-breadcrumb/src/Breadcrumb/__tests__/Breadcrumb.test.tsx b/packages/ui-breadcrumb/src/Breadcrumb/v2/__tests__/Breadcrumb.test.tsx similarity index 100% rename from packages/ui-breadcrumb/src/Breadcrumb/__tests__/Breadcrumb.test.tsx rename to packages/ui-breadcrumb/src/Breadcrumb/v2/__tests__/Breadcrumb.test.tsx diff --git a/packages/ui-breadcrumb/src/Breadcrumb/index.tsx b/packages/ui-breadcrumb/src/Breadcrumb/v2/index.tsx similarity index 100% rename from packages/ui-breadcrumb/src/Breadcrumb/index.tsx rename to packages/ui-breadcrumb/src/Breadcrumb/v2/index.tsx diff --git a/packages/ui-breadcrumb/src/Breadcrumb/props.ts b/packages/ui-breadcrumb/src/Breadcrumb/v2/props.ts similarity index 100% rename from packages/ui-breadcrumb/src/Breadcrumb/props.ts rename to packages/ui-breadcrumb/src/Breadcrumb/v2/props.ts diff --git a/packages/ui-breadcrumb/src/Breadcrumb/styles.ts b/packages/ui-breadcrumb/src/Breadcrumb/v2/styles.ts similarity index 99% rename from packages/ui-breadcrumb/src/Breadcrumb/styles.ts rename to packages/ui-breadcrumb/src/Breadcrumb/v2/styles.ts index e16e627f5f..2ce36d91f3 100644 --- a/packages/ui-breadcrumb/src/Breadcrumb/styles.ts +++ b/packages/ui-breadcrumb/src/Breadcrumb/v2/styles.ts @@ -36,7 +36,7 @@ import type { BreadcrumbProps, BreadcrumbStyle } from './props' */ const generateStyle = ( componentTheme: NewComponentTypes['Breadcrumb'], - props: BreadcrumbProps, + props: BreadcrumbProps ): BreadcrumbStyle => { const { size } = props diff --git a/packages/ui-breadcrumb/src/index.ts b/packages/ui-breadcrumb/src/exports/a.ts similarity index 84% rename from packages/ui-breadcrumb/src/index.ts rename to packages/ui-breadcrumb/src/exports/a.ts index 36c57a2ff3..f72a2d2c00 100644 --- a/packages/ui-breadcrumb/src/index.ts +++ b/packages/ui-breadcrumb/src/exports/a.ts @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -export { Breadcrumb, BreadcrumbLink } from './Breadcrumb' +export { Breadcrumb, BreadcrumbLink } from '../Breadcrumb/v1' -export type { BreadcrumbProps } from './Breadcrumb/props' -export type { BreadcrumbLinkProps } from './Breadcrumb/BreadcrumbLink/props' +export type { BreadcrumbProps } from '../Breadcrumb/v1/props' +export type { BreadcrumbLinkProps } from '../Breadcrumb/v1/BreadcrumbLink/props' diff --git a/packages/ui-breadcrumb/src/exports/b.ts b/packages/ui-breadcrumb/src/exports/b.ts new file mode 100644 index 0000000000..472b3b85a8 --- /dev/null +++ b/packages/ui-breadcrumb/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 { Breadcrumb, BreadcrumbLink } from '../Breadcrumb/v2' + +export type { BreadcrumbProps } from '../Breadcrumb/v2/props' +export type { BreadcrumbLinkProps } from '../Breadcrumb/v2/BreadcrumbLink/props' diff --git a/packages/ui-buttons/package.json b/packages/ui-buttons/package.json index 6b20938a51..2056835d66 100644 --- a/packages/ui-buttons/package.json +++ b/packages/ui-buttons/package.json @@ -57,10 +57,28 @@ "sideEffects": false, "exports": { ".": { - "types": "./types/index.d.ts", - "import": "./es/index.js", - "require": "./lib/index.js", - "default": "./es/index.js" + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_5": { + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_6": { + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./latest": { + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" }, "./lib/*": "./lib/*", "./es/*": "./es/*", diff --git a/packages/ui-buttons/src/BaseButton/README.md b/packages/ui-buttons/src/BaseButton/v1/README.md similarity index 100% rename from packages/ui-buttons/src/BaseButton/README.md rename to packages/ui-buttons/src/BaseButton/v1/README.md diff --git a/packages/ui-buttons/src/BaseButton/__tests__/BaseButton.test.tsx b/packages/ui-buttons/src/BaseButton/v1/__tests__/BaseButton.test.tsx similarity index 100% rename from packages/ui-buttons/src/BaseButton/__tests__/BaseButton.test.tsx rename to packages/ui-buttons/src/BaseButton/v1/__tests__/BaseButton.test.tsx diff --git a/packages/ui-buttons/src/BaseButton/index.tsx b/packages/ui-buttons/src/BaseButton/v1/index.tsx similarity index 99% rename from packages/ui-buttons/src/BaseButton/index.tsx rename to packages/ui-buttons/src/BaseButton/v1/index.tsx index 103d973438..f8182c5701 100644 --- a/packages/ui-buttons/src/BaseButton/index.tsx +++ b/packages/ui-buttons/src/BaseButton/v1/index.tsx @@ -41,7 +41,7 @@ import type { ViewProps } from '@instructure/ui-view' import { isSafari } from '@instructure/ui-utils' import { combineDataCid } from '@instructure/ui-utils' -import { withStyleRework as withStyle } from '@instructure/emotion' +import { withStyleLegacy as withStyle } from '@instructure/emotion' import generateStyles from './styles' import generateComponentTheme from './theme' diff --git a/packages/ui-buttons/src/BaseButton/props.ts b/packages/ui-buttons/src/BaseButton/v1/props.ts similarity index 100% rename from packages/ui-buttons/src/BaseButton/props.ts rename to packages/ui-buttons/src/BaseButton/v1/props.ts diff --git a/packages/ui-buttons/src/BaseButton/styles.ts b/packages/ui-buttons/src/BaseButton/v1/styles.ts similarity index 100% rename from packages/ui-buttons/src/BaseButton/styles.ts rename to packages/ui-buttons/src/BaseButton/v1/styles.ts diff --git a/packages/ui-buttons/src/BaseButton/theme.ts b/packages/ui-buttons/src/BaseButton/v1/theme.ts similarity index 100% rename from packages/ui-buttons/src/BaseButton/theme.ts rename to packages/ui-buttons/src/BaseButton/v1/theme.ts diff --git a/packages/ui-buttons/src/Button/README.md b/packages/ui-buttons/src/Button/v1/README.md similarity index 100% rename from packages/ui-buttons/src/Button/README.md rename to packages/ui-buttons/src/Button/v1/README.md diff --git a/packages/ui-buttons/src/Button/__tests__/Button.test.tsx b/packages/ui-buttons/src/Button/v1/__tests__/Button.test.tsx similarity index 99% rename from packages/ui-buttons/src/Button/__tests__/Button.test.tsx rename to packages/ui-buttons/src/Button/v1/__tests__/Button.test.tsx index f27b6a95b1..77a6ee51b1 100644 --- a/packages/ui-buttons/src/Button/__tests__/Button.test.tsx +++ b/packages/ui-buttons/src/Button/v1/__tests__/Button.test.tsx @@ -28,7 +28,7 @@ import userEvent from '@testing-library/user-event' import '@testing-library/jest-dom' import { runAxeCheck } from '@instructure/ui-axe-check' -import { BaseButton } from '../../BaseButton' +import { BaseButton } from '../../../BaseButton/v1' import { Button } from '../index' describe(' + + + +``` + +#### Centered content (note the nested Flex components and use of the `wrap` property) + +```js +--- +type: example +--- + + + + An amazing thing! + + + + + We love you! + + + + We love you! + + + + We love you! + + + +
    + +
    + +
    +
    +``` + +#### Quick and dirty mobile app layout + +```js +--- +type: example +--- + + + + + App + + + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + + + + + + + + + + + + + + + + + + + + + +``` diff --git a/packages/ui-flex/src/Flex/__tests__/Flex.test.tsx b/packages/ui-flex/src/Flex/v2/__tests__/Flex.test.tsx similarity index 100% rename from packages/ui-flex/src/Flex/__tests__/Flex.test.tsx rename to packages/ui-flex/src/Flex/v2/__tests__/Flex.test.tsx diff --git a/packages/ui-flex/src/Flex/index.tsx b/packages/ui-flex/src/Flex/v2/index.tsx similarity index 100% rename from packages/ui-flex/src/Flex/index.tsx rename to packages/ui-flex/src/Flex/v2/index.tsx diff --git a/packages/ui-flex/src/Flex/props.ts b/packages/ui-flex/src/Flex/v2/props.ts similarity index 100% rename from packages/ui-flex/src/Flex/props.ts rename to packages/ui-flex/src/Flex/v2/props.ts diff --git a/packages/ui-flex/src/Flex/styles.ts b/packages/ui-flex/src/Flex/v2/styles.ts similarity index 100% rename from packages/ui-flex/src/Flex/styles.ts rename to packages/ui-flex/src/Flex/v2/styles.ts diff --git a/packages/ui-flex/src/index.ts b/packages/ui-flex/src/exports/a.ts similarity index 88% rename from packages/ui-flex/src/index.ts rename to packages/ui-flex/src/exports/a.ts index d3ad0f5143..4be8a2c7da 100644 --- a/packages/ui-flex/src/index.ts +++ b/packages/ui-flex/src/exports/a.ts @@ -22,7 +22,7 @@ * SOFTWARE. */ -export { Flex, FlexItem } from './Flex' +export { Flex, FlexItem } from '../Flex/v1' -export type { FlexProps } from './Flex/props' -export type { FlexItemProps } from './Flex/Item/props' +export type { FlexProps } from '../Flex/v1/props' +export type { FlexItemProps } from '../Flex/v1/Item/props' diff --git a/packages/ui-flex/src/exports/b.ts b/packages/ui-flex/src/exports/b.ts new file mode 100644 index 0000000000..501f4452b9 --- /dev/null +++ b/packages/ui-flex/src/exports/b.ts @@ -0,0 +1,28 @@ +/* + * 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 { Flex, FlexItem } from '../Flex/v2' + +export type { FlexProps } from '../Flex/v2/props' +export type { FlexItemProps } from '../Flex/v2/Item/props' diff --git a/packages/ui-form-field/package.json b/packages/ui-form-field/package.json index 05c6566ffe..b528021551 100644 --- a/packages/ui-form-field/package.json +++ b/packages/ui-form-field/package.json @@ -51,10 +51,28 @@ }, "exports": { ".": { - "types": "./types/index.d.ts", - "import": "./es/index.js", - "require": "./lib/index.js", - "default": "./es/index.js" + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_5": { + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_6": { + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" + }, + "./latest": { + "types": "./types/exports/b.d.ts", + "import": "./es/exports/b.js", + "require": "./lib/exports/b.js", + "default": "./es/exports/b.js" }, "./lib/*": "./lib/*", "./es/*": "./es/*", diff --git a/packages/ui-form-field/src/FormField/README.md b/packages/ui-form-field/src/FormField/v1/README.md similarity index 100% rename from packages/ui-form-field/src/FormField/README.md rename to packages/ui-form-field/src/FormField/v1/README.md diff --git a/packages/ui-form-field/src/FormField/__tests__/FormField.test.tsx b/packages/ui-form-field/src/FormField/v1/__tests__/FormField.test.tsx similarity index 100% rename from packages/ui-form-field/src/FormField/__tests__/FormField.test.tsx rename to packages/ui-form-field/src/FormField/v1/__tests__/FormField.test.tsx diff --git a/packages/ui-form-field/src/FormField/index.tsx b/packages/ui-form-field/src/FormField/v1/index.tsx similarity index 94% rename from packages/ui-form-field/src/FormField/index.tsx rename to packages/ui-form-field/src/FormField/v1/index.tsx index 6d41f551ef..821271771c 100644 --- a/packages/ui-form-field/src/FormField/index.tsx +++ b/packages/ui-form-field/src/FormField/v1/index.tsx @@ -26,10 +26,9 @@ import { Component } from 'react' import { omitProps, pickProps } from '@instructure/ui-react-utils' -import { - FormFieldLayout, - allowedProps as formFieldLayoutAllowedProps -} from '../FormFieldLayout' +import { allowedProps as formFieldLayoutAllowedProps } from '../../FormFieldLayout/v1/props' + +import { FormFieldLayout } from '../../FormFieldLayout/v2' import { allowedProps } from './props' import type { FormFieldProps } from './props' diff --git a/packages/ui-form-field/src/FormField/props.ts b/packages/ui-form-field/src/FormField/v1/props.ts similarity index 97% rename from packages/ui-form-field/src/FormField/props.ts rename to packages/ui-form-field/src/FormField/v1/props.ts index f5e3c1b491..b7a5f1eb9f 100644 --- a/packages/ui-form-field/src/FormField/props.ts +++ b/packages/ui-form-field/src/FormField/v1/props.ts @@ -25,7 +25,7 @@ import React from 'react' import type { OtherHTMLAttributes } from '@instructure/shared-types' -import type { FormMessage } from '../FormPropTypes' +import type { FormMessage } from '@instructure/ui-form-field/latest' import type { Spacing } from '@instructure/emotion' type FormFieldOwnProps = { diff --git a/packages/ui-form-field/src/FormFieldGroup/v1/README.md b/packages/ui-form-field/src/FormFieldGroup/v1/README.md new file mode 100644 index 0000000000..c721b059b9 --- /dev/null +++ b/packages/ui-form-field/src/FormFieldGroup/v1/README.md @@ -0,0 +1,114 @@ +--- +describes: FormFieldGroup +--- + +This is a helper component that is used by most of the custom form +components. Perfect if you need to wrap a complex group of form fields +(Play with the different properties inside the code editor +to see how they affect the overall look and feel). The first example +sets the `layout to inline` and sets the `vAlign to middle` and `small rowSpacing` + +```js +--- +type: example +--- + + + + + + + + + + + + +``` + +This example sets the `layout to columns` and sets the `vAlign to top` +and the `colSpacing to medium` + +```js +--- +type: example +--- + + + + + + + + + + +``` + +This example sets the `layout to stacked` and sets the `rowSpacing to large` + +```js +--- +type: example +--- + + + + + + + + + + +``` + +### Guidelines + +```js +--- +type: embed +--- + +
    + Avoid placeholder text (it creates usability problems by increasing cognitive load, low contrast, lack of screen reader compatibility, etc.) +
    +
    +``` diff --git a/packages/ui-form-field/src/FormFieldGroup/v1/index.tsx b/packages/ui-form-field/src/FormFieldGroup/v1/index.tsx new file mode 100644 index 0000000000..6f738c1f6c --- /dev/null +++ b/packages/ui-form-field/src/FormFieldGroup/v1/index.tsx @@ -0,0 +1,193 @@ +/* + * 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 { Component, Children, ReactElement, AriaAttributes } from 'react' + +import { Grid } from '@instructure/ui-grid' +import { pickProps, omitProps } from '@instructure/ui-react-utils' +import { withStyleLegacy as withStyle } from '@instructure/emotion' + +import { allowedProps as FormFieldLayoutAllowedProps } from '../../FormFieldLayout/v1/props' + +import { FormFieldLayout } from '@instructure/ui-form-field/latest' + +import generateStyle from './styles' +import generateComponentTheme from './theme' + +import { allowedProps } from './props' +import type { FormFieldGroupProps, FormFieldGroupStyleProps } from './props' + +/** +--- +category: components +--- +**/ +@withStyle(generateStyle, generateComponentTheme) +class FormFieldGroup extends Component { + static readonly componentId = 'FormFieldGroup' + + static allowedProps = allowedProps + static defaultProps = { + as: 'fieldset', + disabled: false, + rowSpacing: 'medium', + colSpacing: 'small', + vAlign: 'middle', + isGroup: true + } + + ref: Element | null = null + + handleRef = (el: Element | null) => { + const { elementRef } = this.props + + this.ref = el + + if (typeof elementRef === 'function') { + elementRef(el) + } + } + + componentDidMount() { + this.props.makeStyles?.(this.makeStylesVariables) + } + + componentDidUpdate() { + this.props.makeStyles?.(this.makeStylesVariables) + } + + get makeStylesVariables(): FormFieldGroupStyleProps { + // new form errors dont need borders + const oldInvalid = + !!this.props.messages && + this.props.messages.findIndex((message) => { + return message.type === 'error' + }) >= 0 + return { invalid: oldInvalid } + } + + get invalid() { + return ( + !!this.props.messages && + this.props.messages.findIndex((message) => { + return message.type === 'error' || message.type === 'newError' + }) >= 0 + ) + } + + renderColumns() { + return Children.map(this.props.children, (child, index) => { + return child ? ( + ).props.width + ? 'auto' + : undefined + } + key={index} + > + {child} + + ) : null + }) + } + + renderChildren() { + return ( + + {this.renderColumns()} + + ) + } + + renderFields() { + const { styles } = this.props + + return ( + + {this.renderChildren()} + + ) + } + + render() { + const { styles, makeStyles, isGroup, ...props } = this.props + // This is quite ugly, but according to ARIA spec the `aria-invalid` prop + // can only be used with certain roles see + // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-invalid#associated_roles + // `aria-invalid` is put on in FormFieldLayout because the error message + // DOM part gets there its ID. + let ariaInvalid: AriaAttributes['aria-invalid'] = undefined + if ( + this.props.role && + this.invalid && + [ + 'application', + 'checkbox', + 'combobox', + 'gridcell', + 'listbox', + 'radiogroup', + 'slider', + 'spinbutton', + 'textbox', + 'tree', + 'columnheader', + 'rowheader', + 'searchbox', + 'switch', + 'treegrid' + ].includes(this.props.role) + ) { + ariaInvalid = 'true' + } + return ( + + {this.renderFields()} + + ) + } +} + +export default FormFieldGroup +export { FormFieldGroup } diff --git a/packages/ui-form-field/src/FormFieldGroup/v1/props.ts b/packages/ui-form-field/src/FormFieldGroup/v1/props.ts new file mode 100644 index 0000000000..de022eb87b --- /dev/null +++ b/packages/ui-form-field/src/FormFieldGroup/v1/props.ts @@ -0,0 +1,103 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import type { + AsElementType, + FormFieldGroupTheme, + OtherHTMLAttributes +} from '@instructure/shared-types' +import type { WithStyleProps, ComponentStyle } from '@instructure/emotion' +import type { FormFieldLayoutOwnProps } from '../../FormFieldLayout/v2/props' +import type { FormMessage } from '@instructure/ui-form-field/latest' + +type FormFieldGroupOwnProps = { + description: React.ReactNode + /** + * the element type to render as + */ + as?: AsElementType + /** + * Array of objects with shape: `{ + * text: React.ReactNode, + * type: One of: ['newError', 'error', 'hint', 'success', 'screenreader-only'] + * }` + */ + messages?: FormMessage[] + /** + * id for the form field messages + */ + messagesId?: string + disabled?: boolean + children?: React.ReactNode + layout?: 'stacked' | 'columns' | 'inline' + rowSpacing?: 'none' | 'small' | 'medium' | 'large' + colSpacing?: 'none' | 'small' | 'medium' | 'large' + vAlign?: 'top' | 'middle' | 'bottom' + startAt?: 'small' | 'medium' | 'large' | 'x-large' | null + /** + * provides a reference to the underlying html root element + */ + elementRef?: (element: Element | null) => void +} + +type FormFieldGroupStyleProps = { + invalid: boolean +} + +type PropKeys = keyof FormFieldGroupOwnProps + +type AllowedPropKeys = Readonly> + +type FormFieldGroupProps = FormFieldGroupOwnProps & + WithStyleProps & + OtherHTMLAttributes & + // Adding other props that can be passed to FormFieldLayout, + // excluding the ones we set manually + Omit< + FormFieldLayoutOwnProps, + 'messages' | 'messagesId' | 'vAlign' | 'layout' | 'label' | 'children' + > + +type FormFieldGroupStyle = ComponentStyle<'formFieldGroup'> +const allowedProps: AllowedPropKeys = [ + 'description', + 'as', + 'messages', + 'messagesId', + 'disabled', + 'children', + 'layout', + 'rowSpacing', + 'colSpacing', + 'vAlign', + 'startAt', + 'elementRef' +] + +export type { + FormFieldGroupProps, + FormFieldGroupStyleProps, + FormFieldGroupStyle +} +export { allowedProps } diff --git a/packages/ui-form-field/src/FormFieldGroup/v1/styles.ts b/packages/ui-form-field/src/FormFieldGroup/v1/styles.ts new file mode 100644 index 0000000000..2cd9af6cc6 --- /dev/null +++ b/packages/ui-form-field/src/FormFieldGroup/v1/styles.ts @@ -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. + */ + +import type { FormFieldGroupTheme } from '@instructure/shared-types' +import type { + FormFieldGroupProps, + FormFieldGroupStyleProps, + FormFieldGroupStyle +} from './props' + +/** + * --- + * private: true + * --- + * Generates the style object from the theme and provided additional information + * @param {Object} componentTheme The theme variable object. + * @param {Object} props the props of the component, the style is applied to + * @param {Object} state the state of the component, the style is applied to + * @return {Object} The final style object, which will be used in the component + */ +const generateStyle = ( + componentTheme: FormFieldGroupTheme, + props: FormFieldGroupProps, + state: FormFieldGroupStyleProps +): FormFieldGroupStyle => { + const { disabled } = props + const { invalid } = state + + return { + formFieldGroup: { + label: 'formFieldGroup', + border: `${componentTheme.borderWidth} ${componentTheme.borderStyle} ${componentTheme.borderColor}`, + borderRadius: componentTheme.borderRadius, + display: 'block', + + ...(invalid && { + borderColor: componentTheme.errorBorderColor, + padding: componentTheme.errorFieldsPadding + }), + + ...(disabled && { + opacity: 0.6, + cursor: 'not-allowed', + pointerEvents: 'none' + }) + } + } +} + +export default generateStyle diff --git a/packages/ui-form-field/src/FormFieldGroup/v1/theme.ts b/packages/ui-form-field/src/FormFieldGroup/v1/theme.ts new file mode 100644 index 0000000000..cbd422ed8b --- /dev/null +++ b/packages/ui-form-field/src/FormFieldGroup/v1/theme.ts @@ -0,0 +1,51 @@ +/* + * 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 { Theme } from '@instructure/ui-themes' +import { FormFieldGroupTheme } from '@instructure/shared-types' + +/** + * Generates the theme object for the component from the theme and provided additional information + * @param {Object} theme The actual theme object. + * @return {Object} The final theme object with the overrides and component variables + */ +const generateComponentTheme = (theme: Theme): FormFieldGroupTheme => { + const { borders, colors, spacing } = theme + + const componentVariables: FormFieldGroupTheme = { + borderWidth: borders?.widthSmall, + borderStyle: borders?.style, + borderColor: 'transparent', + borderRadius: borders?.radiusMedium, + + errorBorderColor: colors?.contrasts?.red4570, + errorFieldsPadding: spacing?.xSmall + } + + return { + ...componentVariables + } +} + +export default generateComponentTheme diff --git a/packages/ui-form-field/src/FormFieldGroup/README.md b/packages/ui-form-field/src/FormFieldGroup/v2/README.md similarity index 100% rename from packages/ui-form-field/src/FormFieldGroup/README.md rename to packages/ui-form-field/src/FormFieldGroup/v2/README.md diff --git a/packages/ui-form-field/src/FormFieldGroup/__tests__/FormFieldGroup.test.tsx b/packages/ui-form-field/src/FormFieldGroup/v2/__tests__/FormFieldGroup.test.tsx similarity index 98% rename from packages/ui-form-field/src/FormFieldGroup/__tests__/FormFieldGroup.test.tsx rename to packages/ui-form-field/src/FormFieldGroup/v2/__tests__/FormFieldGroup.test.tsx index 24cef531eb..ab4a5fa55c 100644 --- a/packages/ui-form-field/src/FormFieldGroup/__tests__/FormFieldGroup.test.tsx +++ b/packages/ui-form-field/src/FormFieldGroup/v2/__tests__/FormFieldGroup.test.tsx @@ -28,7 +28,7 @@ import { runAxeCheck } from '@instructure/ui-axe-check' import '@testing-library/jest-dom' import { FormFieldGroup } from '../index' -import { FormMessage } from '../../FormPropTypes' +import { FormMessage } from '@instructure/ui-form-field/latest' describe('', () => { let consoleWarningMock: ReturnType diff --git a/packages/ui-form-field/src/FormFieldGroup/index.tsx b/packages/ui-form-field/src/FormFieldGroup/v2/index.tsx similarity index 95% rename from packages/ui-form-field/src/FormFieldGroup/index.tsx rename to packages/ui-form-field/src/FormFieldGroup/v2/index.tsx index 04db261efc..7fb06ce72b 100644 --- a/packages/ui-form-field/src/FormFieldGroup/index.tsx +++ b/packages/ui-form-field/src/FormFieldGroup/v2/index.tsx @@ -24,14 +24,13 @@ import { Component, Children, ReactElement, AriaAttributes } from 'react' -import { Grid } from '@instructure/ui-grid' +import { Grid } from '@instructure/ui-grid/latest' import { pickProps, omitProps } from '@instructure/ui-react-utils' import { withStyle } from '@instructure/emotion' -import { - FormFieldLayout, - allowedProps as formFieldLayoutAllowedProps -} from '../FormFieldLayout' +import { allowedProps as formFieldLayoutAllowedProps } from '../../FormFieldLayout/v2/props' + +import { FormFieldLayout } from '../../FormFieldLayout/v2' import generateStyle from './styles' diff --git a/packages/ui-form-field/src/FormFieldGroup/props.ts b/packages/ui-form-field/src/FormFieldGroup/v2/props.ts similarity index 95% rename from packages/ui-form-field/src/FormFieldGroup/props.ts rename to packages/ui-form-field/src/FormFieldGroup/v2/props.ts index 5c58865ef8..08c60d7e16 100644 --- a/packages/ui-form-field/src/FormFieldGroup/props.ts +++ b/packages/ui-form-field/src/FormFieldGroup/v2/props.ts @@ -28,8 +28,8 @@ import type { OtherHTMLAttributes } from '@instructure/shared-types' import type { WithStyleProps, ComponentStyle } from '@instructure/emotion' -import type { FormFieldLayoutOwnProps } from '../FormFieldLayout/props' -import type { FormMessage } from '../FormPropTypes' +import type { FormFieldLayoutOwnProps } from '../../FormFieldLayout/v2/props' +import type { FormMessage } from '@instructure/ui-form-field/latest' type FormFieldGroupOwnProps = { description: React.ReactNode diff --git a/packages/ui-form-field/src/FormFieldGroup/styles.ts b/packages/ui-form-field/src/FormFieldGroup/v2/styles.ts similarity index 100% rename from packages/ui-form-field/src/FormFieldGroup/styles.ts rename to packages/ui-form-field/src/FormFieldGroup/v2/styles.ts diff --git a/packages/ui-form-field/src/FormFieldLabel/index.tsx b/packages/ui-form-field/src/FormFieldLabel/v1/index.tsx similarity index 97% rename from packages/ui-form-field/src/FormFieldLabel/index.tsx rename to packages/ui-form-field/src/FormFieldLabel/v1/index.tsx index f4ac2b3d67..9fef2b322e 100644 --- a/packages/ui-form-field/src/FormFieldLabel/index.tsx +++ b/packages/ui-form-field/src/FormFieldLabel/v1/index.tsx @@ -25,7 +25,7 @@ import { Component } from 'react' import { omitProps, getElementType } from '@instructure/ui-react-utils' -import { withStyleRework as withStyle } from '@instructure/emotion' +import { withStyleLegacy as withStyle } from '@instructure/emotion' import generateStyle from './styles' import generateComponentTheme from './theme' diff --git a/packages/ui-form-field/src/FormFieldLabel/props.ts b/packages/ui-form-field/src/FormFieldLabel/v1/props.ts similarity index 100% rename from packages/ui-form-field/src/FormFieldLabel/props.ts rename to packages/ui-form-field/src/FormFieldLabel/v1/props.ts diff --git a/packages/ui-form-field/src/FormFieldLabel/styles.ts b/packages/ui-form-field/src/FormFieldLabel/v1/styles.ts similarity index 100% rename from packages/ui-form-field/src/FormFieldLabel/styles.ts rename to packages/ui-form-field/src/FormFieldLabel/v1/styles.ts diff --git a/packages/ui-form-field/src/FormFieldLabel/theme.ts b/packages/ui-form-field/src/FormFieldLabel/v1/theme.ts similarity index 100% rename from packages/ui-form-field/src/FormFieldLabel/theme.ts rename to packages/ui-form-field/src/FormFieldLabel/v1/theme.ts diff --git a/packages/ui-form-field/src/FormFieldLabel/__tests__/FormFieldLabel.test.tsx b/packages/ui-form-field/src/FormFieldLabel/v2/__tests__/FormFieldLabel.test.tsx similarity index 100% rename from packages/ui-form-field/src/FormFieldLabel/__tests__/FormFieldLabel.test.tsx rename to packages/ui-form-field/src/FormFieldLabel/v2/__tests__/FormFieldLabel.test.tsx diff --git a/packages/ui-form-field/src/FormFieldLabel/v2/index.tsx b/packages/ui-form-field/src/FormFieldLabel/v2/index.tsx new file mode 100644 index 0000000000..9fef2b322e --- /dev/null +++ b/packages/ui-form-field/src/FormFieldLabel/v2/index.tsx @@ -0,0 +1,95 @@ +/* + * 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 { Component } from 'react' + +import { omitProps, getElementType } from '@instructure/ui-react-utils' +import { withStyleLegacy as withStyle } from '@instructure/emotion' + +import generateStyle from './styles' +import generateComponentTheme from './theme' + +import { allowedProps } from './props' +import type { FormFieldLabelProps } from './props' + +/** +--- +parent: FormField +--- + +This is a helper component that is used by most of the custom form +components. In most cases it shouldn't be used directly. + +```js +--- +type: example +--- +Hello +``` + + @deprecated since version 10. This is an internal component that will be + removed in the future +**/ +@withStyle(generateStyle, generateComponentTheme) +class FormFieldLabel extends Component { + static readonly componentId = 'FormFieldLabel' + + static allowedProps = allowedProps + static defaultProps = { + as: 'span' + } as const + + ref: Element | null = null + + handleRef = (el: Element | null) => { + this.ref = el + } + + componentDidMount() { + this.props.makeStyles?.() + } + + componentDidUpdate() { + this.props.makeStyles?.() + } + + render() { + const ElementType = getElementType(FormFieldLabel, this.props) + + const { styles, children } = this.props + + return ( + + {children} + + ) + } +} + +export default FormFieldLabel +export { FormFieldLabel } diff --git a/packages/ui-form-field/src/FormFieldLabel/v2/props.ts b/packages/ui-form-field/src/FormFieldLabel/v2/props.ts new file mode 100644 index 0000000000..cfb4ac4de7 --- /dev/null +++ b/packages/ui-form-field/src/FormFieldLabel/v2/props.ts @@ -0,0 +1,49 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import type { + AsElementType, + FormFieldLabelTheme, + OtherHTMLAttributes +} from '@instructure/shared-types' +import type { WithStyleProps, ComponentStyle } from '@instructure/emotion' + +type FormFieldLabelOwnProps = { + children: React.ReactNode + as?: AsElementType +} + +type PropKeys = keyof FormFieldLabelOwnProps + +type AllowedPropKeys = Readonly> + +type FormFieldLabelProps = FormFieldLabelOwnProps & + WithStyleProps & + OtherHTMLAttributes + +type FormFieldLabelStyle = ComponentStyle<'formFieldLabel'> +const allowedProps: AllowedPropKeys = ['as', 'children'] + +export type { FormFieldLabelProps, FormFieldLabelStyle } +export { allowedProps } diff --git a/packages/ui-form-field/src/FormFieldLabel/v2/styles.ts b/packages/ui-form-field/src/FormFieldLabel/v2/styles.ts new file mode 100644 index 0000000000..4900c1a92e --- /dev/null +++ b/packages/ui-form-field/src/FormFieldLabel/v2/styles.ts @@ -0,0 +1,74 @@ +/* + * 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 { hasVisibleChildren } from '@instructure/ui-a11y-utils' + +import type { FormFieldLabelTheme } from '@instructure/shared-types' +import type { FormFieldLabelProps, FormFieldLabelStyle } from './props' + +/** + * --- + * private: true + * --- + * Generates the style object from the theme and provided additional information + * @param {Object} componentTheme The theme variable object. + * @param {Object} props the props of the component, the style is applied to + * @param {Object} state the state of the component, the style is applied to + * @return {Object} The final style object, which will be used in the component + */ +const generateStyle = ( + componentTheme: FormFieldLabelTheme, + props: FormFieldLabelProps +): FormFieldLabelStyle => { + const { children } = props + + const hasContent = hasVisibleChildren(children) + + const labelStyles = { + all: 'initial', + display: 'block', + ...(hasContent && { + color: componentTheme.color, + fontFamily: componentTheme.fontFamily, + fontWeight: componentTheme.fontWeight, + fontSize: componentTheme.fontSize, + lineHeight: componentTheme.lineHeight, + margin: 0, + textAlign: 'inherit' + }) + } + + return { + formFieldLabel: { + label: 'formFieldLabel', + ...labelStyles, + + // NOTE: needs separate groups for `:is()` and `:-webkit-any()` because of css selector group validation (see https://www.w3.org/TR/selectors-3/#grouping) + '&:is(label)': labelStyles, + '&:-webkit-any(label)': labelStyles + } + } +} + +export default generateStyle diff --git a/packages/ui-form-field/src/FormFieldLabel/v2/theme.ts b/packages/ui-form-field/src/FormFieldLabel/v2/theme.ts new file mode 100644 index 0000000000..e30f82a61d --- /dev/null +++ b/packages/ui-form-field/src/FormFieldLabel/v2/theme.ts @@ -0,0 +1,56 @@ +/* + * 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 { Theme, ThemeSpecificStyle } from '@instructure/ui-themes' +import { FormFieldLabelTheme } from '@instructure/shared-types' + +/** + * Generates the theme object for the component from the theme and provided additional information + * @param {Object} theme The actual theme object. + * @return {Object} The final theme object with the overrides and component variables + */ +const generateComponentTheme = (theme: Theme): FormFieldLabelTheme => { + const { colors, typography, key: themeName } = theme + + const themeSpecificStyle: ThemeSpecificStyle = { + canvas: { + color: theme['ic-brand-font-color-dark'] + } + } + + const componentVariables: FormFieldLabelTheme = { + color: colors?.contrasts?.grey125125, + fontFamily: typography?.fontFamily, + fontWeight: typography?.fontWeightBold, + fontSize: typography?.fontSizeMedium, + lineHeight: typography?.lineHeightFit + } + + return { + ...componentVariables, + ...themeSpecificStyle[themeName] + } +} + +export default generateComponentTheme diff --git a/packages/ui-form-field/src/FormFieldLayout/v1/index.tsx b/packages/ui-form-field/src/FormFieldLayout/v1/index.tsx new file mode 100644 index 0000000000..1f8834dc7b --- /dev/null +++ b/packages/ui-form-field/src/FormFieldLayout/v1/index.tsx @@ -0,0 +1,209 @@ +/* + * 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 { Component } from 'react' +import { hasVisibleChildren } from '@instructure/ui-a11y-utils' +import { + omitProps, + getElementType, + withDeterministicId +} from '@instructure/ui-react-utils' + +import { withStyleLegacy as withStyle } from '@instructure/emotion' +import { FormFieldMessages } from '@instructure/ui-form-field/latest' +import generateStyle from './styles' +import { allowedProps, FormFieldStyleProps } from './props' +import type { FormFieldLayoutProps } from './props' +import generateComponentTheme from './theme' + +/** +--- +parent: FormField +--- +**/ +@withDeterministicId() +@withStyle(generateStyle, generateComponentTheme) +class FormFieldLayout extends Component { + static readonly componentId = 'FormFieldLayout' + + static allowedProps = allowedProps + static defaultProps = { + inline: false, + layout: 'stacked', + as: 'label', + labelAlign: 'end' + } as const + + constructor(props: FormFieldLayoutProps) { + super(props) + this._messagesId = props.messagesId || props.deterministicId!() + this._labelId = props.deterministicId!('FormField-Label') + } + + private _messagesId: string + private _labelId: string + + ref: Element | null = null + + handleRef = (el: Element | null) => { + const { elementRef } = this.props + + this.ref = el + + if (typeof elementRef === 'function') { + elementRef(el) + } + } + + componentDidMount() { + this.props.makeStyles?.(this.makeStyleProps()) + } + + componentDidUpdate() { + this.props.makeStyles?.(this.makeStyleProps()) + } + + makeStyleProps = (): FormFieldStyleProps => { + const hasNewErrorMsgAndIsGroup = + !!this.props.messages?.find((m) => m.type === 'newError') && + !!this.props.isGroup + return { + hasMessages: this.hasMessages, + hasVisibleLabel: this.hasVisibleLabel, + // if true render error message above the controls (and below the label) + hasNewErrorMsgAndIsGroup: hasNewErrorMsgAndIsGroup + } + } + + get hasVisibleLabel() { + return this.props.label ? hasVisibleChildren(this.props.label) : false + } + + get hasMessages() { + if (!this.props.messages || this.props.messages.length == 0) { + return false + } + for (const msg of this.props.messages) { + if (msg.text) { + if (typeof msg.text === 'string') { + return msg.text.length > 0 + } + // this is more complicated (e.g. an array, a Component,...) + // but we don't try to optimize here for these cases + return true + } + } + return false + } + + get elementType() { + return getElementType(FormFieldLayout, this.props) + } + + handleInputContainerRef = (node: HTMLElement | null) => { + if (typeof this.props.inputContainerRef === 'function') { + this.props.inputContainerRef(node) + } + } + + renderLabel() { + if (this.hasVisibleLabel) { + if (this.elementType == 'fieldset') { + // `legend` has some special built in CSS, this can only be reset + // this way https://stackoverflow.com/a/65866981/319473 + return ( + + + {this.props.label} + + + ) + } + return ( + {this.props.label} + ) + } else if (this.props.label) { + if (this.elementType == 'fieldset') { + return ( + + {this.props.label} + + ) + } + // needs to be wrapped because it needs an `id` + return ( +
    + {this.props.label} +
    + ) + } else return null + } + + renderVisibleMessages() { + return this.hasMessages ? ( + + ) : null + } + + render() { + // Should be `