Skip to content

Commit 0aa3a3a

Browse files
committed
draft - Button next with PFE
1 parent f20f073 commit 0aa3a3a

7 files changed

Lines changed: 425 additions & 1 deletion

File tree

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,5 +125,9 @@
125125
},
126126
"resolutions": {
127127
"dompurify": "3.4.13"
128+
},
129+
"dependencies": {
130+
"@lit/react": "^1.0.8",
131+
"@patternfly/elements": "^5.0.0"
128132
}
129133
}
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import { forwardRef, type ReactNode, type Ref } from 'react';
2+
import type { OUIAProps } from '../../helpers/OUIA/ouia';
3+
import type { PfV5Button as PfV5ButtonElement } from '@patternfly/elements/pf-v5-button/pf-v5-button.js';
4+
import { V5Button } from '@patternfly/elements/react/pf-v5-button/pf-v5-button.js';
5+
import type { BadgeCountObject } from '../Button';
6+
7+
type PfButtonVariant =
8+
| 'primary'
9+
| 'secondary'
10+
| 'tertiary'
11+
| 'danger'
12+
| 'warning'
13+
| 'link'
14+
| 'plain'
15+
| 'control'
16+
| 'stateful';
17+
18+
export interface FeltButtonProps extends OUIAProps {
19+
children?: ReactNode;
20+
className?: string;
21+
component?: React.ElementType<any> | React.ComponentType<any>;
22+
isClicked?: boolean;
23+
isBlock?: boolean;
24+
isDisabled?: boolean;
25+
isAriaDisabled?: boolean;
26+
isLoading?: boolean;
27+
spinnerAriaValueText?: string;
28+
spinnerAriaLabel?: string;
29+
spinnerAriaLabelledBy?: string;
30+
inoperableEvents?: string[];
31+
isInline?: boolean;
32+
isFavorite?: boolean;
33+
isFavorited?: boolean;
34+
size?: 'default' | 'sm' | 'lg';
35+
type?: 'button' | 'submit' | 'reset';
36+
variant?: PfButtonVariant;
37+
state?: 'read' | 'unread' | 'attention';
38+
hasNoPadding?: boolean;
39+
iconPosition?: 'start' | 'end' | 'left' | 'right';
40+
'aria-label'?: string;
41+
icon?: ReactNode | null;
42+
tabIndex?: number;
43+
isDanger?: boolean;
44+
isExpanded?: boolean;
45+
isSettings?: boolean;
46+
isHamburger?: boolean;
47+
hamburgerVariant?: 'expand' | 'collapse';
48+
isCircle?: boolean;
49+
isDocked?: boolean;
50+
isTextExpanded?: boolean;
51+
countOptions?: BadgeCountObject;
52+
ouiaId?: number | string;
53+
ouiaSafe?: boolean;
54+
onClick?: React.MouseEventHandler;
55+
href?: string;
56+
target?: string;
57+
id?: string;
58+
style?: React.CSSProperties;
59+
title?: string;
60+
name?: string;
61+
value?: string;
62+
role?: string;
63+
[key: `data-${string}`]: string | undefined;
64+
[key: `aria-${string}`]: string | undefined;
65+
}
66+
67+
const unsupportedProps: Record<string, string> = {
68+
component:
69+
'The "component" prop is not supported. pf-v5-button renders as a <button> (or <a> for link+href). Use variant="link" with href for anchor behavior.',
70+
isAriaDisabled: 'The "isAriaDisabled" prop is not supported. pf-v5-button only supports native disabled.',
71+
isClicked: 'The "isClicked" prop is not supported by pf-v5-button.',
72+
isFavorite: 'The "isFavorite" prop is not supported by pf-v5-button.',
73+
isFavorited: 'The "isFavorited" prop is not supported by pf-v5-button.',
74+
isSettings: 'The "isSettings" prop is not supported by pf-v5-button.',
75+
isHamburger: 'The "isHamburger" prop is not supported by pf-v5-button.',
76+
hamburgerVariant: 'The "hamburgerVariant" prop is not supported by pf-v5-button.',
77+
isCircle: 'The "isCircle" prop is not supported by pf-v5-button.',
78+
isDocked: 'The "isDocked" prop is not supported by pf-v5-button.',
79+
isTextExpanded: 'The "isTextExpanded" prop is not supported by pf-v5-button.',
80+
countOptions: 'The "countOptions" prop is not supported by pf-v5-button.',
81+
hasNoPadding: 'The "hasNoPadding" prop is not supported by pf-v5-button.',
82+
inoperableEvents: 'The "inoperableEvents" prop is not supported. pf-v5-button does not support aria-disabled.',
83+
spinnerAriaValueText: 'The "spinnerAriaValueText" prop is not supported. Use the "loading-label" attribute instead.',
84+
spinnerAriaLabelledBy: 'The "spinnerAriaLabelledBy" prop is not supported by pf-v5-button.',
85+
ouiaId: 'The "ouiaId" prop is not supported by pf-v5-button.',
86+
ouiaSafe: 'The "ouiaSafe" prop is not supported by pf-v5-button.',
87+
state: 'The "state" prop (stateful variant) is not supported by pf-v5-button.',
88+
isExpanded: 'The "isExpanded" prop is not supported by pf-v5-button.'
89+
};
90+
91+
const warnedProps = new Set<string>();
92+
93+
function warnUnsupported(propName: string) {
94+
if (warnedProps.has(propName)) {
95+
return;
96+
}
97+
warnedProps.add(propName);
98+
const message = unsupportedProps[propName];
99+
if (message) {
100+
// eslint-disable-next-line no-console
101+
console.warn(`FeltButton: ${message}`);
102+
}
103+
}
104+
105+
function mapSizeToPfe(size?: 'default' | 'sm' | 'lg'): 'small' | 'large' | undefined {
106+
switch (size) {
107+
case 'sm':
108+
return 'small';
109+
case 'lg':
110+
return 'large';
111+
default:
112+
return undefined;
113+
}
114+
}
115+
116+
function resolveVariant(
117+
variant: PfButtonVariant = 'primary',
118+
isDanger?: boolean
119+
): {
120+
pfeVariant: 'primary' | 'secondary' | 'tertiary' | 'control' | 'link';
121+
danger: boolean;
122+
warning: boolean;
123+
plain: boolean;
124+
} {
125+
switch (variant) {
126+
case 'danger':
127+
return { pfeVariant: 'primary', danger: true, warning: false, plain: false };
128+
case 'warning':
129+
return { pfeVariant: 'primary', danger: false, warning: true, plain: false };
130+
case 'plain':
131+
return { pfeVariant: 'primary', danger: false, warning: false, plain: true };
132+
case 'stateful':
133+
return { pfeVariant: 'primary', danger: false, warning: false, plain: false };
134+
default:
135+
return {
136+
pfeVariant: variant,
137+
danger: !!(isDanger && (variant === 'secondary' || variant === 'link')),
138+
warning: false,
139+
plain: false
140+
};
141+
}
142+
}
143+
144+
const FeltButtonBase = (
145+
{
146+
children = null,
147+
className,
148+
component,
149+
isClicked,
150+
isBlock = false,
151+
isDisabled = false,
152+
isAriaDisabled,
153+
isLoading,
154+
spinnerAriaValueText,
155+
spinnerAriaLabel,
156+
spinnerAriaLabelledBy,
157+
inoperableEvents,
158+
isInline = false,
159+
isFavorite,
160+
isFavorited,
161+
size = 'default',
162+
type = 'button',
163+
variant = 'primary',
164+
state,
165+
hasNoPadding,
166+
iconPosition = 'start',
167+
'aria-label': ariaLabel,
168+
icon = null,
169+
tabIndex,
170+
isDanger,
171+
isExpanded,
172+
isSettings,
173+
isHamburger,
174+
hamburgerVariant,
175+
isCircle,
176+
isDocked,
177+
isTextExpanded,
178+
countOptions,
179+
ouiaId,
180+
ouiaSafe,
181+
onClick,
182+
href,
183+
target,
184+
name,
185+
value,
186+
...rest
187+
}: FeltButtonProps,
188+
ref: Ref<PfV5ButtonElement> // replaces hidden prop innerRef with native forwardRef
189+
) => {
190+
// Warn for unsupported props (once per prop)
191+
const unsupportedValues: Record<string, unknown> = {
192+
component,
193+
isAriaDisabled,
194+
isClicked,
195+
isFavorite,
196+
isFavorited,
197+
isSettings,
198+
isHamburger,
199+
hamburgerVariant,
200+
isCircle,
201+
isDocked,
202+
isTextExpanded,
203+
countOptions,
204+
hasNoPadding,
205+
inoperableEvents,
206+
spinnerAriaValueText,
207+
spinnerAriaLabelledBy,
208+
ouiaId,
209+
ouiaSafe,
210+
state,
211+
isExpanded
212+
};
213+
for (const prop of Object.keys(unsupportedProps)) {
214+
if (unsupportedValues[prop] !== undefined && unsupportedValues[prop] !== false) {
215+
warnUnsupported(prop);
216+
}
217+
}
218+
if (variant === 'stateful') {
219+
warnUnsupported('state');
220+
}
221+
222+
const { pfeVariant, danger, warning, plain } = resolveVariant(variant, isDanger);
223+
const isIconAtEnd = iconPosition === 'end' || iconPosition === 'right';
224+
225+
return (
226+
<V5Button
227+
ref={ref}
228+
className={className}
229+
variant={pfeVariant}
230+
danger={danger}
231+
warning={warning}
232+
plain={plain}
233+
disabled={isDisabled}
234+
loading={isLoading || false}
235+
inline={isInline}
236+
block={isBlock}
237+
size={mapSizeToPfe(size)}
238+
type={type}
239+
label={ariaLabel || undefined}
240+
name={name}
241+
value={value}
242+
href={pfeVariant === 'link' ? href : undefined}
243+
target={pfeVariant === 'link' && href ? target : undefined}
244+
loading-label={spinnerAriaLabel}
245+
tabIndex={tabIndex}
246+
onClick={onClick}
247+
{...rest}
248+
>
249+
{/* icon position doesn't work - slot is static inside web component. would need a start and end icon slot to support. */}
250+
{icon && !isIconAtEnd && <span slot="icon">{icon}</span>}
251+
{children}
252+
{icon && isIconAtEnd && <span slot="icon">{icon}</span>}
253+
</V5Button>
254+
);
255+
};
256+
257+
export const FeltButton = forwardRef<PfV5ButtonElement, FeltButtonProps>(FeltButtonBase);
258+
259+
FeltButton.displayName = 'FeltButton';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
id: Button
3+
section: components
4+
source: react-next
5+
propComponents: ['FeltButton', 'BadgeCountObject']
6+
ouia: true
7+
---
8+
9+
import RhMicronsCloseIcon from '@patternfly/react-icons/dist/esm/icons/rh-microns-close-icon';
10+
import RhUiExternalLinkFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-external-link-fill-icon';
11+
import RhUiAddCircleFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-add-circle-fill-icon';
12+
import RhUiCopyFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-copy-fill-icon';
13+
14+
## Examples
15+
16+
### Variant examples
17+
18+
```ts file="./FeltButtonVariations.tsx"
19+
20+
```
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { FeltButton } from '@patternfly/react-core/components/ButtonNext';
2+
import { Flex } from '@patternfly/react-core';
3+
import RhMicronsCloseIcon from '@patternfly/react-icons/dist/esm/icons/rh-microns-close-icon';
4+
import RhUiExternalLinkFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-external-link-fill-icon';
5+
import RhUiAddCircleFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-add-circle-fill-icon';
6+
import RhUiCopyFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-copy-fill-icon';
7+
8+
export const FeltButtonVariations: React.FunctionComponent = () => (
9+
<>
10+
<Flex columnGap={{ default: 'columnGapSm' }}>
11+
<FeltButton variant="primary">Primary</FeltButton>
12+
<FeltButton variant="secondary">Secondary</FeltButton>
13+
<FeltButton variant="secondary" isDanger>
14+
Danger Secondary
15+
</FeltButton>
16+
<FeltButton variant="tertiary">Tertiary</FeltButton>
17+
<FeltButton variant="danger">Danger</FeltButton>
18+
<FeltButton variant="warning">Warning</FeltButton>
19+
</Flex>
20+
<br />
21+
<Flex columnGap={{ default: 'columnGapSm' }}>
22+
<FeltButton variant="link" icon={<RhUiAddCircleFillIcon />}>
23+
Link
24+
</FeltButton>
25+
<FeltButton variant="link" icon={<RhUiExternalLinkFillIcon />} iconPosition="end">
26+
Link
27+
</FeltButton>
28+
<FeltButton variant="link" isInline>
29+
Inline link
30+
</FeltButton>
31+
<FeltButton variant="link" isDanger>
32+
Danger link
33+
</FeltButton>
34+
<FeltButton variant="plain" aria-label="Action" icon={<RhMicronsCloseIcon />} />
35+
</Flex>
36+
<br />
37+
<Flex columnGap={{ default: 'columnGapSm' }}>
38+
<FeltButton variant="control">Control</FeltButton>
39+
<FeltButton variant="control" aria-label="Copy" icon={<RhUiCopyFillIcon />} />
40+
</Flex>
41+
</>
42+
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './FeltButton';

packages/react-core/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './Banner';
1212
export * from './Brand';
1313
export * from './Breadcrumb';
1414
export * from './Button';
15+
export * from './ButtonNext';
1516
export * from './CalendarMonth';
1617
export * from './Card';
1718
export * from './Checkbox';

0 commit comments

Comments
 (0)