Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/react-core/src/components/Drawer/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Comment on lines +8 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix deprecation JSDoc typo in public API docs.

The deprecation text has a formatting typo (DrawerSection\instead`) that will show up in generated docs and editor hovers.

Proposed fix
-   * `@deprecated` `DrawerColorVariant.noBackground` is deprecated. Use the `isPlain` prop on `DrawerPanelContent` and the `DrawerSection`instead.
+   * `@deprecated` `DrawerColorVariant.noBackground` is deprecated. Use the `isPlain` prop on `DrawerPanelContent` and `DrawerSection` instead.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* @deprecated `DrawerColorVariant.noBackground` is deprecated. Use the `isPlain` prop on `DrawerPanelContent` and the `DrawerSection`instead.
*/
/**
* `@deprecated` `DrawerColorVariant.noBackground` is deprecated. Use the `isPlain` prop on `DrawerPanelContent` and `DrawerSection` instead.
*/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-core/src/components/Drawer/Drawer.tsx` around lines 8 - 10,
Typo in the deprecation JSDoc for DrawerColorVariant.noBackground: fix the
formatting by replacing "DrawerSection\`instead" with "DrawerSection instead"
(or re-wrap correctly as `DrawerSection` instead) so generated docs/hovers
render properly; update the comment in the Drawer.tsx JSDoc for
DrawerColorVariant.noBackground and keep the rest of the message referencing
using the isPlain prop on DrawerPanelContent and DrawerSection intact.

noBackground = 'no-background'
}

Expand Down
17 changes: 16 additions & 1 deletion packages/react-core/src/components/Drawer/DrawerPanelContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export interface DrawerPanelContentProps extends Omit<React.HTMLProps<HTMLDivEle
isResizable?: boolean;
/** @beta Flag indicating that the drawer panel should disable glass styles. This prop is intended to work with isPill drawers. */
hasNoGlass?: boolean;
/** @beta Flag indicating that the drawer panel should use glass styles when in glass theme */
isGlass?: boolean;
/** @beta Flag indicating that the drawer panel should use plain styles. This only applies when the drawer is static or inline. */
isPlain?: boolean;
/** @beta Flag indicating that plain styles should be disabled when glass styles are used. This only applies when the drawer is static or inline. */
isNoPlainOnGlass?: boolean;
/** Callback for resize end. */
onResize?: (event: MouseEvent | TouchEvent | React.KeyboardEvent, width: number, id: string) => void;
/** The minimum size of a drawer. */
Expand All @@ -56,7 +62,10 @@ export interface DrawerPanelContentProps extends Omit<React.HTMLProps<HTMLDivEle
xl?: 'width_25' | 'width_33' | 'width_50' | 'width_66' | 'width_75' | 'width_100';
'2xl'?: 'width_25' | 'width_33' | 'width_50' | 'width_66' | 'width_75' | 'width_100';
};
/** Color variant of the background of the drawer panel */
/**
* Color variant of the background of the drawer panel.
* The `no-background`is deprecated; use the `isPlain` prop instead.
*/
Comment on lines +65 to +68
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clean up deprecation sentence spacing in colorVariant docs.

There’s a missing space in ``no-backgroundis, which hurts API doc readability.

Proposed fix
-   * The `no-background`is deprecated; use the `isPlain` prop instead.
+   * The `no-background` value is deprecated; use the `isPlain` prop instead.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Color variant of the background of the drawer panel.
* The `no-background`is deprecated; use the `isPlain` prop instead.
*/
/**
* Color variant of the background of the drawer panel.
* The `no-background` value is deprecated; use the `isPlain` prop instead.
*/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-core/src/components/Drawer/DrawerPanelContent.tsx` around
lines 65 - 68, The JSDoc for the colorVariant prop in DrawerPanelContent.tsx has
a missing space in the deprecation sentence; update the comment where
`colorVariant` is documented (in DrawerPanelContent.tsx) to read
"`no-background` is deprecated; use the `isPlain` prop instead." (i.e., insert a
space between `no-background` and "is") so the API docs render correctly and
improve readability.

colorVariant?: DrawerColorVariant | 'no-background' | 'default' | 'secondary';
/** Adds and customizes a focus trap on the drawer panel content. */
focusTrap?: DrawerPanelFocusTrapObject;
Expand All @@ -71,6 +80,9 @@ export const DrawerPanelContent: React.FunctionComponent<DrawerPanelContentProps
hasNoBorder = false,
isResizable = false,
hasNoGlass = false,
isGlass = false,
isPlain = false,
isNoPlainOnGlass = false,
onResize,
minSize,
defaultSize,
Expand Down Expand Up @@ -368,6 +380,9 @@ export const DrawerPanelContent: React.FunctionComponent<DrawerPanelContentProps
styles.drawerPanel,
isResizable && styles.modifiers.resizable,
hasNoGlass && 'pf-m-no-glass',
isGlass && styles.modifiers.glass,
isPlain && styles.modifiers.plain,
isNoPlainOnGlass && styles.modifiers.noPlainOnGlass,
hasNoBorder && styles.modifiers.noBorder,
formatBreakpointMods(widths, styles),
colorVariant === DrawerColorVariant.noBackground && styles.modifiers.noBackground,
Expand Down
9 changes: 8 additions & 1 deletion packages/react-core/src/components/Drawer/DrawerSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,26 @@ export interface DrawerSectionProps extends React.HTMLProps<HTMLDivElement> {
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<DrawerSectionProps> = ({
className = '',
children,
colorVariant = DrawerColorVariant.default,
isPlain = false,
...props
}: DrawerSectionProps) => (
<div
className={css(
styles.drawerSection,
isPlain && styles.modifiers.plain,
colorVariant === DrawerColorVariant.noBackground && styles.modifiers.noBackground,
colorVariant === DrawerColorVariant.secondary && styles.modifiers.secondary,
className
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ test(`Renders with only class ${styles.drawerPanel} by default`, () => {
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 isExpanded>
<DrawerPanelContent colorVariant="no-background">Drawer panel content</DrawerPanelContent>
Expand Down Expand Up @@ -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 isExpanded>
<DrawerPanelContent isGlass>Drawer panel content</DrawerPanelContent>
</Drawer>
);

expect(screen.getByText('Drawer panel content')).toHaveClass(styles.modifiers.glass);
});

test(`Renders with class ${styles.modifiers.plain} when isPlain is true`, () => {
render(
<Drawer isExpanded>
<DrawerPanelContent isPlain>Drawer panel content</DrawerPanelContent>
</Drawer>
);

expect(screen.getByText('Drawer panel content')).toHaveClass(styles.modifiers.plain);
});

test(`Renders with class ${styles.modifiers.noPlainOnGlass} when isNoPlainOnGlass is true`, () => {
render(
<Drawer isExpanded>
<DrawerPanelContent isNoPlainOnGlass>Drawer panel content</DrawerPanelContent>
</Drawer>
);

expect(screen.getByText('Drawer panel content')).toHaveClass(styles.modifiers.noPlainOnGlass);
});
Original file line number Diff line number Diff line change
@@ -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(<DrawerSection>Section content</DrawerSection>);

expect(screen.getByText('Section content')).toHaveClass(styles.drawerSection, { exact: true });
});

test(`Applies ${styles.drawerSection} and ${styles.modifiers.plain} when isPlain is true`, () => {
render(<DrawerSection isPlain>Section content</DrawerSection>);

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(<DrawerSection isPlain={false}>Section content</DrawerSection>);

expect(screen.getByText('Section content')).not.toHaveClass(styles.modifiers.plain);
});

test(`Applies plain and secondary modifiers together when isPlain and colorVariant are set`, () => {
render(
<DrawerSection isPlain colorVariant={DrawerColorVariant.secondary}>
Section content
</DrawerSection>
);

const section = screen.getByText('Section content');
expect(section).toHaveClass(styles.drawerSection);
expect(section).toHaveClass(styles.modifiers.plain);
expect(section).toHaveClass(styles.modifiers.secondary);
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ propComponents:
DrawerCloseButton,
DrawerPanelDescription,
DrawerPanelBody,
DrawerPanelFocusTrapObject
DrawerPanelFocusTrapObject,
]
section: components
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export const PrimaryDetailContentPadding: React.FunctionComponent = () => {
);

const panelContent = (
<DrawerPanelContent>
<DrawerPanelContent isPlain>
<DrawerHead>
<Title headingLevel="h2" size="xl">
node-{drawerPanelBodyContent}
Expand Down Expand Up @@ -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>
Expand Down
Binary file not shown.
191 changes: 191 additions & 0 deletions packages/react-integration/cypress/integration/drawer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,157 @@
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;
};
Comment on lines +26 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle percentage alpha correctly in rgbSlashAlpha.

parseFloat('50%') returns 50, so semi-transparent colors written as percentages can be treated as opaque by mistake.

Proposed fix
 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 rawAlpha = color.slice(slash + 1, close).trim();
+  const parsedAlpha = parseFloat(rawAlpha);
+  if (Number.isNaN(parsedAlpha)) {
+    return undefined;
+  }
+  return rawAlpha.endsWith('%') ? parsedAlpha / 100 : parsedAlpha;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 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 rawAlpha = color.slice(slash + 1, close).trim();
const parsedAlpha = parseFloat(rawAlpha);
if (Number.isNaN(parsedAlpha)) {
return undefined;
}
return rawAlpha.endsWith('%') ? parsedAlpha / 100 : parsedAlpha;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-integration/cypress/integration/drawer.spec.ts` around lines
26 - 37, The rgbSlashAlpha function misinterprets percentage alphas (e.g.,
"50%") because parseFloat returns 50; update rgbSlashAlpha to detect if the
extracted alpha string ends with '%' and, if so, parse the numeric part and
divide by 100 before returning; also trim whitespace, validate the parsed value
and return undefined for NaN or out-of-range values (ensure final alpha is
clamped/validated to 0–1). Reference: rgbSlashAlpha.


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);
});
});

// Blocked on Core: https://github.com/patternfly/patternfly/issues/8340
it.skip('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');
Expand All @@ -18,6 +167,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.';
Expand Down
Loading
Loading