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 UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ Use [the changelog guidelines](/documentation/Versioning%20and%20changelog.md) t
### Enhancements

- Tightened up the Navigation component UI density. ([#4874](https://github.com/Shopify/polaris-react/pull/4874))
- Updated the Navigation IA ([#4902](https://github.com/Shopify/polaris-react/pull/4902))
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we could be a bit more clear here about the changes to the nav options

- Added new `duplicateRootItem` prop to a Navigation Section to support new mobile Navigation IA ([#4902](https://github.com/Shopify/polaris-react/pull/4902))
- Updated mobile behaviour of Navigation to only show one sub-section at a time ([#4902](https://github.com/Shopify/polaris-react/pull/4902))

### Bug fixes

Expand Down
83 changes: 80 additions & 3 deletions src/components/Navigation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ Use to present a navigation menu in the [frame](https://polaris.shopify.com/comp
</Navigation>
```

### Navigation with an active secondary navigation item
### Navigation with multiple secondary navigations

Use to present a secondary action, related to a section and to title the section.

Expand All @@ -212,6 +212,38 @@ Use to present a secondary action, related to a section and to title the section
label: 'Orders',
icon: OrdersMinor,
badge: '15',
subNavigationItems: [
{
url: '/admin/orders/collections',
disabled: false,
selected: false,
label: 'Collections',
},
{
url: '/admin/orders/inventory',
disabled: false,
label: 'Inventory',
},
],
},
{
url: '/path/to/place',
label: 'Marketing',
icon: MarketingMinor,
badge: '15',
subNavigationItems: [
{
url: '/admin/analytics/collections',
disabled: false,
selected: false,
label: 'Reports',
},
{
url: '/admin/analytics/inventory',
disabled: false,
label: 'Live view',
},
],
},
{
url: '/admin/products',
Expand All @@ -220,10 +252,55 @@ Use to present a secondary action, related to a section and to title the section
selected: true,
subNavigationItems: [
{
url: '/admin/products',
url: '/?path=/story/all-components-navigation--navigation-with-multiple-secondary-navigations',
disabled: false,
selected: false,
label: 'Collections',
},
{
url: '/admin/products/inventory',
disabled: false,
selected: true,
label: 'All products',
label: 'Inventory',
},
],
},
]}
/>
</Navigation>
```

### Navigation with an active root item with secondary navigation items

Use to present a secondary action, related to a section and to title the section.

```jsx
<Navigation location="/">
<Navigation.Section
duplicateRootItem
items={[
{
url: '/path/to/place',
label: 'Home',
icon: HomeMinor,
},
{
url: '/path/to/place',
label: 'Orders',
icon: OrdersMinor,
badge: '15',
},
{
url: '/admin/products',
label: 'Products',
icon: ProductsMinor,
selected: true,
subNavigationItems: [
{
url: '/admin/products/collections',
disabled: false,
selected: false,
label: 'Collections',
},
{
url: '/admin/products/inventory',
Expand Down
2 changes: 2 additions & 0 deletions src/components/Navigation/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ $nav-animation-variables: (
.Item-selected:hover &,
.subNavigationActive &,
.subNavigationActive:hover &,
.Item-child-active &,
.Item-child-active:hover &,
.Item-selected.keyFocused & {
@include recolor-icon(
$fill-color: var(--p-action-primary),
Expand Down
16 changes: 11 additions & 5 deletions src/components/Navigation/components/Item/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export interface ItemProps extends ItemURLDetails {
subNavigationItems?: SubNavigationItem[];
secondaryAction?: SecondaryAction;
onClick?(): void;
onToggleExpandedState?(): void;
expanded?: boolean;
shouldResizeIcon?: boolean;
}

Expand Down Expand Up @@ -86,20 +88,21 @@ export function Item({
matchPaths,
excludePaths,
external,
onToggleExpandedState,
expanded,
shouldResizeIcon,
}: ItemProps) {
const i18n = useI18n();
const {isNavigationCollapsed} = useMediaQuery();
const secondaryNavigationId = useUniqueId('SecondaryNavigation');
const {location, onNavigationDismiss} = useContext(NavigationContext);
const [expanded, setExpanded] = useState(false);
const [keyFocused, setKeyFocused] = useState(false);

useEffect(() => {
if (!isNavigationCollapsed && expanded) {
setExpanded(false);
onToggleExpandedState?.();
}
}, [expanded, isNavigationCollapsed]);
}, [expanded, isNavigationCollapsed, onToggleExpandedState]);

const handleKeyUp = useCallback(
(event) => {
Expand Down Expand Up @@ -249,11 +252,14 @@ export function Item({

const showExpanded = selected || expanded || childIsActive;

const canBeActive = subNavigationItems.length === 0 || !childIsActive;

const itemClassName = classNames(
styles.Item,
disabled && styles['Item-disabled'],
selected && subNavigationItems.length === 0 && styles['Item-selected'],
selected && canBeActive && styles['Item-selected'],
showExpanded && styles.subNavigationActive,
childIsActive && styles['Item-child-active'],
keyFocused && styles.keyFocused,
);

Expand Down Expand Up @@ -347,7 +353,7 @@ export function Item({
isNavigationCollapsed
) {
event.preventDefault();
setExpanded(!expanded);
onToggleExpandedState?.();
} else if (onNavigationDismiss) {
onNavigationDismiss();
if (onClick && onClick !== onNavigationDismiss) {
Expand Down
44 changes: 44 additions & 0 deletions src/components/Navigation/components/Item/tests/Item.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {PlusMinor, ExternalMinor} from '@shopify/polaris-icons';
import {matchMedia} from '@shopify/jest-dom-mocks';
import {mountWithApp} from 'tests/utilities';

import {PolarisTestProvider} from '../../../../PolarisTestProvider';
import type {MediaQueryContext} from '../../../../../utilities/media-query';
import {Badge} from '../../../../Badge';
import {Icon} from '../../../../Icon';
import {Indicator} from '../../../../Indicator';
Expand Down Expand Up @@ -649,6 +651,30 @@ describe('<Nav.Item />', () => {
expect(spy).toHaveBeenCalledTimes(1);
});
});

describe('onToggleExpandedState', () => {
it('fires the onToggleExpandedState handler when clicked', () => {
const onToggleExpandedState = jest.fn();
const item = mountWithNavigationAndPolarisTestProvider(
<Item
label="some label"
disabled={false}
url="/admin/orders"
onToggleExpandedState={onToggleExpandedState}
subNavigationItems={[{label: 'sub item', url: '/sub-item'}]}
/>,
{location: '/admin/orders'},
{isNavigationCollapsed: true},
);
item?.find('a')?.trigger('onClick', {
preventDefault: jest.fn(),
currentTarget: {
getAttribute: () => 'baz',
},
});
expect(onToggleExpandedState).toHaveBeenCalledTimes(1);
});
});
});

describe('keyFocused', () => {
Expand Down Expand Up @@ -725,3 +751,21 @@ function mountWithNavigationProvider(
</NavigationContext.Provider>,
);
}

function mountWithNavigationAndPolarisTestProvider(
node: React.ReactElement,
navigationContext: React.ContextType<typeof NavigationContext> = {
location: '',
},
mediaQueryContext: React.ContextType<typeof MediaQueryContext> = {
isNavigationCollapsed: false,
},
) {
return mountWithApp(
<NavigationContext.Provider value={navigationContext}>
<PolarisTestProvider mediaQuery={mediaQueryContext}>
{node}
</PolarisTestProvider>
</NavigationContext.Provider>,
);
}
20 changes: 16 additions & 4 deletions src/components/Navigation/components/Section/Section.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, {useEffect, useRef} from 'react';
import React, {useEffect, useRef, useState} from 'react';
import {HorizontalDotsMinor} from '@shopify/polaris-icons';

import {classNames} from '../../../../utilities/css';
import {navigationBarCollapsed} from '../../../../utilities/breakpoints';
import {useMediaQuery} from '../../../../utilities/media-query';
import {useUniqueId} from '../../../../utilities/unique-id';
import {useToggle} from '../../../../utilities/use-toggle';
import {Collapsible} from '../../../Collapsible';
Expand Down Expand Up @@ -43,6 +43,8 @@ export function Section({
setFalse: setExpandedFalse,
} = useToggle(false);
const animationFrame = useRef<number | null>(null);
const {isNavigationCollapsed} = useMediaQuery();
const [expandedIndex, setExpandedIndex] = useState<number>();

const handleClick = (
onClick: ItemProps['onClick'],
Expand All @@ -57,7 +59,7 @@ export function Section({
cancelAnimationFrame(animationFrame.current);
}

if (!hasSubNavItems || !navigationBarCollapsed().matches) {
if (!hasSubNavItems || !isNavigationCollapsed) {
animationFrame.current = requestAnimationFrame(setExpandedFalse);
}
};
Expand Down Expand Up @@ -93,18 +95,28 @@ export function Section({
</li>
);

const itemsMarkup = items.map((item) => {
const itemsMarkup = items.map((item, index) => {
const {onClick, label, subNavigationItems, ...rest} = item;
const hasSubNavItems =
subNavigationItems != null && subNavigationItems.length > 0;

const handleToggleExpandedState = () => {
if (expandedIndex === index) {
setExpandedIndex(-1);
} else {
setExpandedIndex(index);
}
};

return (
<Item
key={label}
{...rest}
label={label}
subNavigationItems={subNavigationItems}
onClick={handleClick(onClick, hasSubNavItems)}
onToggleExpandedState={handleToggleExpandedState}
expanded={expandedIndex === index}
/>
);
});
Expand Down
Loading