Skip to content

Commit

Permalink
feat(popover): add customization (#1915)
Browse files Browse the repository at this point in the history
* feat(popover): add customization, tests, and stories
  • Loading branch information
zahnster committed Oct 8, 2021
1 parent ba982f9 commit db9f966
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 20 deletions.
6 changes: 6 additions & 0 deletions .changeset/warm-readers-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@twilio-paste/popover': minor
'@twilio-paste/core': minor
---

[Popover] Enable component to respect element customizations set on the customization provider. Component now enables setting an element name on the underlying HTML element and checks the emotion theme object to determine whether it should merge in custom styles to the ones set by the component author.
112 changes: 112 additions & 0 deletions packages/paste-core/components/popover/__tests__/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import * as React from 'react';
import {render, screen, waitFor} from '@testing-library/react';
import {matchers} from 'jest-emotion';
import {Theme} from '@twilio-paste/theme';
import {CustomizationProvider} from '@twilio-paste/customization';
import {Text} from '@twilio-paste/text';
// @ts-ignore typescript doesn't like js imports
import axe from '../../../../../.jest/axe-helper';
import {PopoverTop, StateHookExample} from '../stories/index.stories';
import {Popover, PopoverContainer, PopoverButton} from '../src';

expect.extend(matchers);

describe('Popover', () => {
describe('Render', () => {
Expand Down Expand Up @@ -78,4 +84,110 @@ describe('Popover', () => {
expect(results).toHaveNoViolations();
});
});

describe('Customization', () => {
it('should set default data-paste-element attribute on Popover and customizable children and respect custom styles', (): void => {
render(
<CustomizationProvider
baseTheme="default"
elements={{
POPOVER: {
backgroundColor: 'colorBackground',
},
POPOVER_BUTTON: {
backgroundColor: 'colorBackgroundBrandStronger',
},
POPOVER_CLOSE_BUTTON: {
backgroundColor: 'colorBackgroundBodyInverse',
},
POPOVER_CLOSE_ICON: {
color: 'colorTextInverse',
},
}}
>
<PopoverContainer baseId="test-id">
<PopoverButton variant="primary" data-testid="popover-button">
Open popover
</PopoverButton>
<Popover aria-label="Popover" data-testid="popover">
<Text as="span">This is the Twilio styled popover that you can use in all your applications.</Text>
</Popover>
</PopoverContainer>
</CustomizationProvider>
);

const popoverComp = screen.getByTestId('popover');
const popoverButton = screen.getByTestId('popover-button');

// presence of popover hooks
expect(popoverComp.querySelector('[data-paste-element="POPOVER"]')).toBeInTheDocument();
expect(popoverComp.querySelector('[data-paste-element="POPOVER_CLOSE_BUTTON"]')).toBeInTheDocument();
expect(popoverComp.querySelector('[data-paste-element="POPOVER_CLOSE_ICON"]')).toBeInTheDocument();
expect(popoverButton).toHaveAttribute('data-paste-element', 'POPOVER_BUTTON');

// applied style rules
expect(popoverComp.querySelector('[data-paste-element="POPOVER"]')).toHaveStyleRule(
'background-color',
'rgb(244,244,246)'
);
expect(popoverComp.querySelector('[data-paste-element="POPOVER_CLOSE_BUTTON"]')).toHaveStyleRule(
'background-color',
'rgb(18,28,45)'
);
expect(popoverComp.querySelector('[data-paste-element="POPOVER_CLOSE_ICON"]')).toHaveStyleRule(
'color',
'rgb(255,255,255)'
);
expect(popoverButton).toHaveStyleRule('background-color', 'rgb(6,3,58)');
});

it('should set a custom element name and properly apply styles to Popover and customizable children', (): void => {
render(
<CustomizationProvider
baseTheme="default"
elements={{
MYPOPOVER: {
backgroundColor: 'colorBackground',
},
MYPOPOVER_BUTTON: {
backgroundColor: 'colorBackgroundBrandStronger',
},
MYPOPOVER_CLOSE_BUTTON: {
backgroundColor: 'colorBackgroundBodyInverse',
},
MYPOPOVER_CLOSE_ICON: {
color: 'colorTextInverse',
},
}}
>
<PopoverContainer baseId="test-id">
<PopoverButton element="MYPOPOVER_BUTTON" variant="primary" data-testid="popover-button">
Open popover
</PopoverButton>
<Popover element="MYPOPOVER" aria-label="Popover" data-testid="popover">
<Text as="span">This is the Twilio styled popover that you can use in all your applications.</Text>
</Popover>
</PopoverContainer>
</CustomizationProvider>
);

const popoverComp = screen.getByTestId('popover');
const popoverButton = screen.getByTestId('popover-button');

expect(popoverComp.querySelector('[data-paste-element="MYPOPOVER"]')).toHaveStyleRule(
'background-color',
'rgb(244,244,246)'
);
expect(popoverComp.querySelector('[data-paste-element="MYPOPOVER_CLOSE_BUTTON"]')).toHaveStyleRule(
'background-color',
'rgb(18,28,45)'
);
expect(popoverComp.querySelector('[data-paste-element="MYPOPOVER_CLOSE_ICON"]')).toHaveStyleRule(
'color',
'rgb(255,255,255)'
);

expect(popoverButton).toHaveStyleRule('background-color', 'rgb(6,3,58)');
});
});
});
26 changes: 16 additions & 10 deletions packages/paste-core/components/popover/src/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,22 @@ const StyledPopover = React.forwardRef<HTMLDivElement, BoxProps>(({style, ...pro
);
});

export interface PopoverProps {
export interface PopoverProps extends Pick<BoxProps, 'element'> {
'aria-label': string;
children: React.ReactNode;
}

const Popover = React.forwardRef<HTMLDivElement, PopoverProps>(({children, ...props}, ref) => {
const Popover = React.forwardRef<HTMLDivElement, PopoverProps>(({children, element = 'POPOVER', ...props}, ref) => {
const popover = React.useContext(PopoverContext);
return (
<NonModalDialogPrimitive {...(popover as any)} {...props} as={StyledPopover} ref={ref} preventBodyScroll={false}>
{/* import Paste Theme Based Styles due to portal positioning. */}
<StyledBase>
<PopoverArrow {...(popover as any)} />
<Box paddingX="space80" paddingY="space70">
<Box element={element} paddingX="space80" paddingY="space70">
<Box position="absolute" right={8} top={8}>
<Button
element={`${element}_CLOSE_BUTTON`}
variant="reset"
size="reset"
// @ts-ignore
Expand All @@ -51,7 +52,13 @@ const Popover = React.forwardRef<HTMLDivElement, PopoverProps>(({children, ...pr
// https://reakit.io/docs/popover/#initial-focus
onClick={popover.hide}
>
<CloseIcon decorative={false} color="colorTextWeak" size="sizeIcon10" title="Close popover" />
<CloseIcon
element={`${element}_CLOSE_ICON`}
decorative={false}
color="colorTextWeak"
size="sizeIcon10"
title="Close popover"
/>
</Button>
</Box>
{children}
Expand All @@ -61,12 +68,11 @@ const Popover = React.forwardRef<HTMLDivElement, PopoverProps>(({children, ...pr
);
});

if (process.env.NODE_ENV === 'development') {
Popover.propTypes = {
'aria-label': PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
};
}
Popover.propTypes = {
'aria-label': PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
element: PropTypes.string,
};

Popover.displayName = 'Popover';
export {Popover};
26 changes: 16 additions & 10 deletions packages/paste-core/components/popover/src/PopoverButton.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import type {ButtonProps} from '@twilio-paste/button';
import type {BoxProps} from '@twilio-paste/box';
import {Button} from '@twilio-paste/button';
import {NonModalDialogDisclosurePrimitive} from '@twilio-paste/non-modal-dialog-primitive';
import {PopoverContext} from './PopoverContext';

export interface PopoverButtonProps extends ButtonProps {
export interface PopoverButtonProps extends ButtonProps, Pick<BoxProps, 'element'> {
id?: string;
children: React.ReactNode;
toggle?: () => void;
}

const PopoverButton = React.forwardRef<HTMLButtonElement, PopoverButtonProps>(
({children, ...popoverButtonProps}, ref) => {
({children, element = 'POPOVER_BUTTON', ...popoverButtonProps}, ref) => {
const popover = React.useContext(PopoverContext);
return (
<NonModalDialogDisclosurePrimitive {...(popover as any)} {...popoverButtonProps} as={Button} ref={ref}>
<NonModalDialogDisclosurePrimitive
element={element}
{...(popover as any)}
{...popoverButtonProps}
as={Button}
ref={ref}
>
{children}
</NonModalDialogDisclosurePrimitive>
);
}
);

if (process.env.NODE_ENV === 'development') {
PopoverButton.propTypes = {
id: PropTypes.string,
children: PropTypes.node.isRequired,
toggle: PropTypes.func,
};
}
PopoverButton.propTypes = {
id: PropTypes.string,
children: PropTypes.node.isRequired,
toggle: PropTypes.func,
element: PropTypes.string,
};

PopoverButton.displayName = 'PopoverButton';
export {PopoverButton};
34 changes: 34 additions & 0 deletions packages/paste-core/components/popover/stories/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Box} from '@twilio-paste/box';
import {Button} from '@twilio-paste/button';
import {Stack} from '@twilio-paste/stack';
import {Text} from '@twilio-paste/text';
import {CustomizationProvider} from '@twilio-paste/customization';
import {usePopoverState, Popover, PopoverContainer, PopoverButton} from '../src';

// eslint-disable-next-line import/no-default-export
Expand Down Expand Up @@ -98,3 +99,36 @@ export const StateHookExample: React.FC = () => {
</Box>
);
};

export const Customization: React.FC = () => {
return (
<CustomizationProvider
baseTheme="default"
elements={{
POPOVER: {
backgroundColor: 'colorBackgroundNeutralWeakest',
fontWeight: 'fontWeightBold',
},
POPOVER_BUTTON: {
backgroundColor: 'colorBackgroundBrandStronger',
},
POPOVER_CLOSE_BUTTON: {
backgroundColor: 'colorBackgroundBodyInverse',
borderRadius: 'borderRadius20',
},
POPOVER_CLOSE_ICON: {
color: 'colorTextInverse',
},
}}
>
<Box height="300px">
<PopoverContainer baseId="test-id" visible>
<PopoverButton variant="primary">Open popover</PopoverButton>
<Popover aria-label="Popover">
<Text as="span">This is the Twilio styled popover that you can use in all your applications.</Text>
</Popover>
</PopoverContainer>
</Box>
</CustomizationProvider>
);
};

0 comments on commit db9f966

Please sign in to comment.