diff --git a/jest.config.js b/jest.config.js index 4e28a21b5ff..620b84b3b63 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,7 +10,8 @@ module.exports = { ], roots: ['/packages'], transform: { - '^.+\\.[jt]sx?$': 'babel-jest' + '^.+\\.[jt]sx?$': 'babel-jest', + '^.+\\.svg$': 'jest-transform-stub' }, setupFilesAfterEnv: ['/packages/testSetup.ts'], transformIgnorePatterns: ['node_modules/(?!@patternfly|@novnc|@popperjs|lodash|monaco-editor|react-monaco-editor)'], diff --git a/package.json b/package.json index f1271956560..45f13ae3a79 100644 --- a/package.json +++ b/package.json @@ -37,13 +37,14 @@ "@octokit/rest": "^20.0.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "14.4.3", - "@types/jest": "29.5.3", + "@testing-library/user-event": "14.5.1", + "@types/jest": "29.5.5", "@types/react": "^18", "@types/react-dom": "^18", "@typescript-eslint/eslint-plugin": "^5.59.2", "@typescript-eslint/parser": "^5.59.2", "babel-jest": "^27.2.5", + "jest-transform-stub": "^2.0.0", "concurrently": "^7.6.0", "eslint": "^8.39.0", "eslint-plugin-markdown": "^3.0.0", diff --git a/packages/eslint-plugin-patternfly-react/CHANGELOG.md b/packages/eslint-plugin-patternfly-react/CHANGELOG.md index d176e2cdb47..ed9512b4598 100644 --- a/packages/eslint-plugin-patternfly-react/CHANGELOG.md +++ b/packages/eslint-plugin-patternfly-react/CHANGELOG.md @@ -9,6 +9,14 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline - Release alphas from v6 and rebase from main ([#9692](https://github.com/patternfly/patternfly-react/issues/9692)) ([bb022ff](https://github.com/patternfly/patternfly-react/commit/bb022ffc65da8e8c5b5c984412f936cea9424676)), closes [#9651](https://github.com/patternfly/patternfly-react/issues/9651) [#9627](https://github.com/patternfly/patternfly-react/issues/9627) [#9555](https://github.com/patternfly/patternfly-react/issues/9555) [#9543](https://github.com/patternfly/patternfly-react/issues/9543) [#9578](https://github.com/patternfly/patternfly-react/issues/9578) [#9519](https://github.com/patternfly/patternfly-react/issues/9519) [#9603](https://github.com/patternfly/patternfly-react/issues/9603) [#9655](https://github.com/patternfly/patternfly-react/issues/9655) [#9614](https://github.com/patternfly/patternfly-react/issues/9614) [#9606](https://github.com/patternfly/patternfly-react/issues/9606) [#9628](https://github.com/patternfly/patternfly-react/issues/9628) [#9635](https://github.com/patternfly/patternfly-react/issues/9635) [#9649](https://github.com/patternfly/patternfly-react/issues/9649) [#9642](https://github.com/patternfly/patternfly-react/issues/9642) [#9633](https://github.com/patternfly/patternfly-react/issues/9633) [#9637](https://github.com/patternfly/patternfly-react/issues/9637) [#9584](https://github.com/patternfly/patternfly-react/issues/9584) [#9284](https://github.com/patternfly/patternfly-react/issues/9284) +# [5.2.0-prerelease.0](https://github.com/patternfly/patternfly-react/compare/eslint-plugin-patternfly-react@5.1.1...eslint-plugin-patternfly-react@5.2.0-prerelease.0) (2023-10-05) + +**Note:** Version bump only for package eslint-plugin-patternfly-react + +## 5.1.1 (2023-10-05) + +**Note:** Version bump only for package eslint-plugin-patternfly-react + ## 5.1.1-prerelease.1 (2023-08-29) ### Bug Fixes diff --git a/packages/eslint-plugin-patternfly-react/package.json b/packages/eslint-plugin-patternfly-react/package.json index 5d650a4db57..d236648c25a 100644 --- a/packages/eslint-plugin-patternfly-react/package.json +++ b/packages/eslint-plugin-patternfly-react/package.json @@ -1,6 +1,7 @@ { "name": "eslint-plugin-patternfly-react", "version": "6.0.0-alpha.1", + "version": "5.2.0-prerelease.0", "private": false, "main": "./lib/index.js", "license": "MIT", diff --git a/packages/react-charts/CHANGELOG.md b/packages/react-charts/CHANGELOG.md index f5979cb9505..11a8c78de76 100644 --- a/packages/react-charts/CHANGELOG.md +++ b/packages/react-charts/CHANGELOG.md @@ -19,6 +19,52 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline **Note:** Version bump only for package @patternfly/react-charts +# 7.2.0-prerelease.5 (2023-10-26) + +**Note:** Version bump only for package @patternfly/react-charts + +# 7.2.0-prerelease.4 (2023-10-10) + +**Note:** Version bump only for package @patternfly/react-charts + +# [7.2.0-prerelease.3](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@7.2.0-prerelease.2...@patternfly/react-charts@7.2.0-prerelease.3) (2023-10-10) + +**Note:** Version bump only for package @patternfly/react-charts + +# 7.2.0-prerelease.2 (2023-10-09) + +**Note:** Version bump only for package @patternfly/react-charts + +# [7.2.0-prerelease.1](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@7.2.0-prerelease.0...@patternfly/react-charts@7.2.0-prerelease.1) (2023-10-09) + +### Bug Fixes + +- **charts:** legendAllowWrap function generates an null error ([#9719](https://github.com/patternfly/patternfly-react/issues/9719)) ([5b4cbf4](https://github.com/patternfly/patternfly-react/commit/5b4cbf41bf11785ffe66dc263c7c26e61d25cacd)) + +# [7.2.0-prerelease.0](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@7.1.1...@patternfly/react-charts@7.2.0-prerelease.0) (2023-10-05) + +**Note:** Version bump only for package @patternfly/react-charts + +## [7.1.1](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@7.1.1-prerelease.10...@patternfly/react-charts@7.1.1) (2023-10-05) + +**Note:** Version bump only for package @patternfly/react-charts + +## 7.1.1-prerelease.10 (2023-10-03) + +### Bug Fixes + +- whitespace changes to trigger prereleases ([#9702](https://github.com/patternfly/patternfly-react/issues/9702)) ([741c248](https://github.com/patternfly/patternfly-react/commit/741c24825b503e116c77cf304aa3e4e3a9ff7841)) + +## [7.1.1-prerelease.9](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@7.1.1-prerelease.8...@patternfly/react-charts@7.1.1-prerelease.9) (2023-09-22) + +### Features + +- **charts:** add RTL legend support ([#9570](https://github.com/patternfly/patternfly-react/issues/9570)) ([99d8d97](https://github.com/patternfly/patternfly-react/commit/99d8d975eeb2d562357cdcaa37502748d5794564)) + +## 7.1.1-prerelease.8 (2023-09-21) + +**Note:** Version bump only for package @patternfly/react-charts + ## [7.1.1-prerelease.7](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@7.1.1-prerelease.6...@patternfly/react-charts@7.1.1-prerelease.7) (2023-09-18) **Note:** Version bump only for package @patternfly/react-charts diff --git a/packages/react-charts/README.md b/packages/react-charts/README.md index ccb84630357..f4afdae7292 100644 --- a/packages/react-charts/README.md +++ b/packages/react-charts/README.md @@ -89,4 +89,4 @@ yarn test packages/react-charts ``` [patternfly-4]: https://github.com/patternfly/patternfly -[docs]: https://patternfly-react.surge.sh/ +[docs]: https://react-staging.patternfly.org/ diff --git a/packages/react-charts/src/components/Chart/Chart.tsx b/packages/react-charts/src/components/Chart/Chart.tsx index 10803be4699..4c5882dbad1 100644 --- a/packages/react-charts/src/components/Chart/Chart.tsx +++ b/packages/react-charts/src/components/Chart/Chart.tsx @@ -627,7 +627,7 @@ export const Chart: React.FunctionComponent = ({ // Callback to compliment legendAllowWrap const computedLegend = getLegend(); useEffect(() => { - if (typeof legendAllowWrap === 'function') { + if (computedLegend?.props && typeof legendAllowWrap === 'function') { const extraHeight = getLegendItemsExtraHeight({ legendData: computedLegend.props.data, legendOrientation: computedLegend.props.orientation, diff --git a/packages/react-charts/src/components/ChartPie/ChartPie.tsx b/packages/react-charts/src/components/ChartPie/ChartPie.tsx index 899cabe0bd7..61263aeecc8 100644 --- a/packages/react-charts/src/components/ChartPie/ChartPie.tsx +++ b/packages/react-charts/src/components/ChartPie/ChartPie.tsx @@ -651,7 +651,7 @@ export const ChartPie: React.FunctionComponent = ({ // Callback to compliment legendAllowWrap const computedLegend = getLegend(); useEffect(() => { - if (typeof legendAllowWrap === 'function') { + if (computedLegend?.props && typeof legendAllowWrap === 'function') { const extraHeight = getLegendItemsExtraHeight({ legendData: computedLegend.props.data, legendOrientation: computedLegend.props.orientation, diff --git a/packages/react-charts/src/components/ResizeObserver/examples/resizeObserver.md b/packages/react-charts/src/components/ResizeObserver/examples/resizeObserver.md index adfe14c99bc..0e77eac5cbb 100644 --- a/packages/react-charts/src/components/ResizeObserver/examples/resizeObserver.md +++ b/packages/react-charts/src/components/ResizeObserver/examples/resizeObserver.md @@ -161,6 +161,24 @@ class MultiColorChart extends React.Component { this.setState({ width: this.containerRef.current.clientWidth }); } }; + this.handleLegendAllowWrap = (extraHeight) => { + if (extraHeight !== this.state.extraHeight) { + this.setState({ extraHeight }); + } + } + this.getHeight = (baseHeight) => { + const { extraHeight } = this.state; + return baseHeight + extraHeight; + }; + this.getPadding = () => { + const { extraHeight } = this.state; + return { + bottom: 100 + extraHeight, // Adjusted to accomodate legend + left: 50, + right: 50, + top: 50, + }; + }; } componentDidMount() { @@ -174,11 +192,11 @@ class MultiColorChart extends React.Component { render() { const { width } = this.state; - const itemsPerRow = width > 650 ? 4 : 2; + const height = this.getHeight(250); return (
-
+
} + legendAllowWrap={this.handleLegendAllowWrap} legendPosition="bottom-left" legendComponent={ } - height={250} + height={height} name="chart2" - padding={{ - bottom: 100, // Adjusted to accomodate legend - left: 50, - right: 50, - top: 50 - }} + padding={this.getPadding()} maxDomain={{ y: 9 }} themeColor={ChartThemeColor.multiUnordered} width={width} diff --git a/packages/react-code-editor/CHANGELOG.md b/packages/react-code-editor/CHANGELOG.md index e52f550d09a..18bd629e2e0 100644 --- a/packages/react-code-editor/CHANGELOG.md +++ b/packages/react-code-editor/CHANGELOG.md @@ -45,6 +45,134 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline **Note:** Version bump only for package @patternfly/react-code-editor +# [5.2.0-prerelease.19](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.18...@patternfly/react-code-editor@5.2.0-prerelease.19) (2023-10-30) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.18](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.17...@patternfly/react-code-editor@5.2.0-prerelease.18) (2023-10-27) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.17](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.16...@patternfly/react-code-editor@5.2.0-prerelease.17) (2023-10-27) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.16](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.15...@patternfly/react-code-editor@5.2.0-prerelease.16) (2023-10-27) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.15](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.14...@patternfly/react-code-editor@5.2.0-prerelease.15) (2023-10-26) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.14](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.13...@patternfly/react-code-editor@5.2.0-prerelease.14) (2023-10-23) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.13](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.12...@patternfly/react-code-editor@5.2.0-prerelease.13) (2023-10-20) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.12](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.11...@patternfly/react-code-editor@5.2.0-prerelease.12) (2023-10-18) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.11](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.10...@patternfly/react-code-editor@5.2.0-prerelease.11) (2023-10-17) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.10](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.9...@patternfly/react-code-editor@5.2.0-prerelease.10) (2023-10-16) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.9](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.8...@patternfly/react-code-editor@5.2.0-prerelease.9) (2023-10-16) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.8](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.7...@patternfly/react-code-editor@5.2.0-prerelease.8) (2023-10-16) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.7](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.6...@patternfly/react-code-editor@5.2.0-prerelease.7) (2023-10-16) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.6](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.5...@patternfly/react-code-editor@5.2.0-prerelease.6) (2023-10-12) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.5](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.4...@patternfly/react-code-editor@5.2.0-prerelease.5) (2023-10-10) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.4](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.3...@patternfly/react-code-editor@5.2.0-prerelease.4) (2023-10-10) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.3](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.2...@patternfly/react-code-editor@5.2.0-prerelease.3) (2023-10-10) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# 5.2.0-prerelease.2 (2023-10-09) + +**Note:** Version bump only for package @patternfly/react-code-editor + +# [5.2.0-prerelease.1](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.2.0-prerelease.0...@patternfly/react-code-editor@5.2.0-prerelease.1) (2023-10-09) + +### Bug Fixes + +- **CodeEditor:** hide button and link when read-only ([#9668](https://github.com/patternfly/patternfly-react/issues/9668)) ([0346933](https://github.com/patternfly/patternfly-react/commit/0346933cb61a3b11a2591acbde4760f58abffbcb)) + +# [5.2.0-prerelease.0](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.1.1...@patternfly/react-code-editor@5.2.0-prerelease.0) (2023-10-05) + +**Note:** Version bump only for package @patternfly/react-code-editor + +## [5.1.1](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.1.1-prerelease.23...@patternfly/react-code-editor@5.1.1) (2023-10-05) + +**Note:** Version bump only for package @patternfly/react-code-editor + +## 5.1.1-prerelease.23 (2023-10-03) + +### Bug Fixes + +- whitespace changes to trigger prereleases ([#9702](https://github.com/patternfly/patternfly-react/issues/9702)) ([741c248](https://github.com/patternfly/patternfly-react/commit/741c24825b503e116c77cf304aa3e4e3a9ff7841)) + +## [5.1.1-prerelease.22](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.1.1-prerelease.21...@patternfly/react-code-editor@5.1.1-prerelease.22) (2023-09-25) + +**Note:** Version bump only for package @patternfly/react-code-editor + +## [5.1.1-prerelease.21](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.1.1-prerelease.20...@patternfly/react-code-editor@5.1.1-prerelease.21) (2023-09-25) + +**Note:** Version bump only for package @patternfly/react-code-editor + +## [5.1.1-prerelease.20](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.1.1-prerelease.19...@patternfly/react-code-editor@5.1.1-prerelease.20) (2023-09-22) + +**Note:** Version bump only for package @patternfly/react-code-editor + +## [5.1.1-prerelease.19](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.1.1-prerelease.18...@patternfly/react-code-editor@5.1.1-prerelease.19) (2023-09-21) + +**Note:** Version bump only for package @patternfly/react-code-editor + +## [5.1.1-prerelease.18](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.1.1-prerelease.17...@patternfly/react-code-editor@5.1.1-prerelease.18) (2023-09-21) + +**Note:** Version bump only for package @patternfly/react-code-editor + +## [5.1.1-prerelease.17](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.1.1-prerelease.16...@patternfly/react-code-editor@5.1.1-prerelease.17) (2023-09-21) + +**Note:** Version bump only for package @patternfly/react-code-editor + +## [5.1.1-prerelease.16](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.1.1-prerelease.15...@patternfly/react-code-editor@5.1.1-prerelease.16) (2023-09-21) + +**Note:** Version bump only for package @patternfly/react-code-editor + +## [5.1.1-prerelease.15](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.1.1-prerelease.14...@patternfly/react-code-editor@5.1.1-prerelease.15) (2023-09-21) + +**Note:** Version bump only for package @patternfly/react-code-editor + +## [5.1.1-prerelease.14](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.1.1-prerelease.13...@patternfly/react-code-editor@5.1.1-prerelease.14) (2023-09-20) + +**Note:** Version bump only for package @patternfly/react-code-editor + ## [5.1.1-prerelease.13](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.1.1-prerelease.12...@patternfly/react-code-editor@5.1.1-prerelease.13) (2023-09-19) ### Bug Fixes diff --git a/packages/react-code-editor/src/components/CodeEditor/CodeEditor.tsx b/packages/react-code-editor/src/components/CodeEditor/CodeEditor.tsx index 8af4ada5c97..2dcbacc83b2 100644 --- a/packages/react-code-editor/src/components/CodeEditor/CodeEditor.tsx +++ b/packages/react-code-editor/src/components/CodeEditor/CodeEditor.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/CodeEditor/code-editor'; +import fileUploadStyles from '@patternfly/react-styles/css/components/FileUpload/file-upload'; import { Button, ButtonVariant, @@ -526,18 +527,20 @@ class CodeEditor extends React.Component { headingLevel="h4" /> {emptyStateBody} - - - - - - - - + {!isReadOnly && ( + + + + + + + + + )} ) : ( @@ -546,13 +549,15 @@ class CodeEditor extends React.Component { icon={} headingLevel="h4" /> - - - - - + {!isReadOnly && ( + + + + + + )} )); @@ -605,7 +610,7 @@ class CodeEditor extends React.Component { } {
{headerMainContent}
} {!!shortcutsPopoverProps.bodyContent && ( -
+
- - - {alerts} - + + + {alerts} ); }; @@ -137,7 +158,7 @@ const AlertTimeout: React.FunctionComponent = () => { An alert can contain additional, hidden information that is made visible when users click a caret icon. This information can be expanded and collapsed each time the icon is clicked. -It is not recommended to use an expandable alert with a `timeout` in a [toast alert group](/components/alert#toast-alert-group) because the alert could timeout before users have time to interact with and view the entire alert. +It is not recommended to use an expandable alert with a `timeout` in a [toast alert group](/components/alert#toast-alert-group) because the alert could timeout before users have time to interact with and view the entire alert. See the [toast alert considerations](/components/alert/accessibility#toast-alerts) section of the alert accessibility documentation to understand the accessibility risks associated with using toast alerts. @@ -150,13 +171,8 @@ import { Alert, AlertActionCloseButton, AlertActionLink } from '@patternfly/reac isExpandable variant="success" title="Success alert title" - actionClose={ alert('Clicked the close button')} />} - actionLinks={ - - alert('Clicked on View details')}>View details - alert('Clicked on Ignore')}>Ignore - - } + // eslint-disable-next-line no-console + actionClose={ console.log('Clicked the close button')} />} >

Success alert description. This should tell the user more information about the alert.

@@ -165,17 +181,22 @@ import { Alert, AlertActionCloseButton, AlertActionLink } from '@patternfly/reac isInline variant="success" title="Success alert title" - actionClose={ alert('Clicked the close button')} />} actionLinks={ - alert('Clicked on View details')}>View details - alert('Clicked on Ignore')}>Ignore + + View details + + console.log('Clicked on Ignore')} + > + Ignore + - } + } >

Success alert description. This should tell the user more information about the alert.

- +; ``` ### Truncated alerts @@ -187,16 +208,28 @@ import React from 'react'; import { Alert } from '@patternfly/react-core'; - - + - + - + `} + /> +; ``` ### Custom icons @@ -218,7 +251,7 @@ import LaptopIcon from '@patternfly/react-icons/dist/esm/icons/laptop-icon'; } variant="success" title="Success alert title" /> } variant="warning" title="Warning alert title" /> } variant="danger" title="Danger alert title" /> - +; ``` ### Inline alerts variants @@ -234,8 +267,9 @@ import { Alert } from '@patternfly/react-core'; - +; ``` + ### Inline alert variations All general alert variations can use the `isInline` property to apply inline styling. @@ -248,17 +282,22 @@ import { Alert, AlertActionCloseButton, AlertActionLink } from '@patternfly/reac isInline variant="success" title="Success alert title" - actionClose={ alert('Clicked the close button')} />} actionLinks={ - alert('Clicked on View details')}>View details - alert('Clicked on Ignore')}>Ignore + + View details + + console.log('Clicked on Ignore')} + > + Ignore + } >

Success alert description. This should tell the user more information about the alert.

- alert('Clicked the close button')} />}> +

Success alert description. This should tell the user more information about the alert.{' '} This is a link. @@ -268,22 +307,16 @@ import { Alert, AlertActionCloseButton, AlertActionLink } from '@patternfly/reac isInline variant="success" title="Success alert title" - actionClose={ alert('Clicked the close button')} />} - actionLinks={ - - alert('Clicked on View details')}>View details - alert('Clicked on Ignore')}>Ignore - - } - /> - alert('Clicked the close button')} />} - /> - - + // eslint-disable-next-line no-console + actionClose={ console.log('Clicked the close button')} />} + > +

Short alert description.

+
+ + +

Short alert description.

+
+; ``` ### Plain inline alert variants @@ -299,7 +332,7 @@ import { Alert } from '@patternfly/react-core'; - +; ``` ### Plain inline alert variations @@ -308,15 +341,10 @@ It is not recommended to use a plain inline alert with `actionClose` nor `action ```ts import React from 'react'; -import { Alert, AlertActionCloseButton, AlertActionLink } from '@patternfly/react-core'; - +import { Alert } from '@patternfly/react-core'; +

Success alert description. This should tell the user more information about the alert.

-
+
; ``` ### Static live region alerts @@ -334,7 +362,8 @@ import { Alert, AlertActionCloseButton } from '@patternfly/react-core'; isLiveRegion variant="info" title="Default live region configuration" - actionClose={ alert('Clicked the close button')} />} + // eslint-disable-next-line no-console + actionClose={ console.log('Clicked the close button')} />} > This alert uses the recommended isLiveRegion prop to automatically set ARIA attributes and CSS classes.
@@ -344,12 +373,13 @@ import { Alert, AlertActionCloseButton } from '@patternfly/react-core'; aria-atomic="true" variant="info" title="Customized live region" - actionClose={ alert('Clicked the close button')} />} + // eslint-disable-next-line no-console + actionClose={ console.log('Clicked the close button')} />} > - You can alternatively omit the isLiveRegion prop to specify ARIA attributes and CSS manually on - the containing element. + You can alternatively omit the isLiveRegion prop to specify ARIA attributes and CSS manually on the + containing element.
- +; ``` ### Dynamic live region alerts @@ -357,19 +387,22 @@ import { Alert, AlertActionCloseButton } from '@patternfly/react-core'; Alerts that are asynchronously appended into dynamic [alert groups](/components/alert/#alert-group-examples) via the `isLiveRegion` property will be announced to assistive technology the moment the change happens, following the strategy used for `aria-atomic`, which defaults to false. This means only changes of type "addition" will be announced. ```ts file="AlertDynamicLiveRegion.tsx" + ``` -### Asynchronous live region alerts +### Asynchronous live region alerts + +This example shows how an alert could be triggered by an asynchronous event in the application. Note that you can customize how the alert will be announced to assistive technology. See the [alert accessibility](/components/alert/accessibility/) for more information. -This example shows how an alert could be triggered by an asynchronous event in the application. Note that you can customize how the alert will be announced to assistive technology. See the [https://www.patternfly.org/v4/components/alert/accessibility](alert accessibility) for more information. ```ts file="AlertAsyncLiveRegion.tsx" + ``` ## Alert group examples An alert group stacks and positions 2 or more alerts in a live region, either in a layer over the main content of a page or inline with the page content. Alert groups should always rank alerts by age, stacking new alerts on top of old ones as they surface. -### Alert group variants +### Alert group variants Alert groups can be one of the following variants: @@ -386,15 +419,17 @@ Dynamic alerts that are generated after the page initially loads must be appende All alert group variants may combine multiple [alert variants](/components/alert) For example, the following static inline alert group includes one "success" alert and one "info" alert. ```ts file="./AlertGroupStatic.tsx" + ``` ### Toast alert group -Toast alert groups are created by passing in the `isToast` and `isLiveRegion` properties. +Toast alert groups are created by passing in the `isToast` and `isLiveRegion` properties. Click the buttons in the example below to add alerts to a toast group. ```ts file="./AlertGroupToast.tsx" + ``` ### Toast alert group with overflow capture @@ -403,11 +438,12 @@ Users will see an overflow message once a predefined number of alerts are displa In the following example, an overflow message will appear when more than 4 alerts would be shown. When a 5th alert would appear, an overflow message is shown instead. -When an overflow message appears in an `AlertGroup` using the `isLiveRegion` property, the "view 1 more alert" text label will be announced, but the alert message will not. +When an overflow message appears in an `AlertGroup` using the `isLiveRegion` property, the "view 1 more alert" text label will be announced, but the alert message will not. -Users navigating via keyboard or another assistive technology will need a way to navigate to and reveal hidden alerts before they disappear. Alternatively, there should be a place where notifications or alerts are collected to be viewed or read later. +Users navigating via keyboard or another assistive technology will need a way to navigate to and reveal hidden alerts before they disappear. Alternatively, there should be a place where notifications or alerts are collected to be viewed or read later. ```ts file="AlertGroupToastOverflowCapture.tsx" + ``` ### Asynchronous alert groups @@ -417,6 +453,7 @@ The following example shows how alerts can be triggered by an asynchronous event See the [alert accessibility tab](/components/alert/accessibility) for more information on customizing this behavior. ```ts file="./AlertGroupAsync.tsx" + ``` ### Dynamic alert groups @@ -424,13 +461,15 @@ See the [alert accessibility tab](/components/alert/accessibility) for more info Click the buttons in the example below to add dynamic alerts to a group. ```ts file="./AlertGroupSingularDynamic.tsx" + ``` ### Dynamic alert group with overflow message -In the following example, there can be a maximum of 4 alerts shown at once. +In the following example, there can be a maximum of 4 alerts shown at once. ```ts file="AlertGroupSingularDynamicOverflow.tsx" + ``` ### Multiple dynamic alert groups @@ -438,4 +477,5 @@ In the following example, there can be a maximum of 4 alerts shown at once. You may add multiple alerts to an alert group at once. Click the "add alert collection" button in the example below to add a batch of 3 toast alerts to a group. ```ts file="./AlertGroupMultipleDynamic.tsx" + ``` diff --git a/packages/react-core/src/components/Alert/examples/AlertDynamicLiveRegion.tsx b/packages/react-core/src/components/Alert/examples/AlertDynamicLiveRegion.tsx index af6bbbf41fe..4a4bcfb74c8 100644 --- a/packages/react-core/src/components/Alert/examples/AlertDynamicLiveRegion.tsx +++ b/packages/react-core/src/components/Alert/examples/AlertDynamicLiveRegion.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Alert, AlertGroup, AlertVariant, InputGroup } from '@patternfly/react-core'; +import buttonStyles from '@patternfly/react-styles/css/components/Button/button'; interface AlertInfo { title: string; @@ -10,7 +11,7 @@ interface AlertInfo { export const DynamicLiveRegionAlert: React.FunctionComponent = () => { const [alerts, setAlerts] = React.useState([]); const getUniqueId: () => number = () => new Date().getTime(); - const btnClasses = ['pf-v5-c-button', 'pf-m-secondary'].join(' '); + const btnClasses = [buttonStyles.button, buttonStyles.modifiers.secondary].join(' '); const addAlert = (alertInfo: AlertInfo) => { setAlerts((prevAlertInfo) => [...prevAlertInfo, alertInfo]); diff --git a/packages/react-core/src/components/Alert/examples/AlertGroupAsync.tsx b/packages/react-core/src/components/Alert/examples/AlertGroupAsync.tsx index 3b4af10ddc1..2a2e894ec15 100644 --- a/packages/react-core/src/components/Alert/examples/AlertGroupAsync.tsx +++ b/packages/react-core/src/components/Alert/examples/AlertGroupAsync.tsx @@ -9,12 +9,13 @@ import { InputGroupItem, useInterval } from '@patternfly/react-core'; +import buttonStyles from '@patternfly/react-styles/css/components/Button/button'; export const AlertGroupAsync: React.FunctionComponent = () => { const [alerts, setAlerts] = React.useState[]>([]); const [isRunning, setIsRunning] = React.useState(false); - const btnClasses = ['pf-v5-c-button', 'pf-m-secondary'].join(' '); + const btnClasses = [buttonStyles.button, buttonStyles.modifiers.secondary].join(' '); const getUniqueId = () => new Date().getTime(); diff --git a/packages/react-core/src/components/Alert/examples/AlertGroupMultipleDynamic.tsx b/packages/react-core/src/components/Alert/examples/AlertGroupMultipleDynamic.tsx index 4573b4653e3..bc638abb4f2 100644 --- a/packages/react-core/src/components/Alert/examples/AlertGroupMultipleDynamic.tsx +++ b/packages/react-core/src/components/Alert/examples/AlertGroupMultipleDynamic.tsx @@ -8,6 +8,7 @@ import { InputGroup, InputGroupItem } from '@patternfly/react-core'; +import buttonStyles from '@patternfly/react-styles/css/components/Button/button'; export const AlertGroupMultipleDynamic: React.FunctionComponent = () => { const [alerts, setAlerts] = React.useState[]>([]); @@ -20,7 +21,7 @@ export const AlertGroupMultipleDynamic: React.FunctionComponent = () => { setAlerts((prevAlerts) => [...prevAlerts.filter((alert) => alert.key !== key)]); }; - const btnClasses = ['pf-v5-c-button', 'pf-m-secondary'].join(' '); + const btnClasses = [buttonStyles.button, buttonStyles.modifiers.secondary].join(' '); const getUniqueId = () => String.fromCharCode(65 + Math.floor(Math.random() * 26)) + Date.now(); diff --git a/packages/react-core/src/components/Alert/examples/AlertGroupSingularDynamic.tsx b/packages/react-core/src/components/Alert/examples/AlertGroupSingularDynamic.tsx index 577c6a30c71..d70d1584e8b 100644 --- a/packages/react-core/src/components/Alert/examples/AlertGroupSingularDynamic.tsx +++ b/packages/react-core/src/components/Alert/examples/AlertGroupSingularDynamic.tsx @@ -8,6 +8,7 @@ import { InputGroup, InputGroupItem } from '@patternfly/react-core'; +import buttonStyles from '@patternfly/react-styles/css/components/Button/button'; export const AlertGroupSingularDynamic: React.FunctionComponent = () => { const [alerts, setAlerts] = React.useState[]>([]); @@ -20,7 +21,7 @@ export const AlertGroupSingularDynamic: React.FunctionComponent = () => { setAlerts((prevAlerts) => [...prevAlerts.filter((alert) => alert.key !== key)]); }; - const btnClasses = ['pf-v5-c-button', 'pf-m-secondary'].join(' '); + const btnClasses = [buttonStyles.button, buttonStyles.modifiers.secondary].join(' '); const getUniqueId = () => new Date().getTime(); diff --git a/packages/react-core/src/components/Alert/examples/AlertGroupSingularDynamicOverflow.tsx b/packages/react-core/src/components/Alert/examples/AlertGroupSingularDynamicOverflow.tsx index 8f2d5172432..f827dbe5048 100644 --- a/packages/react-core/src/components/Alert/examples/AlertGroupSingularDynamicOverflow.tsx +++ b/packages/react-core/src/components/Alert/examples/AlertGroupSingularDynamicOverflow.tsx @@ -8,6 +8,7 @@ import { InputGroup, InputGroupItem } from '@patternfly/react-core'; +import buttonStyles from '@patternfly/react-styles/css/components/Button/button'; export const AlertGroupSingularDynamicOverflow: React.FunctionComponent = () => { const [alerts, setAlerts] = React.useState[]>([]); @@ -34,7 +35,7 @@ export const AlertGroupSingularDynamicOverflow: React.FunctionComponent = () => setOverflowMessage(getOverflowMessage(newAlerts.length)); }; - const btnClasses = ['pf-v5-c-button', 'pf-m-secondary'].join(' '); + const btnClasses = [buttonStyles.button, buttonStyles.modifiers.secondary].join(' '); const getUniqueId = () => new Date().getTime(); diff --git a/packages/react-core/src/components/Alert/examples/AlertGroupToast.tsx b/packages/react-core/src/components/Alert/examples/AlertGroupToast.tsx index be6ca455fe4..1bb01c58f83 100644 --- a/packages/react-core/src/components/Alert/examples/AlertGroupToast.tsx +++ b/packages/react-core/src/components/Alert/examples/AlertGroupToast.tsx @@ -8,6 +8,7 @@ import { InputGroup, InputGroupItem } from '@patternfly/react-core'; +import buttonStyles from '@patternfly/react-styles/css/components/Button/button'; export const AlertGroupToast: React.FunctionComponent = () => { const [alerts, setAlerts] = React.useState[]>([]); @@ -20,7 +21,7 @@ export const AlertGroupToast: React.FunctionComponent = () => { setAlerts((prevAlerts) => [...prevAlerts.filter((alert) => alert.key !== key)]); }; - const btnClasses = ['pf-v5-c-button', 'pf-m-secondary'].join(' '); + const btnClasses = [buttonStyles.button, buttonStyles.modifiers.secondary].join(' '); const getUniqueId = () => new Date().getTime(); diff --git a/packages/react-core/src/components/Alert/examples/AlertGroupToastOverflowCapture.tsx b/packages/react-core/src/components/Alert/examples/AlertGroupToastOverflowCapture.tsx index 4115d295cab..88c94e7b4ba 100644 --- a/packages/react-core/src/components/Alert/examples/AlertGroupToastOverflowCapture.tsx +++ b/packages/react-core/src/components/Alert/examples/AlertGroupToastOverflowCapture.tsx @@ -8,6 +8,7 @@ import { InputGroup, InputGroupItem } from '@patternfly/react-core'; +import buttonStyles from '@patternfly/react-styles/css/components/Button/button'; export const AlertGroupToastOverflowCapture: React.FunctionComponent = () => { const [alerts, setAlerts] = React.useState[]>([]); @@ -34,7 +35,7 @@ export const AlertGroupToastOverflowCapture: React.FunctionComponent = () => { setOverflowMessage(getOverflowMessage(newAlerts.length)); }; - const btnClasses = ['pf-v5-c-button', 'pf-m-secondary'].join(' '); + const btnClasses = [buttonStyles.button, buttonStyles.modifiers.secondary].join(' '); const getUniqueId = () => new Date().getTime(); diff --git a/packages/react-core/src/components/Avatar/__tests__/Avatar.test.tsx b/packages/react-core/src/components/Avatar/__tests__/Avatar.test.tsx index b8ac9f13e98..ad3a30e2684 100644 --- a/packages/react-core/src/components/Avatar/__tests__/Avatar.test.tsx +++ b/packages/react-core/src/components/Avatar/__tests__/Avatar.test.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { render, screen } from '@testing-library/react'; import { Avatar } from '../Avatar'; +import styles from '@patternfly/react-styles/css/components/Avatar/avatar'; test('Renders simple avatar', () => { render( @@ -13,7 +14,7 @@ test('Renders simple avatar', () => { test('Renders without any modifier class when border and size props are not passed', () => { render(); - expect(screen.getByRole('img')).toHaveClass('pf-v5-c-avatar', { exact: true }); + expect(screen.getByRole('img')).toHaveClass(styles.avatar, { exact: true }); }); test('Renders with class name pf-m-light when "light" is passed as border prop', () => { diff --git a/packages/react-core/src/components/BackToTop/BackToTop.tsx b/packages/react-core/src/components/BackToTop/BackToTop.tsx index 48d82742a41..2651bc6731f 100644 --- a/packages/react-core/src/components/BackToTop/BackToTop.tsx +++ b/packages/react-core/src/components/BackToTop/BackToTop.tsx @@ -34,12 +34,14 @@ const BackToTopBase: React.FunctionComponent = ({ const [scrollElement, setScrollElement] = React.useState(null); const toggleVisible = () => { - const scrolled = scrollElement.scrollY ? scrollElement.scrollY : scrollElement.scrollTop; - if (!isAlwaysVisible) { - if (scrolled > 400) { - setVisible(true); - } else { - setVisible(false); + if (scrollElement) { + const scrolled = scrollElement.scrollY ? scrollElement.scrollY : scrollElement.scrollTop; + if (!isAlwaysVisible) { + if (scrolled > 400) { + setVisible(true); + } else { + setVisible(false); + } } } }; diff --git a/packages/react-core/src/components/BackToTop/__tests__/BackToTop.test.tsx b/packages/react-core/src/components/BackToTop/__tests__/BackToTop.test.tsx index 4eb9ac41732..438bae01e72 100644 --- a/packages/react-core/src/components/BackToTop/__tests__/BackToTop.test.tsx +++ b/packages/react-core/src/components/BackToTop/__tests__/BackToTop.test.tsx @@ -2,6 +2,7 @@ import React, { RefObject } from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; import { BackToTop } from '../BackToTop'; import userEvent from '@testing-library/user-event'; +import styles from '@patternfly/react-styles/css/components/BackToTop/back-to-top'; jest.mock('../../Button'); @@ -18,7 +19,7 @@ test('Renders BackToTop', () => { test('Renders with the default class', () => { render(); - expect(screen.getByRole(`button`).parentElement).toHaveClass('pf-v5-c-back-to-top'); + expect(screen.getByRole(`button`).parentElement).toHaveClass(styles.backToTop); }); test('BackToTop is not yet visible', () => { diff --git a/packages/react-core/src/components/Backdrop/__tests__/Backdrop.test.tsx b/packages/react-core/src/components/Backdrop/__tests__/Backdrop.test.tsx index c4bb8386cf7..b49797cc25b 100644 --- a/packages/react-core/src/components/Backdrop/__tests__/Backdrop.test.tsx +++ b/packages/react-core/src/components/Backdrop/__tests__/Backdrop.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { Backdrop } from '../Backdrop'; +import styles from '@patternfly/react-styles/css/components/Backdrop/backdrop'; test('Renders without children', () => { render( @@ -16,14 +17,14 @@ test('Renders children', () => { expect(screen.getByText('Test')).toBeVisible(); }); -test('Renders with the pf-v5-c-backdrop', () => { +test(`Renders with the ${styles.backdrop}`, () => { render(Test); - expect(screen.getByText('Test')).toHaveClass('pf-v5-c-backdrop'); + expect(screen.getByText('Test')).toHaveClass(styles.backdrop); }); -test('Renders with only the class pf-v5-c-backdrop by default', () => { +test(`Renders with only the class ${styles.backdrop} by default`, () => { render(Test); - expect(screen.getByText('Test')).toHaveClass('pf-v5-c-backdrop', { exact: true }); + expect(screen.getByText('Test')).toHaveClass(styles.backdrop, { exact: true }); }); test('Renders with custom class name when className prop is passed', () => { diff --git a/packages/react-core/src/components/BackgroundImage/BackgroundImage.tsx b/packages/react-core/src/components/BackgroundImage/BackgroundImage.tsx index f8303224167..9586f2b5d96 100644 --- a/packages/react-core/src/components/BackgroundImage/BackgroundImage.tsx +++ b/packages/react-core/src/components/BackgroundImage/BackgroundImage.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/BackgroundImage/background-image'; +import cssBackgroundImage from '@patternfly/react-tokens/dist/esm/c_background_image_BackgroundImage'; export interface BackgroundImageProps extends React.HTMLProps { /** The URL or file path of the image for the background */ @@ -19,7 +20,7 @@ export const BackgroundImage: React.FunctionComponent = ({ className={css(styles.backgroundImage, className)} style={ { - '--pf-v5-c-background-image--BackgroundImage': `url(${src})` + [cssBackgroundImage.name]: `url(${src})` } as React.CSSProperties } {...props} diff --git a/packages/react-core/src/components/BackgroundImage/__tests__/BackgroundImage.test.tsx b/packages/react-core/src/components/BackgroundImage/__tests__/BackgroundImage.test.tsx index 5bed2e3e323..ecd1cb47a6a 100644 --- a/packages/react-core/src/components/BackgroundImage/__tests__/BackgroundImage.test.tsx +++ b/packages/react-core/src/components/BackgroundImage/__tests__/BackgroundImage.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { BackgroundImage } from '../BackgroundImage'; import styles from '@patternfly/react-styles/css/components/BackgroundImage/background-image'; +import cssBackgroundImage from '@patternfly/react-tokens/dist/esm/c_background_image_BackgroundImage'; test(`renders with default className ${styles.backgroundImage}`, () => { render(); @@ -15,11 +16,7 @@ test('spreads additional props', () => { test('has src URL applied to style', () => { render(); - - expect(screen.getByTestId('test-id')).toHaveAttribute( - 'style', - '--pf-v5-c-background-image--BackgroundImage: url(/image/url.png);' - ); + expect(screen.getByTestId('test-id')).toHaveAttribute('style', `${cssBackgroundImage.name}: url(/image/url.png);`); }); test('renders with custom className when one is provided', () => { diff --git a/packages/react-core/src/components/Badge/__tests__/Badge.test.tsx b/packages/react-core/src/components/Badge/__tests__/Badge.test.tsx index 0fdeb96dd03..5c565dc5543 100644 --- a/packages/react-core/src/components/Badge/__tests__/Badge.test.tsx +++ b/packages/react-core/src/components/Badge/__tests__/Badge.test.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { render, screen } from '@testing-library/react'; import { Badge } from '../Badge'; +import styles from '@patternfly/react-styles/css/components/Badge/badge'; test('Renders without children', () => { render( @@ -16,14 +17,14 @@ test('Renders children', () => { expect(screen.getByText('Test')).toBeVisible(); }); -test('Renders with class name pf-v5-c-badge', () => { +test(`Renders with class name ${styles.badge}`, () => { render(Test); - expect(screen.getByText('Test')).toHaveClass('pf-v5-c-badge'); + expect(screen.getByText('Test')).toHaveClass(styles.badge); }); -test('Renders with class name pf-m-unread by default', () => { +test(`Renders with class name ${styles.modifiers.unread} by default`, () => { render(Test); - expect(screen.getByText('Test')).toHaveClass('pf-m-unread'); + expect(screen.getByText('Test')).toHaveClass(styles.modifiers.unread); }); test('Renders with class name pf-m-read when isRead prop is true', () => { diff --git a/packages/react-core/src/components/Banner/__tests__/Banner.test.tsx b/packages/react-core/src/components/Banner/__tests__/Banner.test.tsx index 888b33c1287..e13ab61b4b3 100644 --- a/packages/react-core/src/components/Banner/__tests__/Banner.test.tsx +++ b/packages/react-core/src/components/Banner/__tests__/Banner.test.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { render, screen } from '@testing-library/react'; import { Banner } from '../Banner'; +import styles from '@patternfly/react-styles/css/components/Banner/banner'; test('Renders without children', () => { render( @@ -16,9 +17,9 @@ test('Renders children', () => { expect(screen.getByText('Test')).toBeVisible(); }); -test('Renders with class name pf-v5-c-banner', () => { +test(`Renders with class name ${styles.banner}`, () => { render(Test); - expect(screen.getByText('Test')).toHaveClass('pf-v5-c-banner'); + expect(screen.getByText('Test')).toHaveClass(styles.banner); }); test('Renders with custom class name when className prop is provided', () => { @@ -28,7 +29,7 @@ test('Renders with custom class name when className prop is provided', () => { test('Renders without any modifier class when variant prop is not passed', () => { render(Test); - expect(screen.getByText('Test')).toHaveClass('pf-v5-c-banner', { exact: true }); + expect(screen.getByText('Test')).toHaveClass(styles.banner, { exact: true }); }); test('Renders with class name pf-m-green when "green" is passed to variant prop', () => { diff --git a/packages/react-core/src/components/Brand/Brand.tsx b/packages/react-core/src/components/Brand/Brand.tsx index 03ed8a9bcea..4d68d61ba4c 100644 --- a/packages/react-core/src/components/Brand/Brand.tsx +++ b/packages/react-core/src/components/Brand/Brand.tsx @@ -2,6 +2,9 @@ import * as React from 'react'; import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/Brand/brand'; import { setBreakpointCssVars } from '../../helpers'; +import cssBrandHeight from '@patternfly/react-tokens/dist/esm/c_brand_Height'; +import cssBrandWidth from '@patternfly/react-tokens/dist/esm/c_brand_Width'; + export interface BrandProps extends React.DetailedHTMLProps, HTMLImageElement> { /** Transforms the Brand into a element from an element. Container for child elements. */ @@ -45,14 +48,14 @@ export const Brand: React.FunctionComponent = ({ let responsiveStyles; if (widths !== undefined) { responsiveStyles = { - ...setBreakpointCssVars(widths, '--pf-v5-c-brand--Width') + ...setBreakpointCssVars(widths, cssBrandWidth.name) }; } if (heights !== undefined) { responsiveStyles = { ...responsiveStyles, - ...setBreakpointCssVars(heights, '--pf-v5-c-brand--Height') + ...setBreakpointCssVars(heights, cssBrandHeight.name) }; } diff --git a/packages/react-core/src/components/Button/__tests__/Button.test.tsx b/packages/react-core/src/components/Button/__tests__/Button.test.tsx index 7e82d60481d..cbd758c1c83 100644 --- a/packages/react-core/src/components/Button/__tests__/Button.test.tsx +++ b/packages/react-core/src/components/Button/__tests__/Button.test.tsx @@ -5,145 +5,203 @@ import { render, screen } from '@testing-library/react'; import CartArrowDownIcon from '@patternfly/react-icons/dist/esm/icons/cart-arrow-down-icon'; import { Button, ButtonVariant } from '../Button'; -describe('Button', () => { - Object.values(ButtonVariant).forEach(variant => { - test(`${variant} button`, () => { - const { asFragment } = render( - - ); - expect(asFragment()).toMatchSnapshot(); +Object.values(ButtonVariant).forEach((variant) => { + if (variant !== 'primary') { + test(`Does not render with class pf-m-${variant} by default`, () => { + render(); + expect(screen.getByRole('button')).not.toHaveClass(`pf-m-${variant}`); }); + } + + test(`Renders with class pf-m-${variant} when variant=${variant}`, () => { + render(); + expect(screen.getByRole('button')).toHaveClass(`pf-m-${variant}`); }); +}); - test('it adds an aria-label to plain buttons', () => { - const label = 'aria-label test'; - render(
+ ); - expect(screen.getByLabelText(label)).toBeTruthy(); - }); + expect(screen.getByTestId('container').firstChild).toBeVisible(); +}); - test('link with icon', () => { - const { asFragment } = render( - - ); - expect(asFragment()).toMatchSnapshot(); - }); +test('Renders with class pf-v5-c-button by default', () => { + render(); + expect(screen.getByRole('button')).toHaveClass('pf-v5-c-button'); +}); - test('isBlock', () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); +test('Renders with class pf-m-primary by default', () => { + render(); + expect(screen.getByText('Button')).toHaveClass('pf-m-primary'); +}); - test('isDisabled', () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); +test('Renders with custom class', () => { + render(); + expect(screen.getByRole('button')).toHaveClass('custom-class'); +}); - test('isDanger secondary', () => { - const { asFragment } = render( - - ); - expect(asFragment()).toMatchSnapshot(); - }); +test('Renders with an aria-label', () => { + const label = 'aria-label test'; + render( - ); - expect(asFragment()).toMatchSnapshot(); - }); + expect(screen.getByLabelText(label)).toHaveAccessibleName('aria-label test'); +}); - test('isAriaDisabled button', () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); +test('Renders with class pf-m-block when isBlock = true', () => { + render(); + expect(screen.getByRole('button')).toHaveClass('pf-m-block'); +}); - test('isAriaDisabled link button', () => { - const { asFragment } = render( - - ); - expect(asFragment()).toMatchSnapshot(); - }); +test('Renders with class pf-m-active when isActive = true', () => { + render(); + expect(screen.getByRole('button')).toHaveClass('pf-m-active'); +}); - test('isInline', () => { - const { asFragment } = render( - - ); - expect(asFragment()).toMatchSnapshot(); - }); +test('Renders with class pf-m-disabled when isDisabled = true', () => { + render(); + expect(screen.getByRole('button')).toHaveClass('pf-m-disabled'); +}); - test('size small', () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); +test('Renders with class pf-m-aria-disabled when isAriaDisabled = true', () => { + render(); + expect(screen.getByRole('button')).toHaveClass('pf-m-aria-disabled'); +}); - test('size large', () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); +test('Does not disable button when isDisabled = true and component = a', () => { + render( + + ); + expect(screen.getByText('Disabled yet focusable button')).not.toHaveProperty('disabled'); +}); - test('isLoading', () => { - const { asFragment } = render( - - ); - expect(asFragment()).toMatchSnapshot(); - }); +test('Renders with class pf-m-danger when isDanger = true and variant = secondary', () => { + render( + + ); + expect(screen.getByRole('button')).toHaveClass('pf-m-danger', 'pf-m-secondary'); +}); - test('isLoading inline link', () => { - const { asFragment } = render( - - ); - expect(asFragment()).toMatchSnapshot(); - }); +test('Renders with class pf-m-danger when isDanger = true and variant = link', () => { + render( + + ); + expect(screen.getByRole('button')).toHaveClass('pf-m-danger', 'pf-m-link'); +}); - test('isLoading icon only', () => { - const { asFragment } = render( -
} /> - ); - expect(asFragment()).toMatchSnapshot(); - }); +test('Does not render with class pf-m-danger when isDanger = true and variant != secondary or link', () => { + render( + + ); + expect(screen.getByRole('button')).not.toHaveClass('pf-m-danger'); +}); - test('allows passing in a string as the component', () => { - const component = 'a'; - render(); +test('Does not render with class pf-m-danger when isDanger = true and variant = tertiary', () => { + render( + + ); + expect(screen.getByRole('button')).not.toHaveClass('pf-m-danger'); +}); - expect(screen.getByText('anchor button')).toBeInTheDocument(); - }); +test('Does not render with class pf-m-danger when isDanger = true and variant = control', () => { + render( + + ); + expect(screen.getByRole('button')).not.toHaveClass('pf-m-danger'); +}); - test('allows passing in a React Component as the component', () => { - const Component = () =>
im a div
; - render( + ); + expect(screen.getByRole('button')).toHaveClass('pf-m-inline'); +}); - expect(screen.getByText('im a div')).toBeInTheDocument(); - }); +test('Renders with class pf-m-small when size = sm', () => { + render(); + expect(screen.getByRole('button')).toHaveClass('pf-m-small'); +}); - test('aria-disabled is set to true and tabIndex to -1 if component is not a button and is disabled', () => { - const { asFragment } = render( - - ); - expect(asFragment()).toMatchSnapshot(); - }); +test('Renders with class pf-m-display-lg when size = lg', () => { + render(); + expect(screen.getByRole('button')).toHaveClass('pf-m-display-lg'); +}); - test('setting tab index through props', () => { - render(); - expect(screen.getByRole('button')).toHaveAttribute('tabindex', '0'); - }); +test('Renders with class pf-m-in-progress when isLoading = true', () => { + render( + + ); + expect(screen.getByRole('button')).toHaveClass('pf-m-in-progress'); +}); + +test('Renders with class pf-m-progress when isLoading is defined and isLoading = false', () => { + render( + + ); + expect(screen.getByRole('button')).toHaveClass('pf-m-progress'); +}); + +test('Renders without class pf-m-progress when isLoading = false and variant = plain', () => { + render( + + ); + expect(screen.getByRole('button')).not.toHaveClass('pf-m-progress'); +}); + +test('Renders custom icon with class pf-m-in-progress when isLoading = true and icon is present', () => { + render( +
} /> + ); + + expect(screen.getByText('ICON')).toBeVisible(); + expect(screen.getByText('ICON').parentElement).toHaveClass('pf-m-in-progress'); +}); + +test('Renders as custom component when component is passed', () => { + const component = 'a'; + render(); + + expect(screen.getByText('anchor button').tagName).toBe('A'); +}); + +test('aria-disabled is set to true and tabIndex to -1 if component is not a button and is disabled', () => { + render( + + ); + expect(screen.getByText('Disabled Anchor Button')).toHaveAttribute('tabindex', '-1'); +}); + +test('setting tab index through props', () => { + render(); + expect(screen.getByRole('button')).toHaveAttribute('tabindex', '0'); +}); + +test(`Renders basic button`, () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); }); diff --git a/packages/react-core/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap b/packages/react-core/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap index 6b5396f1bed..b0cb22e0620 100644 --- a/packages/react-core/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap +++ b/packages/react-core/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap @@ -1,417 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Button aria-disabled is set to true and tabIndex to -1 if component is not a button and is disabled 1`] = ` - - - Disabled Anchor Button - - -`; - -exports[`Button control button 1`] = ` - - - -`; - -exports[`Button danger button 1`] = ` - - - -`; - -exports[`Button isAriaDisabled button 1`] = ` - - - -`; - -exports[`Button isAriaDisabled link button 1`] = ` - - - Disabled yet focusable button - - -`; - -exports[`Button isBlock 1`] = ` - - - -`; - -exports[`Button isDanger link 1`] = ` - - - -`; - -exports[`Button isDanger secondary 1`] = ` - - - -`; - -exports[`Button isDisabled 1`] = ` - - - -`; - -exports[`Button isInline 1`] = ` - - - -`; - -exports[`Button isLoading 1`] = ` - - - -`; - -exports[`Button isLoading icon only 1`] = ` +exports[`Renders basic button 1`] = ` - -`; - -exports[`Button isLoading inline link 1`] = ` - - - -`; - -exports[`Button link button 1`] = ` - - - -`; - -exports[`Button link with icon 1`] = ` - - - -`; - -exports[`Button plain button 1`] = ` - - - -`; - -exports[`Button primary button 1`] = ` - - - -`; - -exports[`Button secondary button 1`] = ` - - - -`; - -exports[`Button size large 1`] = ` - - - -`; - -exports[`Button size small 1`] = ` - - - -`; - -exports[`Button tertiary button 1`] = ` - - - -`; - -exports[`Button warning button 1`] = ` - - `; diff --git a/packages/react-core/src/components/Button/examples/Button.md b/packages/react-core/src/components/Button/examples/Button.md index d426105ab29..dc280242e1a 100644 --- a/packages/react-core/src/components/Button/examples/Button.md +++ b/packages/react-core/src/components/Button/examples/Button.md @@ -32,7 +32,7 @@ PatternFly supports several button styling variants to be used in different scen | Warning | Warning buttons may be used for actions that change an important setting or behavior, but not in a destructive or irreversible way. | | Link | Links are labeled, but have no background or border. Use for actions that require less emphasis, actions that navigate users to another page within the same window, and/or actions that navigate to external pages in a new window. Links may be placed inline with text using the `isInline` property.| | Plain | Plain buttons have no styling and are intended to be labeled with icons. | -| Control | Control buttons can be labeled with text or icons. Primarily intended to be paired with other controls in an [input group](https://www.patternfly.org/v4/components/input-group). | +| Control | Control buttons can be labeled with text or icons. Primarily intended to be paired with other controls in an [input group](/components/input-group). | ### Disabled buttons diff --git a/packages/react-core/src/components/CalendarMonth/CalendarMonth.tsx b/packages/react-core/src/components/CalendarMonth/CalendarMonth.tsx index de4cb47d7f4..583824c3a37 100644 --- a/packages/react-core/src/components/CalendarMonth/CalendarMonth.tsx +++ b/packages/react-core/src/components/CalendarMonth/CalendarMonth.tsx @@ -154,12 +154,13 @@ export const CalendarMonth = ({ const [isSelectOpen, setIsSelectOpen] = React.useState(false); const getInitialDate = () => { - const initDate = new Date(dateProp); - if (isValidDate(initDate)) { - return initDate; - } else { - return isValidDate(rangeStart) ? rangeStart : today; + if (isValidDate(dateProp)) { + return dateProp; } + if (isValidDate(rangeStart)) { + return rangeStart; + } + return today; }; const initialDate = getInitialDate(); const [focusedDate, setFocusedDate] = React.useState(initialDate); @@ -211,43 +212,25 @@ export const CalendarMonth = ({ } }; - const changeMonth = (month: number) => { - const newDate = new Date(focusedDate); - const desiredDay = newDate.getDate(); - const monthDays = new Date(newDate.getFullYear(), (month + 1) % 12, 0).getDate(); // Setting day 0 of the next month returns the last day of current month - - if (monthDays < desiredDay) { - newDate.setDate(monthDays); - } - - newDate.setMonth(month); - - if (initialDate.getDate() > desiredDay && monthDays > desiredDay) { - newDate.setDate(initialDate.getDate()); - } + const changeYear = (newYear: number) => changeMonth(focusedDate.getMonth(), newYear); - return newDate; - }; + const changeMonth = (newMonth: number, newYear?: number) => + new Date(newYear ?? focusedDate.getFullYear(), newMonth, 1); const addMonth = (toAdd: -1 | 1) => { - let newMonth = new Date(focusedDate).getMonth() + toAdd; + let newMonth = focusedDate.getMonth() + toAdd; + let newYear = focusedDate.getFullYear(); + if (newMonth === -1) { newMonth = 11; + newYear--; } else if (newMonth === 12) { newMonth = 0; + newYear++; } - const newDate = changeMonth(newMonth); - if (toAdd === 1 && newMonth === 0) { - newDate.setFullYear(newDate.getFullYear() + 1); - } - if (toAdd === -1 && newMonth === 11) { - newDate.setFullYear(newDate.getFullYear() - 1); - } - return newDate; - }; - const yearHasFebruary29th = (year: number) => new Date(year, 1, 29).getMonth() === 1; - const dateIsFebruary29th = (date: Date) => date.getMonth() === 1 && date.getDate() === 29; + return changeMonth(newMonth, newYear); + }; const prevMonth = addMonth(-1); const nextMonth = addMonth(1); @@ -340,19 +323,11 @@ export const CalendarMonth = ({ type="number" value={yearFormatted} onChange={(ev: React.FormEvent, year: string) => { - const newDate = new Date(focusedDate); - if (dateIsFebruary29th(newDate) && !yearHasFebruary29th(+year)) { - newDate.setDate(28); - newDate.setMonth(1); - } - if (dateIsFebruary29th(initialDate) && yearHasFebruary29th(+year)) { - newDate.setFullYear(+year); - newDate.setDate(29); - } - newDate.setFullYear(+year); + const newDate = changeYear(Number(year)); setFocusedDate(newDate); setHoveredDate(newDate); setShouldFocus(false); + focusRef.current?.blur(); // will unfocus a date when changing year via up/down arrows onMonthChange(ev, newDate); }} /> diff --git a/packages/react-core/src/components/Checkbox/__tests__/Checkbox.test.tsx b/packages/react-core/src/components/Checkbox/__tests__/Checkbox.test.tsx index eb1c40ba3a6..38fdb71b497 100644 --- a/packages/react-core/src/components/Checkbox/__tests__/Checkbox.test.tsx +++ b/packages/react-core/src/components/Checkbox/__tests__/Checkbox.test.tsx @@ -4,92 +4,247 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Checkbox } from '../Checkbox'; +import styles from '@patternfly/react-styles/css/components/Check/check'; -describe('Checkbox', () => { - test('controlled', () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); +test(`Renders with only the class ${styles.checkInput} on the check by default`, () => { + render(); - test('controlled - 3rd state', () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); + expect(screen.getByRole('checkbox')).toHaveClass(styles.checkInput, { exact: true }); +}); - test('uncontrolled', () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); +test(`Renders with only the classes ${styles.check} and ${styles.modifiers.standalone} on the check wrapper by default`, () => { + render(); - test('isDisabled', () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); + expect(screen.getByRole('checkbox').parentElement).toHaveClass(`${styles.check} ${styles.modifiers.standalone}`, { + exact: true }); +}); - test('label is string', () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); +test('Renders with additional classes passed via className', () => { + render(); - test('label is function', () => { - const functionLabel = () =>

Header

; - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); + expect(screen.getByRole('checkbox').parentElement).toHaveClass('test-class'); +}); - test('label is node', () => { - const { asFragment } = render(Header} id="check" isChecked aria-label="check" />); - expect(asFragment()).toMatchSnapshot(); - }); +test('Renders with additional classes passed via inputClassName', () => { + render(); - test('passing class', () => { - const { asFragment } = render( - - ); - expect(asFragment()).toMatchSnapshot(); - }); + expect(screen.getByRole('checkbox')).toHaveClass('test-class'); +}); - test('passing HTML attribute', () => { - const { asFragment } = render( - - ); - expect(asFragment()).toMatchSnapshot(); - }); +test('Does not set the checkbox as invalid by default', () => { + render(); - test('passing description', () => { - render(); - expect(screen.getByText('Text description...')).toBeInTheDocument(); - }); + expect(screen.getByRole('checkbox')).toBeValid(); +}); - test('passing body', () => { - render(); +test('Sets the checkbox as invalid when isValid is false', () => { + render(); - expect(screen.getByText('This is where custom content goes.')).toBeInTheDocument(); - }); + expect(screen.getByRole('checkbox')).toBeInvalid(); +}); - test('checkbox onChange handler called when component is clicked', async () => { - const onChangeHandler = jest.fn(); - const user = userEvent.setup(); +test('Does not set the checkbox as disabled by default', () => { + render(); - render(); + expect(screen.getByRole('checkbox')).not.toBeDisabled(); +}); - await user.click(screen.getByLabelText('check')); - expect(onChangeHandler).toHaveBeenCalled(); - }); +test(`Sets the checkbox as disabled when isDisabled is passed`, () => { + render(); - test('should throw console error when no id is given', () => { - const myMock = jest.fn(); - global.console = { ...global.console, error: myMock }; + expect(screen.getByRole('checkbox')).toBeDisabled(); +}); - render(); - expect(myMock).toHaveBeenCalled(); - }); +test('Sets the label as disabled when isDisabled and label are passed', () => { + render(); - test('renders component wrapper as span', () => { - const { container } = render( - - ); - const span = container.querySelector('span'); - expect(span).toHaveClass('pf-v5-c-check'); - }); + expect(screen.getByLabelText('test label')).toBeDisabled(); +}); + +test('Does not set the checkbox as required by default', () => { + render(); + + expect(screen.getByRole('checkbox')).not.toBeRequired(); +}); + +test(`Sets the checkbox as required when isRequired is passed`, () => { + render(); + + expect(screen.getByRole('checkbox')).toBeRequired(); +}); + +test('Does not set the checkbox as checked by default', () => { + render(); + + expect(screen.getByRole('checkbox')).not.toBeChecked(); +}); + +test(`Sets the checkbox as checked when isChecked is passed`, () => { + render(); + + expect(screen.getByRole('checkbox')).toBeChecked(); +}); + +test(`Sets the checkbox as checked when checked is passed`, () => { + render(); + + expect(screen.getByRole('checkbox')).toBeChecked(); +}); + +test(`Calls onChange when the checkbox is clicked`, async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + render(); + + await user.click(screen.getByRole('checkbox')); + + expect(onChange).toHaveBeenCalledTimes(1); +}); + +test('Does not call onChange when the checkbox is not clicked', () => { + const onChange = jest.fn(); + render(); + + expect(onChange).not.toHaveBeenCalled(); +}); + +test(`Calls onChange with the event and the checked value when the checkbox is clicked`, async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + render(); + + await user.click(screen.getByRole('checkbox')); + + expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ type: 'change' }), true); +}); + +test(`Calls onChange with the event and the checked value when the checkbox is clicked and isChecked is passed`, async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + render(); + + await user.click(screen.getByRole('checkbox')); + + expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ type: 'change' }), false); +}); + +test('Does not render a label by default', () => { + render(); + + expect(screen.queryByLabelText(/\w+/)).not.toBeInTheDocument(); +}); + +test('Renders a label when label is passed', () => { + render(); + + expect(screen.getByLabelText('test label')).toBeVisible(); +}); + +test('Associates the label with the checkbox', () => { + render(); + + expect(screen.getByRole('checkbox')).toHaveAccessibleName('test label'); +}); + +test('Does not render an asterisk when a label is passed but isRequired is not', () => { + render(); + + expect(screen.queryByText('*')).not.toBeInTheDocument(); +}); + +test('Renders an asterisk when isRequired and a label are passed', () => { + render(); + + expect(screen.getByText('*')).toBeVisible(); +}); + +test(`Wraps the required asterisk in the ${styles.checkLabelRequired} className`, () => { + render(); + + expect(screen.getByText('*')).toHaveClass(styles.checkLabelRequired, { exact: true }); +}); + +test('Renders with the provided id', () => { + render(); + + expect(screen.getByRole('checkbox')).toHaveAttribute('id', 'test-id'); +}); + +test('Does not render an aria-label by default', () => { + render(); + + expect(screen.getByRole('checkbox')).not.toHaveAttribute('aria-label'); +}); + +test('Sets the name to the passed aria-label', () => { + render(); + + expect(screen.getByRole('checkbox')).toHaveAccessibleName('test aria-label'); +}); + +test('Does not render a description by default', () => { + render(); + + expect(screen.queryByText(/\w+/)).not.toBeInTheDocument(); +}); + +test('Renders a description when description is passed', () => { + render(); + + expect(screen.getByText('test description')).toBeVisible(); +}); + +test(`Renders the passed description with the ${styles.checkDescription} className`, () => { + render(); + + expect(screen.getByText('test description')).toHaveClass(styles.checkDescription, { exact: true }); +}); + +test('Does not render a body by default', () => { + render(); + + expect(screen.queryByText(/\w+/)).not.toBeInTheDocument(); +}); + +test('Renders a body when body is passed', () => { + render(); + + expect(screen.getByText('test body')).toBeVisible(); +}); + +test(`Renders the passed body with the ${styles.checkBody} className`, () => { + render(); + + expect(screen.getByText('test body')).toHaveClass(styles.checkBody, { exact: true }); +}); + +test('Renders the check wrapper as a div by default', () => { + render(); + + expect(screen.getByRole('checkbox').parentElement?.tagName).toBe('DIV'); +}); + +test('Renders with the provided component', () => { + render(); + + expect(screen.getByRole('checkbox').parentElement?.tagName).toBe('SPAN'); +}); + +test(`Spreads additional props`, () => { + render(); + + expect(screen.getByTestId('test-id')).toBeInTheDocument(); +}); + +test(`Sets the checkbox as checked by default when defaultChecked is passed`, () => { + render(); + + expect(screen.getByRole('checkbox')).toBeChecked(); +}); + +test('Matches snapshot', () => { + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); }); diff --git a/packages/react-core/src/components/Checkbox/__tests__/__snapshots__/Checkbox.test.tsx.snap b/packages/react-core/src/components/Checkbox/__tests__/__snapshots__/Checkbox.test.tsx.snap index 5521f2f3be4..12be1114f46 100644 --- a/packages/react-core/src/components/Checkbox/__tests__/__snapshots__/Checkbox.test.tsx.snap +++ b/packages/react-core/src/components/Checkbox/__tests__/__snapshots__/Checkbox.test.tsx.snap @@ -1,212 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Checkbox controlled - 3rd state 1`] = ` +exports[`Matches snapshot 1`] = `
-
-
-`; - -exports[`Checkbox controlled 1`] = ` - -
- -
-
-`; - -exports[`Checkbox isDisabled 1`] = ` - -
- -
-
-`; - -exports[`Checkbox label is function 1`] = ` - -
- - -
-
-`; - -exports[`Checkbox label is node 1`] = ` - -
- - -
-
-`; - -exports[`Checkbox label is string 1`] = ` - -
- - -
-
-`; - -exports[`Checkbox passing HTML attribute 1`] = ` - -
- - -
-
-`; - -exports[`Checkbox passing class 1`] = ` - -
- - -
-
-`; - -exports[`Checkbox uncontrolled 1`] = ` - -
-
diff --git a/packages/react-core/src/components/Chip/Chip.tsx b/packages/react-core/src/components/Chip/Chip.tsx index af7b7369f9d..52c6c62d518 100644 --- a/packages/react-core/src/components/Chip/Chip.tsx +++ b/packages/react-core/src/components/Chip/Chip.tsx @@ -6,6 +6,7 @@ import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; import styles from '@patternfly/react-styles/css/components/Chip/chip'; import { GenerateId } from '../../helpers/GenerateId/GenerateId'; import { getOUIAProps, OUIAProps, getDefaultOUIAId } from '../../helpers'; +import cssChipTextMaxWidth from '@patternfly/react-tokens/dist/esm/c_chip__text_MaxWidth'; export interface ChipProps extends React.HTMLProps, OUIAProps { /** Badge to add to the chip. The badge will be rendered after the chip text. */ @@ -81,7 +82,7 @@ class Chip extends React.Component { } setChipStyle = () => ({ - '--pf-v5-c-chip__text--MaxWidth': this.props.textMaxWidth + [cssChipTextMaxWidth.name]: this.props.textMaxWidth }); renderOverflowChip = () => { @@ -139,12 +140,13 @@ class Chip extends React.Component { tooltipPosition, component, ouiaId, + textMaxWidth, ...props } = this.props; const Component = component as any; return ( { - test('overflow', () => { - const { asFragment } = render( - - 4 more +jest.mock('../../Tooltip', () => ({ + Tooltip: ({ content, position }) => ( +
+

{`content: ${content}`}

+

{`position: ${position}`}

+
+ ) +})); + +['default chip', 'overflow chip'].forEach((chipType) => { + const isOverflowChip = chipType === 'overflow chip'; + + test(`Renders without children for ${chipType}`, () => { + render( +
+ +
+ ); + + expect(screen.getByTestId('container').firstChild).toBeVisible(); + }); + + test(`Renders with class ${styles.chip} on the ${chipType} container element`, () => { + render( + + Chip text + + ); + + // Only a non-overflow chip will have exactly the class "pf-v5-c-chip", we test for + // additional classes on overflow chips elsewhere in the suite + expect(screen.getByTestId('container')).toHaveClass(styles.chip, { exact: !isOverflowChip }); + }); + + test(`Renders with class ${styles.chipContent} around the ${chipType} content`, () => { + render( + + Chip text ); - expect(asFragment()).toMatchSnapshot(); + + expect(screen.getByTestId('container').firstChild).toHaveClass(styles.chipContent, { exact: true }); + }); + + test(`Renders ${chipType} children with class ${styles.chipText}`, () => { + render(Chip text); + + expect(screen.getByText('Chip text')).toHaveClass(styles.chipText, { exact: true }); }); - test('closable', () => { - const { asFragment } = render( - - Chip + test(`Renders with custom class on the ${chipType} container element`, () => { + render( + + Chip text ); - expect(asFragment()).toMatchSnapshot(); + + expect(screen.getByTestId('container')).toHaveClass('custom-class'); }); - test('closable with tooltip', () => { - const { asFragment } = render( - - 1234567890123456789 + test(`Renders a badge when badge prop is passed to ${chipType}`, () => { + render( + Badge content
}> + Chip text ); - expect(asFragment()).toMatchSnapshot(); + + expect(screen.getByText('Badge content')).toBeVisible(); + }); + + test(`Renders with div container on ${chipType} by default`, () => { + render( + + Chip text + + ); + + expect(screen.getByTestId('container').tagName).toBe('DIV'); }); - test('readonly', () => { - const { asFragment } = render( - - 4 more + test(`Renders with custom container on ${chipType} when component prop is passed`, () => { + render( + + Chip text ); - expect(asFragment()).toMatchSnapshot(); + + expect(screen.getByRole('listitem')).toBeVisible(); }); - test('custom max-width text', () => { - const { asFragment } = render( - - 4 more + test(`Renders maxWidth css var in style attribute when textMaxWidth is passed for ${chipType}`, () => { + render( + + Chip text ); - expect(asFragment()).toMatchSnapshot(); + + expect(screen.getByTestId('container')).toHaveAttribute('style', '--pf-v5-c-chip__text--MaxWidth: 10px;'); }); - test("with role='gridcell'", () => { - const { asFragment } = render( - - I'm a roled chip + test(`Spreads additional props to container for ${chipType}`, () => { + render( + + Chip text ); - expect(asFragment()).toMatchSnapshot(); + + expect(screen.getByTestId('container')).toHaveAttribute('role', 'button'); }); }); + +test(`Renders id prop on ${styles.chipText} container for default chip`, () => { + render(Chip text); + + expect(screen.getByText('Chip text')).toHaveAttribute('id', 'custom-id'); +}); + +test(`Does not render id prop on ${styles.chipText} container for overflow chip`, () => { + render( + + Chip text + + ); + + expect(screen.getByText('Chip text')).not.toHaveAttribute('id'); +}); + +test(`Renders actions container with class ${styles.chipActions} when isReadOnly is false`, () => { + render(Chip text); + + expect(screen.getByRole('button').parentElement).toHaveClass(styles.chipActions); +}); + +test(`Renders aria-labelledby on action close button by default`, () => { + render(Chip text); + + expect(screen.getByRole('button')).toHaveAccessibleName('close Chip text'); +}); + +test(`Renders aria-labelledby on action close button with custom id passed`, () => { + render(Chip text); + + expect(screen.getByRole('button')).toHaveAccessibleName('close Chip text'); +}); + +test(`Renders concatenated aria-label on action close button by default`, () => { + render(Chip text); + + expect(screen.getByRole('button')).toHaveAccessibleName('close Chip text'); +}); + +test(`Renders custom aria-label on action close button when closeBtnAriaLabel is passed`, () => { + render(Chip text); + + expect(screen.getByRole('button')).toHaveAccessibleName('custom label Chip text'); +}); + +test(`Does not render close button action when isOverflowChip is true`, () => { + render(Chip text); + + // Because overflow chip renders as a button, we need to add the accessible name to the query + expect(screen.queryByRole('button', { name: 'close Chip text' })).not.toBeInTheDocument(); +}); + +test(`Does not render close button action when isReadOnly is true`, () => { + render(Chip text); + + expect(screen.queryByRole('button')).not.toBeInTheDocument(); +}); + +test(`Does not render with class ${styles.modifiers.overflow} when isOverflowChip is not passed`, () => { + render(Chip text); + + expect(screen.getByTestId('container')).not.toHaveClass(styles.modifiers.overflow); +}); + +test(`Renders with class ${styles.modifiers.overflow} when isOverflowChip is true`, () => { + render( + + Chip text + + ); + + expect(screen.getByTestId('container')).toHaveClass(styles.modifiers.overflow); +}); + +test(`Calls onClick when close button action is clicked for default chip`, async () => { + const user = userEvent.setup(); + const onClickMock = jest.fn(); + + render(Chip text); + + await user.click(screen.getByRole('button', { name: 'close Chip text' })); + + expect(onClickMock).toHaveBeenCalledTimes(1); +}); + +test(`Does not call onClick when close button action is not clicked for default chip`, async () => { + const user = userEvent.setup(); + const onClickMock = jest.fn(); + + render( + <> + Chip text + + + ); + + await user.click(screen.getByRole('button', { name: 'Test clicker' })); + + expect(onClickMock).not.toHaveBeenCalled(); +}); + +test(`Calls onClick when chip is clicked for overflow chip`, async () => { + const user = userEvent.setup(); + const onClickMock = jest.fn(); + + render( + + Chip text + + ); + + await user.click(screen.getByRole('button', { name: 'Chip text' })); + + expect(onClickMock).toHaveBeenCalledTimes(1); +}); + +test(`Does not call onClick when chip is not clicked for overflow chip`, async () => { + const user = userEvent.setup(); + const onClickMock = jest.fn(); + + render( + <> + + Chip text + + + + ); + + await user.click(screen.getByRole('button', { name: 'Test clicker' })); + + expect(onClickMock).not.toHaveBeenCalled(); +}); + +test('Passes position to Tooltip', () => { + Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { configurable: true, value: 500 }); + render(Test chip text); + + expect(screen.getByText('position: bottom')).toBeVisible(); + Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { configurable: true, value: 0 }); +}); + +test('Passes content to Tooltip', () => { + Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { configurable: true, value: 500 }); + render(Test chip text); + + expect(screen.getByText(`content: Test chip text`)).toBeVisible(); + Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { configurable: true, value: 0 }); +}); + +test('Matches snapshot', () => { + const { asFragment } = render( + + Chip text + + ); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/components/Chip/__tests__/ChipGroup.test.tsx b/packages/react-core/src/components/Chip/__tests__/ChipGroup.test.tsx index 69055dcc3ef..807944710f1 100644 --- a/packages/react-core/src/components/Chip/__tests__/ChipGroup.test.tsx +++ b/packages/react-core/src/components/Chip/__tests__/ChipGroup.test.tsx @@ -5,66 +5,293 @@ import userEvent from '@testing-library/user-event'; import { ChipGroup } from '../index'; import { Chip } from '../../Chip'; +import styles from '@patternfly/react-styles/css/components/Chip/chip-group'; -describe('ChipGroup', () => { - test('chip group default', () => { - const { asFragment } = render( - - 1.1 - - ); - - expect(asFragment()).toMatchSnapshot(); - }); - - test('chip group with category', () => { - const { asFragment } = render( - - 1.1 - - ); - expect(asFragment()).toMatchSnapshot(); - }); - - test('chip group with closable category', () => { - const { asFragment } = render( - - 1.1 - - ); - expect(asFragment()).toMatchSnapshot(); - }); - - test('chip group expanded', async () => { - const user = userEvent.setup(); - - render( - - 1 - 2 - 3 - 4 - - ); - - const moreText = screen.getByText('1 more'); - expect(moreText).toBeInTheDocument(); - - await user.click(moreText); - expect(screen.getByText('Show Less')).toBeInTheDocument(); - }); - - test('chip group will not render if no children passed', () => { - render(); - expect(screen.queryByRole('group')).toBeNull(); - }); - - test('chip group with category and tooltip', () => { - const { asFragment } = render( - - 1.1 - - ); - expect(asFragment()).toMatchSnapshot(); - }); +test('chip group default', () => { + render( + + 1.1 + + ); + + expect(screen.getByRole('group')).toHaveClass(styles.chipGroup); +}); + +test('chip group with custom className', () => { + render( + + 1.1 + + ); + + expect(screen.getByRole('group')).toHaveClass(`${styles.chipGroup} test`); +}); + +test('chip group has aria-label by default when categoryName is not passed', () => { + render( + + 1.1 + + ); + + expect(screen.getByRole('group')).toHaveAccessibleName('Chip group category'); +}); + +test('chip group with custom aria-label', () => { + render( + + 1.1 + + ); + + expect(screen.getByRole('group')).toHaveAccessibleName('test chip group'); +}); + +test('chip group with category', () => { + render( + + 1.1 + + ); + expect(screen.getByText('category')).toBeVisible(); +}); + +test('chip group with category renders with class pf-m-category', () => { + render( + + 1.1 + + ); + expect(screen.getByRole('group')).toHaveClass(`${styles.modifiers.category}`); +}); + +test('chip group has aria-labelledby attribute', () => { + render( + + 1.1 + + ); + expect(screen.getByRole('group')).toHaveAttribute('aria-labelledby', expect.stringContaining(`pf-random-id`)); +}); + +test('chip group has aria-labelledby attribute set to ID value', () => { + render( + + 1.1 + + ); + expect(screen.getByRole('group')).toHaveAttribute('aria-labelledby', 'chip-group-id'); +}); + +test('chip group expands', async () => { + const user = userEvent.setup(); + const chips = ['Chip one', 'Really long chip that goes on and on', 'Chip three', 'Chip four', 'Chip five']; + + render( + + {chips.map((currentChip) => ( + {currentChip} + ))} + + ); + + await user.click(screen.getByText('2 more')); + expect(screen.getByText('Chip five')).toBeVisible(); +}); + +test('chip group with closable category', () => { + render( + + 1.1 + + ); + expect(screen.getByRole('group').lastChild).toHaveClass(styles.chipGroupClose); +}); + +test('chip group with closeBtnAriaLabel', () => { + render( + + 1.1 + + ); + expect(screen.getByLabelText('close button aria label')).toBeInTheDocument(); + expect(screen.getByLabelText('close button aria label')).toHaveAccessibleName("close button aria label category"); +}); + +test('chip group onClick', async () => { + const onClose = jest.fn(); + const user = userEvent.setup(); + + render( + + 1.1 + + ); + + await user.click(screen.getByLabelText('Close chip group')); + expect(onClose).toHaveBeenCalledTimes(1); +}); + +test('chip group onOverflowChipClick', async () => { + const onOverflowChipClick = jest.fn(); + const user = userEvent.setup(); + + render( + + 1 + 2 + 3 + 4 + + ); + + await user.click(screen.getByText('1 more')); + expect(onOverflowChipClick).toHaveBeenCalledTimes(1); +}); + +test('chip group expanded', async () => { + const user = userEvent.setup(); + + render( + + 1 + 2 + 3 + 4 + + ); + + const moreText = screen.getByText('1 more'); + expect(moreText).toBeInTheDocument(); + + await user.click(moreText); + expect(screen.getByText('Show Less')).toBeInTheDocument(); +}); + +test('overflow chip does not render by default when < 4 children are passed', async () => { + const user = userEvent.setup(); + + render( + + 1 + 2 + 3 + + ); + + expect(screen.queryByText('1 more')).not.toBeInTheDocument(); +}); + +test('overflow chip collapsed by default', async () => { + const user = userEvent.setup(); + + render( + + 1 + 2 + 3 + 4 + + ); + + const moreText = screen.getByText('1 more'); + expect(moreText).toBeInTheDocument(); +}); + +test('overflow chip renders with the default numChips prop value of 3', async () => { + const user = userEvent.setup(); + + render( + + 1 + 2 + 3 + 4 + + ); + + const moreText = screen.getByText('1 more'); + expect(moreText).toBeInTheDocument(); + + await user.click(moreText); + expect(screen.getByText('Show Less')).toBeInTheDocument(); +}); + +test('overflow chip renders with numChips prop value of 2', async () => { + const user = userEvent.setup(); + + render( + + 1 + 2 + 3 + + ); + + const moreText = screen.getByText('1 more'); + expect(moreText).toBeInTheDocument(); + + await user.click(moreText); + expect(screen.getByText('Show Less')).toBeInTheDocument(); +}); + +test('clicking the overflow chip causes the text to update with the default props', async () => { + const user = userEvent.setup(); + + render( + + 1 + 2 + 3 + 4 + + ); + + const moreText = screen.getByText('1 more'); + + await user.click(moreText); + expect(screen.getByText('Show Less')).toBeInTheDocument(); +}); + +test('passing in the expandedText and collapsedText props work', async () => { + const user = userEvent.setup(); + + render( + + 1 + 2 + 3 + 4 + + ); + + const moreText = screen.getByText('Expand'); + expect(moreText).toBeInTheDocument(); + + await user.click(moreText); + expect(screen.getByText('Collapse')).toBeInTheDocument(); +}); + +test('passing defaultIsOpen of true causes the chip group to be expanded by default', async () => { + render( + + 1 + 2 + 3 + 4 + + ); + + expect(screen.getByText('Show Less')).toBeInTheDocument(); +}); + +test('chip group will not render if no children passed', () => { + render(); + expect(screen.queryByRole('group')).toBeNull(); +}); + +test('chip group renders to match snapshot', () => { + const { asFragment } = render(chips); + expect(screen.getByRole('group')).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); }); diff --git a/packages/react-core/src/components/Chip/__tests__/__snapshots__/Chip.test.tsx.snap b/packages/react-core/src/components/Chip/__tests__/__snapshots__/Chip.test.tsx.snap index 00b63d8b8fd..2216d2e45cc 100644 --- a/packages/react-core/src/components/Chip/__tests__/__snapshots__/Chip.test.tsx.snap +++ b/packages/react-core/src/components/Chip/__tests__/__snapshots__/Chip.test.tsx.snap @@ -1,22 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Chip closable 1`] = ` +exports[`Matches snapshot 1`] = `
- Chip + Chip text - - - -
-
-`; - -exports[`Chip closable with tooltip 1`] = ` - -
- - - 1234567890123456789 - - - - - -
-
-`; - -exports[`Chip custom max-width text 1`] = ` - -
- - - 4 more - - - - - -
-
-`; - -exports[`Chip overflow 1`] = ` - -
- - - 4 more - - -
-
-`; - -exports[`Chip readonly 1`] = ` - -
- - - 4 more - - -
-
-`; - -exports[`Chip with role='gridcell' 1`] = ` - -
- - - I'm a roled chip - - - - - -
+ chips
`; - -exports[`ChipGroup chip group with category 1`] = ` - -
-
- - category - -
    -
  • -
    - - - 1.1 - - - - - -
    -
  • -
-
-
-
-`; - -exports[`ChipGroup chip group with category and tooltip 1`] = ` - -
-
- - A very long category name - -
    -
  • -
    - - - 1.1 - - - - - -
    -
  • -
-
-
-
-`; - -exports[`ChipGroup chip group with closable category 1`] = ` - -
-
- - category - -
    -
  • -
    - - - 1.1 - - - - - -
    -
  • -
-
-
- -
-
-
-`; diff --git a/packages/react-core/src/components/ClipboardCopy/ClipboardCopy.tsx b/packages/react-core/src/components/ClipboardCopy/ClipboardCopy.tsx index bb70d7af704..b4338b36ce7 100644 --- a/packages/react-core/src/components/ClipboardCopy/ClipboardCopy.tsx +++ b/packages/react-core/src/components/ClipboardCopy/ClipboardCopy.tsx @@ -75,7 +75,7 @@ export interface ClipboardCopyProps extends Omit onChange?: (event: React.FormEvent, text?: string | number) => void; /** The text which is copied. */ children: React.ReactNode; - /** Additional actions for inline clipboard copy. Should be wrapped with ClipboardCopyAction. */ + /** Additional actions for inline-compact clipboard copy. Should be wrapped with ClipboardCopyAction. */ additionalActions?: React.ReactNode; /** Value to overwrite the randomly generated data-ouia-component-id.*/ ouiaId?: number | string; diff --git a/packages/react-core/src/components/ClipboardCopy/ClipboardCopyButton.tsx b/packages/react-core/src/components/ClipboardCopy/ClipboardCopyButton.tsx index e0add211d15..ad55776ca61 100644 --- a/packages/react-core/src/components/ClipboardCopy/ClipboardCopyButton.tsx +++ b/packages/react-core/src/components/ClipboardCopy/ClipboardCopyButton.tsx @@ -57,6 +57,7 @@ export const ClipboardCopyButton: React.FunctionComponent {}, + className, ...props }: ClipboardCopyButtonProps) => { const triggerRef = React.createRef(); @@ -79,6 +80,7 @@ export const ClipboardCopyButton: React.FunctionComponent diff --git a/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopy.test.tsx b/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopy.test.tsx new file mode 100644 index 00000000000..4d2b38cb5a6 --- /dev/null +++ b/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopy.test.tsx @@ -0,0 +1,344 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import { ClipboardCopy } from '../ClipboardCopy'; +import styles from '@patternfly/react-styles/css/components/ClipboardCopy/clipboard-copy'; +import userEvent from '@testing-library/user-event'; + +jest.mock('../ClipboardCopyButton', () => ({ + ClipboardCopyButton: ({ 'aria-label': ariaLabel, children, entryDelay, exitDelay, maxWidth, position, onClick }) => ( +
+

{`exitDelay: ${exitDelay}`}

+

{`entryDelay: ${entryDelay}`}

+

{`maxWidth: ${maxWidth}`}

+

{`position: ${position}`}

+

{`button-ariaLabel: ${ariaLabel}`}

+
{`children: ${children}`}
+ +
+ ) +})); + +jest.mock('../ClipboardCopyToggle', () => ({ + ClipboardCopyToggle: ({ 'aria-label': ariaLabel }) => ( +
+

{`toggle-ariaLabel: ${ariaLabel}`}

+
+ ) +})); + +const children = 'Copyable text'; +const testId = 'clipboard-copy'; + +test(`Renders with class ${styles.clipboardCopy} by default`, () => { + render({children}); + + expect(screen.getByTestId(testId)).toHaveClass(styles.clipboardCopy, { exact: true }); +}); + +test(`Renders with custom class when className is passed`, () => { + render( + + {children} + + ); + + expect(screen.getByTestId(testId)).toHaveClass('test-class'); +}); + +test(`Does not render with class ${styles.modifiers.inline} by default`, () => { + render({children}); + + expect(screen.getByTestId(testId)).not.toHaveClass(styles.modifiers.inline); +}); + +test(`Does not render with class ${styles.modifiers.inline} if variant is not inline-compact`, () => { + render( + + {children} + + ); + + expect(screen.getByTestId(testId)).not.toHaveClass(styles.modifiers.inline); +}); + +test(`Renders with class ${styles.modifiers.inline} when variant is inline-compact`, () => { + render( + + {children} + + ); + + expect(screen.getByTestId(testId)).toHaveClass(styles.modifiers.inline); +}); + +test(`Does not render with class ${styles.modifiers.expanded} by default`, () => { + render({children}); + + expect(screen.getByTestId(testId)).not.toHaveClass(styles.modifiers.expanded); +}); + +test(`Renders with class ${styles.modifiers.expanded} when isExpanded is passed`, () => { + render( + + {children} + + ); + + expect(screen.getByTestId(testId)).toHaveClass(styles.modifiers.expanded); +}); + +test(`Does not render with class ${styles.modifiers.block} by default`, () => { + render({children}); + + expect(screen.getByTestId(testId)).not.toHaveClass(styles.modifiers.block); +}); + +test(`Renders with class ${styles.modifiers.block} when isBlock is passed`, () => { + render( + + {children} + + ); + + expect(screen.getByTestId(testId)).toHaveClass(styles.modifiers.block); +}); + +test('Spreads additional props to container div', () => { + render( + + {children} + + ); + + expect(screen.getByTestId(testId)).toHaveAttribute('role', 'group'); +}); + +test(`Renders children without class ${styles.modifiers.code} when variant is inline-compact and isCode is not passed`, () => { + render( + + {children} + + ); + + expect(screen.getByText(children)).not.toHaveClass(styles.modifiers.code); +}); + +test(`Renders children with class ${styles.modifiers.code} when variant is inline-compact and isCode is passed`, () => { + render( + + {children} + + ); + + expect(screen.getByText(children)).toHaveClass(styles.modifiers.code); +}); + +test('Does not render content passed to additionalActions by default', () => { + render({children}); + + expect(screen.queryByText('Additional action')).toBeNull(); +}); + +test('Does not render content passed to additionalActions when variant is not inline-compact', () => { + render( + + {children} + + ); + + expect(screen.queryByText('Additional action')).toBeNull(); +}); + +test('Renders content passed to additionalActions when variant is inline-compact', () => { + render( + + {children} + + ); + + expect(screen.getByText('Additional action')).toBeVisible(); +}); + +test('Passes hoverTip to ClipboardCopyButton by default', () => { + render({children}); + + expect(screen.getByText('button-ariaLabel: hover tip')).toBeVisible(); + expect(screen.getByText('children: hover tip')).toBeVisible(); +}); + +test('Passes hoverTip to ClipboardCopyButton when variant is inline-compact', () => { + render( + + {children} + + ); + + expect(screen.getByText('button-ariaLabel: hover tip')).toBeVisible(); + expect(screen.getByText('children: hover tip')).toBeVisible(); +}); + +test('Passes clickTip when ClipboardCopyButton clicked', async () => { + const user = userEvent.setup(); + render({children}); + + await user.click(screen.getByRole('button', { name: 'Test CCB clicker' })); + + expect(screen.getByText('children: click tip')).toBeVisible(); +}); + +test('Passes entryDelay to ClipboardCopyButton by default', () => { + render({children}); + + expect(screen.getByText('entryDelay: 100')).toBeVisible(); +}); + +test('Passes entryDelay to ClipboardCopyButton when variant is inline-compact', () => { + render( + + {children} + + ); + + expect(screen.getByText('entryDelay: 100')).toBeVisible(); +}); + +test('Passes exitDelay to ClipboardCopyButton by default', () => { + render({children}); + + expect(screen.getByText('exitDelay: 100')).toBeVisible(); +}); + +test('Passes exitDelay to ClipboardCopyButton when variant is inline-compact', () => { + render( + + {children} + + ); + + expect(screen.getByText('exitDelay: 100')).toBeVisible(); +}); + +test('Passes maxWidth to ClipboardCopyButton by default', () => { + render({children}); + + expect(screen.getByText('maxWidth: 100')).toBeVisible(); +}); + +test('Passes maxWidth to ClipboardCopyButton when variant is inline-compact', () => { + render( + + {children} + + ); + + expect(screen.getByText('maxWidth: 100')).toBeVisible(); +}); + +test('Passes position to ClipboardCopyButton by default', () => { + render({children}); + + expect(screen.getByText('position: bottom')).toBeVisible(); +}); + +test('Passes position to ClipboardCopyButton when variant is inline-compact', () => { + render( + + {children} + + ); + + expect(screen.getByText('position: bottom')).toBeVisible(); +}); + +test('Passes toggleAriaLabel to ClipboardCopyToggle when variant is expansion', () => { + render( + + {children} + + ); + + expect(screen.getByText('toggle-ariaLabel: toggle label')).toBeVisible(); +}); + +test('Does not set textinput to readonly when isReadOnly is not passed', () => { + render({children}); + + expect(screen.getByRole('textbox')).not.toHaveAttribute('readonly'); +}); + +test('Passes isReadOnly to TextInput', () => { + render({children}); + + expect(screen.getByRole('textbox')).toHaveAttribute('readonly'); +}); + +test('Passes textAriaLabel to TextInput', () => { + render({children}); + + expect(screen.getByRole('textbox')).toHaveAccessibleName('text label'); +}); + +test('Calls onChange when ClipboardCopy textinput is typed in', async () => { + const onChangeMock = jest.fn(); + const user = userEvent.setup(); + const typedText = 'stuff'; + + render({children}); + + await user.type(screen.getByRole('textbox'), typedText); + + expect(onChangeMock).toHaveBeenCalledTimes(typedText.length); +}); + +test('Does not call onChange when ClipboardCopy textinput is not typed in', async () => { + const onChangeMock = jest.fn(); + const user = userEvent.setup(); + const typedText = 'stuff'; + + render( + <> + {children} + + + ); + + await user.type(screen.getByRole('textbox', { name: 'native input' }), typedText); + + expect(onChangeMock).not.toHaveBeenCalled(); +}); + +test('Calls onCopy when ClipboardCopyButton is clicked', async () => { + const onCopyMock = jest.fn(); + const user = userEvent.setup(); + + render({children}); + + await user.click(screen.getByRole('button', { name: 'Test CCB clicker' })); + + expect(onCopyMock).toHaveBeenCalledTimes(1); +}); + +test('Does not call onCopy when ClipboardCopyButton is not clicked', async () => { + const onCopyMock = jest.fn(); + const user = userEvent.setup(); + + render( + <> + {children} + + + ); + + await user.click(screen.getByRole('button', { name: 'Test native clicker' })); + + expect(onCopyMock).not.toHaveBeenCalled(); +}); + +test('Matches snapshot', () => { + const { asFragment } = render( + + {children} + + ); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyAction.test.tsx b/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyAction.test.tsx new file mode 100644 index 00000000000..0419477b26b --- /dev/null +++ b/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyAction.test.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import { ClipboardCopyAction } from '../ClipboardCopyAction'; +import styles from '@patternfly/react-styles/css/components/ClipboardCopy/clipboard-copy'; + +test('Renders without children', () => { + render( +
+ +
+ ); + + expect(screen.getByTestId('container').firstChild).toBeVisible(); +}); + +test(`Renders with class ${styles.clipboardCopyActionsItem} by default`, () => { + render(Action text); + + expect(screen.getByText('Action text')).toHaveClass(styles.clipboardCopyActionsItem, { exact: true }); +}); + +test(`Renders with custom class when className is passed`, () => { + render(Action text); + + expect(screen.getByText('Action text')).toHaveClass('custom-class'); +}); + +test(`Spreads additional props`, () => { + render(Action text); + + expect(screen.getByText('Action text')).toHaveAttribute('id', 'test-id'); +}); + +test('Matches snapshot', () => { + const { asFragment } = render(Action text); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyButton.test.tsx b/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyButton.test.tsx index c87ccb90f51..34f027dd1c8 100644 --- a/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyButton.test.tsx +++ b/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyButton.test.tsx @@ -2,36 +2,153 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; - import { ClipboardCopyButton } from '../ClipboardCopyButton'; +import buttonStyles from '@patternfly/react-styles/css/components/Button/button'; + +jest.mock('../../Tooltip', () => ({ + Tooltip: ({ content, children, exitDelay, entryDelay, maxWidth, position, onTooltipHidden }) => ( +
+
{content}
+

{`exitDelay: ${exitDelay}`}

+

{`entryDelay: ${entryDelay}`}

+

{`maxWidth: ${maxWidth}`}

+

{`position: ${position}`}

+ onTooltipHidden clicker + {children} +
+ ) +})); -const props = { - id: 'my-id', - textId: 'my-text-id', - className: 'fancy-copy-button', +const requiredProps = { onClick: jest.fn(), - exitDelay: 1000, - entryDelay: 2000, - maxWidth: '500px', - position: 'right' as 'right', - 'aria-label': 'click this button to copy text' + children: 'Button content', + id: 'button-id', + textId: 'text-id' }; -test('copy button render', () => { - const { asFragment } = render(Copy Me); +// Must be kept as first test to avoid Button's ouiaId updating in snapshots +test('Matches snapshot', () => { + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); -test('copy button onClick', async () => { - const onclick = jest.fn(); - const user = userEvent.setup(); +test('Renders with passed id prop', () => { + render(); + + expect(screen.getByRole('button')).toHaveAttribute('id', 'button-id'); +}); + +test('Renders with aria-labelledby with passed id and textId prop values', () => { + render( + <> +
Copyable text
+ + + ); + + expect(screen.getByRole('button')).toHaveAccessibleName('Copyable input Copyable text'); +}); + +test('Renders with concatenated aria-label by default', () => { + render( + <> +
Copyable text
+ + + ); + + expect(screen.getByRole('button')).toHaveAccessibleName('Copyable input Copyable text'); +}); +test('Renders with concatenated aria-label when custom aria-label is passed', () => { render( - - Copy to Clipboard - + <> +
Copyable text
+ + ); + expect(screen.getByRole('button')).toHaveAccessibleName('Custom label Copyable text'); +}); + +test('Passes className to Button', () => { + render(); + + expect(screen.getByRole('button')).toHaveClass('test-class'); +}); + +test('Passes variant to Button by default', () => { + render(); + + expect(screen.getByRole('button')).toHaveClass(buttonStyles.modifiers.control); +}); + +test('Passes variant to Button when variant is passed', () => { + render(); + + expect(screen.getByRole('button')).toHaveClass(buttonStyles.modifiers.plain); +}); + +test('Calls onClick when ClipboardCopyButton is clicked', async () => { + const user = userEvent.setup(); + + render(); + await user.click(screen.getByRole('button')); - expect(onclick).toHaveBeenCalled(); + expect(requiredProps.onClick).toHaveBeenCalledTimes(1); +}); + +test('Does not call onClick when ClipboardCopyButton is not clicked', async () => { + const user = userEvent.setup(); + + render( + <> + + + + ); + + await user.click(screen.getByRole('button', { name: 'Test clicker' })); + expect(requiredProps.onClick).not.toHaveBeenCalled(); +}); + +test('Passes children to Tooltip content', () => { + render(); + + expect(screen.getByText('Button content')).toBeVisible(); +}); + +test('Passes exitDelay to Tooltip', () => { + render(); + + expect(screen.getByText('exitDelay: 200')).toBeVisible(); +}); + +test('Passes entryDelay to Tooltip', () => { + render(); + + expect(screen.getByText('entryDelay: 200')).toBeVisible(); +}); + +test('Passes maxWidth to Tooltip', () => { + render(); + + expect(screen.getByText('maxWidth: 200px')).toBeVisible(); +}); + +test('Passes position to Tooltip', () => { + render(); + + expect(screen.getByText('position: bottom')).toBeVisible(); +}); + +test('Passes onTooltipHidden to Tooltip', async () => { + const user = userEvent.setup(); + const onTooltipHiddenMock = jest.fn(); + + render(); + + await user.click(screen.getByText('onTooltipHidden clicker')); + + expect(onTooltipHiddenMock).toHaveBeenCalled(); }); diff --git a/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyExpanded.test.tsx b/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyExpanded.test.tsx index 0a9c8b32174..d91426d6e17 100644 --- a/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyExpanded.test.tsx +++ b/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyExpanded.test.tsx @@ -1,23 +1,80 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { screen, render } from '@testing-library/react'; import { ClipboardCopyExpanded } from '../ClipboardCopyExpanded'; +import styles from '@patternfly/react-styles/css/components/ClipboardCopy/clipboard-copy'; +import userEvent from '@testing-library/user-event'; -const props = { - className: 'class-1', - id: 'id-1' -}; +test(`Renders with classname ${styles.clipboardCopyExpandableContent} by default`, () => { + render(Expanded content); -test('expanded content render', () => { - const { asFragment } = render(This is my text); - expect(asFragment()).toMatchSnapshot(); + expect(screen.getByText('Expanded content')).toHaveClass(styles.clipboardCopyExpandableContent, { exact: true }); +}); + +test(`Renders with custom class when className is passed`, () => { + render(Expanded content); + + expect(screen.getByText('Expanded content')).toHaveClass('test-class'); +}); + +test('Does not render with
 tag by default', () => {
+  render(Expanded content);
+
+  expect(screen.getByText('Expanded content').tagName).not.toBe('PRE');
+});
+
+test('Renders with 
 tag when isCode is true', () => {
+  render(Expanded content);
+
+  expect(screen.getByText('Expanded content').tagName).toBe('PRE');
+});
+
+test('Renders with contenteditable attribute of true by default', () => {
+  render(Expanded content);
+
+  expect(screen.getByText('Expanded content')).toHaveAttribute('contenteditable', 'true');
+});
+
+test('Renders with contenteditable attribute of false when isReadOnly is passed', () => {
+  render(Expanded content);
+
+  expect(screen.getByText('Expanded content')).toHaveAttribute('contenteditable', 'false');
+});
+
+test('Calls onChange when expanded content is typed in', async () => {
+  const user = userEvent.setup();
+  const onChangeMock = jest.fn();
+
+  render(Expanded content);
+
+  await user.type(screen.getByText('Expanded content'), 's');
+
+  expect(onChangeMock).toHaveBeenCalledTimes(1);
 });
 
-test('expanded code content render', () => {
-  const { asFragment } = render(
-    {`{
-    "name": "@patternfly/react-core",
-    "version": "1.33.2"
-  }`}
+test('Does not call onChange when expanded content is not typed in', async () => {
+  const user = userEvent.setup();
+  const onChangeMock = jest.fn();
+
+  render(
+    <>
+      Expanded content
+      
+    
   );
+
+  await user.type(screen.getByRole('textbox'), 'A');
+
+  expect(onChangeMock).not.toHaveBeenCalled();
+});
+
+test('Spreads additional props to container', () => {
+  render(Expanded content);
+
+  expect(screen.getByText('Expanded content')).toHaveAttribute('data-prop', 'test');
+});
+
+test('Matches snapshot', () => {
+  const { asFragment } = render(Expanded content);
+
   expect(asFragment()).toMatchSnapshot();
 });
diff --git a/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyToggle.test.tsx b/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyToggle.test.tsx
index b3885e985ab..bb6a297c470 100644
--- a/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyToggle.test.tsx
+++ b/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopyToggle.test.tsx
@@ -1,48 +1,92 @@
 import React from 'react';
-
-import { render, screen } from '@testing-library/react';
+import { screen, render } from '@testing-library/react';
+import { ClipboardCopyToggle } from '../ClipboardCopyToggle';
+import styles from '@patternfly/react-styles/css/components/ClipboardCopy/clipboard-copy';
 import userEvent from '@testing-library/user-event';
 
-import { ClipboardCopyToggle, ClipboardCopyToggleProps } from '../ClipboardCopyToggle';
-
-const props: ClipboardCopyToggleProps = {
-  id: 'my-id',
-  textId: 'my-text-id',
-  contentId: 'my-content-id',
-  isExpanded: false,
-  className: 'myclassName',
-  onClick: jest.fn()
+const onClickMock = jest.fn();
+const requiredProps = {
+  id: 'main-id',
+  textId: 'text-id',
+  contentId: 'content-id',
+  onClick: onClickMock
 };
 
-describe('ClipboardCopyToggle', () => {
-  test('toggle button render', () => {
-    const desc = 'toggle content';
-    const { asFragment } = render();
+// Must be kept as first test to avoid Button's ouiaId updating in snapshots
+test('Matches snapshot', () => {
+  const { asFragment } = render();
+
+  expect(asFragment()).toMatchSnapshot();
+});
+
+test('Renders without children', () => {
+  render(
+    
+ +
+ ); + + expect(screen.getByTestId('container').firstChild).toBeVisible(); +}); + +test('Renders with id prop', () => { + render(); + + expect(screen.getByRole('button')).toHaveAttribute('id', requiredProps.id); +}); + +test('Renders with aria-labelledby concatenated from id and textId props', () => { + render( + <> + + Test content + + ); - expect(asFragment()).toMatchSnapshot(); - }); + expect(screen.getByRole('button')).toHaveAccessibleName('Toggle content Test content'); +}); + +test('Renders with aria-controls with passed in contentId prop', () => { + render(); + + expect(screen.getByRole('button')).toHaveAttribute('aria-controls', requiredProps.contentId); +}); - test('toggle button onClick', async () => { - const onclick = jest.fn(); - const user = userEvent.setup(); +test('Renders with aria-expanded of false by default', () => { + render(); - render(); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false'); +}); - await user.click(screen.getByRole('button')); - expect(onclick).toHaveBeenCalled(); - }); +test('Renders with aria-expanded based on isExpanded prop', () => { + render(); - test('has aria-expanded set to true when isExpanded is true', () => { - render(); + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'); +}); - const toggleButton = screen.getByRole('button'); - expect(toggleButton).toHaveAttribute('aria-expanded', 'true'); - }); +test('Calls onClick when clipboard toggle is clicked', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button')); + expect(onClickMock).toHaveBeenCalledTimes(1); +}); + +test('Does not call onClick when clipboard toggle is not clicked', async () => { + const user = userEvent.setup(); + render( + <> + + + + ); + + await user.click(screen.getByRole('button', { name: 'Test clicker' })); + expect(onClickMock).not.toHaveBeenCalled(); +}); - test('has aria-expanded set to false when isExpanded is false', () => { - render(); +test('Spreads additional props to container', () => { + render(); - const toggleButton = screen.getByRole('button'); - expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); - }); + expect(screen.getByRole('button')).toHaveAttribute('data-prop', 'test'); }); diff --git a/packages/react-core/src/components/ClipboardCopy/__tests__/Generated/ClipboardCopy.test.tsx b/packages/react-core/src/components/ClipboardCopy/__tests__/Generated/ClipboardCopy.test.tsx deleted file mode 100644 index 612abfd0e14..00000000000 --- a/packages/react-core/src/components/ClipboardCopy/__tests__/Generated/ClipboardCopy.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * This test was generated - */ -import * as React from 'react'; -import { render } from '@testing-library/react'; -import { ClipboardCopy } from '../../ClipboardCopy'; -// any missing imports can usually be resolved by adding them here -import {} from '../..'; - -it('ClipboardCopy should match snapshot (auto-generated)', () => { - const { asFragment } = render( - , text?: React.ReactNode) => { - const clipboard = event.currentTarget.parentElement; - const el = document.createElement('input'); - el.value = text.toString(); - clipboard.appendChild(el); - el.select(); - document.execCommand('copy'); - clipboard.removeChild(el); - }} - onChange={(): any => undefined} - children={
ReactNode
} - /> - ); - expect(asFragment()).toMatchSnapshot(); -}); diff --git a/packages/react-core/src/components/ClipboardCopy/__tests__/Generated/__snapshots__/ClipboardCopy.test.tsx.snap b/packages/react-core/src/components/ClipboardCopy/__tests__/Generated/__snapshots__/ClipboardCopy.test.tsx.snap deleted file mode 100644 index 9b881779f24..00000000000 --- a/packages/react-core/src/components/ClipboardCopy/__tests__/Generated/__snapshots__/ClipboardCopy.test.tsx.snap +++ /dev/null @@ -1,55 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ClipboardCopy should match snapshot (auto-generated) 1`] = ` - -
-
- - - - -
-
-
-`; diff --git a/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopy.test.tsx.snap b/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopy.test.tsx.snap new file mode 100644 index 00000000000..832f82d2ba4 --- /dev/null +++ b/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopy.test.tsx.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches snapshot 1`] = ` + +
+
+ + + +
+

+ exitDelay: 1500 +

+

+ entryDelay: 300 +

+

+ maxWidth: 150px +

+

+ position: top +

+

+ button-ariaLabel: Copy to clipboard +

+
+ children: Copy to clipboard +
+ +
+
+
+
+`; diff --git a/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyAction.test.tsx.snap b/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyAction.test.tsx.snap new file mode 100644 index 00000000000..0b7ff942012 --- /dev/null +++ b/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyAction.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches snapshot 1`] = ` + + + Action text + + +`; diff --git a/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyButton.test.tsx.snap b/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyButton.test.tsx.snap index 8ba62b22f84..0ca5758ef2c 100644 --- a/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyButton.test.tsx.snap +++ b/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyButton.test.tsx.snap @@ -1,31 +1,57 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`copy button render 1`] = ` +exports[`Matches snapshot 1`] = ` - +
+ Button content +
+ +

+ exitDelay: 0 +

+

+ entryDelay: 300 +

+

+ maxWidth: 100px +

+

+ position: top +

+ + onTooltipHidden clicker + + +
`; diff --git a/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyExpanded.test.tsx.snap b/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyExpanded.test.tsx.snap index 01bc2ca3bce..3204f2ba149 100644 --- a/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyExpanded.test.tsx.snap +++ b/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyExpanded.test.tsx.snap @@ -1,32 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`expanded code content render 1`] = ` +exports[`Matches snapshot 1`] = `
-
-      {
-    "name": "@patternfly/react-core",
-    "version": "1.33.2"
-  }
-    
-
-
-`; - -exports[`expanded content render 1`] = ` - -
- This is my text + Expanded content
`; diff --git a/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyToggle.test.tsx.snap b/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyToggle.test.tsx.snap index ea395c521de..cd43bec9fc7 100644 --- a/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyToggle.test.tsx.snap +++ b/packages/react-core/src/components/ClipboardCopy/__tests__/__snapshots__/ClipboardCopyToggle.test.tsx.snap @@ -1,18 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ClipboardCopyToggle toggle button render 1`] = ` +exports[`Matches snapshot 1`] = ` diff --git a/packages/react-core/src/components/Dropdown/Dropdown.tsx b/packages/react-core/src/components/Dropdown/Dropdown.tsx index 5fed1c51552..a35ad8b4b4d 100644 --- a/packages/react-core/src/components/Dropdown/Dropdown.tsx +++ b/packages/react-core/src/components/Dropdown/Dropdown.tsx @@ -19,6 +19,8 @@ export interface DropdownPopperProps { enableFlip?: boolean; /** The container to append the popper to. Defaults to 'inline'. */ appendTo?: HTMLElement | (() => HTMLElement) | 'inline'; + /** Flag to prevent the popper from overflowing its container and becoming partially obscured. */ + preventOverflow?: boolean; } export interface DropdownToggleProps { diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorControl.tsx b/packages/react-core/src/components/DualListSelector/DualListSelectorControl.tsx index 39bfe859e39..266098bdb85 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorControl.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorControl.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { css } from '@patternfly/react-styles'; import { Button, ButtonVariant } from '../Button'; import { Tooltip } from '../Tooltip'; +import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector'; /** Renders an individual control button for moving selected options between each * dual list selector pane. @@ -40,7 +41,7 @@ export const DualListSelectorControlBase: React.FunctionComponent +
diff --git a/packages/react-core/src/components/Form/examples/FormGroupLabelInfo.tsx b/packages/react-core/src/components/Form/examples/FormGroupLabelInfo.tsx index de8075cadb1..be5df5fce6d 100644 --- a/packages/react-core/src/components/Form/examples/FormGroupLabelInfo.tsx +++ b/packages/react-core/src/components/Form/examples/FormGroupLabelInfo.tsx @@ -9,6 +9,7 @@ import { FormHelperText } from '@patternfly/react-core'; import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; +import styles from '@patternfly/react-styles/css/components/Form/form'; export const FormGroupLabelInfo: React.FunctionComponent = () => { const [name, setName] = React.useState(''); @@ -55,7 +56,7 @@ export const FormGroupLabelInfo: React.FunctionComponent = () => { aria-label="More info for name field" onClick={(e) => e.preventDefault()} aria-describedby="form-group-label-info" - className="pf-v5-c-form__group-label-help" + className={styles.formGroupLabelHelp} > diff --git a/packages/react-core/src/components/Form/examples/FormLimitWidth.tsx b/packages/react-core/src/components/Form/examples/FormLimitWidth.tsx index 98512d63484..8be09a9d2d7 100644 --- a/packages/react-core/src/components/Form/examples/FormLimitWidth.tsx +++ b/packages/react-core/src/components/Form/examples/FormLimitWidth.tsx @@ -13,6 +13,7 @@ import { FormHelperText } from '@patternfly/react-core'; import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; +import styles from '@patternfly/react-styles/css/components/Form/form'; export const FormLimitWidth: React.FunctionComponent = () => { const [name, setName] = React.useState(''); @@ -68,7 +69,7 @@ export const FormLimitWidth: React.FunctionComponent = () => { aria-label="More info for name field" onClick={(e) => e.preventDefault()} aria-describedby="simple-form-name-02" - className="pf-v5-c-form__group-label-help" + className={styles.formGroupLabelHelp} > diff --git a/packages/react-core/src/components/Hint/__tests__/Hint.test.tsx b/packages/react-core/src/components/Hint/__tests__/Hint.test.tsx index 10000957fc3..aa84b2d5c35 100644 --- a/packages/react-core/src/components/Hint/__tests__/Hint.test.tsx +++ b/packages/react-core/src/components/Hint/__tests__/Hint.test.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { render, screen } from '@testing-library/react'; import { Hint } from '../Hint'; +import styles from '@patternfly/react-styles/css/components/Hint/hint'; test('renders without children', () => { render(); @@ -14,12 +15,12 @@ test('renders children', () => { expect(screen.getByText('Test')).toBeVisible(); }); -test('renders with class pf-v5-c-hint', () => { +test(`renders with class ${styles.hint}`, () => { render(Test); const hint = screen.getByText('Test'); - expect(hint).toHaveClass('pf-v5-c-hint'); + expect(hint).toHaveClass(styles.hint); }); test('renders with custom class names provided via prop', () => { @@ -46,12 +47,12 @@ test('renders actions options', () => { expect(actions).toBeVisible(); }); -test('renders with class pf-v5-c-hint__actions if there is an action prop', () => { +test(`renders with class ${styles.hintActions} if there is an action prop`, () => { render(Test); const hint = screen.getByText('actions'); - expect(hint).toHaveClass('pf-v5-c-hint__actions'); + expect(hint).toHaveClass(styles.hintActions); }); test('renders with inherited element props spread to the component', () => { diff --git a/packages/react-core/src/components/Hint/__tests__/HintBody.test.tsx b/packages/react-core/src/components/Hint/__tests__/HintBody.test.tsx index d9142507670..8c2c72b7b78 100644 --- a/packages/react-core/src/components/Hint/__tests__/HintBody.test.tsx +++ b/packages/react-core/src/components/Hint/__tests__/HintBody.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; +import styles from '@patternfly/react-styles/css/components/Hint/hint'; import { HintBody } from '../HintBody'; @@ -16,12 +17,12 @@ test('renders children', () => { expect(screen.getByRole('button', { name: 'Test Me' })).toBeVisible(); }); -test('renders with class pf-v5-c-hint__body', () => { +test(`renders with class ${styles.hintBody}`, () => { render(Hint Body Test); const body = screen.getByText('Hint Body Test'); - expect(body).toHaveClass('pf-v5-c-hint__body'); + expect(body).toHaveClass(styles.hintBody); }); test('renders with custom class names provided via prop', () => { diff --git a/packages/react-core/src/components/Hint/__tests__/HintFooter.test.tsx b/packages/react-core/src/components/Hint/__tests__/HintFooter.test.tsx index f71df174798..1e8e6daac33 100644 --- a/packages/react-core/src/components/Hint/__tests__/HintFooter.test.tsx +++ b/packages/react-core/src/components/Hint/__tests__/HintFooter.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; +import styles from '@patternfly/react-styles/css/components/Hint/hint'; import { HintFooter } from '../HintFooter'; @@ -16,12 +17,12 @@ test('renders children', () => { expect(screen.getByRole('button', { name: 'Test Me' })).toBeVisible(); }); -test('renders with class pf-v5-c-hint__footer', () => { +test(`renders with class ${styles.hintFooter}`, () => { render(Hint Body Test); const body = screen.getByText('Hint Body Test'); - expect(body).toHaveClass('pf-v5-c-hint__footer'); + expect(body).toHaveClass(styles.hintFooter); }); test('renders with custom class names provided via prop', () => { diff --git a/packages/react-core/src/components/Hint/__tests__/HintTitle.test.tsx b/packages/react-core/src/components/Hint/__tests__/HintTitle.test.tsx index 8fd536bcbf6..62164522f20 100644 --- a/packages/react-core/src/components/Hint/__tests__/HintTitle.test.tsx +++ b/packages/react-core/src/components/Hint/__tests__/HintTitle.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; +import styles from '@patternfly/react-styles/css/components/Hint/hint'; import { HintTitle } from '../HintTitle'; @@ -16,12 +17,12 @@ test('renders children', () => { expect(screen.getByRole('button', { name: 'Test Me' })).toBeVisible(); }); -test('renders with class pf-v5-c-hint__title', () => { +test(`renders with class ${styles.hintTitle}`, () => { render(Hint Body Test); const body = screen.getByText('Hint Body Test'); - expect(body).toHaveClass('pf-v5-c-hint__title'); + expect(body).toHaveClass(styles.hintTitle); }); test('renders with custom class names provided via prop', () => { diff --git a/packages/react-core/src/components/Icon/__tests__/Icon.test.tsx b/packages/react-core/src/components/Icon/__tests__/Icon.test.tsx index 8c3373d3a39..b2f2e67de4f 100644 --- a/packages/react-core/src/components/Icon/__tests__/Icon.test.tsx +++ b/packages/react-core/src/components/Icon/__tests__/Icon.test.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { render, screen } from '@testing-library/react'; import { Icon } from '../Icon'; import { CheckIcon } from '@patternfly/react-icons'; +import styles from '@patternfly/react-styles/css/components/Icon/icon'; test('renders basic icon successfully', () => { const { asFragment } = render( @@ -19,9 +20,9 @@ test('checks basic icon structure', () => { ); const iconContainer = screen.getByTitle('icon'); - expect(iconContainer).toHaveClass('pf-v5-c-icon'); - const iconContent = iconContainer.querySelector('.pf-v5-c-icon__content'); - expect(iconContent).toHaveClass('pf-v5-c-icon__content'); + expect(iconContainer).toHaveClass(styles.icon); + const iconContent = iconContainer.querySelector(`.${styles.iconContent}`); + expect(iconContent).toHaveClass(styles.iconContent); }); test('renders without children', () => { @@ -47,7 +48,7 @@ Object.values(['sm', 'md', 'lg', 'xl']).forEach((size) => { ); - const iconContainer = screen.getByTitle(`content-${size}-icon`).querySelector('.pf-v5-c-icon__content'); + const iconContainer = screen.getByTitle(`content-${size}-icon`).querySelector(`.${styles.iconContent}`); expect(iconContainer).toHaveClass(`pf-m-${size}`); }); @@ -59,7 +60,7 @@ test('check icon without iconSize', () => { ); - const iconContainer = screen.getByTitle('no-icon-size').querySelector('.pf-v5-c-icon__content'); + const iconContainer = screen.getByTitle('no-icon-size').querySelector(`.${styles.iconContent}`); expect(Array.from(iconContainer?.classList || []).some((c) => /pf-m-*/.test(c))); // Check no modifier classes have been added }); @@ -70,7 +71,7 @@ Object.values(['sm', 'md', 'lg', 'xl']).forEach((size) => { ); - const iconContainer = screen.getByTitle(`progress-content-${size}-icon`).querySelector('.pf-v5-c-icon__progress'); + const iconContainer = screen.getByTitle(`progress-content-${size}-icon`).querySelector(`.${styles.iconProgress}`); expect(iconContainer).toHaveClass(`pf-m-${size}`); }); @@ -82,7 +83,7 @@ test('check icon without progress icon size', () => { ); - const iconContainer = screen.getByTitle('no-progress-icon-size').querySelector('.pf-v5-c-icon__progress'); + const iconContainer = screen.getByTitle('no-progress-icon-size').querySelector(`.${styles.iconProgress}`); expect(Array.from(iconContainer?.classList || []).some((c) => /pf-m-*/.test(c))); // Check no modifier classes have been added }); @@ -116,7 +117,7 @@ Object.values(['custom', 'info', 'success', 'warning', 'danger']).forEach((statu ); - const iconContent = screen.getByTitle(`${status}-icon`).querySelector('.pf-v5-c-icon__content'); + const iconContent = screen.getByTitle(`${status}-icon`).querySelector(`.${styles.iconContent}`); expect(iconContent).toHaveClass(`pf-m-${status}`); }); @@ -128,7 +129,7 @@ test('check icon without status', () => { ); - const iconContent = screen.getByTitle('no-status').querySelector('.pf-v5-c-icon__content'); + const iconContent = screen.getByTitle('no-status').querySelector(`.${styles.iconContent}`); expect(Array.from(iconContent?.classList || []).some((c) => /pf-m-*/.test(c))); // Check no modifier classes have been added }); @@ -162,8 +163,8 @@ test('sets isInProgress successfully', () => { const iconContainer = screen.getByTitle('progress-icon'); expect(iconContainer).toHaveClass('pf-m-in-progress'); - const iconContent = iconContainer.querySelector('.pf-v5-c-icon__progress'); - expect(iconContent).toHaveClass('pf-v5-c-icon__progress'); + const iconContent = iconContainer.querySelector(`.${styles.iconProgress}`); + expect(iconContent).toHaveClass(styles.iconProgress); }); test('check icon without isInProgress', () => { diff --git a/packages/react-core/src/components/InputGroup/InputGroup.tsx b/packages/react-core/src/components/InputGroup/InputGroup.tsx index 55cc6d90c29..e116eb465b7 100644 --- a/packages/react-core/src/components/InputGroup/InputGroup.tsx +++ b/packages/react-core/src/components/InputGroup/InputGroup.tsx @@ -1,9 +1,6 @@ import * as React from 'react'; import styles from '@patternfly/react-styles/css/components/InputGroup/input-group'; import { css } from '@patternfly/react-styles'; -import { FormSelect } from '../FormSelect'; -import { TextArea } from '../TextArea'; -import { TextInput } from '../TextInput'; export interface InputGroupProps extends React.HTMLProps { /** Additional classes added to the input group. */ @@ -20,61 +17,12 @@ export const InputGroupBase: React.FunctionComponent = ({ innerRef, ...props }: InputGroupProps) => { - const getIdItem = () => { - const getChildId = (_children: any) => - React.Children.toArray(_children).find( - (_child: any) => !formCtrls.includes(_child?.type?.displayName) && _child?.props?.id - ); - let childId = getChildId(children); - if (childId) { - return childId; - } - React.Children.toArray(children).find((child: any) => { - const _childId = getChildId(child.props.children); - if (_childId) { - childId = _childId; - return true; - } - }); - return childId; - }; - const formCtrls = [FormSelect, TextArea, TextInput].map((comp) => comp.displayName); - const idItem = getIdItem() as React.ReactElement<{ id: string }>; const ref = React.useRef(null); const inputGroupRef = innerRef || ref; - const childrenWithId = React.Children.map(children, (child: any) => { - if (child?.type.displayName === 'InputGroupItem') { - const newChildren = React.Children.map(child.props.children, (_child) => { - if (!_child.props) { - return _child; - } - if (_child.props['aria-describedby']) { - return _child; - } - if (!formCtrls.includes(_child.type.displayName)) { - return _child; - } - return React.cloneElement(_child, { - 'aria-describedby': _child.props['aria-describedby'] === '' ? undefined : idItem?.props?.id - }); - }); - return React.cloneElement(child, {}, newChildren); - } - - if (child?.props['aria-describedby']) { - return child; - } - if (!formCtrls.includes(child?.type.displayName)) { - return child; - } - return React.cloneElement(child, { - 'aria-describedby': child.props['aria-describedby'] === '' ? undefined : idItem?.props?.id - }); - }); return (
- {idItem ? childrenWithId : children} + {children}
); }; diff --git a/packages/react-core/src/components/InputGroup/__tests__/InputGroup.test.tsx b/packages/react-core/src/components/InputGroup/__tests__/InputGroup.test.tsx index cca2d2bf250..dd35dd426f7 100644 --- a/packages/react-core/src/components/InputGroup/__tests__/InputGroup.test.tsx +++ b/packages/react-core/src/components/InputGroup/__tests__/InputGroup.test.tsx @@ -8,59 +8,22 @@ import { Button } from '../../Button'; import { TextInput } from '../../TextInput'; describe('InputGroup', () => { - test('add aria-describedby to form-control if one of the non form-controls has id', () => { - // In this test, TextInput is a form-control component and Button is not. - // If Button has an id props, this should be used in aria-describedby. + // Regression test for https://github.com/patternfly/patternfly-react/issues/9667 + test('wont add aria-describedby automatically to form-control', () => { render( - + - ); - expect(screen.getByLabelText('some text')).toHaveAttribute('aria-describedby', 'button-id'); - }); - test('wont add aria-describedby to form-control if describedby is empty string', () => { - // In this test, TextInput is a form-control component and Button is not. - // If Button has an id props, this should be used in aria-describedby, but this - // example has an empty aria-describedby to prevent that from happening. - render( - - - - - - - - - ); - expect(screen.getByLabelText('some text')).not.toHaveAttribute('aria-describedby'); - }); - - test('wont override aria-describedby in form-control if describedby has value', () => { - // In this test, TextInput is a form-control component and Button is not. - // If Button has an id props, this should be used in aria-describedby, but this - // example has a predefined aria-describedby to prevent that from happening - render( - - - - - - - - - ); - expect(screen.getByLabelText('some text')).toHaveAttribute('aria-describedby', 'myself'); + const formControl = screen.getByLabelText("User password"); + expect(formControl).not.toHaveAttribute("aria-describedby"); }); }); diff --git a/packages/react-core/src/components/JumpLinks/JumpLinks.tsx b/packages/react-core/src/components/JumpLinks/JumpLinks.tsx index 953b163d128..345dcb2ef87 100644 --- a/packages/react-core/src/components/JumpLinks/JumpLinks.tsx +++ b/packages/react-core/src/components/JumpLinks/JumpLinks.tsx @@ -227,7 +227,7 @@ export const JumpLinks: React.FunctionComponent = ({ {...props} >
-
+
{expandable && (
@@ -128,7 +129,7 @@ export const ModalWithForm: React.FunctionComponent = () => { aria-label="More info for e-mail field" onClick={(e) => e.preventDefault()} aria-describedby="modal-with-form-form-email" - className="pf-v5-c-form__group-label-help" + className={formStyles.formGroupLabelHelp} > @@ -176,7 +177,7 @@ export const ModalWithForm: React.FunctionComponent = () => { aria-label="More info for address field" onClick={(e) => e.preventDefault()} aria-describedby="modal-with-form-form-address" - className="pf-v5-c-form__group-label-help" + className={formStyles.formGroupLabelHelp} > diff --git a/packages/react-core/src/components/MultipleFileUpload/MultipleFileUploadStatus.tsx b/packages/react-core/src/components/MultipleFileUpload/MultipleFileUploadStatus.tsx index 10ec7937fff..6d48dfd11f2 100644 --- a/packages/react-core/src/components/MultipleFileUpload/MultipleFileUploadStatus.tsx +++ b/packages/react-core/src/components/MultipleFileUpload/MultipleFileUploadStatus.tsx @@ -75,7 +75,7 @@ export const MultipleFileUploadStatus: React.FunctionComponent -
    +
      {children}
    diff --git a/packages/react-core/src/components/MultipleFileUpload/examples/MultipleFileUploadBasic.tsx b/packages/react-core/src/components/MultipleFileUpload/examples/MultipleFileUploadBasic.tsx index bf92c4438be..a55c4f73bb5 100644 --- a/packages/react-core/src/components/MultipleFileUpload/examples/MultipleFileUploadBasic.tsx +++ b/packages/react-core/src/components/MultipleFileUpload/examples/MultipleFileUploadBasic.tsx @@ -62,7 +62,8 @@ export const MultipleFileUploadBasic: React.FunctionComponent = () => { const updateCurrentFiles = (files: File[]) => { if (fileUploadShouldFail) { const corruptedFiles = files.map((file) => ({ ...file, lastModified: 'foo' as unknown as number })); - setCurrentFiles((prevFiles) => [...prevFiles, ...corruptedFiles]); + // eslint-disable-next-line + setCurrentFiles((prevFiles) => [...prevFiles, ...corruptedFiles as any]); } else { setCurrentFiles((prevFiles) => [...prevFiles, ...files]); } diff --git a/packages/react-core/src/components/Nav/NavExpandable.tsx b/packages/react-core/src/components/Nav/NavExpandable.tsx index d69327115b4..624750b4327 100644 --- a/packages/react-core/src/components/Nav/NavExpandable.tsx +++ b/packages/react-core/src/components/Nav/NavExpandable.tsx @@ -9,10 +9,10 @@ import { PickOptional } from '../../helpers/typeUtils'; import { getOUIAProps, OUIAProps, getDefaultOUIAId } from '../../helpers'; export interface NavExpandableProps - extends React.DetailedHTMLProps, HTMLLIElement>, + extends Omit, HTMLLIElement>, 'title'>, OUIAProps { - /** Title shown for the expandable list */ - title: string; + /** Title content shown for the expandable list */ + title: React.ReactNode; /** If defined, screen readers will read this text instead of the list title */ srText?: string; /** Boolean to programatically expand or collapse section */ @@ -126,14 +126,14 @@ class NavExpandable extends React.Component {({ isSidebarOpen }) => (
- + diff --git a/packages/react-core/src/components/NotificationDrawer/NotificationDrawerList.tsx b/packages/react-core/src/components/NotificationDrawer/NotificationDrawerList.tsx index e377d19270c..2f186ce71ff 100644 --- a/packages/react-core/src/components/NotificationDrawer/NotificationDrawerList.tsx +++ b/packages/react-core/src/components/NotificationDrawer/NotificationDrawerList.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { css } from '@patternfly/react-styles'; +import styles from '@patternfly/react-styles/css/components/NotificationDrawer/notification-drawer'; export interface NotificationDrawerListProps extends React.HTMLProps { /** Content rendered inside the notification drawer list body */ @@ -22,7 +23,7 @@ export const NotificationDrawerList: React.FunctionComponent (