diff --git a/.changeset/chat-input-bar.md b/.changeset/chat-input-bar.md new file mode 100644 index 0000000000..3c6a027a5a --- /dev/null +++ b/.changeset/chat-input-bar.md @@ -0,0 +1,10 @@ +--- +'@lg-chat/input-bar': major +--- + +[LG-4121](https://jira.mongodb.org/browse/LG-4121): `InputBar` renders results menu in top layer using popover API. As a result, the following props are deprecated and removed: +- `portalClassName` +- `portalContainer` +- `portalRef` +- `scrollContainer` +- `usePortal` diff --git a/.changeset/chip.md b/.changeset/chip.md new file mode 100644 index 0000000000..6b675aac4a --- /dev/null +++ b/.changeset/chip.md @@ -0,0 +1,17 @@ +--- +'@leafygreen-ui/chip': major +--- + +[LG-4121](https://jira.mongodb.org/browse/LG-4121): Removes `popoverZIndex` prop because the `InlineDefinition` component instance will now render in the top layer + +#### Migration guide + +##### Old +```js + +``` + +##### New +```js + +``` diff --git a/.changeset/cli.md b/.changeset/cli.md new file mode 100644 index 0000000000..ddc73f0dc0 --- /dev/null +++ b/.changeset/cli.md @@ -0,0 +1,5 @@ +--- +'@lg-tools/cli': minor +--- + +Adds `--packages` flag to `lg codemod` command. Passing in this flag will specify which package names should be filtered for in a given codemod. diff --git a/.changeset/code.md b/.changeset/code.md new file mode 100644 index 0000000000..3b86a08e18 --- /dev/null +++ b/.changeset/code.md @@ -0,0 +1,26 @@ +--- +'@leafygreen-ui/code': major +--- + +[LG-4121](https://jira.mongodb.org/browse/LG-4121): `Code` renders the copy button tooltip and language selector in the top layer using popover API. As a result, the following props are removed: +- `popoverZIndex` +- `portalClassName` +- `portalContainer` +- `scrollContainer` +- `usePortal` + +#### Migration guide + +Use [popover-v12 codemod](https://github.com/mongodb/leafygreen-ui/tree/main/tools/codemods#popover-v12) for migration assistance. + +##### Old +```js + + +``` + +##### New +```js + + +``` diff --git a/.changeset/codemods.md b/.changeset/codemods.md new file mode 100644 index 0000000000..65651c2c78 --- /dev/null +++ b/.changeset/codemods.md @@ -0,0 +1,42 @@ +--- +'@lg-tools/codemods': minor +--- + +[LG-4525](https://jira.mongodb.org/browse/LG-4525) Adds `popover-v12` codemod which can be used to refactor popover component instances. Users can filter for specific packages using the `--packages` flag. + +This codemod does the following: + +1. Adds an explicit `usePortal={true}` declaration if left undefined and consolidates the `usePortal` and `renderMode` props into a single `renderMode` prop for components in the following packages: + +- `@leafygreen-ui/combobox` +- `@leafygreen-ui/menu` +- `@leafygreen-ui/popover` +- `@leafygreen-ui/select` +- `@leafygreen-ui/split-button` +- `@leafygreen-ui/tooltip` + +2. Removes `popoverZIndex`, `portalClassName`, `portalContainer`, `portalRef`, `scrollContainer`, and `usePortal` props from the following components: + +- `@leafygreen-ui/info-sprinkle` +- `@leafygreen-ui/inline-definition` +- `@leafygreen-ui/number-input` + +3. Removes `popoverZIndex`, `portalClassName`, `portalContainer`, `portalRef`, and `scrollContainer` props from the following components: + +- `@leafygreen-ui/date-picker` +- `@leafygreen-ui/guide-cue` + +4. Removes `popoverZIndex`, `portalClassName`, `portalContainer`, `scrollContainer`, and `usePortal` props from `Code` component in the `@leafygreen-ui/code` package + +5. Removes `portalClassName`, `portalContainer`, `portalRef`, `scrollContainer`, and `usePortal` props from `SearchInput` component in the `@leafygreen-ui/search-input` package + +6. Removes `shouldTooltipUsePortal` prop from `Copyable` component in the `@leafygreen-ui/copyable` package + +7. Replaces `justify="fit"` prop value with `justify="middle"` for components in the following packages: + +- `@leafygreen-ui/date-picker` +- `@leafygreen-ui/info-sprinkle` +- `@leafygreen-ui/inline-definition` +- `@leafygreen-ui/menu` +- `@leafygreen-ui/popover` +- `@leafygreen-ui/tooltip` diff --git a/.changeset/combobox.md b/.changeset/combobox.md new file mode 100644 index 0000000000..84e23684d8 --- /dev/null +++ b/.changeset/combobox.md @@ -0,0 +1,23 @@ +--- +'@leafygreen-ui/combobox': major +--- + +[LG-4121](https://jira.mongodb.org/browse/LG-4121): Replaces `usePortal` prop with `renderMode` prop. `renderMode="inline"` and `renderMode="portal"` are deprecated, and all popover elements should migrate to using the top layer. The old default was `usePortal=true`, and the new default is `renderMode="top-layer"`. + +See [@leafygreen-ui/popover package 12.0.0 changelog](https://github.com/mongodb/leafygreen-ui/blob/main/packages/popover/CHANGELOG.md#1200) for more info. + +#### Migration guide + +Use [popover-v12 codemod](https://github.com/mongodb/leafygreen-ui/tree/main/tools/codemods#popover-v12) for migration assistance. + +##### Old +```js + + +``` + +##### New +```js + + +``` diff --git a/.changeset/copyable.md b/.changeset/copyable.md new file mode 100644 index 0000000000..542d0c8a41 --- /dev/null +++ b/.changeset/copyable.md @@ -0,0 +1,21 @@ +--- +'@leafygreen-ui/copyable': major +--- + +[LG-4121](https://jira.mongodb.org/browse/LG-4121): `Copyable` renders tooltip in the top layer using popover API. As a result, the `shouldTooltipUsePortal` prop is removed + +#### Migration guide + +Use [popover-v12 codemod](https://github.com/mongodb/leafygreen-ui/tree/main/tools/codemods#popover-v12) for migration assistance. + +##### Old +```js + + +``` + +##### New +```js + + +``` diff --git a/.changeset/date-picker.md b/.changeset/date-picker.md new file mode 100644 index 0000000000..67a596bcab --- /dev/null +++ b/.changeset/date-picker.md @@ -0,0 +1,30 @@ +--- +'@leafygreen-ui/date-picker': major +--- + +[LG-4121](https://jira.mongodb.org/browse/LG-4121): `DatePicker` renders menu, month selector, and year selector in top layer using popover API. As a result, the following props are deprecated and removed: +- `popoverZIndex` +- `portalClassName` +- `portalContainer` +- `portalRef` +- `scrollContainer` + +Additional changes include: +- Deprecates and removes `justify="fit"`. Instead, use `justify="middle"` +- Removes unused `contentClassName` prop + +#### Migration guide + +Use [popover-v12 codemod](https://github.com/mongodb/leafygreen-ui/tree/main/tools/codemods#popover-v12) for migration assistance. + +##### Old +```js + + +``` + +##### New +```js + + +``` diff --git a/.changeset/guide-cue.md b/.changeset/guide-cue.md new file mode 100644 index 0000000000..de58a04927 --- /dev/null +++ b/.changeset/guide-cue.md @@ -0,0 +1,26 @@ +--- +'@leafygreen-ui/guide-cue': major +--- + +[LG-4121](https://jira.mongodb.org/browse/LG-4121): `GuideCue` renders beacon and tooltip using popover API. As a result, the following props are removed: +- `popoverZIndex` +- `portalClassName` +- `portalContainer` +- `portalRef` +- `scrollContainer` + +#### Migration guide + +Use [popover-v12 codemod](https://github.com/mongodb/leafygreen-ui/tree/main/tools/codemods#popover-v12) for migration assistance. + +##### Old +```js + + +``` + +##### New +```js + + +``` diff --git a/.changeset/hooks.md b/.changeset/hooks.md new file mode 100644 index 0000000000..00024ecfb8 --- /dev/null +++ b/.changeset/hooks.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/hooks': minor +--- + +Add `useMergeRefs` hook for merging array of refs into a single memoized callback ref or `null` diff --git a/.changeset/info-sprinkle.md b/.changeset/info-sprinkle.md new file mode 100644 index 0000000000..a92a6ce9d4 --- /dev/null +++ b/.changeset/info-sprinkle.md @@ -0,0 +1,31 @@ +--- +'@leafygreen-ui/info-sprinkle': major +--- + +[LG-4121](https://jira.mongodb.org/browse/LG-4121): `InfoSprinkle` renders tooltip in the top layer using popover API. As a result, the following props are removed: +- `popoverZIndex` +- `portalClassName` +- `portalContainer` +- `portalRef` +- `scrollContainer` +- `usePortal` + +Additional changes include: +- Deprecates and removes `justify="fit"`. Instead, use `justify="middle"` +- Opens tooltip immediately on hover instead of default 500ms delay + +#### Migration guide + +Use [popover-v12 codemod](https://github.com/mongodb/leafygreen-ui/tree/main/tools/codemods#popover-v12) for migration assistance. + +##### Old +```js + + +``` + +##### New +```js + + +``` diff --git a/.changeset/inline-definition.md b/.changeset/inline-definition.md new file mode 100644 index 0000000000..df57fd05bb --- /dev/null +++ b/.changeset/inline-definition.md @@ -0,0 +1,32 @@ +--- +'@leafygreen-ui/inline-definition': major +--- + +[LG-4121](https://jira.mongodb.org/browse/LG-4121): `InlineDefinition` renders tooltip in the top layer using popover API. As a result, the following props are removed: +- `popoverZIndex` +- `portalClassName` +- `portalContainer` +- `portalRef` +- `scrollContainer` +- `usePortal` + +Additional changes include: +- Deprecates and removes `justify="fit"`. Instead, use `justify="middle"` +- Opens tooltip immediately on hover instead of default 500ms delay +- Reorganizes file structure + +#### Migration guide + +Use [popover-v12 codemod](https://github.com/mongodb/leafygreen-ui/tree/main/tools/codemods#popover-v12) for migration assistance. + +##### Old +```js + + +``` + +##### New +```js + + +``` diff --git a/.changeset/lg-provider.md b/.changeset/lg-provider.md new file mode 100644 index 0000000000..d2106eb9d6 --- /dev/null +++ b/.changeset/lg-provider.md @@ -0,0 +1,7 @@ +--- +'@leafygreen-ui/leafygreen-provider': minor +--- + +[LG-4446](https://jira.mongodb.org/browse/LG-4446): Adds `PopoverPropsContext` to pass props to a deeply nested popover element + +Additionally exposes a `forceUseTopLayer` prop in the `LeafyGreenProvider` which can be used to test interactions with all LG popover elements forcibly set to `renderMode="top-layer"`. This can help pressure test for any regressions to more confidently and safely migrate. However, this should only be used when all LG dependencies are relying on v12+ of `@leafygreen-ui/popover`. diff --git a/.changeset/menu.md b/.changeset/menu.md new file mode 100644 index 0000000000..68b1a325d2 --- /dev/null +++ b/.changeset/menu.md @@ -0,0 +1,26 @@ +--- +'@leafygreen-ui/menu': major +--- + +[LG-4121](https://jira.mongodb.org/browse/LG-4121): Replaces `usePortal` prop with `renderMode` prop. `renderMode="inline"` and `renderMode="portal"` are deprecated, and all popover elements should migrate to using the top layer. The old default was `usePortal=true`, and the new default is `renderMode="top-layer"`. + +See [@leafygreen-ui/popover package 12.0.0 changelog](https://github.com/mongodb/leafygreen-ui/blob/main/packages/popover/CHANGELOG.md#1200) for more info. + +Additional changes include: +- Deprecates and removes `justify="fit"`. Instead, use `justify="middle"` + +#### Migration guide + +Use [popover-v12 codemod](https://github.com/mongodb/leafygreen-ui/tree/main/tools/codemods#popover-v12) for migration assistance. + +##### Old +```js + + +``` + +##### New +```js + + +``` diff --git a/.changeset/number-input.md b/.changeset/number-input.md new file mode 100644 index 0000000000..d7341019f3 --- /dev/null +++ b/.changeset/number-input.md @@ -0,0 +1,30 @@ +--- +'@leafygreen-ui/number-input': major +--- + +[LG-4121](https://jira.mongodb.org/browse/LG-4121): `NumberInput` renders unit selector and tooltip in the top layer using popover API. As a result, the following props are removed: +- `popoverZIndex` +- `portalClassName` +- `portalContainer` +- `portalRef` +- `scrollContainer` +- `usePortal` + +Additional changes include: +- Opens tooltip immediately on hover instead of default 500ms delay + +#### Migration guide + +Use [popover-v12 codemod](https://github.com/mongodb/leafygreen-ui/tree/main/tools/codemods#popover-v12) for migration assistance. + +##### Old +```js + + +``` + +##### New +```js + + +``` diff --git a/.changeset/odd-llamas-work.md b/.changeset/odd-llamas-work.md new file mode 100644 index 0000000000..fdcfbb9e0c --- /dev/null +++ b/.changeset/odd-llamas-work.md @@ -0,0 +1,5 @@ +--- +'@lg-tools/validate': minor +--- + +Updates `ignoreFilePatterns` and `depcheckOptions.ignorePatterns` to exclude validating dependencies in `*.input.*` and `*.output.*` files in `tools/codemods` directory diff --git a/.changeset/pagination.md b/.changeset/pagination.md new file mode 100644 index 0000000000..3fd591786a --- /dev/null +++ b/.changeset/pagination.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/pagination': major +--- + +[LG-4121](https://jira.mongodb.org/browse/LG-4121): `Pagination` renders page selectors in the top layer using popover API diff --git a/.changeset/pipeline.md b/.changeset/pipeline.md new file mode 100644 index 0000000000..dced02c2da --- /dev/null +++ b/.changeset/pipeline.md @@ -0,0 +1,7 @@ +--- +'@leafygreen-ui/pipeline': major +--- + +[LG-4121](https://jira.mongodb.org/browse/LG-4121): `Pipeline` renders tooltip in the top layer using popover API + +Additionally, the tooltip opens immediately on hover instead of default 500ms delay diff --git a/.changeset/popover.md b/.changeset/popover.md new file mode 100644 index 0000000000..b9682641b8 --- /dev/null +++ b/.changeset/popover.md @@ -0,0 +1,69 @@ +--- +'@leafygreen-ui/popover': major +--- + +[LG-4445](https://jira.mongodb.org/browse/LG-4445): Replaces `usePortal` prop with `renderMode` prop with values of `'inline'`, `'portal'`, and `'top-layer'`. `renderMode="inline"` and `renderMode="portal"` are deprecated, and all popover elements should migrate to using the top layer. The old default was `usePortal=true`, and the new default is `renderMode="top-layer"`. + - When `renderMode="top-layer"` or `renderMode` is `undefined`, the popover element will render in the top layer using the [popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) + - Adds `dismissMode` prop to control dismissal behavior of the popover element. [Read more about the popover attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/popover) + - Adds `onToggle` prop to run a callback function when the visibility of a popover element rendered in the top layer is toggled + - When `renderMode="inline"`, the popover element will render inline in the DOM where it's written + - When `renderMode="portal"`, the popover element will portal into a new div appended to the body. Alternatively, it can be portaled into a provided `portalContainer` element + +[LG-4446](https://jira.mongodb.org/browse/LG-4446): The `PopoverPropsProvider` from the `@leafygreen-ui/leafygreen-provider` package can be used to pass props to a deeply nested popover element. It will read `PopoverPropsContext` values if an explicit prop is not defined in the popover component instance. This applies for the following props: + - `dismissMode` + - `onEnter` + - `onEntering` + - `onEntered` + - `onExit` + - `onExiting` + - `onExited` + - `onToggle` + - `popoverZIndex` + - `portalClassName` + - `portalContainer` + - `portalRef` + - `renderMode` + - `scrollContainer` + - `spacing` + +Additional changes include: +- Adds and exports `getPopoverRenderModeProps` util to pick popover props based on given `renderMode` value +- Deprecates and removes `justify="fit"`. Instead, use `justify="middle"` +- Removes unused `contentClassName` prop +- Updates default value of `spacing` prop from 10px to 4px +- Replaces internal position utils with `@floating-ui/react` + +#### Migration guide + +Use [popover-v12 codemod](https://github.com/mongodb/leafygreen-ui/tree/main/tools/codemods#popover-v12) for migration assistance. + +##### Old +```js + + + +``` + +##### New +```js + + + +``` + +##### Globally render popover elements in top layer +After running the codemod and addressing manual updates, the new `forceUseTopLayer` prop in the `LeafyGreenProvider` can be used to test interactions with all LG popover elements forcibly set to `renderMode="top-layer"`. This can help pressure test for any regressions to more confidently and safely migrate. + +```js +import { Combobox } from '@leafygreen-ui/combobox'; +import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; +import Popover from '@leafygreen-ui/popover'; +import { Select } from '@leafygreen-ui/select'; + +{/* all LG popover elements will render in top layer */} + + + + + +> & { customActionButtons?: Array; showCustomActionButtons?: boolean; className?: string; -} & PopoverProps; +}; function Panel({ language, @@ -38,23 +37,10 @@ function Panel({ showCopyButton, customActionButtons, showCustomActionButtons, - usePortal, - portalClassName, - portalContainer, - scrollContainer, - popoverZIndex, className, }: PanelProps) { const { theme } = useDarkMode(); - const popoverProps = { - popoverZIndex, - usePortal, - portalClassName, - portalContainer, - scrollContainer, - } as const; - return (
)} diff --git a/packages/code/src/types.ts b/packages/code/src/types.ts index 650c1d10ae..a56164b2c9 100644 --- a/packages/code/src/types.ts +++ b/packages/code/src/types.ts @@ -52,36 +52,6 @@ export interface SyntaxProps extends HTMLElementProps<'code'> { highlightLines?: LineHighlightingDefinition; } -export interface PopoverProps { - /** - * Specifies that the popover content should be rendered at the end of the DOM, - * rather than in the DOM tree. - * - * default: `true` - */ - usePortal?: boolean; - - /** - * When usePortal is `true`, specifies a class name to apply to the root element of the portal. - */ - portalClassName?: string; - - /** - * When usePortal is `true`, specifies an element to portal within. The default behavior is to generate a div at the end of the document to render within. - */ - portalContainer?: HTMLElement | null; - - /** - * When usePortal is `true`, specifies the scrollable element to position relative to. - */ - scrollContainer?: HTMLElement | null; - - /** - * Number that controls the z-index of the popover element directly. - */ - popoverZIndex?: number; -} - export type CodeProps = Omit< SyntaxProps, 'onCopy' | 'language' | 'onChange' @@ -160,8 +130,7 @@ export type CodeProps = Omit< */ onChange: (arg0: LanguageOption) => void; } - ) & - PopoverProps; + ); export interface LanguageOption { displayName: string; @@ -169,7 +138,7 @@ export interface LanguageOption { image?: React.ReactElement; } -export interface LanguageSwitcher extends PopoverProps { +export interface LanguageSwitcher { onChange: (arg0: LanguageOption) => void; language: LanguageOption['displayName']; languageOptions: Array; diff --git a/packages/combobox/README.md b/packages/combobox/README.md index ee02b08710..5d25d53301 100644 --- a/packages/combobox/README.md +++ b/packages/combobox/README.md @@ -78,7 +78,7 @@ import { Combobox, ComboboxOption } from '@leafygreen-ui/combobox'; ## Properties | Prop | Type | Description | Default | -| ------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| ------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------ | | `children` | ``, `` | Define the Combobox Options by passing children | | | `multiselect` | `boolean` | Defines whether a user can select multiple options, or only a single option. When using TypeScript, `multiselect` affects the valid values of `initialValue`, `value`, and `onChange` | `false` | | `initialValue` | `Array`, `string` | The initial selection. Must be a string (or array of strings) that matches the `value` prop of a `ComboboxOption`. Changing the `initialValue` after initial render will not change the selection. | | @@ -106,7 +106,7 @@ import { Combobox, ComboboxOption } from '@leafygreen-ui/combobox'; | `chipTruncationLocation` | `'start'`, `'middle'`, `'end'`, `'none'` | Defines where the ellipses appear in a Chip when the length exceeds the `chipCharacterLimit` | 'none' | | `chipCharacterLimit` | `number` | Defined the character limit of a multiselect Chip before they start truncating. Note: the three ellipses dots are included in the character limit. | 12 | | `className` | `string` | The className passed to the root element of the component. | | -| `usePortal` | `boolean` | Will position Popover's children relative to its parent without using a Portal, if `usePortal` is set to false. NOTE: The parent element should be CSS position `relative`, `fixed`, or `absolute` if using this option. | `true` | +| `renderMode` | `'inline'` \| `'portal'` \| `'top-layer'` | Options to render the popover element
\* [deprecated] `'inline'` will render the popover element inline in the DOM where it's written
\* [deprecated] `'portal'` will render the popover element in a new div appended to the body. Alternatively, can be portaled into a provided `portalContainer`
\* `'top-layer'` will render the popover element in the top layer | `'top-layer'` | `true` | | `portalContainer` | `HTMLElement` \| `null` | Sets the container used for the popover's portal. | | | `scrollContainer` | `HTMLElement` \| `null` | If the popover portal has a scrollable ancestor other than the window, this prop allows passing a reference to that element to allow the portal to position properly. | | | `portalClassName` | `string` | Passes the given className to the popover's portal container if the default portal container is being used. | | diff --git a/packages/combobox/src/Combobox/Combobox.spec.tsx b/packages/combobox/src/Combobox/Combobox.spec.tsx index 792af32979..f88ccecd08 100644 --- a/packages/combobox/src/Combobox/Combobox.spec.tsx +++ b/packages/combobox/src/Combobox/Combobox.spec.tsx @@ -16,6 +16,7 @@ import isUndefined from 'lodash/isUndefined'; import Button from '@leafygreen-ui/button'; import { keyMap } from '@leafygreen-ui/lib'; +import { RenderMode } from '@leafygreen-ui/popover'; import { eventContainingTargetValue } from '@leafygreen-ui/testing-lib'; import { OptionObject } from '../ComboboxOption/ComboboxOption.types'; @@ -35,9 +36,12 @@ import { describe('packages/combobox', () => { describe('A11y', () => { test('does not have basic accessibility violations', async () => { - const { container, openMenu } = renderCombobox(); - openMenu(); + const { container, openMenu } = renderCombobox('single', { + renderMode: 'portal', + }); + const { menuContainerEl } = openMenu(); await waitFor(async () => { + expect(menuContainerEl).toBeVisible(); const results = await axe(container); expect(results).toHaveNoViolations(); }); @@ -74,6 +78,7 @@ describe('packages/combobox', () => { document.body.appendChild(portalContainer); const portalRef = createRef(); const { openMenu } = renderCombobox(select, { + renderMode: RenderMode.Portal, portalContainer, portalRef, }); @@ -802,7 +807,7 @@ describe('packages/combobox', () => { }); describe('Click clear button', () => { - test('Clicking clear all button clears selection', () => { + test('Clicking clear all button clears selection', async () => { const initialValue = select === 'single' ? 'apple' : ['apple', 'banana', 'carrot']; const { inputEl, clearButtonEl, queryAllChips } = renderCombobox( @@ -818,7 +823,7 @@ describe('packages/combobox', () => { if (select === 'multiple') { expect(queryAllChips()).toHaveLength(0); } else { - expect(inputEl).toHaveValue(''); + await waitFor(() => expect(inputEl).toHaveValue('')); } }); }); diff --git a/packages/combobox/src/Combobox/Combobox.tsx b/packages/combobox/src/Combobox/Combobox.tsx index c5f31d11a5..4f84dce82c 100644 --- a/packages/combobox/src/Combobox/Combobox.tsx +++ b/packages/combobox/src/Combobox/Combobox.tsx @@ -32,9 +32,15 @@ import { import Icon from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; import LeafyGreenProvider, { + PopoverPropsProvider, useDarkMode, } from '@leafygreen-ui/leafygreen-provider'; import { consoleOnce, isComponentType, keyMap } from '@leafygreen-ui/lib'; +import { + DismissMode, + getPopoverRenderModeProps, + RenderMode, +} from '@leafygreen-ui/popover'; import { Description, Label } from '@leafygreen-ui/typography'; import { ComboboxChip } from '../ComboboxChip'; @@ -128,7 +134,7 @@ export function Combobox({ chipTruncationLocation, chipCharacterLimit = 12, className, - usePortal = true, + renderMode = RenderMode.TopLayer, portalClassName, portalContainer, portalRef, @@ -1156,15 +1162,14 @@ export function Combobox({ const popoverProps = { popoverZIndex, - ...(usePortal - ? { - usePortal, - portalClassName, - portalContainer, - portalRef, - scrollContainer, - } - : { usePortal }), + ...getPopoverRenderModeProps({ + dismissMode: DismissMode.Manual, + portalClassName, + portalContainer, + portalRef, + renderMode, + scrollContainer, + }), } as const; const formFieldFeedbackProps = { @@ -1321,19 +1326,20 @@ export function Combobox({ * Menu * / *******/} - - {renderedOptionsJSX} - + + + {renderedOptionsJSX} + +
@@ -1426,7 +1432,7 @@ Combobox.propTypes = { filteredOptions: PropTypes.arrayOf(PropTypes.string), // Popover Props popoverZIndex: PropTypes.number, - usePortal: PropTypes.bool, + renderMode: PropTypes.oneOf(Object.values(RenderMode)), scrollContainer: PropTypes.elementType, portalContainer: PropTypes.elementType, portalRef: PropTypes.shape({ diff --git a/packages/combobox/src/Combobox/Combobox.types.ts b/packages/combobox/src/Combobox/Combobox.types.ts index 25b24f1370..f247537e26 100644 --- a/packages/combobox/src/Combobox/Combobox.types.ts +++ b/packages/combobox/src/Combobox/Combobox.types.ts @@ -2,7 +2,7 @@ import React, { ReactNode } from 'react'; import { type ChipProps } from '@leafygreen-ui/chip'; import { Either, HTMLElementProps } from '@leafygreen-ui/lib'; -import { PortalControlProps } from '@leafygreen-ui/popover'; +import { PopoverProps } from '@leafygreen-ui/popover'; import { ComboboxSize, @@ -56,11 +56,19 @@ export interface ComboboxMultiselectProps { type PartialChipProps = Pick< ChipProps, - 'chipTruncationLocation' | 'chipCharacterLimit' | 'popoverZIndex' + 'chipTruncationLocation' | 'chipCharacterLimit' >; export type BaseComboboxProps = Omit, 'onChange'> & - PortalControlProps & + Pick< + PopoverProps, + | 'popoverZIndex' + | 'portalClassName' + | 'portalContainer' + | 'portalRef' + | 'renderMode' + | 'scrollContainer' + > & PartialChipProps & { /** * Defines the Combobox Options by passing children. Must be `ComboboxOption` or `ComboboxGroup` diff --git a/packages/combobox/src/ComboboxChip/ComboboxChip.tsx b/packages/combobox/src/ComboboxChip/ComboboxChip.tsx index 0c5a52828b..b5569ebfd7 100644 --- a/packages/combobox/src/ComboboxChip/ComboboxChip.tsx +++ b/packages/combobox/src/ComboboxChip/ComboboxChip.tsx @@ -25,7 +25,6 @@ export const ComboboxChip = React.forwardRef< overflow, chipTruncationLocation = TruncationLocation.End, chipCharacterLimit = 12, - popoverZIndex, } = useContext(ComboboxContext); const updatedChipTruncationLocation = @@ -80,7 +79,6 @@ export const ComboboxChip = React.forwardRef< baseFontSize={BaseFontSize.Body1} chipCharacterLimit={chipCharacterLimit} chipTruncationLocation={updatedChipTruncationLocation} - popoverZIndex={popoverZIndex} variant={Variant.Gray} ref={chipRef} disabled={disabled} diff --git a/packages/combobox/src/ComboboxMenu/ComboboxMenu.tsx b/packages/combobox/src/ComboboxMenu/ComboboxMenu.tsx index 90c2791865..b70ef432e0 100644 --- a/packages/combobox/src/ComboboxMenu/ComboboxMenu.tsx +++ b/packages/combobox/src/ComboboxMenu/ComboboxMenu.tsx @@ -6,7 +6,7 @@ import { useAvailableSpace, useForwardedRef } from '@leafygreen-ui/hooks'; import Icon from '@leafygreen-ui/icon'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; import { palette } from '@leafygreen-ui/palette'; -import Popover, { PortalControlProps } from '@leafygreen-ui/popover'; +import Popover from '@leafygreen-ui/popover'; import { Error } from '@leafygreen-ui/typography'; import { ComboboxProps } from '../Combobox'; @@ -30,14 +30,10 @@ type ComboboxMenuProps = { id: string; labelId: string; menuWidth: number; -} & PortalControlProps & - Pick< - ComboboxProps, - | 'searchLoadingMessage' - | 'searchErrorMessage' - | 'searchEmptyMessage' - | 'popoverZIndex' - >; +} & Pick< + ComboboxProps, + 'searchLoadingMessage' | 'searchErrorMessage' | 'searchEmptyMessage' +>; export const ComboboxMenu = React.forwardRef( ( @@ -50,7 +46,6 @@ export const ComboboxMenu = React.forwardRef( searchLoadingMessage, searchErrorMessage, searchEmptyMessage, - ...popoverProps }: ComboboxMenuProps, forwardedRef, ) => { @@ -140,7 +135,6 @@ export const ComboboxMenu = React.forwardRef( refEl={refEl} adjustOnMutation={true} className={cx(popoverStyle(menuWidth), popoverThemeStyle[theme])} - {...popoverProps} >
= { }, args: { copyable: true, - shouldTooltipUsePortal: true, darkMode: false, label: 'Label', description: 'Description', @@ -39,7 +38,6 @@ const meta: StoryMetaType = { copyable: { control: 'boolean' }, label: { control: 'text' }, description: { control: 'text' }, - shouldTooltipUsePortal: { control: 'boolean' }, children: storybookArgTypes.children, darkMode: storybookArgTypes.darkMode, }, diff --git a/packages/copyable/src/Copyable/Copyable.spec.tsx b/packages/copyable/src/Copyable/Copyable.spec.tsx index 2ab8ada253..9bccf9c2ce 100644 --- a/packages/copyable/src/Copyable/Copyable.spec.tsx +++ b/packages/copyable/src/Copyable/Copyable.spec.tsx @@ -1,11 +1,5 @@ import React from 'react'; -import { - act, - fireEvent, - render, - waitFor, - waitForElementToBeRemoved, -} from '@testing-library/react'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; import ClipboardJS from 'clipboard'; import { axe } from 'jest-axe'; @@ -113,14 +107,12 @@ describe('packages/copyable', () => { }, ); - await waitFor(() => expect(getByText('Copied!')).toBeVisible()); + const tooltip = getByText('Copied!'); - // Tooltip should remain visible for a while - await new Promise(resolve => setTimeout(resolve, 1000)); - expect(getByText('Copied!')).toBeVisible(); - - // Tooltip should eventually disappear - await waitForElementToBeRemoved(() => queryByText('Copied!')); + await waitFor(() => expect(tooltip).toBeVisible()); + await waitFor(() => expect(tooltip).not.toBeVisible(), { + timeout: 2000, + }); }, ); }); diff --git a/packages/copyable/src/Copyable/Copyable.tsx b/packages/copyable/src/Copyable/Copyable.tsx index ee5ff23422..81f9c8bb66 100644 --- a/packages/copyable/src/Copyable/Copyable.tsx +++ b/packages/copyable/src/Copyable/Copyable.tsx @@ -11,13 +11,19 @@ import { usePopoverPortalContainer, } from '@leafygreen-ui/leafygreen-provider'; import { BaseFontSize } from '@leafygreen-ui/tokens'; -import Tooltip, { Align, Justify, TriggerEvent } from '@leafygreen-ui/tooltip'; +import Tooltip, { + Align, + Justify, + RenderMode, + TriggerEvent, +} from '@leafygreen-ui/tooltip'; import { Description, Label, useUpdatedBaseFontSize, } from '@leafygreen-ui/typography'; +import { TOOLTIP_VISIBLE_DURATION } from './constants'; import { buttonContainerStyle, buttonStyle, @@ -45,7 +51,6 @@ export default function Copyable({ description, label, onCopy, - shouldTooltipUsePortal = true, size: SizeProp, }: CopyableProps) { const { theme, darkMode } = useDarkMode(darkModeProp); @@ -97,7 +102,7 @@ export default function Copyable({ if (copied) { const timeoutId = setTimeout(() => { setCopied(false); - }, 1500); + }, TOOLTIP_VISIBLE_DURATION); return () => clearTimeout(timeoutId); } @@ -183,7 +188,7 @@ export default function Copyable({ } triggerEvent={TriggerEvent.Click} - usePortal={shouldTooltipUsePortal} + renderMode={RenderMode.TopLayer} > Copied! @@ -204,5 +209,4 @@ Copyable.propTypes = { description: PropTypes.string, className: PropTypes.string, copyable: PropTypes.bool, - shouldTooltipUsePortal: PropTypes.bool, }; diff --git a/packages/copyable/src/Copyable/Copyable.types.ts b/packages/copyable/src/Copyable/Copyable.types.ts index f232fb2c9e..ea8f065238 100644 --- a/packages/copyable/src/Copyable/Copyable.types.ts +++ b/packages/copyable/src/Copyable/Copyable.types.ts @@ -35,10 +35,5 @@ export interface CopyableProps extends HTMLElementProps<'div'> { size?: Size; - /** - * If `true`, the tooltip rendered as feedback when the user clicks the copy button will be rendered using a portal - */ - shouldTooltipUsePortal?: boolean; - children: string; } diff --git a/packages/copyable/src/Copyable/constants.ts b/packages/copyable/src/Copyable/constants.ts new file mode 100644 index 0000000000..921673e5bd --- /dev/null +++ b/packages/copyable/src/Copyable/constants.ts @@ -0,0 +1 @@ +export const TOOLTIP_VISIBLE_DURATION = 1500; diff --git a/packages/date-picker/README.md b/packages/date-picker/README.md index 152a16480d..ca3cffe2ce 100644 --- a/packages/date-picker/README.md +++ b/packages/date-picker/README.md @@ -61,7 +61,7 @@ const [date, setDate] = useState(); ## Popover Props -Date Picker extends [Popover props](https://www.mongodb.design/component/popover/documentation/) but omits the following props: `usePortal`, `refEl`, `children`, `className`, `onClick`, and `active`. +Date Picker extends [Popover props](https://www.mongodb.design/component/popover/documentation/) but omits the following props: `active`, `children`, `className`, `dismissMode`, `onClick`, `onToggle`, `popoverZIndex`, `portalClassName`, `portalContainer`, `portalRef`, `refEl`, `renderMode`, and `scrollContainer`. ## 🔎 Glossary diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index cec9dbdf64..71767988aa 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -1,4 +1,4 @@ -import React, { createRef } from 'react'; +import React from 'react'; import { fireEvent, render, @@ -408,27 +408,6 @@ describe('packages/date-picker', () => { expect(menuContainerEl).toBeInTheDocument(); }); - test('appends to the end of the DOM', async () => { - const { findMenuElements, container } = renderDatePicker({ - initialOpen: true, - }); - const { menuContainerEl } = await findMenuElements(); - expect(container).not.toContain(menuContainerEl); - }); - - test('accepts a portalRef', () => { - const portalContainer = document.createElement('div'); - document.body.appendChild(portalContainer); - const portalRef = createRef(); - renderDatePicker({ - initialOpen: true, - portalContainer, - portalRef, - }); - expect(portalRef.current).toBeDefined(); - expect(portalRef.current).toBe(portalContainer); - }); - test('menu is initially closed when rendered with `initialOpen` and `disabled`', async () => { const { findMenuElements } = renderDatePicker({ initialOpen: true, @@ -3669,9 +3648,6 @@ describe('packages/date-picker', () => { {/* @ts-expect-error - needs label/aria-label/aria-labelledby */} - {/* @ts-expect-error - does not accept usePortal prop */} - - @@ -3694,21 +3670,16 @@ describe('packages/date-picker', () => { initialOpen={false} autoComplete="off" darkMode={false} - portalClassName="" - scrollContainer={{} as HTMLElement} - portalContainer={{} as HTMLElement} align="bottom" justify="start" spacing={10} adjustOnMutation={true} - popoverZIndex={1} onEnter={() => {}} onEntering={() => {}} onEntered={() => {}} onExit={() => {}} onExiting={() => {}} onExited={() => {}} - contentClassName="" /> ; }); diff --git a/packages/date-picker/src/DatePicker/DatePicker.tsx b/packages/date-picker/src/DatePicker/DatePicker.tsx index 6917a3ffe8..43da9e94b0 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.tsx @@ -96,22 +96,4 @@ DatePicker.propTypes = { initialOpen: PropTypes.bool, autoComplete: PropTypes.oneOf(Object.values(AutoComplete)), darkMode: PropTypes.bool, - // Popover Props - popoverZIndex: PropTypes.number, - portalContainer: - typeof window !== 'undefined' - ? PropTypes.instanceOf(Element) - : PropTypes.any, - /// @ts-expect-error Types of property '[nominalTypeHack]' are incompatible. - portalRef: PropTypes.shape({ - current: - typeof window !== 'undefined' - ? PropTypes.instanceOf(Element) - : PropTypes.any, - }), - scrollContainer: - typeof window !== 'undefined' - ? PropTypes.instanceOf(Element) - : PropTypes.any, - portalClassName: PropTypes.string, }; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index d5a82e9ce2..b5533dafe0 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -301,7 +301,6 @@ export const DatePickerMenu = forwardRef( spacing={spacing[1]} data-today={today.toISOString()} className={menuWrapperStyles} - usePortal onKeyDown={handleMenuKeyPress} onTransitionEnd={handleMenuTransitionEntered} onExited={handleMenuTransitionExited} diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.types.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.types.ts index 1a21370038..d1bfed1f94 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.types.ts +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.types.ts @@ -1,7 +1,16 @@ import { HTMLElementProps } from '@leafygreen-ui/lib'; import { PopoverProps } from '@leafygreen-ui/popover'; -import { PortalControlProps } from '@leafygreen-ui/popover'; -export type DatePickerMenuProps = PortalControlProps & - Omit & +export type DatePickerMenuProps = Omit< + PopoverProps, + | 'children' + | 'dismissMode' + | 'onToggle' + | 'popoverZIndex' + | 'portalClassName' + | 'portalContainer' + | 'portalRef' + | 'renderMode' + | 'scrollContainer' +> & HTMLElementProps<'div'>; diff --git a/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.tsx b/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.tsx index e7859e4cee..a87de27285 100644 --- a/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.tsx +++ b/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.tsx @@ -6,11 +6,26 @@ import { useDarkMode, } from '@leafygreen-ui/leafygreen-provider'; import { HTMLElementProps } from '@leafygreen-ui/lib'; -import Popover, { PopoverProps } from '@leafygreen-ui/popover'; +import Popover, { + DismissMode, + PopoverProps, + RenderMode, +} from '@leafygreen-ui/popover'; import { menuStyles } from './MenuWrapper.styles'; -export type MenuWrapperProps = PopoverProps & HTMLElementProps<'div'>; +export type MenuWrapperProps = Omit< + PopoverProps, + | 'dismissMode' + | 'onToggle' + | 'popoverZIndex' + | 'portalClassName' + | 'portalContainer' + | 'portalRef' + | 'renderMode' + | 'scrollContainer' +> & + HTMLElementProps<'div'>; /** * A simple styled popover component @@ -24,8 +39,13 @@ export const MenuWrapper = forwardRef( ref={fwdRef} className={cx(menuStyles[theme], className)} {...props} + dismissMode={DismissMode.Manual} + renderMode={RenderMode.TopLayer} > - {/* Prevents the opening and closing state of a select dropdown from propagating up to other PopoverProviders in parent components. E.g. Modal */} + {/* + * Prevents the opening and closing state of a select dropdown from propagating up + * to other PopoverProviders in parent components. E.g. Modal + */} {children} ); diff --git a/packages/date-picker/src/shared/constants.ts b/packages/date-picker/src/shared/constants.ts index 96f78df967..9d63ac22e1 100644 --- a/packages/date-picker/src/shared/constants.ts +++ b/packages/date-picker/src/shared/constants.ts @@ -1,4 +1,5 @@ import { Month } from '@leafygreen-ui/date-utils'; +import { RenderMode } from '@leafygreen-ui/popover'; import { DropdownWidthBasis } from '@leafygreen-ui/select'; /** @@ -78,7 +79,5 @@ export const selectElementProps = { size: 'xsmall', allowDeselect: false, dropdownWidthBasis: DropdownWidthBasis.Option, - // using no portal so the select menus are included in the backdrop "foreground" - // there is currently no way to pass a ref into the Select portal to use in backdrop "foreground" - usePortal: false, + renderMode: RenderMode.TopLayer, } as const; diff --git a/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts b/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts index ce34aa137f..fe3ebff2ec 100644 --- a/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts +++ b/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts @@ -30,22 +30,16 @@ export type ContextPropKeys = keyof SharedDatePickerProviderProps & * Prop names that are extended from popoverProps * */ export const modifiedPopoverPropNames: Array = [ - 'scrollContainer', - 'portalContainer', - 'portalRef', - 'portalClassName', 'align', 'justify', 'spacing', 'adjustOnMutation', - 'popoverZIndex', 'onEnter', 'onEntering', 'onEntered', 'onExit', 'onExiting', 'onExited', - 'contentClassName', ]; /** diff --git a/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts b/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts index adf2a821fb..47ee0057e7 100644 --- a/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts +++ b/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts @@ -8,7 +8,19 @@ import { AutoComplete, DatePickerState } from './types'; export type ModifiedPopoverProps = Omit< PopoverProps, - 'usePortal' | 'refEl' | 'children' | 'className' | 'active' | 'onClick' + | 'active' + | 'children' + | 'className' + | 'dismissMode' + | 'onClick' + | 'onToggle' + | 'popoverZIndex' + | 'portalClassName' + | 'portalContainer' + | 'portalRef' + | 'refEl' + | 'renderMode' + | 'scrollContainer' >; export type BaseDatePickerProps = { diff --git a/packages/guide-cue/README.md b/packages/guide-cue/README.md index d58ccd6949..49cdbb376d 100644 --- a/packages/guide-cue/README.md +++ b/packages/guide-cue/README.md @@ -96,8 +96,4 @@ The variant that is shown depends on the number of steps. If `numberOfSteps > 1` | `tooltipAlign` | `'top'` \| `'bottom'` \| `'left'` \| `'right'` | Determines the alignment of the tooltip. | `top` | | `tooltipJustify` | `'start'` \| `'middle'` \| `'end'` | Determines the justification of the tooltip. | `middle` | | `beaconAlign` | `'top'` \| `'bottom'` \| `'left'` \| `'right'` \| `'center-horizontal'` \| `'center-vertical'` | Determines the alignment of the beacon(animated pulsing circle that appears on top of the trigger element). This only applies to the multi-step tooltip. | `center-horizontal` | -| `portalContainer` | `HTMLElement` \| `null` | Sets the container used for the popover's portal. NOTE: If using a `scrollContainer` make sure that the `portalContainer` is contained within the `scrollContainer`. E.g, passing the same refrence to `scrollContainer` and `portalContainer`. | | -| `scrollContainer` | `HTMLElement` \| `null` | If the popover portal has a scrollable ancestor other than the window, this prop allows passing a reference to that element to allow the portal to position properly. | | -| `portalClassName` | `string` | Passes the given className to the popover's portal container if the default portal container is being used. | | -| `popoverZIndex` | `number` | Sets the z-index CSS property for the popover. | | | ... | native `div` attributes | Any other props will be spread on the tooltip `div` element | | diff --git a/packages/guide-cue/package.json b/packages/guide-cue/package.json index da08661f40..539f4b2d01 100644 --- a/packages/guide-cue/package.json +++ b/packages/guide-cue/package.json @@ -30,9 +30,6 @@ "access": "public" }, "dependencies": { - "focus-trap": "6.9.4", - "focus-trap-react": "9.0.2", - "polished": "^4.2.2", "@leafygreen-ui/a11y": "^1.4.13", "@leafygreen-ui/button": "^21.2.0", "@leafygreen-ui/emotion": "^4.0.8", @@ -43,10 +40,12 @@ "@leafygreen-ui/palette": "^4.0.9", "@leafygreen-ui/popover": "^11.4.0", "@leafygreen-ui/tooltip": "^11.1.0", - "@leafygreen-ui/typography": "^19.0.0" + "@leafygreen-ui/typography": "^19.0.0", + "focus-trap": "6.9.4", + "focus-trap-react": "9.0.2", + "polished": "^4.2.2" }, "devDependencies": { - "@leafygreen-ui/tokens": "^2.8.0", "@lg-tools/storybook-utils": "^0.1.1" }, "peerDependencies": { diff --git a/packages/guide-cue/src/GuideCue.spec.tsx b/packages/guide-cue/src/GuideCue.spec.tsx index 4edca4f8df..9542d2c0a4 100644 --- a/packages/guide-cue/src/GuideCue.spec.tsx +++ b/packages/guide-cue/src/GuideCue.spec.tsx @@ -158,14 +158,6 @@ describe('packages/guide-cue', () => { expect(guideCue).not.toBeInTheDocument(); }); - test('content should render in a portal', () => { - const { container, queryByTestId } = renderGuideCue({ - open: true, - }); - const guideCue = queryByTestId(guideCueTestId); - expect(container).not.toContainElement(guideCue); - }); - test('number of steps should not be visible', () => { const { queryByText } = renderGuideCue({ open: true, @@ -192,32 +184,6 @@ describe('packages/guide-cue', () => { const body = getByText(guideCueChildren); expect(body).toBeInTheDocument(); }); - - test('will render inside portal and scroll container', async () => { - const elem = document.createElement('div'); - document.body.appendChild(elem); - renderGuideCue({ - open: true, - portalContainer: elem, - scrollContainer: elem, - }); - await act(async () => { - expect(elem.innerHTML.includes(guideCueTitle)).toBe(true); - }); - }); - - test('accepts a portalRef', async () => { - const portalContainer = document.createElement('div'); - document.body.appendChild(portalContainer); - const portalRef = createRef(); - renderGuideCue({ - open: true, - portalContainer, - portalRef, - }); - expect(portalRef.current).toBeDefined(); - expect(portalRef.current).toBe(portalContainer); - }); }); describe('Multi-step tooltip', () => { @@ -334,17 +300,6 @@ describe('packages/guide-cue', () => { expect(modal).not.toBeInTheDocument(); }); - test('content should render in a portal', async () => { - const { container, findByTestId } = renderGuideCue({ - open: true, - numberOfSteps: 2, - currentStep: 1, - }); - - const guideCue = await findByTestId(guideCueTestId); - expect(container).not.toContainElement(guideCue); - }); - test('number of steps should be visible', async () => { const { getByText } = renderGuideCue({ open: true, @@ -396,20 +351,5 @@ describe('packages/guide-cue', () => { const numOfButtons = getAllByRole('button').length; await waitFor(() => expect(numOfButtons).toEqual(2)); }); - - test('will render inside portal and scroll container', async () => { - const elem = document.createElement('div'); - document.body.appendChild(elem); - const { findByText } = renderGuideCue({ - open: true, - numberOfSteps: 2, - currentStep: 1, - portalContainer: elem, - scrollContainer: elem, - }); - - const guideCue = await findByText(guideCueTitle); - expect(elem).toContainElement(guideCue); - }); }); }); diff --git a/packages/guide-cue/src/GuideCue.stories.tsx b/packages/guide-cue/src/GuideCue.stories.tsx index 7455252466..a6fa8781a6 100644 --- a/packages/guide-cue/src/GuideCue.stories.tsx +++ b/packages/guide-cue/src/GuideCue.stories.tsx @@ -12,7 +12,6 @@ import Button from '@leafygreen-ui/button'; import { css } from '@leafygreen-ui/emotion'; import { palette } from '@leafygreen-ui/palette'; import { Align } from '@leafygreen-ui/popover'; -import { transitionDuration } from '@leafygreen-ui/tokens'; import { Body } from '@leafygreen-ui/typography'; import { GuideCue, GuideCueProps, TooltipAlign, TooltipJustify } from '.'; @@ -80,11 +79,11 @@ const meta: StoryMetaType = {
@@ -191,12 +190,11 @@ export const ScrollableContainer: StoryFn = ( ) => { const [open, setOpen] = useState(false); const triggerRef = useRef(null); - const portalContainer = useRef(null); const { children, darkMode } = args; return (
-
+
<> @@ -65,6 +76,4 @@ InfoSprinkle.propTypes = { setOpen: PropTypes.func, id: PropTypes.string, shouldClose: PropTypes.func, - usePortal: PropTypes.bool, - portalClassName: PropTypes.string, }; diff --git a/packages/info-sprinkle/src/InfoSprinkle/InfoSprinkle.types.ts b/packages/info-sprinkle/src/InfoSprinkle/InfoSprinkle.types.ts index e0d9816fce..e39bd373e8 100644 --- a/packages/info-sprinkle/src/InfoSprinkle/InfoSprinkle.types.ts +++ b/packages/info-sprinkle/src/InfoSprinkle/InfoSprinkle.types.ts @@ -6,7 +6,18 @@ export { Align, Justify }; type ModifiedTooltipProps = Omit< TooltipProps, - 'onClick' | 'trigger' | 'triggerEvent' | 'refEl' | 'spacing' + | 'dismissMode' + | 'onClick' + | 'popoverZIndex' + | 'portalClassName' + | 'portalContainer' + | 'portalRef' + | 'refEl' + | 'renderMode' + | 'scrollContainer' + | 'spacing' + | 'trigger' + | 'triggerEvent' >; export interface InfoSprinkleProps extends ModifiedTooltipProps { diff --git a/packages/inline-definition/README.md b/packages/inline-definition/README.md index 04bfd4419b..eb2f3b014b 100644 --- a/packages/inline-definition/README.md +++ b/packages/inline-definition/README.md @@ -58,6 +58,6 @@ npm install @leafygreen-ui/inline-definition | `children` | `string` | Text that will appear underlined | | | `className` | `string` | className will be applied to the trigger element | | | `align` | `'top'`, `'bottom'`, `'left'`, `'right'` | Determines the preferred alignment of the tooltip relative to the component's children. | `'top'` | -| `justify` | `'start'`, `'middle'`, `'end'`, `'fit'` | Determines the preferred justification of the tooltip (based on the alignment) relative to the element passed to the component's children. | `'start'` | +| `justify` | `'start'`, `'middle'`, `'end'` | Determines the preferred justification of the tooltip (based on the alignment) relative to the element passed to the component's children. | `'start'` | | `darkMode` | `boolean` | Determines if the component will appear in dark mode. | `false` | | `tooltipClassName` | `string` | className to be applied to the tooltip element | | diff --git a/packages/inline-definition/package.json b/packages/inline-definition/package.json index 88d8d8b23b..276bf530d0 100644 --- a/packages/inline-definition/package.json +++ b/packages/inline-definition/package.json @@ -25,6 +25,7 @@ "@leafygreen-ui/emotion": "^4.0.8", "@leafygreen-ui/lib": "^13.3.0", "@leafygreen-ui/palette": "^4.0.9", + "@leafygreen-ui/tokens": "^2.11.0", "@leafygreen-ui/tooltip": "^11.0.4" }, "peerDependencies": { diff --git a/packages/inline-definition/src/InlineDefinition.spec.tsx b/packages/inline-definition/src/InlineDefinition.spec.tsx index 5ff598f6e8..a19102ceaf 100644 --- a/packages/inline-definition/src/InlineDefinition.spec.tsx +++ b/packages/inline-definition/src/InlineDefinition.spec.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { act, - fireEvent, render, screen, waitFor, waitForElementToBeRemoved, } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { axe } from 'jest-axe'; import { H2 } from '@leafygreen-ui/typography'; @@ -33,7 +33,7 @@ describe('packages/inline-definition', () => { expect(results).toHaveNoViolations(); let newResults = null as any; - act(() => void fireEvent.mouseEnter(getByText('Shard'))); + act(() => void userEvent.hover(getByText('Shard'))); await act(async () => { newResults = await axe(container); }); @@ -54,18 +54,15 @@ describe('packages/inline-definition', () => { renderInlineDefinition(); const children = screen.getByText('Shard'); - fireEvent.mouseEnter(children); + userEvent.hover(children); - await waitFor( - () => expect(screen.getByText(shardDefinition)).toBeVisible(), - { timeout: 500 }, + await waitFor(() => + expect(screen.getByText(shardDefinition)).toBeVisible(), ); - fireEvent.mouseLeave(children); + userEvent.unhover(children); - await waitForElementToBeRemoved(screen.getByText(shardDefinition), { - timeout: 500, - }); + await waitForElementToBeRemoved(screen.getByText(shardDefinition)); }); /* eslint-disable jest/no-disabled-tests, jest/expect-expect */ diff --git a/packages/inline-definition/src/InlineDefinition.stories.tsx b/packages/inline-definition/src/InlineDefinition.stories.tsx index dabffb090f..32eb549dc0 100644 --- a/packages/inline-definition/src/InlineDefinition.stories.tsx +++ b/packages/inline-definition/src/InlineDefinition.stories.tsx @@ -24,6 +24,21 @@ const meta: StoryMetaType = { darkMode: [false, true], }, args: { open: true }, + decorator: Instance => { + return ( +
+ +
+ ); + }, }, }, args: { diff --git a/packages/inline-definition/src/InlineDefinition.styles.ts b/packages/inline-definition/src/InlineDefinition.styles.ts new file mode 100644 index 0000000000..841eb96162 --- /dev/null +++ b/packages/inline-definition/src/InlineDefinition.styles.ts @@ -0,0 +1,50 @@ +import { css, cx } from '@leafygreen-ui/emotion'; +import { Theme } from '@leafygreen-ui/lib'; +import { palette } from '@leafygreen-ui/palette'; + +const triggerElementStyles = css` + border-radius: 2px; + text-decoration: underline dotted 2px; + text-underline-offset: 0.125em; + + &:hover { + a > * { + // Remove the Link underline styles + &::after { + height: 0; + } + } + } + + &:focus, + &:focus-visible { + outline-color: ${palette.blue.light1}; + outline-offset: 3px; + outline-style: solid; + outline-width: 2px; + } +`; + +const triggerElementModeStyles: Record = { + [Theme.Light]: css` + text-decoration-color: ${palette.black}; + + &:hover, + &:focus, + &:focus-visible { + text-decoration-color: ${palette.black}; + } + `, + [Theme.Dark]: css` + text-decoration-color: ${palette.gray.light2}; + + &:hover, + &:focus, + &:focus-visible { + text-decoration-color: ${palette.gray.light2}; + } + `, +}; + +export const getTriggerElementStyles = (theme: Theme, className?: string) => + cx(triggerElementStyles, triggerElementModeStyles[theme], className); diff --git a/packages/inline-definition/src/InlineDefinition.tsx b/packages/inline-definition/src/InlineDefinition.tsx index aef15f167b..89fe93acf0 100644 --- a/packages/inline-definition/src/InlineDefinition.tsx +++ b/packages/inline-definition/src/InlineDefinition.tsx @@ -1,74 +1,11 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { css, cx } from '@leafygreen-ui/emotion'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; -import { Theme } from '@leafygreen-ui/lib'; -import { palette } from '@leafygreen-ui/palette'; -import Tooltip, { TooltipProps } from '@leafygreen-ui/tooltip'; +import Tooltip, { RenderMode } from '@leafygreen-ui/tooltip'; -const triggerElementStyles = css` - border-radius: 2px; - text-decoration: underline dotted 2px; - text-underline-offset: 0.125em; - - &:hover { - a > * { - // Remove the Link underline styles - &::after { - height: 0; - } - } - } - - &:focus, - &:focus-visible { - outline-color: ${palette.blue.light1}; - outline-offset: 3px; - outline-style: solid; - outline-width: 2px; - } -`; - -const triggerElementModeStyles: Record = { - [Theme.Light]: css` - text-decoration-color: ${palette.black}; - - &:hover, - &:focus, - &:focus-visible { - text-decoration-color: ${palette.black}; - } - `, - [Theme.Dark]: css` - text-decoration-color: ${palette.gray.light2}; - - &:hover, - &:focus, - &:focus-visible { - text-decoration-color: ${palette.gray.light2}; - } - `, -}; - -export interface InlineDefinitionProps extends Partial { - /** - * Trigger element for the definition tooltip - * @required - */ - children: TooltipProps['children']; - - /** - * ReactNode rendered inside the tooltip - * @required - */ - definition: React.ReactNode; - - /** - * `className` prop passed to the Tooltip component instance - */ - tooltipClassName?: string; -} +import { getTriggerElementStyles } from './InlineDefinition.styles'; +import { InlineDefinitionProps } from './InlineDefinition.types'; /** * Inline Definition @@ -86,23 +23,28 @@ function InlineDefinition({ ...tooltipProps }: InlineDefinitionProps) { const { theme, darkMode } = useDarkMode(darkModeProp); + const [tooltipOpen, setTooltipOpen] = useState(false); + + const handleMouseEnter = () => { + setTooltipOpen(true); + }; return ( {children} diff --git a/packages/inline-definition/src/InlineDefinition.types.ts b/packages/inline-definition/src/InlineDefinition.types.ts new file mode 100644 index 0000000000..d7a18a635d --- /dev/null +++ b/packages/inline-definition/src/InlineDefinition.types.ts @@ -0,0 +1,32 @@ +import { TooltipProps } from '@leafygreen-ui/tooltip'; + +export interface InlineDefinitionProps + extends Partial< + Omit< + TooltipProps, + | 'dismissMode' + | 'popoverZIndex' + | 'portalClassName' + | 'portalContainer' + | 'portalRef' + | 'renderMode' + | 'scrollContainer' + > + > { + /** + * Trigger element for the definition tooltip + * @required + */ + children: TooltipProps['children']; + + /** + * ReactNode rendered inside the tooltip + * @required + */ + definition: React.ReactNode; + + /** + * `className` prop passed to the Tooltip component instance + */ + tooltipClassName?: string; +} diff --git a/packages/inline-definition/src/index.ts b/packages/inline-definition/src/index.ts index c724740fcf..fc3cb137a9 100644 --- a/packages/inline-definition/src/index.ts +++ b/packages/inline-definition/src/index.ts @@ -1 +1,2 @@ -export { default, type InlineDefinitionProps } from './InlineDefinition'; +export { default } from './InlineDefinition'; +export { type InlineDefinitionProps } from './InlineDefinition.types'; diff --git a/packages/leafygreen-provider/README.md b/packages/leafygreen-provider/README.md index e2f1d196c8..99c4615392 100644 --- a/packages/leafygreen-provider/README.md +++ b/packages/leafygreen-provider/README.md @@ -44,11 +44,44 @@ import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; ### Properties -| Prop | Type | Description | Default | -| -------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| `children` | `node` | Children passed to `LeafyGreenProvider` will be unmodified, aside from having access to its state. | | -| `baseFontSize` | `14`, `16` | Describes the `font-size` that the application is using. `` and `` components use this value to determine the `font-size` and `line-height` applied to their content | `14` | -| `darkMode` | `boolean` | Determines if LG components should be rendered in dark mode. | | +| Prop | Type | Description | Default | +| ------------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `children` | `node` | Children passed to `LeafyGreenProvider` will be unmodified, aside from having access to its state. | | +| `baseFontSize` | `14`, `16` | Describes the `font-size` that the application is using. `` and `` components use this value to determine the `font-size` and `line-height` applied to their content | `14` | +| `darkMode` | `boolean` | Determines if LG components should be rendered in dark mode. | | +| `forceUseTopLayer` | `boolean` | Determines globally if popover elements using `Popover` component from `@leafygreen-ui/popover` package should render in top layer | `false` | + +## PopoverPropsProvider + +The `PopoverPropsProvider` can be used to pass props to a deeply nested popover element. + +### Example + +```js +import { PopoverPropsProvider } from '@leafygreen-ui/leafygreen-provider'; + +const ParentComponentWithNestedPopover = ({ ...popoverProps }) => { + return ( + + + + ); +}; +``` + +### Properties + +| Prop | Type | Description | Default | +| ------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `dismissMode` | `'auto'` \| `'manual'` | Options to control how the popover element is dismissed. This will only apply when `renderMode` is `'top-layer'`
\* `'auto'` will automatically handle dismissal on backdrop click or esc key press, ensuring only one popover is visible at a time
\* `'manual'` will require that the consumer handle dismissal manually | `'auto'` | +| `onToggle` | `(e: ToggleEvent) => void;` | Function that is called when the popover is toggled. This will only apply when `renderMode` is `'top-layer'` | | +| `popoverZIndex` (deprecated) | `number` | Sets the z-index CSS property for the popover. This will only apply if `usePortal` is defined and `renderMode` is not `'top-layer'` | | +| `portalClassName` (deprecated) | `string` | Passes the given className to the popover's portal container if the default portal container is being used. This will only apply when `renderMode` is `'portal'` | | +| `portalContainer` (deprecated) | `HTMLElement` \| `null` | Sets the container used for the popover's portal. This will only apply when `renderMode` is `'portal'`.
NOTE: If using a `scrollContainer` make sure that the `portalContainer` is contained within the `scrollContainer`. E.g, passing the same reference to `scrollContainer` and `portalContainer`. | | +| `portalRef` (deprecated) | `string` | Passes a ref to forward to the portal element. This will only apply when `renderMode` is `'portal'` | | +| `renderMode` | `'inline'` \| `'portal'` \| `'top-layer'` | Options to render the popover element
\* [deprecated] `'inline'` will render the popover element inline in the DOM where it's written
\* [deprecated] `'portal'` will render the popover element in a new div appended to the body. Alternatively, can be portaled into a provided `portalContainer`
\* `'top-layer'` will render the popover element in the top layer | `'top-layer'` | +| `scrollContainer` (deprecated) | `HTMLElement` \| `null` | If the popover portal has a scrollable ancestor other than the window, this prop allows passing a reference to that element to allow the portal to position properly. This will only apply when `renderMode` is `'portal'` | | +| `spacing` | `number` | Specifies the amount of spacing (in pixels) between the trigger element and the content element. | `4` | ## useUsingKeyboardContext @@ -111,6 +144,8 @@ function InlineCode({ children, className }: InlineCodeProps) { This hook is meant for internal use. It allows components to read the value of the dark mode prop from the LeafyGreen provider and overwrite the value locally if necessary. +### Example + ```js import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; diff --git a/packages/leafygreen-provider/package.json b/packages/leafygreen-provider/package.json index 1f0f444fd2..b773826a4d 100644 --- a/packages/leafygreen-provider/package.json +++ b/packages/leafygreen-provider/package.json @@ -22,8 +22,9 @@ "access": "public" }, "dependencies": { + "@leafygreen-ui/hooks": "^8.1.3", "@leafygreen-ui/lib": "^13.3.0", - "@leafygreen-ui/hooks": "^8.1.3" + "react-transition-group": "^4.4.5" }, "gitHead": "dd71a2d404218ccec2e657df9c0263dc1c15b9e0", "homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/leafygreen-provider", diff --git a/packages/leafygreen-provider/src/LeafyGreenContext.spec.tsx b/packages/leafygreen-provider/src/LeafyGreenContext.spec.tsx index d9550b0fc5..808ad467e2 100644 --- a/packages/leafygreen-provider/src/LeafyGreenContext.spec.tsx +++ b/packages/leafygreen-provider/src/LeafyGreenContext.spec.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { cleanup, render } from '@testing-library/react'; -import { LeafyGreenProviderProps } from './LeafyGreenContext'; +import { LeafyGreenProviderProps } from './LeafyGreenContext.types'; import LeafyGreenProvider, { useBaseFontSize, useDarkMode, @@ -53,6 +53,7 @@ describe('packages/leafygreen-provider/LeafyGreenProvider', () => { baseFontSize = 16, portalId = 'portal', scrollId = 'scroll', + forceUseTopLayer = false, ) => { const portalContainer = document.createElement('div'); portalContainer.setAttribute('id', portalId); @@ -66,6 +67,7 @@ describe('packages/leafygreen-provider/LeafyGreenProvider', () => { portalContainer, scrollContainer, }, + forceUseTopLayer, } as Required; }; diff --git a/packages/leafygreen-provider/src/LeafyGreenContext.tsx b/packages/leafygreen-provider/src/LeafyGreenContext.tsx index 9bdb852fb0..57dca90d6c 100644 --- a/packages/leafygreen-provider/src/LeafyGreenContext.tsx +++ b/packages/leafygreen-provider/src/LeafyGreenContext.tsx @@ -1,35 +1,26 @@ import React, { PropsWithChildren, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { DarkModeProps } from '@leafygreen-ui/lib'; - import DarkModeProvider, { useDarkModeContext } from './DarkModeContext'; -import PortalContextProvider, { - PortalContextValues, +import { LeafyGreenProviderProps } from './LeafyGreenContext.types'; +import { MigrationProvider, useMigrationContext } from './MigrationContext'; +import { + PortalContextProvider, usePopoverPortalContainer, } from './PortalContext'; -import TypographyProvider, { - TypographyProviderProps, - useBaseFontSize, -} from './TypographyContext'; +import TypographyProvider, { useBaseFontSize } from './TypographyContext'; import UsingKeyboardProvider from './UsingKeyboardContext'; -export type LeafyGreenProviderProps = { - /** - * Define a container HTMLElement for components that utilize the `Portal` component - */ - popoverPortalContainer?: PortalContextValues['popover']; -} & TypographyProviderProps & - DarkModeProps; - function LeafyGreenProvider({ children, baseFontSize: fontSizeProp, popoverPortalContainer: popoverPortalContainerProp, darkMode: darkModeProp, + forceUseTopLayer: forceUseTopLayerProp = false, }: PropsWithChildren) { - // if the prop is set, we use that - // if the prop is not set, we use outer context + /** + * If `darkMode` prop is provided, use that. Otherwise, use context value + */ const { contextDarkMode: inheritedDarkMode } = useDarkModeContext(); const [darkModeState, setDarkMode] = useState( darkModeProp ?? inheritedDarkMode, @@ -39,14 +30,26 @@ function LeafyGreenProvider({ setDarkMode(darkModeProp ?? inheritedDarkMode); }, [darkModeProp, inheritedDarkMode]); - // Similarly with base font size + /** + * If `baseFontSize` prop is provided, use that. Otherwise, use context value + */ const inheritedFontSize = useBaseFontSize(); const baseFontSize = fontSizeProp ?? inheritedFontSize; - // and popover portal container + + /** + * If `popoverPortalContainer` prop is provided, use that. Otherwise, use context value + */ const inheritedContainer = usePopoverPortalContainer(); const popoverPortalContainer = popoverPortalContainerProp ?? inheritedContainer; + /** + * If `forceUseTopLayerProp` is true, it will globally apply to all children + */ + const migrationContext = useMigrationContext(); + const forceUseTopLayer = + forceUseTopLayerProp || migrationContext.forceUseTopLayer; + return ( @@ -55,7 +58,9 @@ function LeafyGreenProvider({ contextDarkMode={darkModeState} setDarkMode={setDarkMode} > - {children} + + {children} + diff --git a/packages/leafygreen-provider/src/LeafyGreenContext.types.ts b/packages/leafygreen-provider/src/LeafyGreenContext.types.ts new file mode 100644 index 0000000000..a076357097 --- /dev/null +++ b/packages/leafygreen-provider/src/LeafyGreenContext.types.ts @@ -0,0 +1,14 @@ +import { DarkModeProps } from '@leafygreen-ui/lib'; + +import { MigrationContextType } from './MigrationContext'; +import { PortalContextValues } from './PortalContext'; +import { TypographyProviderProps } from './TypographyContext'; + +export type LeafyGreenProviderProps = { + /** + * Define a container HTMLElement for components that utilize the `Portal` component + */ + popoverPortalContainer?: PortalContextValues['popover']; +} & TypographyProviderProps & + DarkModeProps & + MigrationContextType; diff --git a/packages/leafygreen-provider/src/MigrationContext/MigrationContext.spec.tsx b/packages/leafygreen-provider/src/MigrationContext/MigrationContext.spec.tsx new file mode 100644 index 0000000000..0b5dd69b20 --- /dev/null +++ b/packages/leafygreen-provider/src/MigrationContext/MigrationContext.spec.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { renderHook } from '@leafygreen-ui/testing-lib'; + +import { MigrationProvider, useMigrationContext } from './MigrationContext'; + +const childTestId = 'test-child'; + +describe('packages/leafygreen-provider/MigrationContext', () => { + test('only renders children in the DOM', () => { + const { container, getByTestId } = render( + +
Child element
+
, + ); + const testChild = getByTestId(childTestId); + + expect(container.firstChild).toBe(testChild); + }); +}); + +describe('useMigrationContext', () => { + test('passes provider props correctly', () => { + const customProps = { forceUseTopLayer: true }; + const { result } = renderHook(useMigrationContext, { + wrapper: ({ children }) => ( + {children} + ), + }); + + expect(result.current).toHaveProperty('forceUseTopLayer', true); + }); +}); diff --git a/packages/leafygreen-provider/src/MigrationContext/MigrationContext.tsx b/packages/leafygreen-provider/src/MigrationContext/MigrationContext.tsx new file mode 100644 index 0000000000..784bfe1b16 --- /dev/null +++ b/packages/leafygreen-provider/src/MigrationContext/MigrationContext.tsx @@ -0,0 +1,34 @@ +import React, { createContext, PropsWithChildren, useContext } from 'react'; +import PropTypes from 'prop-types'; + +import { MigrationContextType } from './MigrationContext.types'; + +export const MigrationContext = createContext({ + forceUseTopLayer: false, +}); + +/** + * Access the modal popover context + */ +export const useMigrationContext = (): MigrationContextType => { + return useContext(MigrationContext); +}; + +/** + * Creates a global context for migration purposes. + * Call `useMigrationContext` to access the migration context + */ +export const MigrationProvider = ({ + children, + ...props +}: PropsWithChildren) => { + return ( + + {children} + + ); +}; + +MigrationProvider.displayName = 'MigrationProvider'; + +MigrationProvider.propTypes = { children: PropTypes.node }; diff --git a/packages/leafygreen-provider/src/MigrationContext/MigrationContext.types.ts b/packages/leafygreen-provider/src/MigrationContext/MigrationContext.types.ts new file mode 100644 index 0000000000..1574906596 --- /dev/null +++ b/packages/leafygreen-provider/src/MigrationContext/MigrationContext.types.ts @@ -0,0 +1,7 @@ +export interface MigrationContextType { + /** + * Determines globally if popover elements using `Popover` component from `@leafygreen-ui/popover` package should render in top layer + * @internal + */ + forceUseTopLayer?: boolean; +} diff --git a/packages/leafygreen-provider/src/MigrationContext/index.ts b/packages/leafygreen-provider/src/MigrationContext/index.ts new file mode 100644 index 0000000000..c9a0d2892f --- /dev/null +++ b/packages/leafygreen-provider/src/MigrationContext/index.ts @@ -0,0 +1,6 @@ +export { + MigrationContext, + MigrationProvider, + useMigrationContext, +} from './MigrationContext'; +export { type MigrationContextType } from './MigrationContext.types'; diff --git a/packages/leafygreen-provider/src/PopoverContext/PopoverContext.spec.tsx b/packages/leafygreen-provider/src/PopoverContext/PopoverContext.spec.tsx index 00b3d8dd18..f499c4e247 100644 --- a/packages/leafygreen-provider/src/PopoverContext/PopoverContext.spec.tsx +++ b/packages/leafygreen-provider/src/PopoverContext/PopoverContext.spec.tsx @@ -1,11 +1,12 @@ -import React, { PropsWithChildren } from 'react'; -import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import React from 'react'; +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderHook } from '@leafygreen-ui/testing-lib'; -import { PopoverProvider, type PopoverState, usePopoverContext } from '.'; +import { PopoverProvider, usePopoverContext } from './PopoverContext'; -const childTestID = 'popover-provider'; +const childTestID = 'modal-popover-provider'; const buttonTestId = 'test-button'; function TestContextComponent() { @@ -39,41 +40,23 @@ describe('packages/leafygreen-provider/PopoverContext', () => { const { container, testChild } = renderProvider(); expect(container.firstChild).toBe(testChild); }); - - test('isPopoverOpen is initialized as false', () => { - const { testChild } = renderProvider(); - expect(testChild.textContent).toBe('false'); - }); - - test('when passed true, setIsPopoverOpen sets isPopoverOpen to true', () => { - const { testChild, getByTestId } = renderProvider(); - - // The button's click handler fires setIsPopoverOpen(true) - fireEvent.click(getByTestId(buttonTestId)); - - expect(testChild.textContent).toBe('true'); - }); }); describe('usePopoverContext', () => { - test('is `false` by default', () => { + test('`isPopoverOpen` is `false` by default', () => { const { result } = renderHook(usePopoverContext); expect(result.current.isPopoverOpen).toBeFalsy(); }); - test('setter updates the value', async () => { - const { result, rerender } = renderHook< - PropsWithChildren<{}>, - PopoverState - >(usePopoverContext, { + test('`setIsPopoverOpen` updates the value of `isPopoverOpen`', async () => { + const { result, rerender } = renderHook(usePopoverContext, { wrapper: ({ children }) => {children}, }); act(() => result.current.setIsPopoverOpen(true)); rerender(); - await waitFor(() => { - expect(result.current.isPopoverOpen).toBe(true); - }); + + expect(result.current.isPopoverOpen).toBe(true); }); describe('with test component', () => { @@ -92,7 +75,7 @@ describe('usePopoverContext', () => { const { testChild, getByTestId } = renderTestComponent(); // The button's click handler fires setIsPopoverOpen(true) - fireEvent.click(getByTestId(buttonTestId)); + userEvent.click(getByTestId(buttonTestId)); expect(testChild.textContent).toBe('false'); }); diff --git a/packages/leafygreen-provider/src/PopoverContext/PopoverContext.tsx b/packages/leafygreen-provider/src/PopoverContext/PopoverContext.tsx index 125cab98fb..ccdb20472b 100644 --- a/packages/leafygreen-provider/src/PopoverContext/PopoverContext.tsx +++ b/packages/leafygreen-provider/src/PopoverContext/PopoverContext.tsx @@ -1,40 +1,34 @@ -import React, { createContext, useContext, useMemo, useState } from 'react'; +import React, { + createContext, + PropsWithChildren, + useContext, + useMemo, + useState, +} from 'react'; import PropTypes from 'prop-types'; -export interface PopoverState { - /** - * Whether the most immediate popover ancestor is open - */ - isPopoverOpen: boolean; - /** - * Sets the internal state - * @internal - */ - setIsPopoverOpen: React.Dispatch>; -} - -export const PopoverContext = createContext({ +import { PopoverContextType } from './PopoverContext.types'; + +export const PopoverContext = createContext({ isPopoverOpen: false, setIsPopoverOpen: () => {}, }); /** - * Access the popover state - * @returns `isPopoverOpen: boolean` + * Access the popover context to read and write if a popover element is open in a modal */ -export function usePopoverContext(): PopoverState { +export const usePopoverContext = (): PopoverContextType => { return useContext(PopoverContext); -} - -interface PopoverProviderProps { - children?: React.ReactNode; -} +}; /** - * Creates a Popover context. + * Creates a Popover context to read and write if a popover element is open in a modal * Call `usePopoverContext` to access the popover state + * This is defined separately from `PopoverPropsContext` to avoid incorrectly resetting `isPopoverOpen` value + * We avoid renaming this provider because it will trigger major changes in all packages because + * `@leafygreen-ui/leafygreen-provider` is a peer dependency to all LG packages */ -export function PopoverProvider({ children }: PopoverProviderProps) { +export const PopoverProvider = ({ children }: PropsWithChildren<{}>) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const providerValue = useMemo( @@ -50,7 +44,7 @@ export function PopoverProvider({ children }: PopoverProviderProps) { {children} ); -} +}; PopoverProvider.displayName = 'PopoverProvider'; diff --git a/packages/leafygreen-provider/src/PopoverContext/PopoverContext.types.ts b/packages/leafygreen-provider/src/PopoverContext/PopoverContext.types.ts new file mode 100644 index 0000000000..862b12862b --- /dev/null +++ b/packages/leafygreen-provider/src/PopoverContext/PopoverContext.types.ts @@ -0,0 +1,12 @@ +export interface PopoverContextType { + /** + * Whether a popover element is open in a modal + */ + isPopoverOpen: boolean; + + /** + * Called when a popover element opens or closes in a modal + * @internal + */ + setIsPopoverOpen: React.Dispatch>; +} diff --git a/packages/leafygreen-provider/src/PopoverContext/index.ts b/packages/leafygreen-provider/src/PopoverContext/index.ts index d31edeb0aa..14ccf31e39 100644 --- a/packages/leafygreen-provider/src/PopoverContext/index.ts +++ b/packages/leafygreen-provider/src/PopoverContext/index.ts @@ -1,6 +1,5 @@ export { PopoverContext, PopoverProvider, - type PopoverState, usePopoverContext, } from './PopoverContext'; diff --git a/packages/leafygreen-provider/src/PopoverPropsContext/PopoverPropsContext.spec.tsx b/packages/leafygreen-provider/src/PopoverPropsContext/PopoverPropsContext.spec.tsx new file mode 100644 index 0000000000..baae2139f8 --- /dev/null +++ b/packages/leafygreen-provider/src/PopoverPropsContext/PopoverPropsContext.spec.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +import { renderHook } from '@leafygreen-ui/testing-lib'; + +import { + PopoverPropsProvider, + usePopoverPropsContext, +} from './PopoverPropsContext'; + +const childTestId = 'test-child'; + +describe('packages/leafygreen-provider/PopoverPropsContext', () => { + test('only renders children in the DOM', () => { + const { container, getByTestId } = render( + +
Child element
+
, + ); + const testChild = getByTestId(childTestId); + + expect(container.firstChild).toBe(testChild); + }); +}); + +describe('usePopoverPropsContext', () => { + test('passes provider props correctly', () => { + const mockOnEnter = jest.fn(); + const customProps = { + onEnter: mockOnEnter, + popoverZIndex: 2, + usePortal: true, + }; + const { result } = renderHook(usePopoverPropsContext, { + wrapper: ({ children }) => ( + {children} + ), + }); + + expect(result.current).toHaveProperty('onEnter', mockOnEnter); + expect(result.current).toHaveProperty('popoverZIndex', 2); + expect(result.current).toHaveProperty('usePortal', true); + }); +}); diff --git a/packages/leafygreen-provider/src/PopoverPropsContext/PopoverPropsContext.tsx b/packages/leafygreen-provider/src/PopoverPropsContext/PopoverPropsContext.tsx new file mode 100644 index 0000000000..e85907feda --- /dev/null +++ b/packages/leafygreen-provider/src/PopoverPropsContext/PopoverPropsContext.tsx @@ -0,0 +1,48 @@ +import React, { createContext, PropsWithChildren, useContext } from 'react'; +import PropTypes from 'prop-types'; + +import { + PortalContextProvider, + usePopoverPortalContainer, +} from '../PortalContext'; + +import { PopoverPropsProviderProps } from './PopoverPropsContext.types'; + +export const PopoverPropsContext = createContext({}); + +/** + * Access the popover props context to read props passed to nested popover component instances + */ +export const usePopoverPropsContext = (): PopoverPropsProviderProps => { + return useContext(PopoverPropsContext); +}; + +/** + * Creates a PopoverProps context to pass props to a deeply nested popover element + * Call `usePopoverPropsContext` to access the popover state + * This is defined separately from `PopoverContext` to avoid incorrectly resetting `isPopoverOpen` value + */ +export const PopoverPropsProvider = ({ + children, + ...props +}: PropsWithChildren) => { + const popoverPortalContext = usePopoverPortalContainer(); + const popover = { + portalContainer: + props.portalContainer || popoverPortalContext.portalContainer, + scrollContainer: + props.scrollContainer || popoverPortalContext.scrollContainer, + }; + + return ( + + + {children} + + + ); +}; + +PopoverPropsProvider.displayName = 'PopoverPropsProvider'; + +PopoverPropsProvider.propTypes = { children: PropTypes.node }; diff --git a/packages/leafygreen-provider/src/PopoverPropsContext/PopoverPropsContext.types.ts b/packages/leafygreen-provider/src/PopoverPropsContext/PopoverPropsContext.types.ts new file mode 100644 index 0000000000..546b029e4a --- /dev/null +++ b/packages/leafygreen-provider/src/PopoverPropsContext/PopoverPropsContext.types.ts @@ -0,0 +1,207 @@ +import { Transition } from 'react-transition-group'; + +/** + * These types are duplicated in `@leafygreen-ui/popover`: https://github.com/mongodb/leafygreen-ui/blob/02e1d77e5ed7d55f9b8402299eae0c6d540c53f8/packages/popover/src/Popover.types.ts + * + * We cannot import `PopoverProps` into `@leafygreen-ui/leafygreen-provider` without introducing a circular dependency. + */ + +type TransitionProps = React.ComponentProps>; + +type TransitionLifecycleCallbacks = Pick< + TransitionProps, + 'onEnter' | 'onEntering' | 'onEntered' | 'onExit' | 'onExiting' | 'onExited' +>; + +/** + * Options to render the popover element + * @param Inline will render the popover element inline in the DOM where it's written + * @param Portal will render the popover element in a new div appended to the body. Alternatively, can be portaled into a provided `portalContainer` + * @param TopLayer will render the popover element in the top layer + */ +export const RenderMode = { + Inline: 'inline', + Portal: 'portal', + TopLayer: 'top-layer', +} as const; +export type RenderMode = (typeof RenderMode)[keyof typeof RenderMode]; + +/** + * Options to control how the popover element is dismissed. This should not be altered + * because it is intended to have parity with the web-native {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/popover popover attribute} + * @param Auto will automatically handle dismissal on backdrop click or esc key press, ensuring only one popover is visible at a time + * @param Manual will require that the consumer handle dismissal manually + */ +const DismissMode = { + Auto: 'auto', + Manual: 'manual', +} as const; +type DismissMode = (typeof DismissMode)[keyof typeof DismissMode]; + +/** Local implementation of web-native `ToggleEvent` until we use typescript v5 */ +interface ToggleEvent extends Event { + type: 'toggle'; + newState: 'open' | 'closed'; + oldState: 'open' | 'closed'; +} + +export interface RenderInlineProps { + /** + * Options to render the popover element + * @defaultValue 'top-layer' + * @param Inline will render the popover element inline in the DOM where it's written. This option is deprecated and will be removed in the future. + * @param Portal will render the popover element in a new div appended to the body. Alternatively, can be portaled into a provided `portalContainer`. This option is deprecated and will be removed in the future. + * @param TopLayer will render the popover element in the top layer + */ + renderMode: 'inline'; + + /** + * When `renderMode="top-layer"`, these options can control how a popover element is dismissed + * - `'auto'` will automatically handle dismissal on backdrop click or key press, ensuring only one popover is visible at a time + * - `'manual'` will require that the consumer handle dismissal manually + */ + dismissMode?: never; + + /** + * A callback function that is called when the visibility of a popover element rendered in the top layer is toggled + */ + onToggle?: never; + + /** + * When `renderMode="portal"`, it specifies a class name to apply to the portal element + * @deprecated + */ + portalClassName?: never; + + /** + * When `renderMode="portal"`, it specifies an element to portal within. If not provided, a div is generated at the end of the body + * @deprecated + */ + portalContainer?: never; + + /** + * When `renderMode="portal"`, it passes a ref to forward to the portal element + * @deprecated + */ + portalRef?: never; + + /** + * When `renderMode="portal"`, it specifies the scrollable element to position relative to + * @deprecated + */ + scrollContainer?: never; +} + +export interface RenderPortalProps { + /** + * Options to render the popover element + * @defaultValue 'top-layer' + * @param Inline will render the popover element inline in the DOM where it's written. This option is deprecated and will be removed in the future. + * @param Portal will render the popover element in a new div appended to the body. Alternatively, can be portaled into a provided `portalContainer`. This option is deprecated and will be removed in the future. + * @param TopLayer will render the popover element in the top layer + */ + renderMode: 'portal'; + + /** + * When `renderMode="top-layer"`, these options can control how a popover element is dismissed + * - `'auto'` will automatically handle dismissal on backdrop click or key press, ensuring only one popover is visible at a time + * - `'manual'` will require that the consumer handle dismissal manually + */ + dismissMode?: never; + + /** + * When `renderMode="top-layer"`, this callback function is called when the visibility of a popover element is toggled + */ + onToggle?: never; + + /** + * When `renderMode="portal"`, it specifies a class name to apply to the portal element + * @deprecated + */ + portalClassName?: string; + + /** + * When `renderMode="portal"`, it specifies an element to portal within. If not provided, a div is generated at the end of the body + * @deprecated + */ + portalContainer?: HTMLElement | null; + + /** + * When `renderMode="portal"`, it passes a ref to forward to the portal element + * @deprecated + */ + portalRef?: React.MutableRefObject; + + /** + * When `renderMode="portal"`, it specifies the scrollable element to position relative to + * @deprecated + */ + scrollContainer?: HTMLElement | null; +} + +export interface RenderTopLayerProps { + /** + * Options to render the popover element + * @defaultValue 'top-layer' + * @param Inline will render the popover element inline in the DOM where it's written. This option is deprecated and will be removed in the future. + * @param Portal will render the popover element in a new div appended to the body. Alternatively, can be portaled into a provided `portalContainer`. This option is deprecated and will be removed in the future. + * @param TopLayer will render the popover element in the top layer + */ + renderMode?: 'top-layer'; + + /** + * When `renderMode="top-layer"`, these options can control how a popover element is dismissed + * - `'auto'` will automatically handle dismissal on backdrop click or key press, ensuring only one popover is visible at a time + * - `'manual'` will require that the consumer handle dismissal manually + */ + dismissMode?: DismissMode; + + /** + * A callback function that is called when the visibility of a popover element rendered in the top layer is toggled + */ + onToggle?: (e: ToggleEvent) => void; + + /** + * When `renderMode="portal"`, it specifies a class name to apply to the portal element + * @deprecated + */ + portalClassName?: never; + + /** + * When `renderMode="portal"`, it specifies an element to portal within. If not provided, a div is generated at the end of the body + * @deprecated + */ + portalContainer?: never; + + /** + * When `renderMode="portal"`, it passes a ref to forward to the portal element + * @deprecated + */ + portalRef?: never; + + /** + * When `renderMode="portal"`, it specifies the scrollable element to position relative to + * @deprecated + */ + scrollContainer?: never; +} + +type PopoverRenderModeProps = + | RenderPortalProps + | RenderInlineProps + | RenderTopLayerProps; + +export type PopoverPropsProviderProps = { + /** + * Specifies the amount of spacing (in pixels) between the trigger element and the Popover content. + * + * default: `10` + */ + spacing?: number; + + /** + * Number that controls the z-index of the popover element directly. + */ + popoverZIndex?: number; +} & PopoverRenderModeProps & + TransitionLifecycleCallbacks; diff --git a/packages/leafygreen-provider/src/PopoverPropsContext/index.ts b/packages/leafygreen-provider/src/PopoverPropsContext/index.ts new file mode 100644 index 0000000000..26162fc2e5 --- /dev/null +++ b/packages/leafygreen-provider/src/PopoverPropsContext/index.ts @@ -0,0 +1,9 @@ +export { + PopoverPropsContext, + PopoverPropsProvider, + usePopoverPropsContext, +} from './PopoverPropsContext'; +export { + type PopoverPropsProviderProps, + RenderMode, +} from './PopoverPropsContext.types'; diff --git a/packages/leafygreen-provider/src/PortalContext.tsx b/packages/leafygreen-provider/src/PortalContext.tsx index a1794b3a4b..dc2930f069 100644 --- a/packages/leafygreen-provider/src/PortalContext.tsx +++ b/packages/leafygreen-provider/src/PortalContext.tsx @@ -29,7 +29,7 @@ interface PortalContext { children: React.ReactNode; } -export default function PortalContextProvider({ +export function PortalContextProvider({ popover = defaultPortalContextValues.popover, children, }: PortalContext) { diff --git a/packages/leafygreen-provider/src/index.ts b/packages/leafygreen-provider/src/index.ts index 55cb6400dd..688fdd0468 100644 --- a/packages/leafygreen-provider/src/index.ts +++ b/packages/leafygreen-provider/src/index.ts @@ -1,12 +1,21 @@ export { useDarkMode, useDarkModeContext } from './DarkModeContext'; -export { default, type LeafyGreenProviderProps } from './LeafyGreenContext'; +export { default } from './LeafyGreenContext'; +export { type LeafyGreenProviderProps } from './LeafyGreenContext.types'; +export { useMigrationContext } from './MigrationContext'; export { PopoverContext, PopoverProvider, usePopoverContext, } from './PopoverContext'; export { - default as PortalContextProvider, + PopoverPropsContext, + PopoverPropsProvider, + type PopoverPropsProviderProps, + RenderMode, + usePopoverPropsContext, +} from './PopoverPropsContext'; +export { + PortalContextProvider, usePopoverPortalContainer, } from './PortalContext'; export { useBaseFontSize } from './TypographyContext'; diff --git a/packages/menu/README.md b/packages/menu/README.md index 9efda94fa9..43d819e88a 100644 --- a/packages/menu/README.md +++ b/packages/menu/README.md @@ -207,24 +207,23 @@ would render, but without the correct styles. ## Properties -| Prop | Type | Description | Default | -| ------------------ | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -| `open` | `boolean` | Determines whether or not the `` will appear as open or closed | `false` | -| `setOpen` | `function` | When controlling the component, use `setOpen` to keep track of the `` component's state so that clicks on the document's backdrop as well as a user pressing the Escape Key will close the Menu and update the consuming application's local state accordingly. | | -| `initialOpen` | `boolean` | Passes an initial value for "open" to an uncontrolled menu | `false` | -| `shouldClose` | `function` | Determines if the `Menu` should close when the backdrop or Escape keys are clicked. Defaults to true. | `() => true` | -| `align` | `'top'`, `'bottom'`, `'left'`, `'right'` | Determines the alignment of the `` component relative to a reference element, or the element's nearest parent | `'bottom'` | -| `justify` | `'start'`, `'middle'`, `'end'` | Determines the justification of the `Menu` component (based on the alignment) relative to a reference element or the element's nearest parent | `'end'` | -| `refEl` | `HTMLElement` | Pass a reference to an element that the `Menu` component should be positioned against | | -| `trigger` | `function`, `React.ReactNode` | A `ReactNode` against which the Menu will be positioned. The trigger prop can also support being passed a function. To work as expected, the function must accept an argument of `children`, which should be rendered inside of the function passed to trigger. | | -| `usePortal` | `boolean` | Will position Menu's children relative to its parent without using a Portal if `usePortal` is set to false. NOTE: The parent element should be CSS position relative, fixed, or absolute if using this option. | `true` | -| `adjustOnMutation` | `boolean` | Determines whether or not the `` should reposition itself based on changes to `trigger` or reference element position. | `false` | -| `usePortal` | `boolean` | Determines if the Menu will be rendered within a portal. | `true` | -| `portalContainer` | `HTMLElement`, `null` | Sets the container used for the popover's portal. | | -| `scrollContainer` | `HTMLElement`, `null` | If the popover portal has a scrollable ancestor other than the window, this prop allows passing a reference to that lement to allow the portal to position properly. | | -| `portalClassName` | `string` | Passes the given className to the popover's portal container if the default portal container is being used. | | -| `popoverZIndex` | `number` | Sets the z-index CSS property for the popover. | | -| `darkMode` | `boolean` | Determines whether or not the component will be rendered in dark theme. | | +| Prop | Type | Description | Default | +| ------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `open` | `boolean` | Determines whether or not the `` will appear as open or closed | `false` | +| `setOpen` | `function` | When controlling the component, use `setOpen` to keep track of the `` component's state so that clicks on the document's backdrop as well as a user pressing the Escape Key will close the Menu and update the consuming application's local state accordingly. | | +| `initialOpen` | `boolean` | Passes an initial value for "open" to an uncontrolled menu | `false` | +| `shouldClose` | `function` | Determines if the `Menu` should close when the backdrop or Escape keys are clicked. Defaults to true. | `() => true` | +| `align` | `'top'`, `'bottom'`, `'left'`, `'right'` | Determines the alignment of the `` component relative to a reference element, or the element's nearest parent | `'bottom'` | +| `justify` | `'start'`, `'middle'`, `'end'` | Determines the justification of the `Menu` component (based on the alignment) relative to a reference element or the element's nearest parent | `'end'` | +| `refEl` | `HTMLElement` | Pass a reference to an element that the `Menu` component should be positioned against | | +| `trigger` | `function`, `React.ReactNode` | A `ReactNode` against which the Menu will be positioned. The trigger prop can also support being passed a function. To work as expected, the function must accept an argument of `children`, which should be rendered inside of the function passed to trigger. | | +| `adjustOnMutation` | `boolean` | Determines whether or not the `` should reposition itself based on changes to `trigger` or reference element position. | `false` | +| `renderMode` | `'inline'` \| `'portal'` \| `'top-layer'` | Options to render the popover element
\* [deprecated] `'inline'` will render the popover element inline in the DOM where it's written
\* [deprecated] `'portal'` will render the popover element in a new div appended to the body. Alternatively, can be portaled into a provided `portalContainer`
\* `'top-layer'` will render the popover element in the top layer | `'top-layer'` | +| `portalContainer` (deprecated) | `HTMLElement`, `null` | Sets the container used for the popover's portal. | | +| `scrollContainer` (deprecated) | `HTMLElement`, `null` | If the popover portal has a scrollable ancestor other than the window, this prop allows passing a reference to that lement to allow the portal to position properly. | | +| `portalClassName` (deprecated) | `string` | Passes the given className to the popover's portal container if the default portal container is being used. | | +| `popoverZIndex` (deprecated) | `number` | Sets the z-index CSS property for the popover. | | +| `darkMode` | `boolean` | Determines whether or not the component will be rendered in dark theme. | | _Any other properties will be spread on the Menu `ul` container_ diff --git a/packages/menu/src/Menu.spec.tsx b/packages/menu/src/Menu.spec.tsx index 0067ecb326..cab9246189 100644 --- a/packages/menu/src/Menu.spec.tsx +++ b/packages/menu/src/Menu.spec.tsx @@ -10,6 +10,7 @@ import { import userEvent from '@testing-library/user-event'; import { Optional } from '@leafygreen-ui/lib'; +import { RenderMode } from '@leafygreen-ui/popover'; import { waitForTransition } from '@leafygreen-ui/testing-lib'; import { LGIDs } from './constants'; @@ -255,9 +256,9 @@ describe('packages/menu', () => { expect(menuEl).not.toBeInTheDocument(); }); - test('Returns focus to trigger {usePortal: true}', async () => { + test(`Returns focus to trigger when renderMode=${RenderMode.TopLayer}`, async () => { const { openMenu, triggerEl } = renderMenu({ - usePortal: true, + renderMode: RenderMode.TopLayer, }); const { menuEl } = await openMenu(); @@ -266,9 +267,20 @@ describe('packages/menu', () => { expect(triggerEl).toHaveFocus(); }); - test('Returns focus to trigger {usePortal: false}', async () => { + test(`Returns focus to trigger when renderMode=${RenderMode.Portal}`, async () => { const { openMenu, triggerEl } = renderMenu({ - usePortal: false, + renderMode: RenderMode.Portal, + }); + const { menuEl } = await openMenu(); + + userEventInteraction(menuEl!, key); + await waitForElementToBeRemoved(menuEl); + expect(triggerEl).toHaveFocus(); + }); + + test(`Returns focus to trigger when renderMode=${RenderMode.Inline}`, async () => { + const { openMenu, triggerEl } = renderMenu({ + renderMode: RenderMode.Inline, }); const { menuEl } = await openMenu(); @@ -282,10 +294,8 @@ describe('packages/menu', () => { describe('Down arrow', () => { test('highlights the next option in the menu', async () => { const { openMenu } = renderMenu({}); - const { menuItemElements } = await openMenu(); - - userEvent.keyboard('{arrowdown}'); - expect(menuItemElements[0]).toHaveFocus(); + const { menuItemElements } = await openMenu({ withKeyboard: true }); + await waitFor(() => expect(menuItemElements[0]).toHaveFocus()); userEvent.keyboard('{arrowdown}'); expect(menuItemElements[1]).toHaveFocus(); @@ -293,9 +303,10 @@ describe('packages/menu', () => { test('cycles highlight to the top', async () => { const { openMenu } = renderMenu({}); - const { menuItemElements } = await openMenu(); + const { menuItemElements } = await openMenu({ withKeyboard: true }); + await waitFor(() => expect(menuItemElements[0]).toHaveFocus()); - for (let i = 0; i <= menuItemElements.length; i++) { + for (let i = 0; i < menuItemElements.length; i++) { userEvent.keyboard('{arrowdown}'); } @@ -317,7 +328,7 @@ describe('packages/menu', () => { const { menuItemElements } = await openMenu({ withKeyboard: true }); expect(menuItemElements).toHaveLength(3); - expect(queryByTestId('submenu')).toHaveFocus(); + await waitFor(() => expect(queryByTestId('submenu')).toHaveFocus()); userEvent.keyboard('{arrowdown}'); expect(queryByTestId('item-a')).toHaveFocus(); }); @@ -337,7 +348,7 @@ describe('packages/menu', () => { const { menuItemElements } = await openMenu({ withKeyboard: true }); expect(menuItemElements).toHaveLength(2); - expect(queryByTestId('submenu')).toHaveFocus(); + await waitFor(() => expect(queryByTestId('submenu')).toHaveFocus()); userEvent.keyboard('{arrowdown}'); expect(queryByTestId('item-c')).toHaveFocus(); }); @@ -347,9 +358,9 @@ describe('packages/menu', () => { describe('Up arrow', () => { test('highlights the previous option in the menu', async () => { const { openMenu } = renderMenu({}); - const { menuItemElements } = await openMenu(); + const { menuItemElements } = await openMenu({ withKeyboard: true }); + await waitFor(() => expect(menuItemElements[0]).toHaveFocus()); - userEvent.keyboard('{arrowdown}'); userEvent.keyboard('{arrowdown}'); expect(menuItemElements[1]).toHaveFocus(); @@ -359,7 +370,8 @@ describe('packages/menu', () => { test('cycles highlight to the bottom', async () => { const { openMenu } = renderMenu({}); - const { menuItemElements } = await openMenu(); + const { menuItemElements } = await openMenu({ withKeyboard: true }); + await waitFor(() => expect(menuItemElements[0]).toHaveFocus()); const lastOption = menuItemElements[menuItemElements.length - 1]; userEvent.keyboard('{arrowup}'); @@ -378,7 +390,7 @@ describe('packages/menu', () => { const firstItem = menuItemElements[0]; - expect(firstItem).toHaveFocus(); + await waitFor(() => expect(firstItem).toHaveFocus()); userEvent.keyboard('[Enter]'); @@ -396,7 +408,7 @@ describe('packages/menu', () => { const firstItem = menuItemElements[0]; - expect(firstItem).toHaveFocus(); + await waitFor(() => expect(firstItem).toHaveFocus()); userEvent.keyboard('[Space]'); @@ -427,7 +439,8 @@ describe('packages/menu', () => { }); await openMenu({ withKeyboard: true }); - expect(queryByTestId('submenu')).toHaveFocus(); + await waitFor(() => expect(queryByTestId('submenu')).toHaveFocus()); + userEvent.click(getByTestId(LGIDs.submenuToggle)!); await waitForTransition(); await waitFor(() => { @@ -451,7 +464,8 @@ describe('packages/menu', () => { }); await openMenu({ withKeyboard: true }); - expect(queryByTestId('submenu')).toHaveFocus(); + await waitFor(() => expect(queryByTestId('submenu')).toHaveFocus()); + userEvent.keyboard('{arrowright}'); userEvent.keyboard('{arrowdown}'); expect(queryByTestId('item-a')).toHaveFocus(); @@ -484,7 +498,8 @@ describe('packages/menu', () => { ), }); await openMenu({ withKeyboard: true }); - expect(queryByTestId('submenu')).toHaveFocus(); + await waitFor(() => expect(queryByTestId('submenu')).toHaveFocus()); + userEvent.keyboard('{arrowup}'); expect(queryByTestId('item-c')).toHaveFocus(); @@ -513,7 +528,8 @@ describe('packages/menu', () => { ), }); await openMenu({ withKeyboard: true }); - expect(queryByTestId('submenu')).toHaveFocus(); + await waitFor(() => expect(queryByTestId('submenu')).toHaveFocus()); + userEvent.keyboard('{arrowright}'); // open the submenu userEvent.keyboard('{arrowup}'); expect(queryByTestId('item-c')).toHaveFocus(); diff --git a/packages/menu/src/Menu.stories.tsx b/packages/menu/src/Menu.stories.tsx index 67614b59ec..81bec1bd2f 100644 --- a/packages/menu/src/Menu.stories.tsx +++ b/packages/menu/src/Menu.stories.tsx @@ -15,7 +15,7 @@ import Icon from '@leafygreen-ui/icon'; import CaretDown from '@leafygreen-ui/icon/dist/CaretDown'; import CloudIcon from '@leafygreen-ui/icon/dist/Cloud'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; -import { Align, Justify } from '@leafygreen-ui/popover'; +import { Align, Justify, RenderMode } from '@leafygreen-ui/popover'; import { TestUtils } from '@leafygreen-ui/popover'; const { getAlign, getJustify } = TestUtils; @@ -47,7 +47,13 @@ export default { decorators: [ (StoryFn, _ctx) => ( - +
+ +
), ], @@ -63,13 +69,39 @@ export default { 'setOpen', 'size', 'trigger', - 'usePortal', ], }, + generate: { + storyNames: [ + 'LightModeTopAlign', + 'DarkModeTopAlign', + 'LightModeBottomAlign', + 'DarkModeBottomAlign', + 'LightModeLeftAlign', + 'DarkModeLeftAlign', + 'LightModeRightAlign', + 'DarkModeRightAlign', + ], + combineArgs: { + justify: Object.values(Justify), + }, + excludeCombinations: [ + { + align: [Align.CenterHorizontal, Align.CenterVertical], + }, + ], + decorator: (Instance, ctx) => ( + +
+ +
+
+ ), + }, }, args: { - align: 'bottom', - usePortal: true, + align: Align.Bottom, + renderMode: RenderMode.TopLayer, darkMode: false, renderDarkMenu: false, }, @@ -196,7 +228,7 @@ export const Controlled = { 'setOpen', 'as', 'portalRef', - 'usePortal', + 'renderMode', 'align', 'darkMode', 'justify', @@ -210,58 +242,101 @@ export const Controlled = { }, } satisfies StoryObj; -export const Generated = { - render: () => <>, +const sharedGeneratedStoryArgs = { + open: true, + maxHeight: 200, + children: ( + <> + Lorem + } + active={true} + > + Apple + Banana + Carrot + Dragonfruit + Eggplant + Fig + + + ), + trigger: , +}; + +export const LightModeTopAlign = { + render: <>, args: { - open: true, - maxHeight: 200, - children: ( - <> - Lorem - } - active={true} - > - Apple - Banana - Carrot - Dragonfruit - Eggplant - Fig - - - ), - trigger: , + ...sharedGeneratedStoryArgs, + darkMode: false, + align: Align.Top, }, - parameters: { - generate: { - combineArgs: { - darkMode: [false, true], - align: Object.values(Align), - justify: Object.values(Justify), - }, +}; - excludeCombinations: [ - { - align: [Align.CenterHorizontal, Align.CenterVertical], - }, - { - justify: Justify.Fit, - align: [Align.Left, Align.Right], - }, - ], - decorator: (Instance, ctx) => ( - -
- -
-
- ), - }, +export const DarkModeTopAlign = { + render: <>, + args: { + ...sharedGeneratedStoryArgs, + darkMode: true, + align: Align.Top, }, -} satisfies StoryObj; +}; + +export const LightModeBottomAlign = { + render: <>, + args: { + ...sharedGeneratedStoryArgs, + darkMode: false, + align: Align.Bottom, + }, +}; + +export const DarkModeBottomAlign = { + render: <>, + args: { + ...sharedGeneratedStoryArgs, + darkMode: true, + align: Align.Bottom, + }, +}; + +export const LightModeLeftAlign = { + render: <>, + args: { + ...sharedGeneratedStoryArgs, + darkMode: false, + align: Align.Left, + }, +}; + +export const DarkModeLeftAlign = { + render: <>, + args: { + ...sharedGeneratedStoryArgs, + darkMode: true, + align: Align.Left, + }, +}; + +export const LightModeRightAlign = { + render: <>, + args: { + ...sharedGeneratedStoryArgs, + darkMode: false, + align: Align.Right, + }, +}; + +export const DarkModeRightAlign = { + render: <>, + args: { + ...sharedGeneratedStoryArgs, + darkMode: true, + align: Align.Right, + }, +}; export const InitialLongMenuOpen = { render: () => { diff --git a/packages/menu/src/Menu/Menu.tsx b/packages/menu/src/Menu/Menu.tsx index 6d138267cd..c2d3577482 100644 --- a/packages/menu/src/Menu/Menu.tsx +++ b/packages/menu/src/Menu/Menu.tsx @@ -9,7 +9,13 @@ import { css, cx } from '@leafygreen-ui/emotion'; import { useBackdropClick, useEventListener } from '@leafygreen-ui/hooks'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; import { isDefined, keyMap, Theme } from '@leafygreen-ui/lib'; -import Popover, { Align, Justify } from '@leafygreen-ui/popover'; +import Popover, { + Align, + DismissMode, + getPopoverRenderModeProps, + Justify, + RenderMode, +} from '@leafygreen-ui/popover'; import { LGIDs } from '../constants'; import { useHighlightReducer } from '../HighlightReducer'; @@ -40,7 +46,7 @@ import { MenuProps } from './Menu.types'; * @param props.align Alignment of Menu relative to another element: `top`, `bottom`, `left`, `right`. * @param props.justify Justification of Menu relative to another element: `start`, `middle`, `end`. * @param props.refEl Reference element that Menu should be positioned against. - * @param props.usePortal Boolean to describe if content should be portaled to end of DOM, or appear in DOM tree. + * @param props.renderMode Options to render the popover element: `inline`, `portal`, `top-layer`. * @param props.trigger Trigger element can be ReactNode or function, and, if present, internally manages active state of Menu. * @param props.darkMode Determines whether or not the component will be rendered in dark theme. */ @@ -52,7 +58,6 @@ export const Menu = React.forwardRef(function Menu( shouldClose = () => true, spacing = 6, maxHeight = 344, - usePortal = true, initialOpen = false, open: controlledOpen, setOpen: controlledSetOpen, @@ -62,6 +67,7 @@ export const Menu = React.forwardRef(function Menu( className, refEl, trigger, + renderMode = RenderMode.TopLayer, portalClassName, portalContainer, portalRef, @@ -74,7 +80,8 @@ export const Menu = React.forwardRef(function Menu( const { theme, darkMode } = useDarkMode(darkModeProp); const popoverRef = useRef(null); - const triggerRef = useRef(null); + const defaultTriggerRef = useRef(null); + const triggerRef = refEl ?? defaultTriggerRef; const keyboardUsedRef = useRef(false); const [uncontrolledOpen, uncontrolledSetOpen] = useState(initialOpen); @@ -91,7 +98,7 @@ export const Menu = React.forwardRef(function Menu( }, [setOpen, shouldClose]); const maxMenuHeightValue = useMenuHeight({ - refEl: refEl || triggerRef, + refEl: triggerRef, spacing, maxHeight, }); @@ -137,14 +144,14 @@ export const Menu = React.forwardRef(function Menu( break; case keyMap.Tab: - e.preventDefault(); // Prevent tabbing outside of portal and outside of the DOM when `usePortal={true}` + e.preventDefault(); // Prevent tabbing outside of portal and outside of the DOM when `renderMode="portal"` handleClose(); - (refEl || triggerRef)?.current?.focus(); // Focus the trigger on close + triggerRef?.current?.focus(); // Focus the trigger on close break; case keyMap.Escape: handleClose(); - (refEl || triggerRef)?.current?.focus(); // Focus the trigger on close + triggerRef?.current?.focus(); // Focus the trigger on close break; case keyMap.Space: @@ -160,17 +167,16 @@ export const Menu = React.forwardRef(function Menu( const popoverProps = { popoverZIndex, - ...(usePortal - ? { - spacing, - usePortal, - portalClassName, - portalContainer, - portalRef, - scrollContainer, - } - : { spacing, usePortal }), - }; + spacing, + ...getPopoverRenderModeProps({ + dismissMode: DismissMode.Manual, + portalClassName, + portalContainer, + portalRef, + renderMode, + scrollContainer, + }), + } as const; const popoverContent = ( @@ -189,7 +195,7 @@ export const Menu = React.forwardRef(function Menu( active={open} align={align} justify={justify} - refEl={refEl} + refEl={triggerRef} adjustOnMutation={adjustOnMutation} onEntered={handlePopoverOpen} data-testid={LGIDs.root} @@ -252,29 +258,32 @@ export const Menu = React.forwardRef(function Menu( }; if (typeof trigger === 'function') { - return trigger({ - onClick: triggerClickHandler, - ref: triggerRef, - children: popoverContent, - ['aria-expanded']: open, - ['aria-haspopup']: true, - }); + return ( + <> + {trigger({ + onClick: triggerClickHandler, + ref: triggerRef, + ['aria-expanded']: open, + ['aria-haspopup']: true, + })} + {popoverContent} + + ); } const renderedTrigger = React.cloneElement(trigger, { ref: triggerRef, onClick: triggerClickHandler, - children: ( - <> - {trigger.props.children} - {popoverContent} - - ), ['aria-expanded']: open, ['aria-haspopup']: true, }); - return renderedTrigger; + return ( + <> + {renderedTrigger} + {popoverContent} + + ); } return popoverContent; @@ -299,7 +308,7 @@ Menu.propTypes = { ? PropTypes.instanceOf(Element) : PropTypes.any, }), - usePortal: PropTypes.bool, + renderMode: PropTypes.oneOf(Object.values(RenderMode)), trigger: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), open: PropTypes.bool, setOpen: PropTypes.func, diff --git a/packages/menu/src/Menu/Menu.types.ts b/packages/menu/src/Menu/Menu.types.ts index 0faaacb6b8..48399be7df 100644 --- a/packages/menu/src/Menu/Menu.types.ts +++ b/packages/menu/src/Menu/Menu.types.ts @@ -12,7 +12,8 @@ export type SubMenuType = ReactElement< InferredPolymorphicPropsWithRef >; -export interface MenuProps extends Omit { +export interface MenuProps + extends Omit { /** * The menu items, or submenus * @type `` | `` | `` | `` diff --git a/packages/modal/package.json b/packages/modal/package.json index 66551cb6ea..dfab28b25b 100644 --- a/packages/modal/package.json +++ b/packages/modal/package.json @@ -38,11 +38,11 @@ }, "devDependencies": { "@faker-js/faker": "8.0.2", + "@leafygreen-ui/button": "^21.2.0", + "@leafygreen-ui/code": "^14.3.3", + "@leafygreen-ui/copyable": "^8.0.25", "@leafygreen-ui/select": "^12.1.0", "@leafygreen-ui/typography": "^19.1.0", - "@leafygreen-ui/copyable": "^8.0.25", - "@leafygreen-ui/code": "^14.3.3", - "@leafygreen-ui/button": "^21.2.0", "@lg-tools/storybook-utils": "^0.1.1" }, "peerDependencies": { diff --git a/packages/modal/src/Modal.stories.tsx b/packages/modal/src/Modal.stories.tsx index f3868558d9..d5794209a7 100644 --- a/packages/modal/src/Modal.stories.tsx +++ b/packages/modal/src/Modal.stories.tsx @@ -156,7 +156,6 @@ export const DefaultSelect = (args: ModalProps) => { name="pets" value={value} onChange={setValue} - usePortal={true} > diff --git a/packages/modal/src/Modal/Modal.spec.tsx b/packages/modal/src/Modal/Modal.spec.tsx index b04c027e1a..ceb24e2a4d 100644 --- a/packages/modal/src/Modal/Modal.spec.tsx +++ b/packages/modal/src/Modal/Modal.spec.tsx @@ -139,7 +139,6 @@ describe('packages/modal', () => { size="small" placeholder="animals" name="pets" - usePortal={true} data-testid="modal-select-test-id" > diff --git a/packages/number-input/README.md b/packages/number-input/README.md index f6fccb5062..fd80f1928c 100644 --- a/packages/number-input/README.md +++ b/packages/number-input/README.md @@ -62,29 +62,25 @@ or ## Properties -| Prop | Type | Description | Default | -| ----------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------- | -| `id` | `string` | id associated with the number input. | | -| `label` | `string` | Label shown above the number input. | | -| `description` | `string` | Text shown above the number input but below the label; gives more details about the requirements for the input. | | -| `value` | `string` | The controlled value of the number input. | | -| `disabled` | `boolean` | Disables the component. | `false` | -| `placeholder` | `string` | The placeholder text shown in the input field before the user begins typing. | | -| `size` | `'xsmall'` \| `'small'` \| `'default'` \| `'large'` | Determines the size of the input. | `default` | -| `state` | `'none'`\| `'error'` | Describes the state of the TextInput element before and after the input has been validated | `'none'` | -| `errorMessage` | `string` | Error message that appears below the input. Renders only if `state='error'`. | `'This input needs your attention'` | -| `successMessage` | `string` | Success message that appears below the input. Renders only if `state='valid'`. | `'Success'` | -| `unit` | `string` | The string unit that appears to the right of the input if using a single unit.

Required if using `unitOptions`. When using `unitOptions` this value becomes the controlled value of the select input. | `default` | -| `unitOptions` | `Array<{displayName: string; value: string}>` | The options that appear in the select element attached to the right of the input. | `default` | -| `onChange` | `(e: React.ChangeEvent) => void` | The event handler function for the 'onchange' event. Accepts the change event object as its argument and returns nothing. | -| `onBlur` | `(e: React.ChangeEvent) => void` | The event handler function for the 'onblur' event. Accepts the focus event object as its argument and returns nothing. | | -| `onSelectChange` | `(unit: {displayName: string; value: string}) => void` | A change handler triggered when the unit is changed. | -| `className` | `string` | ClassName for the component. | | -| `inputClassName` | `string` | ClassName for the input component. | | -| `selectClassName` | `string` | ClassName for the select component. | | -| `portalContainer` | `HTMLElement` \| `null` | Sets the container used for the popover's portal. NOTE: If using a `scrollContainer` make sure that the `portalContainer` is contained within the `scrollContainer`. E.g, passing the same reference to `scrollContainer` and `portalContainer`. | | -| `scrollContainer` | `HTMLElement` \| `null` | If the popover portal has a scrollable ancestor other than the window, this prop allows passing a reference to that element to allow the portal to position properly. | | -| `portalClassName` | `string` | Passes the given className to the popover's portal container if the default portal container is being used. | | -| `popoverZIndex` | `number` | Sets the z-index CSS property for the popover. | | -| `darkMode` | `boolean` | Render the component in dark mode. | `false` | -| ... | native `input` attributes | Any other props will be spread on the root `input` element | | +| Prop | Type | Description | Default | +| ----------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | +| `id` | `string` | id associated with the number input. | | +| `label` | `string` | Label shown above the number input. | | +| `description` | `string` | Text shown above the number input but below the label; gives more details about the requirements for the input. | | +| `value` | `string` | The controlled value of the number input. | | +| `disabled` | `boolean` | Disables the component. | `false` | +| `placeholder` | `string` | The placeholder text shown in the input field before the user begins typing. | | +| `size` | `'xsmall'` \| `'small'` \| `'default'` \| `'large'` | Determines the size of the input. | `default` | +| `state` | `'none'`\| `'error'` | Describes the state of the TextInput element before and after the input has been validated | `'none'` | +| `errorMessage` | `string` | Error message that appears below the input. Renders only if `state='error'`. | `'This input needs your attention'` | +| `successMessage` | `string` | Success message that appears below the input. Renders only if `state='valid'`. | `'Success'` | +| `unit` | `string` | The string unit that appears to the right of the input if using a single unit.

Required if using `unitOptions`. When using `unitOptions` this value becomes the controlled value of the select input. | `default` | +| `unitOptions` | `Array<{displayName: string; value: string}>` | The options that appear in the select element attached to the right of the input. | `default` | +| `onChange` | `(e: React.ChangeEvent) => void` | The event handler function for the 'onchange' event. Accepts the change event object as its argument and returns nothing. | +| `onBlur` | `(e: React.ChangeEvent) => void` | The event handler function for the 'onblur' event. Accepts the focus event object as its argument and returns nothing. | | +| `onSelectChange` | `(unit: {displayName: string; value: string}) => void` | A change handler triggered when the unit is changed. | +| `className` | `string` | ClassName for the component. | | +| `inputClassName` | `string` | ClassName for the input component. | | +| `selectClassName` | `string` | ClassName for the select component. | | +| `darkMode` | `boolean` | Render the component in dark mode. | `false` | +| ... | native `input` attributes | Any other props will be spread on the root `input` element | | diff --git a/packages/number-input/src/NumberInput.stories.tsx b/packages/number-input/src/NumberInput.stories.tsx index 544ecd94eb..632149b961 100644 --- a/packages/number-input/src/NumberInput.stories.tsx +++ b/packages/number-input/src/NumberInput.stories.tsx @@ -33,6 +33,17 @@ const unitOptions = [ const meta: StoryMetaType = { title: 'Components/NumberInput', component: NumberInput, + decorators: [ + StoryFn => ( +
+ +
+ ), + ], parameters: { default: 'LiveExample', controls: { diff --git a/packages/number-input/src/NumberInput/NumberInput.spec.tsx b/packages/number-input/src/NumberInput/NumberInput.spec.tsx index 4801bc447a..f65526e539 100644 --- a/packages/number-input/src/NumberInput/NumberInput.spec.tsx +++ b/packages/number-input/src/NumberInput/NumberInput.spec.tsx @@ -1,4 +1,4 @@ -import React, { createRef } from 'react'; +import React from 'react'; import { fireEvent, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { axe } from 'jest-axe'; @@ -351,21 +351,6 @@ describe('packages/number-input', () => { value: selectProps.unitOptions[1].value, }); }); - - test('accepts a portalRef', () => { - const portalContainer = document.createElement('div'); - document.body.appendChild(portalContainer); - const portalRef = createRef(); - const { getByRole } = renderNumberInput({ - ...selectProps, - portalContainer, - portalRef, - }); - const trigger = getByRole('button', { name: unitProps.unit }); - fireEvent.click(trigger); - expect(portalRef.current).toBeDefined(); - expect(portalRef.current).toBe(portalContainer); - }); }); /* eslint-disable jest/no-disabled-tests */ @@ -418,54 +403,6 @@ describe('packages/number-input', () => { id="1" size={Size.Default} /> - - {/* @ts-expect-error - portalClassName should be undefined */} - {}} - label={label} - usePortal={false} - portalClassName="classname" - /> - - {/* @ts-expect-error - scrollContainer should be undefined */} - {}} - label={label} - usePortal={false} - scrollContainer={{} as HTMLElement} - /> - - {/* @ts-expect-error - portalContainer should be undefined */} - {}} - label={label} - usePortal={false} - portalContainer={{} as HTMLElement} - /> - - {}} - label={label} - usePortal={false} - /> - - {}} - label={label} - portalContainer={{} as HTMLElement} - scrollContainer={{} as HTMLElement} - portalClassName="classname" - /> ; }); }); diff --git a/packages/number-input/src/NumberInput/NumberInput.tsx b/packages/number-input/src/NumberInput/NumberInput.tsx index 6bb5702b5c..c9550eab3e 100644 --- a/packages/number-input/src/NumberInput/NumberInput.tsx +++ b/packages/number-input/src/NumberInput/NumberInput.tsx @@ -46,12 +46,6 @@ export const NumberInput = React.forwardRef( errorMessage = DEFAULT_MESSAGES.error, successMessage = DEFAULT_MESSAGES.success, onChange, - popoverZIndex, - usePortal = true, - portalClassName, - portalContainer, - portalRef, - scrollContainer, ...rest }: NumberInputProps, forwardedRef, @@ -70,19 +64,6 @@ export const NumberInput = React.forwardRef( const renderUnitOnly = hasUnit && !hasSelectOptions; const renderSelectOnly = hasUnit && hasSelectOptions && !!isUnitInOptions; - const popoverProps = { - popoverZIndex, - ...(usePortal - ? { - usePortal, - portalClassName, - portalContainer, - portalRef, - scrollContainer, - } - : { usePortal }), - } as const; - const formFieldFeedbackProps = { disabled, errorMessage, @@ -152,7 +133,6 @@ export const NumberInput = React.forwardRef( onChange={onSelectChange} size={size} className={selectClassName} - {...popoverProps} /> )}
@@ -190,21 +170,4 @@ NumberInput.propTypes = { value: PropTypes.string.isRequired, }), ), - // Popover Props - popoverZIndex: PropTypes.number, - scrollContainer: - typeof window !== 'undefined' - ? PropTypes.instanceOf(Element) - : PropTypes.any, - portalContainer: - typeof window !== 'undefined' - ? PropTypes.instanceOf(Element) - : PropTypes.any, - portalRef: PropTypes.shape({ - current: - typeof window !== 'undefined' - ? PropTypes.instanceOf(Element) - : PropTypes.any, - }), - portalClassName: PropTypes.string, } as any; diff --git a/packages/number-input/src/NumberInput/NumberInput.types.ts b/packages/number-input/src/NumberInput/NumberInput.types.ts index 4e3a3861d8..4bb531343b 100644 --- a/packages/number-input/src/NumberInput/NumberInput.types.ts +++ b/packages/number-input/src/NumberInput/NumberInput.types.ts @@ -8,8 +8,6 @@ import { AriaLabelPropsWithLabel } from '@leafygreen-ui/a11y'; import { FormFieldState } from '@leafygreen-ui/form-field'; import { DarkModeProps } from '@leafygreen-ui/lib'; -import { PopoverProps } from '../UnitSelect/UnitSelect.types'; - export const Direction = { Increment: 'increment', Decrement: 'decrement', @@ -154,5 +152,4 @@ export interface BaseNumberInputProps export type NumberInputProps = BaseNumberInputProps & ConditionalUnitSelectProps & - PopoverProps & AriaLabelPropsWithLabel; diff --git a/packages/number-input/src/UnitSelect/UnitSelect.tsx b/packages/number-input/src/UnitSelect/UnitSelect.tsx index 2dd4ed0c86..f561f00b0f 100644 --- a/packages/number-input/src/UnitSelect/UnitSelect.tsx +++ b/packages/number-input/src/UnitSelect/UnitSelect.tsx @@ -2,7 +2,12 @@ import React from 'react'; import { cx } from '@leafygreen-ui/emotion'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; -import { DropdownWidthBasis, Option, Select } from '@leafygreen-ui/select'; +import { + DropdownWidthBasis, + Option, + RenderMode, + Select, +} from '@leafygreen-ui/select'; import { UnitOption } from '../NumberInput/NumberInput.types'; import { UnitSelectButton } from '../UnitSelectButton'; @@ -24,26 +29,11 @@ export function UnitSelect({ unitOptions, onChange, disabled, - usePortal, size, className, - portalClassName, - portalContainer, - portalRef, - scrollContainer, - popoverZIndex, }: UnitSelectProps) { const { theme } = useDarkMode(); - const popoverProps = { - popoverZIndex, - usePortal, - portalClassName, - portalContainer, - portalRef, - scrollContainer, - } as const; - /** * Gets the current unit option using the unit string */ @@ -78,7 +68,7 @@ export function UnitSelect({ disabled={disabled} size={size} data-testid={dataTestId} - {...popoverProps} + renderMode={RenderMode.TopLayer} __INTERNAL__menuButtonSlot__={UnitSelectButton} __INTERNAL__menuButtonSlotProps__={{ disabled, diff --git a/packages/number-input/src/UnitSelect/UnitSelect.types.ts b/packages/number-input/src/UnitSelect/UnitSelect.types.ts index 439bf3362a..414c60eceb 100644 --- a/packages/number-input/src/UnitSelect/UnitSelect.types.ts +++ b/packages/number-input/src/UnitSelect/UnitSelect.types.ts @@ -1,14 +1,6 @@ -import { - PopoverProps as ImportedPopoverProps, - PortalControlProps, -} from '@leafygreen-ui/popover'; - import { Size, UnitOption } from '../NumberInput/NumberInput.types'; -export type PopoverProps = PortalControlProps & - Pick; - -export type UnitSelectProps = { +export interface UnitSelectProps { /** * Id for the select component. */ @@ -51,4 +43,4 @@ export type UnitSelectProps = { * @internal */ ['data-testid']?: string; -} & PopoverProps; +} diff --git a/packages/number-input/src/UnitSelectButton/UnitSelectButton.tsx b/packages/number-input/src/UnitSelectButton/UnitSelectButton.tsx index cae356ee07..64ca2cdff4 100644 --- a/packages/number-input/src/UnitSelectButton/UnitSelectButton.tsx +++ b/packages/number-input/src/UnitSelectButton/UnitSelectButton.tsx @@ -10,7 +10,7 @@ import { popoverClassName, } from '@leafygreen-ui/select'; import { Size } from '@leafygreen-ui/tokens'; -import Tooltip from '@leafygreen-ui/tooltip'; +import Tooltip, { Align, Justify, RenderMode } from '@leafygreen-ui/tooltip'; import { baseStyles, @@ -34,31 +34,17 @@ export const UnitSelectButton = React.forwardRef( children, disabled, displayName, - popoverZIndex, - usePortal, - portalClassName, - portalContainer, - portalRef, - scrollContainer, ...props }: UnitSelectButtonProps, forwardedRef, ) => { - const [open, setOpen] = useState(false); + const [tooltipOpen, setTooltipOpen] = useState(false); const buttonRef: React.MutableRefObject = useForwardedRef( forwardedRef, null, ) as React.MutableRefObject; const { theme } = useDarkMode(); - const popoverProps = { - popoverZIndex, - usePortal, - portalClassName, - portalContainer, - portalRef, - scrollContainer, - } as const; /** * Gets the text node for the selected option. @@ -86,25 +72,26 @@ export const UnitSelectButton = React.forwardRef( if (!popoverParent) { // React 18 automatically batches all updates which appears to break the opening transition. flushSync prevents this state update from automically batching. Instead updates are made synchronously. flushSync(() => { - setOpen(true); + setTooltipOpen(true); }); } } }; - const handleMouseLeave = () => setOpen(false); - const handleOnFocus = () => setOpen(true); - const handleOnBlur = () => setOpen(false); + const handleMouseLeave = () => setTooltipOpen(false); + const handleOnFocus = () => setTooltipOpen(true); + const handleOnBlur = () => setTooltipOpen(false); return (
{displayName} diff --git a/packages/number-input/src/UnitSelectButton/UnitSelectButton.types.ts b/packages/number-input/src/UnitSelectButton/UnitSelectButton.types.ts index 1babce97d9..3f68de4186 100644 --- a/packages/number-input/src/UnitSelectButton/UnitSelectButton.types.ts +++ b/packages/number-input/src/UnitSelectButton/UnitSelectButton.types.ts @@ -1,6 +1,6 @@ import { ButtonProps } from '@leafygreen-ui/button'; -import { PopoverProps, UnitSelectProps } from '../UnitSelect/UnitSelect.types'; +import { UnitSelectProps } from '../UnitSelect/UnitSelect.types'; export type UnitSelectButtonProps = { /** @@ -12,5 +12,4 @@ export type UnitSelectButtonProps = { * The select option that is shown in the select menu button. */ displayName?: string; -} & ButtonProps & - PopoverProps; +} & ButtonProps; diff --git a/packages/pagination/src/Pagination/Pagination.tsx b/packages/pagination/src/Pagination/Pagination.tsx index 439ec822f0..9dc40f0a0e 100644 --- a/packages/pagination/src/Pagination/Pagination.tsx +++ b/packages/pagination/src/Pagination/Pagination.tsx @@ -9,7 +9,12 @@ import ChevronRight from '@leafygreen-ui/icon/dist/ChevronRight'; import IconButton from '@leafygreen-ui/icon-button'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; -import { DropdownWidthBasis, Option, Select } from '@leafygreen-ui/select'; +import { + DropdownWidthBasis, + Option, + RenderMode, + Select, +} from '@leafygreen-ui/select'; import { Body } from '@leafygreen-ui/typography'; import { baseStyles, flexSectionStyles } from './Pagination.styles'; @@ -92,6 +97,7 @@ function Pagination({ allowDeselect={false} size="xsmall" dropdownWidthBasis={DropdownWidthBasis.Option} + renderMode={RenderMode.TopLayer} > {itemsPerPageOptions.map((option: number) => (