Skip to content

Commit 36d8a2d

Browse files
authored
feat(components)!: add Menu, MenuItem, MenuTrigger, Header, Keyboard, Section, Separator, and Text (#1154)
* feat(components): add menu * feat: add supporting components * feat: support slots with selection * feat!: support grid line names * refactor: update class selectors * chore: add changeset * docs: update migration guidelines * feat: update story * feat: add tests * fix: align headers * fix: add missing styles * fix: excludeDecorators
1 parent 9dd6b14 commit 36d8a2d

File tree

29 files changed

+590
-94
lines changed

29 files changed

+590
-94
lines changed

.changeset/blue-papayas-develop.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@launchpad-ui/split-button": minor
3+
"@launchpad-ui/inline-edit": minor
4+
"@launchpad-ui/snackbar": minor
5+
"@launchpad-ui/tooltip": minor
6+
"@launchpad-ui/button": minor
7+
"@launchpad-ui/filter": minor
8+
"@launchpad-ui/alert": minor
9+
"@launchpad-ui/form": minor
10+
"@launchpad-ui/menu": minor
11+
"@launchpad-ui/core": minor
12+
"@launchpad-ui/icons": minor
13+
---
14+
15+
Change class pattern to `[hash]_[local]`

.changeset/five-tigers-walk.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@launchpad-ui/components": patch
3+
---
4+
5+
Add `Menu`, `MenuItem`, `MenuTrigger`, `Header`, `Keyboard`, `Section`, `Separator`, and `Text`

.storybook/preview.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export const parameters = {
5454
},
5555
},
5656
},
57+
docs: {
58+
source: {
59+
excludeDecorators: true,
60+
},
61+
},
5762
};
5863

5964
export const decorators = [

MIGRATION.md

+95
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,100 @@
11
# Migration @launchpad-ui/core
22

3+
## 0.49.0
4+
5+
### Component class name pattern changed
6+
7+
Class names have been updated from `[hash]_[local]_` to `[hash]_[local]`. Update selectors targeting LP class names accordingly:
8+
9+
```css
10+
[class*='_Button_'] {
11+
...
12+
}
13+
```
14+
15+
**After**
16+
17+
```css
18+
[class*='_Button'] {
19+
...
20+
}
21+
```
22+
23+
## 0.48.0
24+
25+
### Remove icons
26+
27+
Icons `pulse-active`, `verified`, and `circles` have been removed.
28+
29+
## 0.47.0
30+
31+
### Base 16 font size
32+
33+
The base font size for all tokens and components is now 16. Update `rem` values accordingly. https://nekocalc.com/px-to-rem-converter
34+
35+
## 0.46.0
36+
37+
### Icon refresh
38+
39+
See https://launchpad.launchdarkly.com/?path=/story/components-icon--default for latest set of icons.
40+
41+
## 0.45.0
42+
43+
### Remove icons
44+
45+
Icons `warning-circle` and `arrow-thin-right-circle` have been removed.
46+
47+
## 0.44.0
48+
49+
### Remove bar-chart icon
50+
51+
Icon `bar-chart` has been replaced by `experiment`.
52+
53+
## 0.42.0
54+
55+
### Use sprites for icons
56+
57+
**Before**
58+
59+
```js
60+
import { Add } from '@launchpad-ui/icons';
61+
62+
const MyIcon = () => <Add size="medium" />;
63+
```
64+
65+
**After**
66+
67+
```js
68+
import { Icon } from '@launchpad-ui/icons';
69+
70+
const MyIcon = () => <Icon name="add" size="medium" />;
71+
```
72+
73+
By default, the component expects `@launchpad-ui/icons/dist/sprite.svg` to be available from `APP_ROOT/static/sprite.svg` in your app. A custom path to the sprite can be set via the `IconContext` provider.
74+
75+
For example, if importing a static asset returns a resolved URL you can do the following in your app to load the icons:
76+
77+
```js
78+
import { IconContext } from '@launchpad-ui/icons';
79+
import icons from '@launchpad-ui/icons/sprite.svg';
80+
import { createRoot } from 'react-dom/client';
81+
82+
const domNode = document.getElementById('root');
83+
const root = createRoot(domNode);
84+
85+
root.render(
86+
<IconContext.Provider value={{ path: icons }}>
87+
<App />
88+
</IconContext.Provider>
89+
);
90+
```
91+
92+
## 0.41.0
93+
94+
### Modal size renaming
95+
96+
Modal size `normal` renamed to `medium`.
97+
398
## 0.40.0
499

5100
### TagGroup API changes

apps/remix/app/data.server.ts

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export async function getComponents() {
4040
{ to: 'components/tooltip', name: 'Tooltip' },
4141
{ to: 'rac/button', name: 'RAC Button', role: 'button' },
4242
{ to: 'rac/link-button', name: 'RAC LinkButton', role: 'link' },
43+
{ to: 'rac/menu', name: 'RAC Menu', role: 'menu' },
4344
{ to: 'rac/modal', name: 'RAC Modal', role: 'dialog' },
4445
{ to: 'rac/popover', name: 'RAC Popover', role: 'dialog' },
4546
{ to: 'rac/progress-bar', name: 'RAC ProgressBar', role: 'progressbar' },

apps/remix/app/routes/rac.menu.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Popover, Menu, MenuItem, MenuTrigger, Button } from '@launchpad-ui/components';
2+
3+
export default function Index() {
4+
return (
5+
<MenuTrigger>
6+
<Button>Trigger</Button>
7+
<Popover isOpen>
8+
<Menu>
9+
<MenuItem>Item one</MenuItem>
10+
<MenuItem>Item two</MenuItem>
11+
<MenuItem>Item three</MenuItem>
12+
</Menu>
13+
</Popover>
14+
</MenuTrigger>
15+
);
16+
}

packages/alert/src/styles/Alert.module.css

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
line-height: 1.25;
4242
}
4343

44-
.Alert :global([class*='_ButtonGroup_']) {
44+
.Alert :global([class*='_ButtonGroup']) {
4545
margin-top: 0.75rem;
4646
}
4747

@@ -197,7 +197,7 @@
197197
margin-left: auto;
198198
}
199199

200-
.Alert-content :global(a:not([class*='_Button_'])) {
200+
.Alert-content :global(a:not([class*='_Button'])) {
201201
color: var(--lp-color-text-interactive-base);
202202

203203
&:hover {

packages/button/__tests__/ButtonGroup.spec.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('ButtonGroup', () => {
2323
</ButtonGroup>
2424
);
2525
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
26-
expect(container.querySelector('[class*="_ButtonGroup--compact_"]')).not.toBeNull();
26+
expect(container.querySelector('[class*="_ButtonGroup--compact"]')).not.toBeNull();
2727
expect(screen.getByRole('button', { name: 'One' })).toBeInTheDocument();
2828
expect(screen.getByRole('button', { name: 'Two' })).toBeInTheDocument();
2929
});
@@ -36,7 +36,7 @@ describe('ButtonGroup', () => {
3636
</ButtonGroup>
3737
);
3838
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
39-
expect(container.querySelector('[class*="_ButtonGroup--large_"]')).not.toBeNull();
39+
expect(container.querySelector('[class*="_ButtonGroup--large"]')).not.toBeNull();
4040
expect(screen.getByRole('button', { name: 'One' })).toBeInTheDocument();
4141
expect(screen.getByRole('button', { name: 'Two' })).toBeInTheDocument();
4242
});

packages/button/src/styles/Button.module.css

+10-10
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@
598598
border-color: var(--Button-color-border-disabled);
599599
}
600600

601-
.Button[disabled] [class*='_icon_'] {
601+
.Button[disabled] [class*='_icon'] {
602602
fill: var(--Button-icon-color-fill-disabled);
603603
}
604604

@@ -784,30 +784,30 @@
784784
}
785785

786786
.ButtonGroup--compact > .Button + .Button,
787-
.ButtonGroup--compact > [class*='_Popover-target_'] + .Button,
788-
.ButtonGroup--compact > .Button + [class*='_Popover-target_'],
789-
.ButtonGroup--compact > [class*='_Popover-target_'] + [class*='_Popover-target_'] {
787+
.ButtonGroup--compact > [class*='_Popover-target'] + .Button,
788+
.ButtonGroup--compact > .Button + [class*='_Popover-target'],
789+
.ButtonGroup--compact > [class*='_Popover-target'] + [class*='_Popover-target'] {
790790
margin-left: -1px;
791791
}
792792

793-
.ButtonGroup--compact > .Button + [class*='_Popover-target_'],
794-
.ButtonGroup--compact > [class*='_Popover-target_'] + [class*='_Popover-target_'] {
793+
.ButtonGroup--compact > .Button + [class*='_Popover-target'],
794+
.ButtonGroup--compact > [class*='_Popover-target'] + [class*='_Popover-target'] {
795795
line-height: 1;
796796
}
797797

798798
.ButtonGroup--compact > .Button:not(:first-child),
799799
.ButtonGroup--compact > .Button:not(:last-child),
800-
.ButtonGroup--compact > [class*='_Popover-target_']:not(:first-child),
801-
.ButtonGroup--compact > [class*='_Popover-target_']:not(:last-child) {
800+
.ButtonGroup--compact > [class*='_Popover-target']:not(:first-child),
801+
.ButtonGroup--compact > [class*='_Popover-target']:not(:last-child) {
802802
border-radius: 0;
803803
}
804804

805805
.ButtonGroup--compact > .Button:first-child,
806-
.ButtonGroup--compact > [class*='_Popover-target_']:first-child button {
806+
.ButtonGroup--compact > [class*='_Popover-target']:first-child button {
807807
border-radius: var(--lp-border-radius-regular) 0 0 var(--lp-border-radius-regular);
808808
}
809809

810810
.ButtonGroup--compact > .Button:last-child,
811-
.ButtonGroup--compact > [class*='_Popover-target_']:last-child button {
811+
.ButtonGroup--compact > [class*='_Popover-target']:last-child button {
812812
border-radius: 0 var(--lp-border-radius-regular) var(--lp-border-radius-regular) 0;
813813
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { it, expect, describe } from 'vitest';
2+
3+
import { render, screen, userEvent } from '../../../test/utils';
4+
import { Menu, MenuItem, MenuTrigger, Button, Popover } from '../src';
5+
6+
describe('Menu', () => {
7+
it('renders', async () => {
8+
const user = userEvent.setup();
9+
render(
10+
<MenuTrigger>
11+
<Button>Trigger</Button>
12+
<Popover>
13+
<Menu>
14+
<MenuItem>Item one</MenuItem>
15+
<MenuItem>Item two</MenuItem>
16+
<MenuItem>Item three</MenuItem>
17+
</Menu>
18+
</Popover>
19+
</MenuTrigger>
20+
);
21+
22+
await user.click(screen.getByRole('button'));
23+
expect(await screen.findByRole('menu')).toBeVisible();
24+
});
25+
});

packages/components/src/Header.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Header } from 'react-aria-components';
2+
3+
export { Header };

packages/components/src/Keyboard.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Keyboard } from 'react-aria-components';
2+
3+
export { Keyboard };

packages/components/src/Menu.tsx

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { ForwardedRef } from 'react';
2+
import type {
3+
MenuProps as AriaMenuProps,
4+
MenuItemProps,
5+
MenuTriggerProps,
6+
} from 'react-aria-components';
7+
8+
import { Icon } from '@launchpad-ui/icons';
9+
import { cva } from 'class-variance-authority';
10+
import { forwardRef } from 'react';
11+
import {
12+
Menu as AriaMenu,
13+
MenuItem as AriaMenuItem,
14+
MenuTrigger,
15+
composeRenderProps,
16+
} from 'react-aria-components';
17+
18+
import styles from './styles/Menu.module.css';
19+
20+
type MenuProps<T> = AriaMenuProps<T>;
21+
22+
const menu = cva(styles.menu);
23+
const item = cva(styles.item);
24+
25+
const _Menu = <T extends object>(
26+
{ className, ...props }: MenuProps<T>,
27+
ref: ForwardedRef<HTMLDivElement>
28+
) => {
29+
return <AriaMenu {...props} ref={ref} className={menu({ className })} />;
30+
};
31+
32+
/**
33+
* A menu displays a list of actions or options that a user can choose.
34+
*
35+
* https://react-spectrum.adobe.com/react-aria/Menu.html
36+
*/
37+
const Menu = forwardRef(_Menu);
38+
39+
const _MenuItem = <T extends object>(
40+
props: MenuItemProps<T>,
41+
ref: ForwardedRef<HTMLDivElement>
42+
) => {
43+
return (
44+
<AriaMenuItem
45+
{...props}
46+
ref={ref}
47+
className={composeRenderProps(props.className, (className, renderProps) =>
48+
item({ ...renderProps, className })
49+
)}
50+
>
51+
{composeRenderProps(props.children, (children, { selectionMode, isSelected }) => (
52+
<>
53+
{selectionMode !== 'none' && (
54+
<span className={styles.check}>{isSelected && <Icon name="check" size="small" />}</span>
55+
)}
56+
{children}
57+
</>
58+
))}
59+
</AriaMenuItem>
60+
);
61+
};
62+
63+
/**
64+
* A MenuItem represents an individual action in a Menu.
65+
*/
66+
const MenuItem = forwardRef(_MenuItem);
67+
68+
export { Menu, MenuItem, MenuTrigger };
69+
export type { MenuProps, MenuItemProps, MenuTriggerProps };

packages/components/src/Section.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { SectionProps } from 'react-aria-components';
2+
3+
import { Section } from 'react-aria-components';
4+
5+
export { Section };
6+
export type { SectionProps };

packages/components/src/Separator.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { SeparatorProps } from 'react-aria-components';
2+
3+
import { Separator } from 'react-aria-components';
4+
5+
export { Separator };
6+
export type { SeparatorProps };

packages/components/src/Text.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { TextProps } from 'react-aria-components';
2+
3+
import { Text } from 'react-aria-components';
4+
5+
export { Text };
6+
export type { TextProps };

0 commit comments

Comments
 (0)