-
Notifications
You must be signed in to change notification settings - Fork 381
Expand file tree
/
Copy pathCardHeader.tsx
More file actions
216 lines (204 loc) · 8.99 KB
/
CardHeader.tsx
File metadata and controls
216 lines (204 loc) · 8.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import { css } from '@patternfly/react-styles';
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 RhMicronsCaretDownIcon from '@patternfly/react-icons/dist/esm/icons/rh-microns-caret-down-icon';
import { Radio } from '../Radio';
import { Checkbox } from '../Checkbox';
import { useSSRSafeId } from '../../helpers';
export interface CardHeaderActionsObject {
/** Actions of the card header */
actions: React.ReactNode;
/** Flag indicating that the actions have no offset */
hasNoOffset?: boolean;
/** Additional classes added to the actions wrapper */
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;
/** Custom ID passed to the selectable card's input or a clickable-only card's button/anchor.
* If omitted, a random unique ID will be assigned to a selectable card's input. */
selectableActionId?: string;
/** Adds an accessible name to the input of a selectable card or clickable button/anchor of a clickable-only card.
* This or selectableActionAriaLabelledby is required for clickable-only cards.
*/
selectableActionAriaLabel?: string;
/** A single or list of space-delimited ID's that provide an accessible name to the input of a selectable card
* or clickable button/anchor of a clickable-only card. This or selectableActionAriaLabelledby is required
* for clickable-only cards.
*/
selectableActionAriaLabelledby?: string;
/** Callback for when a selectable card input changes */
onChange?: (event: React.FormEvent<HTMLInputElement>, checked: boolean) => void;
/** Action to call when a clickable-only card is clicked. This cannot be combined with the to prop. */
onClickAction?: (event: React.MouseEvent) => void;
/** Link to navigate to when a clickable-only card is clicked. This cannot be combined with the onClickAction prop. */
to?: string;
/** Additional props spread to a selectable card input or clickable-only card's button/anchor. */
selectableActionProps?: any;
/** Flag to indicate whether a clickable-only card's link should open in a new tab/window. */
isExternalLink?: boolean;
/** Name for the input element of a selectable card. */
name?: string;
/** @deprecated Flag indicating whether the selectable card input is checked. We recommend using
* the isSelected prop on the card component instead.
*/
isChecked?: boolean;
/** Flag indicating the action is hidden */
isHidden?: boolean;
}
export interface CardHeaderProps extends React.HTMLProps<HTMLDivElement> {
/** Content rendered inside the card header */
children?: React.ReactNode;
/** Additional classes added to the card header */
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 */
onExpand?: (event: React.MouseEvent, id: string) => void;
/** Additional props for expandable toggle button */
toggleButtonProps?: any;
/** Whether to right-align expandable toggle button */
isToggleRightAligned?: boolean;
/** Flag indicating that header wrapping is enabled */
hasWrap?: boolean;
}
export const CardHeader: React.FunctionComponent<CardHeaderProps> = ({
children,
className,
actions,
selectableActions,
id,
onExpand,
toggleButtonProps,
isToggleRightAligned,
hasWrap,
...props
}: CardHeaderProps) => {
const randomId = useSSRSafeId();
return (
<CardContext.Consumer>
{({ cardId, isClickable, isSelectable, isSelected, isDisabled: isCardDisabled }) => {
const cardHeaderToggle = (
<div className={css(styles.cardHeaderToggle)}>
<Button
variant="plain"
type="button"
onClick={(evt) => {
onExpand(evt, cardId);
}}
{...toggleButtonProps}
icon={
<span className={css(styles.cardHeaderToggleIcon)}>
<RhMicronsCaretDownIcon />
</span>
}
/>
</div>
);
const isClickableOrSelectableOnly = (isClickable && !isSelectable) || (isSelectable && !isClickable);
if (actions?.actions && isClickableOrSelectableOnly) {
// eslint-disable-next-line no-console
console.error(
`Card: ${
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 isClickableOnlyCard = isClickable && !isSelectable;
if (
(isClickableOnlyCard || isSelectable) &&
!selectableActions?.selectableActionAriaLabel &&
!selectableActions?.selectableActionAriaLabelledby
) {
// eslint-disable-next-line no-console
console.error(
`Card: ${isClickableOnlyCard ? 'Clickable-only cards' : 'Cards with a selectable input'} must have either the selectableActions.selectableActionAriaLabel or selectableActions.selectableActionAriaLabelledby prop passed in order to provide an accessible name to the clickable element.`
);
}
const SelectableCardInput = selectableActions?.variant === 'single' ? Radio : Checkbox;
const getSelectableProps = () => ({
className: css('pf-m-standalone'),
inputClassName: css(selectableActions?.isHidden && 'pf-v6-screen-reader'),
label: <></>,
'aria-label': selectableActions.selectableActionAriaLabel,
'aria-labelledby': selectableActions.selectableActionAriaLabelledby,
id: selectableActions.selectableActionId ?? `card-selectable-${randomId}`,
name: selectableActions.name,
isDisabled: isCardDisabled,
onChange: selectableActions.onChange,
isChecked: selectableActions.isChecked ?? isSelected,
...selectableActions.selectableActionProps
});
const isClickableLinkCard = selectableActions?.to !== undefined;
const ClickableCardComponent = isClickableLinkCard ? 'a' : 'button';
const getClickableProps = () => {
const isDisabledLinkCard = isCardDisabled && isClickableLinkCard;
const baseProps = {
className: css(
'pf-v6-c-card__clickable-action',
isDisabledLinkCard && styles.modifiers.disabled,
selectableActions?.isHidden && 'pf-v6-screen-reader'
),
id: selectableActions.selectableActionId,
'aria-label': selectableActions.selectableActionAriaLabel,
'aria-labelledby': selectableActions.selectableActionAriaLabelledby,
...selectableActions.selectableActionProps
};
if (isClickableLinkCard) {
return {
...baseProps,
href: selectableActions.to,
...(isCardDisabled && { tabIndex: -1, 'aria-disabled': true }),
...(selectableActions.isExternalLink && { target: '_blank' })
};
}
return { ...baseProps, type: 'button', disabled: isCardDisabled, onClick: selectableActions.onClickAction };
};
return (
<div
className={css(
styles.cardHeader,
isToggleRightAligned && styles.modifiers.toggleRight,
hasWrap && styles.modifiers.wrap,
className
)}
id={id}
{...props}
>
{onExpand && !isToggleRightAligned && cardHeaderToggle}
{(actions || (selectableActions && (isClickable || isSelectable))) && (
<CardActions
className={actions?.className}
hasNoOffset={actions?.hasNoOffset || selectableActions?.hasNoOffset}
>
{actions?.actions}
{selectableActions && (isClickable || isSelectable) && (
<CardSelectableActions className={selectableActions?.className}>
{isSelectable && <SelectableCardInput {...getSelectableProps()} />}
{isClickableOnlyCard && <ClickableCardComponent {...getClickableProps()} />}
</CardSelectableActions>
)}
</CardActions>
)}
{children && <CardHeaderMain>{children}</CardHeaderMain>}
{onExpand && isToggleRightAligned && cardHeaderToggle}
</div>
);
}}
</CardContext.Consumer>
);
};
CardHeader.displayName = 'CardHeader';