Skip to content

Commit

Permalink
feat(toolbar-button): allow to have a link
Browse files Browse the repository at this point in the history
  • Loading branch information
clementprevot committed Mar 26, 2024
1 parent aaf3641 commit 6678a3c
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 107 deletions.
9 changes: 4 additions & 5 deletions packages/fractal/src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
}: ButtonProps,
ref: ForwardedRef<HTMLButtonElement>,
) => {
const hasIcon = Boolean(icon)
const hasChildren = Boolean(children)
if (!hasChildren && isEmpty(label)) {
console.warn(
Expand Down Expand Up @@ -127,8 +128,8 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
disabled
? `${PREFIX}-${GROUP_NAME}--disabled cursor-not-allowed ${variantDisabledClassNames[variant]}`
: `${variantClassNames[variant]} cursor-pointer`,
!isEmpty(icon)
? `${PREFIX}-${GROUP_NAME}--with-addendum ${PREFIX}-${GROUP_NAME}--with-addendum-${iconPosition}`
hasIcon
? `${PREFIX}-${GROUP_NAME}--with-addendum ${PREFIX}-${GROUP_NAME}--with-addendum-${iconPosition === 'left' ? 'prefix' : 'suffix'}`
: '',
// eslint-disable-next-line no-nested-ternary
iconOnly
Expand All @@ -154,8 +155,6 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
</div>
)

const hasIcon = Boolean(icon)

const labelElement = (
<Typography
className={cj(
Expand All @@ -182,7 +181,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
className={cj(
wrap || truncate ? 'min-w-0' : '',
wrap ? 'whitespace-break-spaces' : '',
truncate ? 'truncate' : 'truncate',
truncate ? 'truncate' : '',
)}
>
{label}
Expand Down
46 changes: 6 additions & 40 deletions packages/fractal/src/components/Toolbar/Toolbar.types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { Root } from '@radix-ui/react-toolbar'
import type {
AllHTMLAttributes,
ComponentProps,
MouseEvent,
ReactNode,
} from 'react'
import type { AllHTMLAttributes, ComponentProps, ReactNode } from 'react'

import { ButtonProps } from '@/components/Button'
import type {
CombinedRefs as DropdownCombinedRefs,
DropdownItemGroupProps,
Expand Down Expand Up @@ -41,42 +37,12 @@ export interface ToolbarProps
orientation?: `${Orientations}`
}

export interface ToolbarButtonProps
extends Omit<AllHTMLAttributes<HTMLButtonElement>, 'type'> {
export type ToolbarButtonProps = Omit<
ButtonProps,
'truncate' | 'variant' | 'wrap'
> & {
/** Indicates if the toolbar button is active. */
active?: boolean
/**
* The content of the toolbar button.
*
* Use this for complex content where a string (passed to the `label` prop) is
* not enough.
*/
children?: ReactNode
/** Indicates if the toolbar button is disabled. */
disabled?: boolean
/** An icon to display in the toolbar button. */
icon?: ReactNode
/**
* Indicates if you want to only display the icon.
* The label still is mandatory and will be used as an `aria-label` for
* accessibility.
*/
iconOnly?: boolean
/** The position of the icon relative to the label. */
iconPosition?: 'left' | 'right'
/**
* The content of the toolbar button.
*
* Use this when you only need to display text in a toolbar button.
* If you need more complex content, use the `children` prop.
*
* When using the `children` prop, you can use this prop to set a simple
* textual representation of the item that will be used as the `aria-label`
* and `title` for the toolbar button.
*/
label?: string
/** Event handler called when the toolbar button is clicked. */
onClick?: (event: MouseEvent<HTMLButtonElement>) => void
}

export type ToolbarDropdownProps = Omit<
Expand Down
23 changes: 19 additions & 4 deletions packages/fractal/src/components/Toolbar/ToolbarButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,15 @@ const meta: Meta<ToolbarButtonProps> = {
args: {
active: false,
disabled: false,
icon: 'None',
fullWidth: false,
href: '',
icon: 'Send',
iconOnly: false,
iconPosition: 'left',
iconPosition: 'right',
label: 'Luke Skywalker',
target: '_blank',
type: 'button',
underlined: false,
},
component: ToolbarButton,

Expand All @@ -48,20 +53,30 @@ export const Playground: Story = {
render: ({
active = false,
disabled = false,
icon = undefined,
fullWidth = false,
href = '',
icon = <SendIcon />,
iconOnly = false,
iconPosition = 'left',
iconPosition = 'right',
label = 'Luke Skywalker',
target = '_blank',
type = 'button',
underlined = false,
}) => (
<div className="h-13">
<Toolbar>
<ToolbarButton
active={active}
disabled={disabled}
fullWidth={fullWidth}
href={href}
icon={icon}
iconOnly={iconOnly}
iconPosition={iconPosition}
label={label}
target={target}
type={type}
underlined={underlined}
onClick={action('onClick')}
/>
</Toolbar>
Expand Down
213 changes: 155 additions & 58 deletions packages/fractal/src/components/Toolbar/ToolbarButton.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
'use client'

import { composeRefs } from '@radix-ui/react-compose-refs'
import * as RxToolbar from '@radix-ui/react-toolbar'
import isEmpty from 'lodash/fp/isEmpty'
import isFunction from 'lodash/fp/isFunction'
import omit from 'lodash/fp/omit'
import { type ForwardedRef, forwardRef, useContext, useRef } from 'react'
import {
type ForwardedRef,
type MouseEvent,
type TouchEvent,
forwardRef,
useContext,
} from 'react'

import { Typography } from '@/components/Typography/Typography'
import { PREFIX } from '@/constants'
Expand All @@ -26,11 +32,16 @@ export const ToolbarButton = forwardRef<HTMLButtonElement, ToolbarButtonProps>(
active = false,
children,
disabled = false,
fullWidth = false,
href,
icon,
iconOnly = false,
iconPosition = 'left',
label,
onClick,
target,
type = 'button',
underlined,
...props
}: ToolbarButtonProps,
ref: ForwardedRef<HTMLButtonElement>,
Expand All @@ -43,72 +54,158 @@ export const ToolbarButton = forwardRef<HTMLButtonElement, ToolbarButtonProps>(
)
}

const buttonRef = useRef<HTMLButtonElement>(null)
const combinedRef = composeRefs(ref, buttonRef)

const { disabled: toolbarDisabled } = useContext(ToolbarContext)

const isDisabled = disabled || toolbarDisabled

const handleTouchStart = (event: TouchEvent<HTMLButtonElement>) => {
if (isFunction(props.onTouchStart)) {
props.onTouchStart(event)

return
}

if ('ontouchstart' in document.documentElement && isFunction(onClick)) {
onClick(
event as unknown as MouseEvent<HTMLAnchorElement | HTMLButtonElement>,
)
}
}

const handleTouchEnd = (event: TouchEvent<HTMLButtonElement>) => {
if (isFunction(props.onTouchEnd)) {
props.onTouchEnd(event)
}

if (
'ontouchstart' in document.documentElement &&
!isFunction(props.onTouchStart) &&
isFunction(onClick)
) {
event.preventDefault()
}
}

const asLink = !isEmpty(href)

const classNames = cn(
`${PREFIX}-${GROUP_NAME}__button`,
asLink ? `${PREFIX}-${GROUP_NAME}__button__link` : '',
asLink || underlined === false ? 'no-underline' : '',
'flex h-3 max-h-3 min-h-3 max-w-full items-center justify-center gap-1 rounded-xs bg-transparent p-0 text-left outline-none transition-colors duration-300 ease-out active:transition-none',
fullWidth && !iconOnly
? `${PREFIX}-${GROUP_NAME}__button--full-width w-full`
: '',
// eslint-disable-next-line no-nested-ternary
active && !isDisabled
? 'text-dark'
: isDisabled
? 'text-disabled'
: 'text-placeholder',
disabled
? `${PREFIX}-${GROUP_NAME}__button--disabled cursor-not-allowed`
: 'cursor-pointer hover:bg-decorative-pink-90 hover:text-dark',
hasIcon
? `${PREFIX}-${GROUP_NAME}__button--with-addendum ${PREFIX}-${GROUP_NAME}__button--with-addendum-${iconPosition === 'left' ? 'prefix' : 'suffix'}`
: '',
// eslint-disable-next-line no-nested-ternary
iconOnly
? `${PREFIX}-${GROUP_NAME}__button--icon-only px-half`
: !fullWidth
? 'w-fit'
: '',
// eslint-disable-next-line no-nested-ternary
!iconOnly
? // eslint-disable-next-line no-nested-ternary
hasIcon
? iconPosition === 'left'
? 'pl-half pr-1'
: 'pl-1 pr-half'
: 'px-1'
: '',
props.className,
)

const iconElement = (
<div
className={cj(
`${PREFIX}-${GROUP_NAME}__button__icon`,
`${PREFIX}-${GROUP_NAME}__button__icon--${iconPosition}`,
'mt-0 flex h-3 w-3 items-center [&>svg]:h-3',
asLink
? `${PREFIX}-${GROUP_NAME}__button__link__icon--${iconPosition}`
: '',
)}
>
{icon}
</div>
)

const labelElement = (
<Typography
className={cj(
`${PREFIX}-${GROUP_NAME}__button__label`,
asLink ? `${PREFIX}-${GROUP_NAME}__button__link__label` : '',
'flex max-h-full max-w-full flex-1 items-center justify-center gap-half overflow-hidden text-ellipsis whitespace-nowrap pt-0 text-center align-middle',
// eslint-disable-next-line no-nested-ternary
underlined === false ? 'no-underline' : asLink ? 'underline' : '',
isDisabled
? `${PREFIX}-${GROUP_NAME}__button__label--disabled cursor-not-allowed`
: `cursor-pointer`,
)}
element="div"
variant={active ? 'body-1-median' : 'body-1'}
>
{hasIcon && iconPosition === 'left' && iconElement}
{/* eslint-disable-next-line no-nested-ternary */}
{iconOnly ? (
false
) : hasChildren ? (
children
) : (
<div className="min-w-0 truncate">{label}</div>
)}
{hasIcon && iconPosition === 'right' && iconElement}
</Typography>
)

if (asLink) {
return (
<a
{...(props.id !== undefined ? { id: props.id } : {})}
aria-label={label}
className={classNames}
href={href}
{...(!isEmpty(target) ? { target } : {})}
title={label}
{...(!disabled && isFunction(onClick) ? { onClick } : {})}
{...omit(['className', 'id'], props)}
>
{iconOnly ? iconElement : labelElement}
</a>
)
}

return (
<RxToolbar.Button
{...(props.id !== undefined ? { id: props.id } : {})}
ref={combinedRef}
ref={ref}
aria-label={label}
className={cn(
`${PREFIX}-${GROUP_NAME}__button`,
'flex h-3 max-h-3 max-w-full items-center justify-center gap-1 rounded-xs bg-transparent p-0 text-left outline-none transition-colors duration-300 ease-out active:transition-none',
isDisabled
? `${PREFIX}-${GROUP_NAME}__button--disabled cursor-not-allowed`
: `cursor-pointer hover:bg-decorative-pink-90 hover:text-dark`,
// eslint-disable-next-line no-nested-ternary
active && !isDisabled
? 'text-dark'
: isDisabled
? 'text-disabled'
: 'text-placeholder',
!isEmpty(icon)
? `${PREFIX}-${GROUP_NAME}__button--addendum ${PREFIX}-${GROUP_NAME}--addendum--${iconPosition === 'left' ? 'prefix' : 'suffix'}`
: '',
iconOnly
? `${PREFIX}-${GROUP_NAME}__button--icon-only px-half`
: 'w-fit',
// eslint-disable-next-line no-nested-ternary
!iconOnly
? // eslint-disable-next-line no-nested-ternary
hasIcon
? iconPosition === 'left'
? 'pl-half pr-1'
: 'pl-1 pr-half'
: 'px-1'
: '',
props.className,
)}
className={classNames}
{...(props.dir !== undefined
? { dir: props.dir as 'ltr' | 'rtl' }
: {})}
disabled={isDisabled}
title={label}
type="button"
onClick={onClick}
{...omit(['className', 'id', 'onClick', 'type'], props)}
>
{hasIcon && iconPosition === 'left' && icon}

{!iconOnly && (
<Typography
className={cj(
`${PREFIX}-${GROUP_NAME}__toggle__label`,
'max-h-full max-w-full flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-center align-middle',
isDisabled
? `${PREFIX}-${GROUP_NAME}__toggle_label--disabled cursor-not-allowed`
: `cursor-pointer`,
)}
element={hasChildren ? 'div' : 'label'}
variant={active ? 'body-1-median' : 'body-1'}
>
{hasChildren ? children : label}
</Typography>
type={type}
onTouchEnd={handleTouchEnd}
onTouchStart={handleTouchStart}
{...(isFunction(onClick) ? { onClick } : {})}
{...omit(
['className', 'dir', 'id', 'onTouchEnd', 'onTouchStart'],
props,
)}

{hasIcon && iconPosition === 'right' && icon}
>
{iconOnly ? iconElement : labelElement}
</RxToolbar.Button>
)
},
Expand Down

0 comments on commit 6678a3c

Please sign in to comment.