diff --git a/docs/content/Dialog.md b/docs/content/Dialog.md index 7884eb65d36..23d083efb04 100644 --- a/docs/content/Dialog.md +++ b/docs/content/Dialog.md @@ -8,13 +8,12 @@ The dialog component is used for all modals. It renders on top of the rest of th **Note:** You'll need to manage the `isOpen` state in a wrapper component of your own. For documentation purposes only we've created a mock `State` to handle this, but you should handle the state in the component you consume `Dialog` in or in a wrapper component. -```jsx live + {([isOpen, setIsOpen]) => ( <> - setIsOpen(false)}> - Title + setIsOpen(false)}> Some content @@ -22,19 +21,13 @@ The dialog component is used for all modals. It renders on top of the rest of th )} -``` - -You can also pass any non-text content into the header: -```jsx live +```jsx {([isOpen, setIsOpen]) => ( <> - setIsOpen(false)}> - - - + setIsOpen(false)}> Some content @@ -44,15 +37,23 @@ You can also pass any non-text content into the header: ``` + +You can also pass any React node as the title to override the styling: + +```jsx +Title}> +``` + ## System props -`Dialog` components get the `COMMON` and `LAYOUT` categories of system props. `Dialog.Header` components get `COMMON`, `LAYOUT`, and `FLEX` system props. Read our [System Props](/system-props) doc page for a full list of available props. +Dialog components get the `COMMON` and `LAYOUT` categories of system props. Read our [System Props](/system-props) doc page for a full list of available props. ## Component props +These props are **all required**. + | Prop name | Type | Description | | :- | :- | :- | -| isOpen | Boolean | Set whether or not the dialog is shown | -| onDismiss | Function | A user-provided function that should close the dialog | - -`Dialog.Header` does not take any non-system props. +| title | String or Node | The title shown in the header | +| isOpen | Boolean | Handles opening and closing the dialog | +| onDismiss | Function | Should close the dialog | diff --git a/docs/content/Heading.md b/docs/content/Heading.md index 3e71f224abf..824a33be9f4 100644 --- a/docs/content/Heading.md +++ b/docs/content/Heading.md @@ -2,13 +2,11 @@ title: Heading --- -The Heading component will render an html `h2` tag without any default styling. Other heading level elements `h1-h6` are available through use of the `as` prop (see [System Props](/system-props) for additional explanation). Please reference the [w3 recommendations for headings](https://www.w3.org/WAI/tutorials/page-structure/headings/) to ensure your headings provide an accessible experience for screen reader users. - -**Attention:** Make sure to include a valid heading element to render a Heading component other than `h2` (`H1 Element`). +The Heading component will render an html `h1-6` tag without any default styling. Please reference the [w3 recommendations for headings](https://www.w3.org/WAI/tutorials/page-structure/headings/) to ensure your headings provide an accessible experience for screen reader users. ## Default example ```jsx live -H2 heading with fontSize={1} +With fontSize={1} ``` ## System props @@ -17,6 +15,4 @@ Heading components get `TYPOGRAPHY` and `COMMON` system props. Read our [System ## Component props -| Prop name | Type | Description | -| :-------- | :------ | :----------------------------------------------- | -| as | String | sets the HTML tag for the component | +Heading does not get any additional props other than the system props mentioned above. diff --git a/docs/content/Pagination.md b/docs/content/Pagination.md new file mode 100644 index 00000000000..2dbe002064d --- /dev/null +++ b/docs/content/Pagination.md @@ -0,0 +1,194 @@ +--- +title: Pagination +--- +import State from '../components/State' + +Use the pagination component to create a connected set of links that lead to related pages (for example, previous, next, or page numbers). + +## Basic example + +The pagination component only requires two properties to render: `pageCount`, which is the total number of pages, and `currentPage`, which is the currently selected page number (which should be managed by the consuming application). + +```jsx live + e.preventDefault()} +/> +``` + +However, to handle state changes when the user clicks a page, you also need to pass `onPageChange`, which is a function that takes a click event and page number as an argument: + +```javascript +type PageChangeCallback = (evt: React.MouseEvent, page: number) => void +``` + +By default, clicking a link in the pagination component will cause the browser to navigate to the URL specified by the page. To cancel navigation and handle state management on your own, you should call `preventDefault` on the event, as in this example: + +```jsx live + + {([page, setPage]) => { + const totalPages = 15 + const onPageChange = (evt, page) => { + evt.preventDefault() + setPage(page) + } + + return ( + + Current page: {page} / {totalPages} + + + ) + }} + +``` + +## Customizing link URLs + +To customize the URL generated for each link, you can pass a function to the `hrefBuilder` property. The function should take a page number as an argument and return a URL to use for the link. + +```javascript +type HrefBuilder = (page: number) => string +``` + +```jsx live + + {([lastUrl, setLastUrl]) => { + const onPageChange = (evt, page) => { + evt.preventDefault() + setLastUrl(evt.target.href) + } + const hrefBuilder = (page) => { + return `https://example.com/pages/${page}` + } + + return ( + + The last URL clicked was: {lastUrl} + + + ) + }} + +``` + +## Customizing which pages are shown + +Two props control how many links are displayed in the pagination container at any given time. `marginPageCount` controls how many pages are guaranteed to be displayed on the left and right of the component; `surroundingPageCount` controls how many pages will be displayed to the left and right of the current page. + +```jsx live + e.preventDefault()} +/> +``` + +The algorithm tries to minimize the amount the component shrinks and grows as the user changes pages; for this reason, if any of the pages in the margin (controlled via `marginPageCount`) intersect with pages in the center (controlled by `surroundingPageCount`), the center section will be shifted away from the margin. Consider the following examples, where pages one through six are shown when any of the first four pages are selected. Only when the fifth page is selected and there is a gap between the margin pages and the center pages does a break element appear. + +```jsx live + + {[1, 2, 3, 4, 5].map(page => ( + e.preventDefault()} + /> + ))} + +``` + +### Previous/next pagination + +To hide all the page numbers and create a simple pagination container with just the Previous and Next buttons, set `showPages` to `false`. + +```jsx live + + {([page, setPage]) => { + const totalPages = 10 + const onPageChange = (evt, page) => { + evt.preventDefault() + setPage(page) + } + + return ( + + Current page: {page} / {totalPages} + + + ) + }} + +``` + +## System props + +Pagination components get `COMMON` system props. Read our [System Props](/system-props) doc page for a full list of available props. + +## Component props + +| Name | Type | Default | Description | +| :- | :- | :-: | :- | +| currentPage | Number | | **Required.** The currently selected page. | +| hrefBuilder | Function | `#${page}` | A function to generate links based on page number. | +| marginPageCount | Number | 1 | How many pages to always show at the left and right of the component. | +| onPageChange | Function | no-op | Called with event and page number when a page is clicked. | +| pageCount | Number | | **Required.** The total number of pages. | +| showPages | Boolean | `true` | Whether or not to show the individual page links. | +| surroundingPageCount | Number | 2 | How many pages to display on each side of the currently selected page. | + +## Theming + +The following snippet shows the properties in the theme that control the styling of the pagination component: + +```javascript +{ + // ... rest of theme ... + pagination: { + fontSize, + fontWeight, + borderRadius, + colors: { + normal: { + fg, + bg, + border + }, + disabled: { + fg, + bg, + border + }, + hover: { + fg, + bg, + border + }, + selected: { + fg, + bg, + border + } + } + } +} +``` diff --git a/docs/src/@primer/gatsby-theme-doctocat/nav.yml b/docs/src/@primer/gatsby-theme-doctocat/nav.yml index 7f9f8399039..432a3aadf78 100644 --- a/docs/src/@primer/gatsby-theme-doctocat/nav.yml +++ b/docs/src/@primer/gatsby-theme-doctocat/nav.yml @@ -60,6 +60,8 @@ url: /LabelGroup - title: Link url: /Link + - title: Pagination + url: /Pagination - title: PointerBox url: /PointerBox - title: Popover diff --git a/index.d.ts b/index.d.ts index 0984a358cf8..3d6bcdce6f1 100644 --- a/index.d.ts +++ b/index.d.ts @@ -72,9 +72,7 @@ declare module '@primer/components' { extends BaseProps, CommonProps, TypographyProps, - Omit, 'color'> { - as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' - } + Omit, 'color'> {} export const Heading: React.FunctionComponent @@ -219,6 +217,28 @@ declare module '@primer/components' { export const Link: React.FunctionComponent + export type PaginationHrefBuilder = (page: number) => string + + export type PaginationPageChangeCallback = (e: React.MouseEvent, page: number) => void + + export interface PaginationProps extends CommonProps { + currentPage: number + hrefBuilder?: PaginationHrefBuilder + /** + * How many pages to show on the left and right of the component + */ + marginPageCount?: number + onPageChange?: PaginationPageChangeCallback + pageCount: number + showPages?: boolean + /** + * How many pages to show directly to the left and right of the current page + */ + surroundingPageCount?: number + } + + export const Pagination: React.FunctionComponent + export interface PointerBoxProps extends CommonProps, LayoutProps, BorderBoxProps { caret?: string } @@ -391,15 +411,12 @@ declare module '@primer/components' { export const themeGet: (key: any) => any export interface DialogProps extends CommonProps { + title: string isOpen: boolean onDismiss: () => unknown } - export interface DialogHeaderProps extends FlexProps {} - - export const Dialog: React.FunctionComponent & { - Header: React.FunctionComponent - } + export const Dialog: React.FunctionComponent export interface LabelGroupProps extends CommonProps, Omit, 'color'> {} @@ -536,6 +553,10 @@ declare module '@primer/components/src/Link' { import {Link} from '@primer/components' export default Link } +declare module '@primer/components/src/Pagination' { + import {Pagination} from '@primer/components' + export default Pagination +} declare module '@primer/components/src/PointerBox' { import {PointerBox} from '@primer/components' export default PointerBox diff --git a/src/Dialog.js b/src/Dialog.js index b00d4c46ad1..ca67571f1d6 100644 --- a/src/Dialog.js +++ b/src/Dialog.js @@ -26,7 +26,6 @@ export const StyledDialog = styled(ReachDialog)` box-shadow: 0px 4px 32px rgba(0, 0, 0, 0.35); border-radius: 4px; padding: 0 !important; - position: relative; @media screen and (max-width: 750px) { width: 100vw !important; @@ -46,13 +45,14 @@ const UnstyledButton = styled(Flex).attrs({ background: none; border: none; padding: 0; - - position: absolute; - top: 16px; - right: 16px; ` -const DialogHeaderBase = styled(Flex)` +const DialogHeader = styled(Flex).attrs({ + p: 3, + bg: 'gray.1', + justifyContent: 'space-between', + alignItems: 'center' +})` border-radius: 4px 4px 0px 0px; border-bottom: 1px solid #dad5da; @@ -61,29 +61,22 @@ const DialogHeaderBase = styled(Flex)` } ` -function DialogHeader({theme, children, ...rest}) { - if (React.Children.toArray(children).every(ch => typeof ch === 'string')) { - children = ( - - {children} - - ) - } - - return ( - - {children} - - ) -} - -const Dialog = ({children, ...props}) => { +const Dialog = ({title, children, ...props}) => { return ( <> - - - + + {typeof title === 'string' ? ( + + {title} + + ) : ( + title + )} + + + + {children} @@ -100,17 +93,8 @@ Dialog.propTypes = { children: PropTypes.node.isRequired, isOpen: PropTypes.bool.isRequired, onDismiss: PropTypes.func.isRequired, - theme: PropTypes.object -} - -DialogHeader.defaultProps = { - backgroundColor: 'gray.1', - theme -} - -DialogHeader.propTypes = { - ...Flex.propTypes + theme: PropTypes.object, + title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired } -Dialog.Header = DialogHeader export default Dialog diff --git a/src/Heading.js b/src/Heading.js index 424d3327572..f75ff14c3bc 100644 --- a/src/Heading.js +++ b/src/Heading.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import {TYPOGRAPHY, COMMON, get} from './constants' import theme from './theme' -const Heading = styled.h2` +const Heading = styled.h1` font-weight: ${get('fontWeights.bold')}; font-size: ${get('fontSizes.5')}; margin: 0; @@ -15,10 +15,9 @@ Heading.defaultProps = { } Heading.propTypes = { - as: PropTypes.oneOf(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']), - theme: PropTypes.object, ...COMMON.propTypes, - ...TYPOGRAPHY.propTypes + ...TYPOGRAPHY.propTypes, + theme: PropTypes.object } export default Heading diff --git a/src/Pagination/Pagination.js b/src/Pagination/Pagination.js new file mode 100644 index 00000000000..ff9b56a5b7f --- /dev/null +++ b/src/Pagination/Pagination.js @@ -0,0 +1,163 @@ +import React from 'react' +import PropTypes from 'prop-types' +import styled, {css} from 'styled-components' +import {get, COMMON} from '../constants' +import theme from '../theme' +import Box from '../Box' +import {buildPaginationModel, buildComponentData} from './model' + +const Page = styled.a` + position: relative; + float: left; + padding: 7px 12px; + margin-left: -1px; + font-size: ${get('pagination.fontSize')}; + font-style: normal; + font-weight: ${get('pagination.fontWeight')}; + color: ${get('pagination.colors.normal.fg')}; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + user-select: none; + background: ${get('pagination.colors.normal.bg')}; + border: ${get('borders.1')} ${get('pagination.colors.normal.border')}; + text-decoration: none; + + &:first-child { + margin-left: 0; + border-top-left-radius: ${get('pagination.borderRadius')}; + border-bottom-left-radius: ${get('pagination.borderRadius')}; + } + + &:last-child { + border-top-right-radius: ${get('pagination.borderRadius')}; + border-bottom-right-radius: ${get('pagination.borderRadius')}; + } + + ${props => + !props.selected && + !props.disabled && + css` + &:hover, + &:focus { + z-index: 2; + color: ${get('pagination.colors.hover.fg')}; + background-color: ${get('pagination.colors.hover.bg')}; + border-color: ${get('pagination.colors.hover.border')}; + } + `} + + ${props => + props.selected && + css` + z-index: 3; + color: ${get('pagination.colors.selected.fg')}; + background-color: ${get('pagination.colors.selected.bg')}; + border-color: ${get('pagination.colors.selected.border')}; + `} + + ${props => + props.disabled && + css` + color: ${get('pagination.colors.disabled.fg')}; + cursor: default; + background-color: ${get('pagination.colors.disabled.bg')}; + border-color: ${get('pagination.colors.disabled.border')}; + `} + + ${COMMON}; +` + +function usePaginationPages({ + theme, + pageCount, + currentPage, + onPageChange, + hrefBuilder, + marginPageCount, + showPages, + surroundingPageCount +}) { + const pageChange = React.useCallback(n => e => onPageChange(e, n), [onPageChange]) + + const model = React.useMemo(() => { + return buildPaginationModel(pageCount, currentPage, showPages, marginPageCount, surroundingPageCount) + }, [pageCount, currentPage, showPages, marginPageCount, surroundingPageCount]) + + const children = React.useMemo(() => { + return model.map(page => { + const {props, key, content} = buildComponentData(page, hrefBuilder, pageChange(page.num)) + return ( + + {content} + + ) + }) + }, [model, hrefBuilder, pageChange]) + + return children +} + +const PaginationContainer = styled.nav` + margin-top: 20px; + margin-bottom: 15px; + text-align: center; +` + +function Pagination({ + theme, + pageCount, + currentPage, + onPageChange, + hrefBuilder, + marginPageCount, + showPages, + surroundingPageCount, + ...rest +}) { + const pageElements = usePaginationPages({ + theme, + pageCount, + currentPage, + onPageChange, + hrefBuilder, + marginPageCount, + showPages, + surroundingPageCount + }) + return ( + + + {pageElements} + + + ) +} + +function defaultHrefBuilder(pageNum) { + return `#${pageNum}` +} + +function noop() {} + +Pagination.propTypes = { + currentPage: PropTypes.number.isRequired, + hrefBuilder: PropTypes.func, + marginPageCount: PropTypes.number, + onPageChange: PropTypes.func, + pageCount: PropTypes.number.isRequired, + showPages: PropTypes.bool, + surroundingPageCount: PropTypes.number, + ...COMMON.propTypes +} + +Pagination.defaultProps = { + hrefBuilder: defaultHrefBuilder, + marginPageCount: 1, + onPageChange: noop, + showPages: true, + surroundingPageCount: 2, + theme +} + +export default Pagination diff --git a/src/Pagination/index.js b/src/Pagination/index.js new file mode 100644 index 00000000000..1466412190c --- /dev/null +++ b/src/Pagination/index.js @@ -0,0 +1,3 @@ +import Pagination from './Pagination' + +export default Pagination diff --git a/src/Pagination/model.js b/src/Pagination/model.js new file mode 100644 index 00000000000..9a9dda8a166 --- /dev/null +++ b/src/Pagination/model.js @@ -0,0 +1,170 @@ +export function buildPaginationModel(pageCount, currentPage, showPages, marginPageCount, surroundingPageCount) { + const pages = [] + + if (showPages) { + const pageNums = [] + const addPage = n => { + if (n >= 1 && n <= pageCount) { + pageNums.push(n) + } + } + + // Start by defining the window of pages to show around the current page. + // If the window goes off either edge, shift it until it fits. + let extentLeft = currentPage - surroundingPageCount + let extentRight = currentPage + surroundingPageCount + if (extentLeft < 1 && extentRight > pageCount) { + // Our window is larger than the entire range, + // so simply display every page. + extentLeft = 1 + extentRight = pageCount + } else if (extentLeft < 1) { + while (extentLeft < 1) { + extentLeft++ + extentRight++ + } + } else if (extentRight > pageCount) { + while (extentRight > pageCount) { + extentLeft-- + extentRight-- + } + } + + // Next, include the pages in the margins. + // If a margin page is already covered in the window, + // extend the window to the other direction. + for (let i = 1; i <= marginPageCount; i++) { + const leftPage = i + const rightPage = pageCount - (i - 1) + if (leftPage >= extentLeft) { + extentRight++ + } else { + addPage(leftPage) + } + if (rightPage <= extentRight) { + extentLeft-- + } else { + addPage(rightPage) + } + } + + for (let i = extentLeft; i <= extentRight; i++) { + addPage(i) + } + + const sorted = pageNums + .slice() + .sort((a, b) => a - b) + .filter((item, idx, ary) => !idx || item !== ary[idx - 1]) + for (let idx = 0; idx < sorted.length; idx++) { + const num = sorted[idx] + const selected = num === currentPage + if (idx === 0) { + if (num !== 1) { + // If the first page isn't page one, + // we need to add a break + pages.push({ + type: 'BREAK', + num: 1 + }) + } + pages.push({ + type: 'NUM', + num, + selected + }) + } else { + const last = sorted[idx - 1] + const delta = num - last + if (delta === 1) { + pages.push({ + type: 'NUM', + num, + selected + }) + } else { + // We skipped some, so add a break + pages.push({ + type: 'BREAK', + num: num - 1 + }) + pages.push({ + type: 'NUM', + num, + selected + }) + } + } + } + + const lastPage = pages[pages.length - 1] + if (lastPage.type === 'NUM' && lastPage.num !== pageCount) { + // The last page we rendered wasn't the actual last page, + // so we need an additional break + pages.push({ + type: 'BREAK', + num: pageCount + }) + } + } + + const prev = {type: 'PREV', num: currentPage - 1, disabled: currentPage === 1} + const next = {type: 'NEXT', num: currentPage + 1, disabled: currentPage === pageCount} + return [prev, ...pages, next] +} + +export function buildComponentData(page, hrefBuilder, onClick) { + const props = {} + let content = '' + let key = '' + + switch (page.type) { + case 'PREV': { + key = 'page-prev' + content = 'Previous' + if (page.disabled) { + Object.assign(props, {as: 'span', 'aria-disabled': 'true', disabled: true}) + } else { + Object.assign(props, { + rel: 'prev', + href: hrefBuilder(page.num), + 'aria-label': 'Previous Page', + onClick + }) + } + break + } + case 'NEXT': { + key = 'page-next' + content = 'Next' + if (page.disabled) { + Object.assign(props, {as: 'span', 'aria-disabled': 'true', disabled: true}) + } else { + Object.assign(props, { + rel: 'next', + href: hrefBuilder(page.num), + 'aria-label': 'Next Page', + onClick + }) + } + break + } + case 'NUM': { + key = `page-${page.num}` + content = page.num + if (page.selected) { + Object.assign(props, {as: 'em', 'aria-current': 'page', selected: true}) + } else { + Object.assign(props, {href: hrefBuilder(page.num), 'aria-label': `Page ${page.num}`, onClick}) + } + break + } + case 'BREAK': { + key = `page-${page.num}-break` + content = '…' + Object.assign(props, {as: 'span', disabled: true}) + } + } + + return {props, key, content} +} diff --git a/src/__tests__/BorderBox.js b/src/__tests__/BorderBox.js index 67a2c3becc8..9304f6ab166 100644 --- a/src/__tests__/BorderBox.js +++ b/src/__tests__/BorderBox.js @@ -40,6 +40,6 @@ describe('BorderBox', () => { // the test returns the box shadow value without spaces, so had to manually provide the expected string here it('renders box shadow', () => { - expect(render()).toHaveStyleRule('box-shadow', '0 1px 0 rgba(149,157,165,0.1)') + expect(render()).toHaveStyleRule('box-shadow', '0 1px 1px rgba(27,31,35,0.1)') }) }) diff --git a/src/__tests__/Dialog.js b/src/__tests__/Dialog.js index 38173a350d7..99a32e4d065 100644 --- a/src/__tests__/Dialog.js +++ b/src/__tests__/Dialog.js @@ -8,8 +8,7 @@ import 'babel-polyfill' expect.extend(toHaveNoViolations) const comp = ( - null}> - Title + null}> Some content @@ -23,8 +22,4 @@ describe('Dialog', () => { expect(results).toHaveNoViolations() cleanup() }) - - it('renders consistently', () => { - expect(comp).toMatchSnapshot() - }) }) diff --git a/src/__tests__/Heading.js b/src/__tests__/Heading.js index 2540da192c0..d447cc2c587 100644 --- a/src/__tests__/Heading.js +++ b/src/__tests__/Heading.js @@ -31,8 +31,8 @@ const theme = { } describe('Heading', () => { - it('renders

by default', () => { - expect(render().type).toEqual('h2') + it('renders

by default', () => { + expect(render().type).toEqual('h1') }) it('should have no axe violations', async () => { @@ -94,6 +94,7 @@ describe('Heading', () => { it('respects the "fontStyle" prop', () => { expect(render()).toHaveStyleRule('font-style', 'italic') + expect(render()).toHaveStyleRule('font-style', 'normal') }) it.skip('renders fontSize with f* classes using inverse scale', () => { diff --git a/src/__tests__/Pagination/Pagination.js b/src/__tests__/Pagination/Pagination.js new file mode 100644 index 00000000000..0be0c01aa4d --- /dev/null +++ b/src/__tests__/Pagination/Pagination.js @@ -0,0 +1,41 @@ +import React from 'react' +import Pagination from '../../Pagination' +import {render} from '../../utils/testing' +import {COMMON} from '../../constants' +import {render as HTMLRender, cleanup} from '@testing-library/react' +import {axe, toHaveNoViolations} from 'jest-axe' +import 'babel-polyfill' +expect.extend(toHaveNoViolations) + +const reqProps = {pageCount: 10, currentPage: 1} +const comp = + +describe('Pagination', () => { + it('implements common system props', () => { + expect(Pagination).toImplementSystemProps(COMMON) + }) + + it('should have no axe violations', async () => { + const {container} = HTMLRender(comp) + const results = await axe(container, { + rules: { + // The skip-link rule has to do with entire documents + // and is not relevant to this component. + // See https://dequeuniversity.com/rules/axe/3.3/skip-link?application=axeAPI + 'skip-link': { + enabled: false + } + } + }) + expect(results).toHaveNoViolations() + cleanup() + }) + + it('has default theme', () => { + expect(comp).toSetDefaultTheme() + }) + + it('matches snapshot', () => { + expect(render(comp)).toMatchSnapshot() + }) +}) diff --git a/src/__tests__/Pagination/PaginationModel.js b/src/__tests__/Pagination/PaginationModel.js new file mode 100644 index 00000000000..3dec12c539b --- /dev/null +++ b/src/__tests__/Pagination/PaginationModel.js @@ -0,0 +1,131 @@ +import 'babel-polyfill' +import {buildPaginationModel} from '../../Pagination/model' + +function first(array, count = 1) { + const slice = array.slice(0, count) + return count === 1 ? slice[0] : slice +} + +function last(array, count = 1) { + const len = array.length + const slice = array.slice(len - count, len) + return count === 1 ? slice[0] : slice +} + +describe('Pagination model', () => { + it('sets disabled on prev links', () => { + const model1 = buildPaginationModel(10, 1, true, 1, 2) + expect(first(model1).type).toEqual('PREV') + expect(first(model1).disabled).toBe(true) + + const model2 = buildPaginationModel(10, 2, true, 1, 2) + expect(first(model2).type).toEqual('PREV') + expect(first(model2).disabled).toBe(false) + }) + + it('sets disabled on next links', () => { + const model1 = buildPaginationModel(10, 10, true, 1, 2) + expect(last(model1).type).toEqual('NEXT') + expect(last(model1).disabled).toBe(true) + + const model2 = buildPaginationModel(10, 9, true, 1, 2) + expect(last(model2).type).toEqual('NEXT') + expect(last(model2).disabled).toBe(false) + }) + + it('sets the page number for prev and next links', () => { + const model = buildPaginationModel(10, 5, true, 1, 2) + expect(first(model).num).toEqual(4) + expect(last(model).num).toEqual(6) + }) + + it('ensures margin pages on the left', () => { + const model = buildPaginationModel(10, 10, true, 2, 0) + const slice = first(model, 5) + + const expected = [ + {type: 'PREV', num: 9}, + {type: 'NUM', num: 1}, + {type: 'NUM', num: 2}, + {type: 'BREAK'}, + {type: 'NUM'} + ] + + expect(slice).toMatchObject(expected) + }) + + it('ensures margin pages on the right', () => { + const model = buildPaginationModel(10, 1, true, 2, 0) + const slice = last(model, 5) + + const expected = [ + {type: 'NUM'}, + {type: 'BREAK'}, + {type: 'NUM', num: 9}, + {type: 'NUM', num: 10}, + {type: 'NEXT', num: 2} + ] + + expect(slice).toMatchObject(expected) + }) + + it('ensures that the current page is surrounded by the right number of pages', () => { + const model = buildPaginationModel(10, 5, true, 1, 1) + const expected = [ + {type: 'PREV', num: 4}, + {type: 'NUM', num: 1}, + {type: 'BREAK'}, + {type: 'NUM', num: 4}, + {type: 'NUM', num: 5, selected: true}, + {type: 'NUM', num: 6}, + {type: 'BREAK'}, + {type: 'NUM', num: 10}, + {type: 'NEXT', num: 6} + ] + expect(model).toMatchObject(expected) + }) + + it('adds items to the right if it hits bounds to the left', () => { + const model = buildPaginationModel(15, 2, true, 1, 1) + const expected = [ + {type: 'PREV', num: 1}, + {type: 'NUM', num: 1}, + {type: 'NUM', num: 2, selected: true}, + {type: 'NUM', num: 3}, + // normally with a surround of 1, only 1 and 3 would be shown + // however, since 1 was already shown, we extend to 4 + {type: 'NUM', num: 4}, + {type: 'BREAK'} + ] + expect(first(model, 6)).toMatchObject(expected) + }) + + it('adds items to the left if it hits bounds to the right', () => { + const model = buildPaginationModel(15, 14, true, 1, 1) + const expected = [ + // normally with a surround of 1, only 13 and 15 would be shown + // however, since 15 was already shown, we extend to 12 + {type: 'BREAK'}, + {type: 'NUM', num: 12}, + {type: 'NUM', num: 13}, + {type: 'NUM', num: 14, selected: true}, + {type: 'NUM', num: 15}, + {type: 'NEXT', num: 15} + ] + expect(last(model, 6)).toMatchObject(expected) + }) + + it('correctly creates breaks next to the next/prev links when margin is 0', () => { + const model = buildPaginationModel(10, 5, true, 0, 1) + const expected = [ + {type: 'PREV'}, + {type: 'BREAK', num: 1}, + {type: 'NUM', num: 4}, + {type: 'NUM', num: 5, selected: true}, + {type: 'NUM', num: 6}, + {type: 'BREAK', num: 10}, + {type: 'NEXT'} + ] + expect(model).toMatchObject(expected) + }) +}) diff --git a/src/__tests__/Pagination/__snapshots__/Pagination.js.snap b/src/__tests__/Pagination/__snapshots__/Pagination.js.snap new file mode 100644 index 00000000000..04172d2115d --- /dev/null +++ b/src/__tests__/Pagination/__snapshots__/Pagination.js.snap @@ -0,0 +1,216 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pagination matches snapshot 1`] = ` +.c1 { + display: inline-block; +} + +.c2 { + position: relative; + float: left; + padding: 7px 12px; + margin-left: -1px; + font-size: 13px; + font-style: normal; + font-weight: 600; + color: #0366d6; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background: #fff; + border: 1px solid #e1e4e8; + -webkit-text-decoration: none; + text-decoration: none; + color: #d1d5da; + cursor: default; + background-color: #fafbfc; + border-color: #e1e4e8; +} + +.c2:first-child { + margin-left: 0; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} + +.c2:last-child { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +.c3 { + position: relative; + float: left; + padding: 7px 12px; + margin-left: -1px; + font-size: 13px; + font-style: normal; + font-weight: 600; + color: #0366d6; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background: #fff; + border: 1px solid #e1e4e8; + -webkit-text-decoration: none; + text-decoration: none; + z-index: 3; + color: #fff; + background-color: #0366d6; + border-color: #0366d6; +} + +.c3:first-child { + margin-left: 0; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} + +.c3:last-child { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +.c4 { + position: relative; + float: left; + padding: 7px 12px; + margin-left: -1px; + font-size: 13px; + font-style: normal; + font-weight: 600; + color: #0366d6; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background: #fff; + border: 1px solid #e1e4e8; + -webkit-text-decoration: none; + text-decoration: none; +} + +.c4:first-child { + margin-left: 0; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} + +.c4:last-child { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +.c4:hover, +.c4:focus { + z-index: 2; + color: #0366d6; + background-color: #f6f8fa; + border-color: #e1e4e8; +} + +.c0 { + margin-top: 20px; + margin-bottom: 15px; + text-align: center; +} + + +`; diff --git a/src/__tests__/__snapshots__/BreadcrumbItem.js.snap b/src/__tests__/__snapshots__/BreadcrumbItem.js.snap index 8e9d092b63d..4d31c8bffb4 100644 --- a/src/__tests__/__snapshots__/BreadcrumbItem.js.snap +++ b/src/__tests__/__snapshots__/BreadcrumbItem.js.snap @@ -286,6 +286,33 @@ exports[`Breadcrumb.Item renders the given "as" prop 1`] = ` "condensedUltra": 1, "default": 1.5, }, + "pagination": Object { + "borderRadius": "3px", + "colors": Object { + "disabled": Object { + "bg": "#fafbfc", + "border": "#e1e4e8", + "fg": "#d1d5da", + }, + "hover": Object { + "bg": "#f6f8fa", + "border": "#e1e4e8", + "fg": "#0366d6", + }, + "normal": Object { + "bg": "#fff", + "border": "#e1e4e8", + "fg": "#0366d6", + }, + "selected": Object { + "bg": "#0366d6", + "border": "#0366d6", + "fg": "#fff", + }, + }, + "fontSize": "13px", + "fontWeight": 600, + }, "popovers": Object { "colors": Object { "caret": "rgba(27, 31, 35, 0.15)", @@ -298,15 +325,15 @@ exports[`Breadcrumb.Item renders the given "as" prop 1`] = ` "100px", ], "shadows": Object { - "extra-large": "0 12px 48px rgba(149, 157, 165, 0.3)", + "extra-large": "0 10px 50px rgba(27, 31, 35, 0.07)", "formControl": "inset 0px 2px 0px rgba(225, 228, 232, 0.2)", "formControlDisabled": "inset 0px 2px 0px rgba(220, 227, 237, 0.3)", "formControlFocus": "rgba(3, 102, 214, 0.3) 0px 0px 0px 0.2em", - "large": "0 8px 24px rgba(149, 157, 165, 0.2)", - "medium": "0 3px 6px rgba(149, 157, 165, 0.15)", + "large": "0 1px 15px rgba(27, 31, 35, 0.15)", + "medium": "0 1px 5px rgba(27, 31, 35, 0.15)", "primaryActiveShadow": "inset 0px 1px 0px rgba(20, 70, 32, 0.2)", "primaryShadow": "0px 1px 0px rgba(20, 70, 32, 0.1), inset 0px 2px 0px rgba(255, 255, 255, 0.03)", - "small": "0 1px 0 rgba(149, 157, 165, 0.1)", + "small": "0 1px 1px rgba(27, 31, 35, 0.1)", }, "sizes": Object { "large": "1012px", diff --git a/src/__tests__/__snapshots__/CircleBadge.js.snap b/src/__tests__/__snapshots__/CircleBadge.js.snap index ccce0cf64b7..e35e2e949b2 100644 --- a/src/__tests__/__snapshots__/CircleBadge.js.snap +++ b/src/__tests__/__snapshots__/CircleBadge.js.snap @@ -16,7 +16,7 @@ exports[`CircleBadge respects "as" prop 1`] = ` justify-content: center; background-color: #fff; border-radius: 50%; - box-shadow: 0 3px 6px rgba(149,157,165,0.15); + box-shadow: 0 1px 5px rgba(27,31,35,0.15); width: 96px; height: 96px; } @@ -42,7 +42,7 @@ exports[`CircleBadge respects the inline prop 1`] = ` justify-content: center; background-color: #fff; border-radius: 50%; - box-shadow: 0 3px 6px rgba(149,157,165,0.15); + box-shadow: 0 1px 5px rgba(27,31,35,0.15); width: 96px; height: 96px; } @@ -68,7 +68,7 @@ exports[`CircleBadge respects the variant prop 1`] = ` justify-content: center; background-color: #fff; border-radius: 50%; - box-shadow: 0 3px 6px rgba(149,157,165,0.15); + box-shadow: 0 1px 5px rgba(27,31,35,0.15); width: 128px; height: 128px; } @@ -94,7 +94,7 @@ exports[`CircleBadge uses the size prop to override the variant prop 1`] = ` justify-content: center; background-color: #fff; border-radius: 50%; - box-shadow: 0 3px 6px rgba(149,157,165,0.15); + box-shadow: 0 1px 5px rgba(27,31,35,0.15); width: 20px; height: 20px; } diff --git a/src/__tests__/__snapshots__/Dialog.js.snap b/src/__tests__/__snapshots__/Dialog.js.snap deleted file mode 100644 index 8566ce03799..00000000000 --- a/src/__tests__/__snapshots__/Dialog.js.snap +++ /dev/null @@ -1,1255 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Dialog renders consistently 1`] = ` - - - Title - - - - Some content - - - -`; diff --git a/src/__tests__/__snapshots__/FilterListItem.js.snap b/src/__tests__/__snapshots__/FilterListItem.js.snap index d2c9830fce2..46b6de7b6c7 100644 --- a/src/__tests__/__snapshots__/FilterListItem.js.snap +++ b/src/__tests__/__snapshots__/FilterListItem.js.snap @@ -299,6 +299,33 @@ exports[`FilterList.Item renders the given "as" prop 1`] = ` "condensedUltra": 1, "default": 1.5, }, + "pagination": Object { + "borderRadius": "3px", + "colors": Object { + "disabled": Object { + "bg": "#fafbfc", + "border": "#e1e4e8", + "fg": "#d1d5da", + }, + "hover": Object { + "bg": "#f6f8fa", + "border": "#e1e4e8", + "fg": "#0366d6", + }, + "normal": Object { + "bg": "#fff", + "border": "#e1e4e8", + "fg": "#0366d6", + }, + "selected": Object { + "bg": "#0366d6", + "border": "#0366d6", + "fg": "#fff", + }, + }, + "fontSize": "13px", + "fontWeight": 600, + }, "popovers": Object { "colors": Object { "caret": "rgba(27, 31, 35, 0.15)", @@ -311,15 +338,15 @@ exports[`FilterList.Item renders the given "as" prop 1`] = ` "100px", ], "shadows": Object { - "extra-large": "0 12px 48px rgba(149, 157, 165, 0.3)", + "extra-large": "0 10px 50px rgba(27, 31, 35, 0.07)", "formControl": "inset 0px 2px 0px rgba(225, 228, 232, 0.2)", "formControlDisabled": "inset 0px 2px 0px rgba(220, 227, 237, 0.3)", "formControlFocus": "rgba(3, 102, 214, 0.3) 0px 0px 0px 0.2em", - "large": "0 8px 24px rgba(149, 157, 165, 0.2)", - "medium": "0 3px 6px rgba(149, 157, 165, 0.15)", + "large": "0 1px 15px rgba(27, 31, 35, 0.15)", + "medium": "0 1px 5px rgba(27, 31, 35, 0.15)", "primaryActiveShadow": "inset 0px 1px 0px rgba(20, 70, 32, 0.2)", "primaryShadow": "0px 1px 0px rgba(20, 70, 32, 0.1), inset 0px 2px 0px rgba(255, 255, 255, 0.03)", - "small": "0 1px 0 rgba(149, 157, 165, 0.1)", + "small": "0 1px 1px rgba(27, 31, 35, 0.1)", }, "sizes": Object { "large": "1012px", diff --git a/src/__tests__/__snapshots__/SubNavLink.js.snap b/src/__tests__/__snapshots__/SubNavLink.js.snap index 630d00c25f8..a743fc0410d 100644 --- a/src/__tests__/__snapshots__/SubNavLink.js.snap +++ b/src/__tests__/__snapshots__/SubNavLink.js.snap @@ -326,6 +326,33 @@ exports[`SubNav.Link renders the given "as" prop 1`] = ` "condensedUltra": 1, "default": 1.5, }, + "pagination": Object { + "borderRadius": "3px", + "colors": Object { + "disabled": Object { + "bg": "#fafbfc", + "border": "#e1e4e8", + "fg": "#d1d5da", + }, + "hover": Object { + "bg": "#f6f8fa", + "border": "#e1e4e8", + "fg": "#0366d6", + }, + "normal": Object { + "bg": "#fff", + "border": "#e1e4e8", + "fg": "#0366d6", + }, + "selected": Object { + "bg": "#0366d6", + "border": "#0366d6", + "fg": "#fff", + }, + }, + "fontSize": "13px", + "fontWeight": 600, + }, "popovers": Object { "colors": Object { "caret": "rgba(27, 31, 35, 0.15)", @@ -338,15 +365,15 @@ exports[`SubNav.Link renders the given "as" prop 1`] = ` "100px", ], "shadows": Object { - "extra-large": "0 12px 48px rgba(149, 157, 165, 0.3)", + "extra-large": "0 10px 50px rgba(27, 31, 35, 0.07)", "formControl": "inset 0px 2px 0px rgba(225, 228, 232, 0.2)", "formControlDisabled": "inset 0px 2px 0px rgba(220, 227, 237, 0.3)", "formControlFocus": "rgba(3, 102, 214, 0.3) 0px 0px 0px 0.2em", - "large": "0 8px 24px rgba(149, 157, 165, 0.2)", - "medium": "0 3px 6px rgba(149, 157, 165, 0.15)", + "large": "0 1px 15px rgba(27, 31, 35, 0.15)", + "medium": "0 1px 5px rgba(27, 31, 35, 0.15)", "primaryActiveShadow": "inset 0px 1px 0px rgba(20, 70, 32, 0.2)", "primaryShadow": "0px 1px 0px rgba(20, 70, 32, 0.1), inset 0px 2px 0px rgba(255, 255, 255, 0.03)", - "small": "0 1px 0 rgba(149, 157, 165, 0.1)", + "small": "0 1px 1px rgba(27, 31, 35, 0.1)", }, "sizes": Object { "large": "1012px", diff --git a/src/__tests__/__snapshots__/UnderlineNavLink.js.snap b/src/__tests__/__snapshots__/UnderlineNavLink.js.snap index f2154a32c51..a0c09bb9395 100644 --- a/src/__tests__/__snapshots__/UnderlineNavLink.js.snap +++ b/src/__tests__/__snapshots__/UnderlineNavLink.js.snap @@ -303,6 +303,33 @@ exports[`UnderlineNav.Link renders the given "as" prop 1`] = ` "condensedUltra": 1, "default": 1.5, }, + "pagination": Object { + "borderRadius": "3px", + "colors": Object { + "disabled": Object { + "bg": "#fafbfc", + "border": "#e1e4e8", + "fg": "#d1d5da", + }, + "hover": Object { + "bg": "#f6f8fa", + "border": "#e1e4e8", + "fg": "#0366d6", + }, + "normal": Object { + "bg": "#fff", + "border": "#e1e4e8", + "fg": "#0366d6", + }, + "selected": Object { + "bg": "#0366d6", + "border": "#0366d6", + "fg": "#fff", + }, + }, + "fontSize": "13px", + "fontWeight": 600, + }, "popovers": Object { "colors": Object { "caret": "rgba(27, 31, 35, 0.15)", @@ -315,15 +342,15 @@ exports[`UnderlineNav.Link renders the given "as" prop 1`] = ` "100px", ], "shadows": Object { - "extra-large": "0 12px 48px rgba(149, 157, 165, 0.3)", + "extra-large": "0 10px 50px rgba(27, 31, 35, 0.07)", "formControl": "inset 0px 2px 0px rgba(225, 228, 232, 0.2)", "formControlDisabled": "inset 0px 2px 0px rgba(220, 227, 237, 0.3)", "formControlFocus": "rgba(3, 102, 214, 0.3) 0px 0px 0px 0.2em", - "large": "0 8px 24px rgba(149, 157, 165, 0.2)", - "medium": "0 3px 6px rgba(149, 157, 165, 0.15)", + "large": "0 1px 15px rgba(27, 31, 35, 0.15)", + "medium": "0 1px 5px rgba(27, 31, 35, 0.15)", "primaryActiveShadow": "inset 0px 1px 0px rgba(20, 70, 32, 0.2)", "primaryShadow": "0px 1px 0px rgba(20, 70, 32, 0.1), inset 0px 2px 0px rgba(255, 255, 255, 0.03)", - "small": "0 1px 0 rgba(149, 157, 165, 0.1)", + "small": "0 1px 1px rgba(27, 31, 35, 0.1)", }, "sizes": Object { "large": "1012px", diff --git a/src/index.js b/src/index.js index c94c997caad..1481979d276 100644 --- a/src/index.js +++ b/src/index.js @@ -34,6 +34,7 @@ export {default as Heading} from './Heading' export {default as Label} from './Label' export {default as LabelGroup} from './LabelGroup' export {default as Link} from './Link' +export {default as Pagination} from './Pagination' export {default as PointerBox} from './PointerBox' export {default as Popover} from './Popover' export {default as ProgressBar} from './ProgressBar' diff --git a/src/theme.js b/src/theme.js index 73a69da4104..cccbade424e 100644 --- a/src/theme.js +++ b/src/theme.js @@ -1,6 +1,7 @@ import {black, white, gray, blue, green, orange, purple, red, yellow} from 'primer-colors' import {lineHeights} from 'primer-typography' +// General const colors = { bodytext: gray[9], black, @@ -52,6 +53,59 @@ const colors = { accent: orange[5] } +const breakpoints = ['544px', '768px', '1012px', '1280px'] + +const fonts = { + normal: fontStack([ + '-apple-system', + 'BlinkMacSystemFont', + 'Segoe UI', + 'Helvetica', + 'Arial', + 'sans-serif', + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol' + ]), + mono: fontStack(['SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', 'Courier', 'monospace']) +} + +const fontWeights = { + light: 300, + normal: 400, + semibold: 500, + bold: 600 +} + +const borders = [0, '1px solid'] + +const radii = ['0', '3px', '6px', '100px'] + +const shadows = { + small: '0 1px 1px rgba(27, 31, 35, 0.1)', + medium: '0 1px 5px rgba(27, 31, 35, 0.15)', + large: '0 1px 15px rgba(27, 31, 35, 0.15)', + 'extra-large': '0 10px 50px rgba(27, 31, 35, 0.07)', + formControl: 'inset 0px 2px 0px rgba(225, 228, 232, 0.2)', + formControlDisabled: 'inset 0px 2px 0px rgba(220, 227, 237, 0.3)', + formControlFocus: 'rgba(3, 102, 214, 0.3) 0px 0px 0px 0.2em', + primaryShadow: '0px 1px 0px rgba(20, 70, 32, 0.1), inset 0px 2px 0px rgba(255, 255, 255, 0.03)', + primaryActiveShadow: 'inset 0px 1px 0px rgba(20, 70, 32, 0.2)' +} + +const sizes = { + small: '544px', + medium: '768px', + large: '1012px', + xlarge: '1280px' +} + +const fontSizes = ['12px', '14px', '16px', '20px', '24px', '32px', '40px', '48px'] + +const space = ['0', '4px', '8px', '16px', '24px', '32px', '40px', '48px', '64px', '80px', '96px', '112px', '128px'] + +// Components + const buttons = { default: { color: { @@ -158,52 +212,51 @@ const popovers = { } } +const pagination = { + fontSize: '13px', + fontWeight: fontWeights.bold, + borderRadius: radii[1], + colors: { + normal: { + fg: colors.blue[5], + bg: colors.white, + border: colors.border.gray + }, + disabled: { + fg: colors.gray[3], + bg: colors.gray[0], + border: colors.border.gray + }, + hover: { + fg: colors.blue[5], + bg: colors.gray[1], + border: colors.border.gray + }, + selected: { + fg: colors.white, + bg: colors.blue[5], + border: colors.blue[5] + } + } +} + const theme = { - breakpoints: ['544px', '768px', '1012px', '1280px'], - fonts: { - normal: fontStack([ - '-apple-system', - 'BlinkMacSystemFont', - 'Segoe UI', - 'Helvetica', - 'Arial', - 'sans-serif', - 'Apple Color Emoji', - 'Segoe UI Emoji', - 'Segoe UI Symbol' - ]), - mono: fontStack(['SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', 'Courier', 'monospace']) - }, - fontWeights: { - light: 300, - normal: 400, - semibold: 500, - bold: 600 - }, + // General + borders, + breakpoints, colors, - borders: [0, '1px solid'], - fontSizes: ['12px', '14px', '16px', '20px', '24px', '32px', '40px', '48px'], + fonts, + fontSizes, + fontWeights, lineHeights, - radii: ['0', '3px', '6px', '100px'], - shadows: { - small: '0 1px 0 rgba(149, 157, 165, 0.1)', - medium: '0 3px 6px rgba(149, 157, 165, 0.15)', - large: '0 8px 24px rgba(149, 157, 165, 0.2)', - 'extra-large': '0 12px 48px rgba(149, 157, 165, 0.3)', - formControl: 'inset 0px 2px 0px rgba(225, 228, 232, 0.2)', - formControlDisabled: 'inset 0px 2px 0px rgba(220, 227, 237, 0.3)', - formControlFocus: 'rgba(3, 102, 214, 0.3) 0px 0px 0px 0.2em', - primaryShadow: '0px 1px 0px rgba(20, 70, 32, 0.1), inset 0px 2px 0px rgba(255, 255, 255, 0.03)', - primaryActiveShadow: 'inset 0px 1px 0px rgba(20, 70, 32, 0.2)' - }, - sizes: { - small: '544px', - medium: '768px', - large: '1012px', - xlarge: '1280px' - }, - space: ['0', '4px', '8px', '16px', '24px', '32px', '40px', '48px', '64px', '80px', '96px', '112px', '128px'], + radii, + shadows, + sizes, + space, + + // Components buttons, + pagination, popovers } diff --git a/src/utils/test-matchers.js b/src/utils/test-matchers.js index 0aea935954a..ff70965ae10 100644 --- a/src/utils/test-matchers.js +++ b/src/utils/test-matchers.js @@ -52,7 +52,13 @@ expect.extend({ }, toSetDefaultTheme(Component) { - const wrapper = mount() + let comp + if (Component.type) { + comp = Component + } else { + comp = + } + const wrapper = mount(comp) const pass = this.equals(wrapper.prop('theme'), theme) return { pass,