Skip to content

Commit

Permalink
BREAKING: Use render prop instead of as for composition
Browse files Browse the repository at this point in the history
  • Loading branch information
gnapse committed Jul 2, 2024
1 parent 24dba33 commit 97208a0
Show file tree
Hide file tree
Showing 17 changed files with 216 additions and 206 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Reactist follows [semantic versioning](https://semver.org/) and doesn't introduce breaking changes (API-wise) in minor or patch releases. However, the appearance of a component might change in a minor or patch release so keep an eye on redesigns and make sure your app still looks and feels like you expect it.

# v25.0.0-beta.1

- [BREAKING] User an explicit `render` prop for composition, instead of the `as` prop

# v24.2.0-beta

- [Fix] Include changes from [v23.3.0](#v2330) in the beta release
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"email": "[email protected]",
"url": "http://doist.com"
},
"version": "24.2.0-beta",
"version": "25.0.0-beta.1",
"license": "MIT",
"homepage": "https://github.com/Doist/reactist#readme",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions src/avatar/avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getInitials, emailToIndex } from './utils'
import { getClassNames, ResponsiveProp } from '../utils/responsive-props'
import styles from './avatar.module.css'
import { Box } from '../box'
import type { ObfuscatedClassName } from '../utils/common-types'

const AVATAR_COLORS = [
'#fcc652',
Expand All @@ -29,10 +30,9 @@ const AVATAR_COLORS = [

type AvatarSize = 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl' | 'xxxl'

type Props = {
type Props = ObfuscatedClassName & {
/** @deprecated Please use `exceptionallySetClassName` */
className?: string
exceptionallySetClassName?: string
/** @deprecated */
colorList?: string[]
size?: ResponsiveProp<AvatarSize>
Expand Down
5 changes: 3 additions & 2 deletions src/button/button.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import * as React from 'react'
import { BaseButton } from '../base-button'
import type { BaseButtonProps } from '../base-button'
import type { ObfuscatedClassName } from '../utils/common-types'

type NativeButtonProps = Omit<
React.AllHTMLAttributes<HTMLButtonElement>,
'aria-disabled' | 'className' | keyof BaseButtonProps
>

export type ButtonProps = NativeButtonProps &
BaseButtonProps & {
BaseButtonProps &
ObfuscatedClassName & {
type?: 'button' | 'submit' | 'reset'
exceptionallySetClassName?: string
}

/**
Expand Down
3 changes: 1 addition & 2 deletions src/heading/heading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import * as React from 'react'
import { getClassNames } from '../utils/responsive-props'
import { Box } from '../box'
import styles from './heading.module.css'
import type { ObfuscatedClassName } from '../utils/polymorphism'
import type { Tone } from '../utils/common-types'
import type { ObfuscatedClassName, Tone } from '../utils/common-types'
import type { BoxProps } from '../box'

type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6 | '1' | '2' | '3' | '4' | '5' | '6'
Expand Down
27 changes: 14 additions & 13 deletions src/loading/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react'
import { Box } from '../box'
import { Spinner } from '../spinner'
import type { ObfuscatedClassName } from '../utils/common-types'

type Size = 'xsmall' | 'small' | 'medium' | 'large'

Expand All @@ -9,19 +10,19 @@ type NativeProps = Omit<
'className' | 'aria-describedby' | 'aria-label' | 'aria-labelledby' | 'role' | 'size'
>

type LoadingProps = NativeProps & {
/**
* The size of the loading spinner.
* @default 'small'
*/
size?: Size
/**
* A escape hatch in case you need to provide a custom class name to the container element.
*/
exceptionallySetClassName?: string
/** Identifies the element (or elements) that describes the loading component for assistive technologies. */
'aria-describedby'?: string
} & (
type LoadingProps = NativeProps &
ObfuscatedClassName & {
/**
* The size of the loading spinner.
* @default 'small'
*/
size?: Size

/**
* Identifies the element (or elements) that describes the loading component for assistive technologies.
*/
'aria-describedby'?: string
} & (
| {
/** Defines a string value that labels the current loading component for assistive technologies. */
'aria-label': string
Expand Down
2 changes: 1 addition & 1 deletion src/menu/menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ describe('Menu', () => {
<Menu>
<MenuButton>Links</MenuButton>
<MenuList aria-label="Some options">
<MenuItem as="a" href="https://github.com/Doist/reactist">
<MenuItem render={<a href="https://github.com/Doist/reactist" />}>
Github repo
</MenuItem>
</MenuList>
Expand Down
96 changes: 44 additions & 52 deletions src/menu/menu.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
import * as React from 'react'
import classNames from 'classnames'

import { polymorphicComponent } from '../utils/polymorphism'

//
// Reactist menu is a thin wrapper around Ariakit's menu components. This may or may not be
// temporary. Our goal is to make it transparent for the users of Reactist of this implementation
// detail. We may change in the future the external lib we use, or even implement it all internally,
// as long as we keep the same outer interface as intact as possible.
//
// Around the heavy lifting of the external lib we just add some features to better integrate the
// menu to Reactist's more opinionated approach (e.g. using our button with its custom variants and
// other features, easily show keyboard shortcuts in menu items, etc.)
//
import {
Portal,
MenuStore,
Expand All @@ -22,13 +10,14 @@ import {
Menu as AriakitMenu,
MenuGroup as AriakitMenuGroup,
MenuItem as AriakitMenuItem,
MenuItemProps as AriakitMenuItemProps,
MenuButton as AriakitMenuButton,
MenuButtonProps as AriakitMenuButtonProps,
Role,
} from '@ariakit/react'

import './menu.less'

type NativeProps<E extends HTMLElement> = React.DetailedHTMLProps<React.HTMLAttributes<E>, E>
import type { NativeProps, ObfuscatedClassName } from '../utils/common-types'

type MenuContextState = {
menuStore: MenuStore
Expand All @@ -50,7 +39,7 @@ const MenuContext = React.createContext<MenuContextState>(
// Menu
//

type MenuProps = Omit<MenuStoreProps, 'visible'> & {
interface MenuProps extends Omit<MenuStoreProps, 'visible'> {
/**
* The `Menu` must contain a `MenuList` that defines the menu options. It must also contain a
* `MenuButton` that triggers the menu to be opened or closed.
Expand Down Expand Up @@ -88,12 +77,14 @@ function Menu({ children, onItemSelect, ...props }: MenuProps) {
// MenuButton
//

type MenuButtonProps = Omit<AriakitMenuButtonProps, 'store' | 'className' | 'as'>
interface MenuButtonProps
extends Omit<AriakitMenuButtonProps, 'store' | 'className' | 'as'>,
ObfuscatedClassName {}

/**
* A button to toggle a dropdown menu open or closed.
*/
const MenuButton = polymorphicComponent<'button', MenuButtonProps>(function MenuButton(
const MenuButton = React.forwardRef<HTMLButtonElement, MenuButtonProps>(function MenuButton(
{ exceptionallySetClassName, ...props },
ref,
) {
Expand All @@ -111,39 +102,45 @@ const MenuButton = polymorphicComponent<'button', MenuButtonProps>(function Menu
//
// ContextMenuTrigger
//
const ContextMenuTrigger = polymorphicComponent<'div', unknown>(function ContextMenuTrigger(
{ as: component = 'div', ...props },
ref,
) {
const { setAnchorRect, menuStore } = React.useContext(MenuContext)

const handleContextMenu = React.useCallback(
function handleContextMenu(event: React.MouseEvent) {
event.preventDefault()
setAnchorRect({ x: event.clientX, y: event.clientY })
menuStore.show()
},
[setAnchorRect, menuStore],
)

const isOpen = menuStore.useState('open')
React.useEffect(() => {
if (!isOpen) setAnchorRect(null)
}, [isOpen, setAnchorRect])
interface ContextMenuTriggerProps extends ObfuscatedClassName, NativeProps<HTMLDivElement> {
render?: React.ReactElement
}

return React.createElement(component, { ...props, onContextMenu: handleContextMenu, ref })
})
const ContextMenuTrigger = React.forwardRef<HTMLDivElement, ContextMenuTriggerProps>(
function ContextMenuTrigger({ render, ...props }, ref) {
const { setAnchorRect, menuStore } = React.useContext(MenuContext)

const handleContextMenu = React.useCallback(
function handleContextMenu(event: React.MouseEvent) {
event.preventDefault()
setAnchorRect({ x: event.clientX, y: event.clientY })
menuStore.show()
},
[setAnchorRect, menuStore],
)

const isOpen = menuStore.useState('open')
React.useEffect(() => {
if (!isOpen) setAnchorRect(null)
}, [isOpen, setAnchorRect])

return <Role.div {...props} onContextMenu={handleContextMenu} ref={ref} render={render} />
},
)

//
// MenuList
//

type MenuListProps = Omit<AriakitMenuProps, 'store' | 'className'>
interface MenuListProps
extends Omit<AriakitMenuProps, 'store' | 'className'>,
ObfuscatedClassName {}

/**
* The dropdown menu itself, containing a list of menu items.
*/
const MenuList = polymorphicComponent<'div', MenuListProps>(function MenuList(
const MenuList = React.forwardRef<HTMLDivElement, MenuListProps>(function MenuList(
{ exceptionallySetClassName, modal = true, ...props },
ref,
) {
Expand All @@ -170,19 +167,14 @@ const MenuList = polymorphicComponent<'div', MenuListProps>(function MenuList(
// MenuItem
//

type MenuItemProps = {
interface MenuItemProps extends AriakitMenuItemProps, ObfuscatedClassName {
/**
* An optional value given to this menu item. It is passed on to the parent `Menu`'s
* `onItemSelect` when you provide that instead of (or alongside) providing individual
* `onSelect` callbacks to each menu item.
*/
value?: string

/**
* The content inside the menu item.
*/
children: React.ReactNode

/**
* When `true` the menu item is disabled and won't be selectable or be part of the keyboard
* navigation across the menu options.
Expand Down Expand Up @@ -230,15 +222,14 @@ type MenuItemProps = {
* A menu item inside a menu list. It can be selected by the user, triggering the `onSelect`
* callback.
*/
const MenuItem = polymorphicComponent<'button', MenuItemProps>(function MenuItem(
const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(function MenuItem(
{
value,
children,
onSelect,
hideOnSelect = true,
onClick,
exceptionallySetClassName,
as = 'button',
...props
},
ref,
Expand All @@ -247,7 +238,7 @@ const MenuItem = polymorphicComponent<'button', MenuItemProps>(function MenuItem
const { hide } = menuStore

const handleClick = React.useCallback(
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
function handleClick(event: React.MouseEvent) {
onClick?.(event)
const onSelectResult: unknown =
onSelect && !event.defaultPrevented ? onSelect() : undefined
Expand All @@ -261,7 +252,6 @@ const MenuItem = polymorphicComponent<'button', MenuItemProps>(function MenuItem
return (
<AriakitMenuItem
{...props}
as={as}
store={menuStore}
ref={ref}
onClick={handleClick}
Expand Down Expand Up @@ -328,7 +318,7 @@ const SubMenu = React.forwardRef<HTMLDivElement, SubMenuProps>(function SubMenu(

return (
<Menu onItemSelect={handleSubItemSelect}>
<AriakitMenuItem as="div" store={menuStore} ref={ref} hideOnClick={false}>
<AriakitMenuItem store={menuStore} ref={ref} hideOnClick={false}>
{renderMenuButton}
</AriakitMenuItem>
{list}
Expand All @@ -340,7 +330,9 @@ const SubMenu = React.forwardRef<HTMLDivElement, SubMenuProps>(function SubMenu(
// MenuGroup
//

type MenuGroupProps = Omit<NativeProps<HTMLDivElement>, 'className'> & {
interface MenuGroupProps
extends Omit<NativeProps<HTMLDivElement>, 'className'>,
ObfuscatedClassName {
/**
* A label to be shown visually and also used to semantically label the group.
*/
Expand All @@ -353,7 +345,7 @@ type MenuGroupProps = Omit<NativeProps<HTMLDivElement>, 'className'> & {
* This group does not add any visual separator. You can do that yourself adding `<hr />` elements
* before and/or after the group if you so wish.
*/
const MenuGroup = polymorphicComponent<'div', MenuGroupProps>(function MenuGroup(
const MenuGroup = React.forwardRef<HTMLDivElement, MenuGroupProps>(function MenuGroup(
{ label, children, exceptionallySetClassName, ...props },
ref,
) {
Expand Down
10 changes: 7 additions & 3 deletions src/modal/modal-examples.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,13 @@ export function ModalWithStandardActionsFooter() {
<Column>
<Menu>
<MenuButton
as={ReactistButton}
variant="tertiary"
icon={<ThreeDotsIcon />}
render={
<ReactistButton
variant="tertiary"
icon={<ThreeDotsIcon />}
aria-label="Open menu"
/>
}
/>
<MenuList aria-label="Simple menu">
<MenuItem onSelect={action('Edit')}>Edit</MenuItem>
Expand Down
Loading

0 comments on commit 97208a0

Please sign in to comment.