From c9b4db79ec2b4360f23f42d3ab49f265a56e9447 Mon Sep 17 00:00:00 2001 From: Van Anderson Date: Wed, 5 May 2021 14:18:58 -0500 Subject: [PATCH] Action Menu can have its open state be controlled externally (#1199) * Action Menu can be a controlled component with open state * update docs for ActionMenu * Create pink-lions-suffer.md * Handle onAction for DropdownMenu Items (#1194) * useProvidedStateOrCreate * linter fixes * update hook doc comment * Update src/hooks/useProvidedStateOrCreate.ts Co-authored-by: Dusty Greif * useProvidedOrCreateState has better implementation for generics * useProvidedOrCreateState has better implementation for generics * make string type explicit Co-authored-by: Dusty Greif --- .changeset/pink-lions-suffer.md | 5 +++ docs/content/ActionMenu.mdx | 18 +++++---- src/ActionMenu.tsx | 19 +++++---- .../hooks/useProvidedStateOrCreate.tsx | 39 ++++++++++++++++++ src/hooks/useProvidedStateOrCreate.ts | 27 +++++++++++++ src/stories/ActionMenu.stories.tsx | 40 +++++++++++++++++++ 6 files changed, 133 insertions(+), 15 deletions(-) create mode 100644 .changeset/pink-lions-suffer.md create mode 100644 src/__tests__/hooks/useProvidedStateOrCreate.tsx create mode 100644 src/hooks/useProvidedStateOrCreate.ts diff --git a/.changeset/pink-lions-suffer.md b/.changeset/pink-lions-suffer.md new file mode 100644 index 00000000000..54b3857da98 --- /dev/null +++ b/.changeset/pink-lions-suffer.md @@ -0,0 +1,5 @@ +--- +"@primer/components": patch +--- + +Action Menu can have its open state be controlled externally. diff --git a/docs/content/ActionMenu.mdx b/docs/content/ActionMenu.mdx index fef5d6e07fb..85a098b7423 100644 --- a/docs/content/ActionMenu.mdx +++ b/docs/content/ActionMenu.mdx @@ -68,11 +68,13 @@ An `ActionMenu` is an ActionList-based component for creating a menu of actions ## Component props -| Name | Type | Default | Description | -| :------------ | :------------------------------------ | :---------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| items | `ItemProps[]` | `undefined` | Required. A list of item objects conforming to the `ActionList.Item` props interface. | -| renderItem | `(props: ItemProps) => JSX.Element` | `ActionList.Item` | Optional. If defined, each item in `items` will be passed to this function, allowing for `ActionList`-wide custom item rendering. | -| groupMetadata | `GroupProps[]` | `undefined` | Optional. If defined, `ActionList` will group `items` into `ActionList.Group`s separated by `ActionList.Divider` according to their `groupId` property. | -| renderAnchor | `(props: ButtonProps) => JSX.Element` | `Button` | Optional. If defined, provided component will be used to render the menu anchor. Will receive the selected `Item` text as `children` prop when an item is activated. | -| anchorContent | React.ReactNode | `undefined` | Optional. If defined, it will be passed to the trigger as the elements child. | -| onAction | (props: ItemProps) => void | `undefined` | Optional. If defined, this function will be called when a menu item is activated either by a click or a keyboard press. | +| Name | Type | Default | Description | +| :------------ | :------------------------------------ | :---------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| items | `ItemProps[]` | `undefined` | Required. A list of item objects conforming to the `ActionList.Item` props interface. | +| renderItem | `(props: ItemProps) => JSX.Element` | `ActionList.Item` | Optional. If defined, each item in `items` will be passed to this function, allowing for `ActionList`-wide custom item rendering. | +| groupMetadata | `GroupProps[]` | `undefined` | Optional. If defined, `ActionList` will group `items` into `ActionList.Group`s separated by `ActionList.Divider` according to their `groupId` property. | +| renderAnchor | `(props: ButtonProps) => JSX.Element` | `Button` | Optional. If defined, provided component will be used to render the menu anchor. Will receive the selected `Item` text as `children` prop when an item is activated. | +| anchorContent | React.ReactNode | `undefined` | Optional. If defined, it will be passed to the trigger as the elements child. | +| onAction | (props: ItemProps) => void | `undefined` | Optional. If defined, this function will be called when a menu item is activated either by a click or a keyboard press. | +| open | boolean | `undefined` | Optional. If defined, ActionMenu will use this to control the open/closed state of the Overlay instead of controlling the state internally. Should be used in conjunction with the `setOpen` prop. | +| setOpen | (state: boolean) => void | `undefined` | Optional. If defined, ActionMenu will use this to control the open/closed state of the Overlay instead of controlling the state internally. Should be used in conjunction with the `open` prop. | diff --git a/src/ActionMenu.tsx b/src/ActionMenu.tsx index 8985e00a796..1b06bbeded5 100644 --- a/src/ActionMenu.tsx +++ b/src/ActionMenu.tsx @@ -2,14 +2,16 @@ import {GroupedListProps, List, ListPropsBase} from './ActionList/List' import {Item, ItemProps} from './ActionList/Item' import {Divider} from './ActionList/Divider' import Button, {ButtonProps} from './Button' -import React, {useCallback, useEffect, useRef, useState} from 'react' +import React, {useCallback, useEffect, useRef} from 'react' import {AnchoredOverlay} from './AnchoredOverlay' - +import {useProvidedStateOrCreate} from './hooks/useProvidedStateOrCreate' export interface ActionMenuProps extends Partial>, ListPropsBase { // eslint-disable-next-line @typescript-eslint/no-explicit-any renderAnchor?: (props: any) => JSX.Element anchorContent?: React.ReactNode - onAction?: (props: ItemProps, event: React.MouseEvent | React.KeyboardEvent) => void + onAction?: (props: ItemProps, event?: React.MouseEvent | React.KeyboardEvent) => void + open?: boolean + setOpen?: (s: boolean) => void } const ActionMenuItem = (props: ItemProps) => @@ -21,12 +23,14 @@ const ActionMenuBase = ({ renderAnchor = (props: T) => + +
+ + + + + ) +} +ExternalOpenState.storyName = 'External Open State' + export function ComplexListStory(): JSX.Element { const [option, setOption] = useState('Select an option') const onAction = (itemProps: ItemProps) => {