Skip to content

Commit

Permalink
#282 - Add EbaySegmentedButtons component (#370)
Browse files Browse the repository at this point in the history
* #282 - Add EbaySegmentedButtons component

* Add tests, update readme

* remove unused var

* update types

* update readme

* use className for the button component, update readme

* remove redundant code
  • Loading branch information
darkwebdev authored Oct 11, 2024
1 parent 4b48329 commit bc8829d
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 0 deletions.
42 changes: 42 additions & 0 deletions src/ebay-segmented-buttons/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# EbaySegmentedButtons

## Demo
[Storybook](https://opensource.ebay.com/ebayui-core-react/main/?path=/story/buttons-ebay-segmented-buttons--default)

## Install
```
yarn add @ebay/ui-core-react @ebay/skin
```

## Usage
```
import React from 'react'
import { EbaySegmentedButtons, EbaySegmentedButton as Button } from '@ebay/ui-core-react/ebay-segmented-buttons'
import { EbayIcon } from '@ebay/ui-core-react/ebay-icon'
import '@ebay/skin/segmented-buttons'
export const Example = () => (
<EbaySegmentedButtons
size="large"
onChange={(e, { i, value }) => console.log('Selected:', i, value)}
>
<Button value="1" selected>Button 1</Button>
<Button value="2">Button 2</Button>
<Button value="3"><EbayIcon name="settings24" /> Button 3</Button>
/>
);
```

## EbaySegmentedButtons Props

Name | Type | Required | Description
--- |----------| --- | ---
`size` | enum | No | Can be `regular` (default) or `large`
`onChange` | Function | No | props: (e: event, { index: number, value: string), triggered on selected button change

## EbaySegmentedButton Props

Name | Type | Required | Description
--- | --- |---------------------------------| ---
`value` | String | No | the value to use with `onChange` callback
`selected` | Boolean | No | Whether or not the button is selected
42 changes: 42 additions & 0 deletions src/ebay-segmented-buttons/__tests__/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import { composeStories } from '@storybook/react'
import * as stories from './index.stories'

const { Default, WithIcons } = composeStories(stories)

describe('<EbaySegmentedButtons>', () => {
it('should render the default', () => {
render(<Default />)

expect(screen.getByRole('list')).toBeInTheDocument()
expect(screen.getAllByRole('listitem')).toHaveLength(4)
expect(screen.getAllByRole('button')).toHaveLength(4)

const firstButton = screen.getByRole('button', { name: 'Q1' })
expect(firstButton).toBeInTheDocument()
expect(firstButton).toHaveAttribute('aria-current', 'true')
})

it('should handle button clicks', () => {
const spy = jest.fn()
render(<Default onChange={spy} />)

const firstButton = screen.getByRole('button', { name: 'Q1' })
const secondButton = screen.getByRole('button', { name: 'Q2' })
expect(secondButton).toBeInTheDocument()
expect(secondButton).not.toHaveAttribute('aria-current')

fireEvent.click(secondButton)

expect(spy).toHaveBeenCalledTimes(1)
expect(firstButton).not.toHaveAttribute('aria-current')
expect(secondButton).toHaveAttribute('aria-current', 'true')
})

it('should render buttons with icons', () => {
render(<WithIcons />)
expect(screen.getByRole('list')).toBeInTheDocument()
expect(screen.getAllByRole('listitem')).toHaveLength(2)
})
})
52 changes: 52 additions & 0 deletions src/ebay-segmented-buttons/__tests__/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react'
import { Meta, StoryObj } from '@storybook/react'
import { action } from '@storybook/addon-actions'
import { EbaySegmentedButtons, EbaySegmentedButton as Button } from '..'
import { EbayIcon } from '../../ebay-icon'

export default {
title: 'Buttons/ebay-segmented-buttons',
component: EbaySegmentedButtons,
argTypes: {
size: {
options: ['large', 'regular'],
control: {
type: 'select'
},
table: {
defaultValue: {
summary: "regular",
},
},
},
onChange: {
action: 'changed',
table: {
category: 'Events',
defaultValue: {
summary: 'originalEvent, { index, value }'
}
}
}
}
} as Meta<typeof EbaySegmentedButtons>

export const Default: StoryObj<typeof EbaySegmentedButtons> = {
render: args => (
<EbaySegmentedButtons onChange={action('change')} {...args}>
<Button selected value="quarter1">Q1</Button>
<Button value="quarter2">Q2</Button>
<Button value="quarter3">Q3</Button>
<Button value="quarter4">Q4</Button>
</EbaySegmentedButtons>
)
}

export const WithIcons: StoryObj<typeof EbaySegmentedButtons> = {
render: args => (
<EbaySegmentedButtons onChange={action('change')} {...args}>
<Button selected><EbayIcon name="fullView24"/> Desktop</Button>
<Button><EbayIcon name="mobile24"/> Mobile</Button>
</EbaySegmentedButtons>
)
}
39 changes: 39 additions & 0 deletions src/ebay-segmented-buttons/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { FC } from 'react'
import classNames from 'classnames'
import { excludeComponent, findComponent } from '../common/component-utils'
import { EbayIcon } from '../ebay-icon'
import { SegmentedButtonProps } from './types'

const SegmentedButton: FC<SegmentedButtonProps> = ({
selected,
children,
className,
...rest
}) => {
const icon = findComponent(children, EbayIcon)

const iconWithText = () => {
const text = excludeComponent(children, EbayIcon)

return (
<span className="segmented-buttons__button-cell">
{icon}
<span>{text}</span>
</span>
)
}

return (
<li>
<button
className={classNames('segmented-buttons__button', className)}
aria-current={selected || undefined}
{...rest}
>
{icon ? iconWithText() : children}
</button>
</li>
)
}

export default SegmentedButton
3 changes: 3 additions & 0 deletions src/ebay-segmented-buttons/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as EbaySegmentedButtons } from './segmented-buttons'
export { default as EbaySegmentedButton } from './button'
export type { SegmentedButtonsProps, SegmentedButtonProps, SegmentedButtonSize } from './types'
47 changes: 47 additions & 0 deletions src/ebay-segmented-buttons/segmented-buttons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { cloneElement, FC, ReactElement, useState } from 'react'
import classNames from 'classnames'
import { SegmentedButtonProps, SegmentedButtonsProps } from './types'
import { filterByType } from '../common/component-utils'
import SegmentedButton from './button'

const EbaySegmentedButtons: FC<SegmentedButtonsProps> = ({
size,
className,
onChange = () => {},
children,
...rest
}) => {
const buttons = filterByType(children, SegmentedButton)
const [selectedIndex, setSelectedIndex] = useState(
buttons.findIndex(button => button.props.selected) || 0
)

const handleClick = (e, index: number, value: string) => {
setSelectedIndex(index)
onChange(e, { index, value })
}

return (
<div
className={classNames('segmented-buttons', size && `segmented-buttons--${size}`, className)}
{...rest}
>
<ul>
{buttons.map((button: ReactElement, i) => {
const {
value,
...buttonRest
}: SegmentedButtonProps = button.props

return cloneElement(button, {
...buttonRest,
onClick: e => handleClick(e, i, value),
selected: i === selectedIndex
} as SegmentedButtonProps)
})}
</ul>
</div>
)
}

export default EbaySegmentedButtons
15 changes: 15 additions & 0 deletions src/ebay-segmented-buttons/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ComponentProps } from 'react'
import { EbayChangeEventHandler, EbayMouseEventHandler } from '../common/event-utils/types'

export type SegmentedButtonProps = Omit<ComponentProps<'button'>, 'onClick'> & {
value?: string;
selected?: boolean;
onClick?: EbayMouseEventHandler<HTMLButtonElement>;
}

export type SegmentedButtonSize = 'large' | 'regular'

export type SegmentedButtonsProps = Omit<ComponentProps<'div'>, 'onChange'> & {
size?: SegmentedButtonSize;
onChange?: EbayChangeEventHandler<HTMLButtonElement, { index: number, value?: string }>;
}

0 comments on commit bc8829d

Please sign in to comment.