diff --git a/.eslintrc b/.eslintrc index 2ad5e540..8f755f8c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -76,6 +76,7 @@ "ignoreJSX": "all" }], "react/jsx-indent-props": [2, 4], + "react/jsx-no-bind": "off", "react/jsx-indent": [2, 4], // airbnb-overrides diff --git a/config/jest/setup-tests.ts b/config/jest/setup-tests.ts index 3af4335b..e495609f 100644 --- a/config/jest/setup-tests.ts +++ b/config/jest/setup-tests.ts @@ -1,5 +1,9 @@ import '@testing-library/jest-dom' +// Makeup-js uses these methods, but they are not implemented in jsdom +Element.prototype.scrollIntoView = jest.fn(); +Element.prototype.scroll = jest.fn(); + if (typeof window.URL.createObjectURL === 'undefined') { Object.defineProperty(window.URL, 'createObjectURL', { value: () => {} }); } diff --git a/package.json b/package.json index cd9e1e24..aa053c45 100644 --- a/package.json +++ b/package.json @@ -128,9 +128,12 @@ }, "dependencies": { "classnames": "^2.2.6", - "makeup-expander": "^0.10.1", + "makeup-active-descendant": "^0.7.3", + "makeup-expander": "^0.11.3", "makeup-keyboard-trap": "^0.4.1", + "makeup-prevent-scroll-keys": "^0.3.2", "makeup-screenreader-trap": "^0.4.1", + "makeup-typeahead": "^0.3.2", "react-remove-scroll": "^2.2.0" }, "devDependencies": { diff --git a/src/ebay-listbox-button/__tests__/index.spec.tsx b/src/ebay-listbox-button/__tests__/index.spec.tsx index 8e9bce8a..5369299f 100644 --- a/src/ebay-listbox-button/__tests__/index.spec.tsx +++ b/src/ebay-listbox-button/__tests__/index.spec.tsx @@ -1,5 +1,8 @@ +/// + import React from 'react' -import { fireEvent, render, screen } from '@testing-library/react' +import { act, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { eventOfType } from '../../common/event-utils/__tests__/helpers' import { EbayListboxButton, EbayListboxButtonOption } from '..' @@ -20,7 +23,7 @@ describe('', () => { ) const buttonElement = screen.getByRole('button') - const expandBtnTextId = screen.getByText('Option 2').id + const expandBtnTextId = screen.getAllByText('Option 2')[0].id return { buttonElement, expandBtnTextId } } @@ -115,7 +118,7 @@ describe('', () => { describe('when the button is clicked', () => { beforeEach(async () => { - await fireEvent.click(screen.getByRole('button')) + await userEvent.click(screen.getByRole('button')) }) it('then it has expanded the listbox', () => { expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', `true`) @@ -130,12 +133,12 @@ describe('', () => { }) describe('when the button is clicked again', () => { beforeEach(async () => { - await fireEvent.click(screen.getByRole('button')) + await userEvent.click(screen.getByRole('button')) }) it('then it has collapsed the listbox', () => { expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', `false`) }) - xit('focus should move to button', () => { + it('focus should move to button', () => { const button = screen.getByRole('button') jest.runAllTimers() expect(button).toHaveFocus() @@ -154,10 +157,14 @@ describe('', () => { Option 3 ) - fireEvent.click(screen.getByRole('button')) + act(() => { + userEvent.click(screen.getByRole('button')) + }) + const index = 0 - fireEvent.mouseDown(screen.getAllByRole('option')[index]) - fireEvent.click(screen.getAllByRole('option')[index]) + act(() => { + userEvent.click(screen.getAllByRole('option')[index]) + }) expect(spy).toHaveBeenCalledWith(eventOfType('click'), { index, selected: ['AA'], wasClicked: true }) }) @@ -171,18 +178,24 @@ describe('', () => { Option 3 ) - fireEvent.click(screen.getByRole('button')) - fireEvent.mouseDown(screen.getAllByRole('option')[0]) - fireEvent.click(screen.getAllByRole('option')[0]) + act(() => { + userEvent.click(screen.getByRole('button')) + userEvent.click(screen.getAllByRole('option')[0]) + }) + expect(spy).toHaveBeenCalledWith(eventOfType('click'), { index: 0, selected: ['AA'], wasClicked: true }) - fireEvent.mouseDown(screen.getAllByRole('option')[1]) - fireEvent.click(screen.getAllByRole('option')[1]) + act(() => { + userEvent.click(screen.getAllByRole('option')[1]) + }) + expect(spy).toHaveBeenCalledWith(eventOfType('click'), { index: 1, selected: ['BB'], wasClicked: true }) - fireEvent.mouseDown(screen.getAllByRole('option')[2]) - fireEvent.click(screen.getAllByRole('option')[2]) + act(() => { + userEvent.click(screen.getAllByRole('option')[2]) + }) + expect(spy).toHaveBeenCalledWith(eventOfType('click'), { index: 2, selected: ['CC'], wasClicked: true }) }) }) @@ -196,7 +209,10 @@ describe('', () => { Option 3 ) - fireEvent.click(screen.getByRole('button')) + + act(() => { + userEvent.click(screen.getByRole('button')) + }) expect(spy).toHaveBeenCalledWith() }) @@ -212,8 +228,11 @@ describe('', () => { ) const button = screen.getByRole('button') - fireEvent.click(button) - fireEvent.click(button) + + act(() => { + userEvent.click(button) + userEvent.click(button) + }) expect(spy).toHaveBeenCalledWith() }) diff --git a/src/ebay-listbox-button/__tests__/index.stories.tsx b/src/ebay-listbox-button/__tests__/index.stories.tsx index 73eec4e1..8898a1eb 100644 --- a/src/ebay-listbox-button/__tests__/index.stories.tsx +++ b/src/ebay-listbox-button/__tests__/index.stories.tsx @@ -13,9 +13,9 @@ export const Default = () => ( <> action('onChange')(e, props)} - onCollapse={() => action('onCollapse')()} - onExpand={() => action('onExpand')()} + onChange={action('onChange')} + onCollapse={action('onCollapse')} + onExpand={action('onExpand')} > Option 1 Option 2 @@ -37,7 +37,7 @@ export const PreselectedIndex = () => ( export const DefaultNoSelectedOption = () => ( <> action('onChange')(e, props)} + onChange={(e: ChangeEvent, props?: ChangeEventProps) => action('onChange')(e, props)} > Option 1 Option 2 @@ -143,7 +143,7 @@ export const TooManyOptions = () => ( Option 22 Option 23 Option 24 - Option 25 + Option 25 Option 26 Option 27 Option 28 @@ -157,7 +157,7 @@ export const TooManyOptions = () => ( Option 36 Option 37 Option 38 - Option 39 + Option 39 ) diff --git a/src/ebay-listbox-button/__tests__/render.spec.tsx b/src/ebay-listbox-button/__tests__/render.spec.tsx index ff4388d3..f808471b 100644 --- a/src/ebay-listbox-button/__tests__/render.spec.tsx +++ b/src/ebay-listbox-button/__tests__/render.spec.tsx @@ -1,3 +1,5 @@ +/// + import React from 'react' import { render, within } from '@testing-library/react' import { composeStories } from '@storybook/react' @@ -19,7 +21,7 @@ describe('ebay-listbox-button rendering', () => { it('renders default listbox button correctly', () => { const { container } = render() - const listboxButton: HTMLElement = container.querySelector('.listbox-button') + const listboxButton: HTMLElement = container.querySelector('.listbox-button') as HTMLElement const button = within(listboxButton).getByRole('button', { name: 'Option 2' }) expect(button).toHaveClass('btn btn--form') expect(button).toHaveAttribute('aria-expanded', 'false') @@ -27,7 +29,7 @@ describe('ebay-listbox-button rendering', () => { expect(button).toHaveAttribute('type', 'button') expect(button.querySelector('svg')).toMatchSnapshot() - const select = listboxButton.querySelector('select') + const select = listboxButton.querySelector('select') as HTMLElement expect(select).toHaveClass('listbox-button__native') expect(select).toHaveAttribute('hidden', '') @@ -40,7 +42,7 @@ describe('ebay-listbox-button rendering', () => { it('renders preselected index correctly', () => { const { container } = render() - const listboxButton: HTMLElement = container.querySelector('.listbox-button') + const listboxButton: HTMLElement = container.querySelector('.listbox-button') as HTMLElement const button = within(listboxButton).getByRole('button', { name: 'Option 2' }) expect(button).toHaveClass('btn btn--form') expect(button).toHaveAttribute('aria-expanded', 'false') @@ -52,7 +54,7 @@ describe('ebay-listbox-button rendering', () => { it('renders default listbox button without selected option correctly', () => { const { container } = render() - const listboxButton: HTMLElement = container.querySelector('.listbox-button') + const listboxButton: HTMLElement = container.querySelector('.listbox-button') as HTMLElement const button = within(listboxButton).getByRole('button', { name: '-' }) expect(button).toHaveClass('btn btn--form') expect(button).toHaveAttribute('aria-expanded', 'false') @@ -64,7 +66,7 @@ describe('ebay-listbox-button rendering', () => { it('renders borderless listbox button correctly', () => { const { container } = render() - const listboxButton: HTMLElement = container.querySelector('.listbox-button') + const listboxButton: HTMLElement = container.querySelector('.listbox-button') as HTMLElement const button = within(listboxButton).getByRole('button', { name: 'Option 2' }) expect(button).toHaveClass('btn btn--borderless') expect(button).toHaveAttribute('aria-expanded', 'false') @@ -76,7 +78,7 @@ describe('ebay-listbox-button rendering', () => { it('renders fluid listbox button correctly', () => { const { container } = render() - const listboxButton: HTMLElement = container.querySelector('.listbox-button') + const listboxButton: HTMLElement = container.querySelector('.listbox-button') as HTMLElement expect(listboxButton).toHaveClass('listbox-button--fluid') const button = within(listboxButton).getByRole('button', { name: 'Option 2' }) expect(button).toHaveClass('btn btn--form') @@ -89,11 +91,10 @@ describe('ebay-listbox-button rendering', () => { it('renders disabled state correctly', () => { const { container } = render() - const listboxButton: HTMLElement = container.querySelector('.listbox-button') + const listboxButton: HTMLElement = container.querySelector('.listbox-button') as HTMLElement const button = within(listboxButton).getByRole('button', { name: 'Option 2' }) expect(button).toHaveClass('btn btn--form') - expect(button).toHaveAttribute('aria-expanded', 'false') - expect(button).toHaveAttribute('aria-haspopup', 'listbox') + expect(button).toHaveAttribute('type', 'button') expect(button).toHaveAttribute('disabled') expect(button.querySelector('svg')).toMatchSnapshot() @@ -102,7 +103,7 @@ describe('ebay-listbox-button rendering', () => { it('renders invalid state correctly', () => { const { container } = render() - const listboxButton: HTMLElement = container.querySelector('.listbox-button') + const listboxButton: HTMLElement = container.querySelector('.listbox-button') as HTMLElement const button = within(listboxButton).getByRole('button', { name: 'Option 2' }) expect(button).toHaveClass('btn btn--form') expect(button).toHaveAttribute('aria-expanded', 'false') @@ -115,7 +116,7 @@ describe('ebay-listbox-button rendering', () => { it('renders prefix label correctly', () => { const { container } = render() - const listboxButton: HTMLElement = container.querySelector('.listbox-button') + const listboxButton: HTMLElement = container.querySelector('.listbox-button') as HTMLElement const button = within(listboxButton).getByRole('button', { name: 'Selected: Option 2' }) expect(button).toHaveClass('btn btn--form') expect(button).toHaveAttribute('aria-expanded', 'false') @@ -127,7 +128,7 @@ describe('ebay-listbox-button rendering', () => { it('renders floating label correctly', () => { const { container } = render() - const listboxButton: HTMLElement = container.querySelector('.listbox-button') + const listboxButton: HTMLElement = container.querySelector('.listbox-button') as HTMLElement const button = within(listboxButton).getByRole('button') expect(button).toHaveClass('btn btn--form') expect(button).toHaveAttribute('aria-expanded', 'false') diff --git a/src/ebay-listbox-button/index.ts b/src/ebay-listbox-button/index.ts index 79c10b05..679fd414 100644 --- a/src/ebay-listbox-button/index.ts +++ b/src/ebay-listbox-button/index.ts @@ -1,2 +1 @@ -export { default as EbayListboxButton } from './listbox-button' -export { default as EbayListboxButtonOption } from './listbox-button-option' +export * from './listbox-button' diff --git a/src/ebay-listbox-button/listbox-button-option.tsx b/src/ebay-listbox-button/listbox-button-option.tsx deleted file mode 100644 index 814c2244..00000000 --- a/src/ebay-listbox-button/listbox-button-option.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React, { ComponentProps, FC, MouseEvent, RefObject } from 'react' -import classNames from 'classnames' -import { EbayIcon } from '../ebay-icon' - -type EbayListboxButtonOptionProps = ComponentProps<'input'> & { - selected?: boolean; - index?: number; - onClick?: (event: MouseEvent, value: any, index: number) => void; - innerRef?: RefObject; -}; - -const ListboxOption: FC = ({ - value, - children, - selected, - onClick, - index, - innerRef, - className, - id, - ...rest -}) => { - const wrapperClassName = classNames(`listbox-button__option`, className, - { 'listbox-button__option--active': selected }) - return ( -
{onClick(e, value, index)}} - > - {children} - -
) -} - -export default ListboxOption diff --git a/src/ebay-listbox-button/listbox-button.tsx b/src/ebay-listbox-button/listbox-button.tsx index 188fbc6f..fe3d265b 100644 --- a/src/ebay-listbox-button/listbox-button.tsx +++ b/src/ebay-listbox-button/listbox-button.tsx @@ -1,12 +1,68 @@ import React, { - Children, cloneElement, useEffect, useRef, useState, - ComponentProps, FC, KeyboardEvent, ReactElement, useCallback + ChangeEvent, + ComponentProps, + FC, + ReactElement, + useEffect, + useRef, + useState } from 'react' +import Expander from 'makeup-expander' +import * as scrollKeyPreventer from 'makeup-prevent-scroll-keys' import classNames from 'classnames' +import { EbayListboxOption } from '../ebay-listbox/listbox-option' +import { filterByType } from '../utils' import { EbayIcon } from '../ebay-icon' -import { EbayChangeEventHandler, Key } from '../common/event-utils/types' -import { filterByType } from '../common/component-utils' -import EbayListboxButtonOption from './listbox-button-option' +import { EbayListbox, EbayListboxProps } from '../ebay-listbox/' +import { EbayChangeEventHandler } from '../events' +import { EbayButtonProps } from '../ebay-button' + +const RenderText: FC<{ + alwaysDisplay?: boolean, + icon?: ReactElement, + labelId?: string, + text?: string, + unselectedText?: string, + postfixLabel?: string +}> = ({ + alwaysDisplay, + icon, + labelId, + text, + unselectedText, + postfixLabel +}) => { + if (icon) { + return ( + + {icon} + + ) + } + + if (text || alwaysDisplay) { + const displayText = text || unselectedText + return ( + + {displayText} + {postfixLabel && {postfixLabel}} + + ) + } +} + + +function getSelectedValueIndex({ selected, value, options }) { + if (selected !== undefined) { + return selected + } + + if (value !== undefined) { + return options.findIndex(option => option.props.value === value) + } + + return options.findIndex(option => option.props.selected) +} export type ChangeEventProps = { index: number; @@ -14,7 +70,7 @@ export type ChangeEventProps = { wasClicked: boolean; } -export type EbayListboxButtonProps = Omit, 'onChange'> & { +export type EbayListboxButtonProps = Omit, 'onChange'> & { selected?: number; borderless?: boolean; fluid?: boolean; @@ -23,298 +79,211 @@ export type EbayListboxButtonProps = Omit, 'onChange'> & prefixLabel?: string; floatingLabel?: string; unselectedText?: string; - onChange?: EbayChangeEventHandler; + a11yIconPrefixText?: string; + collapseOnSelect?: boolean; + hasError?: boolean; + listSelection?: EbayListboxProps['listSelection']; + postfixLabel?: string; + split?: EbayButtonProps['split']; + truncate?: EbayButtonProps['truncate']; + variant?: 'form'; + onChange?: EbayChangeEventHandler; onCollapse?: () => void; onExpand?: () => void; } -const ListboxButton: FC = ({ - children, - name, - value, - selected, +export const EbayListboxButtonOption = EbayListboxOption + +export const EbayListboxButton: FC = ({ + 'aria-invalid': invalid, + a11yIconPrefixText, borderless, - fluid, + children, className, + collapseOnSelect, + disabled, + floatingLabel, + fluid, + hasError, + listSelection, maxHeight, + name, + postfixLabel, prefixId, prefixLabel, - floatingLabel, + selected, + split, + truncate, unselectedText = '-', + value, + variant, onChange = () => {}, - onCollapse = () => {}, onExpand = () => {}, + onCollapse = () => {}, ...rest }) => { - const optionsContainerRef = useRef(null) - const optionsParentContainerRef = useRef() - const optionsByIndexRef = useRef(new Map()) + const containerRef = useRef() const buttonRef = useRef() - - const listBoxButtonOptions = filterByType(children, EbayListboxButtonOption) - if (!listBoxButtonOptions.length) { - throw new Error(`EbayListboxButton: Please use a - EbayListboxButtonOption that defines the options of the listbox`) - } - const getInitialSelectedOption = (): { option: any, index: number } => { - const selectedIndex = selected !== undefined ? selected : listBoxButtonOptions.findIndex(({ props }) => - value !== undefined && props.value === value) - const index = selectedIndex > -1 || floatingLabel ? selectedIndex : undefined - return { - option: listBoxButtonOptions[index], - index - } - } - - // Get the default Selected value and set it in the state - const { - option: selectedOptionFromValue, - index: initialSelectedOptionIndex - } = getInitialSelectedOption() - // Update the selected option to the state - const [selectedOption, setSelectedOption] = useState(selectedOptionFromValue) - const [selectedIndex, setSelectedIndex] = useState(initialSelectedOptionIndex) - // Update the expanded status to the state - const [expanded, setExpanded] = useState() - // Additional flag to avoid multiple re-render when users tries to open and close - const [optionsOpened, setOptionsOpened] = useState(false) - const [wasClicked, setWasClicked] = useState() + const expanderRef = useRef() + const options = filterByType(children, EbayListboxOption) + const [selectedIndex, setSelectedIndex] = useState(getSelectedValueIndex({ selected, value, options })) + const selectedOption = options[selectedIndex] + const selectedText = selectedOption && (selectedOption.props.text || selectedOption.props.children) + const selectedIcon = selectedOption && selectedOption.props.icon + const a11ySelectedIconText = + selectedIcon && selectedText + const labelId = prefixId && 'expand-btn-text' useEffect(() => { - setSelectedOption(selectedOptionFromValue) - }, [value]) + setSelectedIndex(getSelectedValueIndex({ selected, value, options })) + }, [value, selected]) - const childrenArray = Children.toArray(children) as ReactElement[] - const getSelectedValueByIndex = (index: number) => childrenArray[index].props.value - const getIndexByValue = useCallback((selectedValue) => - childrenArray.findIndex(({ props }) => props.value === selectedValue), [childrenArray]) - const getSelectedOption = (currentIndex: number) => optionsByIndexRef.current.get(currentIndex) - const setActiveDescendant = (index: number) => { - const optionsContainerEle = optionsContainerRef.current - optionsContainerEle?.setAttribute(`aria-activedescendant`, getSelectedOption(index).id) - } + useEffect(() => { + function handleExpand() { + containerRef.current?.querySelector('.listbox-button__listbox')?.scroll() + onExpand() + } - const collapseListbox = () => { - setExpanded(false) - onCollapse() - } + function handleCollapse() { + requestAnimationFrame(() => { + buttonRef.current?.focus() + }) + onCollapse() + } - const expandListbox = () => { - setExpanded(true) - onExpand() - } + if (options.length && !disabled) { + expanderRef.current = new Expander(containerRef.current, { + alwaysDoFocusManagement: true, + autoCollapse: true, + expandOnClick: true, + simulateSpacebarClick: true, + contentSelector: '.listbox-button__listbox', + hostSelector: '.listbox-button__control', + expandedClass: 'listbox-button--expanded', + focusManagement: 'content' + }) - const toggleListbox = () => { - if (expanded) { - collapseListbox() - } else { - expandListbox() + containerRef.current.addEventListener('expander-expand', handleExpand) + containerRef.current.addEventListener('expander-collapse', handleCollapse) } - } - const onOptionsSelect = (e, index) => { - // OnSelect set the selectedValue to the state and expanded to false to close the list box - setSelectedOption(childrenArray[index]) - setSelectedIndex(index) - collapseListbox() - setActiveDescendant(index) - buttonRef.current.focus() - onChange(e, { index, selected: [getSelectedValueByIndex(index)], wasClicked }) - setWasClicked(false) - } + scrollKeyPreventer.add(buttonRef.current) - const reset = () => { - collapseListbox() - setSelectedOption(childrenArray[initialSelectedOptionIndex]) - } - - const makeOptionActive = (index: number) => { - const optionEle = optionsContainerRef.current.children[index] - optionEle.setAttribute(`aria-selected`, 'true') - optionEle.classList.add(`listbox-button__option--active`) - } + return () => { + if (expanderRef.current) { + expanderRef.current.destroy() + expanderRef.current = undefined + } - const makeOptionInActive = (index: number) => { - const optionEle = optionsContainerRef.current.children[index] - optionEle.setAttribute(`aria-selected`, 'false') - optionEle.classList.remove(`listbox-button__option--active`) - } + if (containerRef.current) { + containerRef.current.removeEventListener('expander-expand', handleExpand) + containerRef.current.removeEventListener('expander-collapse', handleCollapse) + } - // Followed the implementation from W3 - // https://www.w3.org/TR/wai-aria-practices/examples/listbox/listbox-collapsible.html - const scrollOptions = (index: number) => { - const listboxOptionsContainerNode = optionsParentContainerRef.current - const currentTarget = getSelectedOption(index) - if (listboxOptionsContainerNode.scrollHeight > listboxOptionsContainerNode.clientHeight) { - const scrollBottom = listboxOptionsContainerNode.clientHeight + listboxOptionsContainerNode.scrollTop - const elementBottom = currentTarget.offsetTop + currentTarget.offsetHeight - if (elementBottom > scrollBottom) { - listboxOptionsContainerNode.scrollTop = elementBottom - listboxOptionsContainerNode.clientHeight - } else if (currentTarget.offsetTop < listboxOptionsContainerNode.scrollTop) { - listboxOptionsContainerNode.scrollTop = currentTarget.offsetTop + if (buttonRef.current) { + scrollKeyPreventer.remove(buttonRef.current) } } - } + }, [disabled]) - const makeSelections = (updatedIndex) => { - makeOptionActive(selectedIndex === undefined || updatedIndex === -1 ? 0 : updatedIndex) - makeOptionInActive(selectedIndex === undefined || selectedIndex === -1 ? 0 : selectedIndex) - scrollOptions(updatedIndex) - setActiveDescendant(updatedIndex) - setSelectedIndex(updatedIndex) - setSelectedOption(childrenArray[updatedIndex]) - } - - const focusOptionsContainer = (focusOptions?: FocusOptions) => - setTimeout(() => optionsContainerRef?.current?.focus(focusOptions), 0) - const onButtonClick = () => { - toggleListbox() - setOptionsOpened(true) - focusOptionsContainer({ preventScroll: true }) - } - const onButtonKeyup = (e: KeyboardEvent) => { - switch (e.key as Key) { - case 'Escape': - collapseListbox() - break - case 'Enter': - focusOptionsContainer() - break - default: - break - } - } - const onOptionContainerKeydown = (e: KeyboardEvent): void => { - switch (e.key as Key) { - case ' ': - case 'PageUp': - case 'PageDown': - case 'Home': - case 'End': - e.preventDefault() - break - case 'Down': - case 'ArrowDown': - e.preventDefault() - if (selectedIndex !== listBoxButtonOptions.length - 1) { - makeSelections(selectedIndex < listBoxButtonOptions.length - 1 ? selectedIndex + 1 : 0) - } - break - case 'Up': - case 'ArrowUp': - e.preventDefault() - if (selectedIndex !== 0) { - makeSelections(selectedIndex > 0 ? selectedIndex - 1 : listBoxButtonOptions.length - 1) - } - break - case 'Enter': - collapseListbox() - setTimeout(() => setSelectedOption(childrenArray[selectedIndex])) - setTimeout(() => buttonRef.current.focus(), 0) - onChange(e as any, { - index: selectedIndex, - selected: [getSelectedValueByIndex(selectedIndex)], - wasClicked - }) - break - case 'Esc': - case 'Escape': - reset() - break - default: - break + function handleListboxChange(event: ChangeEvent, data: ChangeEventProps) { + if (collapseOnSelect !== false) { + expanderRef.current.expanded = false } - } - // We want to mimic the select box behavior, so we take the onSelect that passed - // at the parent level and use it for the OnClick on the list box since it is a fake dropdown - const updateListBoxButtonOptions = listBoxButtonOptions - .map((child, index) => cloneElement(child, { - index, - key: index, - selected: selectedOption && child.props.value === selectedOption.props.value, - onClick: (e) => onOptionsSelect(e, index), - innerRef: optionNode => !optionNode - ? optionsByIndexRef.current.delete(index) - : optionsByIndexRef.current.set(index, optionNode) - })) - const wrapperClassName = classNames('listbox-button', className, { 'listbox-button--fluid': fluid }) - const buttonClassName = classNames('btn', { - 'btn--form': !borderless, - 'btn--borderless': borderless, - 'btn--floating-label': floatingLabel && selectedOption - }) - const expandBtnTextId = prefixId && 'expand-btn-text' + setSelectedIndex(data.index) + onChange(event, data) + } + function handleListboxEscape() { + expanderRef.current.expanded = false + } - const buttonLabel = ( - <> - {floatingLabel && {floatingLabel}} - {prefixLabel && {prefixLabel}} - - {selectedOption?.props.children || unselectedText} - - - ) + const Container = truncate && !fluid ? 'div' : 'span' return ( - + - {(expanded || optionsOpened) && -
-
onOptionContainerKeydown(e)} - // adding onMouseDown preventDefault b/c on IE the onClick event is not being fired on each - // option https://stackoverflow.com/questions/17769005/onclick-and-onblur-ordering-issue - onMouseDown={(e) => { - e.preventDefault() - setWasClicked(true) - }} - onBlur={() => { - collapseListbox() - setTimeout(() => buttonRef.current.focus(), 0) - }} - > - {updateListBoxButtonOptions} -
-
} - -
+ maxHeight={maxHeight} + onChange={handleListboxChange} + onEscape={handleListboxEscape} + > + {options.map((option, index) => ( + + ))} + + ) } - -export default ListboxButton diff --git a/src/ebay-listbox/README.md b/src/ebay-listbox/README.md new file mode 100644 index 00000000..56205400 --- /dev/null +++ b/src/ebay-listbox/README.md @@ -0,0 +1,100 @@ +# EbayListbox + +## Demo + +[Storybook](https://opensource.ebay.com/ebayui-core-react/main/?path=/docs/building-blocks-ebay-listbox--docs) + +## Import JS + +```jsx harmony +import { + EbayListbox, + EbayListboxOption, + EbayListboxOptionDescription, +} from "@ebay/ui-core-react/ebay-listbox"; +``` + +## Import following styles from SKIN + +```js +import "@ebay/skin/listbox"; +``` + +## Import styles using SCSS/CSS + +```js +import "@ebay/skin/listbox.css"; +``` + +## Usage + +```jsx + + + + + +``` + +## Attributes + +### EbayListbox + +| Name | Type | Required | Description | +| ------------------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------- | +| `name` | string | No | Used for the name attribute of the native ` {}}> + {options.map((option, index) => ( +