diff --git a/packages/react-core/src/components/Drawer/Drawer.tsx b/packages/react-core/src/components/Drawer/Drawer.tsx index 3470426a6b8..eaa6c58dfcd 100644 --- a/packages/react-core/src/components/Drawer/Drawer.tsx +++ b/packages/react-core/src/components/Drawer/Drawer.tsx @@ -5,6 +5,9 @@ import { css } from '@patternfly/react-styles'; export enum DrawerColorVariant { default = 'default', secondary = 'secondary', + /** + * @deprecated `DrawerColorVariant.noBackground` is deprecated. Use the `isPlain` prop on `DrawerPanelContent` and the `DrawerSection`instead. + */ noBackground = 'no-background' } diff --git a/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx b/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx index 814390d3955..2933af16965 100644 --- a/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx +++ b/packages/react-core/src/components/Drawer/DrawerPanelContent.tsx @@ -37,6 +37,12 @@ export interface DrawerPanelContentProps extends Omit void; /** The minimum size of a drawer. */ @@ -56,7 +62,10 @@ export interface DrawerPanelContentProps extends Omit { className?: string; /** Content to be rendered in the drawer section. */ children?: React.ReactNode; - /** Color variant of the background of the drawer Section */ + /** + * Color variant of the background of the drawer section. + * The `no-background` value is deprecated; use the `isPlain` prop instead. + */ colorVariant?: DrawerColorVariant | 'no-background' | 'default' | 'secondary'; + /** @beta Flag indicating that the drawer section should use plain styles. */ + isPlain?: boolean; } export const DrawerSection: React.FunctionComponent = ({ className = '', children, colorVariant = DrawerColorVariant.default, + isPlain = false, ...props }: DrawerSectionProps) => (
{ expect(screen.getByText('Drawer panel content')).toHaveClass(styles.drawerPanel, { exact: true }); }); -test(`Renders with class ${styles.modifiers.noBackground} when colorVariant="no-background"`, () => { +test(`Renders with class ${styles.modifiers.noBackground} when deprecated colorVariant="no-background" is used`, () => { render( Drawer panel content @@ -188,3 +188,33 @@ test(`Renders with class 'pf-m-no-glass' when hasNoGlass is true`, () => { expect(screen.getByText('Drawer panel content')).toHaveClass('pf-m-no-glass'); }); + +test(`Renders with class ${styles.modifiers.glass} when isGlass is true`, () => { + render( + + Drawer panel content + + ); + + expect(screen.getByText('Drawer panel content')).toHaveClass(styles.modifiers.glass); +}); + +test(`Renders with class ${styles.modifiers.plain} when isPlain is true`, () => { + render( + + Drawer panel content + + ); + + expect(screen.getByText('Drawer panel content')).toHaveClass(styles.modifiers.plain); +}); + +test(`Renders with class ${styles.modifiers.noPlainOnGlass} when isNoPlainOnGlass is true`, () => { + render( + + Drawer panel content + + ); + + expect(screen.getByText('Drawer panel content')).toHaveClass(styles.modifiers.noPlainOnGlass); +}); diff --git a/packages/react-core/src/components/Drawer/__tests__/DrawerSection.test.tsx b/packages/react-core/src/components/Drawer/__tests__/DrawerSection.test.tsx new file mode 100644 index 00000000000..e080cb25a8c --- /dev/null +++ b/packages/react-core/src/components/Drawer/__tests__/DrawerSection.test.tsx @@ -0,0 +1,37 @@ +import { render, screen } from '@testing-library/react'; +import { DrawerColorVariant } from '../Drawer'; +import { DrawerSection } from '../DrawerSection'; +import styles from '@patternfly/react-styles/css/components/Drawer/drawer'; + +test(`Renders with only class ${styles.drawerSection} by default`, () => { + render(Section content); + + expect(screen.getByText('Section content')).toHaveClass(styles.drawerSection, { exact: true }); +}); + +test(`Applies ${styles.drawerSection} and ${styles.modifiers.plain} when isPlain is true`, () => { + render(Section content); + + const section = screen.getByText('Section content'); + expect(section).toHaveClass(styles.drawerSection); + expect(section).toHaveClass(styles.modifiers.plain); +}); + +test(`Does not apply ${styles.modifiers.plain} when isPlain is false`, () => { + render(Section content); + + expect(screen.getByText('Section content')).not.toHaveClass(styles.modifiers.plain); +}); + +test(`Applies plain and secondary modifiers together when isPlain and colorVariant are set`, () => { + render( + + Section content + + ); + + const section = screen.getByText('Section content'); + expect(section).toHaveClass(styles.drawerSection); + expect(section).toHaveClass(styles.modifiers.plain); + expect(section).toHaveClass(styles.modifiers.secondary); +}); diff --git a/packages/react-core/src/components/Drawer/examples/Drawer.md b/packages/react-core/src/components/Drawer/examples/Drawer.md index 544bc08a2ba..22d79215fe6 100644 --- a/packages/react-core/src/components/Drawer/examples/Drawer.md +++ b/packages/react-core/src/components/Drawer/examples/Drawer.md @@ -13,7 +13,7 @@ propComponents: DrawerCloseButton, DrawerPanelDescription, DrawerPanelBody, - DrawerPanelFocusTrapObject + DrawerPanelFocusTrapObject, ] section: components --- diff --git a/packages/react-core/src/demos/examples/PrimaryDetail/PrimaryDetailContentPadding.tsx b/packages/react-core/src/demos/examples/PrimaryDetail/PrimaryDetailContentPadding.tsx index c19ed8ca8ca..cd5f84f2bae 100644 --- a/packages/react-core/src/demos/examples/PrimaryDetail/PrimaryDetailContentPadding.tsx +++ b/packages/react-core/src/demos/examples/PrimaryDetail/PrimaryDetailContentPadding.tsx @@ -178,7 +178,7 @@ export const PrimaryDetailContentPadding: React.FunctionComponent = () => { ); const panelContent = ( - + node-{drawerPanelBodyContent} @@ -429,7 +429,7 @@ export const PrimaryDetailContentPadding: React.FunctionComponent = () => { <Divider component="div" /> <PageSection padding={{ default: 'noPadding' }} aria-label="Drawer content section"> <Drawer isExpanded={isDrawerExpanded}> - <DrawerContent panelContent={panelContent} colorVariant="no-background"> + <DrawerContent panelContent={panelContent}> <DrawerContentBody hasPadding>{drawerContent}</DrawerContentBody> </DrawerContent> </Drawer> diff --git a/packages/react-integration/cypress/integration/drawer.spec.ts b/packages/react-integration/cypress/integration/drawer.spec.ts index 02a06d3fb9d..544ddb0cabe 100644 --- a/packages/react-integration/cypress/integration/drawer.spec.ts +++ b/packages/react-integration/cypress/integration/drawer.spec.ts @@ -1,8 +1,156 @@ +const visitDrawerDemoWithGlassTheme = () => { + cy.visit('http://localhost:3000/drawer-demo-nav-link'); + cy.viewport(1280, 800); + cy.document().then((doc) => { + doc.documentElement.classList.add('pf-v6-theme-glass'); + }); + cy.get('html').should('have.class', 'pf-v6-theme-glass'); +}; + +/** Matches integration checks for “glass” panel visuals (semi-transparent bg and/or backdrop-filter). */ +const rgbaCommaAlpha = (color: string): number | undefined => { + if (color === 'transparent') { + return 0; + } + if (!color.startsWith('rgba(') || !color.endsWith(')')) { + return undefined; + } + const inner = color.slice('rgba('.length, -1); + const parts = inner.split(',').map((p) => p.trim()); + if (parts.length !== 4) { + return undefined; + } + return parseFloat(parts[3]); +}; + +const rgbSlashAlpha = (color: string): number | undefined => { + if (!color.startsWith('rgb(')) { + return undefined; + } + const slash = color.indexOf('/'); + const close = color.lastIndexOf(')'); + if (slash === -1 || close === -1 || slash >= close) { + return undefined; + } + const a = parseFloat(color.slice(slash + 1, close).trim()); + return Number.isNaN(a) ? undefined : a; +}; + +const drawerPanelShowsGlassVisualTreatment = (el: HTMLElement): boolean => { + const style = window.getComputedStyle(el); + const bg = style.backgroundColor; + const backdrop = style.backdropFilter; + const alpha = rgbaCommaAlpha(bg) ?? rgbSlashAlpha(bg); + const hasSemiTransparentBackground = alpha !== undefined && alpha < 1; + const hasBackdropBlur = Boolean(backdrop && backdrop !== 'none'); + return hasSemiTransparentBackground || hasBackdropBlur; +}; + +const assertGlassPlainPanel = (testId: string, headlineSnippet: string) => { + cy.get(`[data-testid="${testId}"]`).should(($el) => { + expect($el, testId).to.have.length(1); + expect($el).to.not.have.attr('hidden'); + expect($el).to.not.have.attr('inert'); + expect($el).to.have.class('pf-m-glass'); + expect($el).to.have.class('pf-m-plain'); + expect($el).to.have.class('pf-m-no-plain-on-glass'); + expect($el).to.contain.text(headlineSnippet); + }); + + cy.get(`[data-testid="${testId}"]`).should(($el) => { + if (!drawerPanelShowsGlassVisualTreatment($el[0])) { + const style = window.getComputedStyle($el[0]); + throw new Error( + `expected glass panel (semi-transparent background or backdrop-filter); got backgroundColor=${style.backgroundColor}, backdropFilter=${style.backdropFilter || ''}` + ); + } + }); +}; + describe('Drawer Demo Test', () => { + afterEach(() => { + cy.document().then((doc) => { + doc.documentElement.classList.remove('pf-v6-theme-glass'); + }); + }); + it('Navigate to the drawer demo', () => { cy.visit('http://localhost:3000/drawer-demo-nav-link'); }); + it('glass theme + isInline drawer + plain/glass: panel shows glass treatment (transparent bg and/or backdrop-filter)', () => { + visitDrawerDemoWithGlassTheme(); + + cy.get('#drawer-glass-plain-inline.pf-v6-c-drawer').should(($drawer) => { + expect($drawer).to.have.length(1); + expect($drawer).to.have.class('pf-m-expanded'); + expect($drawer).to.have.class('pf-m-inline'); + expect($drawer).to.not.have.class('pf-m-static'); + }); + + assertGlassPlainPanel('drawer-glass-plain-panel-inline', 'Glass theme plain / no-plain-on-glass combo (inline)'); + }); + + it('glass theme + isStatic drawer + plain/glass: panel shows glass treatment (transparent bg and/or backdrop-filter)', () => { + visitDrawerDemoWithGlassTheme(); + + cy.get('#drawer-glass-plain-static.pf-v6-c-drawer').should(($drawer) => { + expect($drawer).to.have.length(1); + expect($drawer).to.have.class('pf-m-expanded'); + expect($drawer).to.have.class('pf-m-static'); + expect($drawer).to.not.have.class('pf-m-inline'); + }); + + assertGlassPlainPanel('drawer-glass-plain-panel-static', 'Glass theme plain / no-plain-on-glass combo (static)'); + }); + + it('glass theme + default overlay drawer: isPlain / isGlass / isNoPlainOnGlass modifiers do not get glass panel styles from Core', () => { + visitDrawerDemoWithGlassTheme(); + + cy.get('#drawer-glass-plain-overlay.pf-v6-c-drawer').should(($drawer) => { + expect($drawer).to.have.length(1); + expect($drawer).to.have.class('pf-m-expanded'); + expect($drawer).to.not.have.class('pf-m-inline'); + expect($drawer).to.not.have.class('pf-m-static'); + }); + + cy.get('[data-testid="drawer-glass-plain-panel-overlay"]').should(($el) => { + expect($el).to.have.length(1); + expect($el).to.not.have.attr('hidden'); + expect($el).to.not.have.attr('inert'); + expect($el).to.have.class('pf-m-glass'); + expect($el).to.have.class('pf-m-plain'); + expect($el).to.have.class('pf-m-no-plain-on-glass'); + expect($el).to.contain.text('Glass theme plain / no-plain-on-glass combo (overlay)'); + expect( + drawerPanelShowsGlassVisualTreatment($el[0]), + 'Core should not apply glass/plain-on-glass panel treatment without inline or static drawer' + ).to.equal(false); + }); + }); + + it('glass theme: drawer panel has no glass CSS when isGlass is false', () => { + visitDrawerDemoWithGlassTheme(); + + cy.get('#drawer-glass-theme-no-isglass.pf-v6-c-drawer').should(($drawer) => { + expect($drawer).to.have.length(1); + expect($drawer).to.have.class('pf-m-expanded'); + expect($drawer).to.have.class('pf-m-inline'); + }); + + cy.get('[data-testid="drawer-panel-content-glass-theme-no-isglass"]').should(($el) => { + expect($el).to.have.length(1); + expect($el).to.not.have.attr('hidden'); + expect($el).to.not.have.attr('inert'); + expect($el).to.have.class('pf-m-plain'); + expect($el).to.not.have.class('pf-m-glass'); + expect( + drawerPanelShowsGlassVisualTreatment($el[0]), + 'panel must not get glass treatment from theme alone; set isGlass for pf-m-glass and glass styles' + ).to.equal(false); + }); + }); + it('Verify focus is automatically handled with focus trap enabled', () => { cy.get('#toggleFocusTrapButton').click(); cy.get('#focusTrap-panelContent .pf-v6-c-button.pf-m-plain').should('have.focus'); @@ -18,6 +166,48 @@ describe('Drawer Demo Test', () => { cy.get('#toggleCustomFocusButton').click(); }); + it('DrawerSection isPlain: Core applies pf-m-plain and computed styles differ from default section', () => { + cy.visit('http://localhost:3000/drawer-demo-nav-link'); + cy.viewport(1280, 800); + cy.get('#toggleButton').click(); + cy.get('#basic-drawer.pf-v6-c-drawer').should('have.class', 'pf-m-expanded'); + + cy.get('[data-testid="drawer-section-is-plain"]').should(($el) => { + expect($el).to.have.length(1); + expect($el).to.have.class('pf-v6-c-drawer__section'); + expect($el).to.have.class('pf-m-plain'); + }); + + cy.get('[data-testid="drawer-section-default"]').should(($el) => { + expect($el).to.have.length(1); + expect($el).to.have.class('pf-v6-c-drawer__section'); + expect($el).to.not.have.class('pf-m-plain'); + }); + + cy.get('[data-testid="drawer-section-default"]').then(($default) => { + cy.get('[data-testid="drawer-section-is-plain"]').should(($plain) => { + const sDef = window.getComputedStyle($default[0]); + const sPlain = window.getComputedStyle($plain[0]); + const differs = + sPlain.backgroundColor !== sDef.backgroundColor || + sPlain.boxShadow !== sDef.boxShadow || + sPlain.borderTopWidth !== sDef.borderTopWidth; + if (!differs) { + throw new Error( + `expected isPlain section to differ from default in backgroundColor, boxShadow, or borderTopWidth; ` + + `bg default=${sDef.backgroundColor} plain=${sPlain.backgroundColor}; ` + + `boxShadow default=${sDef.boxShadow} plain=${sPlain.boxShadow}; ` + + `borderTopWidth default=${sDef.borderTopWidth} plain=${sPlain.borderTopWidth}` + ); + } + }); + }); + + // Leave basic drawer collapsed for later specs (e.g. "Verify drawer expands and collapses"). + cy.get('#toggleButton').click(); + cy.get('#basic-drawer.pf-v6-c-drawer').should('not.have.class', 'pf-m-expanded'); + }); + it('Verify text in content', () => { const drawerContent = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus pretium est a porttitor vehicula. Quisque vel commodo urna. Morbi mattis rutrum ante, id vehicula ex accumsan ut. Morbi viverra, eros vel porttitor facilisis, eros purus aliquet erat,nec lobortis felis elit pulvinar sem. Vivamus vulputate, risus eget commodo eleifend, eros nibh porta quam, vitae lacinia leo libero at magna. Maecenas aliquam sagittis orci, et posuere nisi ultrices sit amet. Aliquam ex odio, malesuada sed posuere quis, pellentesque at mauris. Phasellus venenatis massa ex, eget pulvinar libero auctor pretium. Aliquam erat volutpat. Duis euismod justo in quam ullamcorper, in commodo massa vulputate.'; diff --git a/packages/react-integration/demo-app-ts/src/components/demos/DrawerDemo/DrawerDemo.tsx b/packages/react-integration/demo-app-ts/src/components/demos/DrawerDemo/DrawerDemo.tsx index 6286a8a2330..86727db2a9c 100644 --- a/packages/react-integration/demo-app-ts/src/components/demos/DrawerDemo/DrawerDemo.tsx +++ b/packages/react-integration/demo-app-ts/src/components/demos/DrawerDemo/DrawerDemo.tsx @@ -118,6 +118,60 @@ export class DrawerDemo extends Component<DrawerProps, DrawerDemoState> { const drawerContent = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus pretium est a porttitor vehicula. Quisque vel commodo urna. Morbi mattis rutrum ante, id vehicula ex accumsan ut. Morbi viverra, eros vel porttitor facilisis, eros purus aliquet erat,nec lobortis felis elit pulvinar sem. Vivamus vulputate, risus eget commodo eleifend, eros nibh porta quam, vitae lacinia leo libero at magna. Maecenas aliquam sagittis orci, et posuere nisi ultrices sit amet. Aliquam ex odio, malesuada sed posuere quis, pellentesque at mauris. Phasellus venenatis massa ex, eget pulvinar libero auctor pretium. Aliquam erat volutpat. Duis euismod justo in quam ullamcorper, in commodo massa vulputate.'; + const glassThemePlainInlinePanelContent = ( + <DrawerPanelContent + id="drawer-panel-glass-plain-inline" + data-testid="drawer-glass-plain-panel-inline" + isPlain + isNoPlainOnGlass + isGlass + > + <DrawerHead> + <span>Glass theme plain / no-plain-on-glass combo (inline)</span> + </DrawerHead> + </DrawerPanelContent> + ); + + const glassThemePlainStaticPanelContent = ( + <DrawerPanelContent + id="drawer-panel-glass-plain-static" + data-testid="drawer-glass-plain-panel-static" + isPlain + isNoPlainOnGlass + isGlass + > + <DrawerHead> + <span>Glass theme plain / no-plain-on-glass combo (static)</span> + </DrawerHead> + </DrawerPanelContent> + ); + + const glassThemePlainOverlayPanelContent = ( + <DrawerPanelContent + id="drawer-panel-glass-plain-overlay" + data-testid="drawer-glass-plain-panel-overlay" + isPlain + isNoPlainOnGlass + isGlass + > + <DrawerHead> + <span>Glass theme plain / no-plain-on-glass combo (overlay)</span> + </DrawerHead> + </DrawerPanelContent> + ); + + const glassThemeInlinePanelNoIsGlass = ( + <DrawerPanelContent + id="drawer-panel-glass-theme-no-isglass" + data-testid="drawer-panel-content-glass-theme-no-isglass" + isPlain + > + <DrawerHead> + <span>Glass theme on html, isPlain, isGlass false</span> + </DrawerHead> + </DrawerPanelContent> + ); + return ( <> <Button id="toggleButton" onClick={this.onClick}> @@ -130,7 +184,12 @@ export class DrawerDemo extends Component<DrawerProps, DrawerDemoState> { position="bottom" style={{ minHeight: '300px', height: '300px' }} > - <DrawerSection colorVariant={DrawerColorVariant.default}>drawer-section</DrawerSection> + <DrawerSection colorVariant={DrawerColorVariant.default} data-testid="drawer-section-default"> + drawer-section + </DrawerSection> + <DrawerSection isPlain data-testid="drawer-section-is-plain"> + drawer-section plain + </DrawerSection> <DrawerContent colorVariant={DrawerColorVariant.default} panelContent={panelContent}> <DrawerContentBody>{drawerContent}</DrawerContentBody> </DrawerContent> @@ -151,6 +210,47 @@ export class DrawerDemo extends Component<DrawerProps, DrawerDemoState> { <DrawerContentBody>{drawerContent}</DrawerContentBody> </DrawerContent> </Drawer> + <div id="drawer-glass-plain-demos"> + {/* + Split drawers so integration tests cover isInline, isStatic, and default overlay separately. + Plain / glass / no-plain-on-glass Core styles apply to inline & static; overlay keeps modifiers from React only. + */} + <Drawer + id="drawer-glass-plain-inline" + isExpanded={true} + isInline={true} + style={{ minHeight: '120px', height: '120px' }} + > + <DrawerContent colorVariant={DrawerColorVariant.default} panelContent={glassThemePlainInlinePanelContent}> + <DrawerContentBody>Glass theme + isPlain + isGlass (inline)</DrawerContentBody> + </DrawerContent> + </Drawer> + <Drawer + id="drawer-glass-plain-static" + isExpanded={true} + isStatic={true} + style={{ minHeight: '120px', height: '120px' }} + > + <DrawerContent colorVariant={DrawerColorVariant.default} panelContent={glassThemePlainStaticPanelContent}> + <DrawerContentBody>Glass theme + isPlain + isGlass (static)</DrawerContentBody> + </DrawerContent> + </Drawer> + <Drawer id="drawer-glass-plain-overlay" isExpanded={true} style={{ minHeight: '120px', height: '120px' }}> + <DrawerContent colorVariant={DrawerColorVariant.default} panelContent={glassThemePlainOverlayPanelContent}> + <DrawerContentBody>Glass theme + isPlain + isGlass (default overlay)</DrawerContentBody> + </DrawerContent> + </Drawer> + <Drawer + id="drawer-glass-theme-no-isglass" + isExpanded={true} + isInline={true} + style={{ minHeight: '120px', height: '120px' }} + > + <DrawerContent colorVariant={DrawerColorVariant.default} panelContent={glassThemeInlinePanelNoIsGlass}> + <DrawerContentBody>pf-v6-theme-glass + isPlain only (no isGlass)</DrawerContentBody> + </DrawerContent> + </Drawer> + </div> </> ); }