Skip to content

Commit 5f5790d

Browse files
authored
feat: update Popover styles (#3032)
1 parent 3c89fd8 commit 5f5790d

File tree

7 files changed

+612
-128
lines changed

7 files changed

+612
-128
lines changed

.changeset/witty-avocados-play.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@twilio-paste/popover': minor
3+
'@twilio-paste/core': minor
4+
---
5+
6+
[Popover] Add new props:
7+
8+
- initialFocusRef: focuses a ref when the Popover opens
9+
- width: sets the width of the Popover, up to size50.
10+
11+
Update styles to align with new Paste Twilio theme

packages/paste-core/components/popover/__tests__/index.spec.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {Theme} from '@twilio-paste/theme';
55
import {CustomizationProvider} from '@twilio-paste/customization';
66
import {Text} from '@twilio-paste/text';
77

8-
import {PopoverTop, StateHookExample, BadgePopover} from '../stories/index.stories';
8+
import {PopoverTop, StateHookExample, BadgePopover, InitialFocus} from '../stories/index.stories';
99
import {Popover, PopoverContainer, PopoverButton} from '../src';
1010

1111
describe('Popover', () => {
@@ -39,6 +39,38 @@ describe('Popover', () => {
3939
expect(renderedPopover.getAttribute('role')).toEqual('dialog');
4040
});
4141

42+
it('should focus the close button when the popover opens', async () => {
43+
render(
44+
<Theme.Provider theme="default">
45+
<PopoverTop />
46+
</Theme.Provider>
47+
);
48+
49+
const renderedPopoverButton = screen.getByRole('button');
50+
51+
await waitFor(() => {
52+
userEvent.click(renderedPopoverButton);
53+
});
54+
55+
expect(document.activeElement).toEqual(screen.getByRole('button', {name: 'Close popover'}));
56+
});
57+
58+
it('should focus the initialFocusRef when the popver opens', async () => {
59+
render(
60+
<Theme.Provider theme="default">
61+
<InitialFocus />
62+
</Theme.Provider>
63+
);
64+
65+
const renderedPopoverButton = screen.getByRole('button');
66+
67+
await waitFor(() => {
68+
userEvent.click(renderedPopoverButton);
69+
});
70+
71+
expect(document.activeElement).toEqual(screen.getByRole('button', {name: 'Click me'}));
72+
});
73+
4274
it('should render a popover and show/hide on external button click', async () => {
4375
render(
4476
<Theme.Provider theme="default">

packages/paste-core/components/popover/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "10.0.2",
44
"category": "interaction",
55
"status": "production",
6-
"description": "A Popover is a page overlay triggered by a click that displays additional interactive content.",
6+
"description": "A Popover is a page overlay triggered by a button that displays additional interactive content.",
77
"author": "Twilio Inc.",
88
"license": "MIT",
99
"main:dev": "src/index.tsx",

packages/paste-core/components/popover/src/Popover.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,22 @@ import {CloseIcon} from '@twilio-paste/icons/esm/CloseIcon';
77
import {StyledBase} from '@twilio-paste/theme';
88
import {NonModalDialogPrimitive} from '@twilio-paste/non-modal-dialog-primitive';
99
import {ScreenReaderOnly} from '@twilio-paste/screen-reader-only';
10+
import type {ResponsiveValue} from '@twilio-paste/styling-library';
1011

1112
import {PopoverArrow} from './PopoverArrow';
1213
import {PopoverContext} from './PopoverContext';
1314

14-
const StyledPopover = React.forwardRef<HTMLDivElement, BoxProps>(({style, ...props}, ref) => {
15+
const StyledPopover = React.forwardRef<HTMLDivElement, BoxProps>(({style, width, ...props}, ref) => {
1516
return (
1617
<Box
1718
{...safelySpreadBoxProps(props)}
19+
width={width}
1820
backgroundColor="colorBackgroundBody"
1921
borderStyle="solid"
2022
borderWidth="borderWidth10"
2123
borderColor="colorBorderWeaker"
22-
borderRadius="borderRadius20"
23-
boxShadow="shadowCard"
24+
borderRadius="borderRadius30"
25+
boxShadow="shadowLow"
2426
maxWidth="size50"
2527
zIndex="zIndex80"
2628
_focus={{outline: 'none'}}
@@ -32,22 +34,33 @@ const StyledPopover = React.forwardRef<HTMLDivElement, BoxProps>(({style, ...pro
3234

3335
StyledPopover.displayName = 'StyledPopover';
3436

37+
type WidthOptions = 'size10' | 'size20' | 'size30' | 'size40' | 'size50';
38+
3539
export interface PopoverProps extends Pick<BoxProps, 'element'> {
3640
'aria-label': string;
3741
children: React.ReactNode;
3842
i18nDismissLabel?: string;
43+
width?: ResponsiveValue<WidthOptions>;
44+
initialFocusRef?: React.RefObject<any>;
3945
}
4046

4147
const Popover = React.forwardRef<HTMLDivElement, PopoverProps>(
42-
({children, element = 'POPOVER', i18nDismissLabel = 'Close popover', ...props}, ref) => {
48+
({children, element = 'POPOVER', i18nDismissLabel = 'Close popover', initialFocusRef, ...props}, ref) => {
4349
const popover = React.useContext(PopoverContext);
50+
51+
React.useEffect(() => {
52+
if (popover.visible && initialFocusRef) {
53+
initialFocusRef.current?.focus();
54+
}
55+
}, [popover.visible, initialFocusRef]);
56+
4457
return (
4558
<NonModalDialogPrimitive {...(popover as any)} {...props} as={StyledPopover} ref={ref} preventBodyScroll={false}>
4659
{/* import Paste Theme Based Styles due to portal positioning. */}
4760
<StyledBase>
4861
<PopoverArrow {...(popover as any)} />
49-
<Box element={element} paddingX="space80" paddingY="space70">
50-
<Box position="absolute" right={8} top={8}>
62+
<Box element={element} padding="space90">
63+
<Box position="absolute" right={16} top={16}>
5164
<Button
5265
element={`${element}_CLOSE_BUTTON`}
5366
variant="secondary_icon"
@@ -73,6 +86,8 @@ Popover.propTypes = {
7386
children: PropTypes.node.isRequired,
7487
element: PropTypes.string,
7588
i18nDismissLabel: PropTypes.string,
89+
initialFocusRef: PropTypes.object as any,
90+
width: PropTypes.oneOf(['size10', 'size20', 'size30', 'size40', 'size50'] as WidthOptions[]),
7691
};
7792

7893
Popover.displayName = 'Popover';

packages/paste-core/components/popover/stories/index.stories.tsx

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,88 @@ export const Default = (): JSX.Element => {
3939
);
4040
};
4141

42-
export const SmallerSize = (): JSX.Element => {
42+
export const Sizes: React.FC = () => {
43+
return (
44+
<Box height="300px" display="grid" gridTemplateColumns="1fr 1fr" gridTemplateRows="auto auto auto">
45+
<Box>
46+
<PopoverContainer baseId={useUID()} visible>
47+
<PopoverButton variant="primary">Open popover</PopoverButton>
48+
<Popover aria-label="Popover" width="size50">
49+
<Text as="span">width is &apos;size50&apos;</Text>
50+
</Popover>
51+
</PopoverContainer>
52+
</Box>
53+
<Box>
54+
<PopoverContainer baseId={useUID()} visible>
55+
<PopoverButton variant="primary">Open popover</PopoverButton>
56+
<Popover aria-label="Popover" width="size40">
57+
<Text as="span">width is &apos;size40&apos;</Text>
58+
</Popover>
59+
</PopoverContainer>
60+
</Box>
61+
<Box>
62+
<PopoverContainer baseId={useUID()} visible>
63+
<PopoverButton variant="primary">Open popover</PopoverButton>
64+
<Popover aria-label="Popover" width="size30">
65+
<Text as="span">width is &apos;size30&apos;</Text>
66+
</Popover>
67+
</PopoverContainer>
68+
</Box>
69+
<Box>
70+
<PopoverContainer baseId={useUID()} visible>
71+
<PopoverButton variant="primary">Open popover</PopoverButton>
72+
<Popover aria-label="Popover" width="size20">
73+
<Text as="span">width is &apos;size20&apos;</Text>
74+
</Popover>
75+
</PopoverContainer>
76+
</Box>
77+
<Box>
78+
<PopoverContainer baseId={useUID()} visible>
79+
<PopoverButton variant="primary">Open popover</PopoverButton>
80+
<Popover aria-label="Popover" width="size10">
81+
<Text as="span">width is &apos;size10&apos;</Text>
82+
</Popover>
83+
</PopoverContainer>
84+
</Box>
85+
</Box>
86+
);
87+
};
88+
89+
export const InitialFocus: React.FC = () => {
4390
const uniqueBaseID = useUID();
91+
const buttonRef = React.createRef<HTMLButtonElement>();
92+
4493
return (
4594
<Box height="300px">
46-
<PopoverContainer baseId={uniqueBaseID} visible>
95+
<PopoverContainer baseId={uniqueBaseID}>
4796
<PopoverButton variant="primary">Open popover</PopoverButton>
48-
<Popover aria-label="Popover">
49-
<Box width="size30">
97+
<Popover aria-label="Popover" initialFocusRef={buttonRef}>
98+
<Box display="flex" flexDirection="column" rowGap="space70">
5099
<Text as="span">This is the Twilio styled popover that you can use in all your applications.</Text>
100+
<Box>
101+
<Button variant="primary" size="small" ref={buttonRef}>
102+
Click me
103+
</Button>
104+
</Box>
51105
</Box>
52106
</Popover>
53107
</PopoverContainer>
54108
</Box>
55109
);
56110
};
57111

58-
export const WideContent = (): JSX.Element => {
112+
export const ResponsiveWidth: StoryFn = () => {
113+
return (
114+
<PopoverContainer baseId={useUID()} visible>
115+
<PopoverButton variant="primary">Open popover</PopoverButton>
116+
<Popover aria-label="Popover" width={['size20', 'size40']}>
117+
<Text as="span">Responsive width Popover</Text>
118+
</Popover>
119+
</PopoverContainer>
120+
);
121+
};
122+
123+
export const WideContent: React.FC = () => {
59124
const date1ID = useUID();
60125
const time1ID = useUID();
61126
const date2ID = useUID();

0 commit comments

Comments
 (0)