diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index dd32281767f3..b39e881d1664 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -24,7 +24,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 +ar uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: languages: javascript-typescript config: | @@ -32,4 +32,4 @@ jobs: - 'packages/cli/src/component/templates/**' - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 diff --git a/.github/workflows/deploy-v12-storybooks.yml b/.github/workflows/deploy-v12-storybooks.yml new file mode 100644 index 000000000000..211bc2665138 --- /dev/null +++ b/.github/workflows/deploy-v12-storybooks.yml @@ -0,0 +1,59 @@ +name: Deploy v12 storybooks to GH pages + +on: + workflow_dispatch: + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-deploy: + runs-on: ubuntu-latest-larger-disk + strategy: + fail-fast: false + matrix: + include: + - package: react + name: React + destination: v12-react-storybook + cname: v2-react.carbondesignsystem.com + - package: web-components + name: Web Components + destination: v12-web-components-storybook + cname: v3-web-components.carbondesignsystem.com + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Use Node.js version from .nvmrc + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: '.nvmrc' + - name: Install dependencies + run: yarn install --immutable + - name: Build project + run: yarn build + - name: Build static ${{ matrix.name }} v12 storybook + run: | + cd packages/${{ matrix.package }} + yarn storybook:v12:build + - name: Prepare static output + run: | + touch packages/${{ matrix.package }}/storybook-static-v12/.nojekyll + echo "${{ matrix.cname }}" > packages/${{ matrix.package }}/storybook-static-v12/CNAME + - name: Push ${{ matrix.name }} v12 storybook + uses: cpina/github-action-push-to-another-repository@55306faa4ed53b815ae49e564af8cfb359d32ae2 # v1.7.3 + env: + API_TOKEN_GITHUB: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + with: + source-directory: + 'packages/${{ matrix.package }}/storybook-static-v12' + destination-github-username: 'carbon-design-system' + destination-repository-name: ${{ matrix.destination }} + user-name: carbon-bot + user-email: carbon@us.ibm.com + target-branch: main + commit-message: 'chore(website): update v12 storybook' diff --git a/.github/workflows/mend-scan.yml b/.github/workflows/mend-scan.yml index cc25dbb1e9fc..75972d81bcb4 100644 --- a/.github/workflows/mend-scan.yml +++ b/.github/workflows/mend-scan.yml @@ -20,7 +20,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: Setup Java - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: distribution: 'temurin' java-version: '17' # Required to run the Mend Unified Agent .jar diff --git a/.github/workflows/metrics-merge-rate.yml b/.github/workflows/metrics-merge-rate.yml index c6d64ace94e1..c54f616f5ea2 100644 --- a/.github/workflows/metrics-merge-rate.yml +++ b/.github/workflows/metrics-merge-rate.yml @@ -46,7 +46,7 @@ jobs: echo "- **Merge Rate**: $MERGE_RATE%" >> $GITHUB_STEP_SUMMARY - name: Send message to Slack - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: incoming-webhook diff --git a/.github/workflows/pull-request-template.yml b/.github/workflows/pull-request-template.yml index 23f9b7c1207a..6fdfc547596c 100644 --- a/.github/workflows/pull-request-template.yml +++ b/.github/workflows/pull-request-template.yml @@ -15,8 +15,9 @@ jobs: follows-pr-template: name: follows template if: - ${{ !github.event.pull_request.draft && (github.event.action != 'edited' - || github.event.changes.body != null) }} + ${{ github.event.pull_request.user.type != 'Bot' && + !github.event.pull_request.draft && (github.event.action != 'edited' || + github.event.changes.body != null) }} runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/.github/workflows/slack-announcement.yml b/.github/workflows/slack-announcement.yml index f8a820404199..f89995ae27d6 100644 --- a/.github/workflows/slack-announcement.yml +++ b/.github/workflows/slack-announcement.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Send slack announcement id: slack - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: incoming-webhook diff --git a/.github/workflows/slack-build-notifications.yml b/.github/workflows/slack-build-notifications.yml index 451f5522e13d..bcaa4403fd1e 100644 --- a/.github/workflows/slack-build-notifications.yml +++ b/.github/workflows/slack-build-notifications.yml @@ -3,12 +3,7 @@ name: Slack Build Notifications on: workflow_run: workflows: - [ - Version, - Release, - Deploy React storybook to GitHub Pages, - promote, - ] + [ci, Version, Release, Deploy React storybook to GitHub Pages, promote] types: [completed] branches: [main] @@ -23,7 +18,7 @@ jobs: steps: - name: Send custom JSON data to Slack id: slack-success - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: incoming-webhook @@ -40,7 +35,7 @@ jobs: steps: - name: Send custom JSON data to Slack id: slack-failure - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: incoming-webhook diff --git a/.github/workflows/slack-office-hours-design.yml b/.github/workflows/slack-office-hours-design.yml index 710faec934ba..9daebb8356ec 100644 --- a/.github/workflows/slack-office-hours-design.yml +++ b/.github/workflows/slack-office-hours-design.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Send custom JSON data to Slack workflow id: slack - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: incoming-webhook diff --git a/.github/workflows/slack-office-hours-dev.yml b/.github/workflows/slack-office-hours-dev.yml index 51a796d5aadf..156317056357 100644 --- a/.github/workflows/slack-office-hours-dev.yml +++ b/.github/workflows/slack-office-hours-dev.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Send custom JSON data to Slack workflow id: slack - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: incoming-webhook @@ -105,7 +105,7 @@ jobs: steps: - name: Send custom JSON data to Slack workflow id: slack - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: incoming-webhook @@ -197,7 +197,7 @@ jobs: steps: - name: Send custom JSON data to Slack workflow id: slack - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: incoming-webhook diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a80cdebc8822..a1e7716ed150 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -7,7 +7,7 @@ jobs: comment: runs-on: ubuntu-latest steps: - - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 + - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0 with: any-of-issue-labels: "status: waiting for author's response 💬,status: needs more info 🤷‍♀️" diff --git a/.gitignore b/.gitignore index 677d0964d56a..cc6b691116e5 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,6 @@ blob-report **/.yarn/cache/ **/.yarn/build-state.yml **/.yarn/install-state.gz + +# Generated DTCG color palette (built from @carbon/colors at build time) +packages/themes/src/dtcg/color-palette.json diff --git a/config/typescript-config-carbon/package.json b/config/typescript-config-carbon/package.json index bcb86535a221..b54ed4ec7119 100644 --- a/config/typescript-config-carbon/package.json +++ b/config/typescript-config-carbon/package.json @@ -1,7 +1,7 @@ { "name": "typescript-config-carbon", "description": "TypeScript configuration for Carbon", - "version": "0.10.0", + "version": "0.11.0", "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/docs/feature-flags.md b/docs/feature-flags.md index 0199e7f59200..24ee835b8fde 100644 --- a/docs/feature-flags.md +++ b/docs/feature-flags.md @@ -20,37 +20,38 @@ test and give us feedback on. They're generally stable and unlikely to change but can change based on your feedback. Flags prefixed with `enable-v12-*` are stable and won't change. They will be -marked as `true` or "on" by default in the next major version, v12. +marked as `true` or "on" by default in the next major version, v12. Use +`enable-v12-release` to enable all `enable-v12-*` flags. For more details on this naming convention, see the [section below](#feature-flag-naming-convention). Unless otherwise specified, flags are `false` by default. -| Flag | Description | Availability | Codemod | -| -------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `enable-dialog-element` | Enable components to utilize the native dialog element | React, Sass | | -| `enable-enhanced-file-uploader` | Enable enhanced FileUploader callbacks with richer data and expanded triggers. | React | | -| `enable-focus-wrap-without-sentinels` | Enable the new focus wrap behavior that doesn't use sentinel nodes | React | | -| `enable-presence` | Enable components to remain unmounted in closed state and mount in open state. | React, Sass | | -| `enable-tile-contrast` | Enable the improved styling for tiles that provides better contrast | Sass | | -| `enable-treeview-controllable` | Enable the new TreeView controllable API | React | | -| `enable-v12-dynamic-floating-styles` | Enable dynamic setting of floating styles for components like Popover, Tooltip, etc. | React | | -| `enable-v12-overflowmenu` | Enable the use of the v12 OverflowMenu leveraging the Menu subcomponents | React | [enable-v12-overflowmenu](https://github.com/carbon-design-system/carbon/tree/main/packages/upgrade#enable-v12-overflowmenu) | -| `enable-v12-structured-list-visible-icons` | Enable icon components within StructuredList to always be visible | Sass | [enable-v12-structured-list-visible-icons](https://github.com/carbon-design-system/carbon/tree/main/packages/upgrade#enable-v12-structured-list-visible-icons) | -| `enable-v12-tile-default-icons` | Enable default icons for Tile components | React | [enable-v12-tile-default-icons](https://github.com/carbon-design-system/carbon/tree/main/packages/upgrade#enable-v12-tile-default-icons) | -| `enable-v12-tile-radio-icons` | Enable rendering of updated radio icon in the tile components | React, Sass | [enable-v12-tile-radio-icons](https://github.com/carbon-design-system/carbon/tree/main/packages/upgrade#enable-v12-tile-radio-icons) | -| `enable-v12-toggle-reduced-label-spacing` | Enable a reduced spacing between the toggle control and its label | Sass | | -| **Deprecated flags** | | | | -| `enable-experimental-tile-contrast` | Deprecated, use `enable-tile-contrast` instead | Sass | | -| `enable-experimental-focus-wrap-without-sentinels` | Deprecated, use `enable-focus-wrap-without-sentinels` instead | React | | +| Flag | Description | Availability | Codemod | +| -------------------------------------------------- | ------------------------------------------------------------------------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `enable-dialog-element` | Enable components to utilize the native dialog element | React, Sass | | +| `enable-enhanced-file-uploader` | Enable enhanced FileUploader callbacks with richer data and expanded triggers. | React | | +| `enable-focus-wrap-without-sentinels` | Enable the new focus wrap behavior that doesn't use sentinel nodes | React | | +| `enable-presence` | Enable components to remain unmounted in closed state and mount in open state. | React, Sass | | +| `enable-tile-contrast` | Enable the improved styling for tiles that provides better contrast | Sass | | +| `enable-treeview-controllable` | Enable the new TreeView controllable API | React | | +| `enable-v12-dynamic-floating-styles` | Enable dynamic setting of floating styles for components like Popover, Tooltip, etc. | React | | +| `enable-v12-overflowmenu` | Enable the use of the v12 OverflowMenu leveraging the Menu subcomponents | React | [enable-v12-overflowmenu](https://github.com/carbon-design-system/carbon/tree/main/packages/upgrade#enable-v12-overflowmenu) | +| `enable-v12-release` | Enable all v12 feature flags | React, Sass, Web Components | | +| `enable-v12-structured-list-visible-icons` | Enable icon components within StructuredList to always be visible | Sass | [enable-v12-structured-list-visible-icons](https://github.com/carbon-design-system/carbon/tree/main/packages/upgrade#enable-v12-structured-list-visible-icons) | +| `enable-v12-tile-default-icons` | Enable default icons for Tile components | React | [enable-v12-tile-default-icons](https://github.com/carbon-design-system/carbon/tree/main/packages/upgrade#enable-v12-tile-default-icons) | +| `enable-v12-tile-radio-icons` | Enable rendering of updated radio icon in the tile components | React, Sass | [enable-v12-tile-radio-icons](https://github.com/carbon-design-system/carbon/tree/main/packages/upgrade#enable-v12-tile-radio-icons) | +| `enable-v12-toggle-reduced-label-spacing` | Enable a reduced spacing between the toggle control and its label | Sass | | +| **Deprecated flags** | | | | +| `enable-experimental-tile-contrast` | Deprecated, use `enable-tile-contrast` instead | Sass | | +| `enable-experimental-focus-wrap-without-sentinels` | Deprecated, use `enable-focus-wrap-without-sentinels` instead | React | | ## Documentation -In addition to this document, within each storybook there are "Feature flags" -folders for some components. Docs page(s) within this folder cover the flag -specifics and usage. The stories within in the folder showcase the impact of -when the flag(s) are turned on. +In addition to this document, some components include `Feature flags` folders +within Storybook. The docs pages in those folders cover flag specifics and +usage. The stories in those folders showcase the impact of turning the flags on. ## Using Codemods for Migration @@ -99,7 +100,7 @@ All feature flags follow a prefix naming convention that indicate status. - Are generally stable and unlikely to change but may change based on user feedback - May require some manual migration or code changes within your project -- Are documented in storybook +- Are documented in Storybook - May not be documented on https://www.carbondesignsystem.com - Need user feedback to ensure we've met all concerns relating to this feature diff --git a/docs/release-schedule.md b/docs/release-schedule.md index 2c2ad17e477e..e2a45fadbaaf 100644 --- a/docs/release-schedule.md +++ b/docs/release-schedule.md @@ -78,6 +78,34 @@ included. The term 'supported release lines' will be used to refer to all release lines that are not End-of-Life. +### Long-term support + +Long-term support, or LTS, is an additional designation given to selected major +releases that require an extended support window. Not every major release is +expected to become an LTS release. + +LTS is intended for consumers whose delivery models do not allow them to +regularly upgrade to the latest Carbon major. This is particularly important for +on-premises products and products that support on-premises customers. These +projects may have a concentrated period of feature development followed by a +long maintenance period during which upgrading Carbon would be difficult or +impractical. + +An LTS release receives the same types of fixes as a Maintenance release, +including security patches, critical bug fixes, and requested non-critical +bugfixes when there is a demonstrated need. The primary difference is the length +and intent of the support commitment. + +Rather than assigning an arbitrary end date when an LTS release is designated, +the Carbon team intends to maintain its release infrastructure and consider +requested fixes for as long as supported products and customers have an +established business need for that release line. + +LTS does not include ongoing feature development and should not be treated as an +alternative to adopting the current Active release when an upgrade is practical. +Its purpose is to preserve the stability and viability of products that cannot +follow Carbon's standard release cadence. + ## Accessibility Active release lines are tested using IBMa's `accessibility-checker` in a real @@ -111,9 +139,15 @@ will need to patch yourself or submit a fix by ## Assets managed under this release schedule This plan covers the design and development assets under maintenance of the -Carbon Design System core team. This includes the `@carbon/react` and -`@carbon/styles` packages, as well as all other packages within the -[`carbon` monorepo](https://github.com/carbon-design-system/carbon). +Carbon Design System core team, including: + +- `@carbon/react` +- `@carbon/web-components` +- `@carbon/styles` +- All other packages within the + [`carbon` monorepo](https://github.com/carbon-design-system/carbon) +- `@carbon/ibm-products` +- `@carbon/ibm-products-web-components` This plan also includes all design guidance and design kit assets (figma, etc.) present in the diff --git a/docs/working-with-v12.md b/docs/working-with-v12.md new file mode 100644 index 000000000000..7b1c701440a5 --- /dev/null +++ b/docs/working-with-v12.md @@ -0,0 +1,314 @@ +# Working with v12 + +This guide is for contributors working on the v12 preview surface in this +repository. v12 work is still developed from within the v11 codebase, so the +important rule is to keep the preview explicit: v11 remains the default +experience, and v12 behavior is enabled through the v12 release flag in the v12 +Storybook environment. + +## v12 Storybooks + +The v12 Storybooks are run separately from the existing ones and use different +ports so they can be open at the same time. + +```sh +cd packages/react +yarn storybook:v12 +``` + +```sh +cd packages/web-components +yarn storybook:v12 +``` + +| | v11 | v12 | +| ----------------------- | ------- | ------- | +| packages/react | `:3011` | `:3012` | +| packages/web-components | `:6011` | `:6012` | + +## The v12 release flag + +The root v12 flag is `enable-v12-release`. It is defined in +`packages/feature-flags/feature-flags.yml` and defaults to `false` so normal v11 +usage stays unchanged. + +When `enable-v12-release` is enabled, every other flag whose name starts with +`enable-v12-` is treated as enabled. That override lives in the shared feature +flag package, not in one component or Storybook layer, because the behavior has +to be consistent across JavaScript scopes and Sass: + +- `packages/feature-flags/src/FeatureFlagScope.ts` handles runtime flag checks. +- `packages/feature-flags/index.scss` handles Sass `enabled(...)` checks. +- `packages/react/src/components/FeatureFlags/index.tsx` exposes + `enableV12Release` for React trees. +- `packages/web-components/src/components/feature-flags/index.ts` recognizes the + `enable-v12-release` attribute for Web Components. + +Prefer enabling `enable-v12-release` when you want the v12 preview as a whole. +Use individual `enable-v12-*` flags only when a story, test, or migration path +needs to isolate one behavior. + +## React usage + +The React v12 Storybook has the `enable-v12-release` flag turned on globally for +every story. + +A global decorator is configured in `.storybook-v12/preview.js` that wraps every +story with ``. That file also calls +`enable('enable-v12-release')` so direct calls to the shared feature flag scope +see the same v12 state. + + + +> [!IMPORTANT] +> You do not need to wrap stories with `` +> +> Component-level `` wrappers are still useful though, especially +> when a story demonstrates a non-v12 flag or a focused migration case. + + + +## Web Components usage + +The Web Components v12 Storybook has the `enable-v12-release` flag turned on +globally for every story. + +A global decorator is configured in `.storybook-v12/preview.js` that wraps every +story with ``. That file also calls +`enable('enable-v12-release')` so direct calls to the shared feature flag scope +see the same v12 state. + + + +> [!IMPORTANT] +> You do not need to wrap stories with `` +> +> Component-level `` wrappers are still useful though, especially +> when a story demonstrates a non-v12 flag or a focused migration case. + + + +Some Web Components Sass is selected through attributes on the component itself, +for example `cds-toggle[enable-v12-toggle-reduced-label-spacing]`. The +`feature-flags` element syncs enabled related flags onto nested components so a +root v12 decorator is enough for those styles to apply. Keep that mapping in +`FeatureFlagsElement.flagComponentMap` up to date when adding attribute-driven +Web Components flags. + +## Sass usage + +For v12 Storybook builds, the `.storybook-v12/main.ts` files add this Sass +configuration through Vite: + +```scss +@use '@carbon/styles/scss/feature-flags' with ( + $feature-flags: ( + 'enable-v12-release': true, + ) +); +``` + +That means component styles compiled in the v12 Storybook see all `enable-v12-*` +Sass flags as enabled. Keep this at the Storybook config level instead of +sprinkling v12 Sass setup through individual stories; the goal is for v12 +Storybook to behave like the future default build. + +Avoid configuring the same Sass module more than once in one compilation unit. +If a story needs one-off Sass to demonstrate a flag, check whether it still +makes sense after `enable-v12-release` is already on by default. + +## Feature flag stories in v12 Storybook + +The v12 Storybooks do not show separate `Feature Flag` sections for stories that +are already committed to the next major. Instead, `enable-v12-*` feature flag +stories are merged into the normal component story section. + +Feature flag stories for non-v12 flags are intentionally left in their +`Feature Flag` sections. Those flags are still experimental or opt-in in v12, so +the v12 Storybook should not make them look like defaults. + +This is generated by `tasks/prepare-v12-storybook.mjs`, which runs before v12 +Storybook dev and build commands. The script: + +- finds React `*.featureflag.stories.*` files and Web Components + `*.feature-flag.stories.*` files in `src/` and `.storybook-v12/stories/`; +- only promotes `src/` feature-flag stories that reference an `enable-v12-*` + flag; +- pairs them with the matching base `*.stories.*` file when one exists; +- replaces matching base story exports with the feature-flag export; +- keeps base-only stories unchanged; +- moves feature-only stories into the normal component section; +- prefixes feature-only story names with `🚀`; +- preserves feature-story decorators and parameters for moved stories. + +The generated files live under `.storybook-v12/generated/` and are intentionally +ignored by Git. Do not edit them. Change the original story files or the +generator instead. + +When the generator runs, it prints the generated wrapper directory relative to +the package command and then lists the story names grouped under each generated +wrapper file. That output is the quickest way to check whether a new story was +promoted or intentionally left in its feature flag section. + +The merge is export-name based. If a feature story is intended to replace an +existing story, use the same export name in both files. If the export name is +different, the v12 Storybook treats it as an additional story and marks it with +`🚀`. + +## Authoring stories for v11, v12, or both + +Most stories should continue to live in the normal `*.stories.*` file. Reach for +a feature-flag story file only when the v12 version needs different markup, +different args, a different wrapper, or a story that should not appear in the +v11 Storybook. + +### Stories for both v11 and v12 + +Add the story to the normal component story file: + +- React: `Component.stories.js` +- Web Components: `component.stories.ts` + +Use this when the story should appear in both Storybooks and the v12 release +flag does not require a different example. In the v12 Storybook, the root v12 +decorator and Sass setup will still be active, so the same story may render with +v12 behavior without needing a duplicate story. + +### v11-only stories + +Add the story only to the normal component story file. Do not add a matching +export to the feature-flag story file. + +This keeps the story in v11 and, by default, also keeps it in v12. If the story +should truly disappear from v12, it needs to be replaced by a feature-flag story +with the same export name that represents the v12 default. Prefer replacement +over hiding; v12 Storybook should show the future default surface, not a +manually curated subset. + +### v12 replacements for existing v11 stories + +Add the v12 version to the matching feature-flag story file and use the same +export name as the v11 story. The story file must reference an `enable-v12-*` +flag for the v12 generator to promote it into the normal component section. + +For example, if the v11 file exports `Default`, the feature-flag file should +also export `Default`. The v12 Storybook generator will use the feature-flag +`Default` in place of the base `Default`, while the v11 Storybook continues to +use the original story. + +Use this when a v12 flag changes the recommended default markup, args, expected +interaction, or visual treatment. Keeping the export name the same is the signal +that this is a replacement, not a new story. + +Do not use this path for non-v12 flags. A story for a flag such as +`enable-whatever-flag` should stay in the `Feature Flag` section in both v11 and +v12 because that behavior is still opt-in. + +### v12-only stories + +Add the story to a feature-flag story file under `.storybook-v12/stories/` and +use a new export name that does not exist in a base story file. + +The v12 Storybook generator will move it into the normal component section and +prefix the display name with `🚀`. This is for examples that are genuinely new +in v12, such as a new component state, API shape, or migration-specific example +that has no v11 equivalent. + +Use the `src/` feature-flag story files for stories that should still be visible +in the v11 feature flag sections and are backed by an `enable-v12-*` flag. Use +`.storybook-v12/stories/` when the story should only exist in the v12 Storybook +or when there is no `src/` feature-flag story to promote. + +Do not rely on the JavaScript export name to include the emoji. The generator +sets the display name through Storybook story metadata so the source remains a +valid, normal export. + +#### A note on VRT + +The v12 Storybooks are preview environments. They are not currently enabled for +Chromatic VRT. Keep visual checks focused on the existing default Storybooks +until v12 ships and these v12 Storybooks become the default package Storybooks; +at that point they should take over the normal Chromatic coverage. + +### Deprecated stories + +Inherited v11 stories whose top-level title is `Deprecated` are filtered out of +the v12 Storybooks. Those stories describe APIs that are deprecated before v12, +so carrying them forward would make the v12 surface look older than it is. + +The v12 Storybooks can still have a `Deprecated` section for APIs that are +introduced in, or still present in, v12 and planned for removal later. Put those +stories under `.storybook-v12/deprecated/` and use a `Deprecated/...` title. The +inherited-story filter intentionally does not apply to local `.storybook-v12` +stories. + +## Sidebar order + +The v12 Storybooks keep the same top-level sidebar order as v11: + +1. Getting Started +2. Components +3. Deprecated +4. Elements +5. Helpers +6. Hooks +7. Layout +8. Preview + +For example, the current `Elements/Motion` placeholder is authored under +`.storybook-v12/stories/` so it only appears in the v12 Storybooks and can +validate the `🚀` prefix behavior under the Elements section. + +## Storybook identity + +The v12 Storybooks use hardcoded package next-version titles in +`packages/*/.storybook-v12/theme.js` and the manager head files: + +- `@carbon/react v2.x` +- `@carbon/web-components v3.x` + +They also add a toolbar item named `🚀 enable-v12-release` with the flag icon. +The toolbar item is only a visual indicator; the actual flag state comes from +the v12 preview decorator and Sass setup. + +Keep that indicator non-interactive unless there is a clear need to support +toggling v12 off inside the v12 Storybook. The main reason it exists is to make +parallel v11 and v12 Storybook windows hard to confuse. + +The v12 welcome pages are package-specific and use the React Storybook welcome +layout. Web Components keeps its longer introductory content in +`Getting Started/Getting started` so `Getting Started/Welcome` can stay a quick +package/version landing page. + +## Publishing + +`.github/workflows/deploy-v12-storybooks.yml` publishes the v12 Storybooks on +every push to `main` and can also be run manually. It follows the existing Web +Components Storybook publish flow: + +1. install dependencies; +2. build the project; +3. build the package v12 Storybook; +4. add `.nojekyll`; +5. push the static output to a separate repository. + +The workflow only publishes static Storybook output. GitHub Pages repository +settings are intentionally not handled here. + +## Things to watch out for + +- Do not turn `enable-v12-release` on by default in the root YAML until v12 is + actually the default release behavior. +- Do not duplicate v12 defaults in every component story. Use the root v12 + Storybook decorator and Sass setup. +- Do not commit `.storybook-v12/generated/` output. +- Do not remove feature-story decorators blindly. Some feature-flag stories + still enable non-v12 flags or carry documentation-specific parameters. +- Keep Web Components attribute syncing in mind for Sass selectors that depend + on host attributes. +- Do not treat the v12 Storybooks as VRT coverage yet. They are intentionally + outside Chromatic until they replace the current default Storybooks for v12. +- When adding a new `enable-v12-*` flag, make sure it is represented anywhere + the package needs it: root YAML, React props if applicable, Web Components + attributes if applicable, Sass defaults if applicable, docs, telemetry, and + tests. diff --git a/e2e/components/Popover/Popover-test.avt.e2e.js b/e2e/components/Popover/Popover-test.avt.e2e.js index 2054e04e1b67..938a4c1de9ac 100644 --- a/e2e/components/Popover/Popover-test.avt.e2e.js +++ b/e2e/components/Popover/Popover-test.avt.e2e.js @@ -64,6 +64,11 @@ test.describe('@avt Popover', () => { await page.keyboard.press('Space'); await expect(page.locator('.cds--popover--open')).toBeVisible(); await page.keyboard.press('Escape'); + + // should not close on Esc, focus is not inside the content + await expect(page.locator('.cds--popover--open')).toBeVisible(); + + await page.keyboard.press('Space'); await expect(page.locator('.cds--popover--open')).toBeHidden(); // Testing right popover diff --git a/e2e/icons-react/__snapshots__/PublicAPI-test.js.snap b/e2e/icons-react/__snapshots__/PublicAPI-test.js.snap index b2f1281d68af..8f45d7ecd5c0 100644 --- a/e2e/icons-react/__snapshots__/PublicAPI-test.js.snap +++ b/e2e/icons-react/__snapshots__/PublicAPI-test.js.snap @@ -15,6 +15,7 @@ exports[`@carbon/icons-react should not update exports without a semver change 1 "ActionDefinition", "ActionUsage", "Activity", + "ActivityTask", "AdapterNotification", "Add", "AddAlt", @@ -438,6 +439,7 @@ exports[`@carbon/icons-react should not update exports without a semver change 1 "CircleStroke", "ClassicBatch", "Classification", + "ClassificationGroups", "ClassifierLanguage", "Clean", "Close", @@ -631,6 +633,7 @@ exports[`@carbon/icons-react should not update exports without a semver change 1 "DataVolumeAlt", "Data_1", "Data_2", + "DatabaseBackups", "DatabaseDatastax", "DatabaseElastic", "DatabaseEnterpriseDb2", @@ -819,6 +822,7 @@ exports[`@carbon/icons-react should not update exports without a semver change 1 "EmissionsManagement", "EnableStep", "Encryption", + "EndToEndMonitoring", "EnergyRenewable", "EnergyReport", "EnergyWaste", @@ -871,6 +875,7 @@ exports[`@carbon/icons-react should not update exports without a semver change 1 "FaceWinkFilled", "FacilityGroup", "FacilityGroupAlternate", + "FacilityGroups", "Factor", "Fade", "Favorite", @@ -969,7 +974,12 @@ exports[`@carbon/icons-react should not update exports without a semver change 1 "GasStationFilled", "Gateway", "GatewayApi", + "GatewayExclusive", + "GatewayInclusive", + "GatewayInclusiveAlt", "GatewayMail", + "GatewayParallel", + "GatewayParallelAlt", "GatewayPublic", "GatewaySecurity", "GatewayUserAccess", @@ -1432,6 +1442,7 @@ exports[`@carbon/icons-react should not update exports without a semver change 1 "LocationStar", "LocationStarFilled", "LocationSupplier", + "LocationSwitch", "Locked", "LockedAndBlocked", "LogicalPartition", @@ -1538,6 +1549,7 @@ exports[`@carbon/icons-react should not update exports without a semver change 1 "MinimapOn", "Minimize", "MinusPlus", + "MissingDataWarning", "Misuse", "MisuseOutline", "MixedRainHail", @@ -1806,6 +1818,7 @@ exports[`@carbon/icons-react should not update exports without a semver change 1 "PortOutput", "PortUsage", "Portfolio", + "PortfolioGroups", "PortfolioManagement", "Power", "PowerEnterprisePoolsMeteredCapacityIntegration", @@ -1829,6 +1842,7 @@ exports[`@carbon/icons-react should not update exports without a semver change 1 "Process", "ProcessAutomate", "Product", + "ProductCategories", "ProductionEnvironment", "ProductionService", "Program", @@ -1842,6 +1856,7 @@ exports[`@carbon/icons-react should not update exports without a semver change 1 "PullRequest", "PunctuationCheck", "Purchase", + "PurchasingOrganizations", "QBarrier", "QBlochSphere", "QCU1", @@ -2180,6 +2195,7 @@ exports[`@carbon/icons-react should not update exports without a semver change 1 "SubjectDefinition", "SubjectUsage", "SubnetAclRules", + "Subprocess", "Subsetting", "Subtract", "SubtractAlt", @@ -2319,6 +2335,8 @@ exports[`@carbon/icons-react should not update exports without a semver change 1 "Ticket", "Tides", "Tif", + "Tilde", + "TildeLarge", "Time", "TimeFilled", "TimePlot", @@ -2400,6 +2418,7 @@ exports[`@carbon/icons-react should not update exports without a semver change 1 "TypePattern", "Types", "Umbrella", + "UnassignedWorkOrder", "Undefined", "UndefinedFilled", "Undo", diff --git a/e2e/icons-vue/__snapshots__/PublicAPI-test.js.snap b/e2e/icons-vue/__snapshots__/PublicAPI-test.js.snap index 3bd03e5893fa..f1b6662bc374 100644 --- a/e2e/icons-vue/__snapshots__/PublicAPI-test.js.snap +++ b/e2e/icons-vue/__snapshots__/PublicAPI-test.js.snap @@ -54,6 +54,10 @@ exports[`@carbon/icons-vue should not update exports without a semver change 1`] "Activity20", "Activity24", "Activity32", + "ActivityTask16", + "ActivityTask20", + "ActivityTask24", + "ActivityTask32", "AdapterNotification16", "AdapterNotification20", "AdapterNotification24", @@ -1743,6 +1747,10 @@ exports[`@carbon/icons-vue should not update exports without a semver change 1`] "Classification20", "Classification24", "Classification32", + "ClassificationGroups16", + "ClassificationGroups20", + "ClassificationGroups24", + "ClassificationGroups32", "ClassifierLanguage16", "ClassifierLanguage20", "ClassifierLanguage24", @@ -2509,6 +2517,10 @@ exports[`@carbon/icons-vue should not update exports without a semver change 1`] "Data_220", "Data_224", "Data_232", + "DatabaseBackups16", + "DatabaseBackups20", + "DatabaseBackups24", + "DatabaseBackups32", "DatabaseDatastax16", "DatabaseDatastax20", "DatabaseDatastax24", @@ -3258,6 +3270,10 @@ exports[`@carbon/icons-vue should not update exports without a semver change 1`] "Encryption20", "Encryption24", "Encryption32", + "EndToEndMonitoring16", + "EndToEndMonitoring20", + "EndToEndMonitoring24", + "EndToEndMonitoring32", "EnergyRenewable16", "EnergyRenewable20", "EnergyRenewable24", @@ -3466,6 +3482,10 @@ exports[`@carbon/icons-vue should not update exports without a semver change 1`] "FacilityGroupAlternate20", "FacilityGroupAlternate24", "FacilityGroupAlternate32", + "FacilityGroups16", + "FacilityGroups20", + "FacilityGroups24", + "FacilityGroups32", "Factor16", "Factor20", "Factor24", @@ -3858,10 +3878,30 @@ exports[`@carbon/icons-vue should not update exports without a semver change 1`] "GatewayApi20", "GatewayApi24", "GatewayApi32", + "GatewayExclusive16", + "GatewayExclusive20", + "GatewayExclusive24", + "GatewayExclusive32", + "GatewayInclusive16", + "GatewayInclusive20", + "GatewayInclusive24", + "GatewayInclusive32", + "GatewayInclusiveAlt16", + "GatewayInclusiveAlt20", + "GatewayInclusiveAlt24", + "GatewayInclusiveAlt32", "GatewayMail16", "GatewayMail20", "GatewayMail24", "GatewayMail32", + "GatewayParallel16", + "GatewayParallel20", + "GatewayParallel24", + "GatewayParallel32", + "GatewayParallelAlt16", + "GatewayParallelAlt20", + "GatewayParallelAlt24", + "GatewayParallelAlt32", "GatewayPublic16", "GatewayPublic20", "GatewayPublic24", @@ -5707,6 +5747,10 @@ exports[`@carbon/icons-vue should not update exports without a semver change 1`] "LocationSupplier20", "LocationSupplier24", "LocationSupplier32", + "LocationSwitch16", + "LocationSwitch20", + "LocationSwitch24", + "LocationSwitch32", "Locked16", "Locked20", "Locked24", @@ -6128,6 +6172,10 @@ exports[`@carbon/icons-vue should not update exports without a semver change 1`] "MinusPlus20", "MinusPlus24", "MinusPlus32", + "MissingDataWarning16", + "MissingDataWarning20", + "MissingDataWarning24", + "MissingDataWarning32", "Misuse16", "Misuse20", "Misuse24", @@ -7200,6 +7248,10 @@ exports[`@carbon/icons-vue should not update exports without a semver change 1`] "Portfolio20", "Portfolio24", "Portfolio32", + "PortfolioGroups16", + "PortfolioGroups20", + "PortfolioGroups24", + "PortfolioGroups32", "PortfolioManagement16", "PortfolioManagement20", "PortfolioManagement24", @@ -7292,6 +7344,10 @@ exports[`@carbon/icons-vue should not update exports without a semver change 1`] "Product20", "Product24", "Product32", + "ProductCategories16", + "ProductCategories20", + "ProductCategories24", + "ProductCategories32", "ProductionEnvironment16", "ProductionEnvironment20", "ProductionEnvironment24", @@ -7344,6 +7400,10 @@ exports[`@carbon/icons-vue should not update exports without a semver change 1`] "Purchase20", "Purchase24", "Purchase32", + "PurchasingOrganizations16", + "PurchasingOrganizations20", + "PurchasingOrganizations24", + "PurchasingOrganizations32", "QBarrier16", "QBarrier20", "QBarrier24", @@ -8693,6 +8753,10 @@ exports[`@carbon/icons-vue should not update exports without a semver change 1`] "SubnetAclRules20", "SubnetAclRules24", "SubnetAclRules32", + "Subprocess16", + "Subprocess20", + "Subprocess24", + "Subprocess32", "Subsetting16", "Subsetting20", "Subsetting24", @@ -9249,6 +9313,14 @@ exports[`@carbon/icons-vue should not update exports without a semver change 1`] "Tif20", "Tif24", "Tif32", + "Tilde16", + "Tilde20", + "Tilde24", + "Tilde32", + "TildeLarge16", + "TildeLarge20", + "TildeLarge24", + "TildeLarge32", "Time16", "Time20", "Time24", @@ -9573,6 +9645,10 @@ exports[`@carbon/icons-vue should not update exports without a semver change 1`] "Umbrella20", "Umbrella24", "Umbrella32", + "UnassignedWorkOrder16", + "UnassignedWorkOrder20", + "UnassignedWorkOrder24", + "UnassignedWorkOrder32", "Undefined16", "Undefined20", "Undefined24", diff --git a/e2e/icons/__snapshots__/PublicAPI-test.js.snap b/e2e/icons/__snapshots__/PublicAPI-test.js.snap index d310a3f66202..274ed39a4853 100644 --- a/e2e/icons/__snapshots__/PublicAPI-test.js.snap +++ b/e2e/icons/__snapshots__/PublicAPI-test.js.snap @@ -54,6 +54,10 @@ exports[`@carbon/icons should not update exports without a semver change 1`] = ` "Activity20", "Activity24", "Activity32", + "ActivityTask16", + "ActivityTask20", + "ActivityTask24", + "ActivityTask32", "AdapterNotification16", "AdapterNotification20", "AdapterNotification24", @@ -1742,6 +1746,10 @@ exports[`@carbon/icons should not update exports without a semver change 1`] = ` "Classification20", "Classification24", "Classification32", + "ClassificationGroups16", + "ClassificationGroups20", + "ClassificationGroups24", + "ClassificationGroups32", "ClassifierLanguage16", "ClassifierLanguage20", "ClassifierLanguage24", @@ -2508,6 +2516,10 @@ exports[`@carbon/icons should not update exports without a semver change 1`] = ` "Data_220", "Data_224", "Data_232", + "DatabaseBackups16", + "DatabaseBackups20", + "DatabaseBackups24", + "DatabaseBackups32", "DatabaseDatastax16", "DatabaseDatastax20", "DatabaseDatastax24", @@ -3257,6 +3269,10 @@ exports[`@carbon/icons should not update exports without a semver change 1`] = ` "Encryption20", "Encryption24", "Encryption32", + "EndToEndMonitoring16", + "EndToEndMonitoring20", + "EndToEndMonitoring24", + "EndToEndMonitoring32", "EnergyRenewable16", "EnergyRenewable20", "EnergyRenewable24", @@ -3465,6 +3481,10 @@ exports[`@carbon/icons should not update exports without a semver change 1`] = ` "FacilityGroupAlternate20", "FacilityGroupAlternate24", "FacilityGroupAlternate32", + "FacilityGroups16", + "FacilityGroups20", + "FacilityGroups24", + "FacilityGroups32", "Factor16", "Factor20", "Factor24", @@ -3857,10 +3877,30 @@ exports[`@carbon/icons should not update exports without a semver change 1`] = ` "GatewayApi20", "GatewayApi24", "GatewayApi32", + "GatewayExclusive16", + "GatewayExclusive20", + "GatewayExclusive24", + "GatewayExclusive32", + "GatewayInclusive16", + "GatewayInclusive20", + "GatewayInclusive24", + "GatewayInclusive32", + "GatewayInclusiveAlt16", + "GatewayInclusiveAlt20", + "GatewayInclusiveAlt24", + "GatewayInclusiveAlt32", "GatewayMail16", "GatewayMail20", "GatewayMail24", "GatewayMail32", + "GatewayParallel16", + "GatewayParallel20", + "GatewayParallel24", + "GatewayParallel32", + "GatewayParallelAlt16", + "GatewayParallelAlt20", + "GatewayParallelAlt24", + "GatewayParallelAlt32", "GatewayPublic16", "GatewayPublic20", "GatewayPublic24", @@ -5706,6 +5746,10 @@ exports[`@carbon/icons should not update exports without a semver change 1`] = ` "LocationSupplier20", "LocationSupplier24", "LocationSupplier32", + "LocationSwitch16", + "LocationSwitch20", + "LocationSwitch24", + "LocationSwitch32", "Locked16", "Locked20", "Locked24", @@ -6127,6 +6171,10 @@ exports[`@carbon/icons should not update exports without a semver change 1`] = ` "MinusPlus20", "MinusPlus24", "MinusPlus32", + "MissingDataWarning16", + "MissingDataWarning20", + "MissingDataWarning24", + "MissingDataWarning32", "Misuse16", "Misuse20", "Misuse24", @@ -7199,6 +7247,10 @@ exports[`@carbon/icons should not update exports without a semver change 1`] = ` "Portfolio20", "Portfolio24", "Portfolio32", + "PortfolioGroups16", + "PortfolioGroups20", + "PortfolioGroups24", + "PortfolioGroups32", "PortfolioManagement16", "PortfolioManagement20", "PortfolioManagement24", @@ -7291,6 +7343,10 @@ exports[`@carbon/icons should not update exports without a semver change 1`] = ` "Product20", "Product24", "Product32", + "ProductCategories16", + "ProductCategories20", + "ProductCategories24", + "ProductCategories32", "ProductionEnvironment16", "ProductionEnvironment20", "ProductionEnvironment24", @@ -7343,6 +7399,10 @@ exports[`@carbon/icons should not update exports without a semver change 1`] = ` "Purchase20", "Purchase24", "Purchase32", + "PurchasingOrganizations16", + "PurchasingOrganizations20", + "PurchasingOrganizations24", + "PurchasingOrganizations32", "QBarrier16", "QBarrier20", "QBarrier24", @@ -8692,6 +8752,10 @@ exports[`@carbon/icons should not update exports without a semver change 1`] = ` "SubnetAclRules20", "SubnetAclRules24", "SubnetAclRules32", + "Subprocess16", + "Subprocess20", + "Subprocess24", + "Subprocess32", "Subsetting16", "Subsetting20", "Subsetting24", @@ -9248,6 +9312,14 @@ exports[`@carbon/icons should not update exports without a semver change 1`] = ` "Tif20", "Tif24", "Tif32", + "Tilde16", + "Tilde20", + "Tilde24", + "Tilde32", + "TildeLarge16", + "TildeLarge20", + "TildeLarge24", + "TildeLarge32", "Time16", "Time20", "Time24", @@ -9572,6 +9644,10 @@ exports[`@carbon/icons should not update exports without a semver change 1`] = ` "Umbrella20", "Umbrella24", "Umbrella32", + "UnassignedWorkOrder16", + "UnassignedWorkOrder20", + "UnassignedWorkOrder24", + "UnassignedWorkOrder32", "Undefined16", "Undefined20", "Undefined24", diff --git a/packages/carbon-components-react/__tests__/postinstall-test.js b/packages/carbon-components-react/__tests__/postinstall-test.js new file mode 100644 index 000000000000..8883c53f87db --- /dev/null +++ b/packages/carbon-components-react/__tests__/postinstall-test.js @@ -0,0 +1,46 @@ +/** + * Copyright IBM Corp. 2024, 2026 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + * + * @jest-environment node + */ + +'use strict'; + +const { spawnSync } = require('child_process'); +const path = require('path'); + +const scriptPath = path.resolve(__dirname, '../scripts/postinstall.js'); + +describe('carbon-components-react postinstall', () => { + let result; + + beforeAll(() => { + result = spawnSync(process.execPath, [scriptPath], { + env: { ...process.env, FORCE_COLOR: '0' }, + encoding: 'utf-8', + }); + }); + + it('exits with status 0', () => { + expect(result.status).toBe(0); + }); + + it('writes the deprecated package name to stderr', () => { + expect(result.stderr).toContain('carbon-components-react'); + }); + + it('writes the deprecation URL to stderr', () => { + expect(result.stderr).toContain( + 'https://carbondesignsystem.com/deprecations/' + ); + }); + + it('does not throw a TypeError', () => { + const output = result.stdout + result.stderr; + expect(output).not.toContain('TypeError'); + expect(output).not.toContain('chalk.yellow is not a function'); + }); +}); diff --git a/packages/carbon-components-react/package.json b/packages/carbon-components-react/package.json index 251039b6efae..d060c3fef74a 100644 --- a/packages/carbon-components-react/package.json +++ b/packages/carbon-components-react/package.json @@ -2,7 +2,7 @@ "name": "carbon-components-react", "private": true, "description": "The Carbon Design System is IBM’s open-source design system for products and experiences. This package reached end of support on September 30, 2024 and will not receive any more updates.", - "version": "8.111.0", + "version": "8.112.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -43,10 +43,10 @@ "sass": "^1.33.0" }, "dependencies": { - "@carbon/react": "^1.111.0", - "@carbon/styles": "^1.110.0", + "@carbon/react": "^1.112.0", + "@carbon/styles": "^1.111.0", "@ibm/telemetry-js": "^1.5.0", - "chalk": "1.1.3" + "chalk": "5.6.2" }, "devDependencies": { "@babel/core": "^7.27.3", diff --git a/packages/carbon-components-react/scripts/postinstall.js b/packages/carbon-components-react/scripts/postinstall.js index 5f1a42a65b1a..ffac1ddf016a 100644 --- a/packages/carbon-components-react/scripts/postinstall.js +++ b/packages/carbon-components-react/scripts/postinstall.js @@ -1,12 +1,23 @@ +/** + * Copyright IBM Corp. 2024, 2026 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + 'use strict'; -const chalk = require('chalk'); +async function main() { + const { default: chalk } = await import('chalk'); + + // eslint-disable-next-line no-console + console.warn( + chalk.yellow('Warning: ') + + 'the ' + + chalk.cyan('carbon-components-react') + + ' package is no longer supported. More info at ' + + chalk.dim.underline.italic('https://carbondesignsystem.com/deprecations/') + ); +} -// eslint-disable-next-line no-console -console.warn( - chalk.yellow('Warning: ') + - 'the ' + - chalk.cyan('carbon-components-react') + - ' package is no longer supported. More info at ' + - chalk.dim.underline.italic('https://carbondesignsystem.com/deprecations/') -); +main(); diff --git a/packages/carbon-components/__tests__/postinstall-test.js b/packages/carbon-components/__tests__/postinstall-test.js new file mode 100644 index 000000000000..617373b9e547 --- /dev/null +++ b/packages/carbon-components/__tests__/postinstall-test.js @@ -0,0 +1,46 @@ +/** + * Copyright IBM Corp. 2024, 2026 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + * + * @jest-environment node + */ + +'use strict'; + +const { spawnSync } = require('child_process'); +const path = require('path'); + +const scriptPath = path.resolve(__dirname, '../scripts/postinstall.js'); + +describe('carbon-components postinstall', () => { + let result; + + beforeAll(() => { + result = spawnSync(process.execPath, [scriptPath], { + env: { ...process.env, FORCE_COLOR: '0' }, + encoding: 'utf-8', + }); + }); + + it('exits with status 0', () => { + expect(result.status).toBe(0); + }); + + it('writes the deprecated package name to stderr', () => { + expect(result.stderr).toContain('carbon-components'); + }); + + it('writes the deprecation URL to stderr', () => { + expect(result.stderr).toContain( + 'https://carbondesignsystem.com/deprecations/' + ); + }); + + it('does not throw a TypeError', () => { + const output = result.stdout + result.stderr; + expect(output).not.toContain('TypeError'); + expect(output).not.toContain('chalk.yellow is not a function'); + }); +}); diff --git a/packages/carbon-components/package.json b/packages/carbon-components/package.json index 0a036ff82dd7..3787dc191f06 100644 --- a/packages/carbon-components/package.json +++ b/packages/carbon-components/package.json @@ -2,7 +2,7 @@ "name": "carbon-components", "private": true, "description": "The Carbon Design System is IBM’s open-source design system for products and experiences. This package reached end of support on September 30, 2024 and will not receive any more updates.", - "version": "11.110.0", + "version": "11.111.0", "license": "Apache-2.0", "repository": { "type": "git", @@ -44,9 +44,9 @@ "sass": "^1.33.0" }, "dependencies": { - "@carbon/styles": "^1.110.0", + "@carbon/styles": "^1.111.0", "@ibm/telemetry-js": "^1.5.0", - "chalk": "1.1.3" + "chalk": "5.6.2" }, "devDependencies": { "@carbon/test-utils": "^10.41.0", diff --git a/packages/carbon-components/scripts/postinstall.js b/packages/carbon-components/scripts/postinstall.js index 054353582475..587dd3bb174e 100644 --- a/packages/carbon-components/scripts/postinstall.js +++ b/packages/carbon-components/scripts/postinstall.js @@ -1,12 +1,23 @@ +/** + * Copyright IBM Corp. 2024, 2026 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + 'use strict'; -const chalk = require('chalk'); +async function main() { + const { default: chalk } = await import('chalk'); + + // eslint-disable-next-line no-console + console.warn( + chalk.yellow('Warning: ') + + 'the ' + + chalk.cyan('carbon-components') + + ' package is no longer supported. More info at ' + + chalk.dim.underline.italic('https://carbondesignsystem.com/deprecations/') + ); +} -// eslint-disable-next-line no-console -console.warn( - chalk.yellow('Warning: ') + - 'the ' + - chalk.cyan('carbon-components') + - ' package is no longer supported. More info at ' + - chalk.dim.underline.italic('https://carbondesignsystem.com/deprecations/') -); +main(); diff --git a/packages/cli/package.json b/packages/cli/package.json index 965a0f68909c..42d3e4f3d3f7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/cli", "description": "Task automation for working with the Carbon Design System", - "version": "11.45.0", + "version": "11.46.0", "license": "Apache-2.0", "type": "module", "bin": { diff --git a/packages/colors/package.json b/packages/colors/package.json index 205596bac977..a593e43f6c47 100644 --- a/packages/colors/package.json +++ b/packages/colors/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/colors", "description": "Colors for digital and software products using the Carbon Design System", - "version": "11.53.0", + "version": "11.54.0", "license": "Apache-2.0", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -42,7 +42,7 @@ "postinstall": "ibmtelemetry --config=telemetry.yml" }, "devDependencies": { - "@carbon/cli": "^11.45.0", + "@carbon/cli": "^11.46.0", "@carbon/cli-reporter": "^10.8.0", "@carbon/scss-generator": "^10.20.0", "@carbon/test-utils": "^10.41.0", @@ -50,7 +50,7 @@ "fs-extra": "^11.0.0", "rimraf": "^6.0.1", "typescript": "^6.0.3", - "typescript-config-carbon": "workspace:^0.10.0" + "typescript-config-carbon": "workspace:^0.11.0" }, "sideEffects": false, "eyeglass": { diff --git a/packages/elements/package.json b/packages/elements/package.json index 9025b139ec13..06bff3ff633d 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/elements", "description": "A collection of design elements in code for the IBM Design Language", - "version": "11.92.0", + "version": "11.93.0", "license": "Apache-2.0", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -40,21 +40,21 @@ "postinstall": "ibmtelemetry --config=telemetry.yml" }, "dependencies": { - "@carbon/colors": "^11.53.0", - "@carbon/grid": "^11.57.0", - "@carbon/icons": "^11.83.0", - "@carbon/layout": "^11.54.0", - "@carbon/motion": "^11.47.0", - "@carbon/themes": "^11.76.0", - "@carbon/type": "^11.62.0", + "@carbon/colors": "^11.54.0", + "@carbon/grid": "^11.58.0", + "@carbon/icons": "^11.84.0", + "@carbon/layout": "^11.55.0", + "@carbon/motion": "^11.48.0", + "@carbon/themes": "^11.77.0", + "@carbon/type": "^11.63.0", "@ibm/telemetry-js": "^1.5.0" }, "devDependencies": { - "@carbon/cli": "^11.45.0", + "@carbon/cli": "^11.46.0", "fs-extra": "^11.0.0", "klaw-sync": "^6.0.0", "rimraf": "^6.0.1", "typescript": "^6.0.3", - "typescript-config-carbon": "workspace:^0.10.0" + "typescript-config-carbon": "workspace:^0.11.0" } } diff --git a/packages/feature-flags/__tests__/scss-test.js b/packages/feature-flags/__tests__/scss-test.js index 20415b539094..6be30f318b4a 100644 --- a/packages/feature-flags/__tests__/scss-test.js +++ b/packages/feature-flags/__tests__/scss-test.js @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2015, 2023 + * Copyright IBM Corp. 2015, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -88,6 +88,41 @@ describe('@carbon/feature-flags', () => { // flag-b expect(getValue(1)).toBe(false); }); + + it('should enable v12 flags when the v12 release flag is enabled', async () => { + const { getValue } = await render(` + @use '../index.scss' as feature-flags with ( + $feature-flags: ( + 'enable-v12-release': true, + 'enable-v12-example': false, + 'enable-not-v12-example': false, + ) + ); + + $_: get-value(feature-flags.enabled('enable-v12-example')); + $_: get-value(feature-flags.enabled('enable-not-v12-example')); + `); + + // enable-v12-example + expect(getValue(0)).toBe(true); + // enable-not-v12-example + expect(getValue(1)).toBe(false); + }); + + it('should allow v12 flags to be enabled independently of the v12 release flag', async () => { + const { getValue } = await render(` + @use '../index.scss' as feature-flags with ( + $feature-flags: ( + 'enable-v12-release': false, + 'enable-v12-example': true, + ) + ); + + $_: get-value(feature-flags.enabled('enable-v12-example')); + `); + + expect(getValue(0)).toBe(true); + }); }); describe('merge', () => { diff --git a/packages/feature-flags/feature-flags.yml b/packages/feature-flags/feature-flags.yml index 16e09a7da628..5c14ae913d92 100644 --- a/packages/feature-flags/feature-flags.yml +++ b/packages/feature-flags/feature-flags.yml @@ -1,5 +1,5 @@ # -# Copyright IBM Corp. 2015, 2023 +# Copyright IBM Corp. 2015, 2026 # # This source code is licensed under the Apache-2.0 license found in the # LICENSE file in the root directory of this source tree. @@ -17,6 +17,10 @@ feature-flags: description: > Enable the features and functionality for the v11 Release enabled: true + - name: enable-v12-release + description: > + Enable the features and functionality for the v12 Release + enabled: false - name: enable-experimental-tile-contrast description: > Deprecated, use enable-tile-contrast instead diff --git a/packages/feature-flags/index.scss b/packages/feature-flags/index.scss index dca276edd809..2c38cac7d744 100644 --- a/packages/feature-flags/index.scss +++ b/packages/feature-flags/index.scss @@ -1,13 +1,17 @@ // -// Copyright IBM Corp. 2015, 2023 +// Copyright IBM Corp. 2015, 2026 // // This source code is licensed under the Apache-2.0 license found in the // LICENSE file in the root directory of this source tree. // @use 'sass:map'; +@use 'sass:string'; @use 'scss/generated/feature-flags'; +$v12-release-flag: 'enable-v12-release'; +$v12-flag-prefix: 'enable-v12-'; + $feature-flags: () !default; $feature-flags: map.merge( feature-flags.$generated-feature-flags, @@ -21,6 +25,14 @@ $feature-flags: map.merge( @error 'Unable to find a feature flag named: #{$name}'; } +@function enabled-by-v12-release($name) { + $is-v12-flag: string.index($name, $v12-flag-prefix) == 1; + $is-v12-release-enabled: map.get($feature-flags, $v12-release-flag) == true; + + @return $name != $v12-release-flag and $is-v12-flag and + $is-v12-release-enabled; +} + @mixin add($name, $enabled: false) { @if map.has-key($feature-flags, $name) == true { @error 'A feature flag named #{$name} already exists'; @@ -50,6 +62,10 @@ $feature-flags: map.merge( /// @returns {Boolean} @function enabled($name) { @if check-for-flag($name) == true { + @if enabled-by-v12-release($name) { + @return true; + } + @return map.get($feature-flags, $name); } } diff --git a/packages/feature-flags/package.json b/packages/feature-flags/package.json index b61eba4db0fd..720a9135f727 100644 --- a/packages/feature-flags/package.json +++ b/packages/feature-flags/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/feature-flags", "description": "Build with feature flags in Carbon", - "version": "1.4.0", + "version": "1.5.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", diff --git a/packages/feature-flags/src/FeatureFlagScope.ts b/packages/feature-flags/src/FeatureFlagScope.ts index 275a8917455d..42ba4dc64603 100644 --- a/packages/feature-flags/src/FeatureFlagScope.ts +++ b/packages/feature-flags/src/FeatureFlagScope.ts @@ -10,6 +10,9 @@ export type FeatureFlagRecord = Record; /** @deprecated Use `FeatureFlagRecord` instead. */ export type FeatureFlags = FeatureFlagRecord; +const v12ReleaseFlag = 'enable-v12-release'; +const v12FlagPrefix = 'enable-v12-'; + export class FeatureFlagScope { flags: Map; @@ -85,6 +88,14 @@ export class FeatureFlagScope { enabled(name: string) { this.checkForFlag(name); + if ( + name !== v12ReleaseFlag && + name.startsWith(v12FlagPrefix) && + this.flags.get(v12ReleaseFlag) === true + ) { + return true; + } + return this.flags.get(name) ?? false; } } diff --git a/packages/feature-flags/src/__tests__/feature-flags-test.js b/packages/feature-flags/src/__tests__/feature-flags-test.js index 21f8735007e9..d4fa527128f1 100644 --- a/packages/feature-flags/src/__tests__/feature-flags-test.js +++ b/packages/feature-flags/src/__tests__/feature-flags-test.js @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2015, 2023 + * Copyright IBM Corp. 2015, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -72,6 +72,26 @@ describe('@carbon/feature-flags', () => { expect(FeatureFlags.enabled('flag-b')).toBe(false); }); + it('should enable v12 flags when the v12 release flag is enabled', () => { + FeatureFlags.merge({ + 'enable-v12-release': true, + 'enable-v12-example': false, + 'enable-not-v12-example': false, + }); + + expect(FeatureFlags.enabled('enable-v12-example')).toBe(true); + expect(FeatureFlags.enabled('enable-not-v12-example')).toBe(false); + }); + + it('should allow v12 flags to be enabled independently of the v12 release flag', () => { + FeatureFlags.merge({ + 'enable-v12-release': false, + 'enable-v12-example': true, + }); + + expect(FeatureFlags.enabled('enable-v12-example')).toBe(true); + }); + it('should throw if the given flag does not exist', () => { expect(() => { FeatureFlags.enabled('flag-a'); diff --git a/packages/grid/package.json b/packages/grid/package.json index bc8c0577e834..c7eda32d997e 100644 --- a/packages/grid/package.json +++ b/packages/grid/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/grid", "description": "Grid for digital and software products using the Carbon Design System", - "version": "11.57.0", + "version": "11.58.0", "license": "Apache-2.0", "repository": { "type": "git", @@ -35,11 +35,11 @@ "postinstall": "ibmtelemetry --config=telemetry.yml" }, "dependencies": { - "@carbon/layout": "^11.54.0", + "@carbon/layout": "^11.55.0", "@ibm/telemetry-js": "^1.5.0" }, "devDependencies": { - "@carbon/cli": "^11.45.0", + "@carbon/cli": "^11.46.0", "rimraf": "^6.0.1" }, "eyeglass": { diff --git a/packages/icon-build-helpers/package.json b/packages/icon-build-helpers/package.json index 0879888d6bf6..82f50055fe2c 100644 --- a/packages/icon-build-helpers/package.json +++ b/packages/icon-build-helpers/package.json @@ -2,7 +2,7 @@ "name": "@carbon/icon-build-helpers", "private": true, "description": "Build helpers for the Carbon Design System icon library", - "version": "1.60.0", + "version": "1.61.0", "license": "Apache-2.0", "main": "src/index.js", "repository": { @@ -27,10 +27,10 @@ "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "@carbon/cli-reporter": "^10.8.0", - "@carbon/icon-helpers": "^10.77.0", - "@hapi/joi": "^17.1.1", + "@carbon/icon-helpers": "^10.78.0", "change-case-all": "^2.1.0", "fs-extra": "^11.0.0", + "joi": "^17.1.1", "js-yaml": "^4.1.0", "memfs": "^4.0.0", "prettier": "^3.3.3", @@ -39,6 +39,6 @@ "svgo": "^4.0.0", "svgson": "^5.2.1", "tsdown": "^0.22.0", - "typescript-config-carbon": "workspace:^0.10.0" + "typescript-config-carbon": "workspace:^0.11.0" } } diff --git a/packages/icon-build-helpers/src/metadata/__tests__/metadata-test.js b/packages/icon-build-helpers/src/metadata/__tests__/metadata-test.js index 404890970d38..57f5d9bd0e2a 100644 --- a/packages/icon-build-helpers/src/metadata/__tests__/metadata-test.js +++ b/packages/icon-build-helpers/src/metadata/__tests__/metadata-test.js @@ -46,7 +46,7 @@ describe('Metadata', () => { validate: jest.fn(), }; const mockSchema = jest.fn(() => { - const Joi = require('@hapi/joi'); + const Joi = require('joi'); return Joi.object({ foo: Joi.string() }); }); Object.defineProperty(mockExtension, 'schema', { diff --git a/packages/icon-build-helpers/src/metadata/extensions/categories.js b/packages/icon-build-helpers/src/metadata/extensions/categories.js index 9bb92cef3837..e9b8f7475d98 100644 --- a/packages/icon-build-helpers/src/metadata/extensions/categories.js +++ b/packages/icon-build-helpers/src/metadata/extensions/categories.js @@ -7,7 +7,7 @@ 'use strict'; -const Joi = require('@hapi/joi'); +const Joi = require('joi'); // Supports both top-level categories and subcategories // diff --git a/packages/icon-build-helpers/src/metadata/extensions/deprecated.js b/packages/icon-build-helpers/src/metadata/extensions/deprecated.js index 55c6fadd2bec..93c6af5048af 100644 --- a/packages/icon-build-helpers/src/metadata/extensions/deprecated.js +++ b/packages/icon-build-helpers/src/metadata/extensions/deprecated.js @@ -7,7 +7,7 @@ 'use strict'; -const Joi = require('@hapi/joi'); +const Joi = require('joi'); // Supports a list of deprecated assets // diff --git a/packages/icon-build-helpers/src/metadata/extensions/icons.js b/packages/icon-build-helpers/src/metadata/extensions/icons.js index 17774755276a..a4e0d7ecd080 100644 --- a/packages/icon-build-helpers/src/metadata/extensions/icons.js +++ b/packages/icon-build-helpers/src/metadata/extensions/icons.js @@ -7,7 +7,7 @@ 'use strict'; -const Joi = require('@hapi/joi'); +const Joi = require('joi'); /** * The default icons extension for the metadata. This validates an icon file diff --git a/packages/icon-build-helpers/src/metadata/extensions/pictograms.js b/packages/icon-build-helpers/src/metadata/extensions/pictograms.js index e895ca5a72ce..ee585077b108 100644 --- a/packages/icon-build-helpers/src/metadata/extensions/pictograms.js +++ b/packages/icon-build-helpers/src/metadata/extensions/pictograms.js @@ -7,7 +7,7 @@ 'use strict'; -const Joi = require('@hapi/joi'); +const Joi = require('joi'); /** * The default pictograms extension for the metadata. This validates a pictogram diff --git a/packages/icon-helpers/package.json b/packages/icon-helpers/package.json index cb0dcc21e105..e92a1561200e 100644 --- a/packages/icon-helpers/package.json +++ b/packages/icon-helpers/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/icon-helpers", "description": "Helpers used alongside icons for digital and software products using the Carbon Design System", - "version": "10.77.0", + "version": "10.78.0", "license": "Apache-2.0", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -37,9 +37,9 @@ "postinstall": "ibmtelemetry --config=telemetry.yml" }, "devDependencies": { - "@carbon/cli": "^11.45.0", + "@carbon/cli": "^11.46.0", "rimraf": "^6.0.1", - "typescript-config-carbon": "workspace:^0.10.0" + "typescript-config-carbon": "workspace:^0.11.0" }, "sideEffects": false, "dependencies": { diff --git a/packages/icons-react/package.json b/packages/icons-react/package.json index 64ad4ba74123..3a574a563151 100644 --- a/packages/icons-react/package.json +++ b/packages/icons-react/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/icons-react", "description": "React components for icons in digital and software products using the Carbon Design System", - "version": "11.83.0", + "version": "11.84.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -38,13 +38,13 @@ "react": ">=16" }, "dependencies": { - "@carbon/icon-helpers": "^10.77.0", + "@carbon/icon-helpers": "^10.78.0", "@ibm/telemetry-js": "^1.5.0", "prop-types": "^15.8.1" }, "devDependencies": { - "@carbon/icon-build-helpers": "^1.60.0", - "@carbon/icons": "^11.83.0", + "@carbon/icon-build-helpers": "^1.61.0", + "@carbon/icons": "^11.84.0", "rimraf": "^6.0.1" }, "sideEffects": false diff --git a/packages/icons-vue/package.json b/packages/icons-vue/package.json index bbb8d8a5e4b1..ebf78c7c466d 100644 --- a/packages/icons-vue/package.json +++ b/packages/icons-vue/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/icons-vue", "description": "Vue components for icons in digital and software products using the Carbon Design System", - "version": "10.132.0", + "version": "10.133.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -30,11 +30,11 @@ "postinstall": "ibmtelemetry --config=telemetry.yml" }, "dependencies": { - "@carbon/icon-helpers": "^10.77.0", + "@carbon/icon-helpers": "^10.78.0", "@ibm/telemetry-js": "^1.5.0" }, "devDependencies": { - "@carbon/icons": "^11.83.0", + "@carbon/icons": "^11.84.0", "rimraf": "^6.0.1", "vue": "^3.0.0" }, diff --git a/packages/icons/categories.yml b/packages/icons/categories.yml index e703140a25a7..1fb2f7b59b15 100644 --- a/packages/icons/categories.yml +++ b/packages/icons/categories.yml @@ -284,6 +284,7 @@ categories: - vertical-view - name: Navigation members: + - activity--task - adapter-notification - add - add--alt @@ -329,6 +330,7 @@ categories: - point - select--window - stickies + - subprocess - subtract - subtract--alt - subtract--filled @@ -414,6 +416,7 @@ categories: - drill-back - drill-down - drill-through + - end-to-end--monitoring - equal--approximately - exit - expand-all @@ -449,6 +452,7 @@ categories: - launch - layers - layers--external + - location--switch - login - logout - loop @@ -539,10 +543,13 @@ categories: - tag--import - tag--none - test-tool + - tilde + - tilde--large - timing-belt - tour - translate - transpose + - unassigned--work-order - undo - unfold--open - update-complete @@ -776,6 +783,7 @@ categories: - cloud-satellite--services - cobol-upgrade-advisor - content-delivery-network + - database--backups - database--datastax - database--elastic - database--enterprisedb @@ -1619,6 +1627,7 @@ categories: - certificate - certificate--check - classification + - classification--groups - closed-caption - closed-caption--alt - closed-caption--filled @@ -1667,6 +1676,7 @@ categories: - DVR - energy--report - export + - facility--groups - feedback-documentation - file--change - file--diff @@ -1743,8 +1753,11 @@ categories: - PNG - policy - popup + - portfolio--groups - PPT - product + - product-categories + - purchasing-organizations - RAW - reference-architecture - repo--artifact @@ -1861,6 +1874,7 @@ categories: - information--square--filled - key--values - locked-and-blocked + - missing-data--warning - misuse - misuse--outline - pending @@ -1994,7 +2008,12 @@ categories: - fork-node - gateway - gateway--api + - gateway--exclusive + - gateway--inclusive + - gateway--inclusive--alt - gateway--mail + - gateway--parallel + - gateway--parallel--alt - gateway--public - gateway--security - gateway--user-access diff --git a/packages/icons/deprecated.yml b/packages/icons/deprecated.yml index 75ddb7edf094..6315f0c72c8f 100644 --- a/packages/icons/deprecated.yml +++ b/packages/icons/deprecated.yml @@ -70,9 +70,6 @@ deprecated: - name: ai-status--rejected reason: Icon no longer relevant to how we represent AI moments - - name: database--redis - reason: - Icon no longer relevant. IBM Cloud Databases for Redis UI icon is retired. New icon with new name was requested - name: ibm-watsonx--code-assistant-for-z--refactor reason: Icon no longer relevant. watsonx Code Assistant for Z Refactoring Assistant has been merged into an existing product. \ No newline at end of file diff --git a/packages/icons/icons.yml b/packages/icons/icons.yml index 8d73cf7c617e..f6101384c214 100644 --- a/packages/icons/icons.yml +++ b/packages/icons/icons.yml @@ -333,6 +333,16 @@ - tracking sizes: - 32 +- name: activity--task + friendly_name: Activity task + aliases: + - activity + - rectangle + - task + - process + - workflow + sizes: + - 32 - name: adapter-notification friendly_name: Adapter notification aliases: @@ -5264,6 +5274,26 @@ - classify sizes: - 32 +- name: classification--groups + friendly_name: Classification groups + aliases: + - classification + - group + - hierarchy + - primary + - primary hierarchy + - reporting + - enterprise reporting + - location + - location grouping + - full inclusion + - file + - document + - tag + - tagging + - frame + sizes: + - 32 - name: classifier--language friendly_name: Classifier language sizes: @@ -7301,6 +7331,24 @@ - dots sizes: - 32 +- name: database--backups + friendly_name: Database backups + aliases: + - database + - backup + - independent backup + - automatic backup + - on-demand backup + - hub + - backup hub + - server + - stack + - cloud + - recover + - data + - recover data + sizes: + - 32 - name: database--datastax friendly_name: IBM Cloud® Databases for DataStax aliases: @@ -9164,6 +9212,20 @@ - secure sizes: - 32 +- name: end-to-end--monitoring + friendly_name: End-to-end monitoring + aliases: + - end-to-end + - monitoring + - observability + - tracing + - distributed tracing + - flow + - transaction flow + - integration + - integration monitoring + sizes: + - 32 - name: energy--renewable friendly_name: Energy renewable aliases: @@ -9763,6 +9825,22 @@ - travel sizes: - 32 +- name: facility--groups + friendly_name: Facility groups + aliases: + - facility + - group + - grouping + - optional grouping + - virtual meters + - location + - location grouping + - reporting + - facility reporting + - pin + - place + sizes: + - 32 - name: facility--group--alternate friendly_name: Facility group alternate aliases: @@ -10831,6 +10909,67 @@ - app sizes: - 32 +- name: gateway--exclusive + friendly_name: Gateway exclusive + aliases: + - gateway + - exclusive + - path + - singular + - split + - routing + - decision + - point + - decision point + - conditional + - branch + - conditional branch + - process + - process branching + - diamond + - cross + sizes: + - 32 +- name: gateway--inclusive + friendly_name: Gateway inclusive + aliases: + - gateway + - inclusive + - split + - routing + - decision + - point + - decision point + - conditional + - branch + - conditional branch + - process + - process branching + - diamond + - circle + sizes: + - 32 +- name: gateway--inclusive--alt + friendly_name: Gateway inclusive alt + aliases: + - gateway + - inclusive + - patch + - combination + - split + - routing + - decision + - point + - decision point + - conditional + - branch + - conditional branch + - process + - process branching + - diamond + - circle + sizes: + - 32 - name: gateway--mail friendly_name: Gateway mail aliases: @@ -10841,6 +10980,38 @@ - message sizes: - 32 +- name: gateway--parallel + friendly_name: Gateway parallel + aliases: + - gateway + - parallel + - path + - simultaneous + - split + - routing + - decision + - point + - decision point + - diamond + - plus + sizes: + - 32 +- name: gateway--parallel--alt + friendly_name: Gateway parallel alt + aliases: + - gateway + - parallel + - path + - simultaneous + - split + - routing + - decision + - point + - decision point + - diamond + - plus + sizes: + - 32 - name: gateway--public friendly_name: Gateway public aliases: @@ -12623,6 +12794,21 @@ - remote sizes: - 32 +- name: portfolio--groups + friendly_name: Portfolio groups + aliases: + - portfolio + - group + - hierarchy + - secondary hierarchy + - locations + - subset + - percentages + - reporting + - briefcase + - frame + sizes: + - 32 - name: portfolio--management friendly_name: Portfolio management aliases: @@ -16645,6 +16831,22 @@ - filled sizes: - 32 +- name: location--switch + friendly_name: Location switch + aliases: + - location + - pin + - place + - switch + - hierarchy + - location hierarchy + - change + - switch location + - cange location + - change parent location + - Maximo + sizes: + - 32 - name: locked friendly_name: Locked aliases: @@ -17932,6 +18134,27 @@ - modify sizes: - 32 +- name: unassigned--work-order + friendly_name: Unassigned work order + aliases: + - unassigned + - work + - order + - user + - task + - unassigned task + - clipboard + - exclamation mark + sizes: + - 32 +- name: missing-data--warning + friendly_name: Missing data warning + aliases: + - missing data warning + - status + - warning + sizes: + - 32 - name: misuse friendly_name: Misuse aliases: @@ -20953,6 +21176,24 @@ - product sizes: - 32 +- name: product-categories + friendly_name: Product categories + aliases: + - product + - category + - scope + - scope 3 + - classification + - label + - taxonomy + - supply chain + - supply chain taxonomy + - box + - container + - label + - tag + sizes: + - 32 - name: production-environment friendly_name: Production environment aliases: @@ -21084,6 +21325,22 @@ - debit sizes: - 32 +- name: purchasing-organizations + friendly_name: Purchasing organizations + aliases: + - purchase + - organization + - entity + - buying entity + - procurement + - procurement organization + - process + - flow + - pin + - place + - location + sizes: + - 32 - name: QC-launch friendly_name: QC launch aliases: @@ -24590,6 +24847,22 @@ - hierarchy sizes: - 32 +- name: subprocess + friendly_name: Subprocess + aliases: + - subprocess + - process + - secondary + - workflow + - folder + - process folder + - group process + - nested process + - rectangle + - add + - plus + sizes: + - 32 - name: subtract friendly_name: Subtract aliases: @@ -25973,6 +26246,31 @@ sizes: - 20 - 32 +- name: tilde + friendly_name: Tilde + aliases: + - tilde + - change + - update + - updated + - edited + - modified + - revision + sizes: + - 32 +- name: tilde--large + friendly_name: Tilde large + aliases: + - tilde + - large + - change + - update + - updated + - edited + - modified + - revision + sizes: + - 32 - name: time--filled friendly_name: Time filled aliases: diff --git a/packages/icons/package.json b/packages/icons/package.json index 3fce1e65dbe1..adb69bc2ecee 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/icons", "description": "Icons for digital and software products using the Carbon Design System", - "version": "11.83.0", + "version": "11.84.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -41,8 +41,8 @@ "postinstall": "ibmtelemetry --config=telemetry.yml" }, "devDependencies": { - "@carbon/cli": "^11.45.0", - "@carbon/icon-build-helpers": "^1.60.0", + "@carbon/cli": "^11.46.0", + "@carbon/icon-build-helpers": "^1.61.0", "rimraf": "^6.0.1" }, "dependencies": { diff --git a/packages/icons/src/svg/32/activity--task.svg b/packages/icons/src/svg/32/activity--task.svg new file mode 100644 index 000000000000..8e970743c041 --- /dev/null +++ b/packages/icons/src/svg/32/activity--task.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/classification--groups.svg b/packages/icons/src/svg/32/classification--groups.svg new file mode 100644 index 000000000000..c646eaa3ec82 --- /dev/null +++ b/packages/icons/src/svg/32/classification--groups.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/database--backups.svg b/packages/icons/src/svg/32/database--backups.svg new file mode 100644 index 000000000000..39a03205f0a7 --- /dev/null +++ b/packages/icons/src/svg/32/database--backups.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/end-to-end--monitoring.svg b/packages/icons/src/svg/32/end-to-end--monitoring.svg new file mode 100644 index 000000000000..58f4117a9e15 --- /dev/null +++ b/packages/icons/src/svg/32/end-to-end--monitoring.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/facility--groups.svg b/packages/icons/src/svg/32/facility--groups.svg new file mode 100644 index 000000000000..27fa0c668e9e --- /dev/null +++ b/packages/icons/src/svg/32/facility--groups.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/gateway--exclusive.svg b/packages/icons/src/svg/32/gateway--exclusive.svg new file mode 100644 index 000000000000..9b6dfc7e2f82 --- /dev/null +++ b/packages/icons/src/svg/32/gateway--exclusive.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/gateway--inclusive--alt.svg b/packages/icons/src/svg/32/gateway--inclusive--alt.svg new file mode 100644 index 000000000000..7761232a4287 --- /dev/null +++ b/packages/icons/src/svg/32/gateway--inclusive--alt.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/gateway--inclusive.svg b/packages/icons/src/svg/32/gateway--inclusive.svg new file mode 100644 index 000000000000..c768ee55ead0 --- /dev/null +++ b/packages/icons/src/svg/32/gateway--inclusive.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/gateway--parallel--alt.svg b/packages/icons/src/svg/32/gateway--parallel--alt.svg new file mode 100644 index 000000000000..5256180d72a5 --- /dev/null +++ b/packages/icons/src/svg/32/gateway--parallel--alt.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/gateway--parallel.svg b/packages/icons/src/svg/32/gateway--parallel.svg new file mode 100644 index 000000000000..8703d3191c7a --- /dev/null +++ b/packages/icons/src/svg/32/gateway--parallel.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/location--switch.svg b/packages/icons/src/svg/32/location--switch.svg new file mode 100755 index 000000000000..5aada8ef2c9b --- /dev/null +++ b/packages/icons/src/svg/32/location--switch.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/missing-data--warning.svg b/packages/icons/src/svg/32/missing-data--warning.svg new file mode 100644 index 000000000000..fe047eba17ca --- /dev/null +++ b/packages/icons/src/svg/32/missing-data--warning.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/portfolio--groups.svg b/packages/icons/src/svg/32/portfolio--groups.svg new file mode 100644 index 000000000000..d1962435bd7e --- /dev/null +++ b/packages/icons/src/svg/32/portfolio--groups.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/product-categories.svg b/packages/icons/src/svg/32/product-categories.svg new file mode 100644 index 000000000000..49853e3e67b3 --- /dev/null +++ b/packages/icons/src/svg/32/product-categories.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/purchasing-organizations.svg b/packages/icons/src/svg/32/purchasing-organizations.svg new file mode 100644 index 000000000000..9dd9b5edd488 --- /dev/null +++ b/packages/icons/src/svg/32/purchasing-organizations.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/subprocess.svg b/packages/icons/src/svg/32/subprocess.svg new file mode 100644 index 000000000000..75741536ad6a --- /dev/null +++ b/packages/icons/src/svg/32/subprocess.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/tilde--large.svg b/packages/icons/src/svg/32/tilde--large.svg new file mode 100644 index 000000000000..77fc4ed013f4 --- /dev/null +++ b/packages/icons/src/svg/32/tilde--large.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/tilde.svg b/packages/icons/src/svg/32/tilde.svg new file mode 100644 index 000000000000..9a15faf43e59 --- /dev/null +++ b/packages/icons/src/svg/32/tilde.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/icons/src/svg/32/unassigned--work-order.svg b/packages/icons/src/svg/32/unassigned--work-order.svg new file mode 100755 index 000000000000..837ce05d2e32 --- /dev/null +++ b/packages/icons/src/svg/32/unassigned--work-order.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/layout/package.json b/packages/layout/package.json index f67fd8a59ca3..f35f0b3c299d 100644 --- a/packages/layout/package.json +++ b/packages/layout/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/layout", "description": "Layout helpers for digital and software products using the Carbon Design System", - "version": "11.54.0", + "version": "11.55.0", "license": "Apache-2.0", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -33,14 +33,14 @@ "postinstall": "ibmtelemetry --config=telemetry.yml" }, "devDependencies": { - "@carbon/cli": "^11.45.0", + "@carbon/cli": "^11.46.0", "@carbon/cli-reporter": "^10.8.0", "@carbon/scss-generator": "^10.20.0", "@carbon/test-utils": "^10.41.0", "core-js": "^3.16.0", "rimraf": "^6.0.1", "typescript": "^6.0.3", - "typescript-config-carbon": "workspace:^0.10.0" + "typescript-config-carbon": "workspace:^0.11.0" }, "dependencies": { "@ibm/telemetry-js": "^1.5.0" diff --git a/packages/motion/package.json b/packages/motion/package.json index 043fbe8f81ec..8b0a131cb536 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/motion", "description": "Motion helpers for digital and software products using the Carbon Design System", - "version": "11.47.0", + "version": "11.48.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -33,10 +33,10 @@ "postinstall": "ibmtelemetry --config=telemetry.yml" }, "devDependencies": { - "@carbon/cli": "^11.45.0", + "@carbon/cli": "^11.46.0", "rimraf": "^6.0.1", "typescript": "^6.0.3", - "typescript-config-carbon": "workspace:^0.10.0" + "typescript-config-carbon": "workspace:^0.11.0" }, "dependencies": { "@ibm/telemetry-js": "^1.5.0" diff --git a/packages/pictograms-react/package.json b/packages/pictograms-react/package.json index 7b0a6a1dfad2..b80826831f8b 100644 --- a/packages/pictograms-react/package.json +++ b/packages/pictograms-react/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/pictograms-react", "description": "React components for pictograms in digital and software products using the Carbon Design System", - "version": "11.105.0", + "version": "11.106.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -40,13 +40,13 @@ "react": ">=16" }, "dependencies": { - "@carbon/icon-helpers": "^10.77.0", + "@carbon/icon-helpers": "^10.78.0", "@ibm/telemetry-js": "^1.5.0", "prop-types": "^15.8.1" }, "devDependencies": { - "@carbon/icon-build-helpers": "^1.60.0", - "@carbon/pictograms": "^12.79.0", + "@carbon/icon-build-helpers": "^1.61.0", + "@carbon/pictograms": "^12.80.0", "rimraf": "^6.0.1" }, "sideEffects": false diff --git a/packages/pictograms/package.json b/packages/pictograms/package.json index 0c2377d8c9b6..6752da030798 100644 --- a/packages/pictograms/package.json +++ b/packages/pictograms/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/pictograms", "description": "Pictograms for digital and software products using the Carbon Design System", - "version": "12.79.0", + "version": "12.80.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -33,7 +33,7 @@ "postinstall": "ibmtelemetry --config=telemetry.yml" }, "devDependencies": { - "@carbon/icon-build-helpers": "^1.60.0", + "@carbon/icon-build-helpers": "^1.61.0", "rimraf": "^6.0.1" }, "dependencies": { diff --git a/packages/react/.storybook-v12/.gitignore b/packages/react/.storybook-v12/.gitignore new file mode 100644 index 000000000000..9e0adcc107c6 --- /dev/null +++ b/packages/react/.storybook-v12/.gitignore @@ -0,0 +1 @@ +/generated/ diff --git a/packages/react/.storybook-v12/Welcome/Welcome.js b/packages/react/.storybook-v12/Welcome/Welcome.js new file mode 100644 index 000000000000..e7a998e31f2b --- /dev/null +++ b/packages/react/.storybook-v12/Welcome/Welcome.js @@ -0,0 +1,56 @@ +/** + * Copyright IBM Corp. 2026 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import './welcome.scss'; +import Link from '../../src/components/Link'; +import { ArrowRight } from '@carbon/icons-react'; +import { Stack } from '../../src/components/Stack'; +import { Callout } from '../../src/components/Notification'; + +export const Welcome = () => { + return ( +
+

@carbon/react

+

v2.x

+ + + + Website + + + GitHub repo + + + Release schedule + + + Security policy + + +
+ ); +}; diff --git a/packages/react/.storybook-v12/Welcome/Welcome.mdx b/packages/react/.storybook-v12/Welcome/Welcome.mdx new file mode 100644 index 000000000000..e186e0d8e857 --- /dev/null +++ b/packages/react/.storybook-v12/Welcome/Welcome.mdx @@ -0,0 +1,8 @@ +import { Welcome as Intro } from './Welcome'; +import { Meta, Unstyled } from '@storybook/addon-docs/blocks'; + + + + + + diff --git a/packages/react/.storybook-v12/Welcome/welcome.scss b/packages/react/.storybook-v12/Welcome/welcome.scss new file mode 100644 index 000000000000..f502ed4cc59f --- /dev/null +++ b/packages/react/.storybook-v12/Welcome/welcome.scss @@ -0,0 +1,46 @@ +// +// Copyright IBM Corp. 2026 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@use '@carbon/styles/scss/type'; +@use '@carbon/styles/scss/colors'; +@use '@carbon/styles/scss/spacing'; + +.welcome__container { + position: fixed; + padding: spacing.$spacing-09; + background: url('../../.storybook/Welcome/carbon_bg.png') no-repeat center + center fixed; + background-size: cover; + block-size: 100vh; + inline-size: 100vw; + inset-block-start: 0; + inset-inline-start: 0; +} + +.welcome__heading { + @include type.type-style('productive-heading-07'); + + color: colors.$white; +} + +.welcome__heading--subtitle { + font-weight: 600; +} + +.welcome__link, +.welcome__link:hover, +.welcome__link.cds--link:visited { + color: colors.$blue-40; +} + +.welcome__links { + margin-block-start: spacing.$spacing-07; +} + +.v12-callout { + margin-block-start: spacing.$spacing-06; +} diff --git a/packages/react/.storybook-v12/main.ts b/packages/react/.storybook-v12/main.ts new file mode 100644 index 000000000000..de2f6a9a194b --- /dev/null +++ b/packages/react/.storybook-v12/main.ts @@ -0,0 +1,121 @@ +/** + * Copyright IBM Corp. 2026 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import type { StorybookConfig } from '@storybook/react-vite'; +import { mergeConfig } from 'vite'; + +import baseConfig from '../.storybook/main.ts'; + +const configDir = fileURLToPath(new URL('.', import.meta.url)); +const manifestPath = path.join(configDir, 'generated', 'manifest.json'); +const manifest = fs.existsSync(manifestPath) + ? JSON.parse(fs.readFileSync(manifestPath, 'utf8')) + : { replacedStories: [] }; +const deprecatedStoriesDir = path.join(configDir, 'deprecated'); +const replacedStories = new Set(manifest.replacedStories); +const replacedDocs = new Set( + manifest.replacedStories.map((story: string) => { + return story.replace(/\.stories\.[^.]+$/, '.mdx'); + }) +); +const v12FeatureFlags = `@use '@carbon/styles/scss/feature-flags' with ($feature-flags: ('enable-v12-release': true));\n`; +const v12FeatureFlagPattern = /\benable-v12-[a-z0-9-]+\b/; +const titlePatterns = { + Deprecated: /(?:title:\s*['"`]|[number] +) { + if (story === './Welcome/Welcome.mdx') { + return story; + } + + if (typeof story === 'string' && story.startsWith('./')) { + return `../.storybook/${story.slice(2)}`; + } + + return story; +} + +const stories = (baseConfig.stories ?? []) + .map(getV12StoryPath) + .filter((story) => { + return ( + typeof story !== 'string' || + (!isV12FeatureFlagStory(story) && + !hasTopLevelTitle(story, 'Deprecated') && + !replacedStories.has(story) && + !replacedDocs.has(story)) + ); + }) + .concat( + ...(fs.existsSync(deprecatedStoriesDir) + ? ['./deprecated/**/*.mdx', './deprecated/**/*.stories.@(js|jsx|ts|tsx)'] + : []), + './generated/**/*.stories.@(js|jsx|ts|tsx)' + ); + +const config: StorybookConfig = { + ...baseConfig, + stories, + async viteFinal(viteConfig, options) { + const baseViteConfig = baseConfig.viteFinal + ? await baseConfig.viteFinal(viteConfig, options) + : viteConfig; + + return mergeConfig(baseViteConfig, { + css: { + preprocessorOptions: { + scss: { + additionalData: v12FeatureFlags, + }, + }, + }, + }); + }, +}; + +export default config; diff --git a/packages/react/.storybook-v12/manager-head.html b/packages/react/.storybook-v12/manager-head.html new file mode 100644 index 000000000000..d7d7c1781b4a --- /dev/null +++ b/packages/react/.storybook-v12/manager-head.html @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/react/.storybook-v12/manager.js b/packages/react/.storybook-v12/manager.js new file mode 100644 index 000000000000..9d3bdab407fd --- /dev/null +++ b/packages/react/.storybook-v12/manager.js @@ -0,0 +1,16 @@ +/** + * Copyright IBM Corp. 2026 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { addons } from 'storybook/manager-api'; +import theme from './theme'; + +addons.setConfig({ + theme, +}); + +localStorage.removeItem('@storybook/ui/store'); +localStorage.removeItem('storybook-layout'); diff --git a/packages/react/.storybook-v12/preview-head.html b/packages/react/.storybook-v12/preview-head.html new file mode 100644 index 000000000000..a63712b85b14 --- /dev/null +++ b/packages/react/.storybook-v12/preview-head.html @@ -0,0 +1,13 @@ + diff --git a/packages/react/.storybook-v12/preview.js b/packages/react/.storybook-v12/preview.js new file mode 100644 index 000000000000..27204111ad24 --- /dev/null +++ b/packages/react/.storybook-v12/preview.js @@ -0,0 +1,84 @@ +/** + * Copyright IBM Corp. 2026, 2026 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { enable } from '@carbon/feature-flags'; +import React from 'react'; + +import basePreview, { argTypesEnhancers } from '../.storybook/preview'; +import { FeatureFlags } from '../src/components/FeatureFlags'; + +enable('enable-v12-release'); + +const v12ReleaseToolbar = { + name: '🚀 enable-v12-release', + description: 'Visual indicator for the v12 Storybook', + defaultValue: true, + toolbar: { + icon: 'flag', + title: '🚀 enable-v12-release', + items: [ + { + value: true, + title: 'This Storybook reflects work in progress for v12.', + }, + ], + dynamicTitle: false, + }, +}; + +const withV12Release = (Story, context) => ( + + + +); + +const { options: baseOptions = {}, ...baseParameters } = + basePreview.parameters ?? {}; +const baseOptionsWithoutStorySort = { ...baseOptions }; +delete baseOptionsWithoutStorySort.storySort; + +export const parameters = { + ...baseParameters, + options: { + ...baseOptionsWithoutStorySort, + storySort: { + method: 'alphabetical', + order: [ + 'Getting Started', + [ + 'Welcome', + 'Getting started', + 'Feature Flags', + 'Custom styles', + 'Carbon CDN style helpers', + 'Form participation', + ], + 'Components', + 'Deprecated', + 'Elements', + 'Helpers', + 'Hooks', + 'Layout', + 'Preview', + ], + }, + }, +}; +export const tags = ['autodocs']; + +const preview = { + ...basePreview, + parameters, + globalTypes: { + ...basePreview.globalTypes, + v12Release: v12ReleaseToolbar, + }, + decorators: [withV12Release, ...(basePreview.decorators ?? [])], +}; + +export { argTypesEnhancers }; +export default preview; diff --git a/packages/react/.storybook-v12/stories/Motion/Motion.featureflag.stories.js b/packages/react/.storybook-v12/stories/Motion/Motion.featureflag.stories.js new file mode 100644 index 000000000000..2c8f2bbc2441 --- /dev/null +++ b/packages/react/.storybook-v12/stories/Motion/Motion.featureflag.stories.js @@ -0,0 +1,18 @@ +/** + * Copyright IBM Corp. 2026 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ +import React from 'react'; + +export default { + title: 'Elements/Motion', +}; + +export const TBD = { + name: '🚀 TBD', + render: () => { + return

Motion guidance for v12 is TBD.

; + }, +}; diff --git a/packages/react/.storybook-v12/theme.js b/packages/react/.storybook-v12/theme.js new file mode 100644 index 000000000000..91b25d5b7f3e --- /dev/null +++ b/packages/react/.storybook-v12/theme.js @@ -0,0 +1,17 @@ +/** + * Copyright IBM Corp. 2026 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { create } from 'storybook/theming'; + +export default create({ + fontBase: "'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif", + fontCode: + "'IBM Plex Mono', Menlo, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', Courier, monospace", + brandTitle: '🚀 @carbon/react v2.x', + brandUrl: + 'https://github.com/carbon-design-system/carbon/tree/main/packages/react', +}); diff --git a/packages/react/.storybook/main.ts b/packages/react/.storybook/main.ts index bfb86f620dfa..78ea278c7e71 100644 --- a/packages/react/.storybook/main.ts +++ b/packages/react/.storybook/main.ts @@ -75,11 +75,6 @@ const config: StorybookConfig = { }, async viteFinal(config) { return mergeConfig(config, { - esbuild: { - include: /\.[jt]sx?$/, - exclude: [], - loader: 'tsx', - }, css: { preprocessorOptions: { scss: { @@ -98,8 +93,8 @@ const config: StorybookConfig = { }, }, optimizeDeps: { - esbuildOptions: { - loader: { + rolldownOptions: { + moduleTypes: { '.js': 'jsx', }, }, diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index d38ba42e8bd5..eaf69816975a 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -796,6 +796,9 @@ Map { "title": { "type": "string", }, + "translateWithId": { + "type": "func", + }, "warn": { "type": "bool", }, @@ -3575,6 +3578,9 @@ Map { "enableV12Overflowmenu": { "type": "bool", }, + "enableV12Release": { + "type": "bool", + }, "enableV12TileDefaultIcons": { "type": "bool", }, @@ -5891,6 +5897,7 @@ Map { "size": { "args": [ [ + "xs", "sm", "md", "lg", @@ -7925,6 +7932,9 @@ Map { "pagesUnknown": { "type": "bool", }, + "renderPageSelect": { + "type": "func", + }, "size": { "args": [ [ @@ -8457,6 +8467,9 @@ Map { "type": "bool", }, "slug": [Function], + "translateWithId": { + "type": "func", + }, "value": { "args": [ [ @@ -8862,6 +8875,9 @@ Map { "type": "oneOf", }, "slug": [Function], + "translateWithId": { + "type": "func", + }, "warn": { "type": "bool", }, @@ -11269,6 +11285,9 @@ Map { "toggled": { "type": "bool", }, + "translateWithId": { + "type": "func", + }, }, }, "ToggleSkeleton" => { @@ -11842,6 +11861,9 @@ Map { "enableV12Overflowmenu": { "type": "bool", }, + "enableV12Release": { + "type": "bool", + }, "enableV12TileDefaultIcons": { "type": "bool", }, @@ -14074,6 +14096,9 @@ Map { "enableV12Overflowmenu": { "type": "bool", }, + "enableV12Release": { + "type": "bool", + }, "enableV12TileDefaultIcons": { "type": "bool", }, diff --git a/packages/react/code-connect/DataTable/TableCell.figma.tsx b/packages/react/code-connect/DataTable/TableCell.figma.tsx index d7873c0e55a0..add447c111eb 100644 --- a/packages/react/code-connect/DataTable/TableCell.figma.tsx +++ b/packages/react/code-connect/DataTable/TableCell.figma.tsx @@ -15,8 +15,8 @@ figma.connect( 'https://www.figma.com/design/YAnB1jKx0yCUL29j6uSLpg/(v11)-All-themes---Carbon-Design-System?node-id=6172-291044&t=5C1lVaCoSygb13J1-4', { props: { - children: figma.instance('Swap slot', { - true: figma.boolean('Slot'), + children: figma.boolean('Slot', { + true: figma.instance('Swap slot'), }), text: figma.boolean('Show text', { true: figma.string('Cell text'), diff --git a/packages/react/code-connect/FileUploader/FileUploader.figma.tsx b/packages/react/code-connect/FileUploader/FileUploader.figma.tsx index 0b3c4e67784f..495e0db22242 100644 --- a/packages/react/code-connect/FileUploader/FileUploader.figma.tsx +++ b/packages/react/code-connect/FileUploader/FileUploader.figma.tsx @@ -29,7 +29,7 @@ figma.connect( Small: 'sm', }), disabled: figma.enum('State', { - Disabled: 'disabled', + Disabled: true, }), }, example: ({ ...props }) => , @@ -80,18 +80,15 @@ figma.connect( errorBody: figma.string('Long desc.'), errorSubject: figma.string('Short desc.'), name: figma.string('Long file name'), - filename113480: figma.string('File name'), size: figma.enum('Size', { Large: 'lg', Medium: 'md', Small: 'sm', }), - state: figma.enum('State', { + status: figma.enum('State', { Uploaded: 'edit', Loading: 'uploading', - Success: 'complete', - 'Error short': 'error-short', - 'Error long': 'error-long', + Complete: 'complete', }), invalid: figma.enum('State', { 'Error short': true, diff --git a/packages/react/code-connect/Slider/Slider.figma.tsx b/packages/react/code-connect/Slider/Slider.figma.tsx index bd65b6f49776..42323e6fed49 100644 --- a/packages/react/code-connect/Slider/Slider.figma.tsx +++ b/packages/react/code-connect/Slider/Slider.figma.tsx @@ -58,8 +58,6 @@ figma.connect( false: true, true: false, }), - invalidText: figma.string('Error text'), - warnText: figma.string('Warning text'), disabled: figma.enum('State', { Disabled: true, }), @@ -81,7 +79,7 @@ figma.connect( } ); -//single +// single skeleton figma.connect( SliderSkeleton, 'https://www.figma.com/design/YAnB1jKx0yCUL29j6uSLpg/(v11)-All-themes---Carbon-Design-System?node-id=3673-40574&m=dev', @@ -91,7 +89,7 @@ figma.connect( } ); -//two handle +// two handle skeleton figma.connect( SliderSkeleton, 'https://www.figma.com/design/YAnB1jKx0yCUL29j6uSLpg/(v11)-All-themes---Carbon-Design-System?node-id=41061-1531&m=dev', diff --git a/packages/react/code-connect/StructuredList/StructuredList.figma.tsx b/packages/react/code-connect/StructuredList/StructuredList.figma.tsx index 8150f6a0f668..40430cf1ab28 100644 --- a/packages/react/code-connect/StructuredList/StructuredList.figma.tsx +++ b/packages/react/code-connect/StructuredList/StructuredList.figma.tsx @@ -108,12 +108,9 @@ figma.connect( { props: { children: figma.children('Col*'), - disabled: figma.enum('State', { - Disabled: true, - }), }, - example: ({ children, disabled }) => ( - {children} + example: ({ children }) => ( + {children} ), } ); diff --git a/packages/react/code-connect/TreeView/TreeView.figma.tsx b/packages/react/code-connect/TreeView/TreeView.figma.tsx index cbd508bbfe49..fabe6e4a2f7e 100644 --- a/packages/react/code-connect/TreeView/TreeView.figma.tsx +++ b/packages/react/code-connect/TreeView/TreeView.figma.tsx @@ -25,8 +25,8 @@ figma.connect( }, example: ({ children, treeNode }) => ( - {/* Figma component doesn't currently nest TreeNodes accurately - code sample below is incomplete */} + {/* TODO: Figma component doesn't currently nest TreeNodes accurately + code sample below is incomplete (Issue #17607). */} {children} ), diff --git a/packages/react/code-connect/UIShell/SideNavMenuItem.figma.tsx b/packages/react/code-connect/UIShell/SideNavMenuItem.figma.tsx index 71883c1ca5e8..ec391a13e07b 100644 --- a/packages/react/code-connect/UIShell/SideNavMenuItem.figma.tsx +++ b/packages/react/code-connect/UIShell/SideNavMenuItem.figma.tsx @@ -7,7 +7,7 @@ // @ts-nocheck import React from 'react'; -import { SideNavMenu, SideNavMenuItem } from '@carbon/react'; +import { SideNavLink, SideNavMenu, SideNavMenuItem } from '@carbon/react'; import figma from '@figma/code-connect'; figma.connect( @@ -29,7 +29,7 @@ figma.connect( ); figma.connect( - SideNavMenuItem, + SideNavLink, 'https://www.figma.com/design/YAnB1jKx0yCUL29j6uSLpg/(v11)-All-themes---Carbon-Design-System?node-id=2346-16194&t=wcK3P98b09VsrxXF-4', { variant: { 'Icon left': 'True' }, @@ -40,9 +40,9 @@ figma.connect( }), }, example: ({ linkText, isActive }) => ( - + {linkText} - + ), } ); diff --git a/packages/react/package.json b/packages/react/package.json index f8ac712bf905..805cced62ab0 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/react", "description": "React components for the Carbon Design System", - "version": "1.111.0", + "version": "1.112.0", "license": "Apache-2.0", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -38,10 +38,13 @@ }, "scripts": { "build": "yarn clean && node tasks/build-styles.js && node tasks/build.js", - "clean": "rimraf es lib icons/index.js icons/index.d.ts icons/index.esm.js storybook-static", + "clean": "rimraf es lib icons/index.js icons/index.d.ts icons/index.esm.js storybook-static storybook-static-v12 .storybook-v12/generated", "postinstall": "ibmtelemetry --config=telemetry.yml", - "storybook": "storybook dev -p 3000", + "storybook": "storybook dev -p 3011", "storybook:build": "storybook build", + "storybook:v12": "yarn storybook:v12:prepare && storybook dev -p 3012 --config-dir .storybook-v12", + "storybook:v12:build": "yarn storybook:v12:prepare && storybook build --config-dir .storybook-v12 --output-dir storybook-static-v12", + "storybook:v12:prepare": "node ../../tasks/prepare-v12-storybook.mjs .", "telemetry:config": "npx -y @ibm/telemetry-js-config-generator generate --id fd89d12b-6a39-48b4-adf4-30c94dc0dddd --endpoint https://www-api.ibm.com/ibm-telemetry/v1/metrics --files ./src/components/**/*.(tsx|js|jsx)" }, "peerDependencies": { @@ -52,16 +55,16 @@ }, "dependencies": { "@babel/runtime": "^7.27.3", - "@carbon/feature-flags": "^1.4.0", - "@carbon/icons-react": "^11.83.0", - "@carbon/layout": "^11.54.0", - "@carbon/styles": "^1.110.0", - "@carbon/utilities": "^0.21.0", + "@carbon/feature-flags": "^1.5.0", + "@carbon/icons-react": "^11.84.0", + "@carbon/layout": "^11.55.0", + "@carbon/styles": "^1.111.0", + "@carbon/utilities": "^0.22.0", "@floating-ui/react": "^0.27.4", "@ibm/telemetry-js": "^1.5.0", "classnames": "2.5.1", "copy-to-clipboard": "^3.3.1", - "downshift": "9.0.10", + "downshift": "9.4.0", "es-toolkit": "^1.27.0", "flatpickr": "4.6.13", "invariant": "^2.2.3", @@ -79,7 +82,7 @@ "@babel/preset-react": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@carbon/test-utils": "^10.41.0", - "@carbon/themes": "^11.76.0", + "@carbon/themes": "^11.77.0", "@figma/code-connect": "^1.4.7", "@stackblitz/sdk": "^1.11.0", "@storybook/addon-a11y": "^10.3.5", @@ -111,7 +114,7 @@ "tinyclip": "^0.1.12", "tsdown": "^0.22.0", "typescript": "^6.0.3", - "typescript-config-carbon": "workspace:^0.10.0", + "typescript-config-carbon": "workspace:^0.11.0", "use-sync-external-store": "^1.5.0", "vite": "^8.0.0" }, diff --git a/packages/react/src/components/Button/Button.tsx b/packages/react/src/components/Button/Button.tsx index d6c66d8028af..a8dee2b2f811 100644 --- a/packages/react/src/components/Button/Button.tsx +++ b/packages/react/src/components/Button/Button.tsx @@ -180,8 +180,15 @@ const Button: ButtonComponent = React.forwardRef( const iconOnlyImage = !ButtonImageElement ? null : ; if (!isIconOnlyButton(hasIconOnly, kind)) { - const { tooltipAlignment, ...propsWithoutTooltipAlignment } = props; - return ; + const { + autoAlign, + tooltipAlignment, + tooltipDropShadow, + tooltipHighContrast, + tooltipPosition, + ...propsWithoutIconOnlyTooltipProps + } = props; + return ; } else { let align: PopoverAlignment | undefined = undefined; diff --git a/packages/react/src/components/Button/__tests__/Button-test.js b/packages/react/src/components/Button/__tests__/Button-test.js index 8a23cd8da46b..3111489426ac 100644 --- a/packages/react/src/components/Button/__tests__/Button-test.js +++ b/packages/react/src/components/Button/__tests__/Button-test.js @@ -85,6 +85,11 @@ describe('Button', () => { expect(screen.getByRole('button')).toBeInTheDocument(); }); + it('should not pass tooltipPosition to the button element when hasIconOnly=false', () => { + render( + {readOnly && ( + + {translateWithId?.('carbon.dropdown.read-only') ?? 'Read only'} + + )} {slug ? ( normalizedDecorator ) : decorator ? ( diff --git a/packages/react/src/components/Dropdown/__tests__/Dropdown-test.js b/packages/react/src/components/Dropdown/__tests__/Dropdown-test.js index 645c9bb14d06..d957b7c920f2 100644 --- a/packages/react/src/components/Dropdown/__tests__/Dropdown-test.js +++ b/packages/react/src/components/Dropdown/__tests__/Dropdown-test.js @@ -679,6 +679,31 @@ describe('Test useEffect ', () => { }); }); +describe('Dropdown readOnly accessibility', () => { + it('should announce readOnly state to screen readers without using aria-disabled', async () => { + render( + + ); + await waitForPosition(); + + const readOnlyText = document.querySelector('.cds--visually-hidden'); + expect(readOnlyText).toBeInTheDocument(); + expect(readOnlyText).toHaveTextContent('Read only'); + + const button = screen.getByRole('combobox'); + expect(button).not.toHaveAttribute('aria-disabled', 'true'); + expect(button.getAttribute('aria-describedby')).toContain( + readOnlyText.getAttribute('id') + ); + }); +}); + describe('Validation message ids', () => { const mockProps = { id: 'test-dropdown', @@ -719,3 +744,25 @@ describe('Validation message ids', () => { ); }); }); + +describe('Dropdown readOnly translation', () => { + it('should support a custom translateWithId for the read-only text', async () => { + render( + + id === 'carbon.dropdown.read-only' ? 'Custom read only' : '' + } + /> + ); + await waitForPosition(); + + const readOnlyText = document.querySelector('.cds--visually-hidden'); + expect(readOnlyText).toBeInTheDocument(); + expect(readOnlyText).toHaveTextContent('Custom read only'); + }); +}); diff --git a/packages/react/src/components/FeatureFlags/__tests__/FeatureFlags-test.js b/packages/react/src/components/FeatureFlags/__tests__/FeatureFlags-test.js index b33dd78d5bef..d79e96916339 100644 --- a/packages/react/src/components/FeatureFlags/__tests__/FeatureFlags-test.js +++ b/packages/react/src/components/FeatureFlags/__tests__/FeatureFlags-test.js @@ -117,6 +117,97 @@ describe('FeatureFlags', () => { }); }); + it('should enable v12 flags with the v12 release flag', () => { + const checkFlags = jest.fn(); + const checkFlag = jest.fn(); + + function TestComponent() { + const featureFlags = useFeatureFlags(); + const enableV12Release = useFeatureFlag('enable-v12-release'); + const enableV12Overflowmenu = useFeatureFlag('enable-v12-overflowmenu'); + const enableTreeviewControllable = useFeatureFlag( + 'enable-treeview-controllable' + ); + + checkFlags({ + enableV12Release: featureFlags.enabled('enable-v12-release'), + enableV12Overflowmenu: featureFlags.enabled('enable-v12-overflowmenu'), + enableTreeviewControllable: featureFlags.enabled( + 'enable-treeview-controllable' + ), + }); + + checkFlag({ + enableV12Release, + enableV12Overflowmenu, + enableTreeviewControllable, + }); + + return null; + } + + render( + + + + ); + + expect(checkFlags).toHaveBeenLastCalledWith({ + enableV12Release: true, + enableV12Overflowmenu: true, + enableTreeviewControllable: false, + }); + expect(checkFlag).toHaveBeenLastCalledWith({ + enableV12Release: true, + enableV12Overflowmenu: true, + enableTreeviewControllable: false, + }); + }); + + it('should inherit the v12 release flag through nested scopes', () => { + const checkFlag = jest.fn(); + + function TestComponent() { + const enableV12Overflowmenu = useFeatureFlag('enable-v12-overflowmenu'); + + checkFlag(enableV12Overflowmenu); + + return null; + } + + render( + + + + + + ); + + expect(checkFlag).toHaveBeenLastCalledWith(true); + }); + + it('should allow nested scopes to disable the v12 release flag', () => { + const checkFlag = jest.fn(); + + function TestComponent() { + const enableV12Overflowmenu = useFeatureFlag('enable-v12-overflowmenu'); + + checkFlag(enableV12Overflowmenu); + + return null; + } + + render( + + + + + + ); + + expect(checkFlag).toHaveBeenLastCalledWith(false); + }); + it('should re-render when flags change', () => { const checkFlags = jest.fn(); const checkFlag = jest.fn(); @@ -199,6 +290,8 @@ describe('FeatureFlags', () => { enableTreeviewControllable: true, }); + // Inner scope adds enableV12Overflowmenu but does not specify + // enableTreeviewControllable, so the parent's true value is inherited. render( @@ -209,9 +302,11 @@ describe('FeatureFlags', () => { expect(checkFlag).toHaveBeenLastCalledWith({ enableV12Overflowmenu: true, - enableTreeviewControllable: false, + enableTreeviewControllable: true, }); + // Explicitly setting a flag to false in an inner scope overrides the + // parent scope's true value. render( @@ -230,6 +325,40 @@ describe('FeatureFlags', () => { }); }); + it('should inherit parent scope flags not specified by a nested FeatureFlags scope', () => { + // When a component wraps itself in a FeatureFlags to enable an internal flag, + // it should not shadow flags set by the consumer in an outer FeatureFlags scope. + const checkFlag = jest.fn(); + + function TestComponent() { + const enableDialogElement = useFeatureFlag('enable-dialog-element'); + const enableFocusWrapWithoutSentinels = useFeatureFlag( + 'enable-focus-wrap-without-sentinels' + ); + + checkFlag({ enableDialogElement, enableFocusWrapWithoutSentinels }); + + return null; + } + + render( + // Consumer enables enableDialogElement in an outer scope + + {/* Component internally wraps with FeatureFlags to set enableFocusWrapWithoutSentinels */} + + + + + ); + + // Both flags should be true: enableDialogElement inherited from outer scope, + // enableFocusWrapWithoutSentinels set by inner scope. + expect(checkFlag).toHaveBeenLastCalledWith({ + enableDialogElement: true, + enableFocusWrapWithoutSentinels: true, + }); + }); + it('should update nested scope when parent scope changes', () => { const checkFlag = jest.fn(); const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); @@ -355,6 +484,56 @@ describe('FeatureFlags', () => { }); describe('should support a prop for each feature flag', () => { + it('enable-v12-release - enableV12Release', () => { + const checkFlags = jest.fn(); + const checkFlag = jest.fn(); + + function TestComponent() { + const featureFlags = useFeatureFlags(); + const enableV12Release = useFeatureFlag('enable-v12-release'); + + checkFlags({ + enableV12Release: featureFlags.enabled('enable-v12-release'), + }); + + checkFlag({ + enableV12Release, + }); + + return null; + } + + // Render the default + const { rerender } = render( + + + + ); + + // Ensure the default value is as defined and as expected + expect(checkFlags).toHaveBeenLastCalledWith({ + enableV12Release: false, + }); + expect(checkFlag).toHaveBeenLastCalledWith({ + enableV12Release: false, + }); + + // Enable the flag + rerender( + + + + ); + + // Ensure that when enabled, this flag does not error + expect(checkFlags).toHaveBeenLastCalledWith({ + enableV12Release: true, + }); + expect(checkFlag).toHaveBeenLastCalledWith({ + enableV12Release: true, + }); + }); + it('enable-v12-tile-default-icons - enableV12TileDefaultIcons', () => { const checkFlags = jest.fn(); const checkFlag = jest.fn(); diff --git a/packages/react/src/components/FeatureFlags/index.tsx b/packages/react/src/components/FeatureFlags/index.tsx index 65733107576a..8c7d74575bef 100644 --- a/packages/react/src/components/FeatureFlags/index.tsx +++ b/packages/react/src/components/FeatureFlags/index.tsx @@ -21,6 +21,7 @@ import { deprecate } from '../../prop-types/deprecate'; export interface FeatureFlagsProps { children?: ReactNode; flags?: Record; + enableV12Release?: boolean; enableV12TileDefaultIcons?: boolean; enableV12TileRadioIcons?: boolean; enableV12Overflowmenu?: boolean; @@ -44,6 +45,22 @@ type FeatureFlagScope = typeof GlobalFeatureFlags; */ const FeatureFlagContext = createContext(GlobalFeatureFlags); +// Maps each camelCase prop name to its kebab-case feature flag key. +const PROP_TO_FLAG: Record = { + enableV12Release: 'enable-v12-release', + enableV12TileDefaultIcons: 'enable-v12-tile-default-icons', + enableV12TileRadioIcons: 'enable-v12-tile-radio-icons', + enableV12Overflowmenu: 'enable-v12-overflowmenu', + enableTreeviewControllable: 'enable-treeview-controllable', + enableExperimentalFocusWrapWithoutSentinels: + 'enable-experimental-focus-wrap-without-sentinels', + enableFocusWrapWithoutSentinels: 'enable-focus-wrap-without-sentinels', + enableDialogElement: 'enable-dialog-element', + enableV12DynamicFloatingStyles: 'enable-v12-dynamic-floating-styles', + enableEnhancedFileUploader: 'enable-enhanced-file-uploader', + enablePresence: 'enable-presence', +}; + /** * Supports an object of feature flag values with the `flags` prop, merging them * along with the current `FeatureFlagContext` to provide consumers to check if @@ -51,40 +68,56 @@ const FeatureFlagContext = createContext(GlobalFeatureFlags); */ export const FeatureFlags = ({ children, - flags = {}, - enableV12TileDefaultIcons = false, - enableV12TileRadioIcons = false, - enableV12Overflowmenu = false, - enableTreeviewControllable = false, - enableExperimentalFocusWrapWithoutSentinels = false, - enableFocusWrapWithoutSentinels = false, - enableDialogElement = false, - enableV12DynamicFloatingStyles = false, - enableEnhancedFileUploader = false, - enablePresence = false, + flags, + enableV12Release, + enableV12TileDefaultIcons, + enableV12TileRadioIcons, + enableV12Overflowmenu, + enableTreeviewControllable, + enableExperimentalFocusWrapWithoutSentinels, + enableFocusWrapWithoutSentinels, + enableDialogElement, + enableV12DynamicFloatingStyles, + enableEnhancedFileUploader, + enablePresence, }: FeatureFlagsProps) => { const parentScope = useContext(FeatureFlagContext); const scope = useMemo(() => { - const combinedFlags = { - 'enable-v12-tile-default-icons': enableV12TileDefaultIcons, - 'enable-v12-tile-radio-icons': enableV12TileRadioIcons, - 'enable-v12-overflowmenu': enableV12Overflowmenu, - 'enable-treeview-controllable': enableTreeviewControllable, - 'enable-experimental-focus-wrap-without-sentinels': - enableExperimentalFocusWrapWithoutSentinels, - 'enable-focus-wrap-without-sentinels': enableFocusWrapWithoutSentinels, - 'enable-dialog-element': enableDialogElement, - 'enable-v12-dynamic-floating-styles': enableV12DynamicFloatingStyles, - 'enable-enhanced-file-uploader': enableEnhancedFileUploader, - 'enable-presence': enablePresence, - ...flags, + // Only include flags that were explicitly provided (not undefined). This + // ensures that unspecified props do not shadow flags set by a parent + // FeatureFlags scope, which is the correct behaviour for nested scopes. + const flagProps = { + enableV12Release, + enableV12TileDefaultIcons, + enableV12TileRadioIcons, + enableV12Overflowmenu, + enableTreeviewControllable, + enableExperimentalFocusWrapWithoutSentinels, + enableFocusWrapWithoutSentinels, + enableDialogElement, + enableV12DynamicFloatingStyles, + enableEnhancedFileUploader, + enablePresence, }; + const explicitFlags: Record = {}; + + for (const [prop, flagKey] of Object.entries(PROP_TO_FLAG)) { + const value = (flagProps as Record)[prop]; + if (value !== undefined) { + explicitFlags[flagKey] = value; + } + } + + if (flags) { + Object.assign(explicitFlags, flags); + } - const scope = createScope(combinedFlags) as FeatureFlagScope; + const scope = createScope(explicitFlags); scope.mergeWithScope(parentScope); return scope; }, [ + enableV12Release, enableV12TileDefaultIcons, enableV12TileRadioIcons, enableV12Overflowmenu, @@ -117,6 +150,7 @@ FeatureFlags.propTypes = { 'been deprecated. Please run the `featureflag-deprecate-flags-prop` codemod to migrate to individual boolean props.' + `npx @carbon/upgrade migrate featureflag-deprecate-flags-prop --write` ), + enableV12Release: PropTypes.bool, enableV12TileDefaultIcons: PropTypes.bool, enableV12TileRadioIcons: PropTypes.bool, enableV12Overflowmenu: PropTypes.bool, diff --git a/packages/react/src/components/IconButton/index.tsx b/packages/react/src/components/IconButton/index.tsx index bbe3bb554b1d..dd95784c2727 100644 --- a/packages/react/src/components/IconButton/index.tsx +++ b/packages/react/src/components/IconButton/index.tsx @@ -366,7 +366,7 @@ IconButton.propTypes = { /** * Specify the size of the Button. */ - size: PropTypes.oneOf(['sm', 'md', 'lg']), + size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']), /** * Optionally specify a `target` when using an `` element. diff --git a/packages/react/src/components/ListBox/next/ListBoxSelection.tsx b/packages/react/src/components/ListBox/next/ListBoxSelection.tsx index bd7fc4ace605..d32ef8e49190 100644 --- a/packages/react/src/components/ListBox/next/ListBoxSelection.tsx +++ b/packages/react/src/components/ListBox/next/ListBoxSelection.tsx @@ -80,6 +80,12 @@ export interface ListBoxSelectionProps extends TranslateWithId { * clear selection element fires a mouseup event */ onMouseUp?: React.MouseEventHandler; + + /** + * Specify an optional `onMouseDown` handler that is called when the underlying + * clear selection element fires a mousedown event + */ + onMouseDown?: React.MouseEventHandler; } function ListBoxSelection({ @@ -190,8 +196,14 @@ ListBoxSelection.propTypes = { onClick: PropTypes.func, /** - * Specify an optional `onClick` handler that is called when the underlying - * clear selection element is clicked + * Specify an optional `onMouseDown` handler that is called when the underlying + * clear selection element fires a mousedown event + */ + onMouseDown: PropTypes.func, + + /** + * Specify an optional `onMouseUp` handler that is called when the underlying + * clear selection element fires a mouseup event */ onMouseUp: PropTypes.func, diff --git a/packages/react/src/components/Modal/Modal-test.js b/packages/react/src/components/Modal/Modal-test.js index ea343574a0c1..f48cd60881c6 100644 --- a/packages/react/src/components/Modal/Modal-test.js +++ b/packages/react/src/components/Modal/Modal-test.js @@ -1670,3 +1670,39 @@ describe.each([ expect(onRequestClose).toHaveBeenCalled(); }); }); + +describe('enableDialogElement role attribute', () => { + it('should preserve native dialog attributes for non-alert modals', () => { + render( + + +

Body

+
+
+ ); + + const modal = screen.getByRole('dialog'); + + expect(modal).toBeInTheDocument(); + expect(modal).not.toHaveAttribute('role'); + expect(modal).not.toHaveAttribute('aria-describedby'); + }); + + it('should set alertdialog attributes for alert modals', () => { + render( + + +

Body

+
+
+ ); + + const modal = screen.getByRole('alertdialog'); + const modalBodyId = modal.getAttribute('aria-describedby'); + + expect(modal).toBeInTheDocument(); + expect(modal).toHaveAttribute('role', 'alertdialog'); + expect(modalBodyId).toMatch(/^cds--modal-body--modal-id-/); + expect(document.getElementById(modalBodyId)).toHaveTextContent('Body'); + }); +}); diff --git a/packages/react/src/components/Modal/Modal.tsx b/packages/react/src/components/Modal/Modal.tsx index 7d5d9840f0f8..0e7adb24e9a8 100644 --- a/packages/react/src/components/Modal/Modal.tsx +++ b/packages/react/src/components/Modal/Modal.tsx @@ -738,8 +738,8 @@ const ModalDialog = React.forwardRef(function ModalDialog( focusAfterCloseRef={launcherButtonRef} modal ref={innerModal} - role={isAlertDialog ? 'alertdialog' : ''} - aria-describedby={isAlertDialog ? modalBodyId : ''} + role={isAlertDialog ? 'alertdialog' : undefined} + aria-describedby={isAlertDialog ? modalBodyId : undefined} className={containerClasses} aria-label={ariaLabel} data-exiting={presenceContext?.isExiting || undefined}> diff --git a/packages/react/src/components/Modal/ModalPresence.stories.scss b/packages/react/src/components/Modal/ModalPresence.stories.scss index 347109856a17..67799718fe35 100644 --- a/packages/react/src/components/Modal/ModalPresence.stories.scss +++ b/packages/react/src/components/Modal/ModalPresence.stories.scss @@ -8,6 +8,7 @@ @use '@carbon/react/scss/config' with ( $prefix: 'dialog-refactor' ); +@use '@carbon/react/scss/layout'; @use '@carbon/react/scss/components/modal/modal'; @use '@carbon/react/scss/components/dialog/dialog'; @use '@carbon/react/scss/components/text-input/text-input'; diff --git a/packages/react/src/components/MultiSelect/FilterableMultiSelect.tsx b/packages/react/src/components/MultiSelect/FilterableMultiSelect.tsx index 3c56cf34e2f6..b133595179d9 100644 --- a/packages/react/src/components/MultiSelect/FilterableMultiSelect.tsx +++ b/packages/react/src/components/MultiSelect/FilterableMultiSelect.tsx @@ -12,8 +12,6 @@ import Downshift, { useMultipleSelection, type UseComboboxProps, type UseMultipleSelectionProps, - UseComboboxInterface, - UseComboboxStateChangeTypes, UseMultipleSelectionInterface, } from 'downshift'; import isEqual from 'react-fast-compare'; @@ -89,9 +87,8 @@ const { InputChange, InputKeyDownEscape, FunctionSetHighlightedIndex, -} = useCombobox.stateChangeTypes as UseComboboxInterface['stateChangeTypes'] & { - ToggleButtonClick: UseComboboxStateChangeTypes.ToggleButtonClick; -}; + FunctionSetInputValue, +} = useCombobox.stateChangeTypes; const { SelectedItemKeyDownBackspace, @@ -105,7 +102,9 @@ export interface FilterableMultiSelectProps extends MultiSelectSortingProps, React.RefAttributes, TranslateWithId< - ListBoxSelectionTranslationKey | ListBoxMenuIconTranslationKey + | ListBoxSelectionTranslationKey + | ListBoxMenuIconTranslationKey + | 'carbon.multi-select.read-only' > { /** * Specify a label to be read by screen readers on the container node @@ -596,6 +595,7 @@ export const FilterableMultiSelect = forwardRef(function FilterableMultiSelect< ); const menuId = `${id}__menu`; const inputId = `${id}-input`; + const readOnlyId = `${id}-readonly-text`; useEffect(() => { if (!isOpen) { @@ -817,11 +817,14 @@ export const FilterableMultiSelect = forwardRef(function FilterableMultiSelect< event?: KeyboardEvent | MouseEvent ) { const value = textInput.current?.value; - if ( - value?.length === 1 || - (event && 'key' in event && match(event, keys.Escape)) - ) { + const isEscape = event && 'key' in event && match(event, keys.Escape); + const isClick = value && !(event && 'key' in event); + if (value?.length === 1 || isEscape || isClick) { setInputValue(''); + onInputValueChange?.({ + inputValue: '', + type: FunctionSetInputValue, + }); } else { setInputValue(value ?? ''); } @@ -1027,6 +1030,12 @@ export const FilterableMultiSelect = forwardRef(function FilterableMultiSelect< { + event.preventDefault(); + }} onMouseUp={(event: MouseEvent) => { // If we do not stop this event from propagating, // it seems like Downshift takes our event and @@ -1139,6 +1151,11 @@ export const FilterableMultiSelect = forwardRef(function FilterableMultiSelect< {!inline && showHelperText ? helper : null} + {readOnly && ( + + {translateWithId?.('carbon.multi-select.read-only') ?? 'Read only'} + + )} ); }) as { diff --git a/packages/react/src/components/MultiSelect/MultiSelect.tsx b/packages/react/src/components/MultiSelect/MultiSelect.tsx index c6870844ebcd..375f76b1f7f3 100644 --- a/packages/react/src/components/MultiSelect/MultiSelect.tsx +++ b/packages/react/src/components/MultiSelect/MultiSelect.tsx @@ -98,7 +98,9 @@ interface OnChangeData { export interface MultiSelectProps extends MultiSelectSortingProps, TranslateWithId< - ListBoxMenuIconTranslationKey | ListBoxSelectionTranslationKey + | ListBoxMenuIconTranslationKey + | ListBoxSelectionTranslationKey + | 'carbon.multi-select.read-only' > { /** * **Experimental**: Will attempt to automatically align the floating @@ -572,6 +574,7 @@ export const MultiSelect = React.forwardRef( ? undefined : `multiselect-helper-text-${multiSelectInstanceId}`; const fieldLabelId = `multiselect-field-label-${multiSelectInstanceId}`; + const readOnlyId = `${id}-readonly-text`; const helperClasses = cx(`${prefix}--form__helper-text`, { [`${prefix}--form__helper-text--disabled`]: disabled, }); @@ -806,11 +809,14 @@ export const MultiSelect = React.forwardRef( type="button" className={`${prefix}--list-box__field`} disabled={disabled} - aria-disabled={disabled || readOnly} + aria-disabled={disabled || undefined} + {...toggleButtonProps} aria-describedby={ - !inline && showHelperText ? helperId : undefined + cx(toggleButtonProps['aria-describedby'], { + [helperId as string]: !inline && showHelperText && helperId, + [readOnlyId]: readOnly, + }) || undefined } - {...toggleButtonProps} ref={mergedRef} {...readOnlyEventHandlers}> @@ -893,6 +899,11 @@ export const MultiSelect = React.forwardRef( {helperText} )} + {readOnly && ( + + {translateWithId?.('carbon.multi-select.read-only') ?? 'Read only'} + + )} ); } diff --git a/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js b/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js index 98fa77a355c9..a4fed635257f 100644 --- a/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js +++ b/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js @@ -7,6 +7,7 @@ import React, { StrictMode } from 'react'; import { act, render, screen } from '@testing-library/react'; +import { useCombobox } from 'downshift'; import { getByText } from '@carbon/test-utils/dom'; import userEvent from '@testing-library/user-event'; import { FilterableMultiSelect } from '../'; @@ -87,6 +88,35 @@ describe('FilterableMultiSelect', () => { ).toBeFalsy(); }); + it('should announce readonly state to screen readers', async () => { + const items = generateItems(4, generateGenericItem); + const label = 'test-label'; + const { container } = render( + + ); + await waitForPosition(); + + // The visually-hidden "Read only" text node should exist + // eslint-disable-next-line testing-library/no-node-access + const readOnlyText = container.querySelector('#test-readonly-text'); + expect(readOnlyText).toBeInTheDocument(); + expect(readOnlyText).toHaveClass('cds--visually-hidden'); + expect(readOnlyText).toHaveTextContent('Read only'); + + // The focusable combobox input should reference the readonly text and be aria-readonly + // eslint-disable-next-line testing-library/no-node-access + const input = container.querySelector('input'); + expect(input).toHaveAttribute('aria-readonly', 'true'); + expect(input.getAttribute('aria-describedby')).toContain( + 'test-readonly-text' + ); + }); + it('should display helper text instead of warning when disabled', async () => { render( { ); }); + it('should call onInputValueChange with empty string when clear button is clicked', async () => { + const onInputValueChange = jest.fn(); + render( + + ); + await waitForPosition(); + + const input = screen.getByPlaceholderText('test'); + + await openMenu(); + await userEvent.type(input, 'abc'); + expect(input).toHaveDisplayValue('abc'); + + onInputValueChange.mockClear(); + + const clearButton = screen.getByRole('button', { + name: 'Clear selected item', + }); + await userEvent.click(clearButton); + + expect(input).toHaveDisplayValue(''); + expect(onInputValueChange).toHaveBeenCalledWith({ + inputValue: '', + type: useCombobox.stateChangeTypes.FunctionSetInputValue, + }); + expect(onInputValueChange).toHaveBeenCalledTimes(1); + }); + it('should clear all selections when clicking clear all button', async () => { const initialSelectedItems = [mockProps.items[0], mockProps.items[1]]; render( @@ -1499,3 +1561,26 @@ describe('FilterableMultiSelect', () => { .forEach((opt) => expect(opt).toHaveAttribute('aria-selected', 'false')); }); }); + +describe('FilterableMultiSelect readOnly translation', () => { + it('should support a custom translateWithId for the read-only text', async () => { + const items = generateItems(4, generateGenericItem); + const { container } = render( + + id === 'carbon.multi-select.read-only' ? 'Custom read only' : '' + } + /> + ); + await waitForPosition(); + + // eslint-disable-next-line testing-library/no-node-access + const readOnlyText = container.querySelector('#test-readonly-text'); + expect(readOnlyText).toBeInTheDocument(); + expect(readOnlyText).toHaveTextContent('Custom read only'); + }); +}); diff --git a/packages/react/src/components/MultiSelect/__tests__/MultiSelect-test.js b/packages/react/src/components/MultiSelect/__tests__/MultiSelect-test.js index f948b72b20f7..eb36bf2a97b1 100644 --- a/packages/react/src/components/MultiSelect/__tests__/MultiSelect-test.js +++ b/packages/react/src/components/MultiSelect/__tests__/MultiSelect-test.js @@ -515,6 +515,28 @@ describe('MultiSelect', () => { ).toBeFalsy(); }); + it('should announce readonly state to screen readers', async () => { + const items = generateItems(4, generateGenericItem); + const { container } = render( + + ); + await waitForPosition(); + + const readOnlyText = container.querySelector(`.${prefix}--visually-hidden`); + expect(readOnlyText).toBeInTheDocument(); + expect(readOnlyText).toHaveTextContent('Read only'); + expect(readOnlyText).toHaveAttribute('id', 'test-readonly-text'); + + // The focusable control should reference the readonly description text and + // must not be announced as disabled purely because it is read-only. + // eslint-disable-next-line testing-library/no-node-access + const button = container.querySelector('.cds--list-box__field'); + expect(button).not.toHaveAttribute('aria-disabled', 'true'); + expect(button.getAttribute('aria-describedby')).toContain( + 'test-readonly-text' + ); + }); + describe('Component API', () => { it('should set the default selected items with the `initialSelectedItems` prop', async () => { const items = generateItems(4, generateGenericItem); @@ -1694,3 +1716,25 @@ describe('MultiSelect', () => { }); }); }); + +describe('MultiSelect readOnly translation', () => { + it('should support a custom translateWithId for the read-only text', async () => { + const items = generateItems(4, generateGenericItem); + const { container } = render( + + id === 'carbon.multi-select.read-only' ? 'Custom read only' : '' + } + /> + ); + await waitForPosition(); + + const readOnlyText = container.querySelector(`.${prefix}--visually-hidden`); + expect(readOnlyText).toBeInTheDocument(); + expect(readOnlyText).toHaveTextContent('Custom read only'); + }); +}); diff --git a/packages/react/src/components/NumberInput/NumberInput.tsx b/packages/react/src/components/NumberInput/NumberInput.tsx index 737427cffebd..ba54e4e55bb9 100644 --- a/packages/react/src/components/NumberInput/NumberInput.tsx +++ b/packages/react/src/components/NumberInput/NumberInput.tsx @@ -41,6 +41,7 @@ import { isComponentElement } from '../../internal'; const translationIds = { 'increment.number': 'increment.number', 'decrement.number': 'decrement.number', + 'carbon.number-input.read-only': 'carbon.number-input.read-only', } as const; type TranslationKey = keyof typeof translationIds; @@ -48,6 +49,7 @@ type TranslationKey = keyof typeof translationIds; const defaultTranslations: Record = { [translationIds['increment.number']]: 'Increment number', [translationIds['decrement.number']]: 'Decrement number', + [translationIds['carbon.number-input.read-only']]: 'Read only', }; const defaultTranslateWithId: TFunc = (messageId) => { @@ -736,6 +738,11 @@ const NumberInput = React.forwardRef( ariaDescribedBy = helperText ? normalizedProps.helperId : undefined; } + const readOnlyId = `${id}-readonly-text`; + const inputDescribedBy = readOnly + ? cx(readOnlyId, ariaDescribedBy) || undefined + : ariaDescribedBy; + function handleOnChange(event) { if (disabled) { return; @@ -916,7 +923,7 @@ const NumberInput = React.forwardRef( {...rest} data-invalid={normalizedProps.invalid ? true : undefined} aria-invalid={normalizedProps.invalid} - aria-describedby={ariaDescribedBy} + aria-describedby={inputDescribedBy} aria-readonly={readOnly} disabled={normalizedProps.disabled} ref={ref} @@ -1033,6 +1040,11 @@ const NumberInput = React.forwardRef( type={type} value={type === 'number' ? value : inputValue} /> + {readOnly && ( + + {t('carbon.number-input.read-only')} + + )} {slug ? ( normalizedDecorator ) : decorator ? ( diff --git a/packages/react/src/components/NumberInput/__tests__/NumberInput-test.js b/packages/react/src/components/NumberInput/__tests__/NumberInput-test.js index de61449477f3..4a1c8f5f5623 100644 --- a/packages/react/src/components/NumberInput/__tests__/NumberInput-test.js +++ b/packages/react/src/components/NumberInput/__tests__/NumberInput-test.js @@ -27,6 +27,10 @@ function translateWithId(id) { return 'decrement'; } + if (id === 'carbon.number-input.read-only') { + return 'Read only'; + } + throw new Error(`Unknown message id: ${id}`); } @@ -454,6 +458,37 @@ describe('NumberInput', () => { ); }); + it('should announce the readonly state to screen readers via a visually-hidden description', () => { + render(); + + const readOnlyText = screen.getByText('Read only'); + expect(readOnlyText).toBeInTheDocument(); + expect(readOnlyText).toHaveClass('cds--visually-hidden'); + expect(readOnlyText).toHaveAttribute('id'); + + const input = screen.getByLabelText('test-label'); + expect(input.getAttribute('aria-describedby')).toContain( + readOnlyText.getAttribute('id') + ); + }); + + it('should support custom translation of the readonly state via translateWithId', () => { + const translateWithId = (id) => + id === 'carbon.number-input.read-only' ? 'Nur lesen' : id; + render( + + ); + + const readOnlyText = screen.getByText('Nur lesen'); + expect(readOnlyText).toBeInTheDocument(); + expect(readOnlyText).toHaveClass('cds--visually-hidden'); + }); + it('should set the defaultValue of the with `defaultValue`', () => { render(); expect(screen.getByLabelText('test-label')).toHaveValue(5); diff --git a/packages/react/src/components/OverflowMenu/OverflowMenu-test.js b/packages/react/src/components/OverflowMenu/OverflowMenu-test.js index c3dc24954ae4..7400745e60f1 100644 --- a/packages/react/src/components/OverflowMenu/OverflowMenu-test.js +++ b/packages/react/src/components/OverflowMenu/OverflowMenu-test.js @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2016, 2023 + * Copyright IBM Corp. 2016, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -204,7 +204,8 @@ describe('OverflowMenu', () => { ); expect(screen.getByRole('button')).toHaveClass( - `cds--overflow-menu--${size}` + `cds--overflow-menu--${size}`, // TODO: V12 - Remove this check + `cds--layout--size-${size}` ); }); }); diff --git a/packages/react/src/components/OverflowMenu/OverflowMenu.tsx b/packages/react/src/components/OverflowMenu/OverflowMenu.tsx index dec0633dd19f..cd09978bbc17 100644 --- a/packages/react/src/components/OverflowMenu/OverflowMenu.tsx +++ b/packages/react/src/components/OverflowMenu/OverflowMenu.tsx @@ -14,6 +14,7 @@ import React, { useCallback, useContext, useEffect, + useReducer, useRef, useState, type ElementType, @@ -280,7 +281,7 @@ export const OverflowMenu = forwardRef( open: openProp, renderIcon: IconElement = OverflowMenuVertical, selectorPrimaryFocus = '[data-floating-menu-primary-focus]', - size = 'md', + size, innerRef, ...other }, @@ -300,6 +301,8 @@ export const OverflowMenu = forwardRef( /** The element ref of the tooltip's trigger button. */ const triggerRef = useRef(null); const wrapperRef = useRef(null); + const [, forceUpdate] = useReducer((x) => x + 1, 0); + const resizeObserverRef = useRef(null); // Sync open prop changes. useEffect(() => { @@ -316,6 +319,36 @@ export const OverflowMenu = forwardRef( } }, []); + /** + * When `size` is brought in from a contextual layout token, FloatingMenu can't detect size changes + * while the menu is open since there is no state/prop update. This ResizeObserver watches + * the trigger button when the menu is open and calls `forceUpdate()` on resize to force a re-render if its + * size changes while the menu is open + */ + + useEffect(() => { + resizeObserverRef.current = new ResizeObserver(() => { + forceUpdate(); + }); + + return () => { + resizeObserverRef.current?.disconnect(); + resizeObserverRef.current = null; + }; + }, []); + + useEffect(() => { + if (!triggerRef.current || !resizeObserverRef.current) { + return; + } + + if (open) { + resizeObserverRef.current.observe(triggerRef.current); + } else { + resizeObserverRef.current.disconnect(); + } + }, [open]); + useEffect(() => { if (open && !prevOpenState.current) { onOpen(); @@ -496,7 +529,9 @@ export const OverflowMenu = forwardRef( const triggerEl = triggerRef.current; if (triggerEl instanceof Element) { return ( - triggerEl.closest('[data-floating-menu-container]') || document.body + triggerEl.closest('[data-floating-menu-container]') || + triggerEl.closest(`.${prefix}--layout`) || + document.body ); } return document.body; @@ -510,7 +545,8 @@ export const OverflowMenu = forwardRef( { [`${prefix}--overflow-menu--open`]: open, [`${prefix}--overflow-menu--light`]: light, - [`${prefix}--overflow-menu--${size}`]: size, + [`${prefix}--overflow-menu--${size}`]: size, // TODO: V12 - Remove this class + [`${prefix}--layout--size-${size}`]: size, } ); @@ -521,7 +557,8 @@ export const OverflowMenu = forwardRef( [`${prefix}--overflow-menu--flip`]: flipped, [`${prefix}--overflow-menu-options--open`]: open, [`${prefix}--overflow-menu-options--light`]: light, - [`${prefix}--overflow-menu-options--${size}`]: size, + [`${prefix}--overflow-menu-options--${size}`]: size, // TODO: V12 - Remove this class + [`${prefix}--layout--size-${size}`]: size, } ); @@ -530,6 +567,10 @@ export const OverflowMenu = forwardRef( iconClass ); + const overflowMenuWrapperClasses = classNames( + `${prefix}--overflow-menu__wrapper` + ); + const childrenWithProps = Children.toArray(children).map((child, index) => { if (isValidElement(child)) { const childElement = child as ReactElement; @@ -580,7 +621,7 @@ export const OverflowMenu = forwardRef( return ( <> ( { [`${prefix}--overflow-menu--open`]: open, }, - size !== defaultSize && `${prefix}--overflow-menu--${size}` + size !== defaultSize && `${prefix}--overflow-menu--${size}`, // TODO: V12 - Remove this class + size !== defaultSize && `${prefix}--layout--size-${size}` ); const floatingRef = mergeRefs(triggerRef, refs.setReference); diff --git a/packages/react/src/components/Pagination/Pagination.tsx b/packages/react/src/components/Pagination/Pagination.tsx index bb98beab4e09..616c8212e1ec 100644 --- a/packages/react/src/components/Pagination/Pagination.tsx +++ b/packages/react/src/components/Pagination/Pagination.tsx @@ -9,6 +9,7 @@ import { CaretLeft, CaretRight } from '@carbon/icons-react'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { IconButton } from '../IconButton'; +import { clamp } from '../../internal/clamp'; import PropTypes from 'prop-types'; import Select from '../Select'; import SelectItem from '../SelectItem'; @@ -116,6 +117,23 @@ export interface PaginationProps */ pageSelectLabelText?: (total: number) => string; + /** + * Provide a custom render function for the page-selection control. + * When provided, the default ` - {selectItems} - + {renderPageSelect ? ( + renderPageSelect({ + currentPage: page, + totalPages, + currentPageSize: pageSize, + pageSelectLabelText: pageSelectLabelText(totalPages), + onSetPage: handleSetPage, + }) + ) : ( + + )} {pageRangeText(page, totalPages)} @@ -643,6 +685,12 @@ Pagination.propTypes = { */ pageSelectLabelText: PropTypes.func, + /** + * Provide a custom render function for the page-selection control. + * When provided, the default ` + {readOnly && ( + + {t('carbon.select.read-only')} + + )} {normalizedProps.invalid && ( @@ -462,6 +491,13 @@ Select.propTypes = { PropTypes.node, 'The `slug` prop has been deprecated and will be removed in the next major version. Use the decorator prop instead.' ), + + /** + * Optional prop to specify the translation function for internationalization. + * Currently used to translate the read-only screen reader announcement. + */ + translateWithId: PropTypes.func, + /** * Specify whether the control is currently in warning state */ diff --git a/packages/react/src/components/Select/__tests__/Select-test.js b/packages/react/src/components/Select/__tests__/Select-test.js index af9e5a1d5ce6..b7ce2460b22c 100644 --- a/packages/react/src/components/Select/__tests__/Select-test.js +++ b/packages/react/src/components/Select/__tests__/Select-test.js @@ -303,6 +303,43 @@ describe('Select', () => { expect(onChange).toHaveBeenCalledTimes(0); }); + it('should announce readOnly state via a visually-hidden description', () => { + const { container } = render( + + ); + + const readOnlyText = container.querySelector( + `.${prefix}--visually-hidden` + ); + expect(readOnlyText).toBeInTheDocument(); + expect(readOnlyText).toHaveTextContent('Read only'); + + const theSelect = screen.getByRole('combobox'); + expect(theSelect.getAttribute('aria-describedby')).toContain( + readOnlyText.getAttribute('id') + ); + }); + + it('supports translateWithId for the read-only screen reader text', () => { + const { container } = render( + + ); + + const readOnlyText = container.querySelector( + `.${prefix}--visually-hidden` + ); + expect(readOnlyText).toBeInTheDocument(); + expect(readOnlyText).toHaveTextContent('Solo lectura'); + }); + it('should respect size prop', () => { const { container } = render( ${!placeholder || value ? undefined @@ -494,6 +506,11 @@ class CDSSelect extends FormMixin(LitElement) { `} ${this._renderItems(this)} + ${readonly + ? html`${readOnlyText}` + : null} ${iconLoader(ChevronDown16, { class: `${prefix}--select__arrow`, 'aria-hidden': 'true', diff --git a/packages/web-components/src/components/slider/__tests__/slider-test.js b/packages/web-components/src/components/slider/__tests__/slider-test.js index 72388d234423..854bae50e8f4 100644 --- a/packages/web-components/src/components/slider/__tests__/slider-test.js +++ b/packages/web-components/src/components/slider/__tests__/slider-test.js @@ -29,6 +29,122 @@ describe('cds-slider', () => { expect(input).to.have.attribute('data-invalid'); }); + it('should announce readonly state to screen readers', async () => { + const el = await fixture( + html` + + ` + ); + await el.updateComplete; + + const readonlyText = el?.shadowRoot?.querySelector('.cds--visually-hidden'); + expect(readonlyText).to.exist; + expect(readonlyText.textContent.trim()).to.equal('Read only'); + expect(readonlyText.id).to.equal('readonly-text'); + + const thumb = el?.shadowRoot?.querySelector('[role="slider"]'); + expect(thumb).to.exist; + expect(thumb.getAttribute('aria-readonly')).to.equal('true'); + expect(thumb.getAttribute('aria-describedby')).to.contain('readonly-text'); + }); + + it('should support a custom read-only text for screen readers', async () => { + const el = await fixture( + html` + + ` + ); + await el.updateComplete; + + const readonlyText = el?.shadowRoot?.querySelector('.cds--visually-hidden'); + expect(readonlyText).to.exist; + expect(readonlyText.textContent.trim()).to.equal('Schreibgeschützt'); + }); + + it('should announce readonly state on a range (two-handle) slider', async () => { + const el = await fixture( + html` + + + ` + ); + await el.updateComplete; + + const readonlyText = el?.shadowRoot?.querySelector('.cds--visually-hidden'); + expect(readonlyText).to.exist; + expect(readonlyText.textContent.trim()).to.equal('Read only'); + + // Both the lower and upper thumbs should reference the readonly description. + const thumbs = el?.shadowRoot?.querySelectorAll('[role="slider"]'); + expect(thumbs.length).to.equal(2); + thumbs.forEach((thumb) => { + expect(thumb.getAttribute('aria-readonly')).to.equal('true'); + expect(thumb.getAttribute('aria-describedby')).to.contain( + 'readonly-text' + ); + }); + + const upperThumb = el?.shadowRoot?.querySelector('#thumb-upper'); + expect(upperThumb).to.exist; + expect(upperThumb.getAttribute('aria-readonly')).to.equal('true'); + }); + + it('should announce readonly state when text inputs are hidden (tooltip thumbs)', async () => { + const el = await fixture( + html` ` + ); + await el.updateComplete; + + const readonlyText = el?.shadowRoot?.querySelector('.cds--visually-hidden'); + expect(readonlyText).to.exist; + expect(readonlyText.textContent.trim()).to.equal('Read only'); + + const thumbs = el?.shadowRoot?.querySelectorAll('[role="slider"]'); + expect(thumbs.length).to.equal(2); + thumbs.forEach((thumb) => { + expect(thumb.getAttribute('aria-readonly')).to.equal('true'); + expect(thumb.getAttribute('aria-describedby')).to.contain( + 'readonly-text' + ); + }); + }); + it('should display warn state and message', async () => { const el = await fixture( html` ${labelText}
+ ${readonly + ? html`${readOnlyText}` + : null} ${unstable_valueUpper || unstable_valueUpper === '' ? html` ` : ''} @@ -1020,6 +1033,10 @@ class CDSSlider extends HostListenerMixin(FormMixin(FocusMixin(LitElement))) { class="${thumbLowerClasses}" role="slider" tabindex="${!readonly ? 0 : -1}" + aria-readonly=${ifDefined(readonly ? 'true' : undefined)} + aria-describedby=${ifDefined( + readonly ? 'readonly-text' : undefined + )} aria-valuemax="${max}" aria-valuemin="${min}" aria-valuenow="${value}" @@ -1063,6 +1080,10 @@ class CDSSlider extends HostListenerMixin(FormMixin(FocusMixin(LitElement))) { class="${thumbLowerClasses}" role="slider" tabindex="${!readonly ? 0 : -1}" + aria-readonly=${ifDefined(readonly ? 'true' : undefined)} + aria-describedby=${ifDefined( + readonly ? 'readonly-text' : undefined + )} aria-valuemax="${max}" aria-valuemin="${min}" aria-valuenow="${value}" @@ -1107,6 +1128,10 @@ class CDSSlider extends HostListenerMixin(FormMixin(FocusMixin(LitElement))) { class="${thumbUpperClasses}" role="slider" tabindex="${!readonly ? 0 : -1}" + aria-readonly=${ifDefined(readonly ? 'true' : undefined)} + aria-describedby=${ifDefined( + readonly ? 'readonly-text' : undefined + )} aria-valuemax="${max}" aria-valuemin="${min}" aria-valuenow="${unstable_valueUpper}" @@ -1149,6 +1174,10 @@ class CDSSlider extends HostListenerMixin(FormMixin(FocusMixin(LitElement))) { class="${thumbUpperClasses}" role="slider" tabindex="${!readonly ? 0 : -1}" + aria-readonly=${ifDefined(readonly ? 'true' : undefined)} + aria-describedby=${ifDefined( + readonly ? 'readonly-text' : undefined + )} aria-valuemax="${max}" aria-valuemin="${min}" aria-valuenow="${unstable_valueUpper}" diff --git a/packages/web-components/src/components/slug/slug.scss b/packages/web-components/src/components/slug/slug.scss index b942d1e32624..925d3cc04dd2 100644 --- a/packages/web-components/src/components/slug/slug.scss +++ b/packages/web-components/src/components/slug/slug.scss @@ -12,7 +12,7 @@ @use '@carbon/styles/scss/motion' as *; @use '@carbon/styles/scss/spacing' as *; @use '@carbon/styles/scss/utilities/convert' as *; -@use '@carbon/styles/scss/layout' as *; +@use '@carbon/styles/scss/utilities/layout' as *; @use '@carbon/styles/scss/utilities/ai-gradient' as *; @use '@carbon/styles/scss/components/slug/index' as *; diff --git a/packages/web-components/src/components/tabs/tabs.scss b/packages/web-components/src/components/tabs/tabs.scss index 4936b22089cc..8cbc9dbdef5b 100644 --- a/packages/web-components/src/components/tabs/tabs.scss +++ b/packages/web-components/src/components/tabs/tabs.scss @@ -16,12 +16,15 @@ @use '@carbon/styles/scss/components/tabs/index'; @use '@carbon/styles/scss/components/tabs/vars' as *; @use '@carbon/styles/scss/utilities/button-reset'; -@use '@carbon/styles/scss/layout' as *; +@use '@carbon/styles/scss/utilities/layout' as *; @use '@carbon/styles/scss/utilities/layout'; @use '@carbon/styles/scss/utilities/convert'; @use '@carbon/styles/scss/utilities/skeleton' as *; @use '@carbon/styles/scss/utilities/update_fields_on_layer' as *; +@include emit-layout-context-classes(); +@include emit-layout-context-classes('density'); + $inset-transition: inset 110ms motion(standard, productive); :host(#{$prefix}-tabs), diff --git a/packages/web-components/src/components/text-input/text-input.scss b/packages/web-components/src/components/text-input/text-input.scss index 05f8685a65bf..35a1cca191e9 100644 --- a/packages/web-components/src/components/text-input/text-input.scss +++ b/packages/web-components/src/components/text-input/text-input.scss @@ -12,9 +12,11 @@ $css--plex: true !default; @use '@carbon/styles/scss/utilities/convert' as *; @use '@carbon/styles/scss/utilities' as *; @use '@carbon/styles/scss/components/text-input/index'; -@use '@carbon/styles/scss/layout' as *; +@use '@carbon/styles/scss/utilities/layout' as *; @use '@carbon/styles/scss/utilities/ai-gradient' as *; +@include emit-layout-context-classes(); + :host(#{$prefix}-text-input), :host(#{$prefix}-text-input-skeleton) { @include emit-layout-tokens(); diff --git a/packages/web-components/src/components/textarea/textarea.scss b/packages/web-components/src/components/textarea/textarea.scss index 6426f8198c04..5b19c8202e17 100644 --- a/packages/web-components/src/components/textarea/textarea.scss +++ b/packages/web-components/src/components/textarea/textarea.scss @@ -7,7 +7,7 @@ $css--plex: true !default; @use '@carbon/styles/scss/config' as *; -@use '@carbon/styles/scss/layout' as *; +@use '@carbon/styles/scss/utilities/layout' as *; @use '@carbon/styles/scss/spacing' as *; @use '@carbon/styles/scss/components/text-area/index'; @use '@carbon/styles/scss/utilities/ai-gradient' as *; diff --git a/packages/web-components/src/components/tile/tile.scss b/packages/web-components/src/components/tile/tile.scss index 71593d6e1c38..708429ca72bf 100644 --- a/packages/web-components/src/components/tile/tile.scss +++ b/packages/web-components/src/components/tile/tile.scss @@ -16,7 +16,7 @@ $css--plex: true !default; @use '@carbon/styles/scss/components/form/index' as *; @use '@carbon/styles/scss/utilities/ai-gradient' as *; @use '@carbon/styles/scss/theme' as *; -@use '@carbon/styles/scss/layout' as *; +@use '@carbon/styles/scss/utilities/layout' as *; :host(#{$prefix}-tile) { @extend .#{$prefix}--tile; diff --git a/packages/web-components/src/components/time-picker/time-picker.scss b/packages/web-components/src/components/time-picker/time-picker.scss index 27ef8ef8ab6d..0e004e23dc45 100644 --- a/packages/web-components/src/components/time-picker/time-picker.scss +++ b/packages/web-components/src/components/time-picker/time-picker.scss @@ -13,7 +13,7 @@ $css--plex: true !default; @use '@carbon/styles/scss/utilities' as *; @use '@carbon/styles/scss/components/time-picker/index'; @use '@carbon/styles/scss/components/select'; -@use '@carbon/styles/scss/layout' as *; +@use '@carbon/styles/scss/utilities/layout' as *; @use '@carbon/styles/scss/utilities/convert' as convert; :host(#{$prefix}-time-picker) { diff --git a/packages/web-components/src/components/toggle/__tests__/toggle-test.js b/packages/web-components/src/components/toggle/__tests__/toggle-test.js index 116e1270f535..3733ca8a0fec 100644 --- a/packages/web-components/src/components/toggle/__tests__/toggle-test.js +++ b/packages/web-components/src/components/toggle/__tests__/toggle-test.js @@ -145,6 +145,17 @@ describe('cds-toggle', () => { expect(el.toggled).to.be.false; }); + it('should support a custom read-only-text', async () => { + const el = await fixture(html` + + `); + await el.updateComplete; + + const visuallyHidden = el.shadowRoot.querySelector('.cds--visually-hidden'); + expect(visuallyHidden).to.exist; + expect(visuallyHidden.textContent.trim()).to.equal('Solo lectura'); + }); + it('can be controlled with using toggled', async () => { const el = await fixture(html` `); await el.updateComplete; diff --git a/packages/web-components/src/components/toggle/toggle.ts b/packages/web-components/src/components/toggle/toggle.ts index aa0fee490d5c..910131db118c 100644 --- a/packages/web-components/src/components/toggle/toggle.ts +++ b/packages/web-components/src/components/toggle/toggle.ts @@ -131,6 +131,12 @@ class CDSToggle extends HostListenerMixin(CDSCheckbox) { @property({ reflect: true, attribute: 'read-only', type: Boolean }) readOnly = false; + /** + * The text announced to screen readers when the component is read-only. + */ + @property({ attribute: 'read-only-text' }) + readOnlyText = 'Read only'; + /** * Toggle size. */ @@ -198,6 +204,7 @@ class CDSToggle extends HostListenerMixin(CDSCheckbox) { labelA, labelB, value, + readOnly, _handleChange: handleChange, } = this; const inputClasses = classMap({ @@ -235,12 +242,19 @@ class CDSToggle extends HostListenerMixin(CDSCheckbox) { type="button" aria-checked=${toggled} aria-labelledby=${ifDefined(ariaLabelledby)} + aria-readonly=${ifDefined(readOnly ? 'true' : undefined)} + aria-describedby=${ifDefined(readOnly ? 'readonly-text' : undefined)} .checked=${toggled} name="${ifDefined(name)}" value="${ifDefined(value)}" ?disabled=${disabled} id="${id}" @click=${handleChange}> + ${readOnly + ? html`${this.readOnlyText}` + : null}