Skip to content

Commit ef604a8

Browse files
committed
fix: Implement TopNavigation breakpoints in CSS
This commit switches from JavaScript-based breakpoints to CSS container queries for TopNavigation, making its appearance consistent when rendered with SSR. To support container queries, we need `container-type: inline-size;` on the TopNavigation root element. This causes overflowing content to get clipped (at least in Safari), so I've enabled `expandToViewport` to render utility dropdowns in a portal. Note that OverflowMenu doesn't need this treatment because it expands the height of TopNavigation rather than overflowing. Fixes #3337
1 parent 1ec4cef commit ef604a8

File tree

11 files changed

+174
-131
lines changed

11 files changed

+174
-131
lines changed

src/button-dropdown/__tests__/button-dropdown-item-click.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ describe('default href navigation', () => {
213213
test(`allows event propagation ${props.expandToViewport ? 'with portal' : 'without portal'}`, () => {
214214
const { onClickSpy, wrapper } = renderWrappedButtonDropdown({ ...props, items });
215215
act(() => wrapper.openDropdown());
216-
act(() => wrapper.findItemById('i1')!.click());
216+
act(() => wrapper.findItemById('i1', { expandToViewport: props.expandToViewport })!.click());
217217
expect(onClickSpy).toHaveBeenCalled();
218218
});
219219

src/internal/components/menu-dropdown/index.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const ButtonTrigger = React.forwardRef(
2020
(
2121
{
2222
testUtilsClass,
23+
internalClass,
2324
iconName,
2425
iconUrl,
2526
iconAlt,
@@ -40,7 +41,7 @@ export const ButtonTrigger = React.forwardRef(
4041
<button
4142
ref={ref}
4243
type="button"
43-
className={clsx(styles.button, styles[`offset-right-${offsetRight}`], testUtilsClass, {
44+
className={clsx(styles.button, styles[`offset-right-${offsetRight}`], testUtilsClass, internalClass, {
4445
[styles.expanded]: expanded,
4546
})}
4647
aria-label={ariaLabel}
@@ -49,7 +50,7 @@ export const ButtonTrigger = React.forwardRef(
4950
disabled={disabled}
5051
onClick={event => {
5152
event.preventDefault();
52-
onClick && onClick();
53+
onClick?.();
5354
}}
5455
>
5556
{hasIcon && (
@@ -79,6 +80,7 @@ const MenuDropdown = ({
7980
badge,
8081
offsetRight,
8182
children,
83+
internalClass,
8284
...props
8385
}: MenuDropdownProps) => {
8486
const baseProps = getBaseProps(props);
@@ -94,6 +96,7 @@ const MenuDropdown = ({
9496
return (
9597
<ButtonTrigger
9698
testUtilsClass={testUtilsClass}
99+
internalClass={internalClass}
97100
ref={triggerRef}
98101
disabled={disabled}
99102
expanded={isOpen}

src/internal/components/menu-dropdown/interfaces.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { IconProps } from '../../../icon/interfaces';
55

66
export interface ButtonTriggerProps {
77
testUtilsClass?: string;
8+
internalClass?: string;
89
iconName?: IconProps.Name;
910
iconUrl?: string;
1011
iconAlt?: string;
@@ -19,12 +20,14 @@ export interface ButtonTriggerProps {
1920
expanded?: boolean;
2021
}
2122

22-
export interface MenuDropdownProps extends InternalButtonDropdownProps {
23+
export interface MenuDropdownProps extends Omit<InternalButtonDropdownProps, 'items'> {
24+
items: InternalButtonDropdownProps['items'];
2325
iconName?: IconProps.Name;
2426
iconUrl?: string;
2527
iconAlt?: string;
2628
iconSvg?: React.ReactNode;
2729
badge?: boolean;
2830
description?: string;
2931
offsetRight?: 'l' | 'xxl';
32+
internalClass?: string;
3033
}

src/internal/styles/foundation/breakpoints.scss

+37-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
// Breakpoints
6+
// Media query breakpoints
77
$breakpoint-xxx-small: 0;
88
$breakpoint-xx-small: 576px;
99
$breakpoint-x-small: 688px;
@@ -16,6 +16,18 @@ $breakpoint-xx-large: 2540px;
1616
$_smallest_breakpoint: $breakpoint-xxx-small;
1717
$_largest_breakpoint: $breakpoint-x-large;
1818

19+
// Container breakpoints, matching the Grid component
20+
$container-breakpoint-default: -1;
21+
$container-breakpoint-xxs: 465px;
22+
$container-breakpoint-xs: 688px;
23+
$container-breakpoint-s: 912px;
24+
$container-breakpoint-m: 1120px;
25+
$container-breakpoint-l: 1320px;
26+
$container-breakpoint-xl: 1840px;
27+
28+
$_container_smallest_breakpoint: $container-breakpoint-default;
29+
$_container_largest_breakpoint: $container-breakpoint-xl;
30+
1931
// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.
2032
// Makes the @content apply to the wider than given breakpoint.
2133
@mixin media-breakpoint-up($breakpoint) {
@@ -39,3 +51,27 @@ $_largest_breakpoint: $breakpoint-x-large;
3951
@content;
4052
}
4153
}
54+
55+
// Container query for widths greater than the given breakpoint.
56+
// Matches the behavior of getMatchingBreakpoint in breakpoints.ts
57+
@mixin container-breakpoint-up($breakpoint) {
58+
@if $breakpoint != $_container_smallest_breakpoint {
59+
@container (min-width: #{$breakpoint + 1px}) {
60+
@content;
61+
}
62+
} @else {
63+
@content;
64+
}
65+
}
66+
67+
// Container query for widths less than or equal to the given breakpoint.
68+
// Matches the behavior of matchBreakpointMapping in breakpoints.ts
69+
@mixin container-breakpoint-down($breakpoint) {
70+
@if $breakpoint != $_container_largest_breakpoint {
71+
@container (max-width: #{$breakpoint}) {
72+
@content;
73+
}
74+
} @else {
75+
@content;
76+
}
77+
}

src/test-utils/dom/button-dropdown/index.ts

+42-12
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,30 @@ export default class ButtonDropdownWrapper extends ComponentWrapper {
4040
* Finds an item in the open dropdown by item id. Returns null if there is no open dropdown.
4141
*
4242
* This utility does not open the dropdown. To find dropdown items, call `openDropdown()` first.
43+
*
44+
* @param options
45+
* * expandToViewport (boolean) - Use this when the component under test is rendered with an `expandToViewport` flag.
4346
*/
44-
findItemById(id: string): ElementWrapper | null {
47+
findItemById(id: string, options = { expandToViewport: false }): ElementWrapper | null {
4548
const itemSelector = `.${itemStyles['item-element']}[data-testid="${id}"]`;
46-
return this.findOpenDropdown()?.find(itemSelector) || this.find(itemSelector);
49+
return options.expandToViewport
50+
? createWrapper().find(itemSelector)
51+
: this.findOpenDropdown()?.find(itemSelector) || this.find(itemSelector);
4752
}
4853

4954
/**
5055
* Finds `checked` value of item in the open dropdown by item id. Returns null if there is no open dropdown or the item is not a checkbox item.
5156
*
5257
* This utility does not open the dropdown. To find dropdown items, call `openDropdown()` first.
58+
*
59+
* @param options
60+
* * expandToViewport (boolean) - Use this when the component under test is rendered with an `expandToViewport` flag.
5361
*/
54-
@usesDom findItemCheckedById(id: string): string | null {
62+
@usesDom findItemCheckedById(id: string, options = { expandToViewport: false }): string | null {
5563
const itemSelector = `.${itemStyles['item-element']}[data-testid="${id}"]`;
56-
const item = this.findOpenDropdown()?.find(itemSelector) || this.find(itemSelector);
64+
const item = options.expandToViewport
65+
? createWrapper().find(itemSelector)
66+
: this.findOpenDropdown()?.find(itemSelector) || this.find(itemSelector);
5767
if (!item) {
5868
return null;
5969
}
@@ -68,36 +78,56 @@ export default class ButtonDropdownWrapper extends ComponentWrapper {
6878
* Finds an expandable category in the open dropdown by category id. Returns null if there is no open dropdown.
6979
*
7080
* This utility does not open the dropdown. To find dropdown items, call `openDropdown()` first.
81+
*
82+
* @param options
83+
* * expandToViewport (boolean) - Use this when the component under test is rendered with an `expandToViewport` flag.
7184
*/
72-
findExpandableCategoryById(id: string): ElementWrapper | null {
85+
findExpandableCategoryById(id: string, options = { expandToViewport: false }): ElementWrapper | null {
7386
const expandableCategorySelector = `.${categoryStyles.expandable}[data-testid="${id}"]`;
74-
return this.findOpenDropdown()?.find(expandableCategorySelector) || this.find(expandableCategorySelector);
87+
return options.expandToViewport
88+
? createWrapper().find(expandableCategorySelector)
89+
: this.findOpenDropdown()?.find(expandableCategorySelector) || this.find(expandableCategorySelector);
7590
}
7691

7792
/**
7893
* Finds the highlighted item in the open dropdown. Returns null if there is no open dropdown.
7994
*
8095
* This utility does not open the dropdown. To find dropdown items, call `openDropdown()` first.
96+
*
97+
* @param options
98+
* * expandToViewport (boolean) - Use this when the component under test is rendered with an `expandToViewport` flag.
8199
*/
82-
findHighlightedItem(): ElementWrapper | null {
100+
findHighlightedItem(options = { expandToViewport: false }): ElementWrapper | null {
83101
const highlightedItemSelector = `.${itemStyles['item-element']}.${itemStyles.highlighted}`;
84-
return this.findOpenDropdown()?.find(highlightedItemSelector) || this.find(highlightedItemSelector);
102+
return options.expandToViewport
103+
? createWrapper().find(highlightedItemSelector)
104+
: this.findOpenDropdown()?.find(highlightedItemSelector) || this.find(highlightedItemSelector);
85105
}
86106

87107
/**
88108
* Finds all the items in the open dropdown. Returns empty array if there is no open dropdown.
89109
*
90110
* This utility does not open the dropdown. To find dropdown items, call `openDropdown()` first.
111+
*
112+
* @param options
113+
* * expandToViewport (boolean) - Use this when the component under test is rendered with an `expandToViewport` flag.
91114
*/
92-
findItems(): Array<ElementWrapper> {
93-
return this.findOpenDropdown()?.findAll(`.${itemStyles['item-element']}`) || [];
115+
findItems(options = { expandToViewport: false }): Array<ElementWrapper> {
116+
return options.expandToViewport
117+
? createWrapper().findAll(`.${itemStyles['item-element']}`)
118+
: this.findOpenDropdown()?.findAll(`.${itemStyles['item-element']}`) || [];
94119
}
95120

96121
/**
97122
* Finds the disabled reason tooltip for a dropdown item. Returns null if no disabled item with `disabledReason` is highlighted.
123+
*
124+
* @param options
125+
* * expandToViewport (boolean) - Use this when the component under test is rendered with an `expandToViewport` flag.
98126
*/
99-
findDisabledReason(): ElementWrapper | null {
100-
return createWrapper().find(`[data-testid="button-dropdown-disabled-reason"]`);
127+
findDisabledReason(options = { expandToViewport: false }): ElementWrapper | null {
128+
return options.expandToViewport
129+
? createWrapper().find(`[data-testid="button-dropdown-disabled-reason"]`)
130+
: createWrapper().find(`[data-testid="button-dropdown-disabled-reason"]`);
101131
}
102132

103133
@usesDom

src/test-utils/dom/top-navigation/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,10 @@ export class TopNavigationMenuDropdownWrapper extends ButtonDropdownWrapper {
101101
}
102102

103103
findTitle(): ElementWrapper | null {
104-
return this.findByClassName(buttonDropdownStyles.title);
104+
return createWrapper().findComponent(`.${buttonDropdownStyles.title}`, ElementWrapper);
105105
}
106106

107107
findDescription(): ElementWrapper | null {
108-
return this.findByClassName(buttonDropdownStyles.description);
108+
return createWrapper().findComponent(`.${buttonDropdownStyles.description}`, ElementWrapper);
109109
}
110110
}

src/top-navigation/__tests__/top-navigation-utility.test.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,9 @@ describe('TopNavigation Utility part', () => {
196196
}).findMenuDropdownType()!;
197197
menuWrapper.openDropdown();
198198

199-
items.forEach((item, i) => expect(menuWrapper.findItems()[i].getElement()).toHaveTextContent(item.text));
199+
items.forEach((item, i) =>
200+
expect(menuWrapper.findItems({ expandToViewport: true })[i].getElement()).toHaveTextContent(item.text)
201+
);
200202
});
201203

202204
it('does not show title in the dropdown if there is visible text', () => {

0 commit comments

Comments
 (0)