diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/getNodeForAttributeFixer.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getNodeForAttributeFixer.ts
new file mode 100644
index 00000000..a55ecd0d
--- /dev/null
+++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getNodeForAttributeFixer.ts
@@ -0,0 +1,41 @@
+import { Rule } from "eslint";
+import { JSXAttribute } from "estree-jsx";
+import { getVariableDeclaration } from "./JSXAttributes";
+
+/** Used to find the node where a prop value is initially assigned, to then be passed
+ * as a fixer function's nodeOrToken argument. Useful for when a prop may have an inline value, e.g. ``, or
+ * is passed an identifier, e.g. `const val = "value"; `
+ */
+export function getNodeForAttributeFixer(
+ context: Rule.RuleContext,
+ attribute: JSXAttribute
+) {
+ if (!attribute.value) {
+ return;
+ }
+
+ if (
+ attribute.value.type === "JSXExpressionContainer" &&
+ attribute.value.expression.type === "Identifier"
+ ) {
+ const scope = context.getSourceCode().getScope(attribute);
+ const variableDeclaration = getVariableDeclaration(
+ attribute.value.expression.name,
+ scope
+ );
+
+ return variableDeclaration && variableDeclaration.defs[0].node.init;
+ }
+
+ if (attribute.value.type === "Literal") {
+ return attribute.value;
+ }
+ if (
+ attribute.value.type === "JSXExpressionContainer" &&
+ ["ObjectExpression", "MemberExpression"].includes(
+ attribute.value.expression.type
+ )
+ ) {
+ return attribute.value.expression;
+ }
+}
diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/getObjectProperty.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getObjectProperty.ts
new file mode 100644
index 00000000..8446eaf2
--- /dev/null
+++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getObjectProperty.ts
@@ -0,0 +1,39 @@
+import { Rule } from "eslint";
+import { Property, Identifier } from "estree-jsx";
+import { getVariableDeclaration } from "./JSXAttributes";
+
+/** Can be used to run logic on the specified property of an ObjectExpression or
+ * only if the specified property exists.
+ */
+export function getObjectProperty(
+ context: Rule.RuleContext,
+ properties: Property[],
+ name: string
+) {
+ if (!properties.length) {
+ return;
+ }
+
+ const matchedProperty = properties.find((property) => {
+ const isIdentifier = property.key.type === "Identifier";
+ const { computed } = property;
+
+ // E.g. const key = "key"; {[key]: value}
+ if (isIdentifier && computed) {
+ const scope = context.getSourceCode().getScope(property);
+ const propertyName = (property.key as Identifier).name;
+ const propertyVariable = getVariableDeclaration(propertyName, scope);
+ return propertyVariable?.defs[0].node.init.value === name;
+ }
+ // E.g. {key: value}
+ if (isIdentifier && !computed) {
+ return (property.key as Identifier).name === name;
+ }
+ // E.g. {"key": value} or {["key"]: value}
+ if (property.key.type === "Literal") {
+ return property.key.value === name;
+ }
+ });
+
+ return matchedProperty;
+}
diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts
index dc0bbe6b..19608166 100644
--- a/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts
+++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts
@@ -10,7 +10,9 @@ export * from "./getEndRange";
export * from "./getFromPackage";
export * from "./getImportedName";
export * from "./getLocalComponentName";
+export * from "./getNodeForAttributeFixer";
export * from "./getNodeName";
+export * from "./getObjectProperty";
export * from "./getSpecifierFromImports";
export * from "./getText";
export * from "./hasCodemodDataTag";
@@ -24,4 +26,5 @@ export * from "./nodeMatches";
export * from "./pfPackageMatches";
export * from "./removeElement";
export * from "./removeEmptyLineAfter";
+export * from "./removePropertiesFromObjectExpression";
export * from "./renameProps";
diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/removePropertiesFromObjectExpression.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/removePropertiesFromObjectExpression.ts
new file mode 100644
index 00000000..28822924
--- /dev/null
+++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/removePropertiesFromObjectExpression.ts
@@ -0,0 +1,42 @@
+import { Property } from "estree-jsx";
+
+/** Can be used to take the returned array and join it into a replacement string of object
+ * key:value pairs.
+ */
+export function removePropertiesFromObjectExpression(
+ currentProperties: Property[],
+ propertiesToRemove: (Property | undefined)[]
+) {
+ if (!currentProperties) {
+ return [];
+ }
+ if (!propertiesToRemove) {
+ return currentProperties;
+ }
+
+ const propertyNamesToRemove = propertiesToRemove.map((property) => {
+ if (property?.key.type === "Identifier") {
+ return property.key.name;
+ }
+
+ if (property?.key.type === "Literal") {
+ return property.key.value;
+ }
+
+ return "";
+ });
+
+ const propertiesToKeep = currentProperties.filter((property) => {
+ if (property.key.type === "Identifier") {
+ return !propertyNamesToRemove.includes(property.key.name);
+ }
+
+ if (property.key.type === "Literal") {
+ return propertyNamesToRemove.includes(property.key.value);
+ }
+
+ return false;
+ });
+
+ return propertiesToKeep;
+}
diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.md b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.md
new file mode 100644
index 00000000..3bf9cfbc
--- /dev/null
+++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.md
@@ -0,0 +1,17 @@
+### card-updated-clickable-markup [(#10859)](https://github.com/patternfly/patternfly-react/pull/10859)
+
+The markup for clickable-only cards has been updated. Additionally, the `selectableActions.selectableActionId` and `selectableActions.name` props are no longer necessary to pass to CardHeader for clickable-only cards. Passing them in will not cause any errors, but running the fix for this rule will remove them.
+
+#### Examples
+
+In:
+
+```jsx
+%inputExample%
+```
+
+Out:
+
+```jsx
+%outputExample%
+```
diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.test.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.test.ts
new file mode 100644
index 00000000..d61619ae
--- /dev/null
+++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.test.ts
@@ -0,0 +1,121 @@
+const ruleTester = require("../../ruletester");
+import * as rule from "./card-updated-clickable-markup";
+
+ruleTester.run("card-updated-clickable-markup", rule, {
+ valid: [
+ {
+ code: ``,
+ },
+ {
+ code: ``,
+ },
+ {
+ code: `import { Card } from '@patternfly/react-core'; `,
+ },
+ {
+ code: `import { Card } from '@patternfly/react-core'; `,
+ },
+ {
+ code: `import { CardHeader } from '@patternfly/react-core'; `,
+ },
+ ],
+ invalid: [
+ {
+ code: `import { Card, CardHeader } from '@patternfly/react-core'; `,
+ output: `import { Card, CardHeader } from '@patternfly/react-core'; `,
+ errors: [
+ {
+ message: "The markup for clickable-only cards has been updated.",
+ type: "JSXElement",
+ },
+ ],
+ },
+ {
+ code: `import { Card, CardHeader } from '@patternfly/react-core'; `,
+ output: `import { Card, CardHeader } from '@patternfly/react-core'; `,
+ errors: [
+ {
+ message:
+ "The markup for clickable-only cards has been updated.Additionally, the `selectableActions.selectableActionId` and `selectableActions.name` props are no longer necessary to pass to CardHeader for clickable-only cards.",
+ type: "JSXElement",
+ },
+ ],
+ },
+ {
+ code: `import { Card, CardHeader } from '@patternfly/react-core'; `,
+ output: `import { Card, CardHeader } from '@patternfly/react-core'; `,
+ errors: [
+ {
+ message:
+ "The markup for clickable-only cards has been updated.Additionally, the `selectableActions.selectableActionId` and `selectableActions.name` props are no longer necessary to pass to CardHeader for clickable-only cards.",
+ // message: `The markup for clickable-only cards has been updated, now using button and anchor elements for the respective clickable action. The \`selectableActions.selectableActionId\` and \`selectableActions.name\` props are also no longer necessary for clickable-only cards. Passing them in will not cause any errors, but running the fix for this rule will remove them.`,
+ type: "JSXElement",
+ },
+ ],
+ },
+ {
+ code: `import { Card, CardHeader } from '@patternfly/react-core'; const obj = {name: 'Test', selectableActionId: 'Id'}; `,
+ output: `import { Card, CardHeader } from '@patternfly/react-core'; const obj = {}; `,
+ errors: [
+ {
+ message:
+ "The markup for clickable-only cards has been updated.Additionally, the `selectableActions.selectableActionId` and `selectableActions.name` props are no longer necessary to pass to CardHeader for clickable-only cards.",
+ type: "JSXElement",
+ },
+ ],
+ },
+ {
+ code: `import { Card, CardHeader } from '@patternfly/react-core'; const obj = {to: "#", name: 'Test', extra: "thing", selectableActionId: 'Id'}; `,
+ output: `import { Card, CardHeader } from '@patternfly/react-core'; const obj = {to: "#", extra: "thing"}; `,
+ errors: [
+ {
+ message:
+ "The markup for clickable-only cards has been updated.Additionally, the `selectableActions.selectableActionId` and `selectableActions.name` props are no longer necessary to pass to CardHeader for clickable-only cards.",
+ type: "JSXElement",
+ },
+ ],
+ },
+ // Aliased
+ {
+ code: `import { Card as CustomCard, CardHeader as CustomCardHeader } from '@patternfly/react-core'; `,
+ output: `import { Card as CustomCard, CardHeader as CustomCardHeader } from '@patternfly/react-core'; `,
+ errors: [
+ {
+ message: "The markup for clickable-only cards has been updated.",
+ type: "JSXElement",
+ },
+ ],
+ },
+ // Dist Imports
+ {
+ code: `import { Card, CardHeader } from '@patternfly/react-core/dist/esm/components/Card/index.js'; `,
+ output: `import { Card, CardHeader } from '@patternfly/react-core/dist/esm/components/Card/index.js'; `,
+ errors: [
+ {
+ message: "The markup for clickable-only cards has been updated.",
+ type: "JSXElement",
+ },
+ ],
+ },
+ {
+ code: `import { Card, CardHeader } from '@patternfly/react-core/dist/js/components/Card/index.js'; `,
+ output: `import { Card, CardHeader } from '@patternfly/react-core/dist/js/components/Card/index.js'; `,
+ errors: [
+ {
+ message: "The markup for clickable-only cards has been updated.",
+ type: "JSXElement",
+ },
+ ],
+ },
+ {
+ code: `import { Card, CardHeader } from '@patternfly/react-core/dist/dynamic/components/Card/index.js'; `,
+ output: `import { Card, CardHeader } from '@patternfly/react-core/dist/dynamic/components/Card/index.js'; `,
+ errors: [
+ {
+ message: "The markup for clickable-only cards has been updated.",
+ type: "JSXElement",
+ },
+ ],
+ },
+ ],
+});
diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.ts
new file mode 100644
index 00000000..c440f4e8
--- /dev/null
+++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.ts
@@ -0,0 +1,118 @@
+import { Rule } from "eslint";
+import { JSXElement, Property, Literal } from "estree-jsx";
+import {
+ getAllImportsFromPackage,
+ checkMatchingJSXOpeningElement,
+ getAttribute,
+ getAttributeValue,
+ getSpecifierFromImports,
+ getChildJSXElementByName,
+ getObjectProperty,
+ removePropertiesFromObjectExpression,
+ getNodeForAttributeFixer,
+} from "../../helpers";
+
+// https://github.com/patternfly/patternfly-react/pull/10859
+module.exports = {
+ meta: { fixable: "code" },
+ create: function (context: Rule.RuleContext) {
+ const basePackage = "@patternfly/react-core";
+ const componentImports = getAllImportsFromPackage(context, basePackage, [
+ "Card",
+ "CardHeader",
+ ]);
+ const cardImport = getSpecifierFromImports(componentImports, "Card");
+ const cardHeaderImport = getSpecifierFromImports(
+ componentImports,
+ "CardHeader"
+ );
+
+ // Actionable cards won't work as intended if both components aren't imported, hence
+ // we shouldn't need to relay any message if that is the case
+ return !cardImport || !cardHeaderImport
+ ? {}
+ : {
+ JSXElement(node: JSXElement) {
+ if (
+ checkMatchingJSXOpeningElement(node.openingElement, cardImport)
+ ) {
+ const isClickableProp = getAttribute(node, "isClickable");
+ const isSelectableProp = getAttribute(node, "isSelectable");
+
+ if ((isClickableProp && isSelectableProp) || !isClickableProp) {
+ return;
+ }
+
+ const cardHeaderChild = getChildJSXElementByName(
+ node,
+ cardHeaderImport.local.name
+ );
+ const selectableActionsProp = cardHeaderChild
+ ? getAttribute(cardHeaderChild, "selectableActions")
+ : undefined;
+ if (!cardHeaderChild || !selectableActionsProp) {
+ return;
+ }
+ const selectableActionsValue = getAttributeValue(
+ context,
+ selectableActionsProp.value
+ );
+ const nameProperty = getObjectProperty(
+ context,
+ selectableActionsValue,
+ "name"
+ );
+ const idProperty = getObjectProperty(
+ context,
+ selectableActionsValue,
+ "selectableActionId"
+ );
+
+ const baseMessage =
+ "The markup for clickable-only cards has been updated.";
+ const message = `${baseMessage}${
+ nameProperty || idProperty
+ ? "Additionally, the `selectableActions.selectableActionId` and `selectableActions.name` props are no longer necessary to pass to CardHeader for clickable-only cards."
+ : ""
+ }`;
+ context.report({
+ node,
+ message,
+ fix(fixer) {
+ const validPropertiesToRemove = [
+ nameProperty,
+ idProperty,
+ ].filter((property) => !!property);
+ if (
+ !validPropertiesToRemove.length ||
+ !selectableActionsProp.value
+ ) {
+ return [];
+ }
+ const propertiesToKeep = removePropertiesFromObjectExpression(
+ selectableActionsValue,
+ validPropertiesToRemove
+ );
+ const replacementProperties = propertiesToKeep
+ .map((property: Property) =>
+ context.getSourceCode().getText(property)
+ )
+ .join(", ");
+
+ const nodeToUpdate = getNodeForAttributeFixer(
+ context,
+ selectableActionsProp
+ );
+ return fixer.replaceText(
+ nodeToUpdate,
+ propertiesToKeep.length
+ ? `{${replacementProperties}}`
+ : "{}"
+ );
+ },
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/cardUpdatedClickableMarkupInput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/cardUpdatedClickableMarkupInput.tsx
new file mode 100644
index 00000000..5df735c1
--- /dev/null
+++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/cardUpdatedClickableMarkupInput.tsx
@@ -0,0 +1,31 @@
+import { Card, CardHeader } from "@patternfly/react-core";
+
+export const CardUpdatedClickableMarkupInput = () => {
+ const selectableActionsObj = { name: "Test2", selectableActionId: "Id2" };
+ const selectableActionsObjMany = {
+ to: "#",
+ name: "Test2",
+ selectableActionProps: {},
+ selectableActionId: "Id2",
+ };
+
+ return (
+ <>
+
+ {},
+ }}
+ />
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/cardUpdatedClickableMarkupOutput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/cardUpdatedClickableMarkupOutput.tsx
new file mode 100644
index 00000000..469780ad
--- /dev/null
+++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/cardUpdatedClickableMarkupOutput.tsx
@@ -0,0 +1,22 @@
+import { Card, CardHeader } from "@patternfly/react-core";
+
+export const CardUpdatedClickableMarkupInput = () => {
+ const selectableActionsObj = {};
+ const selectableActionsObjMany = {to: "#", selectableActionProps: {}};
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+};