diff --git a/packages/components/src/Accordion/Accordion.stories.tsx b/packages/components/src/Accordion/Accordion.stories.tsx index 038b976b1..1ab5ef600 100644 --- a/packages/components/src/Accordion/Accordion.stories.tsx +++ b/packages/components/src/Accordion/Accordion.stories.tsx @@ -12,7 +12,7 @@ export default { layout: 'centered' }, render: (args) => ( - +

This is item 1 section

diff --git a/packages/components/src/Accordion/Accordion.tsx b/packages/components/src/Accordion/Accordion.tsx index a03dd58f3..637c3ef2b 100644 --- a/packages/components/src/Accordion/Accordion.tsx +++ b/packages/components/src/Accordion/Accordion.tsx @@ -18,12 +18,15 @@ type NativeAttrs = Omit, (keyof InheritAttrs & Pro type AccordionProps = Props & InheritAttrs & NativeAttrs; const Accordion = >( - { size = 'base', ...props }: AccordionProps, + { size = 'base', defaultExpandedKeys, disabledKeys, expandedKeys, onExpandedChange, ...props }: AccordionProps, ref: Ref ): JSX.Element => { - const state = useTreeState(props); + const ariaProps = { defaultExpandedKeys, disabledKeys, expandedKeys, onExpandedChange, ...props }; + + const state = useTreeState(ariaProps); + const accordionRef = useDOMRef(ref); - const { accordionProps } = useAccordion(props, state, accordionRef); + const { accordionProps } = useAccordion(ariaProps, state, accordionRef); return (
diff --git a/packages/components/src/Accordion/__tests__/Accordion.test.tsx b/packages/components/src/Accordion/__tests__/Accordion.test.tsx index 579218545..a0a8fac40 100644 --- a/packages/components/src/Accordion/__tests__/Accordion.test.tsx +++ b/packages/components/src/Accordion/__tests__/Accordion.test.tsx @@ -1,6 +1,7 @@ -import { render } from '@testing-library/react'; -import * as React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; import { testA11y } from '@interlay/test-utils'; +import { Key, createRef, useState } from 'react'; +import userEvent from '@testing-library/user-event'; import { Accordion, AccordionItem } from '..'; @@ -16,7 +17,7 @@ describe('Accordion', () => { }); it('ref should be forwarded', () => { - const ref = React.createRef(); + const ref = createRef(); render( @@ -33,4 +34,83 @@ describe('Accordion', () => { ); }); + + it('should have default expanded key', async () => { + render( + + + Content 1 + + + Content 2 + + + ); + + expect(screen.getByRole('button', { name: /item 1/i })).toHaveAttribute('aria-expanded', 'true'); + }); + + it('should have disable key', async () => { + render( + + + Content 1 + + + Content 2 + + + ); + + expect(screen.getByRole('button', { name: /item 1/i })).toBeDisabled(); + }); + + it('should be able to control expanded keys', async () => { + const Component = () => { + const [state, setState] = useState>(new Set(['1'])); + + return ( + + + Content 1 + + + Content 2 + + + ); + }; + + render(); + + expect(screen.getByRole('button', { name: /item 1/i })).toHaveAttribute('aria-expanded', 'true'); + + userEvent.click(screen.getByRole('button', { name: /item 2/i })); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /item 2/i })).toHaveAttribute('aria-expanded', 'true'); + }); + }); + + it('should emit onExpandedChange', async () => { + const onExpandedChange = jest.fn(); + + render( + + + Content 1 + + + Content 2 + + + ); + + userEvent.click(screen.getByRole('button', { name: /item 1/i })); + + await waitFor(() => { + expect(onExpandedChange).toHaveBeenCalledTimes(1); + expect(onExpandedChange).toHaveBeenCalledWith(new Set(['1'])); + }); + }); }); diff --git a/packages/components/src/Dialog/__tests__/Dialog.test.tsx b/packages/components/src/Dialog/__tests__/Dialog.test.tsx index f71c3ac18..83b3a4b5d 100644 --- a/packages/components/src/Dialog/__tests__/Dialog.test.tsx +++ b/packages/components/src/Dialog/__tests__/Dialog.test.tsx @@ -1,6 +1,7 @@ -import { render } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { createRef } from 'react'; import { testA11y } from '@interlay/test-utils'; +import userEvent from '@testing-library/user-event'; import { Dialog, DialogBody, DialogDivider, DialogFooter, DialogHeader } from '..'; @@ -43,4 +44,38 @@ describe('Dialog', () => { ); }); + + it('should dialog sections', async () => { + render( + + title + + body + footer + + ); + + expect(screen.getByRole('heading', { level: 3, name: /title/i })).toBeInTheDocument(); + expect(screen.getByText(/body/i)).toBeInTheDocument(); + expect(screen.getByText(/footer/i)).toBeInTheDocument(); + }); + + it('should emit onClose using close btn', async () => { + const handleClose = jest.fn(); + + render( + + title + + body + footer + + ); + + userEvent.click(screen.getByRole('button', { name: /dismiss/i })); + + await waitFor(() => { + expect(handleClose).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/components/src/Modal/__tests__/Modal.test.tsx b/packages/components/src/Modal/__tests__/Modal.test.tsx index 0ddb069f4..899425b9b 100644 --- a/packages/components/src/Modal/__tests__/Modal.test.tsx +++ b/packages/components/src/Modal/__tests__/Modal.test.tsx @@ -1,6 +1,7 @@ -import { render } from '@testing-library/react'; -import { createRef } from 'react'; +import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import { createRef, useState } from 'react'; import { testA11y } from '@interlay/test-utils'; +import userEvent from '@testing-library/user-event'; import { Modal, ModalBody, ModalDivider, ModalFooter, ModalHeader } from '..'; @@ -43,4 +44,106 @@ describe('Modal', () => { ); }); + + it('should control open state', async () => { + const Component = () => { + const [isOpen, setOpen] = useState(false); + + return ( + <> + + setOpen(false)}> + title + + body + footer + + + ); + }; + + render(); + + userEvent.click(screen.getByRole('button', { name: /trigger/i })); + + await waitFor(() => { + expect(screen.getByRole('dialog', { name: /title/i })); + }); + + userEvent.click(screen.getByRole('button', { name: /dismiss/i })); + + await waitForElementToBeRemoved(screen.getByRole('dialog', { name: /title/i })); + }); + + it('should modal sections', async () => { + render( + + title + + body + footer + + ); + + expect(screen.getByRole('heading', { level: 3, name: /title/i })).toBeInTheDocument(); + expect(screen.getByText(/body/i)).toBeInTheDocument(); + expect(screen.getByText(/footer/i)).toBeInTheDocument(); + }); + + it('should emit onClose using close btn', async () => { + const handleClose = jest.fn(); + + render( + + title + + body + footer + + ); + + userEvent.click(screen.getByRole('button', { name: /dismiss/i })); + + await waitFor(() => { + expect(handleClose).toHaveBeenCalledTimes(1); + }); + }); + + it('should emit onClose using ESC key', async () => { + const handleClose = jest.fn(); + + render( + + title + + body + footer + + ); + + userEvent.keyboard('{Escape}'); + + await waitFor(() => { + expect(handleClose).toHaveBeenCalledTimes(1); + }); + }); + + it('should emit onClose by clicking modal wrapper', async () => { + const handleClose = jest.fn(); + + render( + + title + + body + footer + + ); + + userEvent.click(screen.getByRole('dialog', { name: /title/i }).parentElement?.parentElement as any); + + await waitFor(() => { + expect(handleClose).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/components/src/Overlay/OpenTransition.tsx b/packages/components/src/Overlay/OpenTransition.tsx index 969e98df2..350d9bd40 100644 --- a/packages/components/src/Overlay/OpenTransition.tsx +++ b/packages/components/src/Overlay/OpenTransition.tsx @@ -21,7 +21,8 @@ type OpenTransitionProps = Props & InheritAttrs; const OpenTransition = (props: OpenTransitionProps): any => { // Do not apply any transition if in chromatic (based on react-spectrum) if (process.env.NODE_ENV === 'test') { - return Children.map(props.children, (child) => child && cloneElement(child as any, { isOpen: props.in })); + // MEMO: removed { isOpen: props.in } because of error with prop for component that do not recognize it + return Children.map(props.children, (child) => child && cloneElement(child as any)); } return ( diff --git a/packages/components/src/Overlay/__tests__/Overlay.test.tsx b/packages/components/src/Overlay/__tests__/Overlay.test.tsx index 1c55f1de3..5d097a074 100644 --- a/packages/components/src/Overlay/__tests__/Overlay.test.tsx +++ b/packages/components/src/Overlay/__tests__/Overlay.test.tsx @@ -9,7 +9,7 @@ describe('Overlay', () => { const ref = createRef(); const wrapper = render( - +
); diff --git a/packages/components/src/Popover/__tests__/Popover.test.tsx b/packages/components/src/Popover/__tests__/Popover.test.tsx index de46fec41..a25b62ab3 100644 --- a/packages/components/src/Popover/__tests__/Popover.test.tsx +++ b/packages/components/src/Popover/__tests__/Popover.test.tsx @@ -1,8 +1,10 @@ -import { render } from '@testing-library/react'; +import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; import { createRef } from 'react'; import { testA11y } from '@interlay/test-utils'; +import userEvent from '@testing-library/user-event'; import { Popover, PopoverBody, PopoverContent, PopoverFooter, PopoverHeader, PopoverTrigger } from '..'; +import { CTA } from '../../CTA'; describe('Popover', () => { it('should render correctly', () => { @@ -55,4 +57,118 @@ describe('Popover', () => { ); }); + + it('should able to open by using trigger', async () => { + render( + + + trigger + + + header + body + footer + + + ); + + userEvent.click(screen.getByRole('button', { name: /trigger/i })); + + await waitFor(() => { + expect(screen.getByRole('dialog', { name: /header/i })); + }); + }); + + it('should able to close popover by using ESC', async () => { + render( + + + trigger + + + header + body + footer + + + ); + + userEvent.click(screen.getByRole('button', { name: /trigger/i })); + + await waitFor(() => { + expect(screen.getByRole('dialog', { name: /header/i })); + }); + + userEvent.keyboard('{Escape}'); + + await waitForElementToBeRemoved(screen.getByRole('dialog', { name: /header/i })); + }); + + it('should able to close popover by clicking outside of the component', async () => { + render( + + + trigger + + + header + body + footer + + + ); + + userEvent.click(screen.getByRole('button', { name: /trigger/i })); + + await waitFor(() => { + expect(screen.getByRole('dialog', { name: /header/i })); + }); + + userEvent.click(document.body); + + await waitForElementToBeRemoved(screen.getByRole('dialog', { name: /header/i })); + }); + + it('should emit onOpenChange', async () => { + const handleOpenChange = jest.fn(); + + render( + + + trigger + + + header + body + footer + + + ); + + userEvent.click(screen.getByRole('button', { name: /trigger/i })); + + await waitFor(() => { + expect(screen.getByRole('dialog', { name: /header/i })); + }); + + expect(handleOpenChange).toHaveBeenCalledTimes(1); + expect(handleOpenChange).toHaveBeenCalledWith(true); + }); + + it('should be default open', async () => { + render( + + + trigger + + + header + body + footer + + + ); + + expect(screen.getByRole('dialog', { name: /header/i })); + }); }); diff --git a/packages/components/src/Switch/Switch.tsx b/packages/components/src/Switch/Switch.tsx index ee61e5b5c..8e1d6ef0d 100644 --- a/packages/components/src/Switch/Switch.tsx +++ b/packages/components/src/Switch/Switch.tsx @@ -4,7 +4,7 @@ import { AriaSwitchProps, useSwitch } from '@react-aria/switch'; import { mergeProps } from '@react-aria/utils'; import { useToggleState } from '@react-stately/toggle'; import { PressEvent } from '@react-types/shared'; -import { ChangeEvent, forwardRef, HTMLAttributes, useRef } from 'react'; +import { ChangeEvent, ChangeEventHandler, forwardRef, HTMLAttributes, useRef } from 'react'; import { Placement } from '@interlay/theme'; import { useDOMRef } from '@interlay/hooks'; @@ -14,6 +14,7 @@ import { StyledInput, StyledLabel, StyledSwitch, StyledWrapper } from './Switch. type Props = { onChange?: (e: ChangeEvent) => void; + onValueChange?: (isSelected: boolean) => void; onPress?: (e: PressEvent) => void; labelProps?: TextProps; labelPlacement?: Extract; @@ -26,11 +27,26 @@ type InheritAttrs = Omit; type SwitchProps = Props & NativeAttrs & InheritAttrs; const Switch = forwardRef( - ({ children, onChange, className, style, hidden, labelProps, labelPlacement, ...props }, ref): JSX.Element => { + ( + { + children, + onChange, + onValueChange, + className, + style, + hidden, + labelProps, + labelPlacement, + isSelected, + isReadOnly, + ...props + }, + ref + ): JSX.Element => { const labelRef = useDOMRef(ref); const inputRef = useRef(null); - const ariaProps: AriaSwitchProps = { children, ...props }; + const ariaProps: AriaSwitchProps = { children, isSelected, isReadOnly, ...props }; const state = useToggleState(ariaProps); const { inputProps } = useSwitch(ariaProps, state, inputRef); @@ -41,6 +57,13 @@ const Switch = forwardRef( const { pressProps } = usePress(props); + const handleChange: ChangeEventHandler = (e) => { + const isSelected = e.target.checked; + + onChange?.(e); + onValueChange?.(isSelected); + }; + return ( ( hidden={hidden} style={style} > - + {children && {children}} diff --git a/packages/components/src/Switch/__tests__/Switch.test.tsx b/packages/components/src/Switch/__tests__/Switch.test.tsx index cf60bc161..9db22d991 100644 --- a/packages/components/src/Switch/__tests__/Switch.test.tsx +++ b/packages/components/src/Switch/__tests__/Switch.test.tsx @@ -1,6 +1,7 @@ -import { render } from '@testing-library/react'; -import { createRef } from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import { createRef, useState } from 'react'; import { testA11y } from '@interlay/test-utils'; +import userEvent from '@testing-library/user-event'; import { Switch } from '..'; @@ -22,4 +23,57 @@ describe('Switch', () => { it('should pass a11y', async () => { await testA11y(switch); }); + + it('should emit onChange and onValueChange', async () => { + const handleChange = jest.fn(); + const handleValueChange = jest.fn(); + + render( + + switch + + ); + + userEvent.click(screen.getByRole('switch', { name: /switch/i })); + + await waitFor(() => { + expect(handleChange).toHaveBeenCalledTimes(1); + expect(handleValueChange).toHaveBeenCalledTimes(1); + expect(handleValueChange).toHaveBeenCalledWith(true); + }); + }); + + it('should control value', async () => { + const Component = () => { + const [state, setState] = useState(false); + + return ( + setState(isSelected)}> + switch + + ); + }; + + render(); + + expect(screen.getByRole('switch', { name: /switch/i })).not.toBeChecked(); + + userEvent.click(screen.getByRole('switch', { name: /switch/i })); + + await waitFor(() => { + expect(screen.getByRole('switch', { name: /switch/i })).toBeChecked(); + }); + }); + + it('should be disabled', async () => { + render(switch); + + expect(screen.getByRole('switch', { name: /switch/i })).toBeDisabled(); + }); + + it('should be read only', async () => { + render(switch); + + expect(screen.getByRole('switch', { name: /switch/i })).toHaveAttribute('aria-readonly', 'true'); + }); }); diff --git a/packages/components/src/Tabs/__tests__/Tabs.test.tsx b/packages/components/src/Tabs/__tests__/Tabs.test.tsx index c322949ae..e8455d604 100644 --- a/packages/components/src/Tabs/__tests__/Tabs.test.tsx +++ b/packages/components/src/Tabs/__tests__/Tabs.test.tsx @@ -1,6 +1,7 @@ -import { render } from '@testing-library/react'; -import { createRef } from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import { Key, createRef, useState } from 'react'; import { testA11y } from '@interlay/test-utils'; +import userEvent from '@testing-library/user-event'; import { Tabs, TabsItem } from '..'; @@ -40,4 +41,62 @@ describe('Tabs', () => { ); }); + + it('should display default selected key', async () => { + render( + + + item 1 + + + item 2 + + + ); + + expect(screen.getByRole('tab', { name: /title 2/i })).toHaveAttribute('aria-selected', 'true'); + expect(screen.getByRole('tabpanel')).toHaveTextContent(/item 2$/i); + }); + + it('should be able to control open tab', async () => { + const Component = () => { + const [state, setState] = useState('2'); + + return ( + + + item 1 + + + item 2 + + + ); + }; + + render(); + + expect(screen.getByRole('tab', { name: /title 2/i })).toHaveAttribute('aria-selected', 'true'); + + userEvent.click(screen.getByRole('tab', { name: /title 1/i })); + + await waitFor(() => { + expect(screen.getByRole('tab', { name: /title 1/i })).toHaveAttribute('aria-selected', 'true'); + }); + }); + + it('should disabled tab', async () => { + render( + + + item 1 + + + item 2 + + + ); + + expect(screen.getByRole('tab', { name: /title 1/i })).toHaveAttribute('aria-disabled', 'true'); + }); }); diff --git a/packages/components/src/Tooltip/__tests__/Tooltip.test.tsx b/packages/components/src/Tooltip/__tests__/Tooltip.test.tsx index ad660d9af..a723e330a 100644 --- a/packages/components/src/Tooltip/__tests__/Tooltip.test.tsx +++ b/packages/components/src/Tooltip/__tests__/Tooltip.test.tsx @@ -1,14 +1,13 @@ +import { testA11y } from '@interlay/test-utils'; import { render } from '@testing-library/react'; import { createRef } from 'react'; -import { testA11y } from '@interlay/test-utils'; import { Tooltip } from '..'; -// FIXME: isOpen prop throwing error -describe.skip('Tooltip', () => { +describe('Tooltip', () => { it('should render correctly', () => { const wrapper = render( - + trigger ); @@ -20,7 +19,7 @@ describe.skip('Tooltip', () => { const ref = createRef(); render( - + trigger ); @@ -30,7 +29,7 @@ describe.skip('Tooltip', () => { it('should pass a11y', async () => { await testA11y( - + trigger );