-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NV-3417: Pagination component + vitest setup (#5107)
- Loading branch information
Joel Anton
authored
Feb 1, 2024
1 parent
0494f9c
commit 39acfd3
Showing
26 changed files
with
1,944 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,47 @@ | ||
import React from 'react'; | ||
import { useDarkMode } from 'storybook-dark-mode'; | ||
import React, { useEffect } from 'react'; | ||
import { addons } from '@storybook/preview-api'; | ||
import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode'; | ||
import { ThemeProvider } from '../src/ThemeProvider'; | ||
import { DocsContainer } from './Doc.container'; | ||
import { useLocalThemePreference } from '@novu/shared-web'; | ||
|
||
export const parameters = { | ||
layout: 'fullscreen', | ||
viewMode: 'docs', | ||
docs: { | ||
container: DocsContainer, | ||
}, | ||
darkMode: { | ||
current: 'dark', | ||
}, | ||
actions: { argTypesRegex: '^on[A-Z].*' }, | ||
controls: { | ||
matchers: { | ||
color: /(background|color)$/i, | ||
date: /Date$/, | ||
}, | ||
}, | ||
darkMode: { | ||
current: 'dark', | ||
classTarget: 'html', | ||
}, | ||
}; | ||
|
||
function ThemeWrapper(props) { | ||
const channel = addons.getChannel(); | ||
function ColorSchemeThemeWrapper({ children }) { | ||
const { setThemeStatus } = useLocalThemePreference(); | ||
|
||
const handleColorScheme = (value) => { | ||
setThemeStatus(value ? 'dark' : 'light'); | ||
}; | ||
|
||
useEffect(() => { | ||
channel.on(DARK_MODE_EVENT_NAME, handleColorScheme); | ||
return () => channel.off(DARK_MODE_EVENT_NAME, handleColorScheme); | ||
}, [channel]); | ||
|
||
return ( | ||
<div style={{ margin: '3em' }}> | ||
<ThemeProvider dark={useDarkMode()}>{props.children}</ThemeProvider> | ||
<ThemeProvider>{children}</ThemeProvider> | ||
</div> | ||
); | ||
} | ||
|
||
export const decorators = [(renderStory) => <ThemeWrapper>{renderStory()}</ThemeWrapper>]; | ||
export const decorators = [(renderStory) => <ColorSchemeThemeWrapper>{renderStory()}</ColorSchemeThemeWrapper>]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import styled from '@emotion/styled'; | ||
import { Box, BoxProps } from '@mantine/core'; | ||
import { forwardRef, PropsWithChildren, useContext, useEffect, useState } from 'react'; | ||
import { ChevronLeft, ChevronRight } from '../icons'; | ||
import { ControlButton } from './ControlButton'; | ||
import { DEFAULT_ELLIPSIS_NODE, DEFAULT_SIBLING_COUNT, MAX_SIBLING_COUNT, MIN_SIBLING_COUNT } from './Pagination.const'; | ||
import { PaginationContext } from './PaginationContext'; | ||
import { getPaginationSymbols, PaginationSymbol } from './util'; | ||
import { clamp } from '../utils'; | ||
import { IconControlButton } from './IconControlButton'; | ||
|
||
const Group = styled(Box)<BoxProps & React.RefAttributes<HTMLDivElement>>( | ||
({ theme }) => ` | ||
display: flex; | ||
flex-direction: row; | ||
align-items: center; | ||
/* TODO: use theme value */ | ||
gap: 0.25rem; | ||
` | ||
); | ||
|
||
export interface IControlBarProps { | ||
/** the quantity of items to show on each side of the "current page" */ | ||
siblingCount?: number; | ||
/** the node to render when showing a gap between two disparate page numbers. Defaults to "..." */ | ||
ellipsisNode?: JSX.Element; | ||
className?: string; | ||
} | ||
|
||
/** | ||
* Primary pagination navigation component. | ||
* | ||
* `children` is optional, and if included, will override the default behavior. | ||
* If using your own children, use `Pagination.ControlButton` to hook into the PaginationContext. | ||
* @requires this component to be a child of a Pagination component | ||
*/ | ||
export const ControlBar = forwardRef<HTMLDivElement, PropsWithChildren<IControlBarProps>>( | ||
({ className, siblingCount = DEFAULT_SIBLING_COUNT, ellipsisNode = DEFAULT_ELLIPSIS_NODE, children }, ref) => { | ||
const { currentPageNumber, totalPageCount } = useContext(PaginationContext); | ||
const [clampedSiblingCount, setClampedSiblingCount] = useState<number>(siblingCount); | ||
|
||
useEffect(() => { | ||
// ensure the sibling count is within the allowed range | ||
if (siblingCount < MIN_SIBLING_COUNT || siblingCount > MAX_SIBLING_COUNT) { | ||
setClampedSiblingCount(clamp(siblingCount, MIN_SIBLING_COUNT, MAX_SIBLING_COUNT)); | ||
} | ||
}, [siblingCount, setClampedSiblingCount]); | ||
|
||
const renderCentralButton = (curPageSymbol: PaginationSymbol, index: number) => { | ||
if (curPageSymbol === 'ELLIPSIS') { | ||
return ( | ||
<ControlButton key={`pagination-ellipsis-btn-${index}`} disabled> | ||
{ellipsisNode} | ||
</ControlButton> | ||
); | ||
} | ||
|
||
return ( | ||
<ControlButton | ||
key={`pagination-page-number-btn-${curPageSymbol}-${index}`} | ||
onClick={({ onPageChange }) => { | ||
onPageChange(curPageSymbol); | ||
}} | ||
isCurrentPage={curPageSymbol === currentPageNumber} | ||
> | ||
{curPageSymbol} | ||
</ControlButton> | ||
); | ||
}; | ||
|
||
return ( | ||
<Group ref={ref} className={className}> | ||
{children || ( | ||
<> | ||
<IconControlButton | ||
onClick={({ onPageChange, currentPageNumber: curPageNum }) => { | ||
onPageChange(curPageNum - 1); | ||
}} | ||
disabled={currentPageNumber === 1} | ||
> | ||
<ChevronLeft /> | ||
</IconControlButton> | ||
{getPaginationSymbols({ totalPageCount, currentPageNumber, siblingCount: clampedSiblingCount }).map( | ||
renderCentralButton | ||
)} | ||
<IconControlButton | ||
onClick={({ onPageChange, currentPageNumber: curPageNum }) => { | ||
onPageChange(curPageNum + 1); | ||
}} | ||
disabled={currentPageNumber === totalPageCount} | ||
> | ||
{<ChevronRight />} | ||
</IconControlButton> | ||
</> | ||
)} | ||
</Group> | ||
); | ||
} | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import styled from '@emotion/styled'; | ||
import { CSSProperties, forwardRef, useContext } from 'react'; | ||
import { colors } from '../config'; | ||
import { Button, IButtonProps } from '../button/Button'; | ||
import { IPaginationContext, PaginationContext } from './PaginationContext'; | ||
|
||
export type TPageButtonClickHandler = (ctx: IPaginationContext) => void; | ||
|
||
type StylingProps = Pick<IControlButtonProps, 'isCurrentPage'>; | ||
|
||
// TODO: Fix `theme` type once design system is ready and then use theme values | ||
const getFontColor = ({ theme, isCurrentPage }: { theme: any } & StylingProps): string => { | ||
return theme.colorScheme === 'dark' | ||
? isCurrentPage | ||
? colors.white | ||
: colors.B60 | ||
: isCurrentPage | ||
? colors.BGDark // TODO: speak with Design -- this is bad, we should not be using a "BG" color for font | ||
: colors.B60; | ||
}; | ||
|
||
// TODO: Fix `theme` type once design system is ready and then use theme values | ||
const getFontWeight = ({ theme, isCurrentPage }: { theme: any } & StylingProps): CSSProperties['fontWeight'] => { | ||
return isCurrentPage ? 700 : 600; | ||
}; | ||
|
||
// TODO: Fix `theme` type once design system is ready and then use theme values | ||
const getBackgroundColor = ({ theme, isCurrentPage }: { theme: any } & StylingProps): CSSProperties['fontWeight'] => { | ||
return isCurrentPage ? (theme.colorScheme === 'dark' ? colors.B30 : colors.BGLight) : 'none'; | ||
}; | ||
|
||
const StyledButton = styled(Button)<StylingProps>( | ||
({ theme, isCurrentPage }) => ` | ||
font-weight: ${getFontWeight({ theme, isCurrentPage })}; | ||
background: ${getBackgroundColor({ theme, isCurrentPage })}; | ||
color: ${getFontColor({ theme, isCurrentPage })}; | ||
&:disabled { | ||
background: ${getBackgroundColor({ theme, isCurrentPage })}; | ||
color: ${getFontColor({ theme, isCurrentPage })}; | ||
} | ||
/* override mantine */ | ||
height: inherit; | ||
/* TODO: theme values for next few lines */ | ||
border-radius: 4px; | ||
line-height: 20px; | ||
padding: 2px 3.5px; | ||
min-width: 24px; | ||
` | ||
); | ||
|
||
export interface IControlButtonProps extends Omit<IButtonProps, 'onClick'> { | ||
onClick?: TPageButtonClickHandler; | ||
/** Does the button represent the currently-selected page */ | ||
isCurrentPage?: boolean; | ||
} | ||
|
||
/** | ||
* Button for navigating to a specific page. | ||
* @requires this component to be a child of a Pagination component | ||
*/ | ||
export const ControlButton: React.FC<IControlButtonProps> = forwardRef<HTMLButtonElement, IControlButtonProps>( | ||
({ onClick, className, id, disabled, isCurrentPage, ...buttonProps }, buttonRef) => { | ||
const paginationCtx = useContext(PaginationContext); | ||
|
||
// hydrate the click handler with the context | ||
const handleClick = () => onClick?.(paginationCtx); | ||
|
||
return ( | ||
<StyledButton | ||
isCurrentPage={isCurrentPage} | ||
id={id} | ||
onClick={handleClick} | ||
disabled={disabled || !onClick} | ||
ref={buttonRef} | ||
className={className} | ||
> | ||
{buttonProps.children} | ||
</StyledButton> | ||
); | ||
} | ||
); |
Oops, something went wrong.