diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000000..b9fb8f7989 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,30 @@ +coverage: + status: + project: + default: + informational: true + patch: + default: + informational: true + +# This is the configuration on how the codecov report layout will look like in PR's +comment: + layout: 'header, diff, flags, components' + +component_management: + default_rules: + statuses: + - type: project + target: auto + threshold: 1% + branches: + - '!main' + individual_components: + - component_id: ibm-products + name: ibm-products + paths: + - ../packages/ibm-products/** + - component_id: ibm-products-web-components + name: ibm-products-web-components + paths: + - ../packages/ibm-products-web-components/** diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db36d8c09c..24b0e36786 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,7 +92,11 @@ jobs: - name: Install run: yarn - name: CI tests for c4p - run: yarn ci-check:test:c4p + run: yarn ci-check:test:c4p --collectCoverage + - name: Upload coverage reports to Codecov with GitHub Action + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: CI snapshot tests for c4p run: yarn ci-check:test:c4p:snapshot test-c4p-wc: diff --git a/README.md b/README.md index f142ddf950..912790a09d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ [![Netlify status](https://img.shields.io/netlify/e8cd9972-0fc8-4c51-a911-e9a930ca6605)](https://app.netlify.com/sites/carbon-for-ibm-products/deploys) [![GitHub Lerna version](https://img.shields.io/github/lerna-json/v/carbon-design-system/ibm-products)](https://lerna.js.org) [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](https://github.com/carbon-design-system/ibm-products/blob/master/.github/CONTRIBUTING.md) +[![codecov](https://codecov.io/gh/carbon-design-system/ibm-products/graph/badge.svg?token=TKEL92HSUK)](https://codecov.io/gh/carbon-design-system/ibm-products) ## 🚀 Getting started diff --git a/cspell.json b/cspell.json index ab24962bc0..71f1680b85 100644 --- a/cspell.json +++ b/cspell.json @@ -1,7 +1,11 @@ { "version": "0.1", "language": "en", - "dictionaries": ["contributors", "html", "packages"], + "dictionaries": [ + "contributors", + "html", + "packages" + ], "dictionaryDefinitions": [ { "name": "contributors", @@ -95,7 +99,6 @@ "disttags", "dragbar", "draggable", - "explainability", "interstitialscreenviewmodule", "draghandle", "dragmode", @@ -103,6 +106,7 @@ "editsidepanel", "emptystate", "erroremptystate", + "explainability", "exportmodal", "expressivecard", "fieldsets", @@ -116,19 +120,20 @@ "homescreen", "httperror", "httperrorother", - "interstitialscreenview", "importmodal", "inlineedit", - "jsnext", - "Menlo", - "Neue", - "noninteractive", "inlinetip", + "interstitialscreenview", + "interstitialscreenviewmodule", + "jsnext", "listbox", "loglevel", + "Menlo", "mordech", "namor", + "Neue", "nodataemptystate", + "noninteractive", "nonlinearreading", "nonselectablerows", "noreply", @@ -186,9 +191,9 @@ "toggletip", "toggletipbutton", "toolbarbutton", - "tsickle", "treegrid", "treeview", + "tsickle", "typeahead", "typeof", "unauthorizedemptystate", diff --git a/e2e/components/CreateFlows/CreateTearsheet-test.avt.e2e.js b/e2e/components/CreateFlows/CreateTearsheet-test.avt.e2e.js index 86dc21c75c..71ab0198ed 100644 --- a/e2e/components/CreateFlows/CreateTearsheet-test.avt.e2e.js +++ b/e2e/components/CreateFlows/CreateTearsheet-test.avt.e2e.js @@ -32,4 +32,107 @@ test.describe('CreateTearsheet @avt', () => { 'CreateTearsheet @avt-default-state' ); }); + + test('@avt-focus-move-properly-across-steps', async ({ page }) => { + await visitStory(page, { + component: 'CreateTearsheet', + id: 'ibm-products-patterns-create-flows-createtearsheet--multi-step-tearsheet', + globals: { + carbonTheme: 'white', + }, + }); + + const modalElement = page.locator(`.${carbon.prefix}--modal.is-visible`); + // Pressing 'Tab' key to focus on the "Open CreateTearsheet" button in the Storybook + await page.keyboard.press('Tab'); + // Pressing 'Enter' key to open the Tearsheet + await page.keyboard.press('Enter'); + + await expect(modalElement).toBeVisible(); + await modalElement.evaluate((element) => + Promise.all( + element.getAnimations().map((animation) => animation.finished) + ) + ); + + const learnMoreAnchor = page.getByText('Learn more.'); + const step1Input1 = page.locator( + '#tearsheet-multi-step-story-text-input-multi-step-1' + ); + const nextButton = page.getByText('Next'); + const backButton = page.getByText('Back'); + // Expect the Learn More link to be focused + await expect(learnMoreAnchor).toBeFocused(); + + // Switch focus to input box + await page.keyboard.press('Tab'); + // Expect the input box to be focused + await expect(step1Input1).toBeFocused(); + + // Type some text in the input field + await page.keyboard.type('H'); + // Expect the Next button to be enabled at this moment + await expect(nextButton).toBeEnabled(); + + // Switch focus to next button + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + + // Expect next button to be focused + await expect(nextButton).toBeFocused(); + + // Goto next step by pressing enter + await page.keyboard.press('Enter'); + + const step2Input1 = page.locator('#custom-step-input'); + // Expect the Step 2 input field is focused + await expect(step2Input1).toBeFocused(); + // Also confirm the Next button disabled in this step + await expect(nextButton).toBeDisabled(); + + // Type some text in the input field + await page.keyboard.type('L'); + // Expect Next button enabled now + await expect(nextButton).toBeEnabled(); + + // Switch focus to next button + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + + // Goto next step by pressing enter + await page.keyboard.press('Enter'); + + const step3Input1 = page.locator('#carbon-number'); + // Expect the first input element to be focuses + await expect(step3Input1).toBeFocused(); + + // Switch focus to next button + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + + // Goto previous step by pressing enter + await expect(backButton).toBeFocused(); + await page.keyboard.press('Enter'); + + // Expect the first element in the previous step to be focused + await expect(step2Input1).toBeFocused(); + + // Switch focus to next button + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + + // Goto previous step by pressing enter + await expect(backButton).toBeFocused(); + await page.keyboard.press('Enter'); + + // Expect the previous page first element to be focused + await expect(learnMoreAnchor).toBeFocused(); + }); }); diff --git a/packages/ibm-products-styles/src/components/SidePanel/_side-panel.scss b/packages/ibm-products-styles/src/components/SidePanel/_side-panel.scss index a826fd2871..4ac8a7d741 100644 --- a/packages/ibm-products-styles/src/components/SidePanel/_side-panel.scss +++ b/packages/ibm-products-styles/src/components/SidePanel/_side-panel.scss @@ -109,7 +109,8 @@ $action-set-block-class: #{c4p-settings.$pkg-prefix}--action-set; left: 0; border-right: 1px solid $border-subtle-02; } - &.#{$block-class}.#{$block-class}--has-slug { + &.#{$block-class}.#{$block-class}--has-slug, + &.#{$block-class}.#{$block-class}--has-ai-label { border-color: transparent; box-shadow: inset 0 -80px 70px -65px $ai-inner-shadow, 0 4px 10px 2px $ai-drop-shadow; @@ -195,13 +196,15 @@ $action-set-block-class: #{c4p-settings.$pkg-prefix}--action-set; &.#{$block-class}:has(.#{$block-class}__action-toolbar), &.#{$block-class}--has-action-toolbar, - &.#{$block-class}--has-slug { + &.#{$block-class}--has-slug, + &.#{$block-class}--has-ai-label { --#{$block-class}--title-padding-right: #{$spacing-10}; } &.#{$block-class}:has(.#{$block-class}__action-toolbar), &.#{$block-class}--has-action-toolbar { - &.#{$block-class}--has-slug { + &.#{$block-class}--has-slug, + &.#{$block-class}--has-ai-label { --#{$block-class}--title-padding-right: #{$spacing-11}; } } @@ -308,7 +311,8 @@ $action-set-block-class: #{c4p-settings.$pkg-prefix}--action-set; padding-top: $spacing-03; } - &.#{$block-class}--has-slug .#{$block-class}--scrolls { + &.#{$block-class}--has-slug .#{$block-class}--scrolls, + &.#{$block-class}--has-ai-label .#{$block-class}--scrolls { @include utilities.ai-popover-gradient('default', 0, 'layer'); box-shadow: inset 0 -80px 70px -65px $ai-inner-shadow, @@ -362,7 +366,8 @@ $action-set-block-class: #{c4p-settings.$pkg-prefix}--action-set; height: $spacing-08; } - .#{$block-class}__slug-and-close { + .#{$block-class}__slug-and-close, + .#{$block-class}__ai-label-and-close { position: absolute; z-index: 10; /* must be higher than title container border bottom */ top: 0; @@ -464,7 +469,8 @@ $action-set-block-class: #{c4p-settings.$pkg-prefix}--action-set; inset: 0; } -.#{$block-class}--has-slug + .#{$block-class}__overlay { +.#{$block-class}--has-slug + .#{$block-class}__overlay, +.#{$block-class}--has-ai-label + .#{$block-class}__overlay { /* stylelint-disable-next-line carbon/theme-token-use */ background-color: $ai-overlay; } diff --git a/packages/ibm-products/src/components/CreateTearsheet/CreateTearsheet.test.js b/packages/ibm-products/src/components/CreateTearsheet/CreateTearsheet.test.js index c9180d56aa..f8432dafdf 100644 --- a/packages/ibm-products/src/components/CreateTearsheet/CreateTearsheet.test.js +++ b/packages/ibm-products/src/components/CreateTearsheet/CreateTearsheet.test.js @@ -309,7 +309,10 @@ describe(CreateTearsheet.displayName, () => { const createTearsheetSteps = tearsheetElement.querySelector( `.${createTearsheetBlockClass}__content .${carbon.prefix}--form` ).children; - click(nextButtonElement); + act(() => { + /* fire events that update state */ + click(nextButtonElement); + }); expect( createTearsheetSteps[1].classList.contains( `.${createTearsheetBlockClass}__step__step--visible-section` diff --git a/packages/ibm-products/src/components/SidePanel/SidePanel.stories.jsx b/packages/ibm-products/src/components/SidePanel/SidePanel.stories.jsx index d3e3063591..b9b81c54fa 100644 --- a/packages/ibm-products/src/components/SidePanel/SidePanel.stories.jsx +++ b/packages/ibm-products/src/components/SidePanel/SidePanel.stories.jsx @@ -26,8 +26,8 @@ import { Header, HeaderContainer, HeaderName, - unstable__Slug as Slug, - unstable__SlugContent as SlugContent, + AILabel, + AILabelContent, } from '@carbon/react'; import { Copy, TrashCan, Settings } from '@carbon/react/icons'; @@ -219,9 +219,9 @@ const actionSets = [ [], ]; -const sampleSlug = ( - - +const sampleAILabel = ( + +

AI Explained

84%

@@ -235,8 +235,8 @@ const sampleSlug = (

Model type

Foundation model

-
-
+ + ); // eslint-disable-next-line react/prop-types @@ -435,12 +435,31 @@ docs: { }, options: [0, 1], }, + aiLabel: { + control: { + type: 'select', + labels: { + 0: 'No AI Label', + 1: 'with AI Label', + }, + default: 0, + }, + description: + 'Optional prop that is intended for any scenario where something is being generated by AI to reinforce AI transparency, accountability, and explainability at the UI level.', + options: [0, 1], + }, }, decorators: [sidePanelDecorator(renderUIShellHeader, prefix)], }; // eslint-disable-next-line react/prop-types -const SlideOverTemplate = ({ minimalContent, actions, slug, ...args }) => { +const SlideOverTemplate = ({ + minimalContent, + actions, + aiLabel, + slug, + ...args +}) => { const [open, setOpen] = useState(false); const testRef = useRef(); const buttonRef = useRef(); @@ -460,7 +479,8 @@ const SlideOverTemplate = ({ minimalContent, actions, slug, ...args }) => { onRequestClose={() => setOpen(false)} actions={actionSets[actions]} ref={testRef} - slug={slug && sampleSlug} + aiLabel={aiLabel && sampleAILabel} + slug={slug && sampleAILabel} launcherButtonRef={buttonRef} > {!minimalContent && } @@ -472,6 +492,7 @@ const SlideOverTemplate = ({ minimalContent, actions, slug, ...args }) => { const FirstElementDisabledTemplate = ({ minimalContent, actions, + aiLabel, slug, ...args }) => { @@ -494,7 +515,8 @@ const FirstElementDisabledTemplate = ({ onRequestClose={() => setOpen(false)} actions={actionSets[actions]} ref={testRef} - slug={slug && sampleSlug} + aiLabel={aiLabel && sampleAILabel} + slug={slug && sampleAILabel} launcherButtonRef={buttonRef} > {!minimalContent && ( @@ -533,7 +555,7 @@ const FirstElementDisabledTemplate = ({ }; // eslint-disable-next-line react/prop-types -const StepTemplate = ({ actions, slug, ...args }) => { +const StepTemplate = ({ actions, aiLabel, slug, ...args }) => { const [open, setOpen] = useState(false); const [currentStep, setCurrentStep] = useState(0); const buttonRef = useRef(); @@ -554,7 +576,8 @@ const StepTemplate = ({ actions, slug, ...args }) => { currentStep={currentStep} onNavigationBack={() => setCurrentStep((prev) => prev - 1)} actions={actionSets[actions]} - slug={slug && sampleSlug} + aiLabel={aiLabel && sampleAILabel} + slug={slug && sampleAILabel} launcherButtonRef={buttonRef} > { }; // eslint-disable-next-line react/prop-types -const SlideInTemplate = ({ actions, slug, ...args }) => { +const SlideInTemplate = ({ actions, aiLabel, slug, ...args }) => { const [open, setOpen] = useState(false); const buttonRef = useRef(); @@ -589,7 +612,8 @@ const SlideInTemplate = ({ actions, slug, ...args }) => { open={open} onRequestClose={() => setOpen(false)} actions={actionSets[actions]} - slug={slug && sampleSlug} + aiLabel={aiLabel && sampleAILabel} + slug={slug && sampleAILabel} launcherButtonRef={buttonRef} > @@ -649,6 +673,14 @@ PanelWithSecondStep.args = { ...defaultStoryProps, }; +export const WithAILabel = SlideOverTemplate.bind({}); +WithAILabel.args = { + includeOverlay: true, + actions: 0, + aiLabel: 1, + ...defaultStoryProps, +}; + export const SpecifyElementToHaveInitialFocus = SlideOverTemplate.bind({}); SpecifyElementToHaveInitialFocus.args = { actions: 0, diff --git a/packages/ibm-products/src/components/SidePanel/SidePanel.tsx b/packages/ibm-products/src/components/SidePanel/SidePanel.tsx index b9068dd710..020caaa8d0 100644 --- a/packages/ibm-products/src/components/SidePanel/SidePanel.tsx +++ b/packages/ibm-products/src/components/SidePanel/SidePanel.tsx @@ -170,10 +170,16 @@ type SidePanelBaseProps = { slideIn?: boolean; /** + * @deprecated please use the `aiLabel` prop * **Experimental:** Provide a `Slug` component to be rendered inside the `SidePanel` component */ slug?: ReactNode; + /** + * Optional prop that is intended for any scenario where something is being generated by AI to reinforce AI transparency, accountability, and explainability at the UI level. + */ + aiLabel?: ReactNode; + /** * Sets the subtitle text */ @@ -234,6 +240,7 @@ export let SidePanel = React.forwardRef( actionToolbarButtons, actions, + aiLabel, animateTitle = defaults.animateTitle, children, className, @@ -663,7 +670,7 @@ export let SidePanel = React.forwardRef( [`${blockClass}--right-placement`]: placement === 'right', [`${blockClass}--left-placement`]: placement === 'left', [`${blockClass}--slide-in`]: slideIn, - [`${blockClass}--has-slug`]: slug, + [`${blockClass}--has-ai-label`]: !!aiLabel || !!slug, [`${blockClass}--condensed-actions`]: condensedActions, [`${blockClass}--has-overlay`]: includeOverlay, }, @@ -697,14 +704,31 @@ export let SidePanel = React.forwardRef( ); const renderHeader = () => { - const slugCloseSize = + const aiLabelCloseSize = actions && actions.length && /l/.test(size) ? 'md' : 'sm'; - let normalizedSlug; + let normalizedAILabel; + /** + * slug is deprecated + * can remove this condition in future release + */ if (slug && slug['type']?.displayName === 'Slug') { - normalizedSlug = React.cloneElement(slug as React.ReactElement, { - // slug size is sm unless actions and size > md - size: slugCloseSize, - }); + normalizedAILabel = React.cloneElement( + slug as React.ReactElement, + { + // slug size is sm unless actions and size > md + size: aiLabelCloseSize, + } + ); + } + + if (aiLabel && aiLabel['type']?.displayName === 'AILabel') { + normalizedAILabel = React.cloneElement( + aiLabel as React.ReactElement, + { + // aiLabel size is sm unless actions and size > md + size: aiLabelCloseSize, + } + ); } return ( @@ -721,7 +745,7 @@ export let SidePanel = React.forwardRef( {currentStep > 0 && (