UNPKG

@atlaskit/popup

Version:

A popup displays brief content in an overlay.

387 lines (386 loc) • 15.7 kB
import { type ComponentType, type CSSProperties, type Dispatch, type PropsWithChildren, type default as React, type ReactNode, type Ref, type SetStateAction } from 'react'; import { type StrictXCSSProp } from '@atlaskit/css'; import type { Modifier, Placement, PopperChildrenProps } from '@atlaskit/popper/main'; export interface TriggerAriaProps { /** * Identifies the popup element that the trigger controls. * Should match the `id` of the popup content for screen readers to understand the relationship. */ 'aria-controls'?: string; /** * Announces to assistive technology whether the popup is currently open or closed. */ 'aria-expanded': boolean; /** * Informs assistive technology that this element triggers a popup. */ 'aria-haspopup': boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'; } export interface TriggerProps extends TriggerAriaProps { /** * React ref that will be attached to the trigger element. */ ref: Ref<any>; 'data-ds--level'?: string; } export type PopupRef = HTMLDivElement | null; export type TriggerRef = HTMLElement | HTMLButtonElement | null; export interface ContentProps { /** * This will reposition the popup if any of the content has changed. * This is useful when positions change, and the popup wasn't aware. */ update: PopperChildrenProps['update']; /** * Passed through from the parent popup. */ isOpen: boolean; /** * Passed through from the parent popup. */ onClose?: BaseProps['onClose']; /** * Escape hatch to set the initial focus for a specific element, when the popup is opened. * * The specified element needs to be available in the DOM as soon as the popup is opened, otherwise it * will not be focused upon. This means it will not work for content that renders into the popup content * after it has already opened (e.g. async or lazy loaded content). */ setInitialFocusRef: Dispatch<SetStateAction<HTMLElement | null>>; } /** * Props passed to a custom `popupComponent`, which renders as the popup container in * place of the default surface. * * **Contract on the top-layer path (`platform-dst-top-layer`).** The container is * the first child of a `display: flex` popover host whose `& > *` rule sets * `flex-grow: 1`, `min-inline-size: 0` and `min-block-size: 0`. A size cap * (`shouldFitViewport`, or the always-on viewport backstop) reaches the content only * through that first child, so a container has to: * * 1. Render exactly one in-flow root. A second in-flow root becomes a second flex * item, laid out in a row beside the first. * 2. Keep that root in flow, not `position: absolute` or `fixed`. An out-of-flow root * is not a flex item, so `flex-grow` and cross-axis stretch never reach it. * 3. Give the root ITSELF a non-`visible` computed `overflow`, so it is the scroll * container. The `min-*-size: 0` reset caps the root's box whatever its * `overflow` is; a `visible` root's content spills out of a correctly-sized box * instead of scrolling. * 4. Leave the block size `auto`, so cross-axis stretch can size it. * 5. Not pin the inline size above the cap, and not set `flex-shrink: 0`. * 6. Put the `overflow` on the root, not on a descendant. `& > *` reaches one level. * * `shouldFitViewport` is forwarded on both paths, so the container can own its * `overflow` (several in-tree containers key an `overflow: auto` branch off it). * * `shouldRenderToParent` is NOT forwarded on the top-layer path, by decision: * everything renders in the top layer, so the container receives `undefined`, and a * `(!shouldRenderToParent || shouldFitViewport)` branch reads as "fitting applies", * which is the intended reading there. */ export interface PopupComponentProps { /** * Children passed through by the parent popup. */ children: ReactNode; /** * Placement passed through by the parent popup. */ 'data-placement': Placement; /** * Test ID passed through by the parent popup. */ 'data-testid'?: string; /** * ID passed through by the parent popup. */ id?: string; /** * Style that should be assigned to the root element. */ style: CSSProperties; xcss?: StrictXCSSProp<'padding' | 'paddingInline' | 'paddingBlock' | 'width', never>; /** * Tab index passed through by the parent popup. */ tabIndex: number | undefined; /** * Controls whether the popup is rendered inline within its parent component or in a portal at the document root. * `true` renders the popup in the DOM node closest to the trigger; focus is not trapped inside the element. * `false` renders the popup in React.Portal and focus is trapped inside the element. * The default is `false`. */ shouldRenderToParent?: boolean; /** * This fits the popup width to its parent's width. * When set to `true`, the trigger and popup elements will be wrapped in a `div` with `position: relative`. * The popup will be rendered as a sibling to the trigger element, and will be full width. * The default is `false`. */ shouldFitContainer?: boolean; /** * Determines if the popup will have a `max-width` and `max-height` set to * constrain it to the viewport. */ shouldFitViewport?: boolean; /** * The "default" appearance is used for standard popups. * The "UNSAFE_modal-below-sm" appearance makes the popup appear as a modal when the viewport is smaller than "sm". * The appearance should only be set to "UNSAFE_modal-below-sm" if the provided popup includes a close button. */ appearance?: 'default' | 'UNSAFE_modal-below-sm'; /** * Class name to apply to the popup container element. */ className?: string; /** * Boolean to indicate if the reference element is hidden. */ isReferenceHidden?: boolean; /** * Use this to set the accessibility role for the popup. * We strongly recommend using only `menu` or `dialog`. */ role?: string; } interface BaseProps { /** * The "default" appearance is used for standard popups. * The "UNSAFE_modal-below-sm" appearance makes the popup appear as a modal when the viewport is smaller than "sm". */ appearance?: 'default' | 'UNSAFE_modal-below-sm'; /** * Bounded style overrides. */ xcss?: StrictXCSSProp<'padding' | 'paddingInline' | 'paddingBlock' | 'paddingInlineStart' | 'paddingInlineEnd' | 'paddingBlockStart' | 'paddingBlockEnd' | 'width', never>; /** * Use this to either show or hide the popup. * When set to `false` the popup will not render anything to the DOM. */ isOpen: boolean; /** * Render props for content that is displayed inside the popup. */ content: (props: ContentProps) => React.ReactNode; /** * ID that is assigned to the popup container element. */ id?: string; /** * The distance the popup should be offset from the reference in the format of [along, away] (units in px). * The default is `[0, 8]`, which means the popup will be `8px` away from the edge of the reference specified * by the `placement` prop. * * @private * Note: when `platform-dst-top-layer` is enabled, both the `along` and `away` * values are passed through to the top-layer placement (via `fromLegacyPlacement`), * matching the legacy popper-js behaviour. */ offset?: [number, number]; /** * Placement of where the popup should be displayed relative to the trigger element. * The default is `"auto"`. */ placement?: Placement; /** * This is a list of backup placements for the popup to try. * When the preferred placement doesn't have enough space, * the modifier will test the ones provided in the list, and use the first suitable one. * If no fallback placements are suitable, it reverts back to the original placement. * * @private * @deprecated No-op when `platform-dst-top-layer` is enabled — CSS Anchor Positioning * generates fallbacks automatically via `position-try-fallbacks`. */ fallbackPlacements?: Placement[]; /** * The boundary element that the popup will check for overflow. * The default is `"clippingParents"` which are parent scroll containers, * but can be set to any element. * * @private * @deprecated No-op when `platform-dst-top-layer` is enabled — the viewport * is the natural boundary for top-layer elements. */ boundary?: 'clippingParents' | HTMLElement; /** * The root boundary that the popup will check for overflow. * The default is `"viewport"` but it can be set to `"document"`. * * @private * @deprecated No-op when `platform-dst-top-layer` is enabled — the viewport * is the natural boundary for top-layer elements. */ rootBoundary?: 'viewport' | 'document'; /** * Allows the popup to be placed on the opposite side of its trigger if it doesn't fit in the viewport. * The default is `true`. * * @private * @deprecated No-op when `platform-dst-top-layer` is enabled — CSS Anchor Positioning * handles flipping natively via `position-try-fallbacks`. */ shouldFlip?: boolean; /** * A `testId` prop is provided for specified elements, * which is a unique string that appears as a data attribute `data-testid` in the rendered code, * serving as a hook for automated tests. */ testId?: string; /** * Handler that is called when the popup wants to close itself. * This can happen when: * - the user clicks away from the popup * - the user presses the escape key * - the popup is closed programatically. In this case, the `event` argument will be `null`. * * You'll want to use this to set open state accordingly, and then pump it back into the `isOpen` prop. */ onClose?( /** * The event that triggered the close. * The argument value will be `null` when the popup is closed programatically and has no corresponding event. */ event: Event | React.MouseEvent | React.KeyboardEvent | null, currentLevel?: number | any): void; /** * The element that is shown when `isOpen` prop is `true`. * The result of the `content` prop will be placed as children here. * The default is an element with an elevation of `e200` with _no padding_. */ popupComponent?: ComponentType<PopupComponentProps> | React.ForwardRefExoticComponent<React.PropsWithoutRef<PopupComponentProps> & React.RefAttributes<HTMLDivElement>>; /** * This controls whether the popup takes focus when opening. * This changes the `popupComponent` component tabIndex to `null`. * The default is `true`. */ autoFocus?: boolean; /** * This controls if the event which handles clicks outside the popup is be bound with * `capture: true`. */ shouldUseCaptureOnOutsideClick?: boolean; /** * Allows consumers to ignore specific close events, for example when an external overlay * should be treated as part of the popup interaction. */ shouldIgnoreCloseEvent?: (event: Event | React.MouseEvent | React.KeyboardEvent) => boolean; /** * The root element where the popup should be rendered. * Defaults to `false`. * * @private * @deprecated No-op when `platform-dst-top-layer` is enabled — content always * renders in the browser's top layer. */ shouldRenderToParent?: boolean; /** * This fits the popup width to its parent's width. * When set to `true`, the trigger and popup elements will be wrapped in a `div` with `position: relative`. * The popup will be rendered as a sibling to the trigger element, and will be full width. * The default is `false`. */ shouldFitContainer?: boolean; /** * This makes the popup close on Tab key press. It will only work when `shouldRenderToParent` is `true`. * The default is `false`. * * @private * @deprecated No-op when `platform-dst-top-layer` is enabled — focus trapping * is role-based in the top-layer path. */ shouldDisableFocusLock?: boolean; /** * This determines whether the popup trigger will be focused when the popup content closes. * The default is `true`. */ shouldReturnFocus?: boolean; /** * This controls the positioning strategy to use. Can vary between `absolute` and `fixed`. * The default is `fixed`. * * @private * @deprecated No-op when `platform-dst-top-layer` is enabled — CSS Anchor Positioning * replaces Popper's positioning strategy. */ strategy?: 'absolute' | 'fixed'; /** * Use this to set the accessibility role for the popup. * We strongly recommend using only `menu` or `dialog`. * Must be used along with `label` or `titleId`. */ role?: string; /** * Refers to an `aria-label` attribute. Sets an accessible name for the popup to announce it to users of assistive technology. * Usage of either this, or the `titleId` attribute is strongly recommended. */ label?: string; /** * Id referenced by the popup `aria-labelledby` attribute. * Usage of either this, or the `label` attribute is strongly recommended. */ titleId?: string; /** * Additional modifiers and modifier overwrites. * for more details - https://popper.js.org/docs/v1/#modifiers */ modifiers?: Partial<Modifier<string, object>>[]; /** * Determines if the popup will have a `max-width` and `max-height` set to * constrain it to the viewport. */ shouldFitViewport?: boolean; } interface InternalPopupProps extends BaseProps { /** * Render props used to anchor the popup to your content. * Make this an interactive element, * such as an `@atlaskit/button` component. */ trigger: (props: TriggerProps) => React.ReactNode; /** * Z-index that the popup should be displayed in. * This is passed to the portal component. * The default is 400. * * @private * @deprecated No-op when `platform-dst-top-layer` is enabled — the browser's * top layer manages stacking without z-index. */ zIndex?: number; } type StandardPopupProps = InternalPopupProps & { shouldFitContainer?: false; }; type ShouldFitContainerPopupProps = InternalPopupProps & { shouldFitContainer: true; shouldRenderToParent?: true; strategy?: 'absolute'; }; export type PopupProps = StandardPopupProps | ShouldFitContainerPopupProps; export interface PopperWrapperProps extends BaseProps { zIndex?: number; triggerRef: TriggerRef; } export type CloseManagerHook = Pick<PopupProps, 'isOpen' | 'onClose' | 'shouldIgnoreCloseEvent'> & { popupRef: PopupRef; triggerRef: TriggerRef; shouldUseCaptureOnOutsideClick?: boolean; shouldCloseOnTab?: boolean; shouldDisableFocusTrap: boolean; shouldRenderToParent?: boolean; autoFocus: boolean; }; export type FocusManagerHook = { initialFocusRef: HTMLElement | null; popupRef: PopupRef; shouldCloseOnTab?: boolean; triggerRef: TriggerRef; autoFocus: boolean; shouldDisableFocusTrap: boolean; shouldReturnFocus: boolean; shouldRenderToParent?: boolean; }; export type RepositionOnUpdateProps = PropsWithChildren<{ update: PopperChildrenProps['update']; }>; export {};