From 2f30ccf9b8319c2e7ed7052758f6522f8ff864a5 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Thu, 5 Jun 2025 16:42:10 -0400 Subject: [PATCH 01/14] feat(Button/MenuToggle): added support for hamburger/settings animations --- .../src/components/Button/Button.tsx | 46 +++++++++++++++++-- .../src/components/Button/examples/Button.md | 10 ++++ .../Button/examples/ButtonHamburger.tsx | 39 ++++++++++++++++ .../Button/examples/ButtonSettings.tsx | 8 ++++ .../src/components/MenuToggle/MenuToggle.tsx | 7 ++- .../MenuToggle/examples/MenuToggle.md | 5 ++ .../examples/MenuToggleSettings.tsx | 8 ++++ .../react-core/src/helpers/hamburgerIcon.tsx | 9 ++++ packages/react-core/src/helpers/index.ts | 1 + 9 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 packages/react-core/src/components/Button/examples/ButtonHamburger.tsx create mode 100644 packages/react-core/src/components/Button/examples/ButtonSettings.tsx create mode 100644 packages/react-core/src/components/MenuToggle/examples/MenuToggleSettings.tsx create mode 100644 packages/react-core/src/helpers/hamburgerIcon.tsx diff --git a/packages/react-core/src/components/Button/Button.tsx b/packages/react-core/src/components/Button/Button.tsx index 64b09d6284d..be71b89a1dd 100644 --- a/packages/react-core/src/components/Button/Button.tsx +++ b/packages/react-core/src/components/Button/Button.tsx @@ -6,6 +6,9 @@ import { useOUIAProps, OUIAProps } from '../../helpers/OUIA/ouia'; import { Badge } from '../Badge'; import StarIcon from '@patternfly/react-icons/dist/esm/icons/star-icon'; import OutlinedStarIcon from '@patternfly/react-icons/dist/esm/icons/outlined-star-icon'; +import CogIcon from '@patternfly/react-icons/icons/cog-icon/dist/esm/icons/cog-icon'; +// TODO: replace following hamburger import when https://github.com/patternfly/patternfly-react/issues/11858 is resolved +import { hamburgerIcon } from '../../helpers/hamburgerIcon'; export enum ButtonVariant { primary = 'primary', @@ -97,6 +100,14 @@ export interface ButtonProps extends Omit, 'r tabIndex?: number; /** Adds danger styling to secondary or link button variants */ isDanger?: boolean; + /** Flag indicating whether content the button controls is expanded or not. Required when isHamburger is true. */ + isExpanded?: boolean; + /** Flag indicating the button is a settings button. This will override the variant and icon properties. */ + isSettings?: boolean; + /** Flag indicating the button is a hamburger button. This will override the children, variant, and icon properties. */ + isHamburger?: boolean; + /** Adjusts and animates the hamburger icon to indicate what will happen upon clicking the button. */ + hamburgerVariant?: 'expand' | 'collapse'; /** @hide Forwarded ref */ innerRef?: React.Ref; /** Adds count number to button */ @@ -117,6 +128,10 @@ const ButtonBase: React.FunctionComponent = ({ isAriaDisabled = false, isLoading = null, isDanger = false, + isExpanded, + isSettings, + isHamburger, + hamburgerVariant, spinnerAriaValueText, spinnerAriaLabelledBy, spinnerAriaLabel, @@ -146,13 +161,27 @@ const ButtonBase: React.FunctionComponent = ({ 'Button: Each favorite button must have a unique accessible name provided via aria-label or aria-labelledby' ); } + if (isHamburger && ![true, false].includes(isExpanded)) { + // eslint-disable-next-line no-console + console.error( + 'Button: when the isHamburger property is passed in, you must also pass in a boolean value to the isExpanded property. It is expected that a hamburger button controls the expansion of other content.' + ); + } + // TODO: Remove isSettings in breaking change to throw this warning for any non-hamburger button that does not have children or aria-label + if ((isHamburger && !ariaLabel) || (isSettings && (!ariaLabel || !children || !props['aria-labelledby']))) { + // eslint-disable-next-line no-console + console.error( + 'Button: you must provide either visible text content or an accessible name via the aria-label or aria-labelledby properties.' + ); + } const ouiaProps = useOUIAProps(Button.displayName, ouiaId, ouiaSafe, variant); const Component = component as any; const isButtonElement = Component === 'button'; const isInlineSpan = isInline && Component === 'span'; const isIconAlignedAtEnd = iconPosition === 'end' || iconPosition === 'right'; - const shouldOverrideIcon = isFavorite; + const shouldForcePlainVariant = isSettings || isHamburger; + const shouldOverrideIcon = isSettings || isHamburger || isFavorite; const preventedEvents = inoperableEvents.reduce( (handlers, eventToPrevent) => ({ @@ -190,6 +219,12 @@ const ButtonBase: React.FunctionComponent = ({ ); } + if (isSettings) { + iconContent = ; + } + if (isHamburger) { + iconContent = hamburgerIcon; + } if (icon && !shouldOverrideIcon) { iconContent = icon; } @@ -202,9 +237,8 @@ const ButtonBase: React.FunctionComponent = ({ ) ); }; - const _icon = renderIcon(); - const _children = children && {children}; + const _children = children && !isHamburger && {children}; // We only want to render the aria-disabled attribute when true, similar to the disabled attribute natively. const shouldRenderAriaDisabled = isAriaDisabled || (!isButtonElement && isDisabled); @@ -214,9 +248,13 @@ const ButtonBase: React.FunctionComponent = ({ {...(isAriaDisabled ? preventedEvents : null)} {...(shouldRenderAriaDisabled && { 'aria-disabled': true })} aria-label={ariaLabel} + aria-expanded={isExpanded} className={css( styles.button, - styles.modifiers[variant], + shouldForcePlainVariant ? styles.modifiers.plain : styles.modifiers[variant], + isSettings && styles.modifiers.settings, + isHamburger && styles.modifiers.hamburger, + isHamburger && hamburgerVariant && styles.modifiers[hamburgerVariant], isBlock && styles.modifiers.block, isDisabled && !isButtonElement && styles.modifiers.disabled, isAriaDisabled && styles.modifiers.ariaDisabled, diff --git a/packages/react-core/src/components/Button/examples/Button.md b/packages/react-core/src/components/Button/examples/Button.md index 0df98d4121a..d6d580c3186 100644 --- a/packages/react-core/src/components/Button/examples/Button.md +++ b/packages/react-core/src/components/Button/examples/Button.md @@ -132,6 +132,16 @@ You can pass both the `isFavorite` and `variant="plain"` properties into the ` { + const [isHovered, setIsHovered] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + const [hamburgerVariant, setHamburgerVariant] = useState<'expand' | 'collapse'>('expand'); + + const handleClick = () => { + setHamburgerVariant((prevVariant) => (prevVariant === 'expand' ? 'collapse' : 'expand')); + setIsExpanded(!isExpanded); + }; + + const onHoverFocus = () => { + setIsHovered(true); + }; + const onLeaveBlur = () => { + setIsHovered(false); + }; + + return ( + + + +); diff --git a/packages/react-core/src/components/MenuToggle/MenuToggle.tsx b/packages/react-core/src/components/MenuToggle/MenuToggle.tsx index e48982bd87c..0ccce59c491 100644 --- a/packages/react-core/src/components/MenuToggle/MenuToggle.tsx +++ b/packages/react-core/src/components/MenuToggle/MenuToggle.tsx @@ -3,6 +3,7 @@ import styles from '@patternfly/react-styles/css/components/MenuToggle/menu-togg import { css } from '@patternfly/react-styles'; import CaretDownIcon from '@patternfly/react-icons/dist/esm/icons/caret-down-icon'; import { BadgeProps } from '../Badge'; +import CogIcon from '@patternfly/react-icons/icons/cog-icon/dist/esm/icons/cog-icon'; import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon'; @@ -44,6 +45,8 @@ export interface MenuToggleProps isFullWidth?: boolean; /** Flag indicating the toggle contains placeholder text */ isPlaceholder?: boolean; + /** Flag indicating whether the toggle is a settings toggle. */ + isSettings?: boolean; /** Elements to display before the toggle button. When included, renders the menu toggle as a split button. */ splitButtonItems?: React.ReactNode[]; /** Variant styles of the menu toggle */ @@ -100,6 +103,7 @@ class MenuToggleBase extends Component { isFullHeight, isFullWidth, isPlaceholder, + isSettings, splitButtonItems, variant, status, @@ -144,7 +148,7 @@ class MenuToggleBase extends Component { const content = ( <> - {icon && {icon}} + {(icon || isSettings) && {isSettings ? : icon}} {isTypeahead ? children : children && {children}} {isValidElement(badge) && {badge}} {isTypeahead ? ( @@ -177,6 +181,7 @@ class MenuToggleBase extends Component { isFullWidth && styles.modifiers.fullWidth, isDisabled && styles.modifiers.disabled, isPlaceholder && styles.modifiers.placeholder, + isSettings && styles.modifiers.settings, size === MenuToggleSize.sm && styles.modifiers.small, className ); diff --git a/packages/react-core/src/components/MenuToggle/examples/MenuToggle.md b/packages/react-core/src/components/MenuToggle/examples/MenuToggle.md index bed5edd7cae..d0d7a9f2553 100644 --- a/packages/react-core/src/components/MenuToggle/examples/MenuToggle.md +++ b/packages/react-core/src/components/MenuToggle/examples/MenuToggle.md @@ -70,6 +70,11 @@ import { MenuToggle, Badge } from '@patternfly/react-core'; ``` +### Settings toggle + +```ts file="./MenuToggleSettings.tsx" +``` + ### With icons To add a recognizable icon to a menu toggle, use the `icon` property. The following example adds a `CogIcon` to the toggle. diff --git a/packages/react-core/src/components/MenuToggle/examples/MenuToggleSettings.tsx b/packages/react-core/src/components/MenuToggle/examples/MenuToggleSettings.tsx new file mode 100644 index 00000000000..d29d2c3f606 --- /dev/null +++ b/packages/react-core/src/components/MenuToggle/examples/MenuToggleSettings.tsx @@ -0,0 +1,8 @@ +import { MenuToggle, Flex } from '@patternfly/react-core'; + +export const MenuToggleSettings: React.FunctionComponent = () => ( + + Settings + + +); diff --git a/packages/react-core/src/helpers/hamburgerIcon.tsx b/packages/react-core/src/helpers/hamburgerIcon.tsx new file mode 100644 index 00000000000..826fd4dd17e --- /dev/null +++ b/packages/react-core/src/helpers/hamburgerIcon.tsx @@ -0,0 +1,9 @@ +// TODO: remove this file when https://github.com/patternfly/patternfly-react/issues/11858 is resolved +export const hamburgerIcon = ( + + + + + + +); diff --git a/packages/react-core/src/helpers/index.ts b/packages/react-core/src/helpers/index.ts index c2aee2b6820..411d67d3fb0 100644 --- a/packages/react-core/src/helpers/index.ts +++ b/packages/react-core/src/helpers/index.ts @@ -11,3 +11,4 @@ export * from './KeyboardHandler'; export * from './resizeObserver'; export * from './useInterval'; export * from './datetimeUtils'; +export * from './hamburgerIcon'; From 721e9bb705eb60605c7a64bfa8e4dada585cfad7 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Thu, 5 Jun 2025 16:42:42 -0400 Subject: [PATCH 02/14] Updated existing examples and demos --- .../components/Masthead/examples/MastheadBasic.tsx | 3 +-- .../Masthead/examples/MastheadBasicMixedContent.tsx | 3 +-- .../Masthead/examples/MastheadDisplayInline.tsx | 3 +-- .../Masthead/examples/MastheadDisplayStack.tsx | 3 +-- .../examples/MastheadDisplayStackInlineResponsive.tsx | 3 +-- .../components/Masthead/examples/MastheadInsets.tsx | 3 +-- .../Masthead/examples/MastheadLogoCustomComponent.tsx | 3 +-- .../src/components/Page/PageToggleButton.tsx | 11 +++++++++-- .../components/Page/examples/PageCenteredSection.tsx | 7 ++----- .../src/components/Page/examples/PageGroupSection.tsx | 7 ++----- .../Page/examples/PageMainSectionPadding.tsx | 7 ++----- .../Page/examples/PageMainSectionVariations.tsx | 7 ++----- .../Page/examples/PageMultipleSidebarBody.tsx | 7 ++----- .../components/Page/examples/PageUncontrolledNav.tsx | 5 +---- .../src/components/Page/examples/PageVerticalNav.tsx | 7 ++----- .../Page/examples/PageWithOrWithoutFill.tsx | 7 ++----- packages/react-core/src/demos/DashboardHeader.tsx | 7 ++----- .../examples/NotificationDrawerBasic.tsx | 7 ++----- .../examples/NotificationDrawerGrouped.tsx | 7 ++----- .../src/demos/RTL/examples/PaginatedTable.tsx | 11 ++--------- .../examples/Masthead/MastheadWithHorizontalNav.tsx | 2 +- .../MastheadWithUtilitiesAndUserDropdownMenu.tsx | 7 ++----- .../react-core/src/demos/examples/Nav/NavFlyout.tsx | 9 +++------ .../src/demos/examples/Nav/NavHorizontal.tsx | 2 +- .../demos/examples/Nav/NavHorizontalWithSubnav.tsx | 7 ++----- .../react-core/src/demos/examples/Nav/NavManual.tsx | 9 +++------ .../src/demos/examples/Page/PageContextSelector.tsx | 9 +++------ .../examples/Page/PageStickySectionBreadcrumb.tsx | 7 ++----- .../demos/examples/Page/PageStickySectionGroup.tsx | 7 ++----- .../examples/Page/PageStickySectionGroupAlternate.tsx | 7 ++----- .../examples/Toolbar/ConsoleLogViewerToolbar.tsx | 5 ++--- .../src/demos/examples/Wizard/InPageWithDrawer.tsx | 5 +---- .../Wizard/InPageWithDrawerInformationalStep.tsx | 5 +---- packages/react-integration/demo-app-ts/src/App.tsx | 5 +---- .../components/demos/MastheadDemo/MastheadDemo.tsx | 3 +-- .../src/components/demos/PageDemo/PageDemo.tsx | 7 ++----- .../demos/PageDemo/PageManagedSidebarClosedDemo.tsx | 5 +---- packages/react-table/src/demos/DashboardHeader.tsx | 7 ++----- 38 files changed, 71 insertions(+), 155 deletions(-) diff --git a/packages/react-core/src/components/Masthead/examples/MastheadBasic.tsx b/packages/react-core/src/components/Masthead/examples/MastheadBasic.tsx index f58bcf95a15..c7fbc3fd1c2 100644 --- a/packages/react-core/src/components/Masthead/examples/MastheadBasic.tsx +++ b/packages/react-core/src/components/Masthead/examples/MastheadBasic.tsx @@ -7,13 +7,12 @@ import { MastheadContent, Button } from '@patternfly/react-core'; -import BarsIcon from '@patternfly/react-icons/dist/js/icons/bars-icon'; export const MastheadBasic: React.FunctionComponent = () => ( - ); }} diff --git a/packages/react-core/src/components/Page/examples/PageCenteredSection.tsx b/packages/react-core/src/components/Page/examples/PageCenteredSection.tsx index 4a5f7ec6edb..5056133af7a 100644 --- a/packages/react-core/src/components/Page/examples/PageCenteredSection.tsx +++ b/packages/react-core/src/components/Page/examples/PageCenteredSection.tsx @@ -17,7 +17,6 @@ import { Card, CardBody } from '@patternfly/react-core'; -import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon'; import pageSectionWidthLimitMaxWidth from '@patternfly/react-tokens/dist/esm/c_page_section_m_limit_width_MaxWidth'; export const PageCenteredSection: React.FunctionComponent = () => { @@ -40,14 +39,12 @@ export const PageCenteredSection: React.FunctionComponent = () => { - - + isHamburgerButton + /> diff --git a/packages/react-core/src/components/Page/examples/PageGroupSection.tsx b/packages/react-core/src/components/Page/examples/PageGroupSection.tsx index 2afbef70a96..13acf4f65b9 100644 --- a/packages/react-core/src/components/Page/examples/PageGroupSection.tsx +++ b/packages/react-core/src/components/Page/examples/PageGroupSection.tsx @@ -22,7 +22,6 @@ import { ToolbarContent, ToolbarItem } from '@patternfly/react-core'; -import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon'; export const PageGroupSection: React.FunctionComponent = () => { const [isSidebarOpen, setIsSidebarOpen] = useState(true); @@ -44,14 +43,12 @@ export const PageGroupSection: React.FunctionComponent = () => { - - + isHamburgerButton + /> diff --git a/packages/react-core/src/components/Page/examples/PageMainSectionPadding.tsx b/packages/react-core/src/components/Page/examples/PageMainSectionPadding.tsx index d3b8459bafb..96ee9199d7b 100644 --- a/packages/react-core/src/components/Page/examples/PageMainSectionPadding.tsx +++ b/packages/react-core/src/components/Page/examples/PageMainSectionPadding.tsx @@ -15,7 +15,6 @@ import { ToolbarContent, ToolbarItem } from '@patternfly/react-core'; -import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon'; export const PageMainSectionPadding: React.FunctionComponent = () => { const [isSidebarOpen, setIsSidebarOpen] = useState(true); @@ -37,14 +36,12 @@ export const PageMainSectionPadding: React.FunctionComponent = () => { - - + isHamburgerButton + /> diff --git a/packages/react-core/src/components/Page/examples/PageMainSectionVariations.tsx b/packages/react-core/src/components/Page/examples/PageMainSectionVariations.tsx index 24e90688458..101f6057170 100644 --- a/packages/react-core/src/components/Page/examples/PageMainSectionVariations.tsx +++ b/packages/react-core/src/components/Page/examples/PageMainSectionVariations.tsx @@ -15,7 +15,6 @@ import { ToolbarContent, ToolbarItem } from '@patternfly/react-core'; -import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon'; export const PageMainSectionPadding: React.FunctionComponent = () => { const [isSidebarOpen, setIsSidebarOpen] = useState(true); @@ -37,14 +36,12 @@ export const PageMainSectionPadding: React.FunctionComponent = () => { - - + isHamburgerButton + /> diff --git a/packages/react-core/src/components/Page/examples/PageMultipleSidebarBody.tsx b/packages/react-core/src/components/Page/examples/PageMultipleSidebarBody.tsx index 8d9ef03a760..ff3d5b0369d 100644 --- a/packages/react-core/src/components/Page/examples/PageMultipleSidebarBody.tsx +++ b/packages/react-core/src/components/Page/examples/PageMultipleSidebarBody.tsx @@ -15,7 +15,6 @@ import { ToolbarContent, ToolbarItem } from '@patternfly/react-core'; -import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon'; export const PageMultipleSidebarBody: React.FunctionComponent = () => { const [isSidebarOpen, setIsSidebarOpen] = useState(true); @@ -37,14 +36,12 @@ export const PageMultipleSidebarBody: React.FunctionComponent = () => { - - + isHamburgerButton + /> diff --git a/packages/react-core/src/components/Page/examples/PageUncontrolledNav.tsx b/packages/react-core/src/components/Page/examples/PageUncontrolledNav.tsx index 229d3282ed3..7db77b8333b 100644 --- a/packages/react-core/src/components/Page/examples/PageUncontrolledNav.tsx +++ b/packages/react-core/src/components/Page/examples/PageUncontrolledNav.tsx @@ -14,7 +14,6 @@ import { ToolbarContent, ToolbarItem } from '@patternfly/react-core'; -import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon'; export const PageUncontrolledNav: React.FunctionComponent = () => { const headerToolbar = ( @@ -29,9 +28,7 @@ export const PageUncontrolledNav: React.FunctionComponent = () => { - - - + diff --git a/packages/react-core/src/components/Page/examples/PageVerticalNav.tsx b/packages/react-core/src/components/Page/examples/PageVerticalNav.tsx index 40a8bec260a..dc7f6ab7a77 100644 --- a/packages/react-core/src/components/Page/examples/PageVerticalNav.tsx +++ b/packages/react-core/src/components/Page/examples/PageVerticalNav.tsx @@ -15,7 +15,6 @@ import { ToolbarContent, ToolbarItem } from '@patternfly/react-core'; -import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon'; export const PageVerticalNav: React.FunctionComponent = () => { const [isSidebarOpen, setIsSidebarOpen] = useState(true); @@ -37,14 +36,12 @@ export const PageVerticalNav: React.FunctionComponent = () => { - - + /> diff --git a/packages/react-core/src/components/Page/examples/PageWithOrWithoutFill.tsx b/packages/react-core/src/components/Page/examples/PageWithOrWithoutFill.tsx index e1ed070a173..3f2da77c839 100644 --- a/packages/react-core/src/components/Page/examples/PageWithOrWithoutFill.tsx +++ b/packages/react-core/src/components/Page/examples/PageWithOrWithoutFill.tsx @@ -15,7 +15,6 @@ import { ToolbarContent, ToolbarItem } from '@patternfly/react-core'; -import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon'; export const PageWithOrWithoutFill: React.FunctionComponent = () => { const [isSidebarOpen, setIsSidebarOpen] = useState(true); @@ -37,14 +36,12 @@ export const PageWithOrWithoutFill: React.FunctionComponent = () => { - - + /> diff --git a/packages/react-core/src/demos/DashboardHeader.tsx b/packages/react-core/src/demos/DashboardHeader.tsx index 0be0b556bb0..a89a7967986 100644 --- a/packages/react-core/src/demos/DashboardHeader.tsx +++ b/packages/react-core/src/demos/DashboardHeader.tsx @@ -23,7 +23,6 @@ import { ToolbarItem, PageToggleButton } from '../components'; -import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon'; import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon'; import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; @@ -132,9 +131,7 @@ export const DashboardHeader: React.FC = ({ notificationBa - - - + {patternflyLogo} @@ -159,7 +156,7 @@ export const DashboardHeader: React.FC = ({ notificationBa )} - ); - expect(screen.getByRole('button')).not.toHaveClass(`pf-m-${variant}`); + expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers[variant]); }); } - test(`Renders with class pf-m-${variant} when variant=${variant}`, () => { + test(`Renders with class ${styles.modifiers[variant]} when variant=${variant}`, () => { render(); - expect(screen.getByRole('button')).toHaveClass(`pf-m-${variant}`); + expect(screen.getByRole('button')).toHaveClass(styles.modifiers[variant]); }); }); @@ -26,14 +31,14 @@ test('Renders without children', () => { expect(screen.getByTestId('container').firstChild).toBeVisible(); }); -test('Renders with class pf-v6-c-button by default', () => { +test(`Renders with class ${styles.button} by default`, () => { render(); - expect(screen.getByRole('button')).toHaveClass('pf-v6-c-button'); + expect(screen.getByRole('button')).toHaveClass(styles.button); }); -test('Renders with class pf-m-primary by default', () => { +test(`Renders with class ${styles.modifiers.primary} by default`, () => { render(); - expect(screen.getByText('Button').parentElement).toHaveClass('pf-m-primary'); + expect(screen.getByText('Button').parentElement).toHaveClass(styles.modifiers.primary); }); test('Renders with custom class', () => { @@ -48,28 +53,28 @@ test('Renders with an aria-label', () => { expect(screen.getByLabelText(label)).toHaveAccessibleName('aria-label test'); }); -test('Renders with class pf-m-block when isBlock = true', () => { +test(`Renders with class ${styles.modifiers.block} when isBlock = true`, () => { render(); - expect(screen.getByRole('button')).toHaveClass('pf-m-block'); + expect(screen.getByRole('button')).toHaveClass(styles.modifiers.block); }); -test('Renders with class pf-m-clicked when isClicked = true', () => { +test(`Renders with class ${styles.modifiers.clicked} when isClicked = true`, () => { render(); - expect(screen.getByRole('button')).toHaveClass('pf-m-clicked'); + expect(screen.getByRole('button')).toHaveClass(styles.modifiers.clicked); }); -test('Does not render with class pf-m-disabled by default when isDisabled = true', () => { +test(`Does not render with class ${styles.modifiers.disabled} by default when isDisabled = true`, () => { render(); - expect(screen.getByRole('button')).not.toHaveClass('pf-m-disabled'); + expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.disabled); }); -test('Renders with class pf-m-disabled when isDisabled = true and component is not a button', () => { +test(`Renders with class ${styles.modifiers.disabled} when isDisabled = true and component is not a button`, () => { render( ); - expect(screen.getByText('Disabled Anchor Button').parentElement).toHaveClass('pf-m-disabled'); + expect(screen.getByText('Disabled Anchor Button').parentElement).toHaveClass(styles.modifiers.disabled); }); test(`aria-disabled and class ${styles.modifiers.ariaDisabled} are not rendered when isAriaDisabled is not passed by default`, () => { @@ -105,120 +110,117 @@ test('Does not disable button when isDisabled = true and component = a', () => { expect(screen.getByText('Disabled yet focusable button')).not.toHaveProperty('disabled'); }); -test('Renders with class pf-m-unread by default when variant = stateful', () => { +test(`Renders with class ${styles.modifiers.unread} by default when variant = stateful`, () => { render(); - expect(screen.getByRole('button')).toHaveClass('pf-m-stateful', 'pf-m-unread'); + expect(screen.getByRole('button')).toHaveClass(styles.modifiers.unread); }); Object.values(ButtonState).forEach((state) => { - test(`Renders with class pf-m-${state} when state = ${state} and variant = stateful`, () => { + test(`Renders with class ${styles.modifiers[state]} when state = ${state} and variant = stateful`, () => { render( ); - expect(screen.getByRole('button')).toHaveClass('pf-m-stateful', `pf-m-${state}`); + expect(screen.getByRole('button')).toHaveClass(styles.modifiers[state]); }); }); -test('Renders with class pf-m-danger when isDanger = true and variant = secondary', () => { - render( - - ); - expect(screen.getByRole('button')).toHaveClass('pf-m-danger', 'pf-m-secondary'); +Object.values(validDangerVariants).forEach((validDangerVariant) => { + test(`Renders with class ${styles.modifiers.danger} when isDanger is true and variant = ${validDangerVariant}`, () => { + render( + + ); + expect(screen.getByRole('button')).toHaveClass(styles.modifiers.danger); + }); }); -test('Renders with class pf-m-danger when isDanger = true and variant = link', () => { - render( - - ); - expect(screen.getByRole('button')).toHaveClass('pf-m-danger', 'pf-m-link'); +Object.values(invalidDangerVariants).forEach((invalidDangerVariant) => { + test(`Does not render with class ${styles.modifiers.danger} when isDanger is true and variant = ${invalidDangerVariant}`, () => { + render( + + ); + expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.danger); + }); }); -test('Does not render with class pf-m-danger when isDanger = true and variant != secondary or link', () => { +test(`Renders with class ${styles.modifiers.inline} when isInline = true and variant = link`, () => { render( - ); - expect(screen.getByRole('button')).not.toHaveClass('pf-m-danger'); + expect(screen.getByRole('button')).toHaveClass(styles.modifiers.inline); }); -test('Does not render with class pf-m-danger when isDanger = true and variant = tertiary', () => { - render( - - ); - expect(screen.getByRole('button')).not.toHaveClass('pf-m-danger'); +Object.values(invalidInlineVariants).forEach((invalidInlineVariant) => { + test(`Does not render with class ${styles.modifiers.inline} when isInline is true and variant = ${invalidInlineVariants}`, () => { + render( + + ); + expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.inline); + }); }); -test('Does not render with class pf-m-danger when isDanger = true and variant = control', () => { - render( - - ); - expect(screen.getByRole('button')).not.toHaveClass('pf-m-danger'); +test(`Renders with class ${styles.modifiers.small} when size = sm`, () => { + render(); + expect(screen.getByRole('button')).toHaveClass(styles.modifiers.small); +}); + +test(`Renders with class ${styles.modifiers.displayLg} when size = lg`, () => { + render(); + expect(screen.getByRole('button')).toHaveClass(styles.modifiers.displayLg); }); -test('Renders with class pf-m-inline when isInline = true and variant = link', () => { +test(`Renders with classes ${styles.modifiers.inProgress} when isLoading = true`, () => { render( - ); - expect(screen.getByRole('button')).toHaveClass('pf-m-inline'); -}); - -test('Renders with class pf-m-small when size = sm', () => { - render(); - expect(screen.getByRole('button')).toHaveClass('pf-m-small'); -}); - -test('Renders with class pf-m-display-lg when size = lg', () => { - render(); - expect(screen.getByRole('button')).toHaveClass('pf-m-display-lg'); + expect(screen.getByRole('button')).toHaveClass(styles.modifiers.inProgress); }); -test('Renders with class pf-m-in-progress when isLoading = true', () => { +test(`Renders with class ${styles.modifiers.progress} when isLoading is true`, () => { render( ); - expect(screen.getByRole('button')).toHaveClass('pf-m-in-progress'); + expect(screen.getByRole('button')).toHaveClass(styles.modifiers.progress); }); -test('Renders with class pf-m-progress when isLoading is defined and isLoading = false', () => { +test(`Renders with class ${styles.modifiers.progress} when isLoading is defined and isLoading = false`, () => { render( ); - expect(screen.getByRole('button')).toHaveClass('pf-m-progress'); + expect(screen.getByRole('button')).toHaveClass(styles.modifiers.progress); }); -test('Renders without class pf-m-progress when isLoading = false and variant = plain', () => { +test(`Renders without class ${styles.modifiers.progress} when isLoading = false and variant = plain`, () => { render( ); - expect(screen.getByRole('button')).not.toHaveClass('pf-m-progress'); + expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.progress); }); -test('Renders custom icon with class pf-m-in-progress when isLoading = true and icon is present', () => { +test(`Renders custom icon with class ${styles.modifiers.inProgress} when isLoading = true and icon is present`, () => { render( ); + + expect(screen.getByRole('button')).not.toHaveAttribute('aria-expanded'); +}); + +test('Renders with aria-expanded when isExpanded is true', () => { + render(); + + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'); +}); + +test('Renders with aria-expanded when isExpanded is false', () => { + render(); + + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false'); +}); + +// Remove this test when isExpanded prop in Button code is moved to after the spread props +test('Passing aria-expanded overrides isExpanded', () => { + render( + + ); + + expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false'); +}); + +describe('Hamburger button', () => { + test('Throws console error when isHamburger is true and isExpanded is not passed', () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(); + + render( + ); + + expect(screen.queryByText('Hamburger text content')).not.toBeInTheDocument(); + }); +}); + +describe('Settings button', () => { + // TODO: Remove isSettings in breaking change to throw error for any non-hamburger button that does not have children or aria-label + test('Throws console error when isSettings is true and neither children, aria-label nor aria-lablledby are passed', () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(); + + render(); + + expect(consoleError).not.toHaveBeenCalledWith( + 'Button: you must provide either visible text content or an accessible name via the aria-label or aria-labelledby properties.' + ); + }); + + // TODO: Remove isSettings in breaking change to throw error for any non-hamburger button that does not have children or aria-label + test('Does not throw console error when isSettings is true and aria-label is passed', () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(); + + render(); expect(asFragment()).toMatchSnapshot(); diff --git a/packages/react-core/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap b/packages/react-core/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap index ee4c4cab432..38631eb934d 100644 --- a/packages/react-core/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap +++ b/packages/react-core/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap @@ -5,7 +5,7 @@ exports[`Renders basic button 1`] = ` + ); + expect(consoleError).not.toHaveBeenCalledWith( + 'Button: you must provide either visible text content or an accessible name via the aria-label or aria-labelledby properties.' + ); + }); + + // TODO: Remove isHamburger in breaking change to throw error for any button that does not have children or aria name test('Does not throw console error when isHamburger is true and aria-label is passed', () => { const consoleError = jest.spyOn(console, 'error').mockImplementation(); @@ -326,6 +342,7 @@ describe('Hamburger button', () => { ); }); + // TODO: Remove isHamburger in breaking change to throw error for any button that does not have children or aria name test('Does not throw console error when isHamburger is true and aria-labelledby is passed', () => { const consoleError = jest.spyOn(console, 'error').mockImplementation(); @@ -341,19 +358,32 @@ describe('Hamburger button', () => { ); }); + test(`Does not render with class ${styles.modifiers.hamburger} by default`, () => { + render( - ); - - expect(screen.queryByText('Hamburger text content')).not.toBeInTheDocument(); - }); }); describe('Settings button', () => { - // TODO: Remove isSettings in breaking change to throw error for any non-hamburger button that does not have children or aria-label + // TODO: Remove isSettings in breaking change to throw error for any button that does not have children or aria name test('Throws console error when isSettings is true and neither children, aria-label nor aria-lablledby are passed', () => { const consoleError = jest.spyOn(console, 'error').mockImplementation(); @@ -404,7 +417,7 @@ describe('Settings button', () => { ); }); - // TODO: Remove isSettings in breaking change to throw error for any non-hamburger button that does not have children or aria-label + // TODO: Remove isSettings in breaking change to throw error for any button that does not have children or aria name test('Does not throw console error when isSettings is true and children is passed', () => { const consoleError = jest.spyOn(console, 'error').mockImplementation(); @@ -415,7 +428,7 @@ describe('Settings button', () => { ); }); - // TODO: Remove isSettings in breaking change to throw error for any non-hamburger button that does not have children or aria-label + // TODO: Remove isSettings in breaking change to throw error for any button that does not have children or aria name test('Does not throw console error when isSettings is true and aria-label is passed', () => { const consoleError = jest.spyOn(console, 'error').mockImplementation(); @@ -426,7 +439,7 @@ describe('Settings button', () => { ); }); - // TODO: Remove isSettings in breaking change to throw error for any non-hamburger button that does not have children or aria-label + // TODO: Remove isSettings in breaking change to throw error for any button that does not have children or aria name test('Does not throw console error when isSettings is true and aria-labelledby is passed', () => { const consoleError = jest.spyOn(console, 'error').mockImplementation(); @@ -442,17 +455,16 @@ describe('Settings button', () => { ); }); - test(`Renders with class ${styles.modifiers.settings} when isSettings is true`, () => { - render( + ); diff --git a/packages/react-core/src/components/Masthead/examples/MastheadBasic.tsx b/packages/react-core/src/components/Masthead/examples/MastheadBasic.tsx index c7fbc3fd1c2..b47a82f464a 100644 --- a/packages/react-core/src/components/Masthead/examples/MastheadBasic.tsx +++ b/packages/react-core/src/components/Masthead/examples/MastheadBasic.tsx @@ -12,7 +12,7 @@ export const MastheadBasic: React.FunctionComponent = () => ( - ); + expect(asFragment()).toMatchSnapshot(); }); diff --git a/packages/react-core/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap b/packages/react-core/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap index 29d46df4ed5..16c86ef752a 100644 --- a/packages/react-core/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap +++ b/packages/react-core/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap @@ -5,7 +5,7 @@ exports[`Renders basic button 1`] = `