diff --git a/.changeset/witty-avocados-play.md b/.changeset/witty-avocados-play.md new file mode 100644 index 0000000000..d0dcac4850 --- /dev/null +++ b/.changeset/witty-avocados-play.md @@ -0,0 +1,11 @@ +--- +'@twilio-paste/popover': minor +'@twilio-paste/core': minor +--- + +[Popover] Add new props: + +- initialFocusRef: focuses a ref when the Popover opens +- width: sets the width of the Popover, up to size50. + +Update styles to align with new Paste Twilio theme diff --git a/packages/paste-core/components/popover/__tests__/index.spec.tsx b/packages/paste-core/components/popover/__tests__/index.spec.tsx index a446304501..ae5f829e59 100644 --- a/packages/paste-core/components/popover/__tests__/index.spec.tsx +++ b/packages/paste-core/components/popover/__tests__/index.spec.tsx @@ -5,7 +5,7 @@ import {Theme} from '@twilio-paste/theme'; import {CustomizationProvider} from '@twilio-paste/customization'; import {Text} from '@twilio-paste/text'; -import {PopoverTop, StateHookExample, BadgePopover} from '../stories/index.stories'; +import {PopoverTop, StateHookExample, BadgePopover, InitialFocus} from '../stories/index.stories'; import {Popover, PopoverContainer, PopoverButton} from '../src'; describe('Popover', () => { @@ -39,6 +39,38 @@ describe('Popover', () => { expect(renderedPopover.getAttribute('role')).toEqual('dialog'); }); + it('should focus the close button when the popover opens', async () => { + render( + + + + ); + + const renderedPopoverButton = screen.getByRole('button'); + + await waitFor(() => { + userEvent.click(renderedPopoverButton); + }); + + expect(document.activeElement).toEqual(screen.getByRole('button', {name: 'Close popover'})); + }); + + it('should focus the initialFocusRef when the popver opens', async () => { + render( + + + + ); + + const renderedPopoverButton = screen.getByRole('button'); + + await waitFor(() => { + userEvent.click(renderedPopoverButton); + }); + + expect(document.activeElement).toEqual(screen.getByRole('button', {name: 'Click me'})); + }); + it('should render a popover and show/hide on external button click', async () => { render( diff --git a/packages/paste-core/components/popover/package.json b/packages/paste-core/components/popover/package.json index eb303012f4..0157d9298f 100644 --- a/packages/paste-core/components/popover/package.json +++ b/packages/paste-core/components/popover/package.json @@ -3,7 +3,7 @@ "version": "10.0.2", "category": "interaction", "status": "production", - "description": "A Popover is a page overlay triggered by a click that displays additional interactive content.", + "description": "A Popover is a page overlay triggered by a button that displays additional interactive content.", "author": "Twilio Inc.", "license": "MIT", "main:dev": "src/index.tsx", diff --git a/packages/paste-core/components/popover/src/Popover.tsx b/packages/paste-core/components/popover/src/Popover.tsx index ce50c1abd9..ea51b1a5cc 100644 --- a/packages/paste-core/components/popover/src/Popover.tsx +++ b/packages/paste-core/components/popover/src/Popover.tsx @@ -7,20 +7,22 @@ import {CloseIcon} from '@twilio-paste/icons/esm/CloseIcon'; import {StyledBase} from '@twilio-paste/theme'; import {NonModalDialogPrimitive} from '@twilio-paste/non-modal-dialog-primitive'; import {ScreenReaderOnly} from '@twilio-paste/screen-reader-only'; +import type {ResponsiveValue} from '@twilio-paste/styling-library'; import {PopoverArrow} from './PopoverArrow'; import {PopoverContext} from './PopoverContext'; -const StyledPopover = React.forwardRef(({style, ...props}, ref) => { +const StyledPopover = React.forwardRef(({style, width, ...props}, ref) => { return ( (({style, ...pro StyledPopover.displayName = 'StyledPopover'; +type WidthOptions = 'size10' | 'size20' | 'size30' | 'size40' | 'size50'; + export interface PopoverProps extends Pick { 'aria-label': string; children: React.ReactNode; i18nDismissLabel?: string; + width?: ResponsiveValue; + initialFocusRef?: React.RefObject; } const Popover = React.forwardRef( - ({children, element = 'POPOVER', i18nDismissLabel = 'Close popover', ...props}, ref) => { + ({children, element = 'POPOVER', i18nDismissLabel = 'Close popover', initialFocusRef, ...props}, ref) => { const popover = React.useContext(PopoverContext); + + React.useEffect(() => { + if (popover.visible && initialFocusRef) { + initialFocusRef.current?.focus(); + } + }, [popover.visible, initialFocusRef]); + return ( {/* import Paste Theme Based Styles due to portal positioning. */} - - + + + @@ -55,7 +109,18 @@ export const SmallerSize = (): JSX.Element => { ); }; -export const WideContent = (): JSX.Element => { +export const ResponsiveWidth: StoryFn = () => { + return ( + + Open popover + + Responsive width Popover + + + ); +}; + +export const WideContent: React.FC = () => { const date1ID = useUID(); const time1ID = useUID(); const date2ID = useUID(); diff --git a/packages/paste-website/src/component-examples/PopoverExamples.ts b/packages/paste-website/src/component-examples/PopoverExamples.ts index f7c1e6acbc..a9079d1a9a 100644 --- a/packages/paste-website/src/component-examples/PopoverExamples.ts +++ b/packages/paste-website/src/component-examples/PopoverExamples.ts @@ -21,22 +21,163 @@ render( ) `.trim(); -export const rightExample = ` -const PopoverRightExample = () => { +export const interactiveContent = ` +const PopoverExample = () => { + const saveButtonRef = React.createRef(); + return ( - + Open popover - - - This is the Twilio styled popover that you can use in all your applications. - + + Update API key permissions + + It is really important to update your permissions. + + + + Full access + + + Restricted access + + + + + + + + + + + ); +}; + +render( + +)`.trim(); + +export const initialFocus = ` +const countryList = [ + {code: 'AD', label: 'Andorra', phone: '376'}, + {code: 'AE', label: 'United Arab Emirates', phone: '971'}, + {code: 'AF', label: 'Afghanistan', phone: '93'}, + {code: 'AG', label: 'Antigua and Barbuda', phone: '1-268'}, + {code: 'AI', label: 'Anguilla', phone: '1-264'}, + {code: 'AL', label: 'Albania', phone: '355'}, + {code: 'AM', label: 'Armenia', phone: '374'}, + {code: 'AO', label: 'Angola', phone: '244'}, + {code: 'AQ', label: 'Antarctica', phone: '672'}, + {code: 'AR', label: 'Argentina', phone: '54'}, + {code: 'AS', label: 'American Samoa', phone: '1-684'}, + {code: 'AT', label: 'Austria', phone: '44'}, + {code: 'BS', label: 'Bahamas', phone: '43'}, + {code: 'BH', label: 'Bahrain', phone: '48'}, + {code: 'BD', label: 'Bangladesh', phone: '50'}, + {code: 'BB', label: 'Barbados', phone: '52'}, + {code: 'BY', label: 'Belarus', phone: '112'}, + {code: 'BE', label: 'Belgium', phone: '56'}, + {code: 'BZ', label: 'Belize', phone: '84'}, + {code: 'BJ', label: 'Benin', phone: '204'}, + {code: 'BM', label: 'Bermuda', phone: '60'}, + {code: 'BT', label: 'Bhutan', phone: '64'}, + {code: 'BO', label: 'Bolivia', phone: '68'}, + {code: 'BW', label: 'Botswana', phone: '72'}, + {code: 'BR', label: 'Brazil', phone: '76'}, + {code: 'KH', label: 'Cambodia', phone: '116'}, + {code: 'CA', label: 'Canada', phone: '124'}, +]; + +const PopoverExample = () => { + const phoneInputRef = React.createRef(); + const seed = useUIDSeed(); + + return ( + + Open popover + +
+ + Edit phone number + + + + + + + + + + + + + +
); }; render( - + +)`.trim(); + +export const postionExample = ` +const PopoverPositionExample = () => { + return ( + + + Open top + + + This is the Twilio styled popover that you can use in all your applications. + + + + + Open right + + + This is the Twilio styled popover that you can use in all your applications. + + + + + Open bottom + + + This is the Twilio styled popover that you can use in all your applications. + + + + + Open left + + + This is the Twilio styled popover that you can use in all your applications. + + + + + ); +}; + +render( + ) `.trim(); @@ -44,7 +185,7 @@ export const StateHookPopoverExample = ` const StateHookExample = () => { const popover = usePopoverState({baseId: 'test-id'}); return ( - + Open popover @@ -57,7 +198,7 @@ const StateHookExample = () => { - +
); }; @@ -166,22 +307,107 @@ render( `.trim(); export const setWidthExample = ` +const countryList = [ + {code: 'AD', label: 'Andorra', phone: '376'}, + {code: 'AE', label: 'United Arab Emirates', phone: '971'}, + {code: 'AF', label: 'Afghanistan', phone: '93'}, + {code: 'AG', label: 'Antigua and Barbuda', phone: '1-268'}, + {code: 'AI', label: 'Anguilla', phone: '1-264'}, + {code: 'AL', label: 'Albania', phone: '355'}, + {code: 'AM', label: 'Armenia', phone: '374'}, + {code: 'AO', label: 'Angola', phone: '244'}, + {code: 'AQ', label: 'Antarctica', phone: '672'}, + {code: 'AR', label: 'Argentina', phone: '54'}, + {code: 'AS', label: 'American Samoa', phone: '1-684'}, + {code: 'AT', label: 'Austria', phone: '44'}, + {code: 'BS', label: 'Bahamas', phone: '43'}, + {code: 'BH', label: 'Bahrain', phone: '48'}, + {code: 'BD', label: 'Bangladesh', phone: '50'}, + {code: 'BB', label: 'Barbados', phone: '52'}, + {code: 'BY', label: 'Belarus', phone: '112'}, + {code: 'BE', label: 'Belgium', phone: '56'}, + {code: 'BZ', label: 'Belize', phone: '84'}, + {code: 'BJ', label: 'Benin', phone: '204'}, + {code: 'BM', label: 'Bermuda', phone: '60'}, + {code: 'BT', label: 'Bhutan', phone: '64'}, + {code: 'BO', label: 'Bolivia', phone: '68'}, + {code: 'BW', label: 'Botswana', phone: '72'}, + {code: 'BR', label: 'Brazil', phone: '76'}, + {code: 'KH', label: 'Cambodia', phone: '116'}, + {code: 'CA', label: 'Canada', phone: '124'}, +]; + const PopoverExample = () => { + const seed = useUIDSeed(); return ( - - ✊ Action - - - Heads up! - - - Black Lives Matter. We stand with the Black community - - - - - - + + + Auto width popover + +
+ + Edit phone number + + + + + + + + + + + + + +
+
+
+ + size40 popover + +
+ + Edit phone number + + + + + + + + + + + + + +
+
+
+
); }; diff --git a/packages/paste-website/src/pages/components/popover/index.mdx b/packages/paste-website/src/pages/components/popover/index.mdx index d9cf782825..d5d245d8be 100644 --- a/packages/paste-website/src/pages/components/popover/index.mdx +++ b/packages/paste-website/src/pages/components/popover/index.mdx @@ -5,33 +5,43 @@ export const meta = { slug: '/components/popover/', }; -import {Anchor} from '@twilio-paste/anchor'; -import {Heading} from '@twilio-paste/heading'; -import {Button} from '@twilio-paste/button'; -import {Separator} from '@twilio-paste/separator'; -import {Stack} from '@twilio-paste/stack'; -import {AspectRatio} from '@twilio-paste/aspect-ratio'; -import {Box} from '@twilio-paste/box'; import {usePopoverState, Popover, PopoverContainer, PopoverButton, PopoverBadgeButton} from '@twilio-paste/popover'; import Changelog from '@twilio-paste/popover/CHANGELOG.md'; +import packageJson from '@twilio-paste/popover/package.json'; +import {AspectRatio} from '@twilio-paste/aspect-ratio'; +import {Box} from '@twilio-paste/box'; +import {Button} from '@twilio-paste/button'; +import {ButtonGroup} from '@twilio-paste/button-group'; +import {CheckboxGroup, Checkbox} from '@twilio-paste/checkbox'; +import {Form, FormActions, FormControl} from '@twilio-paste/form'; +import {Heading} from '@twilio-paste/heading'; +import {Input} from '@twilio-paste/input'; +import {Label} from '@twilio-paste/label'; +import {Paragraph} from '@twilio-paste/paragraph'; +import {Select, Option} from '@twilio-paste/select'; +import {Separator} from '@twilio-paste/separator'; import {Text} from '@twilio-paste/text'; +import {useUIDSeed} from '@twilio-paste/uid-library'; import {PlusIcon} from '@twilio-paste/icons/esm/PlusIcon'; import {NewIcon} from '@twilio-paste/icons/esm/NewIcon'; import {WarningIcon} from '@twilio-paste/icons/esm/WarningIcon'; import {InformationIcon} from '@twilio-paste/icons/esm/InformationIcon'; + import {SidebarCategoryRoutes} from '../../../constants'; import { defaultExample, - rightExample, + postionExample, StateHookPopoverExample, badgeExample, buttonVariantsExample, setWidthExample, i18nExample, + interactiveContent, + initialFocus, } from '../../../component-examples/PopoverExamples'; -import packageJson from '@twilio-paste/popover/package.json'; import DefaultLayout from '../../../layouts/DefaultLayout'; import {getFeature, getNavigationData} from '../../../utils/api'; +import {DoDont, Do, Dont} from '../../../components/DoDont'; export default DefaultLayout; @@ -83,16 +93,47 @@ export const getStaticProps = async () => { ### About Popover -The Popover component is a non-modal dialog that is commonly used for displaying additional rich content -above your UI. +The Popover component can be used for displaying supplemental or contextual interactive content over your UI. It is built on top of our [Non-modal Dialog primitive](/primitives/non-modal-dialog-primitive/#about-non-modal-dialog-primitive). A Popover can contain a wide variety of information and interactive content and does not block the user from interacting with the rest of the page. + +Popovers remain actively open until a user dismisses it in one of the following ways: + +- Presses the Esc key +- Presses the close "x" button in the Popover +- Presses a "Cancel" button in in the Popover +- Clicks outside of the Popover +- Performs another action that closes the Popover -### Popover compared to Tooltip +### Accessibility + +Popovers and non-modal dialogs follow these accessibility guidelines: + +- A Popover must be triggered by an explicit user action, e.g., clicking a button. +- It must contain at least one focusable element, usually the close button. +- There should be a close button so screen readers have a specific close action to target. +- When a Popover is shown, focus is placed inside it, as it is a focus trap. A user cannot tab outside of the Popover until it is closed. After the Popover is closed, focus is placed back on the element that triggered it. Focus doesn’t return to the page until the user closes the Popover. +- The Popover should be labeled: + - Setting a value for the aria-labelledby property that refers to a visible Heading within the Popover . + - Providing a label directly specifying by aria-label attribute on the Popover. + +### Popover vs. Tooltip + +#### Differences + +1. Popovers allow for much more dynamic content than Tooltips. They can display additional content that can contain interactive elements, like Buttons or Anchors. It is not possible to add interactivity inside a Tooltip. This is because a user cannot focus within a Tooltip; the content of the Tooltip is only visible when the trigger is hovered or focused. +2. A Popover is opened on Click or Enter. A Tooltip is opened on Hover or Focus. + +#### Usage + +- If you want to provide a concise description of how or what an interactive element in your UI does, use a Tooltip. +- For displaying rich content or interactive elements, such as headings, anchors, or buttons, use a Popover. + +For additional information on Tooltips, [check out Heydon Pickering’s Inclusive Components guidance](https://inclusive-components.design/tooltips-toggletips/).