diff --git a/package-lock.json b/package-lock.json index df827b9d9..3f9e25008 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2402,6 +2402,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { diff --git a/packages/components/dropdown/src/Dropdown.stories.tsx b/packages/components/dropdown/src/Dropdown.stories.tsx index 4df215bbe..0c4bf92f1 100644 --- a/packages/components/dropdown/src/Dropdown.stories.tsx +++ b/packages/components/dropdown/src/Dropdown.stories.tsx @@ -11,22 +11,6 @@ const meta: Meta = { export default meta -const books = [ - { value: 'book-1', text: 'To Kill a Mockingbird' }, - { value: 'book-2', text: 'War and Peace' }, - { value: 'book-3', text: 'The Idiot', disabled: true }, - { value: 'book-4', text: 'A Picture of Dorian Gray' }, - { value: 'book-5', text: '1984' }, - { value: 'book-6', text: 'Pride and Prejudice' }, - { value: 'book-7', text: 'Meditations' }, - { - value: 'book-8', - text: 'The Brothers Karamazov', - }, - { value: 'book-9', text: 'Anna Karenina' }, - { value: 'book-10', text: 'Crime and Punishment' }, -] - export const Default: StoryFn = _args => { const [removeItems, setRemoveItems] = useState(false) @@ -38,15 +22,18 @@ export const Default: StoryFn = _args => { - {books.map(item => { - if (removeItems && item.value === 'book-1') return null - - return ( - - {item.text} - - ) - })} + {!removeItems && To Kill a Mockingbird} + War and Peace + + The Idiot + + {!removeItems && A Picture of Dorian Gray} + 1984 + Pride and Prejudice + Meditations + The Brothers Karamazov + Anna Karenina + Crime and Punishment diff --git a/packages/components/dropdown/src/Dropdown.test.tsx b/packages/components/dropdown/src/Dropdown.test.tsx index 01bf970a2..f73644747 100644 --- a/packages/components/dropdown/src/Dropdown.test.tsx +++ b/packages/components/dropdown/src/Dropdown.test.tsx @@ -1,12 +1,23 @@ import { render, screen } from '@testing-library/react' import { describe, expect, it } from 'vitest' -import { Dropdown } from './Dropdown' +import { Dropdown } from '.' describe('Dropdown', () => { - it('should render', () => { - render() + it('should render list of options', () => { + render( + + + + War and Peace + 1984 + Pride and Prejudice + + + ) - expect(screen.getByText(/dropdown/)).toBeInTheDocument() + expect(screen.getByRole('option', { name: 'War and Peace' })).toBeInTheDocument() + expect(screen.getByRole('option', { name: '1984' })).toBeInTheDocument() + expect(screen.getByRole('option', { name: 'Pride and Prejudice' })).toBeInTheDocument() }) }) diff --git a/packages/components/dropdown/src/DropdownContext.tsx b/packages/components/dropdown/src/DropdownContext.tsx index 882c3d4b2..57d34e105 100644 --- a/packages/components/dropdown/src/DropdownContext.tsx +++ b/packages/components/dropdown/src/DropdownContext.tsx @@ -1,15 +1,9 @@ import { useSelect } from 'downshift' -import { createContext, PropsWithChildren, useContext, useState } from 'react' +import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' import { type DownshiftState, type DropdownItem, type ItemsMap } from './types' -import { getElementByIndex } from './utils' +import { getElementByIndex, getOrderedItems } from './utils' export interface DropdownContextState extends DownshiftState { - /** - * Used by `Dropdown.Item` to register it's data in the global context. - * It makes the context aware of the items to manage. - */ - registerItem: (item: DropdownItem) => void - unregisterItem: (value: string) => void computedItems: ItemsMap higlightedItem: DropdownItem | undefined } @@ -53,37 +47,40 @@ export const DropdownProvider = ({ children }: DropdownContextProps) => { // environment?: Environment }) - const registerItem = (item: DropdownItem) => { - console.log('REGISTER => ', item.value) - - setComputedItems(map => { - return new Map(map.set(item.value, item)) + /** + * Indices in a Map are set when an element is added to the Map. + * If for some reason, in the Dropdown: + * - children order changes + * - children are added + * - children are removed + * + * The Map must be rebuilt from the new children in order to preserve logical indices. + * + * Downshift is heavily indices based for keyboard navigation, so it it important. + */ + const syncItems = () => { + const newMap: ItemsMap = new Map() + + getOrderedItems(children).forEach(({ value, disabled, children }) => { + newMap.set(value, { + value, + disabled: !!disabled, + text: children, + }) }) - } - - const unregisterItem = (value: string) => { - console.log('UNREGISTER => ', value) - - // setComputedItems(map => { - // map.delete(value) - - // return new Map(map) - // }) - // const newComputedItems = new Map(computedItems) - // newComputedItems.delete(value) - // setComputedItems(newComputedItems) + setComputedItems(newMap) } - console.log(computedItems) + useEffect(() => { + syncItems() + }, [children]) return ( diff --git a/packages/components/dropdown/src/DropdownItem.tsx b/packages/components/dropdown/src/DropdownItem.tsx index 757ea9d36..d3112120d 100644 --- a/packages/components/dropdown/src/DropdownItem.tsx +++ b/packages/components/dropdown/src/DropdownItem.tsx @@ -1,36 +1,24 @@ import { cx } from 'class-variance-authority' -import { useEffect } from 'react' import { useDropdown } from './DropdownContext' import { getIndexByKey } from './utils' +export interface ItemProps { + disabled?: boolean + value: string + children: string +} + export const Item = ({ disabled = false, value, children, // TODO: allow more than string and implement Dropdown.ItemText -}: { - disabled?: boolean - value: string - children: string -}) => { - const { - computedItems, - selectedItem, - getItemProps, - registerItem, - unregisterItem, - higlightedItem, - } = useDropdown() +}: ItemProps) => { + const { computedItems, selectedItem, getItemProps, higlightedItem } = useDropdown() const index = getIndexByKey(computedItems, value) const itemData = { disabled, value, text: children } - useEffect(() => { - registerItem(itemData) - - return () => unregisterItem(value) - }, []) - return (
  • { return key !== undefined ? map.get(key) : undefined } + +const getElementId = (element?: ReactElement) => { + return element ? (element.type as FC & { id?: string }).id : '' +} + +export const getOrderedItems = (children: ReactNode, result: ItemProps[] = []): ItemProps[] => { + React.Children.forEach(children, child => { + if (!isValidElement(child)) return + + if (getElementId(child) === 'Item') { + result.push(child.props as ItemProps) + } + + if (child.props.children) { + getOrderedItems(child.props.children, result) + } + }) + + return result +}