|
| 1 | +import { |
| 2 | + Meta, |
| 3 | + Story, |
| 4 | + Props, |
| 5 | + Status, |
| 6 | +} from '../../../../../.storybook/components'; |
| 7 | + |
| 8 | +import * as Stories from './DropdownMenu.stories'; |
| 9 | + |
| 10 | +<Meta of={Stories} /> |
| 11 | + |
| 12 | +# DropdownMenu |
| 13 | + |
| 14 | +<Status variant="experimental" /> |
| 15 | + |
| 16 | +A dropdown menu displays a list of actions or options that a user can choose. |
| 17 | + |
| 18 | +Compared to the [Menu](?path=/docs/components-menu--docs) component, it adds |
| 19 | +submenus and search. |
| 20 | + |
| 21 | +## Import |
| 22 | + |
| 23 | +```tsx |
| 24 | +import { DropdownMenu } from '@koobiq/react-components'; |
| 25 | +``` |
| 26 | + |
| 27 | +## Usage |
| 28 | + |
| 29 | +A dropdown menu is composed of a trigger and its content: |
| 30 | + |
| 31 | +<Story of={Stories.Base} /> |
| 32 | + |
| 33 | +## Props |
| 34 | + |
| 35 | +<Props of={Stories.Base} /> |
| 36 | + |
| 37 | +## Subcomponents |
| 38 | + |
| 39 | +The component has the following helper components: |
| 40 | + |
| 41 | +- `DropdownMenu.Popover` — the overlay, also used for a submenu |
| 42 | +- `DropdownMenu.Content` — the list of items inside the popover |
| 43 | +- `DropdownMenu.Item` — a single action |
| 44 | +- `DropdownMenu.ItemText` — the text of an item, with an optional caption |
| 45 | +- `DropdownMenu.ItemAddon` — an addon of an item, such as an icon or a shortcut |
| 46 | +- `DropdownMenu.Section` — a group of items |
| 47 | +- `DropdownMenu.Header` — a block of custom content, or the heading of a section |
| 48 | +- `DropdownMenu.SubmenuTrigger` — wraps an item and the submenu it opens |
| 49 | +- `DropdownMenu.Autocomplete` — filters the menu with a field |
| 50 | +- `DropdownMenu.Footer` — the text under the items, outside the scroll area |
| 51 | +- `DropdownMenu.Pressable` — makes custom markup usable as a trigger |
| 52 | + |
| 53 | +The popover and the content are separate components, so anything can go between |
| 54 | +them. That is how the search field is added. |
| 55 | + |
| 56 | +## Trigger |
| 57 | + |
| 58 | +The first child of the `DropdownMenu` is the trigger. A `Button` works as one |
| 59 | +without extra props. |
| 60 | + |
| 61 | +For custom markup that is not a button, wrap it in the `DropdownMenu.Pressable` |
| 62 | +(see the [Separators](#separators) example). |
| 63 | + |
| 64 | +The `trigger` prop switches between opening on press (default) and on a long press: |
| 65 | + |
| 66 | +<Story of={Stories.LongPress} /> |
| 67 | + |
| 68 | +## Content |
| 69 | + |
| 70 | +The items can be written out one by one, or passed as data through the `items` |
| 71 | +prop. Give every item a unique `id`. When an item is chosen, the `onAction` |
| 72 | +handler is called with that id. |
| 73 | + |
| 74 | +<Story of={Stories.Content} /> |
| 75 | + |
| 76 | +## Item content |
| 77 | + |
| 78 | +An item is composed of slots: the `DropdownMenu.ItemAddon` for icons and |
| 79 | +shortcuts, and the `DropdownMenu.ItemText` for the label and its caption. |
| 80 | + |
| 81 | +<Story of={Stories.ItemContent} /> |
| 82 | + |
| 83 | +An item built from slots needs the `textValue` prop, because the search and the |
| 84 | +typeahead match on it. Without it the item cannot be found by typing, and it |
| 85 | +disappears on the first keystroke in a searchable menu. The component warns |
| 86 | +about this in development. |
| 87 | + |
| 88 | +Add the `aria-label` prop when an item needs a shorter name for screen readers |
| 89 | +than all of its slots read together. |
| 90 | + |
| 91 | +The `align` prop lines the slots up by their top edge instead of centring them. |
| 92 | +It is useful for a tall item with a caption. |
| 93 | + |
| 94 | +## Selection |
| 95 | + |
| 96 | +The items are not selectable by default. Selection is turned on with the |
| 97 | +`selectionMode` prop. Use the `defaultSelectedKeys` prop to set the selected |
| 98 | +items (uncontrolled), and the `selectedKeys` prop to control them. Both take the |
| 99 | +`id` of the items. |
| 100 | + |
| 101 | +### Single |
| 102 | + |
| 103 | +<Story of={Stories.SelectionSingle} /> |
| 104 | + |
| 105 | +### Multiple |
| 106 | + |
| 107 | +<Story of={Stories.SelectionMultiple} /> |
| 108 | + |
| 109 | +## Disabled items |
| 110 | + |
| 111 | +Items can be marked as disabled with the `disabledKeys` prop of the content, or |
| 112 | +with the `isDisabled` prop of a single item. |
| 113 | + |
| 114 | +<Story of={Stories.DisabledItems} /> |
| 115 | + |
| 116 | +## Links |
| 117 | + |
| 118 | +An item with the `href` prop becomes a link. It navigates instead of calling the |
| 119 | +`onAction` handler, and it is never selectable. |
| 120 | + |
| 121 | +<Story of={Stories.Links} /> |
| 122 | + |
| 123 | +## Client side routing |
| 124 | + |
| 125 | +Wrap the app in the `RouterProvider`, and the link items will navigate through |
| 126 | +your router, be it **Next.js**, **React Router** or another one. |
| 127 | + |
| 128 | +```tsx |
| 129 | +import { useRouter } from 'next/navigation'; |
| 130 | + |
| 131 | +import { RouterProvider } from '@koobiq/react-components'; |
| 132 | + |
| 133 | +export default function App() { |
| 134 | + const router = useRouter(); |
| 135 | + |
| 136 | + return ( |
| 137 | + <RouterProvider navigate={router.push}> |
| 138 | + <DropdownMenu> |
| 139 | + <Button>Pages</Button> |
| 140 | + <DropdownMenu.Popover> |
| 141 | + <DropdownMenu.Content aria-label="pages"> |
| 142 | + <DropdownMenu.Item href="/page1">Page 1</DropdownMenu.Item> |
| 143 | + <DropdownMenu.Item href="/page2">Page 2</DropdownMenu.Item> |
| 144 | + </DropdownMenu.Content> |
| 145 | + </DropdownMenu.Popover> |
| 146 | + </DropdownMenu> |
| 147 | + </RouterProvider> |
| 148 | + ); |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +## Sections |
| 153 | + |
| 154 | +Groups of items can be wrapped in a `DropdownMenu.Section` with the `title` prop. |
| 155 | + |
| 156 | +### Static items |
| 157 | + |
| 158 | +<Story of={Stories.Sections} /> |
| 159 | + |
| 160 | +### Dynamic items |
| 161 | + |
| 162 | +<Story of={Stories.SectionsDynamic} /> |
| 163 | + |
| 164 | +### With Section Level Selection |
| 165 | + |
| 166 | +Each section takes its own `selectionMode`, `selectedKeys` and `onSelectionChange` |
| 167 | +props, so one menu can mix plain actions with a single and a multiple choice. |
| 168 | + |
| 169 | +<Story of={Stories.WithSectionLevelSelection} /> |
| 170 | + |
| 171 | +## Separators |
| 172 | + |
| 173 | +Put a `Divider` between items or sections to group them without a title. The |
| 174 | +`DropdownMenu.Header` renders custom content inside the menu. |
| 175 | + |
| 176 | +<Story of={Stories.Separators} /> |
| 177 | + |
| 178 | +## Submenu |
| 179 | + |
| 180 | +Wrap an item and a nested `DropdownMenu.Popover` in a `DropdownMenu.SubmenuTrigger` |
| 181 | +to give the item a submenu. The chevron is added automatically. A submenu opens |
| 182 | +on hover after the `delay` prop (200 ms by default), on <kbd>ArrowRight</kbd> and |
| 183 | +on press, and closes on <kbd>ArrowLeft</kbd>. Submenus can be nested. |
| 184 | + |
| 185 | +<Story of={Stories.Submenu} /> |
| 186 | + |
| 187 | +## Search |
| 188 | + |
| 189 | +Wrap the content in a `DropdownMenu.Autocomplete` and put a `SearchInput` above |
| 190 | +it. The field has to be next to the menu, not inside it. |
| 191 | + |
| 192 | +Matching ignores the case and the accents. Pass the `filter` prop to change it. |
| 193 | +The focus stays in the field while the arrows move through the items. An item |
| 194 | +that opens a submenu stays when its own text matches, but the items inside a |
| 195 | +submenu are not searched. |
| 196 | + |
| 197 | +<Story of={Stories.Search} /> |
| 198 | + |
| 199 | +The `renderEmptyState` prop of the content replaces the default message, with an |
| 200 | +`EmptyState` for instance. |
| 201 | + |
| 202 | +<Story of={Stories.SearchEmpty} /> |
| 203 | + |
| 204 | +## Footer |
| 205 | + |
| 206 | +The `DropdownMenu.Footer` shows text under the items. It stays in place while the |
| 207 | +items scroll. |
| 208 | + |
| 209 | +<Story of={Stories.DropdownFooter} /> |
| 210 | + |
| 211 | +## Placement |
| 212 | + |
| 213 | +The placement of the menu with respect to its trigger can be adjusted using the |
| 214 | +`placement` prop of the popover. The menu also flips to the opposite side when |
| 215 | +there is not enough space. |
| 216 | + |
| 217 | +<Story of={Stories.Placement} /> |
| 218 | + |
| 219 | +## Open |
| 220 | + |
| 221 | +### Default open |
| 222 | + |
| 223 | +The menu isn't opened by default. The `defaultOpen` prop can be used to set the |
| 224 | +default state. |
| 225 | + |
| 226 | +### Controlled open |
| 227 | + |
| 228 | +The `isOpen` prop can be used to make the opened state controlled. The |
| 229 | +`onOpenChange` event is fired when the open state of the menu changes. |
| 230 | + |
| 231 | +<Story of={Stories.Open} /> |
0 commit comments