diff --git a/packages/react-core/src/components/Card/Card.tsx b/packages/react-core/src/components/Card/Card.tsx index 77a01cc807d..68e4fe3bcc0 100644 --- a/packages/react-core/src/components/Card/Card.tsx +++ b/packages/react-core/src/components/Card/Card.tsx @@ -16,11 +16,15 @@ export interface CardProps extends React.HTMLProps, OUIAProps { isCompact?: boolean; /** Modifies the card to include selectable styling */ isSelectable?: boolean; - /** Specifies the card is selectable, and applies the new raised styling on hover and select */ + /** @deprecated Specifies the card is selectable, and applies raised styling on hover and select */ isSelectableRaised?: boolean; /** Modifies the card to include selected styling */ isSelected?: boolean; - /** Modifies a raised selectable card to have disabled styling */ + /** Modifies the card to include clickable styling */ + isClickable?: boolean; + /** Modifies a clickable or selectable card to have disabled styling. */ + isDisabled?: boolean; + /** @deprecated Modifies a raised selectable card to have disabled styling */ isDisabledRaised?: boolean; /** Modifies the card to include flat styling */ isFlat?: boolean; @@ -34,11 +38,11 @@ export interface CardProps extends React.HTMLProps, OUIAProps { isPlain?: boolean; /** Flag indicating if a card is expanded. Modifies the card to be expandable. */ isExpanded?: boolean; - /** Flag indicating that the card should render a hidden input to make it selectable */ + /** @deprecated Flag indicating that the card should render a hidden input to make it selectable */ hasSelectableInput?: boolean; - /** Aria label to apply to the selectable input if one is rendered */ + /** @deprecated Aria label to apply to the selectable input if one is rendered */ selectableInputAriaLabel?: string; - /** Callback that executes when the selectable input is changed */ + /** @deprecated Callback that executes when the selectable input is changed */ onSelectableInputChange?: (event: React.FormEvent, labelledBy: string) => void; /** Value to overwrite the randomly generated data-ouia-component-id.*/ ouiaId?: number | string; @@ -50,6 +54,9 @@ interface CardContextProps { cardId: string; registerTitleId: (id: string) => void; isExpanded: boolean; + isClickable: boolean; + isSelectable: boolean; + isDisabled: boolean; } interface AriaProps { @@ -60,7 +67,10 @@ interface AriaProps { export const CardContext = React.createContext>({ cardId: '', registerTitleId: () => {}, - isExpanded: false + isExpanded: false, + isClickable: false, + isSelectable: false, + isDisabled: false }); export const Card: React.FunctionComponent = ({ @@ -70,6 +80,8 @@ export const Card: React.FunctionComponent = ({ component = 'div', isCompact = false, isSelectable = false, + isClickable = false, + isDisabled = false, isSelectableRaised = false, isSelected = false, isDisabledRaised = false, @@ -104,9 +116,18 @@ export const Card: React.FunctionComponent = ({ if (isSelectableRaised) { return css(styles.modifiers.selectableRaised, isSelected && styles.modifiers.selectedRaised); } + if (isSelectable && isClickable) { + return css(styles.modifiers.selectable, styles.modifiers.clickable, isSelected && styles.modifiers.current); + } + if (isSelectable) { return css(styles.modifiers.selectable, isSelected && styles.modifiers.selected); } + + if (isClickable) { + return css(styles.modifiers.clickable, isSelected && styles.modifiers.selected); + } + return ''; }; @@ -136,7 +157,10 @@ export const Card: React.FunctionComponent = ({ value={{ cardId: id, registerTitleId, - isExpanded + isExpanded, + isClickable, + isSelectable, + isDisabled }} > {hasSelectableInput && ( @@ -146,7 +170,7 @@ export const Card: React.FunctionComponent = ({ {...ariaProps} type="checkbox" checked={isSelected} - onChange={event => onSelectableInputChange(event, id)} + onChange={(event) => onSelectableInputChange(event, id)} disabled={isDisabledRaised} tabIndex={-1} /> @@ -163,9 +187,10 @@ export const Card: React.FunctionComponent = ({ isFullHeight && styles.modifiers.fullHeight, isPlain && styles.modifiers.plain, getSelectableModifiers(), + isDisabled && styles.modifiers.disabled, className )} - tabIndex={isSelectable || isSelectableRaised ? '0' : undefined} + tabIndex={isSelectableRaised ? '0' : undefined} {...props} {...ouiaProps} > diff --git a/packages/react-core/src/components/Card/CardHeader.tsx b/packages/react-core/src/components/Card/CardHeader.tsx index d5a4e5dc0d0..862f7fbf9d8 100644 --- a/packages/react-core/src/components/Card/CardHeader.tsx +++ b/packages/react-core/src/components/Card/CardHeader.tsx @@ -4,8 +4,11 @@ import styles from '@patternfly/react-styles/css/components/Card/card'; import { CardContext } from './Card'; import { CardHeaderMain } from './CardHeaderMain'; import { CardActions } from './CardActions'; +import { CardSelectableActions } from './CardSelectableActions'; import { Button } from '../Button'; import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon'; +import { Radio } from '../Radio'; +import { Checkbox } from '../Checkbox'; export interface CardHeaderActionsObject { /** Actions of the card header */ @@ -16,6 +19,35 @@ export interface CardHeaderActionsObject { className?: string; } +export interface CardHeaderSelectableActionsObject { + /** Determines the type of input to be used for a selectable card. */ + variant?: 'single' | 'multiple'; + /** Flag indicating that the actions have no offset */ + hasNoOffset?: boolean; + /** Additional classes added to the selectable actions wrapper */ + className?: string; + /** ID passed to the selectable or clickable input */ + selectableActionId: string; + /** Adds an accessible label to the selectable or clickable input */ + selectableActionAriaLabel?: string; + /** Adds an accessible label to the selectable or clickable input by passing in a + * space separated list of id's. + */ + selectableActionAriaLabelledby?: string; + /** Callback for when a selectable card input changes */ + onChange?: (event: React.FormEvent, checked: boolean) => void; + /** Action to call when clickable card is clicked */ + onClickAction?: (event: React.FormEvent | React.MouseEvent) => void; + /** Link to navigate to when clickable card is clicked */ + to?: string; + /** Flag to indicate whether a clickable card's link should open in a new tab/window. */ + isExternalLink?: boolean; + /** Name for the input element of a clickable or selectable card. */ + name?: string; + /** Flag indicating whether the selectable card input is checked */ + isChecked?: boolean; +} + export interface CardHeaderProps extends React.HTMLProps { /** Content rendered inside the card header */ children?: React.ReactNode; @@ -23,6 +55,8 @@ export interface CardHeaderProps extends React.HTMLProps { className?: string; /** Actions of the card header */ actions?: CardHeaderActionsObject; + /** Selectable actions of the card header */ + selectableActions?: CardHeaderSelectableActionsObject; /** ID of the card header. */ id?: string; /** Callback expandable card */ @@ -37,6 +71,7 @@ export const CardHeader: React.FunctionComponent = ({ children, className, actions, + selectableActions, id, onExpand, toggleButtonProps, @@ -44,7 +79,7 @@ export const CardHeader: React.FunctionComponent = ({ ...props }: CardHeaderProps) => ( - {({ cardId }) => { + {({ cardId, isClickable, isSelectable, isDisabled: isCardDisabled }) => { const cardHeaderToggle = (
); + if (actions?.actions && !(isClickable && isSelectable)) { + // eslint-disable-next-line no-console + console.warn( + `${ + isClickable ? 'Clickable' : 'Selectable' + } only cards should not contain any other actions. If you wish to include additional actions, use a clickable and selectable card.` + ); + } + + const handleActionClick = (event: React.FormEvent | React.MouseEvent) => { + if (isClickable) { + if (selectableActions?.onClickAction) { + selectableActions.onClickAction(event); + } else if (selectableActions?.to) { + window.open(selectableActions.to, selectableActions.isExternalLink ? '_blank' : '_self'); + } + } + }; + + const getClickableSelectableProps = () => { + const baseProps = { + className: 'pf-m-standalone', + inputClassName: isClickable && !isSelectable && 'pf-v5-screen-reader', + label: <>, + 'aria-label': selectableActions.selectableActionAriaLabel, + 'aria-labelledby': selectableActions.selectableActionAriaLabelledby, + id: selectableActions.selectableActionId, + name: selectableActions.name, + isDisabled: isCardDisabled + }; + + if (isClickable && !isSelectable) { + return { ...baseProps, onClick: handleActionClick }; + } + if (isSelectable) { + return { ...baseProps, onChange: selectableActions.onChange, isChecked: selectableActions.isChecked }; + } + + return baseProps; + }; + return (
= ({ {...props} > {onExpand && !isToggleRightAligned && cardHeaderToggle} - {actions && {actions.actions} } + {(actions || (selectableActions && (isClickable || isSelectable))) && ( + + {actions?.actions} + {selectableActions && (isClickable || isSelectable) && ( + + {selectableActions?.variant === 'single' || (isClickable && !isSelectable) ? ( + + ) : ( + + )} + + )} + + )} {children && {children}} {onExpand && isToggleRightAligned && cardHeaderToggle}
diff --git a/packages/react-core/src/components/Card/CardSelectableActions.tsx b/packages/react-core/src/components/Card/CardSelectableActions.tsx new file mode 100644 index 00000000000..3b86314bdc9 --- /dev/null +++ b/packages/react-core/src/components/Card/CardSelectableActions.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { css } from '@patternfly/react-styles'; +import styles from '@patternfly/react-styles/css/components/Card/card'; + +export interface CardActionsProps extends React.HTMLProps { + /** Content rendered inside the card action */ + children?: React.ReactNode; + /** Additional classes added to the action */ + className?: string; +} + +export const CardSelectableActions: React.FunctionComponent = ({ + children, + className, + ...props +}: CardActionsProps) => ( +
+ {children} +
+); +CardSelectableActions.displayName = 'CardSelectableActions'; diff --git a/packages/react-core/src/components/Card/__tests__/Card.test.tsx b/packages/react-core/src/components/Card/__tests__/Card.test.tsx index b5a1eaef6d7..2c6e5f94d92 100644 --- a/packages/react-core/src/components/Card/__tests__/Card.test.tsx +++ b/packages/react-core/src/components/Card/__tests__/Card.test.tsx @@ -33,7 +33,7 @@ describe('Card', () => { test('allows passing in a React Component as the component', () => { const Component = () =>
im a div
; - render(); + render(); expect(screen.getByText('im a div')).toBeInTheDocument(); }); @@ -47,16 +47,18 @@ describe('Card', () => { const card = screen.getByText('selectable card'); expect(card).toHaveClass('pf-m-selectable'); - expect(card).toHaveAttribute('tabindex', '0'); }); test('card with isSelectable and isSelected applied ', () => { - render(selected and selectable card); + render( + + selected and selectable card + + ); const card = screen.getByText('selected and selectable card'); expect(card).toHaveClass('pf-m-selectable'); expect(card).toHaveClass('pf-m-selected'); - expect(card).toHaveAttribute('tabindex', '0'); }); test('card with only isSelected applied - not change', () => { @@ -81,7 +83,11 @@ describe('Card', () => { }); test('card with isSelectableRaised and isSelected applied ', () => { - render(raised selected card); + render( + + raised selected card + + ); const card = screen.getByText('raised selected card'); expect(card).toHaveClass('pf-m-selectable-raised'); @@ -154,7 +160,6 @@ describe('Card', () => { }); test('card applies the supplied card title as the aria label of the hidden input', () => { - // this component is used to mock the CardTitle's title registry behavior to keep this a pure unit test const MockCardTitle = ({ children }) => { const { registerTitleId } = React.useContext(CardContext); @@ -179,7 +184,6 @@ describe('Card', () => { }); test('card prioritizes selectableInputAriaLabel over card title labelling via card title', () => { - // this component is used to mock the CardTitle's title registry behavior to keep this a pure unit test const MockCardTitle = ({ children }) => { const { registerTitleId } = React.useContext(CardContext); diff --git a/packages/react-core/src/components/Card/__tests__/__snapshots__/CardHeader.test.tsx.snap b/packages/react-core/src/components/Card/__tests__/__snapshots__/CardHeader.test.tsx.snap index 43ec39e21a4..2e23b19d96b 100644 --- a/packages/react-core/src/components/Card/__tests__/__snapshots__/CardHeader.test.tsx.snap +++ b/packages/react-core/src/components/Card/__tests__/__snapshots__/CardHeader.test.tsx.snap @@ -7,9 +7,7 @@ exports[`CardHeader actions are rendered 1`] = ` >
- -
+ /> `; diff --git a/packages/react-core/src/components/Card/examples/Card.md b/packages/react-core/src/components/Card/examples/Card.md index 449fd16dece..c9f328ba881 100644 --- a/packages/react-core/src/components/Card/examples/Card.md +++ b/packages/react-core/src/components/Card/examples/Card.md @@ -3,7 +3,16 @@ id: Card section: components cssPrefix: pf-c-card propComponents: - ['Card', 'CardHeader', 'CardHeaderActionsObject', 'CardTitle', 'CardBody', 'CardFooter', 'CardExpandableContent'] + [ + 'Card', + 'CardHeader', + 'CardHeaderActionsObject', + 'CardHeaderSelectableActionsObject', + 'CardTitle', + 'CardBody', + 'CardFooter', + 'CardExpandableContent' + ] ouia: true --- @@ -31,14 +40,14 @@ Most modifiers can be used in combination with each other, except for `isCompact ``` -| Modifier | Description | -| --- | --- | -| isCompact | Modifies the card to include compact styling. Should not be used with isLarge. | -| isFlat | Modifies the card to include flat styling. | -| isRounded | Modifies the card to include rounded border styling. | -| isLarge | Modifies the card to be large. Should not be used with isCompact. | -| isFullHeight | Modifies the card so that it fills the total available height of its container. | -| isPlain | Modifies the card to include plain styling, which removes the border and background. | +| Modifier | Description | +| ------------ | ------------------------------------------------------------------------------------ | +| isCompact | Modifies the card to include compact styling. Should not be used with isLarge. | +| isFlat | Modifies the card to include flat styling. | +| isRounded | Modifies the card to include rounded border styling. | +| isLarge | Modifies the card to be large. Should not be used with isCompact. | +| isFullHeight | Modifies the card so that it fills the total available height of its container. | +| isPlain | Modifies the card to include plain styling, which removes the border and background. | ### Header images and actions @@ -98,23 +107,57 @@ A common use case of this is to set all but one body section to `isFilled={false ``` -### Selectable cards +### Selectable -Selectable cards can only be selected one at a time, and are intended for use with [primary-detail layout](/demos/primary-detail). +A card can be selected by clicking anywhere within the card. + +You must avoid rendering any other interactive content within the `` when it is meant to be selectable only. Refer to our [clickable and selectable example](#clickable-and-selectable-cards) if you need a card that is both selectable and has other interactive content. ```ts file='./CardSelectable.tsx' ``` -### Legacy selectable cards +### Single selectable + +When a group of single selectable cards are related, you should pass the same `name` property to each card's `selectableActions` property. + +```ts file='./CardSingleSelectable.tsx' + +``` + +### Clickable + +A card can perform an action or navigate to a link by clicking anywhere within the card. You can also pass in the `isExternalLink` property to `selectableActions` if you want a clickable card's link to open in a new tab or window. + +When a card is meant to be clickable only, you must avoid rendering any other interactive content within the ``, similar to selectable cards. + +```ts file='./CardClickable.tsx' + +``` + +### Clickable and selectable + +A card can be selectable and have additional interactive content by passing both the `isClickable` and `isSelectable` properties to ``. The following example shows how the "clickable" functionality can be rendered anywhere within a selectable card. + +When passing interactive content to a clickable and selectable card that is disabled, you should also ensure the interactive content is disabled as well, if applicable. + +```ts file='./CardClickableSelectable.tsx' + +``` + +### Selectable cards (deprecated) -The following example shows a legacy implementation of selectable cards. This example uses the `isSelectable` property instead of `isSelectableRaised`, which is the current recommendation for implementation. `isSelectable` applies selectable styling, but does not apply raised styling on hover and selection as `isSelectableRaised` does. +The following example shows a deprecated implementation of selectable cards. This example uses the `isSelectable` property instead of `isSelectableRaised`, which is the current recommendation for implementation. `isSelectable` applies selectable styling, but does not apply raised styling on hover and selection as `isSelectableRaised` does. -```ts file='./CardLegacySelectable.tsx' +A `tabIndex={0}` is also manually passed to allow the card to be focused and clicked via keyboard. + +```ts file='./CardDeprecatedSelectable.tsx' ``` -### Selectable card accessibility features +### Selectable card accessibility features (deprecated) + +Note: the following example uses deprecated properties. We recommend using the new `selectableActions` property for the `CardHeader` instead. The following cards demonstrate how the `hasSelectableInput` and `onSelectableInputChange` properties improve accessibility for selectable cards. @@ -128,7 +171,7 @@ The second card does not set `hasSelectableInput` to true, so neither the input We recommend navigating this example using a screen reader to best understand both cards. -```ts file='./CardSelectableA11yHighlight.tsx' +```ts file='./CardDeprecatedSelectableA11yHighlight.tsx' ``` diff --git a/packages/react-core/src/components/Card/examples/CardClickable.tsx b/packages/react-core/src/components/Card/examples/CardClickable.tsx new file mode 100644 index 00000000000..4aa25d8eafc --- /dev/null +++ b/packages/react-core/src/components/Card/examples/CardClickable.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Card, CardHeader, CardTitle, CardBody } from '@patternfly/react-core'; + +export const CardClickable: React.FunctionComponent = () => { + const id1 = 'clickable-card-input-1'; + const id2 = 'clickable-card-input-2'; + const id3 = 'clickable-card-input-3'; + + return ( + + + console.log(`${id1} clicked`), + selectableActionId: id1, + selectableActionAriaLabelledby: 'clickable-card-example-1', + name: 'clickable-card-example' + }} + > + First card + + This card performs an action on click. + + + + Second card + + This card can navigate to a link on click. + + + console.log(`${id3} clicked`), + selectableActionId: id3, + selectableActionAriaLabelledby: 'clickable-card-example-3', + name: 'clickable-card-example' + }} + > + Third card + + This card is clickable but disabled. + + + ); +}; diff --git a/packages/react-core/src/components/Card/examples/CardClickableSelectable.tsx b/packages/react-core/src/components/Card/examples/CardClickableSelectable.tsx new file mode 100644 index 00000000000..193257cbbc7 --- /dev/null +++ b/packages/react-core/src/components/Card/examples/CardClickableSelectable.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { Card, CardHeader, CardTitle, CardBody, Button } from '@patternfly/react-core'; + +export const CardClickable: React.FunctionComponent = () => { + const [isChecked1, setIsChecked1] = React.useState(false); + const [isChecked2, setIsChecked2] = React.useState(false); + const [isChecked3, setIsChecked3] = React.useState(false); + const [isSelected1, setIsSelected1] = React.useState(false); + + const id1 = 'clickable-selectable-card-input-1'; + const id2 = 'clickable-selectable-card-input-2'; + const id3 = 'clickable-selectable-card-input-3'; + + const onClick = () => { + setIsSelected1((prevState) => !prevState); + }; + + const onChange = (event: React.FormEvent, checked: boolean) => { + const name = event.currentTarget.name; + + switch (name) { + case id1: + setIsChecked1(checked); + break; + case id2: + setIsChecked2(checked); + break; + case id3: + setIsChecked3(checked); + break; + } + }; + + return ( + + + + + + + + This card performs an action upon clicking the card title and is selectable. + + + + Second Card + + + This card is selectable and has a link in the card body that navigates to{' '} + + . + + + + + + + + + This card is clickable and selectable, but disabled. + + + ); +}; diff --git a/packages/react-core/src/components/Card/examples/CardLegacySelectable.tsx b/packages/react-core/src/components/Card/examples/CardDeprecatedSelectable.tsx similarity index 98% rename from packages/react-core/src/components/Card/examples/CardLegacySelectable.tsx rename to packages/react-core/src/components/Card/examples/CardDeprecatedSelectable.tsx index e3b92c96e8d..a3c9083a3c5 100644 --- a/packages/react-core/src/components/Card/examples/CardLegacySelectable.tsx +++ b/packages/react-core/src/components/Card/examples/CardDeprecatedSelectable.tsx @@ -102,6 +102,7 @@ export const CardLegacySelectable: React.FunctionComponent = () => { isSelectable isSelected={selected === 'legacy-first-card'} hasSelectableInput + tabIndex={0} > First legacy selectable card @@ -116,6 +117,7 @@ export const CardLegacySelectable: React.FunctionComponent = () => { isSelectable isSelected={selected === 'legacy-second-card'} hasSelectableInput + tabIndex={0} > Second legacy selectable card This is a selectable card. Click me to select me. Click again to deselect me. diff --git a/packages/react-core/src/components/Card/examples/CardSelectableA11yHighlight.tsx b/packages/react-core/src/components/Card/examples/CardDeprecatedSelectableA11yHighlight.tsx similarity index 96% rename from packages/react-core/src/components/Card/examples/CardSelectableA11yHighlight.tsx rename to packages/react-core/src/components/Card/examples/CardDeprecatedSelectableA11yHighlight.tsx index d8b9169e249..5ed107335e9 100644 --- a/packages/react-core/src/components/Card/examples/CardSelectableA11yHighlight.tsx +++ b/packages/react-core/src/components/Card/examples/CardDeprecatedSelectableA11yHighlight.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Card, CardTitle, CardBody } from '@patternfly/react-core'; -export const CardSelectableA11yHighlight: React.FunctionComponent = () => { +export const CardLegacySelectableA11yHighlight: React.FunctionComponent = () => { const [selected, setSelected] = React.useState(''); const onKeyDown = (event: React.KeyboardEvent) => { diff --git a/packages/react-core/src/components/Card/examples/CardSelectable.tsx b/packages/react-core/src/components/Card/examples/CardSelectable.tsx index 90112a13be8..a9de63b669d 100644 --- a/packages/react-core/src/components/Card/examples/CardSelectable.tsx +++ b/packages/react-core/src/components/Card/examples/CardSelectable.tsx @@ -1,129 +1,74 @@ import React from 'react'; -import { - Card, - CardHeader, - CardTitle, - CardBody, - Dropdown, - DropdownList, - DropdownItem, - MenuToggle, - MenuToggleElement, - Divider -} from '@patternfly/react-core'; -import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import { Card, CardHeader, CardTitle, CardBody } from '@patternfly/react-core'; -export const CardSelectable: React.FunctionComponent = () => { - const [selected, setSelected] = React.useState(''); - const [isKebabOpen, setIsKebabOpen] = React.useState(false); +export const SelectableCard: React.FunctionComponent = () => { + const [isChecked1, setIsChecked1] = React.useState(false); + const [isChecked2, setIsChecked2] = React.useState(false); + const [isChecked3, setIsChecked3] = React.useState(false); - const onKeyDown = (event: React.KeyboardEvent) => { - if (event.target !== event.currentTarget) { - return; - } - if ([' ', 'Enter'].includes(event.key)) { - event.preventDefault(); - const newSelected = event.currentTarget.id === selected ? '' : event.currentTarget.id; - setSelected(newSelected); - } - }; + const id1 = 'selectable-card-input-1'; + const id2 = 'selectable-card-input-2'; + const id3 = 'selectable-card-input-3'; - const onClick = (event: React.MouseEvent) => { - const newSelected = event.currentTarget.id === selected ? '' : event.currentTarget.id; - setSelected(newSelected); - }; + const onChange = (event: React.FormEvent, checked: boolean) => { + const name = event.currentTarget.name; - const onChange = (_event: React.FormEvent, labelledById: string) => { - const newSelected = labelledById === selected ? '' : labelledById; - setSelected(newSelected); - }; - - const onToggle = (event: React.MouseEvent | undefined) => { - event?.stopPropagation(); - setIsKebabOpen(!isKebabOpen); - }; - - const onSelect = (event: React.MouseEvent | undefined) => { - event?.stopPropagation(); - setIsKebabOpen(false); + switch (name) { + case id1: + setIsChecked1(checked); + break; + case id2: + setIsChecked2(checked); + break; + case id3: + setIsChecked3(checked); + break; + } }; - const dropdownItems = ( - <> - Action - {/* Prevent default onClick functionality for example purposes */} - event.preventDefault()}> - Link - - - Disabled Action - - event.preventDefault()}> - Disabled Link - - - Separated Action - event.preventDefault()}> - Separated Link - - - ); - - const headerActions = ( - <> - ) => ( - - - )} - isOpen={isKebabOpen} - onOpenChange={(isOpen: boolean) => setIsKebabOpen(isOpen)} - > - {dropdownItems} - - - ); - return ( - - - First card - This is a selectable card. Click me to select me. Click again to deselect me. + + + First card + + This card is selectable. -
- - Second card - This is a selectable card. Click me to select me. Click again to deselect me. + + + Second card + + This card is selectable. -
- - Third card - This is a raised but disabled card. + + + Third card + + This card is selectable but disabled.
); diff --git a/packages/react-core/src/components/Card/examples/CardSingleSelectable.tsx b/packages/react-core/src/components/Card/examples/CardSingleSelectable.tsx new file mode 100644 index 00000000000..6279ac1f6d3 --- /dev/null +++ b/packages/react-core/src/components/Card/examples/CardSingleSelectable.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { Card, CardHeader, CardTitle, CardBody } from '@patternfly/react-core'; + +export const SingleSelectableCard: React.FunctionComponent = () => { + const id1 = 'single-selectable-card-input-1'; + const id2 = 'single-selectable-card-input-2'; + const id3 = 'single-selectable-card-input-3'; + + return ( + + + + First card + + This card is single selectable. + + + + Second card + + This card is single selectable. + + + + Third card + + This card is single selectable but disabled. + + + ); +}; diff --git a/packages/react-core/src/components/Checkbox/Checkbox.tsx b/packages/react-core/src/components/Checkbox/Checkbox.tsx index f20ba4e0dba..ce7b22010fa 100644 --- a/packages/react-core/src/components/Checkbox/Checkbox.tsx +++ b/packages/react-core/src/components/Checkbox/Checkbox.tsx @@ -10,6 +10,8 @@ export interface CheckboxProps OUIAProps { /** Additional classes added to the checkbox. */ className?: string; + /** Additional classed added to the radio input */ + inputClassName?: string; /** Flag to show if the checkbox selection is valid or invalid. */ isValid?: boolean; /** Flag to show if the checkbox is disabled. */ @@ -74,6 +76,7 @@ export class Checkbox extends React.Component { const { 'aria-label': ariaLabel, className, + inputClassName, onChange, isValid, isDisabled, @@ -109,14 +112,14 @@ export class Checkbox extends React.Component { elem && (elem.indeterminate = isChecked === null)} + ref={(elem) => elem && (elem.indeterminate = isChecked === null)} {...checkedProps} {...getOUIAProps(Checkbox.displayName, ouiaId !== undefined ? ouiaId : this.state.ouiaStateId, ouiaSafe)} /> diff --git a/packages/react-core/src/components/Radio/Radio.tsx b/packages/react-core/src/components/Radio/Radio.tsx index 7d6cf739a78..7a369d6c091 100644 --- a/packages/react-core/src/components/Radio/Radio.tsx +++ b/packages/react-core/src/components/Radio/Radio.tsx @@ -7,8 +7,12 @@ import { getOUIAProps, OUIAProps, getDefaultOUIAId } from '../../helpers'; export interface RadioProps extends Omit, 'disabled' | 'label' | 'onChange' | 'type'>, OUIAProps { - /** Additional classes added to the radio. */ + /** Additional classes added to the radio wrapper. This will be a div element if + * isLabelWrapped is true, otherwise this will be a label element. + */ className?: string; + /** Additional classed added to the radio input */ + inputClassName?: string; /** Id of the radio. */ id: string; /** Flag to show if the radio label is wrapped on small screen. */ @@ -70,6 +74,7 @@ export class Radio extends React.Component 'aria-label': ariaLabel, checked, className, + inputClassName, defaultChecked, isLabelWrapped, isLabelBeforeButton, @@ -93,7 +98,7 @@ export class Radio extends React.Component const inputRendered = ( Footer


- + Header Body Footer @@ -171,6 +171,7 @@ export class CardDemo extends React.Component { id="selectableCard" isSelectable isSelected={this.state.selected === 'selectableCard'} + tabIndex={0} onKeyDown={this.onKeyDown} > Header