Skip to content

Commit 9f2889d

Browse files
feat(Page): added responsive docked nav (#12327)
* feat(Page): added responsive docked nav * Added tests, updated props * Reverted page dock variant back to docked (for now) * Fixed querySelector string in tests * Updated prop name to isDocked * Updated prop in tests and demo * Updated snapshots * Moved docked demo to Nav docs * Docs updates from Erin * Additional feedback from Erin * Feedback from Nicole * Feedback from Coker
1 parent 29497f3 commit 9f2889d

15 files changed

Lines changed: 737 additions & 282 deletions

File tree

packages/react-core/src/components/Button/Button.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ export interface ButtonProps extends Omit<React.HTMLProps<HTMLButtonElement>, 'r
109109
hamburgerVariant?: 'expand' | 'collapse';
110110
/** @beta Flag indicating the button is a circle button. Intended for buttons that only contain an icon.. */
111111
isCircle?: boolean;
112+
/** @beta Flag indicating the button is a docked variant button. For use in docked navigation. */
113+
isDocked?: boolean;
114+
/** @beta Flag indicating the docked button should display text. Only applies when isDocked is true. */
115+
isTextExpanded?: boolean;
112116
/** @hide Forwarded ref */
113117
innerRef?: React.Ref<any>;
114118
/** Adds count number to button */
@@ -134,6 +138,8 @@ const ButtonBase: React.FunctionComponent<ButtonProps> = ({
134138
isHamburger,
135139
hamburgerVariant,
136140
isCircle,
141+
isDocked = false,
142+
isTextExpanded = false,
137143
spinnerAriaValueText,
138144
spinnerAriaLabelledBy,
139145
spinnerAriaLabel,
@@ -265,6 +271,8 @@ const ButtonBase: React.FunctionComponent<ButtonProps> = ({
265271
size === ButtonSize.sm && styles.modifiers.small,
266272
size === ButtonSize.lg && styles.modifiers.displayLg,
267273
isCircle && styles.modifiers.circle,
274+
isDocked && styles.modifiers.dock, // Replace with docked class from https://github.com/patternfly/patternfly/pull/8308
275+
isDocked && isTextExpanded && styles.modifiers.textExpanded,
268276
className
269277
)}
270278
disabled={isButtonElement ? isDisabled : null}

packages/react-core/src/components/Button/__tests__/Button.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,48 @@ describe('Favorite button', () => {
555555
});
556556
});
557557

558+
describe('Dock variant', () => {
559+
test(`Renders with class ${styles.modifiers.dock} when isDocked = true`, () => {
560+
render(<Button isDocked>Dock Button</Button>);
561+
expect(screen.getByRole('button')).toHaveClass(styles.modifiers.dock);
562+
});
563+
564+
test(`Does not render with class ${styles.modifiers.dock} when isDocked is not passed`, () => {
565+
render(<Button>Button</Button>);
566+
expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.dock);
567+
});
568+
569+
test(`Renders with class ${styles.modifiers.textExpanded} when isTextExpanded = true and isDocked = true`, () => {
570+
render(
571+
<Button isTextExpanded isDocked>
572+
Text Expanded Button
573+
</Button>
574+
);
575+
expect(screen.getByRole('button')).toHaveClass(styles.modifiers.textExpanded);
576+
});
577+
578+
test(`Does not render with class ${styles.modifiers.textExpanded} when isTextExpanded is not passed`, () => {
579+
render(<Button>Button</Button>);
580+
expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.textExpanded);
581+
});
582+
583+
test(`Does not render with class ${styles.modifiers.textExpanded} when isTextExpanded = true but isDocked is not passed`, () => {
584+
render(<Button isTextExpanded>Text Expanded Button</Button>);
585+
expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.textExpanded);
586+
});
587+
588+
test(`Renders with both ${styles.modifiers.dock} and ${styles.modifiers.textExpanded} when both props are true`, () => {
589+
render(
590+
<Button isDocked isTextExpanded>
591+
Dock Text Expanded Button
592+
</Button>
593+
);
594+
const button = screen.getByRole('button');
595+
expect(button).toHaveClass(styles.modifiers.dock);
596+
expect(button).toHaveClass(styles.modifiers.textExpanded);
597+
});
598+
});
599+
558600
test(`Renders basic button`, () => {
559601
const { asFragment } = render(<Button aria-label="basic button">Basic Button</Button>);
560602
expect(asFragment()).toMatchSnapshot();

packages/react-core/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ exports[`Renders basic button 1`] = `
55
<button
66
aria-label="basic button"
77
class="pf-v6-c-button pf-m-primary"
8-
data-ouia-component-id="OUIA-Generated-Button-primary-:r30:"
8+
data-ouia-component-id="OUIA-Generated-Button-primary-:r36:"
99
data-ouia-component-type="PF6/Button"
1010
data-ouia-safe="true"
1111
type="button"

packages/react-core/src/components/Masthead/MastheadLogo.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ export interface MastheadLogoProps extends React.DetailedHTMLProps<
1111
className?: string;
1212
/** Component type of the masthead logo. */
1313
component?: React.ElementType<any> | React.ComponentType<any>;
14+
/** @beta Flag indicating the logo is a compact variant. Used in docked layouts. */
15+
isCompact?: boolean;
1416
}
1517

1618
export const MastheadLogo: React.FunctionComponent<MastheadLogoProps> = ({
1719
children,
1820
className,
1921
component,
22+
isCompact = false,
2023
...props
2124
}: MastheadLogoProps) => {
2225
let Component = component as any;
@@ -28,7 +31,11 @@ export const MastheadLogo: React.FunctionComponent<MastheadLogoProps> = ({
2831
}
2932
}
3033
return (
31-
<Component className={css(styles.mastheadLogo, className)} {...(Component === 'a' && { tabIndex: 0 })} {...props}>
34+
<Component
35+
className={css(styles.mastheadLogo, isCompact && styles.modifiers.compact, className)}
36+
{...(Component === 'a' && { tabIndex: 0 })}
37+
{...props}
38+
>
3239
{children}
3340
</Component>
3441
);

packages/react-core/src/components/Masthead/__tests__/Masthead.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,29 @@ describe('MastheadLogo', () => {
127127

128128
expect(asFragment()).toMatchSnapshot();
129129
});
130+
131+
test(`Renders with ${styles.modifiers.compact} class when isCompact is true`, () => {
132+
render(
133+
<MastheadLogo isCompact data-testid="compact-logo">
134+
test
135+
</MastheadLogo>
136+
);
137+
expect(screen.getByTestId('compact-logo')).toHaveClass(styles.modifiers.compact);
138+
});
139+
140+
test(`Does not render with ${styles.modifiers.compact} class when isCompact is false`, () => {
141+
render(
142+
<MastheadLogo isCompact={false} data-testid="logo">
143+
test
144+
</MastheadLogo>
145+
);
146+
expect(screen.getByTestId('logo')).not.toHaveClass(styles.modifiers.compact);
147+
});
148+
149+
test(`Does not render with ${styles.modifiers.compact} class when isCompact is not passed`, () => {
150+
render(<MastheadLogo data-testid="logo">test</MastheadLogo>);
151+
expect(screen.getByTestId('logo')).not.toHaveClass(styles.modifiers.compact);
152+
});
130153
});
131154

132155
describe('MastheadContent', () => {

packages/react-core/src/components/MenuToggle/MenuToggle.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export interface MenuToggleProps
5353
isCircle?: boolean;
5454
/** Flag indicating whether the toggle is a settings toggle. This will override the icon property */
5555
isSettings?: boolean;
56+
/** @beta Flag indicating the menu toggle is a docked variant. For use in docked navigation. */
57+
isDocked?: boolean;
58+
/** @beta Flag indicating the docked toggle should display text. Only applies when isDocked is true. */
59+
isTextExpanded?: boolean;
5660
/** Elements to display before the toggle button. When included, renders the menu toggle as a split button. */
5761
splitButtonItems?: React.ReactNode[];
5862
/** Variant styles of the menu toggle */
@@ -88,6 +92,8 @@ class MenuToggleBase extends Component<MenuToggleProps> {
8892
isInForm: false,
8993
isPlaceholder: false,
9094
isCircle: false,
95+
isDocked: false,
96+
isTextExpanded: false,
9197
size: 'default',
9298
ouiaSafe: true
9399
};
@@ -106,6 +112,8 @@ class MenuToggleBase extends Component<MenuToggleProps> {
106112
isPlaceholder,
107113
isCircle,
108114
isSettings,
115+
isDocked,
116+
isTextExpanded,
109117
splitButtonItems,
110118
variant,
111119
status,
@@ -190,6 +198,8 @@ class MenuToggleBase extends Component<MenuToggleProps> {
190198
isDisabled && styles.modifiers.disabled,
191199
isPlaceholder && styles.modifiers.placeholder,
192200
isSettings && styles.modifiers.settings,
201+
isDocked && styles.modifiers.dock, // Replace with docked class from https://github.com/patternfly/patternfly/pull/8308
202+
isDocked && isTextExpanded && styles.modifiers.textExpanded,
193203
size === MenuToggleSize.sm && styles.modifiers.small,
194204
className
195205
);

packages/react-core/src/components/MenuToggle/__tests__/MenuToggle.test.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,43 @@ test('Does not render custom icon when icon prop and isSettings are passed', ()
155155
);
156156
expect(screen.queryByText('Custom icon')).not.toBeInTheDocument();
157157
});
158+
159+
test(`Renders with class ${styles.modifiers.dock} when isDocked is passed`, () => {
160+
render(<MenuToggle isDocked>Dock Toggle</MenuToggle>);
161+
expect(screen.getByRole('button')).toHaveClass(styles.modifiers.dock);
162+
});
163+
164+
test(`Does not render with class ${styles.modifiers.dock} when isDocked is not passed`, () => {
165+
render(<MenuToggle>Toggle</MenuToggle>);
166+
expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.dock);
167+
});
168+
169+
test(`Renders with class ${styles.modifiers.textExpanded} when isTextExpanded is passed and isDocked is passed`, () => {
170+
render(
171+
<MenuToggle isTextExpanded isDocked>
172+
Text Expanded Toggle
173+
</MenuToggle>
174+
);
175+
expect(screen.getByRole('button')).toHaveClass(styles.modifiers.textExpanded);
176+
});
177+
178+
test(`Does not render with class ${styles.modifiers.textExpanded} when isTextExpanded is not passed`, () => {
179+
render(<MenuToggle>Toggle</MenuToggle>);
180+
expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.textExpanded);
181+
});
182+
183+
test(`Does not render with class ${styles.modifiers.textExpanded} when isTextExpanded is passed but isDocked is not passed`, () => {
184+
render(<MenuToggle isTextExpanded>Text Expanded Toggle</MenuToggle>);
185+
expect(screen.getByRole('button')).not.toHaveClass(styles.modifiers.textExpanded);
186+
});
187+
188+
test(`Renders with both ${styles.modifiers.dock} and ${styles.modifiers.textExpanded} when both props are passed`, () => {
189+
render(
190+
<MenuToggle isDocked isTextExpanded>
191+
Dock Text Expanded Toggle
192+
</MenuToggle>
193+
);
194+
const button = screen.getByRole('button');
195+
expect(button).toHaveClass(styles.modifiers.dock);
196+
expect(button).toHaveClass(styles.modifiers.textExpanded);
197+
});

packages/react-core/src/components/Nav/Nav.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export interface NavProps
3939
'aria-label'?: string;
4040
/** The nav variant to use. Docked is in beta. */
4141
variant?: 'default' | 'horizontal' | 'horizontal-subnav' | 'docked';
42+
/** @beta Flag indicating the docked nav should display text. Only applies when variant is docked. */
43+
isTextExpanded?: boolean;
4244
/** Value to overwrite the randomly generated data-ouia-component-id.*/
4345
ouiaId?: number | string;
4446
/** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */
@@ -119,10 +121,11 @@ class Nav extends Component<NavProps, { isScrollable: boolean; flyoutRef: React.
119121
ouiaId,
120122
ouiaSafe,
121123
variant,
124+
isTextExpanded = false,
122125
...props
123126
} = this.props;
124127
const isHorizontal = ['horizontal', 'horizontal-subnav'].includes(variant);
125-
128+
const isDocked = variant === 'docked';
126129
return (
127130
<SSRSafeIds prefix="pf-" ouiaComponentType={`Nav${variant ? `-${variant}` : ''}`}>
128131
{(_, generatedOuiaId) => (
@@ -154,8 +157,9 @@ class Nav extends Component<NavProps, { isScrollable: boolean; flyoutRef: React.
154157
className={css(
155158
styles.nav,
156159
isHorizontal && styles.modifiers.horizontal,
157-
variant === 'docked' && styles.modifiers.docked,
160+
isDocked && styles.modifiers.docked,
158161
variant === 'horizontal-subnav' && styles.modifiers.subnav,
162+
isDocked && isTextExpanded && styles.modifiers.textExpanded,
159163
this.state.isScrollable && styles.modifiers.scrollable,
160164
className
161165
)}

packages/react-core/src/components/Nav/__tests__/Nav.test.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,4 +274,49 @@ describe('Nav', () => {
274274
);
275275
expect(screen.getByTestId('docked-nav')).toHaveClass(styles.modifiers.docked);
276276
});
277+
278+
test(`Renders with ${styles.modifiers.textExpanded} class when isTextExpanded is true and variant is docked`, () => {
279+
renderNav(
280+
<Nav isTextExpanded variant="docked" data-testid="text-expanded-nav">
281+
<NavList>
282+
{props.items.map((item) => (
283+
<NavItem to={item.to} key={item.to}>
284+
{item.label}
285+
</NavItem>
286+
))}
287+
</NavList>
288+
</Nav>
289+
);
290+
expect(screen.getByTestId('text-expanded-nav')).toHaveClass(styles.modifiers.textExpanded);
291+
});
292+
293+
test(`Does not render with ${styles.modifiers.textExpanded} class when isTextExpanded is not passed`, () => {
294+
renderNav(
295+
<Nav data-testid="nav">
296+
<NavList>
297+
{props.items.map((item) => (
298+
<NavItem to={item.to} key={item.to}>
299+
{item.label}
300+
</NavItem>
301+
))}
302+
</NavList>
303+
</Nav>
304+
);
305+
expect(screen.getByTestId('nav')).not.toHaveClass(styles.modifiers.textExpanded);
306+
});
307+
308+
test(`Does not render with ${styles.modifiers.textExpanded} class when isTextExpanded is true but variant is not docked`, () => {
309+
renderNav(
310+
<Nav isTextExpanded data-testid="nav">
311+
<NavList>
312+
{props.items.map((item) => (
313+
<NavItem to={item.to} key={item.to}>
314+
{item.label}
315+
</NavItem>
316+
))}
317+
</NavList>
318+
</Nav>
319+
);
320+
expect(screen.getByTestId('nav')).not.toHaveClass(styles.modifiers.textExpanded);
321+
});
277322
});

packages/react-core/src/components/Page/Page.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,16 @@ export interface PageProps extends React.HTMLProps<HTMLDivElement> {
2222
className?: string;
2323
/** @beta Indicates the layout variant */
2424
variant?: 'default' | 'docked';
25-
/** Masthead component (e.g. <Masthead />) */
25+
/** @beta Flag indicating the docked nav is expanded on mobile. Only applies when variant is docked. */
26+
isDockExpanded?: boolean;
27+
/** @beta Flag indicating the docked nav should display text on desktop. Only applies when variant is docked, and will handle
28+
* setting isTextExpanded on individual isDocked components.
29+
* */
30+
isDockTextExpanded?: boolean;
31+
/** The horizontal masthead content (e.g. <Masthead />). When using the docked variant, this content will only render at mobile viewports. */
2632
masthead?: React.ReactNode;
33+
/** @beta Content to render in the vertical dock when variant of docked is used. At mobile viewports, this content will be replaced with the content passed to masthead. */
34+
dockContent?: React.ReactNode;
2735
/** Sidebar component for a side nav, recommended to be a PageSidebar. If set to null, the page grid layout
2836
* will render without a sidebar.
2937
*/
@@ -232,7 +240,10 @@ class Page extends Component<PageProps, PageState> {
232240
className,
233241
children,
234242
variant,
243+
isDockExpanded = false,
244+
isDockTextExpanded = false,
235245
masthead,
246+
dockContent,
236247
sidebar,
237248
notificationDrawer,
238249
isNotificationDrawerExpanded,
@@ -349,9 +360,18 @@ class Page extends Component<PageProps, PageState> {
349360
>
350361
{skipToContent}
351362
{variant === 'docked' ? (
352-
<div className={css(styles.pageDock)}>
353-
<div className={css(styles.pageDockMain)}>{masthead}</div>
354-
</div>
363+
<>
364+
{masthead}
365+
<div
366+
className={css(
367+
styles.pageDock,
368+
isDockExpanded && styles.modifiers.expanded,
369+
isDockTextExpanded && styles.modifiers.textExpanded
370+
)}
371+
>
372+
<div className={css(styles.pageDockMain)}>{dockContent}</div>
373+
</div>
374+
</>
355375
) : (
356376
masthead
357377
)}

0 commit comments

Comments
 (0)