diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 990d5bb6cfe..00000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: Feature request -about: Suggest a new feature for PatternFly. Features augment or impact end user experience and requires design input. -title: "[Component] - [short description]" -type: Feature -assignees: '' - ---- - -**Is this a new component or an extension of an existing one?** -What is the existing component, if any? - -**Describe the feature** -A clear and concise description of the new feature. What is the expected behavior? - -**Are there visuals for this feature? If applicable, please include examples for each state and for varying widths** -Include screenshots or links to Marvel or other mockups. - -**Any other information?** diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000000..7e9c1bb19c3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,99 @@ +name: Feature request +description: Suggest a new feature or component for PatternFly. +title: "[Component] - [short description]" +labels: ["extension", "needs-triage"] +body: + - type: markdown + attributes: + value: | + ### Thanks for helping improve PatternFly! + Please fill out this form to help the team understand your proposal. + + - type: dropdown + id: category + attributes: + label: Is this a new component or an extension? + options: + - New Component + - Variant of an existing component + - Enhancement to a current feature + validations: + required: true + + - type: input + id: existing_component + attributes: + label: Existing Component + description: If this is a variant or enhancement, which component does it impact? + placeholder: e.g., Table, Select, Wizard + validations: + required: false + + - type: textarea + id: description + attributes: + label: Describe the feature + description: A clear and concise description of the new feature. + placeholder: What is the expected behavior? Is there any specific error handling? + validations: + required: true + + - type: textarea + id: user_story + attributes: + label: User Story + description: Providing context helps us understand the priority. + placeholder: As a [user role], I want to [action] so that [benefit]. + validations: + required: true + + - type: textarea + id: visuals + attributes: + label: Visuals & Mockups + description: Include links to Figma or upload screenshots for desktop/mobile views. + placeholder: | + - Figma link: + - Screenshots: (Drag images here) + validations: + required: false + + - type: textarea + id: interaction_states + attributes: + label: Interaction States & Variations + description: Describe the behavior across different states. + placeholder: | + - Initial/Empty: + - Loading: + - Error/Validation: + - Mobile/Responsive Viewport: + validations: + required: true + + - type: textarea + id: accessibility + attributes: + label: Accessibility (A11y) + description: Specific keyboard interaction or focus management requirements. + placeholder: e.g., Tab navigation, ARIA labels, or focus traps. + validations: + required: false + + - type: input + id: product_release + attributes: + label: Product & Target Release + description: If applicable, what is your product and target release date? + placeholder: e.g., OpenShift 4.15 + validations: + required: false + + - type: checkboxes + id: contribution_check + attributes: + label: Contribution + options: + - label: I am interested in contributing this feature. + - label: I have searched for similar existing requests. + required: true diff --git a/.github/workflows/add-new-issues-to-project.yml b/.github/workflows/add-new-issues-to-project.yml index 114f1cce430..ec999f05b1a 100644 --- a/.github/workflows/add-new-issues-to-project.yml +++ b/.github/workflows/add-new-issues-to-project.yml @@ -16,6 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Team Membership Checker + id: teamcheck # You may pin to the exact commit or the version. # uses: TheModdingInquisition/actions-team-membership@a69636a92bc927f32c3910baac06bacc949c984c uses: TheModdingInquisition/actions-team-membership@v1.0 @@ -27,9 +28,10 @@ jobs: # The organization of the team to check for. Defaults to the context organization. organization: 'patternfly' # If the action should exit if the user is not part of the team. - exit: true + exit: false - name: Add label if user is a team member + if: steps.teamcheck.outputs.permitted == 'true' run: | curl -X POST \ -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000000..2adc63e5c96 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +# Generated icon files - these are auto-generated and should not be formatted +packages/react-icons/scripts/icons/rhIcons*.mjs +packages/react-icons/scripts/icons/rhdsIcons*.mjs + diff --git a/README.md b/README.md index b2cdee88a52..6ad8857abff 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This project provides a set of React components for the [PatternFly project](https://patternfly.org). **Community:** [PatternFly website](https://www.patternfly.org) | [Slack](https://slack.patternfly.org) | [Medium](https://medium.com/patternfly) | [Mailing list](https://www.redhat.com/mailman/listinfo/patternfly) - + ### Table of contents 1. [PatternFly React packages](#patternfly-react-packages) 2. [Setup](#Setup) @@ -16,7 +16,7 @@ This project provides a set of React components for the [PatternFly project](htt Using PatternFly 3? Take a look at the [PatternFly 3 React component information](https://github.com/patternfly/patternfly-react/blob/patternfly-3/README.md). ### PatternFly React packages - + | Package link | Description | | --- | --- | | **:blue_heart: Core packages** | diff --git a/package.json b/package.json index 41033f69eef..2b18643bf39 100644 --- a/package.json +++ b/package.json @@ -29,48 +29,49 @@ "@babel/preset-env": "^7.28.5", "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", - "@eslint/compat": "^1.3.1", + "@eslint/compat": "^1.4.1", "@eslint/js": "^9.32.0", "@octokit/rest": "^21.1.1", + "@rhds/icons": "^2.1.0", "@rollup/plugin-commonjs": "^26.0.3", "@rollup/plugin-node-resolve": "^15.3.1", "@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-terser": "^0.4.4", - "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.2.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/jest": "29.5.14", "@types/node": "^22.16.5", - "@types/react": "^18.3.23", + "@types/react": "^18.3.27", "@types/react-dom": "^18.3.7", "babel-jest": "^29.7.0", - "concurrently": "^9.1.2", + "concurrently": "^9.2.1", "eslint": "^9.32.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-markdown": "^5.1.0", - "eslint-plugin-prettier": "^5.2.6", + "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-compiler": "19.0.0-beta-ebf51a3-20250411", "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-testing-library": "^7.1.1", - "fs-extra": "^11.3.0", - "glob": "^11.0.3", + "eslint-plugin-testing-library": "^7.15.4", + "fs-extra": "^11.3.3", + "glob": "^11.1.0", "globals": "^15.15.0", "husky": "^9.1.7", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-transform-stub": "^2.0.0", - "lerna": "^8.2.1", - "lint-staged": "^15.5.0", + "lerna": "^8.2.4", + "lint-staged": "^15.5.2", "mutation-observer": "^1.0.3", - "plop": "^4.0.1", - "prettier": "^3.5.3", - "publint": "^0.3.9", + "plop": "^4.0.4", + "prettier": "^3.8.1", + "publint": "^0.3.16", "react": "^18.3.1", "react-dom": "^18.3.1", - "rimraf": "^6.0.1", - "rollup": "^4.36.0", + "rimraf": "^6.1.2", + "rollup": "^4.57.1", "rollup-plugin-scss": "^4.0.1", "rollup-plugin-svg": "^2.0.0", "sass": "^1.86.0", diff --git a/packages/eslint-plugin-patternfly-react/CHANGELOG.md b/packages/eslint-plugin-patternfly-react/CHANGELOG.md index 8ccb8afe584..797538e4ca8 100644 --- a/packages/eslint-plugin-patternfly-react/CHANGELOG.md +++ b/packages/eslint-plugin-patternfly-react/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [6.5.0-prerelease.11](https://github.com/patternfly/patternfly-react/compare/eslint-plugin-patternfly-react@6.5.0-prerelease.1...eslint-plugin-patternfly-react@6.5.0-prerelease.11) (2025-12-16) + +**Note:** Version bump only for package eslint-plugin-patternfly-react + # [6.5.0-prerelease.1](https://github.com/patternfly/patternfly-react/compare/eslint-plugin-patternfly-react@6.4.0...eslint-plugin-patternfly-react@6.5.0-prerelease.1) (2025-10-30) **Note:** Version bump only for package eslint-plugin-patternfly-react diff --git a/packages/eslint-plugin-patternfly-react/package.json b/packages/eslint-plugin-patternfly-react/package.json index bdde8d6fab8..63b2b320aaa 100644 --- a/packages/eslint-plugin-patternfly-react/package.json +++ b/packages/eslint-plugin-patternfly-react/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-patternfly-react", - "version": "6.5.0-prerelease.1", + "version": "6.5.0-prerelease.11", "type": "commonjs", "exports": { ".": "./lib/index.js" diff --git a/packages/react-charts/CHANGELOG.md b/packages/react-charts/CHANGELOG.md index 59de911b097..f4de635ef53 100644 --- a/packages/react-charts/CHANGELOG.md +++ b/packages/react-charts/CHANGELOG.md @@ -3,6 +3,40 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.5.0-prerelease.14](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@8.5.0-prerelease.13...@patternfly/react-charts@8.5.0-prerelease.14) (2026-02-18) + +**Note:** Version bump only for package @patternfly/react-charts + +# [8.5.0-prerelease.13](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@8.5.0-prerelease.12...@patternfly/react-charts@8.5.0-prerelease.13) (2026-02-05) + +**Note:** Version bump only for package @patternfly/react-charts + +# [8.5.0-prerelease.12](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@8.5.0-prerelease.11...@patternfly/react-charts@8.5.0-prerelease.12) (2026-01-28) + +**Note:** Version bump only for package @patternfly/react-charts + +# [8.5.0-prerelease.11](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@8.5.0-prerelease.10...@patternfly/react-charts@8.5.0-prerelease.11) (2026-01-09) + +**Note:** Version bump only for package @patternfly/react-charts + +# [8.5.0-prerelease.10](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@8.5.0-prerelease.9...@patternfly/react-charts@8.5.0-prerelease.10) (2025-12-17) + +**Note:** Version bump only for package @patternfly/react-charts + +# [8.5.0-prerelease.9](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@8.5.0-prerelease.6...@patternfly/react-charts@8.5.0-prerelease.9) (2025-12-16) + +**Note:** Version bump only for package @patternfly/react-charts + +# [8.5.0-prerelease.6](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@8.5.0-prerelease.5...@patternfly/react-charts@8.5.0-prerelease.6) (2025-11-14) + +### Bug Fixes + +- Moves items to new nav. ([#12013](https://github.com/patternfly/patternfly-react/issues/12013)) ([ddd0696](https://github.com/patternfly/patternfly-react/commit/ddd0696796134c7d0f9583ce56e26b0df47156cb)) + +# [8.5.0-prerelease.5](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@8.5.0-prerelease.4...@patternfly/react-charts@8.5.0-prerelease.5) (2025-11-12) + +**Note:** Version bump only for package @patternfly/react-charts + # [8.5.0-prerelease.4](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-charts@8.5.0-prerelease.3...@patternfly/react-charts@8.5.0-prerelease.4) (2025-11-06) **Note:** Version bump only for package @patternfly/react-charts diff --git a/packages/react-charts/package.json b/packages/react-charts/package.json index 745a9c83e80..766e91d671d 100644 --- a/packages/react-charts/package.json +++ b/packages/react-charts/package.json @@ -1,6 +1,6 @@ { "name": "@patternfly/react-charts", - "version": "8.5.0-prerelease.4", + "version": "8.5.0-prerelease.14", "description": "This library provides a set of React chart components for use with the PatternFly reference implementation.", "main": "dist/js/index.js", "module": "dist/esm/index.js", @@ -42,12 +42,12 @@ "@patternfly/react-styles": "workspace:^", "@patternfly/react-tokens": "workspace:^", "hoist-non-react-statics": "^3.3.2", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "tslib": "^2.8.1" }, "devDependencies": { - "@types/lodash": "^4.17.20", - "fs-extra": "^11.3.0", + "@types/lodash": "^4.17.21", + "fs-extra": "^11.3.3", "jest-canvas-mock": "^2.5.2" }, "peerDependencies": { diff --git a/packages/react-charts/src/echarts/components/Line/examples/index.md b/packages/react-charts/src/echarts/components/Line/examples/index.md index bbbdb5abdd9..c440cf73d37 100644 --- a/packages/react-charts/src/echarts/components/Line/examples/index.md +++ b/packages/react-charts/src/echarts/components/Line/examples/index.md @@ -1,6 +1,7 @@ --- id: Line chart -section: charts +section: components +subsection: charts propComponents: [ { component: 'Charts', diff --git a/packages/react-charts/src/echarts/components/Sankey/examples/index.md b/packages/react-charts/src/echarts/components/Sankey/examples/index.md index ea47a3892eb..26670100e7f 100644 --- a/packages/react-charts/src/echarts/components/Sankey/examples/index.md +++ b/packages/react-charts/src/echarts/components/Sankey/examples/index.md @@ -1,6 +1,7 @@ --- id: Sankey chart -section: charts +section: components +subsection: charts propComponents: [ { component: 'Charts', diff --git a/packages/react-charts/src/victory/components/ChartArea/examples/ChartArea.md b/packages/react-charts/src/victory/components/ChartArea/examples/ChartArea.md index 1b1f7d9e1ee..8211352198d 100644 --- a/packages/react-charts/src/victory/components/ChartArea/examples/ChartArea.md +++ b/packages/react-charts/src/victory/components/ChartArea/examples/ChartArea.md @@ -1,6 +1,7 @@ --- id: Area chart -section: charts +section: components +subsection: charts propComponents: [ 'Chart', 'ChartArea', diff --git a/packages/react-charts/src/victory/components/ChartBar/examples/ChartBar.md b/packages/react-charts/src/victory/components/ChartBar/examples/ChartBar.md index d9364fdc094..29f57c11b9e 100644 --- a/packages/react-charts/src/victory/components/ChartBar/examples/ChartBar.md +++ b/packages/react-charts/src/victory/components/ChartBar/examples/ChartBar.md @@ -1,6 +1,7 @@ --- id: Bar chart -section: charts +section: components +subsection: charts propComponents: ['Chart', 'ChartAxis', 'ChartBar', 'ChartGroup', 'ChartLabel', 'ChartVoronoiContainer'] hideDarkMode: true --- diff --git a/packages/react-charts/src/victory/components/ChartBoxPlot/examples/ChartBoxPlot.md b/packages/react-charts/src/victory/components/ChartBoxPlot/examples/ChartBoxPlot.md index c6b1a37da37..cccdad20c88 100644 --- a/packages/react-charts/src/victory/components/ChartBoxPlot/examples/ChartBoxPlot.md +++ b/packages/react-charts/src/victory/components/ChartBoxPlot/examples/ChartBoxPlot.md @@ -1,6 +1,7 @@ --- id: Box plot chart -section: charts +section: components +subsection: charts propComponents: [ 'Chart', 'ChartAxis', diff --git a/packages/react-charts/src/victory/components/ChartBullet/examples/ChartBullet.md b/packages/react-charts/src/victory/components/ChartBullet/examples/ChartBullet.md index 32029b6b5c6..9484f65b26d 100644 --- a/packages/react-charts/src/victory/components/ChartBullet/examples/ChartBullet.md +++ b/packages/react-charts/src/victory/components/ChartBullet/examples/ChartBullet.md @@ -1,6 +1,7 @@ --- id: Bullet chart -section: charts +section: components +subsection: charts propComponents: [ 'ChartAxis', 'ChartBullet', diff --git a/packages/react-charts/src/victory/components/ChartCursorTooltip/__snapshots__/ChartCursorFlyout.test.tsx.snap b/packages/react-charts/src/victory/components/ChartCursorTooltip/__snapshots__/ChartCursorFlyout.test.tsx.snap index b655956fd5d..4eb9b6d341d 100644 --- a/packages/react-charts/src/victory/components/ChartCursorTooltip/__snapshots__/ChartCursorFlyout.test.tsx.snap +++ b/packages/react-charts/src/victory/components/ChartCursorTooltip/__snapshots__/ChartCursorFlyout.test.tsx.snap @@ -18,12 +18,12 @@ exports[`allows tooltip via container component 1`] = ` width="200" > - {(isUploadEnabled || emptyState) && !value ? ( + {isUploadEnabled || emptyState ? (
event.stopPropagation() // Prevents clicking TextArea from opening file dialog diff --git a/packages/react-code-editor/src/components/CodeEditor/__test__/__snapshots__/CodeEditor.test.tsx.snap b/packages/react-code-editor/src/components/CodeEditor/__test__/__snapshots__/CodeEditor.test.tsx.snap index 80f02fa90b6..2e567f30153 100644 --- a/packages/react-code-editor/src/components/CodeEditor/__test__/__snapshots__/CodeEditor.test.tsx.snap +++ b/packages/react-code-editor/src/components/CodeEditor/__test__/__snapshots__/CodeEditor.test.tsx.snap @@ -6,124 +6,166 @@ exports[`Matches snapshot with control buttons enabled 1`] = ` class="pf-v6-c-code-editor" > -
-
-
-
diff --git a/packages/react-code-editor/src/components/CodeEditor/examples/CodeEditor.md b/packages/react-code-editor/src/components/CodeEditor/examples/CodeEditor.md index 0c3d3681b5d..7fe074cf68c 100644 --- a/packages/react-code-editor/src/components/CodeEditor/examples/CodeEditor.md +++ b/packages/react-code-editor/src/components/CodeEditor/examples/CodeEditor.md @@ -14,6 +14,7 @@ import HashtagIcon from '@patternfly/react-icons/dist/esm/icons/hashtag-icon'; import MapIcon from '@patternfly/react-icons/dist/esm/icons/map-icon'; import MoonIcon from '@patternfly/react-icons/dist/esm/icons/moon-icon'; import PlayIcon from '@patternfly/react-icons/dist/esm/icons/play-icon'; +import FontIcon from '@patternfly/react-icons/dist/esm/icons/font-icon'; ## Examples diff --git a/packages/react-code-editor/src/components/CodeEditor/examples/CodeEditorConfigurationModal.tsx b/packages/react-code-editor/src/components/CodeEditor/examples/CodeEditorConfigurationModal.tsx index 1f5cc3d7bca..95b4e4e8516 100644 --- a/packages/react-code-editor/src/components/CodeEditor/examples/CodeEditorConfigurationModal.tsx +++ b/packages/react-code-editor/src/components/CodeEditor/examples/CodeEditorConfigurationModal.tsx @@ -2,8 +2,19 @@ import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon'; import MapIcon from '@patternfly/react-icons/dist/esm/icons/map-icon'; import MoonIcon from '@patternfly/react-icons/dist/esm/icons/moon-icon'; import HashtagIcon from '@patternfly/react-icons/dist/esm/icons/hashtag-icon'; +import FontIcon from '@patternfly/react-icons/dist/esm/icons/font-icon'; import { CodeEditor, CodeEditorControl } from '@patternfly/react-code-editor'; -import { Flex, FlexItem, Icon, Modal, ModalBody, ModalHeader, Switch, SwitchProps } from '@patternfly/react-core'; +import { + Flex, + FlexItem, + Icon, + Modal, + ModalBody, + ModalHeader, + NumberInput, + Switch, + SwitchProps +} from '@patternfly/react-core'; import { useState } from 'react'; interface ConfigModalItemProps { @@ -11,76 +22,105 @@ interface ConfigModalItemProps { icon?: React.ReactNode; /** Description of the configuration option. */ description: string; - /** Flag indicating whether the option is enabled or disabled. */ - isChecked?: SwitchProps['isChecked']; - /** onChange handler for the switch. */ - onChange?: SwitchProps['onChange']; /** Title of the configuration option. We assume that titles are unique. */ title: string; - /** Labels for the enabled and disabled states of the switch. */ - labels?: { - enabled: string; - disabled: string; - }; - /** Optional OUIA ID of the configuration option. Also used as a prefix for the ids of inner elements. */ - ouiaId?: string; + /** + * Optional ID of the configuration option. Also used as a prefix for the following elements: + * - `${id}-title` for the element which contains the title + * - `${id}-description` for the element which contains the description + */ + id?: string; + /** + * Slot to render inside the configuration modal. Remember to add `aria-labelledby` and `aria-describedby` props + * to the control inside the slot, pointing to the title and description ids respectively. + */ + slot?: React.ReactNode; } const ConfigModalItem: React.FunctionComponent = ({ icon = , description, - isChecked = false, - labels = { enabled: undefined, disabled: undefined }, - onChange, title, - ouiaId + id = `ConfigModalItem-${title.replace(/\s+/g, '-').toLowerCase()}`, + slot }) => ( {icon} - + {title} -
{description}
+
{description}
- + {slot} +
+); + +interface ConfigModalSwitchProps extends Omit { + /** Flag indicating whether the option is enabled or disabled. */ + isChecked?: SwitchProps['isChecked']; + /** onChange handler for the switch. */ + onChange?: SwitchProps['onChange']; + /** Labels for the enabled and disabled states of the switch. */ + labels?: { + enabled: string; + disabled: string; + }; +} + +const ConfigModalSwitch: React.FunctionComponent = ({ + icon = , + description, + title, + id = `ConfigModalSwitch-${title.replace(/\s+/g, '-').toLowerCase()}`, + isChecked = false, + onChange, + labels = { enabled: undefined, disabled: undefined } +}) => ( + - - + } + /> ); interface ConfigModalControlProps { - /** Array of configuration controls to be rendered inside the modal. */ - controls: ConfigModalItemProps[]; + /** Controls to be rendered inside the configuration modal. */ + children: React.ReactNode; /** Title of the configuration modal. */ title?: string; /** Description of the configuration modal. */ description?: string; - /** Optional OUIA ID of the configuration modal. Also used as a prefix for the ids of inner elements. */ + /** Optional ID of the configuration modal. Also used as a prefix for the ids of inner elements and the OUIA id. */ ouiaId?: string; } const ConfigModalControl: React.FunctionComponent = ({ - controls, + children, title = 'Editor settings', - description = 'Settings will be applied immediately', + description = 'Changes apply immediately', ouiaId = 'CodeEditorConfigurationModal' -}) => { +}: ConfigModalControlProps) => { const [isModalOpen, setIsModalOpen] = useState(false); return ( @@ -89,27 +129,21 @@ const ConfigModalControl: React.FunctionComponent = ({ aria-describedby={`${ouiaId}-body`} aria-labelledby={`${ouiaId}-title`} isOpen={isModalOpen} - onClose={() => setIsModalOpen(!isModalOpen)} + onClose={() => setIsModalOpen(false)} ouiaId={ouiaId} variant="small" > - {controls.map((control) => ( - - ))} + {children} } + isSettings onClick={() => setIsModalOpen(true)} tooltipProps={{ content: title, ariaLive: 'off' }} /> @@ -123,37 +157,62 @@ export const CodeEditorConfigurationModal: React.FunctionComponent = () => { const [isMinimapVisible, setIsMinimapVisible] = useState(true); const [isDarkTheme, setIsDarkTheme] = useState(false); const [isLineNumbersVisible, setIsLineNumbersVisible] = useState(true); + const [fontSize, setFontSize] = useState(14); const onChange = (code: string) => { setCode(code); }; const customControl = ( - setIsMinimapVisible(checked), - icon: - }, - { - title: 'Dark theme', - description: 'Switch the editor to a dark color theme', - isChecked: isDarkTheme, - onChange: (_e, checked) => setIsDarkTheme(checked), - icon: - }, - { - title: 'Line numbers', - description: 'Show line numbers to the left of each line of code', - isChecked: isLineNumbersVisible, - onChange: (_e, checked) => setIsLineNumbersVisible(checked), - icon: + + setIsMinimapVisible(checked)} + icon={} + /> + setIsDarkTheme(checked)} + icon={} + /> + setIsLineNumbersVisible(checked)} + icon={} + /> + } + slot={ + setFontSize((size) => Math.max(5, size - 1))} + onChange={(event) => setFontSize(Number((event.target as HTMLInputElement).value))} + onPlus={() => setFontSize((size) => size + 1)} + widthChars={2} + /> } - ]} - /> + /> + ); return ( @@ -165,6 +224,7 @@ export const CodeEditorConfigurationModal: React.FunctionComponent = () => { isLineNumbersVisible={isLineNumbersVisible} isMinimapVisible={isMinimapVisible} onChange={onChange} + options={{ fontSize }} /> ); }; diff --git a/packages/react-core/CHANGELOG.md b/packages/react-core/CHANGELOG.md index cbf18e13695..fbc529c7715 100644 --- a/packages/react-core/CHANGELOG.md +++ b/packages/react-core/CHANGELOG.md @@ -3,6 +3,136 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [6.5.0-prerelease.33](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.32...@patternfly/react-core@6.5.0-prerelease.33) (2026-02-18) + +### Features + +- **icons:** add swap support for mapped rh-ui icons ([#12245](https://github.com/patternfly/patternfly-react/issues/12245)) ([a81ce0e](https://github.com/patternfly/patternfly-react/commit/a81ce0e1b6fdd7a3e85cdd70e5bb667dc7d82801)) + +# [6.5.0-prerelease.32](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.31...@patternfly/react-core@6.5.0-prerelease.32) (2026-02-06) + +### Bug Fixes + +- update ToggleGroup examples to properly toggle options ([#12239](https://github.com/patternfly/patternfly-react/issues/12239)) ([4332de4](https://github.com/patternfly/patternfly-react/commit/4332de4bb67b03521f99a888715019eefb9a0868)), closes [#12234](https://github.com/patternfly/patternfly-react/issues/12234) + +# [6.5.0-prerelease.31](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.30...@patternfly/react-core@6.5.0-prerelease.31) (2026-02-06) + +### Bug Fixes + +- **LoginPage:** allow brand props passthrough make brand optional ([#12194](https://github.com/patternfly/patternfly-react/issues/12194)) ([86d09b5](https://github.com/patternfly/patternfly-react/commit/86d09b5bbc360ae076d3bbf41a6a8e04e824e4e4)) + +# [6.5.0-prerelease.30](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.29...@patternfly/react-core@6.5.0-prerelease.30) (2026-02-05) + +### Bug Fixes + +- **FileUploadField:** don't render empty dom node ([#12236](https://github.com/patternfly/patternfly-react/issues/12236)) ([57f5823](https://github.com/patternfly/patternfly-react/commit/57f5823f2f2eadbd6d2fa3ec4c8726bee72cefdf)) + +# [6.5.0-prerelease.29](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.28...@patternfly/react-core@6.5.0-prerelease.29) (2026-02-05) + +### Features + +- **TreeView:** add support for disabled TreeViewListItems ([#12140](https://github.com/patternfly/patternfly-react/issues/12140)) ([3514831](https://github.com/patternfly/patternfly-react/commit/35148312f5d9ce4e706ad36b801455fc987842bb)) + +# [6.5.0-prerelease.28](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.27...@patternfly/react-core@6.5.0-prerelease.28) (2026-01-12) + +**Note:** Version bump only for package @patternfly/react-core + +# [6.5.0-prerelease.27](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.26...@patternfly/react-core@6.5.0-prerelease.27) (2026-01-09) + +**Note:** Version bump only for package @patternfly/react-core + +# [6.5.0-prerelease.26](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.25...@patternfly/react-core@6.5.0-prerelease.26) (2026-01-09) + +### Bug Fixes + +- **Label:** render add variant labels as buttons ([#12192](https://github.com/patternfly/patternfly-react/issues/12192)) ([371d2c0](https://github.com/patternfly/patternfly-react/commit/371d2c09d463e03def196dba354ae9b856e9d4ac)) + +# [6.5.0-prerelease.25](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.24...@patternfly/react-core@6.5.0-prerelease.25) (2025-12-17) + +### Features + +- **ClipboardCopy:** added textinput callbacks and props ([#12180](https://github.com/patternfly/patternfly-react/issues/12180)) ([4f5dcda](https://github.com/patternfly/patternfly-react/commit/4f5dcdae381bb12c9f728716e2be4aa5a8c67cb2)) + +# [6.5.0-prerelease.24](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.21...@patternfly/react-core@6.5.0-prerelease.24) (2025-12-16) + +### Bug Fixes + +- **deps:** update dependency focus-trap to v7.6.6 ([#12102](https://github.com/patternfly/patternfly-react/issues/12102)) ([06af4c5](https://github.com/patternfly/patternfly-react/commit/06af4c503674e3fdb07819b75b53a257a7822579)) +- **DrawerPanelContent:** add inert when drawer is closed ([#12027](https://github.com/patternfly/patternfly-react/issues/12027)) ([8928745](https://github.com/patternfly/patternfly-react/commit/8928745fb7364828ed4d581f30f01146bdc9990e)) +- **Drawer:** update refs in examples ([#12152](https://github.com/patternfly/patternfly-react/issues/12152)) ([2767ce0](https://github.com/patternfly/patternfly-react/commit/2767ce0896ef22bbfea5839905ee5cfedac86a80)) +- fixes broken docs link. ([#12157](https://github.com/patternfly/patternfly-react/issues/12157)) ([80b72de](https://github.com/patternfly/patternfly-react/commit/80b72de8ba9a326375cd0919ff5b81ff5b1ebf7c)) +- **form:** keep required asterisk from orphaning ([#11961](https://github.com/patternfly/patternfly-react/issues/11961)) ([d11c186](https://github.com/patternfly/patternfly-react/commit/d11c186313c9820e674fe1cdce7d630b57a62755)) +- **layouts:** update example css & classnames ([#12145](https://github.com/patternfly/patternfly-react/issues/12145)) ([11fe19a](https://github.com/patternfly/patternfly-react/commit/11fe19a02297d85f50e11375820430b2980bc47b)) +- **Wizard:** Fix crash in nav when first sub-step is hidden ([#12166](https://github.com/patternfly/patternfly-react/issues/12166)) ([3730dcf](https://github.com/patternfly/patternfly-react/commit/3730dcfe2a3f35050ba7e39fed16ead322b39521)) + +### Features + +- **Compass:** add isPill to Drawer by default ([#12151](https://github.com/patternfly/patternfly-react/issues/12151)) ([b2cdf1c](https://github.com/patternfly/patternfly-react/commit/b2cdf1cd5d95be91932e6ac11f39e2c0318e6f72)) +- **docked nav:** add support for docked nav layout ([#12175](https://github.com/patternfly/patternfly-react/issues/12175)) ([5519389](https://github.com/patternfly/patternfly-react/commit/55193896b899aa1e2b8dfbbddd644507988ee31f)) + +# [6.5.0-prerelease.21](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.20...@patternfly/react-core@6.5.0-prerelease.21) (2025-12-03) + +**Note:** Version bump only for package @patternfly/react-core + +# [6.5.0-prerelease.20](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.19...@patternfly/react-core@6.5.0-prerelease.20) (2025-12-03) + +**Note:** Version bump only for package @patternfly/react-core + +# [6.5.0-prerelease.19](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.18...@patternfly/react-core@6.5.0-prerelease.19) (2025-12-01) + +### Features + +- **Dropdown:** Add optional container with ouiaId ([#12022](https://github.com/patternfly/patternfly-react/issues/12022)) ([0811452](https://github.com/patternfly/patternfly-react/commit/0811452f2720872e19d08a3781d4399368745dc8)) +- **ExpandableSection:** Allow more control over toggle icon ([#12051](https://github.com/patternfly/patternfly-react/issues/12051)) ([07c2bbf](https://github.com/patternfly/patternfly-react/commit/07c2bbfb32491c8f69dfa4261c9e1edb9136d487)) + +# [6.5.0-prerelease.18](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.17...@patternfly/react-core@6.5.0-prerelease.18) (2025-11-21) + +**Note:** Version bump only for package @patternfly/react-core + +# [6.5.0-prerelease.17](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.16...@patternfly/react-core@6.5.0-prerelease.17) (2025-11-20) + +**Note:** Version bump only for package @patternfly/react-core + +# [6.5.0-prerelease.16](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.15...@patternfly/react-core@6.5.0-prerelease.16) (2025-11-20) + +### Features + +- **Compass:** add CompassMainFooter ([#12137](https://github.com/patternfly/patternfly-react/issues/12137)) ([1da6fc2](https://github.com/patternfly/patternfly-react/commit/1da6fc2fb7d1f09ca31cc95069dab87c587639f5)) + +# [6.5.0-prerelease.15](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.14...@patternfly/react-core@6.5.0-prerelease.15) (2025-11-19) + +### Features + +- **Compass:** add compass nav components ([#12138](https://github.com/patternfly/patternfly-react/issues/12138)) ([4bd98bc](https://github.com/patternfly/patternfly-react/commit/4bd98bc297261f79e4a4a7d351e336cf26a526e4)) + +# [6.5.0-prerelease.14](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.13...@patternfly/react-core@6.5.0-prerelease.14) (2025-11-17) + +### Bug Fixes + +- **Compass:** get props tables rendering correctly ([#12142](https://github.com/patternfly/patternfly-react/issues/12142)) ([399cbc4](https://github.com/patternfly/patternfly-react/commit/399cbc45844152083d0160d63f953aa7f29479d3)) + +# [6.5.0-prerelease.13](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.12...@patternfly/react-core@6.5.0-prerelease.13) (2025-11-14) + +### Bug Fixes + +- Moves items to new nav. ([#12013](https://github.com/patternfly/patternfly-react/issues/12013)) ([ddd0696](https://github.com/patternfly/patternfly-react/commit/ddd0696796134c7d0f9583ce56e26b0df47156cb)) + +### Features + +- **Compass:** updated mainheader structure to be composable ([#12135](https://github.com/patternfly/patternfly-react/issues/12135)) ([6bcdcaf](https://github.com/patternfly/patternfly-react/commit/6bcdcaf116645aad4c9c0d98ca61045976a84946)) + +# [6.5.0-prerelease.12](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.11...@patternfly/react-core@6.5.0-prerelease.12) (2025-11-12) + +### Features + +- **Hero:** added component ([#12131](https://github.com/patternfly/patternfly-react/issues/12131)) ([8da87a2](https://github.com/patternfly/patternfly-react/commit/8da87a2615bb6cfc350a52677ec6bdae335bb6f8)) + +# [6.5.0-prerelease.11](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.10...@patternfly/react-core@6.5.0-prerelease.11) (2025-11-10) + +### Bug Fixes + +- Moved compass component to generative ui. ([#12133](https://github.com/patternfly/patternfly-react/issues/12133)) ([bb46849](https://github.com/patternfly/patternfly-react/commit/bb4684986bbd3fdb879aa0ff60e2747150bd3722)) + # [6.5.0-prerelease.10](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@6.5.0-prerelease.9...@patternfly/react-core@6.5.0-prerelease.10) (2025-11-10) ### Bug Fixes diff --git a/packages/react-core/README.md b/packages/react-core/README.md index a00c99ac118..cdeddc0a630 100644 --- a/packages/react-core/README.md +++ b/packages/react-core/README.md @@ -37,7 +37,7 @@ import '@patternfly/react-core/dist/styles/base.css'; #### Example component usage -```js +```ts import { Button } from '@patternfly/react-core'; export default ; diff --git a/packages/react-core/package.json b/packages/react-core/package.json index acc37f25e74..dee08d8e603 100644 --- a/packages/react-core/package.json +++ b/packages/react-core/package.json @@ -1,6 +1,6 @@ { "name": "@patternfly/react-core", - "version": "6.5.0-prerelease.10", + "version": "6.5.0-prerelease.33", "description": "This library provides a set of common React components for use with the PatternFly reference implementation.", "main": "dist/js/index.js", "module": "dist/esm/index.js", @@ -49,15 +49,15 @@ "@patternfly/react-icons": "workspace:^", "@patternfly/react-styles": "workspace:^", "@patternfly/react-tokens": "workspace:^", - "focus-trap": "7.6.4", + "focus-trap": "7.6.6", "react-dropzone": "^14.3.5", "tslib": "^2.8.1" }, "devDependencies": { - "@patternfly/patternfly": "6.5.0-prerelease.19", + "@patternfly/patternfly": "6.5.0-prerelease.41", "case-anything": "^3.1.2", "css": "^3.0.0", - "fs-extra": "^11.3.0" + "fs-extra": "^11.3.3" }, "peerDependencies": { "react": "^17 || ^18 || ^19", diff --git a/packages/react-core/src/components/AboutModal/__tests__/__snapshots__/AboutModalBoxCloseButton.test.tsx.snap b/packages/react-core/src/components/AboutModal/__tests__/__snapshots__/AboutModalBoxCloseButton.test.tsx.snap index 2d721cf9cf6..3c2698b2529 100644 --- a/packages/react-core/src/components/AboutModal/__tests__/__snapshots__/AboutModalBoxCloseButton.test.tsx.snap +++ b/packages/react-core/src/components/AboutModal/__tests__/__snapshots__/AboutModalBoxCloseButton.test.tsx.snap @@ -22,12 +22,24 @@ exports[`AboutModalBoxCloseButton Test 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 352 512" width="1em" > - + + + + + + @@ -57,12 +69,24 @@ exports[`AboutModalBoxCloseButton Test close button aria label 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 352 512" width="1em" > - + + + + + + @@ -92,12 +116,24 @@ exports[`AboutModalBoxCloseButton Test onclose 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 352 512" width="1em" > - + + + + + + diff --git a/packages/react-core/src/components/Accordion/AccordionToggle.tsx b/packages/react-core/src/components/Accordion/AccordionToggle.tsx index 64bdc846455..f82aa1de125 100644 --- a/packages/react-core/src/components/Accordion/AccordionToggle.tsx +++ b/packages/react-core/src/components/Accordion/AccordionToggle.tsx @@ -4,8 +4,10 @@ import styles from '@patternfly/react-styles/css/components/Accordion/accordion' import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon'; import { AccordionContext, AccordionItemContext } from './AccordionContext'; -export interface AccordionToggleProps - extends React.DetailedHTMLProps, HTMLButtonElement> { +export interface AccordionToggleProps extends React.DetailedHTMLProps< + React.ButtonHTMLAttributes, + HTMLButtonElement +> { /** Content rendered inside the Accordion toggle */ children?: React.ReactNode; /** Additional classes added to the Accordion Toggle */ diff --git a/packages/react-core/src/components/Alert/__tests__/Generated/__snapshots__/AlertActionCloseButton.test.tsx.snap b/packages/react-core/src/components/Alert/__tests__/Generated/__snapshots__/AlertActionCloseButton.test.tsx.snap index 9ebc77452b1..39d5b531958 100644 --- a/packages/react-core/src/components/Alert/__tests__/Generated/__snapshots__/AlertActionCloseButton.test.tsx.snap +++ b/packages/react-core/src/components/Alert/__tests__/Generated/__snapshots__/AlertActionCloseButton.test.tsx.snap @@ -19,12 +19,24 @@ exports[`AlertActionCloseButton should match snapshot 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 352 512" width="1em" > - + + + + + + diff --git a/packages/react-core/src/components/Alert/__tests__/Generated/__snapshots__/AlertIcon.test.tsx.snap b/packages/react-core/src/components/Alert/__tests__/Generated/__snapshots__/AlertIcon.test.tsx.snap index 00a41983f34..ba38160093a 100644 --- a/packages/react-core/src/components/Alert/__tests__/Generated/__snapshots__/AlertIcon.test.tsx.snap +++ b/packages/react-core/src/components/Alert/__tests__/Generated/__snapshots__/AlertIcon.test.tsx.snap @@ -11,12 +11,24 @@ exports[`AlertIcon should match snapshot (auto-generated) 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" width="1em" > - + + + + + +
diff --git a/packages/react-core/src/components/Alert/__tests__/__snapshots__/AlertActionCloseButton.test.tsx.snap b/packages/react-core/src/components/Alert/__tests__/__snapshots__/AlertActionCloseButton.test.tsx.snap index 98f1594e1b4..2568762c47f 100644 --- a/packages/react-core/src/components/Alert/__tests__/__snapshots__/AlertActionCloseButton.test.tsx.snap +++ b/packages/react-core/src/components/Alert/__tests__/__snapshots__/AlertActionCloseButton.test.tsx.snap @@ -28,12 +28,24 @@ exports[`Matches the snapshot 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 352 512" width="1em" > - + + + + + + diff --git a/packages/react-core/src/components/Alert/examples/AlertAsyncLiveRegion.tsx b/packages/react-core/src/components/Alert/examples/AlertAsyncLiveRegion.tsx index 9380ff4d4e2..51553032071 100644 --- a/packages/react-core/src/components/Alert/examples/AlertAsyncLiveRegion.tsx +++ b/packages/react-core/src/components/Alert/examples/AlertAsyncLiveRegion.tsx @@ -42,13 +42,7 @@ export const AsyncLiveRegionAlert: React.FunctionComponent = () => { text="Async alerts on" buttonId="async-alerts-on" isSelected={isActive} - onChange={() => setIsActive(true)} - /> - setIsActive(false)} + onChange={() => setIsActive(!isActive)} /> diff --git a/packages/react-core/src/components/Avatar/Avatar.tsx b/packages/react-core/src/components/Avatar/Avatar.tsx index f7108e8c33e..1f1c2f2cde5 100644 --- a/packages/react-core/src/components/Avatar/Avatar.tsx +++ b/packages/react-core/src/components/Avatar/Avatar.tsx @@ -1,8 +1,10 @@ import styles from '@patternfly/react-styles/css/components/Avatar/avatar'; import { css } from '@patternfly/react-styles'; -export interface AvatarProps - extends React.DetailedHTMLProps, HTMLImageElement> { +export interface AvatarProps extends React.DetailedHTMLProps< + React.ImgHTMLAttributes, + HTMLImageElement +> { /** Additional classes added to the avatar. */ className?: string; /** Attribute that specifies the URL of the image for the avatar. */ diff --git a/packages/react-core/src/components/BackToTop/__tests__/__snapshots__/BackToTop.test.tsx.snap b/packages/react-core/src/components/BackToTop/__tests__/__snapshots__/BackToTop.test.tsx.snap index 0905c8f41cd..5836bd168ee 100644 --- a/packages/react-core/src/components/BackToTop/__tests__/__snapshots__/BackToTop.test.tsx.snap +++ b/packages/react-core/src/components/BackToTop/__tests__/__snapshots__/BackToTop.test.tsx.snap @@ -31,12 +31,24 @@ exports[`Matches the snapshot 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 320 512" width="1em" > - + + + + + + diff --git a/packages/react-core/src/components/Brand/Brand.tsx b/packages/react-core/src/components/Brand/Brand.tsx index 89186f369c6..02c3695a9d4 100644 --- a/packages/react-core/src/components/Brand/Brand.tsx +++ b/packages/react-core/src/components/Brand/Brand.tsx @@ -4,8 +4,10 @@ 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> { +export interface BrandProps extends React.DetailedHTMLProps< + React.ImgHTMLAttributes, + HTMLImageElement +> { /** Transforms the Brand into a element from an element. Container for child elements. */ children?: React.ReactNode; /** Additional classes added to the either type of Brand. */ diff --git a/packages/react-core/src/components/Breadcrumb/__tests__/__snapshots__/Breadcrumb.test.tsx.snap b/packages/react-core/src/components/Breadcrumb/__tests__/__snapshots__/Breadcrumb.test.tsx.snap index c0ef331e2a7..b3fca635b5a 100644 --- a/packages/react-core/src/components/Breadcrumb/__tests__/__snapshots__/Breadcrumb.test.tsx.snap +++ b/packages/react-core/src/components/Breadcrumb/__tests__/__snapshots__/Breadcrumb.test.tsx.snap @@ -53,12 +53,24 @@ exports[`Breadcrumb component should render breadcrumb with children 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 256 512" width="1em" > - + + + + + + - + + + + + + diff --git a/packages/react-core/src/components/Checkbox/Checkbox.tsx b/packages/react-core/src/components/Checkbox/Checkbox.tsx index ed91f65dd9a..5ad9d25e653 100644 --- a/packages/react-core/src/components/Checkbox/Checkbox.tsx +++ b/packages/react-core/src/components/Checkbox/Checkbox.tsx @@ -7,8 +7,7 @@ import { getUniqueId } from '../../helpers/util'; import { ASTERISK } from '../../helpers/htmlConstants'; export interface CheckboxProps - extends Omit, 'type' | 'onChange' | 'disabled' | 'label'>, - OUIAProps { + extends Omit, 'type' | 'onChange' | 'disabled' | 'label'>, OUIAProps { /** Additional classes added to the checkbox wrapper. This wrapper will be div element by default. It will be a label element if * isLabelWrapped is true, or it can be overridden by any element specified in the component prop. */ diff --git a/packages/react-core/src/components/ClipboardCopy/ClipboardCopy.tsx b/packages/react-core/src/components/ClipboardCopy/ClipboardCopy.tsx index 453a5ad4556..1e67e4c3b00 100644 --- a/packages/react-core/src/components/ClipboardCopy/ClipboardCopy.tsx +++ b/packages/react-core/src/components/ClipboardCopy/ClipboardCopy.tsx @@ -55,6 +55,10 @@ export interface ClipboardCopyProps extends Omit textAriaLabel?: string; /** Aria-label to use on the ClipboardCopyToggle. */ toggleAriaLabel?: string; + /** ID to use on the TextInput. */ + inputId?: string; + /** Name attribute to use on the TextInput. */ + inputName?: string; /** Flag to show if the input is read only. */ isReadOnly?: boolean; /** Flag to determine if clipboard copy is in the expanded state initially */ @@ -91,6 +95,10 @@ export interface ClipboardCopyProps extends Omit onCopy?: (event: React.ClipboardEvent, text?: React.ReactNode) => void; /** A function that is triggered on changing the text. */ onChange?: (event: React.FormEvent, text?: string) => void; + /** Callback function when text input is focused */ + onInputFocus?: (event?: any) => void; + /** Callback function when text input is blurred (focus leaves) */ + onInputBlur?: (event?: any) => void; /** The text which is copied. */ children: string | string[]; /** Additional actions for inline clipboard copy. Should be wrapped with ClipboardCopyAction. */ @@ -177,6 +185,8 @@ class ClipboardCopy extends Component { /* eslint-disable @typescript-eslint/no-unused-vars */ isExpanded, onChange, // Don't pass to
+ onInputFocus, // Don't pass to
+ onInputBlur, // Don't pass to
/* eslint-enable @typescript-eslint/no-unused-vars */ isReadOnly, isCode, @@ -189,6 +199,8 @@ class ClipboardCopy extends Component { clickTip, textAriaLabel, toggleAriaLabel, + inputId, + inputName, variant, position, className, @@ -295,8 +307,11 @@ class ClipboardCopy extends Component { readOnlyVariant={isReadOnly || this.state.expanded ? 'default' : undefined} onChange={this.updateText} value={this.state.expanded ? this.state.textWhenExpanded : copyableText} - id={`text-input-${id}`} + id={inputId ?? `text-input-${id}`} + name={inputName} aria-label={textAriaLabel} + onFocus={onInputFocus} + onBlur={onInputBlur} {...(isCode && { dir: 'ltr' })} /> , HTMLButtonElement>, 'ref'> { +export interface ClipboardCopyButtonProps extends Omit< + React.DetailedHTMLProps, HTMLButtonElement>, + 'ref' +> { /** Callback for the copy when the button is clicked */ onClick: (event: React.MouseEvent) => void; /** Content of the copy button */ diff --git a/packages/react-core/src/components/ClipboardCopy/ClipboardCopyToggle.tsx b/packages/react-core/src/components/ClipboardCopy/ClipboardCopyToggle.tsx index e0a29c9537e..0eb90a65dc0 100644 --- a/packages/react-core/src/components/ClipboardCopy/ClipboardCopyToggle.tsx +++ b/packages/react-core/src/components/ClipboardCopy/ClipboardCopyToggle.tsx @@ -3,8 +3,10 @@ import { css } from '@patternfly/react-styles'; import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon'; import { Button } from '../Button'; -export interface ClipboardCopyToggleProps - extends Omit, HTMLButtonElement>, 'ref'> { +export interface ClipboardCopyToggleProps extends Omit< + React.DetailedHTMLProps, HTMLButtonElement>, + 'ref' +> { onClick: (event: React.MouseEvent) => void; id: string; contentId: string; diff --git a/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopy.test.tsx b/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopy.test.tsx index 1b7b51f0637..f014f4ba6bb 100644 --- a/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopy.test.tsx +++ b/packages/react-core/src/components/ClipboardCopy/__tests__/ClipboardCopy.test.tsx @@ -309,6 +309,18 @@ test('Passes textAriaLabel to TextInput', () => { expect(screen.getByRole('textbox')).toHaveAccessibleName('text label'); }); +test('Passes inputId to TextInput', () => { + render({children}); + + expect(screen.getByRole('textbox')).toHaveAttribute('id', 'custom-input-id'); +}); + +test('Passes inputName to TextInput', () => { + render({children}); + + expect(screen.getByRole('textbox')).toHaveAttribute('name', 'custom-input-name'); +}); + test('Calls onChange when ClipboardCopy textinput is typed in', async () => { const onChangeMock = jest.fn(); const user = userEvent.setup(); @@ -338,6 +350,66 @@ test('Does not call onChange when ClipboardCopy textinput is not typed in', asyn expect(onChangeMock).not.toHaveBeenCalled(); }); +test('Calls onFocus when ClipboardCopy textinput is focused', async () => { + const onFocusMock = jest.fn(); + const user = userEvent.setup(); + + render({children}); + + await user.click(screen.getByRole('textbox')); + + expect(onFocusMock).toHaveBeenCalledTimes(1); +}); + +test('Does not call onFocus when ClipboardCopy textinput is not focused', async () => { + const onFocusMock = jest.fn(); + const user = userEvent.setup(); + + render( + <> + {children} + + + ); + + await user.click(screen.getByRole('textbox', { name: 'native input' })); + + expect(onFocusMock).not.toHaveBeenCalled(); +}); + +test('Calls onBlur when ClipboardCopy textinput loses focus', async () => { + const onBlurMock = jest.fn(); + const user = userEvent.setup(); + + render( + <> + {children} + + + ); + + await user.click(screen.getByRole('textbox', { name: 'Copyable input' })); + await user.click(screen.getByRole('textbox', { name: 'native input' })); + + expect(onBlurMock).toHaveBeenCalledTimes(1); +}); + +test('Does not call onBlur when ClipboardCopy textinput does not lose focus', async () => { + const onBlurMock = jest.fn(); + const user = userEvent.setup(); + + render( + <> + {children} + + + ); + + await user.click(screen.getByRole('textbox', { name: 'native input' })); + + expect(onBlurMock).not.toHaveBeenCalled(); +}); + test('Calls onCopy when ClipboardCopyButton is clicked', async () => { const onCopyMock = jest.fn(); const user = userEvent.setup(); 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 index ca7c7931a23..e936ecf1237 100644 --- 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 @@ -18,7 +18,7 @@ exports[`Matches snapshot 1`] = ` - + + + + + + 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 d4baa3e8223..a98d535dc3d 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 @@ -23,12 +23,24 @@ exports[`Matches snapshot 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 256 512" width="1em" > - + + + + + +
diff --git a/packages/react-core/src/components/Compass/Compass.tsx b/packages/react-core/src/components/Compass/Compass.tsx index 14b94635691..619d034d4ac 100644 --- a/packages/react-core/src/components/Compass/Compass.tsx +++ b/packages/react-core/src/components/Compass/Compass.tsx @@ -6,8 +6,10 @@ import compassBackgroundImageLight from '@patternfly/react-tokens/dist/esm/c_com import compassBackgroundImageDark from '@patternfly/react-tokens/dist/esm/c_compass_BackgroundImage_dark'; export interface CompassProps extends React.HTMLProps { - /** Additional classes added to the compass. */ + /** Additional classes added to the Compass. */ className?: string; + /** Content of the docked navigation area of the layout */ + dock?: React.ReactNode; /** Content placed at the top of the layout */ header?: React.ReactNode; /** Flag indicating if the header is expanded */ @@ -30,14 +32,15 @@ export interface CompassProps extends React.HTMLProps { drawerContent?: React.ReactNode; /** Additional props passed to the drawer */ drawerProps?: DrawerProps; - /** Light theme background image path of the compass */ + /** Light theme background image path of the Compass */ backgroundSrcLight?: string; - /** Dark theme background image path of the compass */ + /** Dark theme background image path of the Compass */ backgroundSrcDark?: string; } export const Compass: React.FunctionComponent = ({ className, + dock, header, isHeaderExpanded = true, sidebarStart, @@ -52,7 +55,7 @@ export const Compass: React.FunctionComponent = ({ backgroundSrcLight, backgroundSrcDark, ...props -}) => { +}: CompassProps) => { const hasDrawer = drawerContent !== undefined; const backgroundImageStyles: { [key: string]: string } = {}; @@ -64,38 +67,51 @@ export const Compass: React.FunctionComponent = ({ } const compassContent = ( -
-
- {header} -
-
- {sidebarStart} -
-
{main}
-
- {sidebarEnd} -
-
- {footer} -
+
+ {dock &&
{dock}
} + {header && ( +
+ {header} +
+ )} + {sidebarStart && ( +
+ {sidebarStart} +
+ )} + {main &&
{main}
} + {sidebarEnd && ( +
+ {sidebarEnd} +
+ )} + {footer && ( +
+ {footer} +
+ )}
); if (hasDrawer) { return ( - + {compassContent} diff --git a/packages/react-core/src/components/Compass/CompassContent.tsx b/packages/react-core/src/components/Compass/CompassContent.tsx index 09b3dc1cb19..960c5e52a34 100644 --- a/packages/react-core/src/components/Compass/CompassContent.tsx +++ b/packages/react-core/src/components/Compass/CompassContent.tsx @@ -2,8 +2,8 @@ import { Drawer, DrawerContent, DrawerProps } from '../Drawer'; import styles from '@patternfly/react-styles/css/components/Compass/compass'; import { css } from '@patternfly/react-styles'; -interface CompassContentProps extends React.HTMLProps { - /** Content of the main compass area. Typically one or more CompassPanel components. */ +export interface CompassContentProps extends React.HTMLProps { + /** Content of the main Compass area. Typically one or more CompassPanel components. */ children: React.ReactNode; /** Additional classes added to the CompassContent */ className?: string; @@ -19,7 +19,7 @@ export const CompassContent: React.FunctionComponent = ({ drawerProps, drawerContent, ...props -}) => { +}: CompassContentProps) => { const hasDrawer = drawerContent !== undefined; const compassContent = ( @@ -30,7 +30,7 @@ export const CompassContent: React.FunctionComponent = ({ if (hasDrawer) { return ( - + {compassContent} ); diff --git a/packages/react-core/src/components/Compass/CompassHeader.tsx b/packages/react-core/src/components/Compass/CompassHeader.tsx index c249c1d8858..7de33925245 100644 --- a/packages/react-core/src/components/Compass/CompassHeader.tsx +++ b/packages/react-core/src/components/Compass/CompassHeader.tsx @@ -1,7 +1,7 @@ import styles from '@patternfly/react-styles/css/components/Compass/compass'; import { css } from '@patternfly/react-styles'; -interface CompassHeaderProps { +export interface CompassHeaderProps { /** Content of the logo area */ logo?: React.ReactNode; /** Content of the navigation area */ @@ -10,7 +10,11 @@ interface CompassHeaderProps { profile?: React.ReactNode; } -export const CompassHeader: React.FunctionComponent = ({ logo, nav, profile }) => ( +export const CompassHeader: React.FunctionComponent = ({ + logo, + nav, + profile +}: CompassHeaderProps) => ( <>
{logo}
{nav}
diff --git a/packages/react-core/src/components/Compass/CompassHero.tsx b/packages/react-core/src/components/Compass/CompassHero.tsx index 0987c25277e..ce57d73ccfe 100644 --- a/packages/react-core/src/components/Compass/CompassHero.tsx +++ b/packages/react-core/src/components/Compass/CompassHero.tsx @@ -1,87 +1,22 @@ import styles from '@patternfly/react-styles/css/components/Compass/compass'; import { css } from '@patternfly/react-styles'; -import compassHeroBackgroundImageLight from '@patternfly/react-tokens/dist/esm/c_compass__hero_BackgroundImage_light'; -import compassHeroBackgroundImageDark from '@patternfly/react-tokens/dist/esm/c_compass__hero_BackgroundImage_dark'; -import compassHeroGradientStop1Light from '@patternfly/react-tokens/dist/esm/c_compass__hero_gradient_stop_1_light'; -import compassHeroGradientStop2Light from '@patternfly/react-tokens/dist/esm/c_compass__hero_gradient_stop_2_light'; -import compassHeroGradientStop3Light from '@patternfly/react-tokens/dist/esm/c_compass__hero_gradient_stop_3_light'; -import compassHeroGradientStop1Dark from '@patternfly/react-tokens/dist/esm/c_compass__hero_gradient_stop_1_dark'; -import compassHeroGradientStop2Dark from '@patternfly/react-tokens/dist/esm/c_compass__hero_gradient_stop_2_dark'; -import compassHeroGradientStop3Dark from '@patternfly/react-tokens/dist/esm/c_compass__hero_gradient_stop_3_dark'; - -interface CompassHeroProps extends Omit, 'content'> { +/** A wrapper component to pass a PatternFly Hero component into. */ +export interface CompassHeroProps extends Omit, 'content'> { /** Content of the hero */ children?: React.ReactNode; /** Additional classes added to the hero */ className?: string; - /** Light theme background image path of the hero */ - backgroundSrcLight?: string; - /** Dark theme background image path of the hero */ - backgroundSrcDark?: string; - /** Light theme gradient of the hero */ - gradientLight?: { - stop1?: string; - stop2?: string; - stop3?: string; - }; - /** Dark theme gradient of the hero */ - gradientDark?: { - stop1?: string; - stop2?: string; - stop3?: string; - }; } export const CompassHero: React.FunctionComponent = ({ className, children, - backgroundSrcLight, - backgroundSrcDark, - gradientLight, - gradientDark, ...props -}) => { - const backgroundImageStyles: { [key: string]: string } = {}; - if (backgroundSrcLight) { - backgroundImageStyles[compassHeroBackgroundImageLight.name] = `url(${backgroundSrcLight})`; - } - if (backgroundSrcDark) { - backgroundImageStyles[compassHeroBackgroundImageDark.name] = `url(${backgroundSrcDark})`; - } - - if (gradientLight) { - if (gradientLight.stop1) { - backgroundImageStyles[compassHeroGradientStop1Light.name] = gradientLight.stop1; - } - if (gradientLight.stop2) { - backgroundImageStyles[compassHeroGradientStop2Light.name] = gradientLight.stop2; - } - if (gradientLight.stop3) { - backgroundImageStyles[compassHeroGradientStop3Light.name] = gradientLight.stop3; - } - } - if (gradientDark) { - if (gradientDark.stop1) { - backgroundImageStyles[compassHeroGradientStop1Dark.name] = gradientDark.stop1; - } - if (gradientDark.stop2) { - backgroundImageStyles[compassHeroGradientStop2Dark.name] = gradientDark.stop2; - } - if (gradientDark.stop3) { - backgroundImageStyles[compassHeroGradientStop3Dark.name] = gradientDark.stop3; - } - } - - return ( -
-
{children}
-
- ); -}; +}: CompassHeroProps) => ( +
+ {children} +
+); CompassHero.displayName = 'CompassHero'; diff --git a/packages/react-core/src/components/Compass/CompassMainFooter.tsx b/packages/react-core/src/components/Compass/CompassMainFooter.tsx new file mode 100644 index 00000000000..cbbd60f663f --- /dev/null +++ b/packages/react-core/src/components/Compass/CompassMainFooter.tsx @@ -0,0 +1,24 @@ +import styles from '@patternfly/react-styles/css/components/Compass/compass'; +import { css } from '@patternfly/react-styles'; + +interface CompassMainFooterProps extends Omit, 'title'> { + /** Additional classes added to the main footer */ + className?: string; + /** Main footer content */ + children?: React.ReactNode; + /** Indicates if the main footer is expanded */ + isExpanded?: boolean; +} + +export const CompassMainFooter: React.FunctionComponent = ({ + className, + children, + isExpanded = true, + ...props +}) => ( +
+ {children} +
+); + +CompassMainFooter.displayName = 'CompassMainFooter'; diff --git a/packages/react-core/src/components/Compass/CompassMainHeader.tsx b/packages/react-core/src/components/Compass/CompassMainHeader.tsx index 409f7b2e454..9e52c8e172a 100644 --- a/packages/react-core/src/components/Compass/CompassMainHeader.tsx +++ b/packages/react-core/src/components/Compass/CompassMainHeader.tsx @@ -1,17 +1,27 @@ -import { Flex, FlexItem } from '../../layouts/Flex'; -import { CompassPanel } from './CompassPanel'; +import { CompassPanel, CompassPanelProps } from './CompassPanel'; +import { CompassMainHeaderContent } from './CompassMainHeaderContent'; +import { CompassMainHeaderTitle } from './CompassMainHeaderTitle'; +import { CompassMainHeaderToolbar } from './CompassMainHeaderToolbar'; import styles from '@patternfly/react-styles/css/components/Compass/compass'; import { css } from '@patternfly/react-styles'; -interface CompassMainHeaderProps extends Omit, 'title'> { +/** The wrapper component for header content in the main Compass area. When building out a custom implementation, + * you should ensure any content within the main header is rendered inside a Compass panel and main header content wrappers. + */ + +export interface CompassMainHeaderProps extends Omit, 'title'> { + /** Custom main header content. To opt into a default styling, use the title and toolbar props instead. */ + children?: React.ReactNode; /** Additional classes added to the main header */ className?: string; /** Styled title. If title or toolbar is provided, the children will be ignored. */ title?: React.ReactNode; /** Styled toolbar. If title or toolbar is provided, the children will be ignored. */ toolbar?: React.ReactNode; - /** Custom main header content. To opt into a default styling, use the title and toolbar props instead. */ - children?: React.ReactNode; + /** Additional props passed to the Compass panel that wraps the main header content when using the title or toolbar props. When using the + * children prop, you should pass your own Compass panel. + */ + compassPanelProps?: Omit; } export const CompassMainHeader: React.FunctionComponent = ({ @@ -19,15 +29,16 @@ export const CompassMainHeader: React.FunctionComponent title, toolbar, children, + compassPanelProps, ...props -}) => { +}: CompassMainHeaderProps) => { const _content = title !== undefined || toolbar !== undefined ? ( - - - {title} - {toolbar && {toolbar}} - + + + {title && {title}} + {toolbar && {toolbar}} + ) : ( children diff --git a/packages/react-core/src/components/Compass/CompassMainHeaderContent.tsx b/packages/react-core/src/components/Compass/CompassMainHeaderContent.tsx new file mode 100644 index 00000000000..590ecd25427 --- /dev/null +++ b/packages/react-core/src/components/Compass/CompassMainHeaderContent.tsx @@ -0,0 +1,25 @@ +import styles from '@patternfly/react-styles/css/components/Compass/compass'; +import { css } from '@patternfly/react-styles'; + +/** A wrapper component to be passed as custom content for the Compass main header. This should also be wrapped + * in a Compass panel component. + */ + +export interface CompassMainHeaderContentProps extends React.HTMLProps { + /** Content of the main header content. */ + children: React.ReactNode; + /** Additional classes added to the main header content. */ + className?: string; +} + +export const CompassMainHeaderContent: React.FunctionComponent = ({ + children, + className, + ...props +}: CompassMainHeaderContentProps) => ( +
+ {children} +
+); + +CompassMainHeaderContent.displayName = 'CompassMainHeaderContent'; diff --git a/packages/react-core/src/components/Compass/CompassMainHeaderTitle.tsx b/packages/react-core/src/components/Compass/CompassMainHeaderTitle.tsx new file mode 100644 index 00000000000..5fd5bc269ac --- /dev/null +++ b/packages/react-core/src/components/Compass/CompassMainHeaderTitle.tsx @@ -0,0 +1,25 @@ +import styles from '@patternfly/react-styles/css/components/Compass/compass'; +import { css } from '@patternfly/react-styles'; + +/** A wrapper component for custom title content to be passed into a Compass main header. This should also be wrapped + * by a Compass main header content component. + */ + +export interface CompassMainHeaderTitleProps extends React.HTMLProps { + /** Content of the main header title. */ + children: React.ReactNode; + /** Additional classes added to the main header title. */ + className?: string; +} + +export const CompassMainHeaderTitle: React.FunctionComponent = ({ + children, + className, + ...props +}: CompassMainHeaderTitleProps) => ( +
+ {children} +
+); + +CompassMainHeaderTitle.displayName = 'CompassMainHeaderTitle'; diff --git a/packages/react-core/src/components/Compass/CompassMainHeaderToolbar.tsx b/packages/react-core/src/components/Compass/CompassMainHeaderToolbar.tsx new file mode 100644 index 00000000000..b09529cfc00 --- /dev/null +++ b/packages/react-core/src/components/Compass/CompassMainHeaderToolbar.tsx @@ -0,0 +1,25 @@ +import styles from '@patternfly/react-styles/css/components/Compass/compass'; +import { css } from '@patternfly/react-styles'; + +/** A wrapper component for custom toolbar content to be passed into a Compass main header. This should also be wrapped + * by a Compass main header content component. + */ + +export interface CompassMainHeaderToolbarProps extends React.HTMLProps { + /** Content of the main header toolbar. */ + children: React.ReactNode; + /** Additional classes added to the main header toolbar. */ + className?: string; +} + +export const CompassMainHeaderToolbar: React.FunctionComponent = ({ + children, + className, + ...props +}: CompassMainHeaderToolbarProps) => ( +
+ {children} +
+); + +CompassMainHeaderToolbar.displayName = 'CompassMainHeaderToolbar'; diff --git a/packages/react-core/src/components/Compass/CompassMessageBar.tsx b/packages/react-core/src/components/Compass/CompassMessageBar.tsx index 4e9a737aa37..f38b8a8529b 100644 --- a/packages/react-core/src/components/Compass/CompassMessageBar.tsx +++ b/packages/react-core/src/components/Compass/CompassMessageBar.tsx @@ -1,7 +1,7 @@ import styles from '@patternfly/react-styles/css/components/Compass/compass'; import { css } from '@patternfly/react-styles'; -interface CompassMessageBarProps extends React.HTMLProps { +export interface CompassMessageBarProps extends React.HTMLProps { /** Content of the message bar. Typically a @patternfly/chatbot MessageBar component. */ children?: React.ReactNode; /** Additional classes added to the message bar */ @@ -12,7 +12,7 @@ export const CompassMessageBar: React.FunctionComponent children, className, ...props -}) => ( +}: CompassMessageBarProps) => (
{children}
diff --git a/packages/react-core/src/components/Compass/CompassNavContent.tsx b/packages/react-core/src/components/Compass/CompassNavContent.tsx new file mode 100644 index 00000000000..8c27583213b --- /dev/null +++ b/packages/react-core/src/components/Compass/CompassNavContent.tsx @@ -0,0 +1,20 @@ +import styles from '@patternfly/react-styles/css/components/Compass/compass'; +import { css } from '@patternfly/react-styles'; +export interface CompassNavContentProps extends React.HTMLProps { + /** Content of the nav content wrapper. */ + children: React.ReactNode; + /** Additional classes added to the nav content. */ + className?: string; +} + +export const CompassNavContent: React.FunctionComponent = ({ + children, + className, + ...props +}: CompassNavContentProps) => ( +
+ {children} +
+); + +CompassNavContent.displayName = 'CompassNavContent'; diff --git a/packages/react-core/src/components/Compass/CompassNavHome.tsx b/packages/react-core/src/components/Compass/CompassNavHome.tsx new file mode 100644 index 00000000000..8f9410ddc08 --- /dev/null +++ b/packages/react-core/src/components/Compass/CompassNavHome.tsx @@ -0,0 +1,77 @@ +import { useRef } from 'react'; +import styles from '@patternfly/react-styles/css/components/Compass/compass'; +import { css } from '@patternfly/react-styles'; +import { Button } from '../Button'; +import { Tooltip } from '../Tooltip'; + +const CompassHomeIcon = () => ( + +); + +export interface CompassNavHomeProps extends Omit, 'onClick'> { + /** Content to display in the tooltip. Defaults to "Home". */ + tooltipContent?: React.ReactNode; + /** Click handler for the home button. */ + onClick?: React.MouseEventHandler; + /** Additional classes added to the nav home wrapper. */ + className?: string; + /** Accessible label for the nav home. */ + 'aria-label'?: string; +} + +export const CompassNavHome: React.FunctionComponent = ({ + 'aria-label': ariaLabel = 'Home', + tooltipContent = 'Home', + className, + onClick, + ...props +}: CompassNavHomeProps) => { + const buttonRef = useRef(null); + + return ( +
+ +
+ ); +}; + +CompassNavHome.displayName = 'CompassNavHome'; diff --git a/packages/react-core/src/components/Compass/CompassNavMain.tsx b/packages/react-core/src/components/Compass/CompassNavMain.tsx new file mode 100644 index 00000000000..082e75c61e8 --- /dev/null +++ b/packages/react-core/src/components/Compass/CompassNavMain.tsx @@ -0,0 +1,21 @@ +import styles from '@patternfly/react-styles/css/components/Compass/compass'; +import { css } from '@patternfly/react-styles'; + +export interface CompassNavMainProps extends React.HTMLProps { + /** Content of the nav main section (typically tabs). */ + children: React.ReactNode; + /** Additional classes added to the nav main section. */ + className?: string; +} + +export const CompassNavMain: React.FunctionComponent = ({ + children, + className, + ...props +}: CompassNavMainProps) => ( +
+ {children} +
+); + +CompassNavMain.displayName = 'CompassNavMain'; diff --git a/packages/react-core/src/components/Compass/CompassNavSearch.tsx b/packages/react-core/src/components/Compass/CompassNavSearch.tsx new file mode 100644 index 00000000000..5731b45883c --- /dev/null +++ b/packages/react-core/src/components/Compass/CompassNavSearch.tsx @@ -0,0 +1,70 @@ +import { useRef } from 'react'; +import styles from '@patternfly/react-styles/css/components/Compass/compass'; +import { css } from '@patternfly/react-styles'; +import { Button } from '../Button'; +import { Tooltip } from '../Tooltip'; + +const CompassSearchIcon = () => ( + +); + +export interface CompassNavSearchProps extends Omit, 'onClick'> { + /** Content to display in the tooltip. Defaults to "Search". */ + tooltipContent?: React.ReactNode; + /** Click handler for the search button. */ + onClick?: React.MouseEventHandler; + /** Additional classes added to the nav search wrapper. */ + className?: string; + /** Accessible label for the nav search. */ + 'aria-label'?: string; +} + +export const CompassNavSearch: React.FunctionComponent = ({ + 'aria-label': ariaLabel = 'Search', + tooltipContent = 'Search', + className, + onClick, + ...props +}: CompassNavSearchProps) => { + const buttonRef = useRef(null); + + return ( +
+ +
+ ); +}; + +CompassNavSearch.displayName = 'CompassNavSearch'; diff --git a/packages/react-core/src/components/Compass/CompassPanel.tsx b/packages/react-core/src/components/Compass/CompassPanel.tsx index 6b8096a51e6..d7b77843e77 100644 --- a/packages/react-core/src/components/Compass/CompassPanel.tsx +++ b/packages/react-core/src/components/Compass/CompassPanel.tsx @@ -1,7 +1,7 @@ import styles from '@patternfly/react-styles/css/components/Compass/compass'; import { css } from '@patternfly/react-styles'; -interface CompassPanelProps extends React.HTMLProps { +export interface CompassPanelProps extends React.HTMLProps { /** Content of the panel. */ children: React.ReactNode; /** Additional classes added to the panel. */ @@ -30,7 +30,7 @@ export const CompassPanel: React.FunctionComponent = ({ isFullHeight, isScrollable, ...props -}) => ( +}: CompassPanelProps) => (
{ ); expect(asFragment()).toMatchSnapshot(); }); + +test(`Renders with ${styles.modifiers.dock} class when dock is passed`, () => { + render(Dock content
} data-testid="compass" />); + expect(screen.getByTestId('compass')).toHaveClass(styles.modifiers.dock); +}); + +test(`Does not render with ${styles.modifiers.dock} class when dock is not passed`, () => { + render(); + expect(screen.getByTestId('compass')).not.toHaveClass(styles.modifiers.dock); +}); diff --git a/packages/react-core/src/components/Compass/__tests__/CompassHero.test.tsx b/packages/react-core/src/components/Compass/__tests__/CompassHero.test.tsx index f68ed214b79..c46f8605e73 100644 --- a/packages/react-core/src/components/Compass/__tests__/CompassHero.test.tsx +++ b/packages/react-core/src/components/Compass/__tests__/CompassHero.test.tsx @@ -16,135 +16,23 @@ test('Renders with children', () => { expect(screen.getByText('Test content')).toBeVisible(); }); -test('Renders with custom class name when className prop is provided', () => { - render(Test); - expect(screen.getByText('Test').parentElement).toHaveClass('custom-class'); -}); - -test(`Renders with default ${styles.compassPanel} and ${styles.compassHero} classes on the hero and ${styles.compassHeroBody} class on the hero body`, () => { +test(`Renders with ${styles.compass}__hero class by defaulty`, () => { render(Test); - const heroBodyElement = screen.getByText('Test'); - expect(heroBodyElement).toHaveClass(styles.compassHeroBody); - - const heroElement = heroBodyElement.parentElement; - expect(heroElement).toHaveClass(styles.compassPanel); - expect(heroElement).toHaveClass(styles.compassHero); -}); - -test('Renders with light background image style when backgroundSrcLight is provided', () => { - const backgroundSrc = 'light-bg.jpg'; - render(Test); - expect(screen.getByText('Test').parentElement).toHaveStyle( - `--pf-v6-c-compass__hero--BackgroundImage--light: url(${backgroundSrc})` - ); -}); - -test('Renders with dark background image style when backgroundSrcDark is provided', () => { - const backgroundSrc = 'dark-bg.jpg'; - render(Test); - expect(screen.getByText('Test').parentElement).toHaveStyle( - `--pf-v6-c-compass__hero--BackgroundImage--dark: url(${backgroundSrc})` - ); -}); - -test('Renders with both light and dark background image styles when both are provided', () => { - const lightSrc = 'light-bg.jpg'; - const darkSrc = 'dark-bg.jpg'; - render( - - Test - - ); - const heroElement = screen.getByText('Test').parentElement; - expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--BackgroundImage--light: url(${lightSrc})`); - expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--BackgroundImage--dark: url(${darkSrc})`); -}); - -test('Renders with light gradient styles when gradientLight is provided', () => { - const gradient = { - stop1: '#ff0000', - stop2: '#00ff00', - stop3: '#0000ff' - }; - render(Test); - const heroElement = screen.getByText('Test').parentElement; - expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-1--light: ${gradient.stop1}`); - expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-2--light: ${gradient.stop2}`); - expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-3--light: ${gradient.stop3}`); -}); -test('Renders with dark gradient styles when gradientDark is provided', () => { - const gradient = { - stop1: '#ff0000', - stop2: '#00ff00', - stop3: '#0000ff' - }; - render(Test); - const heroElement = screen.getByText('Test').parentElement; - expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-1--dark: ${gradient.stop1}`); - expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-2--dark: ${gradient.stop2}`); - expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-3--dark: ${gradient.stop3}`); -}); - -test('Renders with both light and dark gradient styles when both are provided', () => { - const lightGradient = { - stop1: '#ff0000', - stop2: '#00ff00', - stop3: '#0000ff' - }; - const darkGradient = { - stop1: '#000000', - stop2: '#ffffff', - stop3: '#808080' - }; - render( - - Test - - ); - const heroElement = screen.getByText('Test').parentElement; - expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-1--light: ${lightGradient.stop1}`); - expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-1--dark: ${darkGradient.stop1}`); + expect(screen.getByText('Test')).toHaveClass(`${styles.compass}__hero`, { exact: true }); }); -test('Renders with both background images and gradient styles when both are provided', () => { - const lightSrc = 'light-bg.jpg'; - const darkSrc = 'dark-bg.jpg'; - const lightGradient = { stop1: '#ff0000' }; - const darkGradient = { stop1: '#000000' }; - - render( - - Test - - ); - const heroElement = screen.getByText('Test').parentElement; - expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--BackgroundImage--light: url(${lightSrc})`); - expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--BackgroundImage--dark: url(${darkSrc})`); - expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-1--light: ${lightGradient.stop1}`); - expect(heroElement).toHaveStyle(`--pf-v6-c-compass__hero--gradient--stop-1--dark: ${darkGradient.stop1}`); +test('Renders with custom class name when className prop is provided', () => { + render(Test); + expect(screen.getByText('Test')).toHaveClass('custom-class'); }); test('Renders with additional props spread to the component', () => { render(Test); - expect(screen.getByText('Test').parentElement).toHaveAccessibleName('Test label'); + expect(screen.getByText('Test')).toHaveAccessibleName('Test label'); }); test('Matches the snapshot', () => { - const { asFragment } = render( - -
Hero content
-
- ); + const { asFragment } = render(Hero content); expect(asFragment()).toMatchSnapshot(); }); diff --git a/packages/react-core/src/components/Compass/__tests__/CompassMainFooter.test.tsx b/packages/react-core/src/components/Compass/__tests__/CompassMainFooter.test.tsx new file mode 100644 index 00000000000..25b955ba77c --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/CompassMainFooter.test.tsx @@ -0,0 +1,52 @@ +import { render, screen } from '@testing-library/react'; +import { CompassMainFooter } from '../CompassMainFooter'; +import styles from '@patternfly/react-styles/css/components/Compass/compass'; + +test('Renders without children', () => { + render( +
+ +
+ ); + expect(screen.getByTestId('test-main-footer').firstChild).toBeVisible(); +}); + +test('Renders with children', () => { + render(Custom content); + expect(screen.getByText('Custom content')).toBeVisible(); +}); + +test('Renders with custom class name when className prop is provided', () => { + render(Test); + expect(screen.getByText('Test')).toHaveClass('custom-class'); +}); + +test(`Renders with default ${styles.compassMainFooter} class`, () => { + render(Test); + expect(screen.getByText('Test')).toHaveClass(styles.compassMainFooter); +}); + +test(`Renders with pf-m-expanded class by default`, () => { + render(Test); + expect(screen.getByText('Test')).toHaveClass('pf-m-expanded'); +}); + +test(`Renders with pf-m-expanded class when isExpanded is true`, () => { + render(Test); + expect(screen.getByText('Test')).toHaveClass('pf-m-expanded'); +}); + +test(`Renders without pf-m-expanded class when isExpanded is false`, () => { + render(Test); + expect(screen.getByText('Test')).not.toHaveClass('pf-m-expanded'); +}); + +test('Renders with additional props spread to the component', () => { + render(Test); + expect(screen.getByText('Test')).toHaveAccessibleName('Test label'); +}); + +test('Matches the snapshot', () => { + const { asFragment } = render(Custom children content); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/components/Compass/__tests__/CompassMainHeader.test.tsx b/packages/react-core/src/components/Compass/__tests__/CompassMainHeader.test.tsx index ef13deb96ef..1ef47f8c7bc 100644 --- a/packages/react-core/src/components/Compass/__tests__/CompassMainHeader.test.tsx +++ b/packages/react-core/src/components/Compass/__tests__/CompassMainHeader.test.tsx @@ -74,9 +74,52 @@ test('Renders children when neither title nor toolbar are provided', () => { expect(screen.getByText('Custom children content')).toBeVisible(); }); +test('Renders CompassPanel when title is passed', () => { + render(); + + const panel = screen.getByTestId('test-id').firstChild; + expect(panel).toHaveClass(styles.compassPanel); +}); + +test('Renders CompassPanel when toolbar is passed', () => { + render(); + + const panel = screen.getByTestId('test-id').firstChild; + expect(panel).toHaveClass(styles.compassPanel); +}); + +test('Does not render CompassPanel when children are passed', () => { + render( + +
Children content
+
+ ); + + const content = screen.getByTestId('test-id').firstChild; + expect(content).not.toHaveClass(styles.compassPanel); +}); + +test('Passes props to CompassPanel when title and compassPanelProps is passed', () => { + render( + + ); + + const panel = screen.getByTestId('test-id').firstChild; + expect(panel).toHaveClass('panel-class'); +}); + +test('Passes props to CompassPanel when toolbar and compassPanelProps is passed', () => { + render( + + ); + + const panel = screen.getByTestId('test-id').firstChild; + expect(panel).toHaveClass('panel-class'); +}); + test('Renders with additional props spread to the component', () => { - render(Test); - expect(screen.getByText('Test')).toHaveAccessibleName('Test label'); + render(Test); + expect(screen.getByText('Test')).toHaveAttribute('id', 'custom-id'); }); test('Matches the snapshot with both title and toolbar', () => { diff --git a/packages/react-core/src/components/Compass/__tests__/CompassMainHeaderContent.test.tsx b/packages/react-core/src/components/Compass/__tests__/CompassMainHeaderContent.test.tsx new file mode 100644 index 00000000000..44475ae7dca --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/CompassMainHeaderContent.test.tsx @@ -0,0 +1,28 @@ +import { render, screen } from '@testing-library/react'; +import { CompassMainHeaderContent } from '../CompassMainHeaderContent'; +import styles from '@patternfly/react-styles/css/components/Compass/compass'; + +test('Renders with children', () => { + render(Custom content); + expect(screen.getByText('Custom content')).toBeVisible(); +}); + +test(`Renders with default ${styles.compass}__main-header-content class`, () => { + render(Test); + expect(screen.getByText('Test')).toHaveClass(`${styles.compass}__main-header-content`); +}); + +test('Renders with custom class name when className prop is provided', () => { + render(Test); + expect(screen.getByText('Test')).toHaveClass('custom-class'); +}); + +test('Renders with additional props spread to the component', () => { + render(Test); + expect(screen.getByText('Test')).toHaveAttribute('id', 'custom-id'); +}); + +test('Matches the snapshot', () => { + const { asFragment } = render(Content); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/components/Compass/__tests__/CompassMainHeaderTitle.test.tsx b/packages/react-core/src/components/Compass/__tests__/CompassMainHeaderTitle.test.tsx new file mode 100644 index 00000000000..9bc10b59b45 --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/CompassMainHeaderTitle.test.tsx @@ -0,0 +1,28 @@ +import { render, screen } from '@testing-library/react'; +import { CompassMainHeaderTitle } from '../CompassMainHeaderTitle'; +import styles from '@patternfly/react-styles/css/components/Compass/compass'; + +test('Renders with children', () => { + render(Custom content); + expect(screen.getByText('Custom content')).toBeVisible(); +}); + +test(`Renders with default ${styles.compass}__main-header-title class`, () => { + render(Test); + expect(screen.getByText('Test')).toHaveClass(`${styles.compass}__main-header-title`); +}); + +test('Renders with custom class name when className prop is provided', () => { + render(Test); + expect(screen.getByText('Test')).toHaveClass('custom-class'); +}); + +test('Renders with additional props spread to the component', () => { + render(Test); + expect(screen.getByText('Test')).toHaveAttribute('id', 'custom-id'); +}); + +test('Matches the snapshot', () => { + const { asFragment } = render(Content); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/components/Compass/__tests__/CompassMainHeaderToolbar.test.tsx b/packages/react-core/src/components/Compass/__tests__/CompassMainHeaderToolbar.test.tsx new file mode 100644 index 00000000000..62ab818f602 --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/CompassMainHeaderToolbar.test.tsx @@ -0,0 +1,28 @@ +import { render, screen } from '@testing-library/react'; +import { CompassMainHeaderToolbar } from '../CompassMainHeaderToolbar'; +import styles from '@patternfly/react-styles/css/components/Compass/compass'; + +test('Renders with children', () => { + render(Custom content); + expect(screen.getByText('Custom content')).toBeVisible(); +}); + +test(`Renders with default ${styles.compass}__main-header-toolbar class`, () => { + render(Test); + expect(screen.getByText('Test')).toHaveClass(`${styles.compass}__main-header-toolbar`); +}); + +test('Renders with custom class name when className prop is provided', () => { + render(Test); + expect(screen.getByText('Test')).toHaveClass('custom-class'); +}); + +test('Renders with additional props spread to the component', () => { + render(Test); + expect(screen.getByText('Test')).toHaveAttribute('id', 'custom-id'); +}); + +test('Matches the snapshot', () => { + const { asFragment } = render(Content); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/components/Compass/__tests__/CompassNavContent.test.tsx b/packages/react-core/src/components/Compass/__tests__/CompassNavContent.test.tsx new file mode 100644 index 00000000000..eb176b5ccae --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/CompassNavContent.test.tsx @@ -0,0 +1,37 @@ +import { render, screen } from '@testing-library/react'; +import { CompassNavContent } from '../CompassNavContent'; +import styles from '@patternfly/react-styles/css/components/Compass/compass'; + +test('Renders with children', () => { + render(Test content); + + expect(screen.getByText('Test content')).toBeVisible(); +}); + +test('Renders with custom class name when className prop is provided', () => { + render(Test); + + expect(screen.getByText('Test')).toHaveClass('custom-class'); +}); + +test(`Renders with default ${styles.compassNavContent} class`, () => { + render(Test); + + expect(screen.getByText('Test')).toHaveClass(styles.compassNavContent, { exact: true }); +}); + +test('Renders with additional props spread to the component', () => { + render(Test); + + expect(screen.getByText('Test')).toHaveAccessibleName('Test label'); +}); + +test('Matches the snapshot', () => { + const { asFragment } = render( + +
Nav content wrapper
+
+ ); + + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/components/Compass/__tests__/CompassNavHome.test.tsx b/packages/react-core/src/components/Compass/__tests__/CompassNavHome.test.tsx new file mode 100644 index 00000000000..2f3b8b6fed1 --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/CompassNavHome.test.tsx @@ -0,0 +1,89 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { CompassNavHome } from '../CompassNavHome'; +import styles from '@patternfly/react-styles/css/components/Compass/compass'; + +test('Renders with default aria-label', () => { + render(); + + expect(screen.getByRole('button', { name: 'Home' })).toBeVisible(); +}); + +test('Renders with custom aria-label when provided', () => { + render(); + + expect(screen.getByRole('button', { name: 'Custom home' })).toBeVisible(); +}); + +test('Renders with default tooltip content', async () => { + const user = userEvent.setup(); + + render(); + + const button = screen.getByRole('button'); + + await user.hover(button); + + await screen.findByRole('tooltip'); + + expect(screen.getByRole('tooltip')).toHaveTextContent('Home'); +}); + +test('Renders with custom tooltip content when provided', async () => { + const user = userEvent.setup(); + + render(); + + const button = screen.getByRole('button'); + + await user.hover(button); + + await screen.findByRole('tooltip'); + expect(screen.getByRole('tooltip')).toHaveTextContent('Custom tooltip'); +}); + +test('Renders with custom class name when className prop is provided', () => { + const { container } = render(); + + expect(container.firstChild).toHaveClass('custom-class'); +}); + +test(`Renders with default class`, () => { + const { container } = render(); + + expect(container.firstChild).toHaveClass(styles.compassNav + '-home', { exact: true }); +}); + +test('Calls onClick handler when button is clicked', async () => { + const user = userEvent.setup(); + const onClick = jest.fn(); + + render(); + + await user.click(screen.getByRole('button', { name: 'Home' })); + + expect(onClick).toHaveBeenCalledTimes(1); +}); + +test('Renders button with plain variant and circle shape', () => { + render(); + + const button = screen.getByRole('button', { name: 'Home' }); + + expect(button).toHaveClass('pf-m-plain'); + expect(button).toHaveClass('pf-m-circle'); +}); + +test('Matches the snapshot', () => { + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); +}); + +test('Matches the snapshot with custom props', () => { + const { asFragment } = render( + + ); + + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/components/Compass/__tests__/CompassNavMain.test.tsx b/packages/react-core/src/components/Compass/__tests__/CompassNavMain.test.tsx new file mode 100644 index 00000000000..0242f9ef64c --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/CompassNavMain.test.tsx @@ -0,0 +1,37 @@ +import { render, screen } from '@testing-library/react'; +import { CompassNavMain } from '../CompassNavMain'; +import styles from '@patternfly/react-styles/css/components/Compass/compass'; + +test('Renders with children', () => { + render(Test content); + + expect(screen.getByText('Test content')).toBeVisible(); +}); + +test('Renders with custom class name when className prop is provided', () => { + render(Test); + + expect(screen.getByText('Test')).toHaveClass('custom-class'); +}); + +test(`Renders with default ${styles.compassNavMain} class`, () => { + render(Test); + + expect(screen.getByText('Test')).toHaveClass(styles.compassNavMain, { exact: true }); +}); + +test('Renders with additional props spread to the component', () => { + render(Test); + + expect(screen.getByText('Test')).toHaveAccessibleName('Test label'); +}); + +test('Matches the snapshot', () => { + const { asFragment } = render( + +
Main tabs content
+
+ ); + + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/components/Compass/__tests__/CompassNavSearch.test.tsx b/packages/react-core/src/components/Compass/__tests__/CompassNavSearch.test.tsx new file mode 100644 index 00000000000..a32111d6899 --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/CompassNavSearch.test.tsx @@ -0,0 +1,87 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { CompassNavSearch } from '../CompassNavSearch'; +import styles from '@patternfly/react-styles/css/components/Compass/compass'; + +test('Renders with default aria-label', () => { + render(); + expect(screen.getByRole('button', { name: 'Search' })).toBeVisible(); +}); + +test('Renders with custom aria-label when provided', () => { + render(); + expect(screen.getByRole('button', { name: 'Custom search' })).toBeVisible(); +}); + +test('Renders with default tooltip content', async () => { + const user = userEvent.setup(); + + render(); + + const button = screen.getByRole('button'); + + await user.hover(button); + + await screen.findByRole('tooltip'); + + expect(screen.getByRole('tooltip')).toHaveTextContent('Search'); +}); + +test('Renders with custom tooltip content when provided', async () => { + const user = userEvent.setup(); + + render(); + + const button = screen.getByRole('button'); + + await user.hover(button); + + await screen.findByRole('tooltip'); + + expect(screen.getByRole('tooltip')).toHaveTextContent('Custom tooltip'); +}); + +test('Renders with custom class name when className prop is provided', () => { + const { container } = render(); + + expect(container.firstChild).toHaveClass('custom-class'); +}); + +test(`Renders with default class`, () => { + const { container } = render(); + + expect(container.firstChild).toHaveClass(styles.compassNav + '-search', { exact: true }); +}); + +test('Calls onClick handler when button is clicked', async () => { + const user = userEvent.setup(); + const onClick = jest.fn(); + + render(); + + await user.click(screen.getByRole('button', { name: 'Search' })); + + expect(onClick).toHaveBeenCalledTimes(1); +}); + +test('Renders button with plain variant and circle shape', () => { + render(); + + const button = screen.getByRole('button', { name: 'Search' }); + + expect(button).toHaveClass('pf-m-plain'); +}); + +test('Matches the snapshot', () => { + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); +}); + +test('Matches the snapshot with custom props', () => { + const { asFragment } = render( + + ); + + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/components/Compass/__tests__/__snapshots__/Compass.test.tsx.snap b/packages/react-core/src/components/Compass/__tests__/__snapshots__/Compass.test.tsx.snap index 5886e96c8ec..a4d1e225ed6 100644 --- a/packages/react-core/src/components/Compass/__tests__/__snapshots__/Compass.test.tsx.snap +++ b/packages/react-core/src/components/Compass/__tests__/__snapshots__/Compass.test.tsx.snap @@ -47,7 +47,7 @@ exports[`Matches the snapshot with basic layout 1`] = ` exports[`Matches the snapshot with drawer 1`] = `
-
-
- Hero content -
-
+ Hero content
`; diff --git a/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainFooter.test.tsx.snap b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainFooter.test.tsx.snap new file mode 100644 index 00000000000..160ef4650a2 --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainFooter.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches the snapshot 1`] = ` + + + +`; diff --git a/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainHeader.test.tsx.snap b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainHeader.test.tsx.snap index a64dcc96592..fe09fdd8fee 100644 --- a/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainHeader.test.tsx.snap +++ b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainHeader.test.tsx.snap @@ -9,17 +9,17 @@ exports[`Matches the snapshot with both title and toolbar 1`] = ` class="pf-v6-c-compass__panel" >
Title
Toolbar diff --git a/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainHeaderContent.test.tsx.snap b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainHeaderContent.test.tsx.snap new file mode 100644 index 00000000000..c49e1a1d468 --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainHeaderContent.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches the snapshot 1`] = ` + +
+ Content +
+
+`; diff --git a/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainHeaderTitle.test.tsx.snap b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainHeaderTitle.test.tsx.snap new file mode 100644 index 00000000000..15ac7ab71eb --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainHeaderTitle.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches the snapshot 1`] = ` + +
+ Content +
+
+`; diff --git a/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainHeaderToolbar.test.tsx.snap b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainHeaderToolbar.test.tsx.snap new file mode 100644 index 00000000000..79550ac0b48 --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassMainHeaderToolbar.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches the snapshot 1`] = ` + +
+ Content +
+
+`; diff --git a/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassNavContent.test.tsx.snap b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassNavContent.test.tsx.snap new file mode 100644 index 00000000000..df1e9a2ddea --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassNavContent.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches the snapshot 1`] = ` + +
+
+ Nav content wrapper +
+
+
+`; diff --git a/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassNavHome.test.tsx.snap b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassNavHome.test.tsx.snap new file mode 100644 index 00000000000..4ff0408656d --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassNavHome.test.tsx.snap @@ -0,0 +1,107 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches the snapshot 1`] = ` + +
+ +
+
+`; + +exports[`Matches the snapshot with custom props 1`] = ` + +
+ +
+
+`; diff --git a/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassNavMain.test.tsx.snap b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassNavMain.test.tsx.snap new file mode 100644 index 00000000000..53987fbf559 --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassNavMain.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches the snapshot 1`] = ` + +
+
+ Main tabs content +
+
+
+`; diff --git a/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassNavSearch.test.tsx.snap b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassNavSearch.test.tsx.snap new file mode 100644 index 00000000000..0bcb2978947 --- /dev/null +++ b/packages/react-core/src/components/Compass/__tests__/__snapshots__/CompassNavSearch.test.tsx.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches the snapshot 1`] = ` + + + +`; + +exports[`Matches the snapshot with custom props 1`] = ` + + + +`; diff --git a/packages/react-core/src/components/Compass/examples/Compass.md b/packages/react-core/src/components/Compass/examples/Compass.md index 7870c02ae1c..a1f9d597df6 100644 --- a/packages/react-core/src/components/Compass/examples/Compass.md +++ b/packages/react-core/src/components/Compass/examples/Compass.md @@ -1,7 +1,8 @@ --- id: Compass cssPrefix: pf-v6-c-compass -section: components +section: AI +subsection: Generative UIs beta: true propComponents: [ @@ -11,37 +12,65 @@ propComponents: 'CompassHero', 'CompassMainHeader', 'CompassPanel', - 'CompassMessageBar' + 'CompassMessageBar', + 'CompassMainFooter' ] --- -import './compass.css'; -import { useRef, useState } from 'react'; +import { useRef, useState, useEffect } from 'react'; import PlayIcon from '@patternfly/react-icons/dist/esm/icons/play-icon'; import OutlinedPlusSquare from '@patternfly/react-icons/dist/esm/icons/outlined-plus-square-icon'; import OutlinedCopy from '@patternfly/react-icons/dist/esm/icons/outlined-copy-icon'; import OutlinedQuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/outlined-question-circle-icon'; +import './compass.css'; + ## Examples ### Basic -In a basic compass layout, content can be passed to the following props to populate areas of the page: +In a basic Compass layout, content can be passed to the following props to populate different areas of the page: + +- `header`: Content rendered at the top of the page, typically including a `` component that divides the header into 3 areas, with a logo or brand, middle navigation, and profile. +- `sidebarStart`: Content rendered at the horizontal start of the page (by default, the left side). +- `main`: Content rendered in the center of the page, typically including a `` or ``, along with a `` filled with 1 or more `` components. +- `sidebarEnd`: Content rendered at the horizontal end of the page (by default, the right side). +- `footer`: Content rendered at the bottom of the page. + +To customize the background image of the `` and `` components, you can use their respective `backgroundSrcLight` and `backgroundSrcDark` props. You can also add and customize a color gradient background for the `` component by using the `gradientLight` and `gradientDark` props. + +```ts isBeta file="CompassBasic.tsx" + +``` -- `header`: content rendered in the top of the page. This will typically be a `CompassHeader` component to break the header into 3 areas consisting of a logo or brand, middle navigation, and profile. -- `sidebarStart`: content rendered in the left side or start side of the page -- `main`: content rendered in the center of the page. This will typically consist of a `CompassMainHeader` or `CompassHero`, along with a `CompassContent` filled with one or more `CompassPanel` components. -- `sidebarEnd`: content rendered in the right side or end side of the page -- `footer`: content rendered in the bottom of the page +### With alternate footer -The background image of the `Compass` and `CompassHero` may be customized by using their respective `backgroundSrcLight` and `backgroundSrcDark` props. The `CompassHero` also allows customization of a color gradient across its container by using the `gradientLight` and `gradientDark` props. +When `footer` is used, its content will fill the width of the screen. By default, when content inside the footer grows, the height and placement of the start and end sidebars will adjust to allow for the change. To modify this behavior and render footer content without interfering with the sidebars, instead place a `` inside the `main` section. This will render content at the bottom of the page between the 2 sidebars, rather than across the entire bottom of the page. -```ts file="CompassBasic.tsx" +```ts file="CompassMainFooterDemo.tsx" ``` -### Demo +### With docked nav + +```ts file="CompassDockLayout.tsx" + +``` + +## Composable structure + +When building a more custom implementation with Compass components, there are some intended or expected structures that must remain present. + +### CompassMainHeader structure -```ts isFullscreen file="CompassDemo.tsx" +When using the `children` property in the `` component, there are specific structural patterns that you should follow, as shown this general code structure. +```noLive + + + + {Your custom content goes here, which can include the and/or sub-components} + + + ``` diff --git a/packages/react-core/src/components/Compass/examples/CompassBasic.tsx b/packages/react-core/src/components/Compass/examples/CompassBasic.tsx index f43c55b79bb..fd3cd1a57cb 100644 --- a/packages/react-core/src/components/Compass/examples/CompassBasic.tsx +++ b/packages/react-core/src/components/Compass/examples/CompassBasic.tsx @@ -1,4 +1,12 @@ -import { Compass, CompassHeader, CompassHero, CompassContent, CompassMainHeader } from '@patternfly/react-core'; +import { + Compass, + CompassHeader, + CompassHero, + CompassContent, + CompassMainHeader, + CompassPanel, + CompassMainHeaderContent +} from '@patternfly/react-core'; import './compass.css'; export const CompassBasic: React.FunctionComponent = () => { @@ -12,7 +20,11 @@ export const CompassBasic: React.FunctionComponent = () => { -
Content title
+ + +
Content title
+
+
Content
@@ -28,6 +40,7 @@ export const CompassBasic: React.FunctionComponent = () => { main={mainContent} sidebarEnd={sidebarEndContent} footer={footerContent} + style={{ height: '600px' }} /> ); }; diff --git a/packages/react-core/src/components/Compass/examples/CompassDockLayout.tsx b/packages/react-core/src/components/Compass/examples/CompassDockLayout.tsx new file mode 100644 index 00000000000..9d7357467c7 --- /dev/null +++ b/packages/react-core/src/components/Compass/examples/CompassDockLayout.tsx @@ -0,0 +1,25 @@ +import { + Compass, + CompassContent, + CompassMainHeader, + CompassPanel, + CompassMainHeaderContent +} from '@patternfly/react-core'; +import './compass.css'; + +export const CompassBasic: React.FunctionComponent = () => { + const dockContent =
Content
; + const mainContent = ( + + + + +
Content title
+
+
+
+
Content
+
+ ); + return ; +}; diff --git a/packages/react-core/src/components/Compass/examples/CompassMainFooterDemo.tsx b/packages/react-core/src/components/Compass/examples/CompassMainFooterDemo.tsx new file mode 100644 index 00000000000..4846f6f07aa --- /dev/null +++ b/packages/react-core/src/components/Compass/examples/CompassMainFooterDemo.tsx @@ -0,0 +1,43 @@ +import { + Compass, + CompassHeader, + CompassHero, + CompassContent, + CompassMainHeader, + CompassMainFooter +} from '@patternfly/react-core'; +import './compass.css'; + +export const CompassMainFooterDemo: React.FunctionComponent = () => { + const headerContent = Logo
} nav={
Nav
} profile={
Profile
} />; + const sidebarStartContent =
Sidebar start
; + // TODO: simplify mainContent to only a div string + const mainContent = ( + <> + +
Hero
+
+ + +
Content title
+
+
Content
+
+ +
Footer
+
+ + ); + const sidebarEndContent =
Sidebar end
; + + return ( + + ); +}; diff --git a/packages/react-core/src/components/Compass/examples/compass.css b/packages/react-core/src/components/Compass/examples/compass.css index 46c3b19746b..26242064ee7 100644 --- a/packages/react-core/src/components/Compass/examples/compass.css +++ b/packages/react-core/src/components/Compass/examples/compass.css @@ -1,8 +1,32 @@ -#ws-react-c-compass-basic [class*="pf-v6-c-compass"] { +#ws-react-a-compass-basic [class*="pf-v6-c-compass"] { position: relative; } -#ws-react-c-compass-basic [class*="pf-v6-c-compass"]::after { +#ws-react-a-compass-basic [class*="pf-v6-c-compass"]::after { + content: ""; + position: absolute; + inset: 0; + border: var(--pf-t--global--border--width--regular) dashed var(--pf-t--global--border--color--default); + pointer-events: none; +} + +#ws-react-a-compass-with-alternate-footer [class*="pf-v6-c-compass"] { + position: relative; +} + +#ws-react-a-compass-with-alternate-footer [class*="pf-v6-c-compass"]:not([class*="footer"])::after { + content: ""; + position: absolute; + inset: 0; + border: var(--pf-t--global--border--width--regular) dashed var(--pf-t--global--border--color--default); + pointer-events: none; +} + +#ws-react-a-compass-with-docked-nav [class*="pf-v6-c-compass"] { + position: relative; +} + +#ws-react-a-compass-with-docked-nav [class*="pf-v6-c-compass"]:not([class*="footer"])::after { content: ""; position: absolute; inset: 0; diff --git a/packages/react-core/src/components/Compass/index.ts b/packages/react-core/src/components/Compass/index.ts index c27bc7678f1..7aef7c2e42e 100644 --- a/packages/react-core/src/components/Compass/index.ts +++ b/packages/react-core/src/components/Compass/index.ts @@ -3,5 +3,13 @@ export * from './CompassContent'; export * from './CompassHeader'; export * from './CompassHero'; export * from './CompassMainHeader'; +export * from './CompassMainHeaderContent'; +export * from './CompassMainHeaderTitle'; +export * from './CompassMainHeaderToolbar'; +export * from './CompassMainFooter'; export * from './CompassMessageBar'; +export * from './CompassNavContent'; +export * from './CompassNavHome'; +export * from './CompassNavMain'; +export * from './CompassNavSearch'; export * from './CompassPanel'; diff --git a/packages/react-core/src/components/DataList/__tests__/Generated/__snapshots__/DataListToggle.test.tsx.snap b/packages/react-core/src/components/DataList/__tests__/Generated/__snapshots__/DataListToggle.test.tsx.snap index c7edd36fb32..ecddb3776ce 100644 --- a/packages/react-core/src/components/DataList/__tests__/Generated/__snapshots__/DataListToggle.test.tsx.snap +++ b/packages/react-core/src/components/DataList/__tests__/Generated/__snapshots__/DataListToggle.test.tsx.snap @@ -32,12 +32,24 @@ exports[`DataListToggle should match snapshot (auto-generated) 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 256 512" width="1em" > - + + + + + +
diff --git a/packages/react-core/src/components/DataList/__tests__/__snapshots__/DataListToggle.test.tsx.snap b/packages/react-core/src/components/DataList/__tests__/__snapshots__/DataListToggle.test.tsx.snap index d876e47445c..be4d5305cc5 100644 --- a/packages/react-core/src/components/DataList/__tests__/__snapshots__/DataListToggle.test.tsx.snap +++ b/packages/react-core/src/components/DataList/__tests__/__snapshots__/DataListToggle.test.tsx.snap @@ -32,12 +32,24 @@ exports[`Renders to match snapshot 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 256 512" width="1em" > - + + + + + +
diff --git a/packages/react-core/src/components/DatePicker/DatePicker.tsx b/packages/react-core/src/components/DatePicker/DatePicker.tsx index 2d780689627..652310fed6f 100644 --- a/packages/react-core/src/components/DatePicker/DatePicker.tsx +++ b/packages/react-core/src/components/DatePicker/DatePicker.tsx @@ -24,7 +24,8 @@ export interface DatePickerRequiredObject { /** The main date picker component. */ export interface DatePickerProps - extends CalendarFormat, + extends + CalendarFormat, Omit, 'onChange' | 'onFocus' | 'onBlur' | 'disabled' | 'ref'> { /** The container to append the menu to. Defaults to 'inline'. * If your menu is being cut off you can append it to an element higher up the DOM tree. diff --git a/packages/react-core/src/components/DatePicker/__tests__/__snapshots__/DatePicker.test.tsx.snap b/packages/react-core/src/components/DatePicker/__tests__/__snapshots__/DatePicker.test.tsx.snap index 5ca4fb315f4..d129682eaef 100644 --- a/packages/react-core/src/components/DatePicker/__tests__/__snapshots__/DatePicker.test.tsx.snap +++ b/packages/react-core/src/components/DatePicker/__tests__/__snapshots__/DatePicker.test.tsx.snap @@ -51,12 +51,24 @@ exports[`With popover opened 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 448 512" width="1em" > - + + + + + + @@ -110,12 +122,24 @@ exports[`With popover opened 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 256 512" width="1em" > - + + + + + + @@ -161,12 +185,24 @@ exports[`With popover opened 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 320 512" width="1em" > - + + + + + + @@ -215,12 +251,24 @@ exports[`With popover opened 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 256 512" width="1em" > - + + + + + + @@ -844,12 +892,24 @@ exports[`disabled date picker 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 448 512" width="1em" > - + + + + + + diff --git a/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx b/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx index 73f5bd6d2a4..9cafec596dc 100644 --- a/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx +++ b/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx @@ -90,6 +90,7 @@ export const DrawerPanelContent: React.FunctionComponent
@@ -308,6 +345,7 @@ exports[`Drawer isExpanded = false and isInline = true and isStatic = false 1`] class="pf-v6-c-drawer__panel" hidden="" id="generated-id" + inert="" />
@@ -364,12 +402,24 @@ exports[`Drawer isExpanded = true and isInline = false and isStatic = false 1`] fill="currentColor" height="1em" role="img" - viewBox="0 0 352 512" width="1em" > - + + + + + + @@ -437,12 +487,24 @@ exports[`Drawer isExpanded = true and isInline = false and isStatic = true 1`] = fill="currentColor" height="1em" role="img" - viewBox="0 0 352 512" width="1em" > - + + + + + + @@ -510,12 +572,24 @@ exports[`Drawer isExpanded = true and isInline = true and isStatic = false 1`] = fill="currentColor" height="1em" role="img" - viewBox="0 0 352 512" width="1em" > - + + + + + + diff --git a/packages/react-core/src/components/Drawer/examples/Drawer.md b/packages/react-core/src/components/Drawer/examples/Drawer.md index d05192c35ec..544bc08a2ba 100644 --- a/packages/react-core/src/components/Drawer/examples/Drawer.md +++ b/packages/react-core/src/components/Drawer/examples/Drawer.md @@ -135,7 +135,7 @@ import accessibility from '@patternfly/react-styles/css/utilities/Accessibility/ ### With focus trap -When a [focus trap](/accessibility/product-development-guide#trapping-focus) is enabled on an element, a user will only be able to interact with the contents of that element until the focus trap is closed or deactivated. +When a [focus trap](/accessibility/develop#trapping-focus) is enabled on an element, a user will only be able to interact with the contents of that element until the focus trap is closed or deactivated. To enable and customize a focus trap on a drawer panel, apply the `focusTrap` property to the `` component. Enabling a focus trap with `focusTrap.enabled` will also automatically place focus on the first focusable element when the drawer panel expands, and return focus to the previously focused element when it collapses. diff --git a/packages/react-core/src/components/Drawer/examples/DrawerAdditionalSectionAboveContent.tsx b/packages/react-core/src/components/Drawer/examples/DrawerAdditionalSectionAboveContent.tsx index df64c62bda8..64395a8c59d 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerAdditionalSectionAboveContent.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerAdditionalSectionAboveContent.tsx @@ -13,7 +13,7 @@ import { export const DrawerAdditionalSectionAboveContent: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerBasic.tsx b/packages/react-core/src/components/Drawer/examples/DrawerBasic.tsx index e792e32a058..803d1d92aab 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerBasic.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerBasic.tsx @@ -14,7 +14,7 @@ import { export const DrawerBasic: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerBasicInline.tsx b/packages/react-core/src/components/Drawer/examples/DrawerBasicInline.tsx index a9ceb86666e..2df988d7740 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerBasicInline.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerBasicInline.tsx @@ -12,7 +12,7 @@ import { export const DrawerBasicInline: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerBasicPill.tsx b/packages/react-core/src/components/Drawer/examples/DrawerBasicPill.tsx index dda20e8229c..fdea78cbfe7 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerBasicPill.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerBasicPill.tsx @@ -12,7 +12,7 @@ import { export const DrawerBasicPill: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerBreakpoint.tsx b/packages/react-core/src/components/Drawer/examples/DrawerBreakpoint.tsx index 74eb0eb514f..0401fa35331 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerBreakpoint.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerBreakpoint.tsx @@ -12,7 +12,7 @@ import { export const DrawerBreakpoint: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerInlinePanelEnd.tsx b/packages/react-core/src/components/Drawer/examples/DrawerInlinePanelEnd.tsx index 9574c6a5360..071cd9a3244 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerInlinePanelEnd.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerInlinePanelEnd.tsx @@ -12,7 +12,7 @@ import { export const DrawerInlinePanelEnd: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerInlinePanelStart.tsx b/packages/react-core/src/components/Drawer/examples/DrawerInlinePanelStart.tsx index c1ed8445748..ffe46a2b9e1 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerInlinePanelStart.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerInlinePanelStart.tsx @@ -12,7 +12,7 @@ import { export const DrawerInlinePanelStart: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerModifiedContentPadding.tsx b/packages/react-core/src/components/Drawer/examples/DrawerModifiedContentPadding.tsx index 218f7b44889..9a06d057e3f 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerModifiedContentPadding.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerModifiedContentPadding.tsx @@ -12,7 +12,7 @@ import { export const DrawerModifiedContentPadding: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerModifiedPanelPadding.tsx b/packages/react-core/src/components/Drawer/examples/DrawerModifiedPanelPadding.tsx index 5f28bd3e451..afabeb0f9df 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerModifiedPanelPadding.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerModifiedPanelPadding.tsx @@ -12,7 +12,7 @@ import { export const DrawerModifiedPanelPadding: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerPanelBottom.tsx b/packages/react-core/src/components/Drawer/examples/DrawerPanelBottom.tsx index c89f7f13e1f..1b765555597 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerPanelBottom.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerPanelBottom.tsx @@ -12,7 +12,7 @@ import { export const DrawerPanelBottom: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerPanelEnd.tsx b/packages/react-core/src/components/Drawer/examples/DrawerPanelEnd.tsx index 852b9eff4c1..6c3460ace2f 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerPanelEnd.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerPanelEnd.tsx @@ -12,7 +12,7 @@ import { export const DrawerPanelEnd: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerPanelStart.tsx b/packages/react-core/src/components/Drawer/examples/DrawerPanelStart.tsx index 60c8cc515e0..a1acde0be10 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerPanelStart.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerPanelStart.tsx @@ -12,7 +12,7 @@ import { export const DrawerPanelStart: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerPillInline.tsx b/packages/react-core/src/components/Drawer/examples/DrawerPillInline.tsx index 2e3a42c2c81..16ae9532739 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerPillInline.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerPillInline.tsx @@ -12,7 +12,7 @@ import { export const DrawerBasicPill: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerResizableAtEnd.tsx b/packages/react-core/src/components/Drawer/examples/DrawerResizableAtEnd.tsx index 252c683ce33..939fcf4a441 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerResizableAtEnd.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerResizableAtEnd.tsx @@ -12,7 +12,7 @@ import { export const DrawerResizableAtEnd: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerResizableAtStart.tsx b/packages/react-core/src/components/Drawer/examples/DrawerResizableAtStart.tsx index b5d2ef2da8f..32e56665aa0 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerResizableAtStart.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerResizableAtStart.tsx @@ -12,7 +12,7 @@ import { export const DrawerResizableAtStart: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerResizableOnBottom.tsx b/packages/react-core/src/components/Drawer/examples/DrawerResizableOnBottom.tsx index 86da473b620..ecce2daa1ca 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerResizableOnBottom.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerResizableOnBottom.tsx @@ -12,7 +12,7 @@ import { export const DrawerResizableOnBottom: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerResizableOnInline.tsx b/packages/react-core/src/components/Drawer/examples/DrawerResizableOnInline.tsx index e4dd4a57bd5..16299b6db8e 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerResizableOnInline.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerResizableOnInline.tsx @@ -12,7 +12,7 @@ import { export const DrawerResizableOnInline: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Drawer/examples/DrawerStatic.tsx b/packages/react-core/src/components/Drawer/examples/DrawerStatic.tsx index 773d693207b..2f5229223cc 100644 --- a/packages/react-core/src/components/Drawer/examples/DrawerStatic.tsx +++ b/packages/react-core/src/components/Drawer/examples/DrawerStatic.tsx @@ -13,7 +13,7 @@ import accessibility from '@patternfly/react-styles/css/utilities/Accessibility/ export const DrawerStatic: React.FunctionComponent = () => { const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); + const drawerRef = useRef(null); const onExpand = () => { drawerRef.current && drawerRef.current.focus(); diff --git a/packages/react-core/src/components/Dropdown/Dropdown.tsx b/packages/react-core/src/components/Dropdown/Dropdown.tsx index e539c528af5..80219aebdc3 100644 --- a/packages/react-core/src/components/Dropdown/Dropdown.tsx +++ b/packages/react-core/src/components/Dropdown/Dropdown.tsx @@ -48,6 +48,12 @@ export interface DropdownProps extends MenuProps, OUIAProps { ouiaId?: number | string; /** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */ ouiaSafe?: boolean; + /** When applied, wraps dropdown in a container with a data-ouia-component-id.*/ + containerOuiaId?: number | string; + /** Set the value of data-ouia-safe for the container when containerOuiaId is applied. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */ + containerOuiaSafe?: boolean; + /** Sets the base component to render for the container. Defaults to */ + containerComponent?: React.ReactNode; /** z-index of the dropdown menu */ zIndex?: number; /** Additional properties to pass to the Popper */ @@ -86,11 +92,16 @@ const DropdownBase: React.FunctionComponent = ({ shouldFocusFirstItemOnOpen = false, shouldPreventScrollOnItemFocus = true, focusTimeoutDelay = 0, + containerOuiaId, + containerOuiaSafe = true, + containerComponent = 'span', ...props }: DropdownProps) => { const localMenuRef = useRef(undefined); const localToggleRef = useRef(undefined); const ouiaProps = useOUIAProps(Dropdown.displayName, ouiaId, ouiaSafe); + const ContainerComponent = containerComponent as any; + const containerOuiaProps = useOUIAProps('Dropdown container', containerOuiaId, containerOuiaSafe); const menuRef = (innerRef as React.RefObject) || localMenuRef; const toggleRef = @@ -185,7 +196,8 @@ const DropdownBase: React.FunctionComponent = ({ ); - return ( + + const popper = ( = ({ {...popperProps} /> ); + + return containerOuiaId ? {popper} : popper; }; export const Dropdown = forwardRef((props: DropdownProps, ref: React.Ref) => ( diff --git a/packages/react-core/src/components/Dropdown/__tests__/Dropdown.test.tsx b/packages/react-core/src/components/Dropdown/__tests__/Dropdown.test.tsx index 73e6d5068d5..b8b78399fe5 100644 --- a/packages/react-core/src/components/Dropdown/__tests__/Dropdown.test.tsx +++ b/packages/react-core/src/components/Dropdown/__tests__/Dropdown.test.tsx @@ -201,6 +201,32 @@ test('onOpenChange is called when passed and user presses esc key', async () => expect(onOpenChange).toBeCalledTimes(1); }); +test('applies containerOuiaId to parent element', () => { + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + const dropdownToggle = screen.getByRole('button', { name: 'Dropdown' }); + const dropdownParent = dropdownToggle?.parentNode?.parentNode; + expect(dropdownParent).toHaveAttribute('data-ouia-component-id', 'test-dropdown'); + expect(dropdownParent).toHaveAttribute('data-ouia-component-type', 'PF6/Dropdown container'); + expect(dropdownParent?.tagName).toBe('SPAN'); +}); + +test('Renders with custom container element when containerComponent is passed', () => { + render( + toggle(toggleRef)}> + {dropdownChildren} + + ); + + const dropdownToggle = screen.getByRole('button', { name: 'Dropdown' }); + const dropdownParent = dropdownToggle?.parentNode?.parentNode; + expect(dropdownParent?.tagName).toBe('DIV'); +}); + test('match snapshot', () => { const { asFragment } = render( `, pass a short messag ```ts file="./DropdownWithDescriptions.tsx" ``` + +### Split toggle with checkbox + +To combine a checkbox or other control with a dropdown menu, use a split button. + +A `` can be rendered as a split button via `splitButtonItems`. Elements to be displayed before the dropdown toggle button (like the ``) must be included in the `splitButtonItems`. + +If the dropdown menu closes upon selection, you will need to manually shift focus back to the toggle element after a user selects an item from the menu. + +```ts file="./DropdownWithSplit.tsx" + +``` diff --git a/packages/react-core/src/components/Dropdown/examples/DropdownWithSplit.tsx b/packages/react-core/src/components/Dropdown/examples/DropdownWithSplit.tsx new file mode 100644 index 00000000000..140a99510cd --- /dev/null +++ b/packages/react-core/src/components/Dropdown/examples/DropdownWithSplit.tsx @@ -0,0 +1,97 @@ +import { + Dropdown, + MenuToggle, + MenuToggleCheckbox, + DropdownItem, + DropdownList, + Divider, + MenuToggleElement +} from '@patternfly/react-core'; +import { useRef, useState } from 'react'; + +export const DropdownSplitButtonText: React.FunctionComponent = () => { + const [isOpen, setIsOpen] = useState(false); + const toggleRef = useRef(null); + + const onFocus = () => { + if (!toggleRef.current) { + return; + } + + const toggleButton = toggleRef.current.querySelector('button[aria-expanded]'); + toggleButton?.focus(); + }; + + const onSelect = () => { + setIsOpen(false); + onFocus(); + }; + + const onToggleClick = () => { + setIsOpen(!isOpen); + }; + + return ( + setIsOpen(isOpen)} + toggle={(toggleRefCallback: React.Ref) => ( + { + // Handle both callback ref and useRef + if (typeof toggleRefCallback === 'function') { + toggleRefCallback(node); + } else if (toggleRefCallback) { + (toggleRefCallback as React.MutableRefObject).current = node; + } + (toggleRef as React.MutableRefObject).current = node; + }} + splitButtonItems={[ + + ]} + aria-label="Dropdown with checkbox split button" + onClick={onToggleClick} + isExpanded={isOpen} + /> + )} + isOpen={isOpen} + > + + + Action + + ev.preventDefault()} + > + Link + + + Disabled Action + + + Disabled Link + + + Aria-disabled Link + + + + Separated Action + + ev.preventDefault()}> + Separated Link + + + + ); +}; diff --git a/packages/react-core/src/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap b/packages/react-core/src/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap index 63dab5eee9e..f9529ee933f 100644 --- a/packages/react-core/src/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap +++ b/packages/react-core/src/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap @@ -90,12 +90,24 @@ exports[`DualListSelector with search inputs 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" width="1em" > - + + + + + + - + + + + + +
diff --git a/packages/react-core/src/components/ExpandableSection/ExpandableSection.tsx b/packages/react-core/src/components/ExpandableSection/ExpandableSection.tsx index e6331b70b35..220e83bab9d 100644 --- a/packages/react-core/src/components/ExpandableSection/ExpandableSection.tsx +++ b/packages/react-core/src/components/ExpandableSection/ExpandableSection.tsx @@ -62,6 +62,10 @@ export interface ExpandableSectionProps extends Omit, + hasToggleIcon = true, children, isExpanded, isDetached, @@ -267,13 +273,10 @@ class ExpandableSection extends Component onToggle(event, !propOrStateIsExpanded)} - {...(variant !== ExpandableSectionVariant.truncate && { - icon: ( - - - - ) - })} + {...(variant !== ExpandableSectionVariant.truncate && + hasToggleIcon && { + icon: {toggleIcon} + })} aria-label={toggleAriaLabel} aria-labelledby={toggleAriaLabelledBy} > diff --git a/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSection.test.tsx b/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSection.test.tsx index 651214938f5..05e1c153181 100644 --- a/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSection.test.tsx +++ b/packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSection.test.tsx @@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'; import { ExpandableSection, ExpandableSectionVariant } from '../ExpandableSection'; import styles from '@patternfly/react-styles/css/components/ExpandableSection/expandable-section'; +import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon'; const props = { contentId: 'content-id', toggleId: 'toggle-id' }; @@ -271,3 +272,33 @@ test('Renders with div wrapper when toggleWrapper="div"', () => { const toggle = screen.getByRole('button').parentElement; expect(toggle?.tagName).toBe('DIV'); }); + +test('Can render custom toggle icon', () => { + render(}>Test content); + + expect(screen.getByTestId('bell-icon')).toBeInTheDocument(); +}); + +test('Does not render toggle icon when hasToggleIcon is false', () => { + render(Test content); + + const button = screen.getByRole('button'); + expect(button.querySelector('.pf-v6-c-expandable-section__toggle-icon')).not.toBeInTheDocument(); +}); + +test('Does not render custom toggle icon when hasToggleIcon is false', () => { + render( + } hasToggleIcon={false}> + Test content + + ); + + expect(screen.queryByTestId('bell-icon')).not.toBeInTheDocument(); +}); + +test('Renders toggle icon by default when hasToggleIcon is true', () => { + render(Test content); + + const button = screen.getByRole('button'); + expect(button.querySelector('.pf-v6-c-expandable-section__toggle-icon')).toBeInTheDocument(); +}); diff --git a/packages/react-core/src/components/ExpandableSection/__tests__/__snapshots__/ExpandableSection.test.tsx.snap b/packages/react-core/src/components/ExpandableSection/__tests__/__snapshots__/ExpandableSection.test.tsx.snap index 8b9918bb6d2..fc4058b5569 100644 --- a/packages/react-core/src/components/ExpandableSection/__tests__/__snapshots__/ExpandableSection.test.tsx.snap +++ b/packages/react-core/src/components/ExpandableSection/__tests__/__snapshots__/ExpandableSection.test.tsx.snap @@ -29,12 +29,24 @@ exports[`Disclosure ExpandableSection 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 256 512" width="1em" > - + + + + + + @@ -82,12 +94,24 @@ exports[`ExpandableSection 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 256 512" width="1em" > - + + + + + + @@ -136,12 +160,24 @@ exports[`Renders ExpandableSection expanded 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 256 512" width="1em" > - + + + + + + @@ -189,12 +225,24 @@ exports[`Renders ExpandableSection indented 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 256 512" width="1em" > - + + + + + + @@ -241,12 +289,24 @@ exports[`Renders Uncontrolled ExpandableSection 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 256 512" width="1em" > - + + + + + + diff --git a/packages/react-core/src/components/FileUpload/FileUpload.tsx b/packages/react-core/src/components/FileUpload/FileUpload.tsx index d7e7cc09043..7317bb31141 100644 --- a/packages/react-core/src/components/FileUpload/FileUpload.tsx +++ b/packages/react-core/src/components/FileUpload/FileUpload.tsx @@ -4,11 +4,10 @@ import { readFile, fileReaderType } from '../../helpers/fileUtils'; import { DropEvent } from '../../helpers/typeUtils'; import { fromEvent } from 'file-selector'; -export interface FileUploadProps - extends Omit< - FileUploadFieldProps, - 'children' | 'onBrowseButtonClick' | 'onClearButtonClick' | 'isDragActive' | 'containerRef' - > { +export interface FileUploadProps extends Omit< + FileUploadFieldProps, + 'children' | 'onBrowseButtonClick' | 'onClearButtonClick' | 'isDragActive' | 'containerRef' +> { /** Flag to allow editing of a text file's contents after it is selected from disk. */ allowEditingUploadedText?: boolean; /** Aria-label for the text area. */ diff --git a/packages/react-core/src/components/FileUpload/FileUploadField.tsx b/packages/react-core/src/components/FileUpload/FileUploadField.tsx index edbfc5d2cad..8be75a1b95f 100644 --- a/packages/react-core/src/components/FileUpload/FileUploadField.tsx +++ b/packages/react-core/src/components/FileUpload/FileUploadField.tsx @@ -172,29 +172,31 @@ export const FileUploadField: React.FunctionComponent = ({
-
- {!hideDefaultPreview && type === fileReaderType.text && ( -