diff --git a/.all-contributorsrc b/.all-contributorsrc index 77c4f7f785b5..f4a82e66915b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1415,6 +1415,16 @@ "contributions": [ "code" ] + }, + { + "login": "ggdawson", + "name": "Garrett Dawson", + "avatar_url": "https://avatars.githubusercontent.com/u/37080130?v=4", + "profile": "https://github.com/ggdawson", + "contributions": [ + "code", + "doc" + ] } ], "commitConvention": "none" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd6b3857f34f..8a17318f04e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Run yarn dedupe @@ -30,7 +30,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies @@ -43,7 +43,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies @@ -58,7 +58,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0 @@ -85,7 +85,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0 @@ -125,7 +125,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0 @@ -188,7 +188,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index de2e3b85b0b2..f75d96a1918e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -24,9 +24,9 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 #v3.24.0 + uses: github/codeql-action/init@379614612a29c9e28f31f39a59013eb8012a51f0 #v3.24.3 with: languages: javascript - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 #v3.24.0 + uses: github/codeql-action/analyze@379614612a29c9e28f31f39a59013eb8012a51f0 #v3.24.3 diff --git a/.github/workflows/deploy-packages.yml b/.github/workflows/deploy-packages.yml index 6781ae9709d9..d9823f06f4a6 100644 --- a/.github/workflows/deploy-packages.yml +++ b/.github/workflows/deploy-packages.yml @@ -21,7 +21,7 @@ jobs: repository: carbon-design-system/design-language-website ref: master - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' @@ -39,7 +39,7 @@ jobs: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.APP_PRIVATE_KEY }} - name: Create Pull Request - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 #v5.0.2 + uses: peter-evans/create-pull-request@b1ddad2c994a25fbc81a28b3ec0e368bb2021c50 #v6.0.0 with: branch: 'release/update-carbon-deps' commit-message: 'chore(release): update carbon deps' @@ -63,7 +63,7 @@ jobs: repository: carbon-design-system/gatsby-theme-carbon ref: main - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' @@ -82,7 +82,7 @@ jobs: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.APP_PRIVATE_KEY }} - name: Create Pull Request - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 #v5.0.2 + uses: peter-evans/create-pull-request@b1ddad2c994a25fbc81a28b3ec0e368bb2021c50 #v6.0.0 with: branch: 'release/update-carbon-deps' commit-message: 'chore(release): update carbon deps' diff --git a/.github/workflows/deploy-react-storybook.yml b/.github/workflows/deploy-react-storybook.yml index 02b5211b8c97..d04a0b891d6f 100644 --- a/.github/workflows/deploy-react-storybook.yml +++ b/.github/workflows/deploy-react-storybook.yml @@ -30,7 +30,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies diff --git a/.github/workflows/nightly-release.yml b/.github/workflows/nightly-release.yml index d9f118448b2a..2816a90fe7ae 100644 --- a/.github/workflows/nightly-release.yml +++ b/.github/workflows/nightly-release.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4bca12705be0..21ae99786698 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/sync-generated-files.yml b/.github/workflows/sync-generated-files.yml index 7bc3159fb16b..5f93e2234309 100644 --- a/.github/workflows/sync-generated-files.yml +++ b/.github/workflows/sync-generated-files.yml @@ -9,7 +9,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies diff --git a/.github/workflows/v10-ci.yml b/.github/workflows/v10-ci.yml index c8c27ad4f281..1bc0c4d41a4d 100644 --- a/.github/workflows/v10-ci.yml +++ b/.github/workflows/v10-ci.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Run yarn dedupe @@ -29,7 +29,7 @@ jobs: with: ref: v10 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies @@ -44,7 +44,7 @@ jobs: with: ref: v10 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies @@ -61,7 +61,7 @@ jobs: with: ref: v10 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0 @@ -89,7 +89,7 @@ jobs: with: ref: v10 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0 diff --git a/.github/workflows/v10-deploy-react-storybook.yml b/.github/workflows/v10-deploy-react-storybook.yml index 5c23c5f3b198..1b4190f98bd1 100644 --- a/.github/workflows/v10-deploy-react-storybook.yml +++ b/.github/workflows/v10-deploy-react-storybook.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' - name: Install dependencies diff --git a/.github/workflows/v10-release.yml b/.github/workflows/v10-release.yml index 60ba2e4423fd..acc9af5c9163 100644 --- a/.github/workflows/v10-release.yml +++ b/.github/workflows/v10-release.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 57c4b01b20c7..228b53de3b3c 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -32,7 +32,7 @@ jobs: with: fetch-depth: '0' - name: Use Node.js 20.x - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 #v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 #v4.0.2 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' @@ -64,7 +64,7 @@ jobs: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.APP_PRIVATE_KEY }} - name: Create Pull Request - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 #v5.0.2 + uses: peter-evans/create-pull-request@b1ddad2c994a25fbc81a28b3ec0e368bb2021c50 #v6.0.0 with: branch: 'release/${{ github.event.inputs.tag }}' commit-message: 'chore(release): ${{ github.event.inputs.tag }}' diff --git a/.husky/commit-msg b/.husky/commit-msg index 0bd658f49625..36158d92becf 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx --no-install commitlint --edit "$1" +npx --no-install commitlint --edit "$1" \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index d37daa075e22..40672d9ff325 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx --no-install lint-staged +npx --no-install lint-staged \ No newline at end of file diff --git a/.yarn/cache/cosmiconfig-npm-9.0.0-47d78cf275-8bdf1dfbb6.zip b/.yarn/cache/cosmiconfig-npm-9.0.0-47d78cf275-8bdf1dfbb6.zip new file mode 100644 index 000000000000..42026d71dac1 Binary files /dev/null and b/.yarn/cache/cosmiconfig-npm-9.0.0-47d78cf275-8bdf1dfbb6.zip differ diff --git a/.yarn/cache/env-paths-npm-2.2.0-ac4ed99068-ba2aea3830.zip b/.yarn/cache/env-paths-npm-2.2.0-ac4ed99068-ba2aea3830.zip deleted file mode 100644 index f87c9d947958..000000000000 Binary files a/.yarn/cache/env-paths-npm-2.2.0-ac4ed99068-ba2aea3830.zip and /dev/null differ diff --git a/.yarn/cache/env-paths-npm-2.2.1-7c7577428c-65b5df55a8.zip b/.yarn/cache/env-paths-npm-2.2.1-7c7577428c-65b5df55a8.zip new file mode 100644 index 000000000000..5fecf17a478a Binary files /dev/null and b/.yarn/cache/env-paths-npm-2.2.1-7c7577428c-65b5df55a8.zip differ diff --git a/.yarn/cache/eslint-plugin-playwright-npm-0.22.1-2faafdc4a4-4833fe0c7b.zip b/.yarn/cache/eslint-plugin-playwright-npm-0.22.1-2faafdc4a4-4833fe0c7b.zip new file mode 100644 index 000000000000..81a19247d0bb Binary files /dev/null and b/.yarn/cache/eslint-plugin-playwright-npm-0.22.1-2faafdc4a4-4833fe0c7b.zip differ diff --git a/.yarn/cache/follow-redirects-npm-1.15.2-1ec1dd82be-8be0d39919.zip b/.yarn/cache/follow-redirects-npm-1.15.2-1ec1dd82be-8be0d39919.zip new file mode 100644 index 000000000000..9aa62ac845da Binary files /dev/null and b/.yarn/cache/follow-redirects-npm-1.15.2-1ec1dd82be-8be0d39919.zip differ diff --git a/.yarn/cache/follow-redirects-npm-1.15.4-3384d6a415-2e8f5f259a.zip b/.yarn/cache/follow-redirects-npm-1.15.4-3384d6a415-2e8f5f259a.zip deleted file mode 100644 index 5bb42e70fe83..000000000000 Binary files a/.yarn/cache/follow-redirects-npm-1.15.4-3384d6a415-2e8f5f259a.zip and /dev/null differ diff --git a/.yarn/cache/globals-npm-13.20.0-4565a722e7-9df85cde2f.zip b/.yarn/cache/globals-npm-13.20.0-4565a722e7-9df85cde2f.zip deleted file mode 100644 index 2620ceb8823f..000000000000 Binary files a/.yarn/cache/globals-npm-13.20.0-4565a722e7-9df85cde2f.zip and /dev/null differ diff --git a/.yarn/cache/globals-npm-13.24.0-cc7713139c-62c5b1997d.zip b/.yarn/cache/globals-npm-13.24.0-cc7713139c-62c5b1997d.zip new file mode 100644 index 000000000000..bac6ed348c80 Binary files /dev/null and b/.yarn/cache/globals-npm-13.24.0-cc7713139c-62c5b1997d.zip differ diff --git a/.yarn/cache/husky-npm-8.0.0-1b3b519c6d-bd147a59a5.zip b/.yarn/cache/husky-npm-8.0.0-1b3b519c6d-bd147a59a5.zip deleted file mode 100644 index f6e9db61a276..000000000000 Binary files a/.yarn/cache/husky-npm-8.0.0-1b3b519c6d-bd147a59a5.zip and /dev/null differ diff --git a/.yarn/cache/husky-npm-9.0.10-93b6062488-c303f1862e.zip b/.yarn/cache/husky-npm-9.0.10-93b6062488-c303f1862e.zip new file mode 100644 index 000000000000..00f17c3d3a7b Binary files /dev/null and b/.yarn/cache/husky-npm-9.0.10-93b6062488-c303f1862e.zip differ diff --git a/.yarn/cache/klona-npm-2.0.5-5d403f2d77-27cc78ea2d.zip b/.yarn/cache/klona-npm-2.0.5-5d403f2d77-27cc78ea2d.zip deleted file mode 100644 index ff2e6b944adb..000000000000 Binary files a/.yarn/cache/klona-npm-2.0.5-5d403f2d77-27cc78ea2d.zip and /dev/null differ diff --git a/.yarn/cache/postcss-loader-npm-7.0.0-e0a0c61fcd-a1d3de713e.zip b/.yarn/cache/postcss-loader-npm-7.0.0-e0a0c61fcd-a1d3de713e.zip deleted file mode 100644 index afb206040637..000000000000 Binary files a/.yarn/cache/postcss-loader-npm-7.0.0-e0a0c61fcd-a1d3de713e.zip and /dev/null differ diff --git a/.yarn/cache/postcss-loader-npm-8.0.0-195ad96888-efbb59d2b7.zip b/.yarn/cache/postcss-loader-npm-8.0.0-195ad96888-efbb59d2b7.zip new file mode 100644 index 000000000000..aa6abd99f964 Binary files /dev/null and b/.yarn/cache/postcss-loader-npm-8.0.0-195ad96888-efbb59d2b7.zip differ diff --git a/README.md b/README.md index 1264581aa702..a2adeec35a52 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,9 @@ check out our [Contributing Guide](/.github/CONTRIBUTING.md) and our
Alexandr Ovchinnikov

💻
J Thomas

💻 + +
Garrett Dawson

💻 📖 + diff --git a/config/eslint-config-carbon/internal.js b/config/eslint-config-carbon/internal.js index 85e321825d58..ad91d07bb129 100644 --- a/config/eslint-config-carbon/internal.js +++ b/config/eslint-config-carbon/internal.js @@ -14,5 +14,6 @@ module.exports = { require.resolve('./plugins/react'), require.resolve('./plugins/storybook'), require.resolve('./plugins/testing-library'), + require.resolve('./plugins/eslint-plugin-playwright'), ], }; diff --git a/config/eslint-config-carbon/plugins/eslint-plugin-playwright.js b/config/eslint-config-carbon/plugins/eslint-plugin-playwright.js new file mode 100644 index 000000000000..a43e27ce1fb5 --- /dev/null +++ b/config/eslint-config-carbon/plugins/eslint-plugin-playwright.js @@ -0,0 +1,34 @@ +const prefixes = [ + '@avt-default-state', + '@avt-advanced-states', + '@avt-keyboard-nav', +]; +const prefixesList = prefixes.join(' | \n'); + +module.exports = { + plugins: ['eslint-plugin-playwright'], + overrides: [ + { + extends: ['plugin:playwright/recommended'], + files: ['*-test.avt.e2e.js'], + rules: { + 'playwright/valid-title': [ + 'error', + { + mustMatch: { + describe: [ + /^(\s*@avt(\s+\S+)*\s*)$/.source, + `Describe titles should start with "@avt" prefix`, + ], + test: [ + /^(\s*(@avt-default-state||@avt-advanced-states||@avt-keyboard-nav)(\s+\S+)*\s*)$/ + .source, + `Test titles should start with one of the following prefixes: ${prefixesList}`, + ], + }, + }, + ], + }, + }, + ], +}; diff --git a/e2e/components/Accordion/Accordion-test.avt.e2e.js b/e2e/components/Accordion/Accordion-test.avt.e2e.js index ee836745df7d..7a224b0176a7 100644 --- a/e2e/components/Accordion/Accordion-test.avt.e2e.js +++ b/e2e/components/Accordion/Accordion-test.avt.e2e.js @@ -10,7 +10,7 @@ import { expect, test } from '@playwright/test'; import { visitStory } from '../../test-utils/storybook'; -test.describe('Accordion @avt', () => { +test.describe('@avt Accordion', () => { test('@avt-default-state', async ({ page }) => { await visitStory(page, { component: 'Accordion', diff --git a/examples/class-prefix/package.json b/examples/class-prefix/package.json index 6718536233ac..b20c78da3879 100644 --- a/examples/class-prefix/package.json +++ b/examples/class-prefix/package.json @@ -1,7 +1,7 @@ { "name": "class-prefix", "private": true, - "version": "0.47.0", + "version": "0.48.0", "type": "module", "scripts": { "dev": "vite", @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "@carbon/react": "^1.50.0", + "@carbon/react": "^1.51.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/codesandbox-styles/package.json b/examples/codesandbox-styles/package.json index d6a12ed4108e..a9b111e1432a 100644 --- a/examples/codesandbox-styles/package.json +++ b/examples/codesandbox-styles/package.json @@ -1,7 +1,7 @@ { "name": "codesandbox-styles", "private": true, - "version": "0.53.0", + "version": "0.54.0", "type": "module", "scripts": { "dev": "vite" @@ -11,6 +11,6 @@ "vite": "^4.3.8" }, "dependencies": { - "@carbon/styles": "^1.50.0" + "@carbon/styles": "^1.51.0" } } diff --git a/examples/custom-theme/package.json b/examples/custom-theme/package.json index 164ece439296..525dbbbaaf94 100644 --- a/examples/custom-theme/package.json +++ b/examples/custom-theme/package.json @@ -1,7 +1,7 @@ { "name": "custom-theme", "private": true, - "version": "0.48.0", + "version": "0.49.0", "type": "module", "scripts": { "dev": "vite", @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "@carbon/react": "^1.50.0", + "@carbon/react": "^1.51.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/id-prefix/package.json b/examples/id-prefix/package.json index 3cf6fb9e9b3b..aac4311dab03 100644 --- a/examples/id-prefix/package.json +++ b/examples/id-prefix/package.json @@ -1,7 +1,7 @@ { "name": "id-prefix", "private": true, - "version": "0.47.0", + "version": "0.48.0", "type": "module", "scripts": { "dev": "vite", @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "@carbon/react": "^1.50.0", + "@carbon/react": "^1.51.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/incremental-migration-vite/package.json b/examples/incremental-migration-vite/package.json index fd1f2114dc6d..c91cdb391c48 100644 --- a/examples/incremental-migration-vite/package.json +++ b/examples/incremental-migration-vite/package.json @@ -1,7 +1,7 @@ { "name": "incremental-migration-vite", "private": true, - "version": "0.15.0", + "version": "0.16.0", "scripts": { "dev": "vite", "build": "vite build", @@ -12,7 +12,7 @@ }, "dependencies": { "@carbon/icons-react": "^10.49.0", - "@carbon/react": "^1.50.0", + "@carbon/react": "^1.51.0", "carbon-components": "^10.57.0", "carbon-components-react": "^7.57.0", "carbon-icons": "^7.0.7", diff --git a/examples/light-dark-mode/package.json b/examples/light-dark-mode/package.json index 00002c141257..a606b4f30d5f 100644 --- a/examples/light-dark-mode/package.json +++ b/examples/light-dark-mode/package.json @@ -1,7 +1,7 @@ { "name": "examples-light-dark", "private": true, - "version": "0.48.0", + "version": "0.49.0", "scripts": { "build": "next build", "dev": "next dev", @@ -9,7 +9,7 @@ "start": "next start" }, "dependencies": { - "@carbon/react": "^1.50.0", + "@carbon/react": "^1.51.0", "next": "13.5.1", "react": "18.2.0", "react-dom": "18.2.0" diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 2d8b7ab5e036..b27d7a22f4c9 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -1,7 +1,7 @@ { "name": "examples-nextjs", "private": true, - "version": "0.50.0", + "version": "0.51.0", "scripts": { "build": "next build", "dev": "next dev", @@ -9,7 +9,7 @@ "start": "next start" }, "dependencies": { - "@carbon/react": "^1.50.0", + "@carbon/react": "^1.51.0", "next": "13.5.6", "react": "18.2.0", "react-dom": "18.2.0" diff --git a/examples/v10-token-compat-in-v11/package.json b/examples/v10-token-compat-in-v11/package.json index c535fc9ae411..ea26cfdbfa88 100644 --- a/examples/v10-token-compat-in-v11/package.json +++ b/examples/v10-token-compat-in-v11/package.json @@ -1,7 +1,7 @@ { "name": "v10-token-compat-in-v11", "private": true, - "version": "0.48.0", + "version": "0.49.0", "type": "module", "scripts": { "dev": "vite", @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "@carbon/react": "^1.50.0", + "@carbon/react": "^1.51.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/vite/package.json b/examples/vite/package.json index d8951e4c35d9..a5acd6af4978 100644 --- a/examples/vite/package.json +++ b/examples/vite/package.json @@ -1,7 +1,7 @@ { "name": "vite", "private": true, - "version": "0.48.0", + "version": "0.49.0", "type": "module", "scripts": { "dev": "vite", @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "@carbon/react": "^1.50.0", + "@carbon/react": "^1.51.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/package.json b/package.json index 9c9dc1c4e905..f9b331d6f230 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "sync": "carbon-cli sync", "test": "cross-env BABEL_ENV=test jest", "test:e2e": "cross-env BABEL_ENV=test jest -c jest.e2e.config.js", - "postinstall": "husky install" + "postinstall": "husky" }, "resolutions": { "@types/react": "~18.2.33", @@ -62,9 +62,10 @@ "cross-spawn": "^7.0.0", "doctoc": "^2.0.0", "eslint": "^8.40.0", + "eslint-plugin-playwright": "^0.22.1", "fs-extra": "^11.0.0", "glob": "^10.0.0", - "husky": "^8.0.0", + "husky": "^9.0.0", "jest": "^28.1.0", "jest-junit": "^16.0.0", "lerna": "^8.0.0", diff --git a/packages/carbon-components-react/package.json b/packages/carbon-components-react/package.json index da362572113b..72e0f3e80267 100644 --- a/packages/carbon-components-react/package.json +++ b/packages/carbon-components-react/package.json @@ -1,7 +1,7 @@ { "name": "carbon-components-react", "description": "The Carbon Design System is IBM’s open-source design system for products and experiences.", - "version": "8.50.0", + "version": "8.51.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -41,8 +41,8 @@ "sass": "^1.33.0" }, "dependencies": { - "@carbon/react": "^1.50.0", - "@carbon/styles": "^1.50.0", + "@carbon/react": "^1.51.0", + "@carbon/styles": "^1.51.0", "@carbon/telemetry": "0.1.0", "chalk": "1.1.3" }, diff --git a/packages/carbon-components-react/scss/components/skeleton-styles/_ai-skeleton-styles.scss b/packages/carbon-components-react/scss/components/skeleton-styles/_ai-skeleton-styles.scss new file mode 100644 index 000000000000..c07f6ee77f00 --- /dev/null +++ b/packages/carbon-components-react/scss/components/skeleton-styles/_ai-skeleton-styles.scss @@ -0,0 +1,9 @@ +// Code generated by carbon-components-react. DO NOT EDIT. +// +// Copyright IBM Corp. 2018, 2023 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@forward '@carbon/styles/scss/components/skeleton-styles/ai-skeleton-styles'; diff --git a/packages/carbon-components/package.json b/packages/carbon-components/package.json index 38ce9b203d51..9a494a7f1d6f 100644 --- a/packages/carbon-components/package.json +++ b/packages/carbon-components/package.json @@ -1,7 +1,7 @@ { "name": "carbon-components", "description": "The Carbon Design System is IBM’s open-source design system for products and experiences.", - "version": "11.50.0", + "version": "11.51.0", "license": "Apache-2.0", "repository": { "type": "git", @@ -42,7 +42,7 @@ "sass": "^1.33.0" }, "dependencies": { - "@carbon/styles": "^1.50.0", + "@carbon/styles": "^1.51.0", "@carbon/telemetry": "0.1.0", "chalk": "1.1.3" }, diff --git a/packages/carbon-components/scss/components/skeleton-styles/_ai-skeleton-styles.scss b/packages/carbon-components/scss/components/skeleton-styles/_ai-skeleton-styles.scss new file mode 100644 index 000000000000..b75ddc45dd0a --- /dev/null +++ b/packages/carbon-components/scss/components/skeleton-styles/_ai-skeleton-styles.scss @@ -0,0 +1,9 @@ +// Code generated by carbon-components. DO NOT EDIT. +// +// Copyright IBM Corp. 2018, 2023 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@forward '@carbon/styles/scss/components/skeleton-styles/ai-skeleton-styles'; diff --git a/packages/elements/package.json b/packages/elements/package.json index 320017643c8b..a7abf940c70f 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.39.0", + "version": "11.40.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -41,7 +41,7 @@ "@carbon/icons": "^11.36.0", "@carbon/layout": "^11.20.0", "@carbon/motion": "^11.16.0", - "@carbon/themes": "^11.31.0", + "@carbon/themes": "^11.32.0", "@carbon/type": "^11.25.0" }, "devDependencies": { diff --git a/packages/elements/src/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/elements/src/__tests__/__snapshots__/PublicAPI-test.js.snap index de34dd015ec2..ed0294894503 100644 --- a/packages/elements/src/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/elements/src/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -16,6 +16,9 @@ Array [ "aiGradientStart01", "aiGradientStart02", "aiInnerShadow", + "aiOverlay", + "aiSkeletonBackground", + "aiSkeletonElementBackground", "background", "backgroundActive", "backgroundBrand", diff --git a/packages/react/README.md b/packages/react/README.md index 09bb47c58110..231335eee797 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -51,6 +51,25 @@ to include `node_modules` in its `includePaths` option. For more information, checkout the [configuration](../styles/docs/sass.md#configuration) section in our Sass docs. +### TypeScript + +There is an ongoing project to add `*.d.ts` files to `@carbon/react`. Though not +all components have yet been typed, you can still use the project successfully +in a TypeScript setting, provided you amend to your `tsconfig.json` or +equivalent configuration file. Include the `skipLibCheck: true` compiler option: + +```json +{ + "compilerOptions": { + "skipLibCheck": true + } +} +``` + +To track the progress of TypeScript adoption, check out: + +- [TypeScript Adoption](https://github.com/orgs/carbon-design-system/projects/53) + ## Usage The `@carbon/react` package provides components and icons for the Carbon Design diff --git a/packages/react/package.json b/packages/react/package.json index 0dfde2833d9b..2d80aa5c4269 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.50.0", + "version": "1.51.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -51,7 +51,7 @@ "@carbon/feature-flags": "^0.16.0", "@carbon/icons-react": "^11.36.0", "@carbon/layout": "^11.20.0", - "@carbon/styles": "^1.50.0", + "@carbon/styles": "^1.51.0", "@ibm/telemetry-js": "^1.2.0", "classnames": "2.5.1", "copy-to-clipboard": "^3.3.1", @@ -79,7 +79,7 @@ "@babel/preset-react": "^7.22.3", "@babel/preset-typescript": "^7.21.5", "@carbon/test-utils": "^10.30.0", - "@carbon/themes": "^11.31.0", + "@carbon/themes": "^11.32.0", "@rollup/plugin-babel": "^6.0.0", "@rollup/plugin-commonjs": "^25.0.0", "@rollup/plugin-node-resolve": "^15.0.0", @@ -108,7 +108,7 @@ "html-webpack-plugin": "^5.5.0", "mini-css-extract-plugin": "^2.4.5", "postcss": "^8.4.5", - "postcss-loader": "^7.0.0", + "postcss-loader": "^8.0.0", "process": "^0.11.10", "prop-types": "^15.7.2", "react": "^18.2.0", diff --git a/packages/react/scss/components/skeleton-styles/_ai-skeleton-styles.scss b/packages/react/scss/components/skeleton-styles/_ai-skeleton-styles.scss new file mode 100644 index 000000000000..c29b3da911ef --- /dev/null +++ b/packages/react/scss/components/skeleton-styles/_ai-skeleton-styles.scss @@ -0,0 +1,9 @@ +// Code generated by @carbon/react. DO NOT EDIT. +// +// Copyright IBM Corp. 2018, 2023 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@forward '@carbon/styles/scss/components/skeleton-styles/ai-skeleton-styles'; diff --git a/packages/react/src/components/AiSkeleton/AiSkeletonIcon.stories.js b/packages/react/src/components/AiSkeleton/AiSkeletonIcon.stories.js new file mode 100644 index 000000000000..0c435924623e --- /dev/null +++ b/packages/react/src/components/AiSkeleton/AiSkeletonIcon.stories.js @@ -0,0 +1,38 @@ +/** + * Copyright IBM Corp. 2016, 2024 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* eslint-disable no-console */ + +import React from 'react'; + +import AiSkeletonIcon from './AiSkeletonIcon'; + +export default { + title: 'Experimental/unstable__AiSkeleton/AiSkeletonIcon', + component: AiSkeletonIcon, +}; + +const propsSkeleton = { + style: { + margin: '50px', + }, +}; + +const propsSkeleton2 = { + style: { + margin: '50px', + width: '24px', + height: '24px', + }, +}; + +export const Default = () => ( + <> + + + +); diff --git a/packages/react/src/components/AiSkeleton/AiSkeletonIcon.tsx b/packages/react/src/components/AiSkeleton/AiSkeletonIcon.tsx new file mode 100644 index 000000000000..e85843607273 --- /dev/null +++ b/packages/react/src/components/AiSkeleton/AiSkeletonIcon.tsx @@ -0,0 +1,47 @@ +/** + * Copyright IBM Corp. 2016, 2024 + * + * 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 PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { usePrefix } from '../../internal/usePrefix'; +import { SkeletonIcon } from '../SkeletonIcon'; + +interface AiSkeletonIconProps { + /** + * Specify an optional className to add. + */ + className?: string; + + /** + * The CSS styles. + */ + style?: React.CSSProperties; +} + +const AiSkeletonIcon = ({ className, ...rest }: AiSkeletonIconProps) => { + const prefix = usePrefix(); + const AiSkeletonIconClasses = classNames(className, { + [`${prefix}--skeleton__icon--ai`]: true, + }); + + return ; +}; + +AiSkeletonIcon.propTypes = { + /** + * Specify an optional className to add. + */ + className: PropTypes.string, + + /** + * The CSS styles. + */ + style: PropTypes.object, +}; + +export default AiSkeletonIcon; diff --git a/packages/react/src/components/AiSkeleton/AiSkeletonPlaceholder.stories.js b/packages/react/src/components/AiSkeleton/AiSkeletonPlaceholder.stories.js new file mode 100644 index 000000000000..5a6b323c50bf --- /dev/null +++ b/packages/react/src/components/AiSkeleton/AiSkeletonPlaceholder.stories.js @@ -0,0 +1,19 @@ +/** + * Copyright IBM Corp. 2016, 2024 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* eslint-disable no-console */ + +import React from 'react'; + +import AiSkeletonPlaceholder from './AiSkeletonPlaceholder'; + +export default { + title: 'Experimental/unstable__AiSkeleton/AiSkeletonPlaceholder', + component: AiSkeletonPlaceholder, +}; + +export const Default = () => ; diff --git a/packages/react/src/components/AiSkeleton/AiSkeletonPlaceholder.tsx b/packages/react/src/components/AiSkeleton/AiSkeletonPlaceholder.tsx new file mode 100644 index 000000000000..cc5e849a4fb9 --- /dev/null +++ b/packages/react/src/components/AiSkeleton/AiSkeletonPlaceholder.tsx @@ -0,0 +1,44 @@ +/** + * Copyright IBM Corp. 2016, 2024 + * + * 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 PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { usePrefix } from '../../internal/usePrefix'; +import { SkeletonPlaceholder } from '../SkeletonPlaceholder'; + +export interface AiSkeletonPlaceholderProps { + /** + * Add a custom class to the component to set the height and width + */ + className?: string; +} + +const AiSkeletonPlaceholder = ({ + className, + ...other +}: AiSkeletonPlaceholderProps) => { + const prefix = usePrefix(); + const AiSkeletonPlaceholderClasses = classNames( + { className, [`${prefix}--skeleton__placeholder--ai`]: true }, + className + ); + + return ( + + ); +}; + +AiSkeletonPlaceholder.propTypes = { + /** + * Add a custom class to the component + * to set the height and width + */ + className: PropTypes.string, +}; + +export default AiSkeletonPlaceholder; diff --git a/packages/react/src/components/AiSkeleton/AiSkeletonText.stories.js b/packages/react/src/components/AiSkeleton/AiSkeletonText.stories.js new file mode 100644 index 000000000000..5f75ea23a228 --- /dev/null +++ b/packages/react/src/components/AiSkeleton/AiSkeletonText.stories.js @@ -0,0 +1,54 @@ +/** + * Copyright IBM Corp. 2016, 2024 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* eslint-disable no-console */ + +import React from 'react'; + +import AiSkeletonText from './AiSkeletonText'; + +export default { + title: 'Experimental/unstable__AiSkeleton/AiSkeletonText', + component: AiSkeletonText, +}; + +export const Default = () => ; + +export const Playground = (args) => ; + +Playground.args = { + heading: false, + paragraph: false, + width: '100%', + lineCount: 3, +}; + +Playground.argTypes = { + className: { + control: false, + }, + heading: { + control: { + type: 'boolean', + }, + }, + paragraph: { + control: { + type: 'boolean', + }, + }, + width: { + control: { + type: 'text', + }, + }, + lineCount: { + control: { + type: 'number', + }, + }, +}; diff --git a/packages/react/src/components/AiSkeleton/AiSkeletonText.tsx b/packages/react/src/components/AiSkeleton/AiSkeletonText.tsx new file mode 100644 index 000000000000..69a1b3a085e1 --- /dev/null +++ b/packages/react/src/components/AiSkeleton/AiSkeletonText.tsx @@ -0,0 +1,73 @@ +/** + * Copyright IBM Corp. 2016, 2024 + * + * 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 PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { usePrefix } from '../../internal/usePrefix'; +import { SkeletonText } from '../SkeletonText'; + +interface AiSkeletonTextProps { + /** + * Specify an optional className to be applied to the container node. + */ + className?: string; + + /** + * Generates skeleton text at a larger size. + */ + heading?: boolean; + + /** + * The number of lines shown if paragraph is true. + */ + lineCount?: number; + + /** + * Set this to true to generate multiple lines of text. + */ + paragraph?: boolean; + + /** + * Width (in px or %) of single line of text or max-width of paragraph lines. + */ + width?: string; +} + +const AiSkeletonText = ({ className, ...rest }: AiSkeletonTextProps) => { + const prefix = usePrefix(); + const aiSkeletonTextClasses = classNames(className, { + [`${prefix}--skeleton__text--ai`]: true, + }); + + return ; +}; + +AiSkeletonText.propTypes = { + /** + * Specify an optional className to be applied to the container node + */ + className: PropTypes.string, + /** + * generates skeleton text at a larger size + */ + heading: PropTypes.bool, + /** + * the number of lines shown if paragraph is true + */ + lineCount: PropTypes.number, + /** + * will generate multiple lines of text + */ + paragraph: PropTypes.bool, + /** + * width (in px or %) of single line of text or max-width of paragraph lines + */ + width: PropTypes.string, +}; + +export default AiSkeletonText; diff --git a/packages/react/src/components/AiSkeleton/index.tsx b/packages/react/src/components/AiSkeleton/index.tsx new file mode 100644 index 000000000000..439f4ce25736 --- /dev/null +++ b/packages/react/src/components/AiSkeleton/index.tsx @@ -0,0 +1,12 @@ +/** + * Copyright IBM Corp. 2016, 2024 + * + * 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 AiSkeletonPlaceholder from './AiSkeletonPlaceholder'; +import AiSkeletonIcon from './AiSkeletonIcon'; +import AiSkeletonText from './AiSkeletonText'; + +export { AiSkeletonText, AiSkeletonIcon, AiSkeletonPlaceholder }; diff --git a/packages/react/src/components/ComposedModal/ComposedModal.stories.js b/packages/react/src/components/ComposedModal/ComposedModal.stories.js index 25b74d738095..7f47641fb028 100644 --- a/packages/react/src/components/ComposedModal/ComposedModal.stories.js +++ b/packages/react/src/components/ComposedModal/ComposedModal.stories.js @@ -41,99 +41,111 @@ export default { }; export const Default = () => { + const [open, setOpen] = useState(true); return ( - - - -

- Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

- - -
- -
+ <> + + setOpen(false)}> + + +

+ Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

+ + +
+ +
+ ); }; export const FullWidth = () => { + const [open, setOpen] = useState(true); return ( - - - - - - - - Column A - - - Column B - - - Column C - - - - - - Row 1 - Row 1 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc - dui magna, finibus id tortor sed, aliquet bibendum augue. Aenean - posuere sem vel euismod dignissim. Nulla ut cursus dolor. - Pellentesque vulputate nisl a porttitor interdum. - - - - Row 2 - Row 2 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc - dui magna, finibus id tortor sed, aliquet bibendum augue. Aenean - posuere sem vel euismod dignissim. Nulla ut cursus dolor. - Pellentesque vulputate nisl a porttitor interdum. - - - - Row 3 - Row 3 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc - dui magna, finibus id tortor sed, aliquet bibendum augue. Aenean - posuere sem vel euismod dignissim. Nulla ut cursus dolor. - Pellentesque vulputate nisl a porttitor interdum. - - - - - - - + <> + + setOpen(false)} isFullWidth> + + + + + + + Column A + + + Column B + + + Column C + + + + + + Row 1 + Row 1 + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc + dui magna, finibus id tortor sed, aliquet bibendum augue. + Aenean posuere sem vel euismod dignissim. Nulla ut cursus + dolor. Pellentesque vulputate nisl a porttitor interdum. + + + + Row 2 + Row 2 + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc + dui magna, finibus id tortor sed, aliquet bibendum augue. + Aenean posuere sem vel euismod dignissim. Nulla ut cursus + dolor. Pellentesque vulputate nisl a porttitor interdum. + + + + Row 3 + Row 3 + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc + dui magna, finibus id tortor sed, aliquet bibendum augue. + Aenean posuere sem vel euismod dignissim. Nulla ut cursus + dolor. Pellentesque vulputate nisl a porttitor interdum. + + + + + + + + ); }; export const PassiveModal = () => { + const [open, setOpen] = useState(true); return ( - - - - + <> + + setOpen(false)}> + + + + ); }; @@ -201,75 +213,80 @@ export const WithStateManager = () => { }; export const WithScrollingContent = () => { + const [open, setOpen] = useState(true); return ( - - - -

- Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus eu - nibh odio. Nunc a consequat est, id porttitor sapien. Proin vitae leo - vitae orci tincidunt auctor eget eget libero. Ut tincidunt ultricies - fringilla. Aliquam erat volutpat. Aenean arcu odio, elementum vel - vehicula vitae, porttitor ac lorem. Sed viverra elit ac risus - tincidunt fermentum. Ut sollicitudin nibh id risus ornare ornare. - Etiam gravida orci ut lectus dictum, quis ultricies felis mollis. - Mauris nec commodo est, nec faucibus nibh. Nunc commodo ante quis - pretium consectetur. Ut ac nisl vitae mi mattis vulputate a at elit. - Nullam porttitor ex eget mi feugiat mattis. Nunc non sodales magna. - Proin ornare tellus quis hendrerit egestas. Donec pharetra leo nec - molestie sollicitudin.{' '} -

- -
- -
- - (item ? item.text : '')} - /> -
- -
+ <> + + setOpen(false)}> + + +

+ Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus + eu nibh odio. Nunc a consequat est, id porttitor sapien. Proin vitae + leo vitae orci tincidunt auctor eget eget libero. Ut tincidunt + ultricies fringilla. Aliquam erat volutpat. Aenean arcu odio, + elementum vel vehicula vitae, porttitor ac lorem. Sed viverra elit + ac risus tincidunt fermentum. Ut sollicitudin nibh id risus ornare + ornare. Etiam gravida orci ut lectus dictum, quis ultricies felis + mollis. Mauris nec commodo est, nec faucibus nibh. Nunc commodo ante + quis pretium consectetur. Ut ac nisl vitae mi mattis vulputate a at + elit. Nullam porttitor ex eget mi feugiat mattis. Nunc non sodales + magna. Proin ornare tellus quis hendrerit egestas. Donec pharetra + leo nec molestie sollicitudin.{' '} +

+ +
+ +
+ + (item ? item.text : '')} + /> +
+ +
+ ); }; export const WithInlineLoading = () => { + const [open, setOpen] = useState(true); const [status, setStatus] = useState('inactive'); const [description, setDescription] = useState('Submitting...'); @@ -296,70 +313,77 @@ export const WithInlineLoading = () => { }; return ( - - - -

- Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

- + + setOpen(false)}> + + +

+ Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

+ + +
+ - -
- -
+ + ); }; export const Playground = (args) => { + const [open, setOpen] = useState(true); return ( - - - -

- Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

- + + setOpen(false)}> + + +

+ Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

+ + +
+ - -
- -
+ + ); }; diff --git a/packages/react/src/components/ComposedModal/ComposedModal.tsx b/packages/react/src/components/ComposedModal/ComposedModal.tsx index 35d3f4ab4dfe..a5295638ceae 100644 --- a/packages/react/src/components/ComposedModal/ComposedModal.tsx +++ b/packages/react/src/components/ComposedModal/ComposedModal.tsx @@ -13,11 +13,12 @@ import { isElement } from 'react-is'; import PropTypes, { ReactNodeLike } from 'prop-types'; import { ModalHeader, type ModalHeaderProps } from './ModalHeader'; import { ModalFooter, type ModalFooterProps } from './ModalFooter'; +import debounce from 'lodash.debounce'; +import useIsomorphicEffect from '../../internal/useIsomorphicEffect'; +import mergeRefs from '../../tools/mergeRefs'; import cx from 'classnames'; - import toggleClass from '../../tools/toggleClass'; import requiredIfGivenPropIsTruthy from '../../prop-types/requiredIfGivenPropIsTruthy'; - import wrapFocus from '../../internal/wrapFocus'; import { usePrefix } from '../../internal/usePrefix'; import { keys, match } from '../../internal/keyboard'; @@ -50,30 +51,51 @@ export const ModalBody = React.forwardRef( ref ) { const prefix = usePrefix(); - const contentClass = cx( - `${prefix}--modal-content`, - hasForm && `${prefix}--modal-content--with-form`, - hasScrollingContent && `${prefix}--modal-scroll-content`, - customClassName - ); + const contentRef = useRef(null); + const [isScrollable, setIsScrollable] = useState(false); + const contentClass = cx({ + [`${prefix}--modal-content`]: true, + [`${prefix}--modal-content--with-form`]: hasForm, + [`${prefix}--modal-scroll-content`]: hasScrollingContent || isScrollable, + customClassName, + }); + + useIsomorphicEffect(() => { + if (contentRef.current) { + setIsScrollable( + contentRef.current.scrollHeight > contentRef.current.clientHeight + ); + } + + function handler() { + if (contentRef.current) { + setIsScrollable( + contentRef.current.scrollHeight > contentRef.current.clientHeight + ); + } + } - const hasScrollingContentProps = hasScrollingContent - ? { tabIndex: 0, role: 'region' } - : {}; + const debouncedHandler = debounce(handler, 200); + window.addEventListener('resize', debouncedHandler); + return () => { + debouncedHandler.cancel(); + window.removeEventListener('resize', debouncedHandler); + }; + }, []); + + const hasScrollingContentProps = + hasScrollingContent || isScrollable + ? { tabIndex: 0, role: 'region' } + : {}; return ( - <> -
- {children} -
- {hasScrollingContent && ( -
- )} - +
+ {children} +
); } ); diff --git a/packages/react/src/components/Copy/Copy-test.js b/packages/react/src/components/Copy/Copy-test.js index 291c2b5b705d..dfbcdd9239ac 100644 --- a/packages/react/src/components/Copy/Copy-test.js +++ b/packages/react/src/components/Copy/Copy-test.js @@ -13,6 +13,8 @@ import { Copy as CopyIcon } from '@carbon/icons-react'; jest.useFakeTimers(); +const prefix = 'cds'; + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); describe('Copy', () => { @@ -95,15 +97,15 @@ describe('Feedback', () => { const button = screen.getByTestId('copy-button-5'); await user.click(button); - expect(button).toHaveClass('cds--copy-btn--animating'); + expect(button).toHaveClass(`${prefix}--copy-btn--animating`); // eslint-disable-next-line testing-library/no-unnecessary-act act(() => { jest.runAllTimers(); fireEvent.animationEnd(screen.getByTestId('copy-button-5'), { - animationName: 'hide-feedback', + animationName: `${prefix}--hide-feedback`, }); }); - expect(button).not.toHaveClass('cds--copy-btn--animating'); + expect(button).not.toHaveClass(`${prefix}--copy-btn--animating`); }); it('should be able to specify the feedback message', async () => { @@ -134,14 +136,14 @@ describe('Feedback', () => { const button = screen.getByTestId('copy-button-7'); await user.click(button); - expect(button).toHaveClass('cds--copy-btn--animating'); + expect(button).toHaveClass(`${prefix}--copy-btn--animating`); // eslint-disable-next-line testing-library/no-unnecessary-act act(() => { jest.runAllTimers(); fireEvent.animationEnd(screen.getByTestId('copy-button-7'), { - animationName: 'hide-feedback', + animationName: `${prefix}--hide-feedback`, }); }); - expect(button).not.toHaveClass('cds--copy-btn--animating'); + expect(button).not.toHaveClass(`${prefix}--copy-btn--animating`); }); }); diff --git a/packages/react/src/components/Copy/Copy.js b/packages/react/src/components/Copy/Copy.js index 10b4976a79f4..55c57d5404e1 100644 --- a/packages/react/src/components/Copy/Copy.js +++ b/packages/react/src/components/Copy/Copy.js @@ -44,7 +44,7 @@ export default function Copy({ }, [handleFadeOut]); const handleAnimationEnd = (event) => { - if (event.animationName === 'hide-feedback') { + if (event.animationName === `${prefix}--hide-feedback`) { setAnimation(''); } }; diff --git a/packages/react/src/components/CopyButton/CopyButton-test.js b/packages/react/src/components/CopyButton/CopyButton-test.js index 9d04876a1b77..e0c52694fdbd 100644 --- a/packages/react/src/components/CopyButton/CopyButton-test.js +++ b/packages/react/src/components/CopyButton/CopyButton-test.js @@ -12,6 +12,7 @@ import CopyButton from '../CopyButton'; jest.useFakeTimers(); const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); +const prefix = 'cds'; describe('CopyButton', () => { it('should set tabIndex if one is passed via props', () => { @@ -77,12 +78,12 @@ describe('Feedback', () => { const button = screen.getByTestId('copy-btn-5'); await user.click(button); - expect(button).toHaveClass('cds--copy-btn--animating'); + expect(button).toHaveClass(`${prefix}--copy-btn--animating`); // eslint-disable-next-line testing-library/no-unnecessary-act act(() => { jest.runAllTimers(); fireEvent.animationEnd(screen.getByTestId('copy-btn-5'), { - animationName: 'hide-feedback', + animationName: `${prefix}--hide-feedback`, }); }); }); @@ -113,12 +114,12 @@ describe('Feedback', () => { const button = screen.getByTestId('copy-btn-7'); await user.click(button); - expect(button).toHaveClass('cds--copy-btn--animating'); + expect(button).toHaveClass(`${prefix}--copy-btn--animating`); // eslint-disable-next-line testing-library/no-unnecessary-act act(() => { jest.runAllTimers(); fireEvent.animationEnd(screen.getByTestId('copy-btn-7'), { - animationName: 'hide-feedback', + animationName: `${prefix}--hide-feedback`, }); }); }); diff --git a/packages/react/src/components/DataTable/DataTable.tsx b/packages/react/src/components/DataTable/DataTable.tsx index 2622ff5b801f..f3b110acd444 100644 --- a/packages/react/src/components/DataTable/DataTable.tsx +++ b/packages/react/src/components/DataTable/DataTable.tsx @@ -91,7 +91,7 @@ export interface DataTableRow { export interface DataTableHeader { key: string; header: React.ReactNode; - slug: React.ReactElement; + slug?: React.ReactElement; } export interface DataTableRenderProps { diff --git a/packages/react/src/components/DataTable/TableToolbarSearch.tsx b/packages/react/src/components/DataTable/TableToolbarSearch.tsx index d23f0d5d6208..71a276523246 100644 --- a/packages/react/src/components/DataTable/TableToolbarSearch.tsx +++ b/packages/react/src/components/DataTable/TableToolbarSearch.tsx @@ -17,12 +17,18 @@ import React, { ReactNode, RefObject, } from 'react'; -import Search from '../Search'; +import Search, { SearchProps } from '../Search'; import setupGetInstanceId from './tools/instanceId'; import { usePrefix } from '../../internal/usePrefix'; import { noopFn } from '../../internal/noopFn'; +import { InternationalProps } from '../../types/common'; const getInstanceId = setupGetInstanceId(); + +export type TableToolbarTranslationKey = + | 'carbon.table.toolbar.search.label' + | 'carbon.table.toolbar.search.placeholder'; + const translationKeys = { 'carbon.table.toolbar.search.label': 'Filter table', 'carbon.table.toolbar.search.placeholder': 'Filter table', @@ -32,14 +38,23 @@ const translateWithId = (id: string): string => { return translationKeys[id]; }; -export interface TableToolbarSearchProps { - children?: ReactNode; +type ExcludedInheritedProps = + | 'defaultValue' + | 'labelText' + | 'onBlur' + | 'onChange' + | 'onExpand' + | 'onFocus' + | 'tabIndex'; - /** - * Provide an optional class name for the search container - */ - className?: string; +export type TableToolbarSearchHandleExpand = ( + event: FocusEvent, + newValue?: boolean +) => void; +export interface TableToolbarSearchProps + extends Omit, + InternationalProps { /** * Specifies if the search should initially render in an expanded state */ @@ -50,35 +65,25 @@ export interface TableToolbarSearchProps { */ defaultValue?: string; - /** - * Specifies if the search should be disabled - */ - disabled?: boolean; - /** * Specifies if the search should expand */ expanded?: boolean; - /** - * Provide an optional id for the search container - */ - id?: string; - /** * Provide an optional label text for the Search component icon */ - labelText?: string; + labelText?: ReactNode; /** * Provide an optional function to be called when the search input loses focus, this will be * passed the event as the first parameter and a function to handle the expanding of the search * input as the second */ - onBlur?: ( + onBlur?( event: FocusEvent, - handleExpand: (event: FocusEvent, value: boolean) => void - ) => void; + handleExpand: TableToolbarSearchHandleExpand + ): void; /** * Provide an optional hook that is called each time the input is updated @@ -88,54 +93,32 @@ export interface TableToolbarSearchProps { value?: string ) => void; - /** - * Optional callback called when the search value is cleared. - */ - onClear?: () => void; - /** * Provide an optional hook that is called each time the input is expanded */ - onExpand?: (event: FocusEvent, value: boolean) => void; + onExpand?(event: FocusEvent, newExpand: boolean): void; /** * Provide an optional function to be called when the search input gains focus, this will be * passed the event as the first parameter and a function to handle the expanding of the search * input as the second. */ - onFocus?: ( + onFocus?( event: FocusEvent, - handleExpand: (event: FocusEvent, value: boolean) => void - ) => void; + handleExpand: TableToolbarSearchHandleExpand + ): void; /** - * Whether the search should be allowed to expand + * Whether the search should be allowed to expand. */ persistent?: boolean; - /** - * Provide an optional placeholder text for the Search component - */ - placeholder?: string; - /** * Provide an optional className for the overall container of the Search */ searchContainerClass?: string; - /** - * Specify the size of the Search - */ - size?: 'sm' | 'md' | 'lg'; - - /** - * Optional prop to specify the tabIndex of the (in expanded state) or the container (in collapsed state) - */ tabIndex?: number | string; - /** - * Provide custom text for the component for each translation id - */ - translateWithId?: (id: string) => string; } const TableToolbarSearch = ({ @@ -160,9 +143,11 @@ const TableToolbarSearch = ({ ...rest }: TableToolbarSearchProps) => { const { current: controlled } = useRef(expandedProp !== undefined); - const [expandedState, setExpandedState] = useState< - string | boolean | undefined - >(defaultExpanded || defaultValue); + + const [expandedState, setExpandedState] = useState( + Boolean(defaultExpanded || defaultValue) + ); + const expanded = controlled ? expandedProp : expandedState; const [value, setValue] = useState(defaultValue || ''); const uniqueId = useMemo(getInstanceId, []); @@ -218,8 +203,10 @@ const TableToolbarSearch = ({ } }; - const handleOnFocus = (event) => handleExpand(event, true); - const handleOnBlur = (event) => !value && handleExpand(event, false); + const handleOnFocus = (event: FocusEvent) => + handleExpand(event, true); + const handleOnBlur = (event: FocusEvent) => + !value && handleExpand(event, false); return ( { /** * A collection of MenuItems to be rendered within this Menu. */ - children?: React.ReactNode; + children?: ReactNode; /** * Additional CSS class names. @@ -46,7 +48,7 @@ interface MenuProps extends React.HTMLAttributes { /** * A label describing the Menu. */ - label?: string; + label: string; /** * Specify how the menu should align with the button element @@ -85,20 +87,20 @@ interface MenuProps extends React.HTMLAttributes { /** * Specify a DOM node where the Menu should be rendered in. Defaults to document.body. */ - target?: any; + target?: HTMLElement; /** * Specify the x position of the Menu. Either pass a single number or an array with two numbers describing your activator's boundaries ([x1, x2]) */ - x?: number | (number | null | undefined)[]; + x?: number | [number, number]; /** * Specify the y position of the Menu. Either pass a single number or an array with two numbers describing your activator's boundaries ([y1, y2]) */ - y?: number | (number | null | undefined)[]; + y?: number | [number, number]; } -const Menu = React.forwardRef(function Menu( +const Menu = forwardRef(function Menu( { children, className, @@ -149,7 +151,7 @@ const Menu = React.forwardRef(function Menu( }, [childState, childDispatch]); const menu = useRef(null); - const ref = useMergedRefs([forwardRef, menu]); + const ref = useMergedRefs([forwardRef, menu]); const [position, setPosition] = useState([-1, -1]); const focusableItems = childContext.state.items.filter( @@ -163,7 +165,7 @@ const Menu = React.forwardRef(function Menu( actionButtonWidth = w; } - // Set RTL based on document direction or `LayoutDirection` + // Set RTL based on the document direction or `LayoutDirection` const { direction } = useLayoutDirection(); function returnFocus() { @@ -450,6 +452,7 @@ Menu.propTypes = { /** * A label describing the Menu. */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error label: PropTypes.string, /** @@ -489,11 +492,13 @@ Menu.propTypes = { /** * Specify a DOM node where the Menu should be rendered in. Defaults to document.body. */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error target: PropTypes.object, /** * Specify the x position of the Menu. Either pass a single number or an array with two numbers describing your activator's boundaries ([x1, x2]) */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error x: PropTypes.oneOfType([ PropTypes.number, PropTypes.arrayOf(PropTypes.number), @@ -502,6 +507,7 @@ Menu.propTypes = { /** * Specify the y position of the Menu. Either pass a single number or an array with two numbers describing your activator's boundaries ([y1, y2]) */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error y: PropTypes.oneOfType([ PropTypes.number, PropTypes.arrayOf(PropTypes.number), diff --git a/packages/react/src/components/Menu/MenuContext.ts b/packages/react/src/components/Menu/MenuContext.ts index b23f2d33249c..3731beb1fce2 100644 --- a/packages/react/src/components/Menu/MenuContext.ts +++ b/packages/react/src/components/Menu/MenuContext.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; +import { createContext, KeyboardEvent, RefObject } from 'react'; type ActionType = { type: 'enableIcons' | 'registerItem'; @@ -18,9 +18,7 @@ type StateType = { hasIcons: boolean; size: 'xs' | 'sm' | 'md' | 'lg' | null; items: any[]; - requestCloseRoot: ( - e: Pick, 'type'> - ) => void; + requestCloseRoot: (e: Pick, 'type'>) => void; }; const menuDefaultState: StateType = { @@ -49,13 +47,23 @@ function menuReducer(state: StateType, action: ActionType) { } } -const MenuContext = React.createContext<{ +type DispatchFuncProps = { + type: 'registerItem' | 'enableIcons'; + payload: { + ref: RefObject; + disabled: boolean; + }; +}; + +type MenuContextProps = { state: StateType; - dispatch: React.Dispatch; -}>({ + dispatch: (props: DispatchFuncProps) => void; +}; + +const MenuContext = createContext({ state: menuDefaultState, // 'dispatch' is populated by the root menu - dispatch: () => {}, + dispatch: (_: DispatchFuncProps) => {}, }); export { MenuContext, menuReducer }; diff --git a/packages/react/src/components/Menu/MenuItem.tsx b/packages/react/src/components/Menu/MenuItem.tsx index 0e6a95b8e7bb..295db5b8a5e1 100644 --- a/packages/react/src/components/Menu/MenuItem.tsx +++ b/packages/react/src/components/Menu/MenuItem.tsx @@ -7,7 +7,21 @@ import cx from 'classnames'; import PropTypes from 'prop-types'; -import React, { useContext, useEffect, useRef, useState } from 'react'; +import React, { + ChangeEventHandler, + ComponentProps, + FC, + ForwardedRef, + forwardRef, + KeyboardEvent, + LiHTMLAttributes, + MouseEvent, + ReactNode, + useContext, + useEffect, + useRef, + useState, +} from 'react'; import { CaretRight, CaretLeft, Checkmark } from '@carbon/icons-react'; import { keys, match } from '../../internal/keyboard'; @@ -21,11 +35,11 @@ import { MenuContext } from './MenuContext'; import { useLayoutDirection } from '../LayoutDirection'; import { Text } from '../Text'; -interface MenuItemProps extends React.LiHTMLAttributes { +export interface MenuItemProps extends LiHTMLAttributes { /** * Optionally provide another Menu to create a submenu. props.children can't be used to specify the content of the MenuItem itself. Use props.label instead. */ - children?: React.ReactNode; + children?: ReactNode; /** * Additional CSS class names. @@ -51,13 +65,13 @@ interface MenuItemProps extends React.LiHTMLAttributes { * Provide an optional function to be called when the MenuItem is clicked. */ onClick?: ( - event: React.KeyboardEvent | React.MouseEvent + event: KeyboardEvent | MouseEvent ) => void; /** * Only applicable if the parent menu is in `basic` mode. Sets the menu item's icon. */ - renderIcon?: any; + renderIcon?: FC; /** * Provide a shortcut for the action of this MenuItem. Note that the component will only render it as a hint but not actually register the shortcut. @@ -67,7 +81,7 @@ interface MenuItemProps extends React.LiHTMLAttributes { const hoverIntentDelay = 150; // in ms -const MenuItem = React.forwardRef( +export const MenuItem = forwardRef( function MenuItem( { children, @@ -86,12 +100,12 @@ const MenuItem = React.forwardRef( const context = useContext(MenuContext); const menuItem = useRef(null); - const ref = useMergedRefs([forwardRef, menuItem]); + const ref = useMergedRefs([forwardRef, menuItem]); const [boundaries, setBoundaries] = useState<{ - x: number | number[]; - y: number | number[]; + x: number | [number, number]; + y: number | [number, number]; }>({ x: -1, y: -1 }); - const [isRtl, setRtl] = useState(false); + const [rtl, setRtl] = useState(false); const hasChildren = Boolean(children); const [submenuOpen, setSubmenuOpen] = useState(false); @@ -116,8 +130,9 @@ const MenuItem = React.forwardRef( if (!menuItem.current) { return; } + const { x, y, width, height } = menuItem.current.getBoundingClientRect(); - if (isRtl) { + if (rtl) { setBoundaries({ x: [-x, x - width], y: [y, y + height], @@ -138,7 +153,7 @@ const MenuItem = React.forwardRef( } function handleClick( - e: React.KeyboardEvent | React.MouseEvent + e: KeyboardEvent | MouseEvent ) { if (!isDisabled) { if (hasChildren) { @@ -194,7 +209,7 @@ const MenuItem = React.forwardRef( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Set RTL based on document direction or `LayoutDirection` + // Set RTL based on the document direction or `LayoutDirection` const { direction } = useLayoutDirection(); useEffect(() => { if (document?.dir === 'rtl' || direction === 'rtl') { @@ -211,6 +226,7 @@ const MenuItem = React.forwardRef( useEffect(() => { if (iconsAllowed && IconElement && !context.state.hasIcons) { + // @ts-ignore - TODO: Should we be passing payload? context.dispatch({ type: 'enableIcons' }); } }, [iconsAllowed, IconElement, context.state.hasIcons, context]); @@ -222,8 +238,8 @@ const MenuItem = React.forwardRef( ref={ref} className={classNames} tabIndex={-1} - aria-disabled={isDisabled} - aria-haspopup={hasChildren || undefined} + aria-disabled={isDisabled ?? undefined} + aria-haspopup={hasChildren ?? undefined} aria-expanded={hasChildren ? submenuOpen : undefined} onClick={handleClick} onMouseEnter={hasChildren ? handleMouseEnter : undefined} @@ -241,7 +257,7 @@ const MenuItem = React.forwardRef( {hasChildren && ( <>
- {isRtl ? : } + {rtl ? : }
{ /** * Specify whether the option should be selected by default. */ defaultSelected?: boolean; - /** - * A required label titling this option. - */ - label: string; - /** * Provide an optional function to be called when the selection state changes. */ - onChange?: React.ChangeEventHandler; + onChange?: ChangeEventHandler; /** - * Pass a bool to props.selected to control the state of this option. + * Controls the state of this option. */ selected?: boolean; } -const MenuItemSelectable = React.forwardRef< +export const MenuItemSelectable = forwardRef< HTMLLIElement, MenuItemSelectableProps >(function MenuItemSelectable( @@ -363,6 +373,7 @@ const MenuItemSelectable = React.forwardRef< useEffect(() => { if (!context.state.hasIcons) { + // @ts-ignore - TODO: Should we be passing payload? context.dispatch({ type: 'enableIcons' }); } }, [context.state.hasIcons, context]); @@ -392,6 +403,7 @@ MenuItemSelectable.propTypes = { /** * Specify whether the option should be selected by default. */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error defaultSelected: PropTypes.bool, /** @@ -402,19 +414,21 @@ MenuItemSelectable.propTypes = { /** * Provide an optional function to be called when the selection state changes. */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error onChange: PropTypes.func, /** * Pass a bool to props.selected to control the state of this option. */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error selected: PropTypes.bool, }; -interface MenuItemGroupProps { +export interface MenuItemGroupProps extends ComponentProps<'ul'> { /** * A collection of MenuItems to be rendered within this group. */ - children?: React.ReactNode; + children?: ReactNode; /** * Additional CSS class names. @@ -427,7 +441,7 @@ interface MenuItemGroupProps { label: string; } -const MenuItemGroup = React.forwardRef( +export const MenuItemGroup = forwardRef( function MenuItemGroup({ children, className, label, ...rest }, forwardRef) { const prefix = usePrefix(); @@ -460,9 +474,10 @@ MenuItemGroup.propTypes = { label: PropTypes.string.isRequired, }; -const defaultItemToString = (item: any) => item.toString(); +const defaultItemToString = (item) => item.toString(); -interface MenuItemRadioGroupProps { +export interface MenuItemRadioGroupProps + extends Omit, 'onChange'> { /** * Additional CSS class names. */ @@ -471,17 +486,17 @@ interface MenuItemRadioGroupProps { /** * Specify the default selected item. Must match the type of props.items. */ - defaultSelectedItem?: any; + defaultSelectedItem?: Item; /** * Provide a function to convert an item to the string that will be rendered. Defaults to item.toString(). */ - itemToString?: (item: any) => string; + itemToString?: (item: Item) => string; /** * Provide the options for this radio group. Can be of any type, as long as you provide an appropriate props.itemToString function. */ - items?: any[]; + items: Item[]; /** * A required label titling this radio group. @@ -491,18 +506,15 @@ interface MenuItemRadioGroupProps { /** * Provide an optional function to be called when the selection changes. */ - onChange?: React.ChangeEventHandler; + onChange?: ChangeEventHandler; /** * Provide props.selectedItem to control the state of this radio group. Must match the type of props.items. */ - selectedItem?: any; + selectedItem?: Item; } -const MenuItemRadioGroup = React.forwardRef< - HTMLLIElement, - MenuItemRadioGroupProps ->(function MenuItemRadioGroup( +export const MenuItemRadioGroup = forwardRef(function MenuItemRadioGroup( { className, defaultSelectedItem, @@ -512,8 +524,8 @@ const MenuItemRadioGroup = React.forwardRef< onChange, selectedItem, ...rest - }, - forwardRef + }: MenuItemRadioGroupProps, + forwardRef: ForwardedRef ) { const prefix = usePrefix(); const context = useContext(MenuContext); @@ -541,6 +553,7 @@ const MenuItemRadioGroup = React.forwardRef< useEffect(() => { if (!context.state.hasIcons) { + // @ts-ignore - TODO: Should we be passing payload? context.dispatch({ type: 'enableIcons' }); } }, [context.state.hasIcons, context]); @@ -550,7 +563,7 @@ const MenuItemRadioGroup = React.forwardRef< return (
    • - {items?.map((item, i) => ( + {items.map((item, i) => ( { /** * Additional CSS class names. */ className?: string; } -const MenuItemDivider = React.forwardRef( +export const MenuItemDivider = forwardRef( function MenuItemDivider({ className, ...rest }, forwardRef) { const prefix = usePrefix(); @@ -629,11 +645,3 @@ MenuItemDivider.propTypes = { */ className: PropTypes.string, }; - -export { - MenuItem, - MenuItemSelectable, - MenuItemGroup, - MenuItemRadioGroup, - MenuItemDivider, -}; diff --git a/packages/react/src/components/MenuButton/index.js b/packages/react/src/components/MenuButton/index.js deleted file mode 100644 index e58cfde881b1..000000000000 --- a/packages/react/src/components/MenuButton/index.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Copyright IBM Corp. 2023 - * - * 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, { useRef, useState } from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import { ChevronDown } from '@carbon/icons-react'; -import Button from '../Button'; -import { Menu } from '../Menu'; - -import { useAttachedMenu } from '../../internal/useAttachedMenu'; -import { useId } from '../../internal/useId'; -import { useMergedRefs } from '../../internal/useMergedRefs'; -import { usePrefix } from '../../internal/usePrefix'; - -const spacing = 0; // top and bottom spacing between the button and the menu. in px -const validButtonKinds = ['primary', 'tertiary', 'ghost']; -const defaultButtonKind = 'primary'; - -const MenuButton = React.forwardRef(function MenuButton( - { - children, - className, - disabled, - kind = defaultButtonKind, - label, - size = 'lg', - menuAlignment = 'bottom', - tabIndex = 0, - ...rest - }, - forwardRef -) { - const id = useId('MenuButton'); - const prefix = usePrefix(); - - const triggerRef = useRef(null); - const menuRef = useRef(null); - const ref = useMergedRefs([forwardRef, triggerRef]); - const [width, setWidth] = useState(0); - const { - open, - x, - y, - handleClick: hookOnClick, - handleMousedown, - handleClose, - } = useAttachedMenu(triggerRef); - - function handleClick() { - if (triggerRef.current) { - const { width: w } = triggerRef.current.getBoundingClientRect(); - setWidth(w); - hookOnClick(); - } - } - - function handleOpen() { - menuRef.current.style.inlineSize = `${width}px`; - menuRef.current.style.minInlineSize = `${width}px`; - if (menuAlignment !== 'bottom' && menuAlignment !== 'top') { - menuRef.current.style.inlineSize = `fit-content`; - } - } - - const containerClasses = classNames( - `${prefix}--menu-button__container`, - className - ); - - const triggerClasses = classNames(`${prefix}--menu-button__trigger`, { - [`${prefix}--menu-button__trigger--open`]: open, - }); - - const menuClasses = classNames(`${prefix}--menu-button__${menuAlignment}`); - - const buttonKind = validButtonKinds.includes(kind) ? kind : defaultButtonKind; - - return ( -
      - - - {children} - -
      - ); -}); - -MenuButton.propTypes = { - /** - * A collection of MenuItems to be rendered as actions for this MenuButton. - */ - children: PropTypes.node.isRequired, - - /** - * Additional CSS class names. - */ - className: PropTypes.string, - - /** - * Specify whether the MenuButton should be disabled, or not. - */ - disabled: PropTypes.bool, - - /** - * Specify the type of button to be used as the base for the trigger button. - */ - kind: PropTypes.oneOf(validButtonKinds), - - /** - * Provide the label to be renderd on the trigger button. - */ - label: PropTypes.string.isRequired, - - /** - * Experimental property. Specify how the menu should align with the button element - */ - menuAlignment: PropTypes.oneOf([ - 'top', - 'top-start', - 'top-end', - 'bottom', - 'bottom-start', - 'bottom-end', - ]), - - /** - * Specify the size of the button and menu. - */ - size: PropTypes.oneOf(['sm', 'md', 'lg']), - - /** - * Specify the tabIndex of the button. - */ - tabIndex: PropTypes.number, -}; - -export { MenuButton }; diff --git a/packages/react/src/components/MenuButton/index.tsx b/packages/react/src/components/MenuButton/index.tsx new file mode 100644 index 000000000000..cf2ab9e874de --- /dev/null +++ b/packages/react/src/components/MenuButton/index.tsx @@ -0,0 +1,233 @@ +/** + * Copyright IBM Corp. 2023 + * + * 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, { + ComponentProps, + forwardRef, + ReactNode, + useRef, + useState, +} from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { ChevronDown } from '@carbon/icons-react'; +import Button from '../Button'; +import { Menu } from '../Menu'; + +import { useAttachedMenu } from '../../internal/useAttachedMenu'; +import { useId } from '../../internal/useId'; +import { useMergedRefs } from '../../internal/useMergedRefs'; +import { usePrefix } from '../../internal/usePrefix'; + +const spacing = 0; // top and bottom spacing between the button and the menu. in px +const validButtonKinds = ['primary', 'tertiary', 'ghost']; +const defaultButtonKind = 'primary'; + +export interface MenuButtonProps extends ComponentProps<'div'> { + /** + * A collection of MenuItems to be rendered as actions for this MenuButton. + */ + children?: ReactNode; + + /** + * Additional CSS class names. + */ + className?: string; + + /** + * Specify whether the MenuButton should be disabled, or not. + */ + disabled?: boolean; + + /** + * Specify the type of button to be used as the base for the trigger button. + */ + kind?: 'primary' | 'tertiary' | 'ghost'; + + /** + * Provide the label to be rendered on the trigger button. + */ + label: string; + + /** + * Experimental property. Specify how the menu should align with the button element + */ + menuAlignment: + | 'top' + | 'top-start' + | 'top-end' + | 'bottom' + | 'bottom-start' + | 'bottom-end'; + + /** + * Specify the size of the button and menu. + */ + size?: 'sm' | 'md' | 'lg'; + + /** + * Specify the tabIndex of the button. + */ + tabIndex?: number; +} + +const MenuButton = forwardRef( + function MenuButton( + { + children, + className, + disabled, + kind = defaultButtonKind, + label, + size = 'lg', + menuAlignment = 'bottom', + tabIndex = 0, + ...rest + }, + forwardRef + ) { + const id = useId('MenuButton'); + const prefix = usePrefix(); + + const triggerRef = useRef(null); + const menuRef = useRef(null); + const ref = useMergedRefs([forwardRef, triggerRef]); + const [width, setWidth] = useState(0); + const { + open, + x, + y, + handleClick: hookOnClick, + handleMousedown, + handleClose, + } = useAttachedMenu(triggerRef); + + function handleClick() { + if (triggerRef.current) { + const { width: w } = triggerRef.current.getBoundingClientRect(); + setWidth(w); + hookOnClick(); + } + } + + function handleOpen() { + if (menuRef.current) { + menuRef.current.style.inlineSize = `${width}px`; + menuRef.current.style.minInlineSize = `${width}px`; + if (menuAlignment !== 'bottom' && menuAlignment !== 'top') { + menuRef.current.style.inlineSize = `fit-content`; + } + } + } + + const containerClasses = classNames( + `${prefix}--menu-button__container`, + className + ); + + const triggerClasses = classNames(`${prefix}--menu-button__trigger`, { + [`${prefix}--menu-button__trigger--open`]: open, + }); + + const menuClasses = classNames(`${prefix}--menu-button__${menuAlignment}`); + + return ( +
      + + + {children} + +
      + ); + } +); + +MenuButton.propTypes = { + /** + * A collection of MenuItems to be rendered as actions for this MenuButton. + */ + children: PropTypes.node.isRequired, + + /** + * Additional CSS class names. + */ + className: PropTypes.string, + + /** + * Specify whether the MenuButton should be disabled, or not. + */ + disabled: PropTypes.bool, + + /** + * Specify the type of button to be used as the base for the trigger button. + */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error + kind: PropTypes.oneOf(validButtonKinds), + + /** + * Provide the label to be renderd on the trigger button. + */ + label: PropTypes.string.isRequired, + + /** + * Experimental property. Specify how the menu should align with the button element + */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error + menuAlignment: PropTypes.oneOf([ + 'top', + 'top-start', + 'top-end', + 'bottom', + 'bottom-start', + 'bottom-end', + ]), + + /** + * Specify the size of the button and menu. + */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error + size: PropTypes.oneOf(['sm', 'md', 'lg']), + + /** + * Specify the tabIndex of the button. + */ + // @ts-ignore-next-line -- avoid spurious (?) TS2322 error + tabIndex: PropTypes.number, +}; + +export { MenuButton }; diff --git a/packages/react/src/components/Modal/Modal.stories.js b/packages/react/src/components/Modal/Modal.stories.js index c3d5961c4244..fb60764d3560 100644 --- a/packages/react/src/components/Modal/Modal.stories.js +++ b/packages/react/src/components/Modal/Modal.stories.js @@ -35,128 +35,143 @@ export default { }; export const Default = () => { + const [open, setOpen] = useState(true); return ( - -

      - Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

      - - - - (item ? item.text : '')} - /> -
      + <> + + setOpen(false)} + modalHeading="Add a custom domain" + modalLabel="Account resources" + primaryButtonText="Add" + secondaryButtonText="Cancel"> +

      + Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

      + + + + (item ? item.text : '')} + /> +
      + ); }; export const FullWidth = () => { + const [open, setOpen] = useState(true); return ( - - - - - - Column A - - - Column B - - - Column C - - - - - - Row 1 - Row 1 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc dui - magna, finibus id tortor sed, aliquet bibendum augue. Aenean - posuere sem vel euismod dignissim. Nulla ut cursus dolor. - Pellentesque vulputate nisl a porttitor interdum. - - - - Row 2 - Row 2 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc dui - magna, finibus id tortor sed, aliquet bibendum augue. Aenean - posuere sem vel euismod dignissim. Nulla ut cursus dolor. - Pellentesque vulputate nisl a porttitor interdum. - - - - Row 3 - Row 3 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc dui - magna, finibus id tortor sed, aliquet bibendum augue. Aenean - posuere sem vel euismod dignissim. Nulla ut cursus dolor. - Pellentesque vulputate nisl a porttitor interdum. - - - - - + <> + + setOpen(false)} + isFullWidth + modalHeading="Full Width Modal" + modalLabel="An example of a modal with no padding" + primaryButtonText="Add" + secondaryButtonText="Cancel"> + + + + + Column A + + + Column B + + + Column C + + + + + + Row 1 + Row 1 + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc + dui magna, finibus id tortor sed, aliquet bibendum augue. Aenean + posuere sem vel euismod dignissim. Nulla ut cursus dolor. + Pellentesque vulputate nisl a porttitor interdum. + + + + Row 2 + Row 2 + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc + dui magna, finibus id tortor sed, aliquet bibendum augue. Aenean + posuere sem vel euismod dignissim. Nulla ut cursus dolor. + Pellentesque vulputate nisl a porttitor interdum. + + + + Row 3 + Row 3 + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc + dui magna, finibus id tortor sed, aliquet bibendum augue. Aenean + posuere sem vel euismod dignissim. Nulla ut cursus dolor. + Pellentesque vulputate nisl a porttitor interdum. + + + + + + ); }; export const DangerModal = () => { + const [open, setOpen] = useState(true); return ( - + <> + + setOpen(false)} + danger + modalHeading="Are you sure you want to delete this custom domain?" + modalLabel="Account resources" + primaryButtonText="Delete" + secondaryButtonText="Cancel" + /> + ); }; @@ -199,156 +214,170 @@ const modalFooter = (numberOfButtons) => { }; export const WithScrollingContent = () => { + const [open, setOpen] = useState(true); return ( - -

      - Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

      -

      - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus eu - nibh odio. Nunc a consequat est, id porttitor sapien. Proin vitae leo - vitae orci tincidunt auctor eget eget libero. Ut tincidunt ultricies - fringilla. Aliquam erat volutpat. Aenean arcu odio, elementum vel - vehicula vitae, porttitor ac lorem. Sed viverra elit ac risus tincidunt - fermentum. Ut sollicitudin nibh id risus ornare ornare. Etiam gravida - orci ut lectus dictum, quis ultricies felis mollis. Mauris nec commodo - est, nec faucibus nibh. Nunc commodo ante quis pretium consectetur. Ut - ac nisl vitae mi mattis vulputate a at elit. Nullam porttitor ex eget mi - feugiat mattis. Nunc non sodales magna. Proin ornare tellus quis - hendrerit egestas. Donec pharetra leo nec molestie sollicitudin.{' '} -

      - -
      - -
      - - (item ? item.text : '')} - /> -
      + <> + + setOpen(false)} + hasScrollingContent + modalHeading="Add a custom domain" + modalLabel="Account resources" + primaryButtonText="Add" + secondaryButtonText="Cancel"> +

      + Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

      +

      + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus eu + nibh odio. Nunc a consequat est, id porttitor sapien. Proin vitae leo + vitae orci tincidunt auctor eget eget libero. Ut tincidunt ultricies + fringilla. Aliquam erat volutpat. Aenean arcu odio, elementum vel + vehicula vitae, porttitor ac lorem. Sed viverra elit ac risus + tincidunt fermentum. Ut sollicitudin nibh id risus ornare ornare. + Etiam gravida orci ut lectus dictum, quis ultricies felis mollis. + Mauris nec commodo est, nec faucibus nibh. Nunc commodo ante quis + pretium consectetur. Ut ac nisl vitae mi mattis vulputate a at elit. + Nullam porttitor ex eget mi feugiat mattis. Nunc non sodales magna. + Proin ornare tellus quis hendrerit egestas. Donec pharetra leo nec + molestie sollicitudin.{' '} +

      + +
      + +
      + + (item ? item.text : '')} + /> +
      + ); }; export const Playground = ({ numberOfButtons, ...args }) => { + const [open, setOpen] = useState(true); return ( - -

      - Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

      - - - {args.hasScrollingContent && ( - <> -

      - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id - accumsan augue. Phasellus consequat augue vitae tellus tincidunt - posuere. Curabitur justo urna, consectetur vel elit iaculis, - ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie - tellus. Quisque consectetur non risus eu rutrum.{' '} -

      -

      - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id - accumsan augue. Phasellus consequat augue vitae tellus tincidunt - posuere. Curabitur justo urna, consectetur vel elit iaculis, - ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie - tellus. Quisque consectetur non risus eu rutrum.{' '} -

      -

      - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id - accumsan augue. Phasellus consequat augue vitae tellus tincidunt - posuere. Curabitur justo urna, consectetur vel elit iaculis, - ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie - tellus. Quisque consectetur non risus eu rutrum.{' '} -

      -

      Lorem ipsum

      -

      - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id - accumsan augue. Phasellus consequat augue vitae tellus tincidunt - posuere. Curabitur justo urna, consectetur vel elit iaculis, - ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie - tellus. Quisque consectetur non risus eu rutrum.{' '} -

      -

      - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id - accumsan augue. Phasellus consequat augue vitae tellus tincidunt - posuere. Curabitur justo urna, consectetur vel elit iaculis, - ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie - tellus. Quisque consectetur non risus eu rutrum.{' '} -

      -

      - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id - accumsan augue. Phasellus consequat augue vitae tellus tincidunt - posuere. Curabitur justo urna, consectetur vel elit iaculis, - ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie - tellus. Quisque consectetur non risus eu rutrum.{' '} -

      -

      - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id - accumsan augue. Phasellus consequat augue vitae tellus tincidunt - posuere. Curabitur justo urna, consectetur vel elit iaculis, - ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie - tellus. Quisque consectetur non risus eu rutrum.{' '} -

      - - )} -
      + <> + + { + action(e); + setOpen(false); + }} + modalHeading="Add a custom domain" + primaryButtonText="Add" + secondaryButtonText="Cancel" + aria-label="Modal content" + open={open} + {...args} + {...modalFooter(numberOfButtons)}> +

      + Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

      + + + {args.hasScrollingContent && ( + <> +

      + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis + molestie tellus. Quisque consectetur non risus eu rutrum.{' '} +

      +

      + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis + molestie tellus. Quisque consectetur non risus eu rutrum.{' '} +

      +

      + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis + molestie tellus. Quisque consectetur non risus eu rutrum.{' '} +

      +

      Lorem ipsum

      +

      + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis + molestie tellus. Quisque consectetur non risus eu rutrum.{' '} +

      +

      + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis + molestie tellus. Quisque consectetur non risus eu rutrum.{' '} +

      +

      + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis + molestie tellus. Quisque consectetur non risus eu rutrum.{' '} +

      +

      + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis + molestie tellus. Quisque consectetur non risus eu rutrum.{' '} +

      + + )} +
      + ); }; @@ -390,9 +419,6 @@ Playground.argTypes = { onKeyDown: { action: 'onKeyDown', }, - onRequestClose: { - action: 'onRequestClose', - }, onRequestSubmit: { action: 'onRequestSubmit', }, @@ -491,11 +517,17 @@ export const WithStateManager = () => { }; export const PassiveModal = () => { + const [open, setOpen] = useState(true); return ( - + <> + + setOpen(false)} + passiveModal + modalHeading="You have been successfully signed out" + /> + ); }; @@ -525,18 +557,23 @@ export const WithInlineLoading = () => { setDescription('Deleting...'); }; + const [open, setOpen] = useState(true); return ( - + <> + + setOpen(false)} + danger + modalHeading="Are you sure you want to delete this custom domain?" + modalLabel="Account resources" + primaryButtonText="Delete" + secondaryButtonText="Cancel" + onRequestSubmit={submit} + loadingStatus={status} + loadingDescription={description} + onLoadingSuccess={resetStatus} + /> + ); }; diff --git a/packages/react/src/components/Modal/Modal.tsx b/packages/react/src/components/Modal/Modal.tsx index 33b3cf03e808..98381d541251 100644 --- a/packages/react/src/components/Modal/Modal.tsx +++ b/packages/react/src/components/Modal/Modal.tsx @@ -6,7 +6,7 @@ */ import PropTypes, { ReactNodeLike } from 'prop-types'; -import React, { useRef, useEffect } from 'react'; +import React, { useRef, useEffect, useState } from 'react'; import classNames from 'classnames'; import { Close } from '@carbon/icons-react'; import toggleClass from '../../tools/toggleClass'; @@ -17,6 +17,8 @@ import requiredIfGivenPropIsTruthy from '../../prop-types/requiredIfGivenPropIsT import wrapFocus, { elementOrParentIsFloatingMenu, } from '../../internal/wrapFocus'; +import debounce from 'lodash.debounce'; +import useIsomorphicEffect from '../../internal/useIsomorphicEffect'; import setupGetInstanceId from '../../tools/setupGetInstanceId'; import { usePrefix } from '../../internal/usePrefix'; import { keys, match } from '../../internal/keyboard'; @@ -253,9 +255,11 @@ const Modal = React.forwardRef(function Modal( const prefix = usePrefix(); const button = useRef(null); const secondaryButton = useRef(); + const contentRef = useRef(null); const innerModal = useRef(null); const startTrap = useRef(null); const endTrap = useRef(null); + const [isScrollable, setIsScrollable] = useState(false); const modalInstanceId = `modal-${getInstanceId()}`; const modalLabelId = `${prefix}--modal-header__label--${modalInstanceId}`; const modalHeadingId = `${prefix}--modal-header__heading--${modalInstanceId}`; @@ -341,7 +345,7 @@ const Modal = React.forwardRef(function Modal( }); const contentClasses = classNames(`${prefix}--modal-content`, { - [`${prefix}--modal-scroll-content`]: hasScrollingContent, + [`${prefix}--modal-scroll-content`]: hasScrollingContent || isScrollable, }); const footerClasses = classNames(`${prefix}--modal-footer`, { @@ -358,14 +362,15 @@ const Modal = React.forwardRef(function Modal( modalLabelStr || ariaLabelProp || modalAriaLabel || modalHeadingStr; const getAriaLabelledBy = modalLabel ? modalLabelId : modalHeadingId; - const hasScrollingContentProps = hasScrollingContent - ? { - tabIndex: 0, - role: 'region', - 'aria-label': ariaLabel, - 'aria-labelledby': getAriaLabelledBy, - } - : {}; + const hasScrollingContentProps = + hasScrollingContent || isScrollable + ? { + tabIndex: 0, + role: 'region', + 'aria-label': ariaLabel, + 'aria-labelledby': getAriaLabelledBy, + } + : {}; const alertDialogProps: ReactAttr = {}; if (alert && passiveModal) { @@ -426,6 +431,29 @@ const Modal = React.forwardRef(function Modal( } }, [open, selectorPrimaryFocus, danger, prefix]); + useIsomorphicEffect(() => { + if (contentRef.current) { + setIsScrollable( + contentRef.current.scrollHeight > contentRef.current.clientHeight + ); + } + + function handler() { + if (contentRef.current) { + setIsScrollable( + contentRef.current.scrollHeight > contentRef.current.clientHeight + ); + } + } + + const debouncedHandler = debounce(handler, 200); + window.addEventListener('resize', debouncedHandler); + return () => { + debouncedHandler.cancel(); + window.removeEventListener('resize', debouncedHandler); + }; + }, []); + // Slug is always size `lg` let normalizedSlug; if (slug && slug['type']?.displayName === 'Slug') { @@ -483,14 +511,12 @@ const Modal = React.forwardRef(function Modal( {!passiveModal && modalButton}
  • {children}
    - {hasScrollingContent && ( -
    - )} {!passiveModal && ( {Array.isArray(secondaryButtons) && secondaryButtons.length <= 2 diff --git a/packages/react/src/components/MultiSelect/MultiSelect.tsx b/packages/react/src/components/MultiSelect/MultiSelect.tsx index 0d27f49c7ca4..db5d76f96a5f 100644 --- a/packages/react/src/components/MultiSelect/MultiSelect.tsx +++ b/packages/react/src/components/MultiSelect/MultiSelect.tsx @@ -137,6 +137,11 @@ export interface MultiSelectProps > { className?: string; + /** + * Specify the text that should be read for screen readers that describes that all items are deleted + */ + clearAnnouncement?: string; + /** * Specify the text that should be read for screen readers that describes total items selected */ @@ -324,6 +329,7 @@ const MultiSelect = React.forwardRef( sortItems = defaultSortItems as MultiSelectProps['sortItems'], compareItems = defaultCompareItems, clearSelectionText = 'To clear selection, press Delete or Backspace', + clearAnnouncement = 'all items have been cleared', clearSelectionDescription = 'Total items selected: ', light, invalid, @@ -353,6 +359,7 @@ const MultiSelect = React.forwardRef( const [isOpen, setIsOpen] = useState(open || false); const [prevOpenProp, setPrevOpenProp] = useState(open); const [topItems, setTopItems] = useState([]); + const [itemsCleared, setItemsCleared] = useState(false); const { selectedItems: controlledSelectedItems, onItemChange, @@ -409,12 +416,17 @@ const MultiSelect = React.forwardRef( e.stopPropagation(); } + if (!isOpen && match(e, keys.Delete) && selectedItems.length > 0) { + setItemsCleared(true); + } + if ( (match(e, keys.Space) || match(e, keys.ArrowDown) || match(e, keys.Enter)) && !isOpen ) { + setItemsCleared(false); setIsOpenWrapper(true); } } @@ -709,6 +721,9 @@ const MultiSelect = React.forwardRef( } )} + {itemsCleared && ( + + )} {!inline && !invalid && !warn && helperText && (
    diff --git a/packages/react/src/components/NumberInput/NumberInput.tsx b/packages/react/src/components/NumberInput/NumberInput.tsx index fbe4477f41c7..1c333b6c63a7 100644 --- a/packages/react/src/components/NumberInput/NumberInput.tsx +++ b/packages/react/src/components/NumberInput/NumberInput.tsx @@ -306,7 +306,7 @@ const NumberInput = React.forwardRef( } const state = { - value: event.target.value, + value: Number(event.target.value), direction: value < event.target.value ? 'up' : 'down', }; setValue(state.value); diff --git a/packages/react/src/components/PaginationNav/PaginationNav.js b/packages/react/src/components/PaginationNav/PaginationNav.js deleted file mode 100644 index 72820a777b7b..000000000000 --- a/packages/react/src/components/PaginationNav/PaginationNav.js +++ /dev/null @@ -1,503 +0,0 @@ -/** - * Copyright IBM Corp. 2020 - * - * 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 PropTypes from 'prop-types'; -import React, { useState, useEffect, useRef } from 'react'; -import classnames from 'classnames'; -import { - CaretRight, - CaretLeft, - OverflowMenuHorizontal, -} from '@carbon/icons-react'; -import { IconButton } from '../IconButton'; -import { usePrefix } from '../../internal/usePrefix'; - -const translationIds = { - 'carbon.pagination-nav.next': 'Next', - 'carbon.pagination-nav.previous': 'Previous', - 'carbon.pagination-nav.item': 'Page', - 'carbon.pagination-nav.active': 'Active', - 'carbon.pagination-nav.of': 'of', -}; - -function translateWithId(messageId) { - return translationIds[messageId]; -} - -// https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state -function usePrevious(value) { - const ref = useRef(); - - useEffect(() => { - ref.current = value; - }); - - return ref.current; -} - -function getCuts(page, totalItems, itemsThatFit, splitPoint = null) { - if (itemsThatFit >= totalItems) { - return { - front: 0, - back: 0, - }; - } - - const split = splitPoint || Math.ceil(itemsThatFit / 2) - 1; - - let frontHidden = page + 1 - split; - let backHidden = totalItems - page - (itemsThatFit - split) + 1; - - if (frontHidden <= 1) { - backHidden -= frontHidden <= 0 ? Math.abs(frontHidden) + 1 : 0; - frontHidden = 0; - } - - if (backHidden <= 1) { - frontHidden -= backHidden <= 0 ? Math.abs(backHidden) + 1 : 0; - backHidden = 0; - } - - return { - front: frontHidden, - back: backHidden, - }; -} - -function DirectionButton({ direction, label, disabled, onClick }) { - const prefix = usePrefix(); - - return ( -
  • - - {direction === 'forward' ? : } - -
  • - ); -} - -function PaginationItem({ - page, - isActive, - onClick, - translateWithId: t = translateWithId, -}) { - const prefix = usePrefix(); - const itemLabel = t('carbon.pagination-nav.item'); - - return ( -
  • - -
  • - ); -} - -function PaginationOverflow({ - fromIndex, - count, - onSelect, - // eslint-disable-next-line react/prop-types - disableOverflow, - translateWithId: t = translateWithId, -}) { - const prefix = usePrefix(); - - //If overflow is disabled, return a select tag with no select options - if (disableOverflow === true && count > 1) { - return ( -
  • -
    - {/* eslint-disable-next-line jsx-a11y/no-onchange */} - -
    - -
    -
    -
  • - ); - } - - if (count > 1) { - return ( -
  • -
    - {/* eslint-disable-next-line jsx-a11y/no-onchange */} - -
    - -
    -
    -
  • - ); - } - - if (count === 1) { - return ( - { - onSelect(fromIndex); - }} - /> - ); - } - - return null; -} - -const PaginationNav = React.forwardRef(function PaginationNav( - { - className, - onChange = () => {}, - totalItems, - disableOverflow, - itemsShown = 10, - page = 0, - loop = false, - translateWithId: t = translateWithId, - ...rest - }, - ref -) { - const [currentPage, setCurrentPage] = useState(page); - const [itemsThatFit, setItemsThatFit] = useState( - itemsShown >= 4 ? itemsShown : 4 - ); - const [cuts, setCuts] = useState( - getCuts(currentPage, totalItems, itemsThatFit) - ); - const prevPage = usePrevious(currentPage); - const prefix = usePrefix(); - const [isOverflowDisabled, setIsOverFlowDisabled] = useState(disableOverflow); - function jumpToItem(index) { - if (index >= 0 && index < totalItems) { - setCurrentPage(index); - onChange(index); - } - } - - function jumpToNext() { - const nextIndex = currentPage + 1; - - if (nextIndex >= totalItems) { - if (loop) { - jumpToItem(0); - } - } else { - jumpToItem(nextIndex); - } - } - - function jumpToPrevious() { - const previousIndex = currentPage - 1; - - if (previousIndex < 0) { - if (loop) { - jumpToItem(totalItems - 1); - } - } else { - jumpToItem(previousIndex); - } - } - - function pageWouldBeHidden(page) { - const startOffset = itemsThatFit <= 4 && page > 1 ? 0 : 1; - - const wouldBeHiddenInFront = page >= startOffset && page <= cuts.front; - const wouldBeHiddenInBack = - page >= totalItems - cuts.back - 1 && page <= totalItems - 2; - - return wouldBeHiddenInFront || wouldBeHiddenInBack; - } - - // jump to new page if props.page is updated - useEffect(() => { - setCurrentPage(page); - }, [page]); - - // re-calculate cuts if props.totalItems or props.itemsShown change - useEffect(() => { - setItemsThatFit(itemsShown >= 4 ? itemsShown : 4); - setCuts(getCuts(currentPage, totalItems, itemsShown)); - }, [totalItems, itemsShown]); // eslint-disable-line react-hooks/exhaustive-deps - - // update cuts if necessary whenever currentPage changes - useEffect(() => { - if (pageWouldBeHidden(currentPage)) { - const delta = currentPage - prevPage || 0; - - if (delta > 0) { - const splitPoint = itemsThatFit - 3; - setCuts(getCuts(currentPage, totalItems, itemsThatFit, splitPoint)); - } else { - const splitPoint = itemsThatFit > 4 ? 2 : 1; - setCuts(getCuts(currentPage, totalItems, itemsThatFit, splitPoint)); - } - } - }, [currentPage]); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - setIsOverFlowDisabled(disableOverflow); - }, [disableOverflow]); - - const classNames = classnames(`${prefix}--pagination-nav`, className); - - const backwardButtonDisabled = !loop && currentPage === 0; - const forwardButtonDisabled = !loop && currentPage === totalItems - 1; - - const startOffset = itemsThatFit <= 4 && currentPage > 1 ? 0 : 1; - - return ( - - ); -}); - -DirectionButton.propTypes = { - /** - * The direction this button represents ("forward" or "backward"). - */ - direction: PropTypes.oneOf(['forward', 'backward']), - - /** - * Whether or not the button should be disabled. - */ - disabled: PropTypes.bool, - - /** - * The label shown in the button's tooltip. - */ - label: PropTypes.string, - - /** - * The callback function called when the button is clicked. - */ - onClick: PropTypes.func, -}; - -PaginationItem.propTypes = { - /** - * Whether or not this is the currently active page. - */ - isActive: PropTypes.bool, - - /** - * The callback function called when the item is clicked. - */ - onClick: PropTypes.func, - - /** - * The page number this item represents. - */ - page: PropTypes.number, - - /** - * Specify a custom translation function that takes in a message identifier - * and returns the localized string for the message - */ - translateWithId: PropTypes.func, -}; - -PaginationOverflow.propTypes = { - /** - * How many items to display in this overflow. - */ - count: PropTypes.number, - - /** - * From which index on this overflow should start displaying pages. - */ - fromIndex: PropTypes.number, - - /** - * The callback function called when the user selects a page from the overflow. - */ - onSelect: PropTypes.func, - - /** - * Specify a custom translation function that takes in a message identifier - * and returns the localized string for the message - */ - translateWithId: PropTypes.func, -}; - -PaginationNav.displayName = 'PaginationNav'; -PaginationNav.propTypes = { - /** - * Additional CSS class names. - */ - className: PropTypes.string, - - /** - * If true, the '...' pagination overflow will not render page links between the first and last rendered buttons. - * Set this to true if you are having performance problems with large data sets. - */ - disableOverflow: PropTypes.bool, // eslint-disable-line react/prop-types - - /** - * The number of items to be shown. - */ - itemsShown: PropTypes.number, - - /** - * Whether user should be able to loop through the items when reaching first / last. - */ - loop: PropTypes.bool, - - /** - * The callback function called when the current page changes. - */ - onChange: PropTypes.func, - - /** - * The index of current page. - */ - page: PropTypes.number, - - /** - * The total number of items. - */ - totalItems: PropTypes.number, - - /** - * Specify a custom translation function that takes in a message identifier - * and returns the localized string for the message - */ - translateWithId: PropTypes.func, -}; - -export default PaginationNav; diff --git a/packages/react/src/components/PaginationNav/PaginationNav.tsx b/packages/react/src/components/PaginationNav/PaginationNav.tsx new file mode 100644 index 000000000000..8daabb8f40cd --- /dev/null +++ b/packages/react/src/components/PaginationNav/PaginationNav.tsx @@ -0,0 +1,650 @@ +/** + * Copyright IBM Corp. 2020 + * + * 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 PropTypes from 'prop-types'; +import React, { useState, useEffect, useRef } from 'react'; +import classnames from 'classnames'; +import { + CaretRight, + CaretLeft, + OverflowMenuHorizontal, +} from '@carbon/icons-react'; +import { IconButton } from '../IconButton'; +import { usePrefix } from '../../internal/usePrefix'; + +const translationIds = { + 'carbon.pagination-nav.next': 'Next', + 'carbon.pagination-nav.previous': 'Previous', + 'carbon.pagination-nav.item': 'Page', + 'carbon.pagination-nav.active': 'Active', + 'carbon.pagination-nav.of': 'of', +}; + +function translateWithId(messageId: string): string { + return translationIds[messageId]; +} + +// https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state +function usePrevious(value: number) { + const ref = useRef(null); + + useEffect(() => { + ref.current = value; + }); + + return ref.current; +} + +function calculateCuts( + page: number, + totalItems: number, + itemsDisplayedOnPage: number, + splitPoint: number | null = null +) { + if (itemsDisplayedOnPage >= totalItems) { + return { + front: 0, + back: 0, + }; + } + + const split = splitPoint || Math.ceil(itemsDisplayedOnPage / 2) - 1; + + let frontHidden = page + 1 - split; + let backHidden = totalItems - page - (itemsDisplayedOnPage - split) + 1; + + if (frontHidden <= 1) { + backHidden -= frontHidden <= 0 ? Math.abs(frontHidden) + 1 : 0; + frontHidden = 0; + } + + if (backHidden <= 1) { + frontHidden -= backHidden <= 0 ? Math.abs(backHidden) + 1 : 0; + backHidden = 0; + } + + return { + front: frontHidden, + back: backHidden, + }; +} + +interface DirectionButtonProps { + /** + * The direction this button represents ("forward" or "backward"). + */ + direction?: 'forward' | 'backward'; + + /** + * Whether or not the button should be disabled. + */ + disabled?: boolean; + + /** + * The label shown in the button's tooltip. + */ + label?: string; + + /** + * The callback function called when the button is clicked. + */ + onClick?: React.MouseEventHandler; +} + +function DirectionButton({ + direction, + label, + disabled, + onClick, +}: DirectionButtonProps) { + const prefix = usePrefix(); + + return ( +
  • + + {direction === 'forward' ? : } + +
  • + ); +} + +interface PaginationItemProps { + /** + * Whether or not this is the currently active page. + */ + isActive?: boolean; + + /** + * The callback function called when the item is clicked. + */ + onClick?: React.MouseEventHandler; + + /** + * The page number this item represents. + */ + page?: number; + + /** + * Specify a custom translation function that takes in a message identifier + * and returns the localized string for the message + */ + translateWithId?: (id: string) => string; +} + +function PaginationItem({ + page, + isActive, + onClick, + translateWithId: t = translateWithId, +}: PaginationItemProps) { + const prefix = usePrefix(); + const itemLabel = t('carbon.pagination-nav.item'); + + return ( +
  • + +
  • + ); +} + +interface PaginationOverflowProps { + /** + * How many items to display in this overflow. + */ + count?: number; + + /** + * From which index on this overflow should start displaying pages. + */ + fromIndex?: number; + + /** + * The callback function called when the user selects a page from the overflow. + */ + onSelect?: (id: number) => void; + + /** + * If true, the '...' pagination overflow will not render page links between the first and last rendered buttons. + * Set this to true if you are having performance problems with large data sets. + */ + disableOverflow?: boolean; + + /** + * Specify a custom translation function that takes in a message identifier + * and returns the localized string for the message + */ + translateWithId?: (id: string) => string; +} + +function PaginationOverflow({ + fromIndex = NaN, + count = NaN, + onSelect, + // eslint-disable-next-line react/prop-types + disableOverflow, + translateWithId: t = translateWithId, +}: PaginationOverflowProps) { + const prefix = usePrefix(); + + //If overflow is disabled, return a select tag with no select options + if (disableOverflow === true && count > 1) { + return ( +
  • +
    + {/* eslint-disable-next-line jsx-a11y/no-onchange */} + +
    + +
    +
    +
  • + ); + } + + if (count > 1) { + return ( +
  • +
    + {/* eslint-disable-next-line jsx-a11y/no-onchange */} + +
    + +
    +
    +
  • + ); + } + + if (count === 1) { + return ( + { + onSelect?.(fromIndex); + }} + /> + ); + } + + return null; +} + +interface PaginationNavProps + extends Omit, 'onChange'> { + /** + * Additional CSS class names. + */ + className?: string; + + /** + * If true, the '...' pagination overflow will not render page links between the first and last rendered buttons. + * Set this to true if you are having performance problems with large data sets. + */ + disableOverflow?: boolean; + + /** + * The number of items to be shown. + */ + itemsShown?: number; + + /** + * Whether user should be able to loop through the items when reaching first / last. + */ + loop?: boolean; + + /** + * The callback function called when the current page changes. + */ + onChange?: (data: number) => void; + + /** + * The index of current page. + */ + page?: number; + + /** + * The total number of items. + */ + totalItems?: number; + + /** + * Specify a custom translation function that takes in a message identifier + * and returns the localized string for the message + */ + translateWithId?: (id: string) => string; +} + +const PaginationNav = React.forwardRef( + function PaginationNav( + { + className, + onChange = () => {}, + totalItems = NaN, + disableOverflow, + itemsShown = 10, + page = 0, + loop = false, + translateWithId: t = translateWithId, + ...rest + }, + ref + ) { + const [currentPage, setCurrentPage] = useState(page); + const [itemsDisplayedOnPage, setItemsDisplayedOnPage] = useState( + itemsShown >= 4 ? itemsShown : 4 + ); + const [cuts, setCuts] = useState( + calculateCuts(currentPage, totalItems, itemsDisplayedOnPage) + ); + const prevPage = usePrevious(currentPage); + const prefix = usePrefix(); + const [isOverflowDisabled, setIsOverFlowDisabled] = + useState(disableOverflow); + function jumpToItem(index: number) { + if (index >= 0 && index < totalItems) { + setCurrentPage(index); + onChange(index); + } + } + + function jumpToNext() { + const nextIndex = currentPage + 1; + + if (nextIndex >= totalItems) { + if (loop) { + jumpToItem(0); + } + } else { + jumpToItem(nextIndex); + } + } + + function jumpToPrevious() { + const previousIndex = currentPage - 1; + + if (previousIndex < 0) { + if (loop) { + jumpToItem(totalItems - 1); + } + } else { + jumpToItem(previousIndex); + } + } + + function pageWouldBeHidden(page: number) { + const startOffset = itemsDisplayedOnPage <= 4 && page > 1 ? 0 : 1; + + const wouldBeHiddenInFront = page >= startOffset && page <= cuts.front; + const wouldBeHiddenInBack = + page >= totalItems - cuts.back - 1 && page <= totalItems - 2; + + return wouldBeHiddenInFront || wouldBeHiddenInBack; + } + + // jump to new page if props.page is updated + useEffect(() => { + setCurrentPage(page); + }, [page]); + + // re-calculate cuts if props.totalItems or props.itemsShown change + useEffect(() => { + setItemsDisplayedOnPage(itemsShown >= 4 ? itemsShown : 4); + setCuts(calculateCuts(currentPage, totalItems, itemsShown)); + }, [totalItems, itemsShown]); // eslint-disable-line react-hooks/exhaustive-deps + + // update cuts if necessary whenever currentPage changes + useEffect(() => { + if (pageWouldBeHidden(currentPage)) { + const delta = currentPage - (prevPage || 0); + + if (delta > 0) { + const splitPoint = itemsDisplayedOnPage - 3; + setCuts( + calculateCuts( + currentPage, + totalItems, + itemsDisplayedOnPage, + splitPoint + ) + ); + } else { + const splitPoint = itemsDisplayedOnPage > 4 ? 2 : 1; + setCuts( + calculateCuts( + currentPage, + totalItems, + itemsDisplayedOnPage, + splitPoint + ) + ); + } + } + }, [currentPage]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + setIsOverFlowDisabled(disableOverflow); + }, [disableOverflow]); + + const classNames = classnames(`${prefix}--pagination-nav`, className); + + const backwardButtonDisabled = !loop && currentPage === 0; + const forwardButtonDisabled = !loop && currentPage === totalItems - 1; + + const startOffset = itemsDisplayedOnPage <= 4 && currentPage > 1 ? 0 : 1; + + return ( + + ); + } +); + +DirectionButton.propTypes = { + /** + * The direction this button represents ("forward" or "backward"). + */ + direction: PropTypes.oneOf(['forward', 'backward']), + + /** + * Whether or not the button should be disabled. + */ + disabled: PropTypes.bool, + + /** + * The label shown in the button's tooltip. + */ + label: PropTypes.string, + + /** + * The callback function called when the button is clicked. + */ + onClick: PropTypes.func, +}; + +PaginationItem.propTypes = { + /** + * Whether or not this is the currently active page. + */ + isActive: PropTypes.bool, + + /** + * The callback function called when the item is clicked. + */ + onClick: PropTypes.func, + + /** + * The page number this item represents. + */ + page: PropTypes.number, + + /** + * Specify a custom translation function that takes in a message identifier + * and returns the localized string for the message + */ + translateWithId: PropTypes.func, +}; + +PaginationOverflow.propTypes = { + /** + * How many items to display in this overflow. + */ + count: PropTypes.number, + + /** + * From which index on this overflow should start displaying pages. + */ + fromIndex: PropTypes.number, + + /** + * The callback function called when the user selects a page from the overflow. + */ + onSelect: PropTypes.func, + + /** + * Specify a custom translation function that takes in a message identifier + * and returns the localized string for the message + */ + translateWithId: PropTypes.func, +}; + +PaginationNav.displayName = 'PaginationNav'; +PaginationNav.propTypes = { + /** + * Additional CSS class names. + */ + className: PropTypes.string, + + /** + * If true, the '...' pagination overflow will not render page links between the first and last rendered buttons. + * Set this to true if you are having performance problems with large data sets. + */ + disableOverflow: PropTypes.bool, // eslint-disable-line react/prop-types + + /** + * The number of items to be shown. + */ + itemsShown: PropTypes.number, + + /** + * Whether user should be able to loop through the items when reaching first / last. + */ + loop: PropTypes.bool, + + /** + * The callback function called when the current page changes. + */ + onChange: PropTypes.func, + + /** + * The index of current page. + */ + page: PropTypes.number, + + /** + * The total number of items. + */ + totalItems: PropTypes.number, + + /** + * Specify a custom translation function that takes in a message identifier + * and returns the localized string for the message + */ + translateWithId: PropTypes.func, +}; + +export default PaginationNav; diff --git a/packages/react/src/components/PaginationNav/index.js b/packages/react/src/components/PaginationNav/index.ts similarity index 100% rename from packages/react/src/components/PaginationNav/index.js rename to packages/react/src/components/PaginationNav/index.ts diff --git a/packages/react/src/components/Slug/Slug-examples.stories.js b/packages/react/src/components/Slug/Slug-examples.stories.js index 8e8e92a506a6..764fc82ea31b 100644 --- a/packages/react/src/components/Slug/Slug-examples.stories.js +++ b/packages/react/src/components/Slug/Slug-examples.stories.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import Button from '../Button'; import Checkbox from '../Checkbox'; import CheckboxGroup from '../CheckboxGroup'; @@ -276,44 +276,88 @@ export const _Combobox = { }; export const _ComposedModal = { + args: { + showButtons: true, + hasScrollingContent: false, + }, argTypes: { slug: { description: '**Experimental**: Provide a `Slug` component to be rendered inside the component', }, + hasScrollingContent: { + description: 'Add scrolling content indicator', + }, + showButtons: { + description: 'Show or hide the Modal buttons', + }, + }, + render: (args) => { + const [open, setOpen] = useState(true); // eslint-disable-line + return ( +
    + + setOpen(false)} slug={slug}> + + +

    + Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a + shared domain, a shared subdomain, or a shared domain and host. +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus + eu nibh odio. Nunc a consequat est, id porttitor sapien. Proin + vitae leo vitae orci tincidunt auctor eget eget libero. Ut + tincidunt ultricies fringilla. Aliquam erat volutpat. Aenean arcu + odio, elementum vel vehicula vitae, porttitor ac lorem. Sed + viverra elit ac risus tincidunt fermentum. Ut sollicitudin nibh id + risus ornare ornare. Etiam gravida orci ut lectus dictum, quis + ultricies felis mollis. Mauris nec commodo est, nec faucibus nibh. + Nunc commodo ante quis pretium consectetur. Ut ac nisl vitae mi + mattis vulputate a at elit. Nullam porttitor ex eget mi feugiat + mattis. Nunc non sodales magna. Proin ornare tellus quis hendrerit + egestas. Donec pharetra leo nec molestie sollicitudin. +

    + + + +

    + Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a + shared domain, a shared subdomain, or a shared domain and host. +

    + +
    + + {args.showButtons && ( + + )} +
    +
    + ); }, - render: () => ( -
    - - - -

    - Custom domains direct requests for your apps in this Cloud Foundry - organization to a URL that you own. A custom domain can be a shared - domain, a shared subdomain, or a shared domain and host. -

    - - -
    - -
    -
    - ), }; export const _DatePicker = { @@ -376,40 +420,71 @@ export const _FilterableMultiselect = { }; export const _Modal = { + args: { + showButtons: true, + hasScrollingContent: false, + }, argTypes: { slug: { description: '**Experimental**: Provide a `Slug` component to be rendered inside the component', }, + hasScrollingContent: { + description: 'Add scrolling content indicator', + }, + showButtons: { + description: 'Show or hide the Modal buttons', + }, + }, + render: (args) => { + const [open, setOpen] = useState(true); // eslint-disable-line + return ( +
    + + setOpen(false)} + modalHeading="Add a custom domain" + modalLabel="Account resources" + primaryButtonText="Add" + secondaryButtonText="Cancel" + passiveModal={!args.showButtons} + hasScrollingContent={args.hasScrollingContent} + slug={slug}> +

    + Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus + eu nibh odio. Nunc a consequat est, id porttitor sapien. Proin vitae + leo vitae orci tincidunt auctor eget eget libero. Ut tincidunt + ultricies fringilla. Aliquam erat volutpat. Aenean arcu odio, + elementum vel vehicula vitae, porttitor ac lorem. Sed viverra elit + ac risus tincidunt fermentum. Ut sollicitudin nibh id risus ornare + ornare. Etiam gravida orci ut lectus dictum, quis ultricies felis + mollis. Mauris nec commodo est, nec faucibus nibh. Nunc commodo ante + quis pretium consectetur. Ut ac nisl vitae mi mattis vulputate a at + elit. Nullam porttitor ex eget mi feugiat mattis. Nunc non sodales + magna. Proin ornare tellus quis hendrerit egestas. Donec pharetra + leo nec molestie sollicitudin. +

    + + +