@atlaskit/popup
Version:
A popup displays brief content in an overlay.
387 lines (386 loc) • 15.7 kB
TypeScript
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 {};