diff --git a/docs/migration.md b/docs/migration.md index fe11541f613..9a12b6f7388 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -76,6 +76,8 @@ consider additional positioning prop support on a case-by-case basis. - `CollapsibleSubNavItem` -> `SubNav.CollapsibleItem` - `SubNavItem` -> `SubNav.Item` - `SubNavItemText` -> `SubNav.ItemText` +- Added `Nav.List` as a semantic wrapper for `Nav.Item`. See the + [README](../packages/chrome/README.md#usages) for details. #### @zendeskgarden/react-colorpickers diff --git a/packages/avatars/demo/~patterns/stories/ChromeStory.tsx b/packages/avatars/demo/~patterns/stories/ChromeStory.tsx index 0639dc4bc8f..226bbe39fb8 100644 --- a/packages/avatars/demo/~patterns/stories/ChromeStory.tsx +++ b/packages/avatars/demo/~patterns/stories/ChromeStory.tsx @@ -8,23 +8,23 @@ import React from 'react'; import { Story } from '@storybook/react'; import Icon from '@zendeskgarden/svg-icons/src/16/grid-2x2-stroke.svg'; -import { Chrome, Body, Header, HeaderItem, HeaderItemIcon } from '@zendeskgarden/react-chrome'; +import { Chrome, Body, Header } from '@zendeskgarden/react-chrome'; import { Avatar, IAvatarProps } from '@zendeskgarden/react-avatars'; export const ChromeStory: Story = args => (
- - + + - - - + + + Example User - +
diff --git a/packages/chrome/README.md b/packages/chrome/README.md index bb297d81503..cf2b2c58288 100644 --- a/packages/chrome/README.md +++ b/packages/chrome/README.md @@ -17,6 +17,7 @@ npm install react react-dom styled-components @zendeskgarden/react-theming import { ThemeProvider } from '@zendeskgarden/react-theming'; import { Chrome, Nav, SubNav, Body, Header, Content, Main } from '@zendeskgarden/react-chrome'; import ConnectIcon from '@zendeskgarden/icons/src/26/relationshape-connect.svg'; +import BrandmarkIcon from '@zendeskgarden/svg-icons/src/26/zendesk.svg'; @@ -27,11 +28,19 @@ import ConnectIcon from '@zendeskgarden/icons/src/26/relationshape-connect.svg'; Zendesk Connect - + + + + + + Home + + + - + - Home + Brandmark diff --git a/packages/chrome/demo/stories/ChromeStory.tsx b/packages/chrome/demo/stories/ChromeStory.tsx index 119bec8fca2..e43b917003e 100644 --- a/packages/chrome/demo/stories/ChromeStory.tsx +++ b/packages/chrome/demo/stories/ChromeStory.tsx @@ -139,20 +139,22 @@ export const ChromeStory: Story = ({ Nav Logo )} - {navItems.map((item, index) => ( - { - setCurrentNav(index); - setCurrentSubNav(0); - onNavClick({ hasSubNav: item.hasSubNav }); - }} - > - {NAV_ICONS[index] || } - {item.text} - - ))} + + {navItems.map((item, index) => ( + { + setCurrentNav(index); + setCurrentSubNav(0); + onNavClick({ hasSubNav: item.hasSubNav }); + }} + > + {NAV_ICONS[index] || } + {item.text} + + ))} + {hasBrandmark && ( diff --git a/packages/chrome/src/elements/nav/Nav.tsx b/packages/chrome/src/elements/nav/Nav.tsx index b3e5bc1e054..5759ee993bd 100644 --- a/packages/chrome/src/elements/nav/Nav.tsx +++ b/packages/chrome/src/elements/nav/Nav.tsx @@ -14,6 +14,7 @@ import { StyledNav } from '../../styled'; import { NavItem } from './NavItem'; import { NavItemIcon } from './NavItemIcon'; import { NavItemText } from './NavItemText'; +import { NavList } from './NavList'; export const NavComponent = React.forwardRef((props, ref) => { const { hue, isLight, isDark } = useChromeContext(); @@ -36,11 +37,13 @@ NavComponent.propTypes = { * @extends HTMLAttributes */ export const Nav = NavComponent as typeof NavComponent & { + List: typeof NavList; Item: typeof NavItem; ItemIcon: typeof NavItemIcon; ItemText: typeof NavItemText; }; +Nav.List = NavList; Nav.Item = NavItem; Nav.ItemIcon = NavItemIcon; Nav.ItemText = NavItemText; diff --git a/packages/chrome/src/elements/nav/NavItem.spec.tsx b/packages/chrome/src/elements/nav/NavItem.spec.tsx index e6da9e87190..947cf64183e 100644 --- a/packages/chrome/src/elements/nav/NavItem.spec.tsx +++ b/packages/chrome/src/elements/nav/NavItem.spec.tsx @@ -9,26 +9,31 @@ import React from 'react'; import { render } from 'garden-test-utils'; import { PALETTE, getColorV8, DEFAULT_THEME } from '@zendeskgarden/react-theming'; import { Chrome } from '../Chrome'; -import { NavItem } from './NavItem'; import { Nav } from './Nav'; import { PRODUCTS, Product } from '../../types'; describe('NavItem', () => { it('passes ref to underlying DOM element', () => { const ref = React.createRef(); - const { container } = render(); + const { getByTestId } = render( + + + + ); - expect(container.firstChild).toBe(ref.current); + expect(getByTestId('item')).toBe(ref.current); }); it('renders expanded styling', () => { - const { container } = render( + const { getByTestId } = render( ); - expect(container.firstChild!.firstChild).toHaveStyle(` + expect(getByTestId('item')).toHaveStyle(` justify-content: start; text-align: inherit; `); @@ -36,7 +41,11 @@ describe('NavItem', () => { describe('Current', () => { it('renders state attribute when current', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + + + ); const currentNavItem = getByTestId('current-nav-item'); @@ -44,7 +53,11 @@ describe('NavItem', () => { }); it('does not render state attribute when not current', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + + + ); const navItem = getByTestId('nav-item'); @@ -54,110 +67,110 @@ describe('NavItem', () => { describe('Order', () => { it('renders correct order if used as brandmark', () => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toHaveStyleRule('order', '1'); }); it('renders correct order if used as logo', () => { - const { container } = render(); + const { container } = render(); - expect(container.firstChild).toHaveStyleRule('order', '0'); + expect(container.firstChild).toHaveStyleRule('order', '-1'); }); }); describe('Opacity', () => { it('renders correct opacity if used as brandmark', () => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toHaveStyleRule('opacity', '0.3'); }); it('renders correct opacity if used as logo', () => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toHaveStyleRule('opacity', '1'); }); it('renders correct opacity if current', () => { - const { container } = render(); + const { getByTestId } = render( + + + + ); - expect(container.firstChild).toHaveStyleRule('opacity', '1'); + expect(getByTestId('item')).toHaveStyleRule('opacity', '1'); }); }); describe('Hover Color', () => { it('renders correct color with dark hue', () => { - const { container } = render( + const { getByTestId } = render( - + + + ); - expect(container.firstChild!.firstChild).toHaveStyleRule( - 'background-color', - 'rgba(0,0,0,0.1)', - { - modifier: '&:hover' - } - ); + expect(getByTestId('item')).toHaveStyleRule('background-color', 'rgba(0,0,0,0.1)', { + modifier: '&:hover' + }); }); it('renders correct color with light hue', () => { - const { container } = render( + const { getByTestId } = render( - + + + ); - expect(container.firstChild!.firstChild).toHaveStyleRule( - 'background-color', - 'rgba(255,255,255,0.1)', - { - modifier: '&:hover' - } - ); + expect(getByTestId('item')).toHaveStyleRule('background-color', 'rgba(255,255,255,0.1)', { + modifier: '&:hover' + }); }); }); describe('Current Color', () => { it('renders correct color by default', () => { - const { container } = render( + const { getByTestId } = render( - + + + ); - expect(container.firstChild!.firstChild).toHaveStyleRule( + expect(getByTestId('item')).toHaveStyleRule( 'background-color', getColorV8('chromeHue', 500, DEFAULT_THEME) ); }); it('renders correct color with dark hue', () => { - const { container } = render( + const { getByTestId } = render( - + + + ); - expect(container.firstChild!.firstChild).toHaveStyleRule( - 'background-color', - 'rgba(255,255,255,0.4)' - ); + expect(getByTestId('item')).toHaveStyleRule('background-color', 'rgba(255,255,255,0.4)'); }); it('renders correct color with light hue', () => { - const { container } = render( + const { getByTestId } = render( - + + + ); - expect(container.firstChild!.firstChild).toHaveStyleRule( - 'background-color', - 'rgba(0,0,0,0.4)' - ); + expect(getByTestId('item')).toHaveStyleRule('background-color', 'rgba(0,0,0,0.4)'); }); }); @@ -174,14 +187,14 @@ describe('NavItem', () => { it('renders correct product color if provided', () => { PRODUCTS.forEach(product => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toHaveStyleRule('color', VALID_COLOR_MAP[product]); }); }); it('renders correct color if no product is provided', () => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toHaveStyleRule('color', 'inherit'); }); diff --git a/packages/chrome/src/elements/nav/NavItem.tsx b/packages/chrome/src/elements/nav/NavItem.tsx index 595ea649f43..51914ed46bf 100644 --- a/packages/chrome/src/elements/nav/NavItem.tsx +++ b/packages/chrome/src/elements/nav/NavItem.tsx @@ -8,9 +8,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import { INavItemProps, PRODUCTS } from '../../types'; -import { StyledNavItem, StyledLogoNavItem, StyledBrandmarkNavItem } from '../../styled'; +import { + StyledNavButton, + StyledLogoNavItem, + StyledBrandmarkNavItem, + StyledNavListItem +} from '../../styled'; import { useNavContext } from '../../utils/useNavContext'; import { useChromeContext } from '../../utils/useChromeContext'; +import { useNavListContext } from '../../utils/useNavListContext'; /** * @deprecated use `Nav.Item` instead @@ -21,10 +27,14 @@ export const NavItem = React.forwardRef( ({ hasLogo, hasBrandmark, product, ...other }, ref) => { const { hue, isLight, isDark } = useChromeContext(); const { isExpanded } = useNavContext(); + const navListContext = useNavListContext(); const ariaCurrent = other.isCurrent || undefined; + const hasList = navListContext?.hasList; + let retVal; + if (hasLogo) { - return ( + retVal = ( ( {...other} /> ); + } else if (hasBrandmark) { + retVal = ; + } else { + retVal = ( + + ); } - if (hasBrandmark) { - return ; + if (hasList) { + retVal = {retVal}; } - return ( - - ); + return retVal; } ); diff --git a/packages/chrome/src/elements/nav/NavList.tsx b/packages/chrome/src/elements/nav/NavList.tsx new file mode 100644 index 00000000000..1614f5e4106 --- /dev/null +++ b/packages/chrome/src/elements/nav/NavList.tsx @@ -0,0 +1,27 @@ +/** + * Copyright Zendesk, Inc. + * + * Use of this source code is governed under the Apache License, Version 2.0 + * found at http://www.apache.org/licenses/LICENSE-2.0. + */ + +import React, { HTMLAttributes, useMemo } from 'react'; +import { StyledNavList } from '../../styled'; +import { NavListContext } from '../../utils/useNavListContext'; + +/** + * @extends HTMLAttributes + */ +export const NavList = React.forwardRef>( + (props, ref) => { + const contextValue = useMemo(() => ({ hasList: true }), []); + + return ( + + + + ); + } +); + +NavList.displayName = 'Nav'; diff --git a/packages/chrome/src/styled/index.ts b/packages/chrome/src/styled/index.ts index 3047b7ff2cf..cfbbc0626d3 100644 --- a/packages/chrome/src/styled/index.ts +++ b/packages/chrome/src/styled/index.ts @@ -21,10 +21,12 @@ export { StyledLogoHeaderItem } from './header/StyledLogoHeaderItem'; export { StyledHeaderItemText } from './header/StyledHeaderItemText'; export { StyledHeaderItemWrapper } from './header/StyledHeaderItemWrapper'; export { StyledNav } from './nav/StyledNav'; +export { StyledNavList } from './nav/StyledNavList'; +export { StyledNavListItem } from './nav/StyledNavListItem'; export { StyledBaseNavItem } from './nav/StyledBaseNavItem'; export { StyledLogoNavItem } from './nav/StyledLogoNavItem'; export { StyledBrandmarkNavItem } from './nav/StyledBrandmarkNavItem'; -export { StyledNavItem } from './nav/StyledNavItem'; +export { StyledNavButton } from './nav/StyledNavButton'; export { StyledNavItemIcon } from './nav/StyledNavItemIcon'; export { StyledNavItemText } from './nav/StyledNavItemText'; export { StyledSubNav } from './subnav/StyledSubNav'; diff --git a/packages/chrome/src/styled/nav/StyledBaseNavItem.ts b/packages/chrome/src/styled/nav/StyledBaseNavItem.ts index 245ea11ed99..a713e82b5ec 100644 --- a/packages/chrome/src/styled/nav/StyledBaseNavItem.ts +++ b/packages/chrome/src/styled/nav/StyledBaseNavItem.ts @@ -26,11 +26,7 @@ const sizeStyles = (props: ThemeProps) => { `; }; -/** - * 1. Button reset. - * 2. Anchor reset. - */ -export const StyledBaseNavItem = styled.button.attrs({ +export const StyledBaseNavItem = styled.div.attrs({ 'data-garden-id': COMPONENT_ID, 'data-garden-version': PACKAGE_VERSION })` @@ -44,12 +40,6 @@ export const StyledBaseNavItem = styled.button.attrs({ box-shadow 0.1s ease-in-out, background-color 0.1s ease-in-out, opacity 0.1s ease-in-out; - border: none; /* [1] */ - box-sizing: border-box; - background: transparent; /* [1] */ - text-decoration: none; /* [2] */ - color: inherit; /* [2] */ - font-size: inherit; /* [1] */ ${props => sizeStyles(props)} `; diff --git a/packages/chrome/src/styled/nav/StyledBrandmarkNavItem.ts b/packages/chrome/src/styled/nav/StyledBrandmarkNavItem.ts index 88e4764ae92..b650b251b00 100644 --- a/packages/chrome/src/styled/nav/StyledBrandmarkNavItem.ts +++ b/packages/chrome/src/styled/nav/StyledBrandmarkNavItem.ts @@ -9,16 +9,19 @@ import styled from 'styled-components'; import { DEFAULT_THEME } from '@zendeskgarden/react-theming'; import { StyledBaseNavItem } from './StyledBaseNavItem'; -const COMPONENT_ID = 'chrome.brandmark_nav_item'; +const COMPONENT_ID = 'chrome.brandmark_nav_list_item'; -export const StyledBrandmarkNavItem = styled(StyledBaseNavItem).attrs({ +/** + * 1. Overrides flex default `min-height: auto` + */ +export const StyledBrandmarkNavItem = styled(StyledBaseNavItem as 'button').attrs({ 'data-garden-id': COMPONENT_ID, - 'data-garden-version': PACKAGE_VERSION, - as: 'div' + 'data-garden-version': PACKAGE_VERSION })` order: 1; opacity: 0.3; margin-top: auto; + min-height: 0; /* [1] */ `; StyledBrandmarkNavItem.defaultProps = { diff --git a/packages/chrome/src/styled/nav/StyledLogoNavItem.ts b/packages/chrome/src/styled/nav/StyledLogoNavItem.ts index 359153c0d27..a249ab7dfaa 100644 --- a/packages/chrome/src/styled/nav/StyledLogoNavItem.ts +++ b/packages/chrome/src/styled/nav/StyledLogoNavItem.ts @@ -10,7 +10,7 @@ import { PALETTE, DEFAULT_THEME } from '@zendeskgarden/react-theming'; import { StyledBaseNavItem } from './StyledBaseNavItem'; import { Product } from '../../types'; -const COMPONENT_ID = 'chrome.logo_nav_item'; +const COMPONENT_ID = 'chrome.logo_nav_list_item'; const retrieveProductColor = (product?: Product) => { switch (product) { @@ -49,14 +49,17 @@ export interface IStyledLogoNavItemProps extends ThemeProps { isLight?: boolean; } -export const StyledLogoNavItem = styled(StyledBaseNavItem).attrs({ +/** + * 1. Overrides flex default `min-height: auto` + */ +export const StyledLogoNavItem = styled(StyledBaseNavItem as 'button').attrs({ 'data-garden-id': COMPONENT_ID, - 'data-garden-version': PACKAGE_VERSION, - as: 'div' + 'data-garden-version': PACKAGE_VERSION })` - order: 0; + order: -1; opacity: 1; cursor: default; + min-height: 0; /* [1] */ ${props => colorStyles(props)}; `; diff --git a/packages/chrome/src/styled/nav/StyledNavItem.ts b/packages/chrome/src/styled/nav/StyledNavButton.ts similarity index 85% rename from packages/chrome/src/styled/nav/StyledNavItem.ts rename to packages/chrome/src/styled/nav/StyledNavButton.ts index 769f59b2347..0faee2170eb 100644 --- a/packages/chrome/src/styled/nav/StyledNavItem.ts +++ b/packages/chrome/src/styled/nav/StyledNavButton.ts @@ -18,7 +18,7 @@ import { StyledBaseNavItem } from './StyledBaseNavItem'; import { StyledNavItemIcon } from './StyledNavItemIcon'; import { getNavWidth } from './StyledNav'; -const COMPONENT_ID = 'chrome.nav_item'; +const COMPONENT_ID = 'chrome.nav_button'; /** * 1. Use outline for focus styling to work with transparent backgrounds @@ -81,17 +81,26 @@ interface IStyledNavItemProps extends ThemeProps { * 2. Button reset * 3. Override `focusStyles` outline (in `colorStyles`) * 4. Use of negative offset to create an inset outline + * 5. Overrides flex default `min-width: auto` + * https://ishadeed.com/article/min-max-css/#setting-min-width-to-zero-with-flexbox */ -export const StyledNavItem = styled(StyledBaseNavItem as 'button').attrs({ +export const StyledNavButton = styled(StyledBaseNavItem as 'button').attrs({ 'data-garden-id': COMPONENT_ID, 'data-garden-version': PACKAGE_VERSION, as: 'button' })` + flex: 1; justify-content: ${props => props.isExpanded && 'start'}; - order: 1; margin: 0; /* [2] */ + border: none; /* [2] */ + box-sizing: border-box; + background: transparent; /* [2] */ cursor: ${props => (props.isCurrent ? 'default' : 'pointer')}; + min-width: 0; /* [5] */ text-align: ${props => props.isExpanded && 'inherit'}; + text-decoration: none; /* [1] */ + color: inherit; /* [1] */ + font-size: inherit; /* [2] */ &:hover, &:focus { @@ -119,6 +128,6 @@ export const StyledNavItem = styled(StyledBaseNavItem as 'button').attrs({ ${props => retrieveComponentStyles(COMPONENT_ID, props)}; `; -StyledNavItem.defaultProps = { +StyledNavButton.defaultProps = { theme: DEFAULT_THEME }; diff --git a/packages/chrome/src/styled/nav/StyledNavItemText.ts b/packages/chrome/src/styled/nav/StyledNavItemText.ts index bfdb059ac9b..428853ee5e7 100644 --- a/packages/chrome/src/styled/nav/StyledNavItemText.ts +++ b/packages/chrome/src/styled/nav/StyledNavItemText.ts @@ -12,7 +12,7 @@ import { DEFAULT_THEME, getLineHeight } from '@zendeskgarden/react-theming'; -import { StyledNavItem } from './StyledNavItem'; +import { StyledNavButton } from './StyledNavButton'; import { getNavWidth } from './StyledNav'; const COMPONENT_ID = 'chrome.nav_item_text'; @@ -40,7 +40,7 @@ export const StyledNavItemText = styled.span.attrs({ ${props => props.isExpanded && ` - ${StyledNavItem} > && { + ${StyledNavButton} > && { position: static; flex: 1; clip: auto; diff --git a/packages/chrome/src/styled/nav/StyledNavList.ts b/packages/chrome/src/styled/nav/StyledNavList.ts new file mode 100644 index 00000000000..ae9c3ff9396 --- /dev/null +++ b/packages/chrome/src/styled/nav/StyledNavList.ts @@ -0,0 +1,30 @@ +/** + * Copyright Zendesk, Inc. + * + * Use of this source code is governed under the Apache License, Version 2.0 + * found at http://www.apache.org/licenses/LICENSE-2.0. + */ + +import styled from 'styled-components'; +import { retrieveComponentStyles, DEFAULT_THEME } from '@zendeskgarden/react-theming'; + +const COMPONENT_ID = 'chrome.nav_list'; + +export const StyledNavList = styled.ul.attrs({ + 'data-garden-id': COMPONENT_ID, + 'data-garden-version': PACKAGE_VERSION +})` + display: flex; + flex: 1; + flex-direction: column; + order: 0; + margin: 0; + padding: 0; + list-style: none; + + ${props => retrieveComponentStyles(COMPONENT_ID, props)}; +`; + +StyledNavList.defaultProps = { + theme: DEFAULT_THEME +}; diff --git a/packages/chrome/src/styled/nav/StyledNavListItem.ts b/packages/chrome/src/styled/nav/StyledNavListItem.ts new file mode 100644 index 00000000000..26d38affba2 --- /dev/null +++ b/packages/chrome/src/styled/nav/StyledNavListItem.ts @@ -0,0 +1,28 @@ +/** + * Copyright Zendesk, Inc. + * + * Use of this source code is governed under the Apache License, Version 2.0 + * found at http://www.apache.org/licenses/LICENSE-2.0. + */ + +import styled from 'styled-components'; +import { retrieveComponentStyles, DEFAULT_THEME } from '@zendeskgarden/react-theming'; + +const COMPONENT_ID = 'chrome.nav_list_item'; + +export const StyledNavListItem = styled.li.attrs({ + 'data-garden-id': COMPONENT_ID, + 'data-garden-version': PACKAGE_VERSION +})` + display: flex; + order: 1; + margin: 0; + padding: 0; + list-style-type: none; + + ${props => retrieveComponentStyles(COMPONENT_ID, props)}; +`; + +StyledNavListItem.defaultProps = { + theme: DEFAULT_THEME +}; diff --git a/packages/chrome/src/utils/useNavListContext.ts b/packages/chrome/src/utils/useNavListContext.ts new file mode 100644 index 00000000000..5a96f3784e4 --- /dev/null +++ b/packages/chrome/src/utils/useNavListContext.ts @@ -0,0 +1,18 @@ +/** + * Copyright Zendesk, Inc. + * + * Use of this source code is governed under the Apache License, Version 2.0 + * found at http://www.apache.org/licenses/LICENSE-2.0. + */ + +import React, { useContext } from 'react'; + +interface INavListContext { + hasList: boolean; +} + +export const NavListContext = React.createContext(undefined); + +export const useNavListContext = () => { + return useContext(NavListContext); +};