diff --git a/.changeset/bright-dodos-deny.md b/.changeset/bright-dodos-deny.md new file mode 100644 index 00000000000..04b4216a504 --- /dev/null +++ b/.changeset/bright-dodos-deny.md @@ -0,0 +1,26 @@ +--- +'@talend/scripts-core': major +'@talend/scripts-config-jest': major +--- + +- fix: enforce timer at the end of all tests. +- feat: mock ally.js has it uses unsupported dom method from jsdom. +- feat: add jest-axe configuration + + +To support floating-ui/react following issue we have decided to add an afterAll to let floating-ui finish stuff +https://github.com/floating-ui/floating-ui/issues/1908 + + +Breaking changes: + +you may have tests where you ask for jest.useFakeTimer without go back to real at some point. This is a side effect and it is not compatible with our change to support floating-ui. + +```diff +jest.useFakeTimers() +render( + +
+ +
+
+ +
+
+ +
+
+ +
+ , + +`; diff --git a/packages/design-system/src/components/Accordion/index.ts b/packages/design-system/src/components/Accordion/index.ts new file mode 100644 index 00000000000..be9e4624973 --- /dev/null +++ b/packages/design-system/src/components/Accordion/index.ts @@ -0,0 +1,2 @@ +export * from './Accordion'; +export * from './Primitive/CollapsiblePanel'; diff --git a/packages/design-system/src/components/Badge/Badge.test.tsx b/packages/design-system/src/components/Badge/Badge.test.tsx new file mode 100644 index 00000000000..ba54febe539 --- /dev/null +++ b/packages/design-system/src/components/Badge/Badge.test.tsx @@ -0,0 +1,52 @@ +import { describe, it, expect } from '@jest/globals'; +import { axe } from 'jest-axe'; +import { render } from '@testing-library/react'; +import { Badge, BadgeDropdown } from './'; + +jest.mock('@talend/utils', () => { + let i = 0; + return { + // we need stable but different uuid (is fixed to 42 by current mock) + randomUUID: () => `mocked-uuid-${i++}`, + }; +}); + +describe('Badge', () => { + it('should render a11y html', async () => { + const selectedValue = '3'; + const setSelectedValue = jest.fn(); + const { container } = render( +
+
+ +
+
+ +
+ TODO: add popover and tag +
, + ); + expect(container.firstChild).toMatchSnapshot(); + const results = await axe(document.body); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/packages/design-system/src/components/Badge/__snapshots__/Badge.test.tsx.snap b/packages/design-system/src/components/Badge/__snapshots__/Badge.test.tsx.snap new file mode 100644 index 00000000000..764475d9b55 --- /dev/null +++ b/packages/design-system/src/components/Badge/__snapshots__/Badge.test.tsx.snap @@ -0,0 +1,273 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Badge should render a11y html 1`] = ` +
+
+ +
+ + Awesome + + +
+
+ + +
+
+
+
+ +
+ + Awesome + + +
+
+ + +
+
+
+ TODO: add popover and tag +
+`; diff --git a/packages/design-system/src/components/Badge/button/BadgeButton.tsx b/packages/design-system/src/components/Badge/button/BadgeButton.tsx index b0d9b75c518..b8779591997 100644 --- a/packages/design-system/src/components/Badge/button/BadgeButton.tsx +++ b/packages/design-system/src/components/Badge/button/BadgeButton.tsx @@ -2,8 +2,8 @@ import { forwardRef, Ref } from 'react'; import classnames from 'classnames'; import styles from './BadgeButton.module.scss'; -import Clickable from '../../Clickable'; import { DataAttributes } from 'src/types'; +import { Clickable } from '../../Clickable/Clickable'; type BadgeButtonProps = { /** diff --git a/packages/design-system/src/components/Badge/primitive/BadgePrimitive.tsx b/packages/design-system/src/components/Badge/primitive/BadgePrimitive.tsx index 0bcbec4e92d..97a5d3c0586 100644 --- a/packages/design-system/src/components/Badge/primitive/BadgePrimitive.tsx +++ b/packages/design-system/src/components/Badge/primitive/BadgePrimitive.tsx @@ -1,6 +1,6 @@ import { Children, PropsWithChildren, Ref } from 'react'; -import Divider from '../../Divider'; +import { Divider } from '../../Divider'; import classnames from 'classnames'; import styles from './BadgePrimitive.module.scss'; diff --git a/packages/design-system/src/components/Badge/variants/BadgeDropdown.tsx b/packages/design-system/src/components/Badge/variants/BadgeDropdown.tsx index 6d3b84181f6..e419df1d6b6 100644 --- a/packages/design-system/src/components/Badge/variants/BadgeDropdown.tsx +++ b/packages/design-system/src/components/Badge/variants/BadgeDropdown.tsx @@ -2,8 +2,7 @@ import { forwardRef, Ref } from 'react'; import { useTranslation } from 'react-i18next'; import { I18N_DOMAIN_DESIGN_SYSTEM } from '../../constants'; -import Dropdown from '../../Dropdown'; -import { DropdownItemType } from '../../Dropdown/Dropdown'; +import { Dropdown, DropdownItemType } from '../../Dropdown'; import { SizedIcon } from '../../Icon'; import { StackHorizontal } from '../../Stack'; diff --git a/packages/design-system/src/components/Badge/variants/BadgePopover.tsx b/packages/design-system/src/components/Badge/variants/BadgePopover.tsx index 77b76da3c1e..e3a15f028e5 100644 --- a/packages/design-system/src/components/Badge/variants/BadgePopover.tsx +++ b/packages/design-system/src/components/Badge/variants/BadgePopover.tsx @@ -1,6 +1,6 @@ import { Fragment, forwardRef, Ref } from 'react'; -import Divider from '../../Divider'; +import { Divider } from '../../Divider'; import { StackHorizontal } from '../../Stack'; import BadgeButton from '../button/BadgeButton'; import BadgePrimitive, { BadgePopoverItem, BadgePrimitiveProps } from '../primitive/BadgePrimitive'; diff --git a/packages/design-system/src/components/Badge/variants/BadgeValue.tsx b/packages/design-system/src/components/Badge/variants/BadgeValue.tsx index 8f28a2331b6..265d8ce0e48 100644 --- a/packages/design-system/src/components/Badge/variants/BadgeValue.tsx +++ b/packages/design-system/src/components/Badge/variants/BadgeValue.tsx @@ -4,7 +4,7 @@ import BadgePrimitive, { BadgePrimitiveProps } from '../primitive/BadgePrimitive import classnames from 'classnames'; import styles from './BadgeValue.module.scss'; import { StackHorizontal } from '../../Stack'; -import Divider from '../../Divider'; +import { Divider } from '../../Divider'; export type BadgeValueProps = BadgePrimitiveProps & { /** diff --git a/packages/design-system/src/components/Breadcrumbs/Breadcrumbs.test.tsx b/packages/design-system/src/components/Breadcrumbs/Breadcrumbs.test.tsx new file mode 100644 index 00000000000..65d8c217c40 --- /dev/null +++ b/packages/design-system/src/components/Breadcrumbs/Breadcrumbs.test.tsx @@ -0,0 +1,54 @@ +import { describe, it, expect } from '@jest/globals'; +import { axe } from 'jest-axe'; +import { render } from '@testing-library/react'; +import { Breadcrumbs } from './'; + +jest.mock('@talend/utils', () => { + let i = 0; + return { + // we need stable but different uuid (is fixed to 42 by current mock) + randomUUID: () => `mocked-uuid-${i++}`, + }; +}); + +describe('Breadcrumbs', () => { + it('should render a11y html', async () => { + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot(); + const results = await axe(document.body); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/packages/design-system/src/components/Breadcrumbs/Breadcrumbs.tsx b/packages/design-system/src/components/Breadcrumbs/Breadcrumbs.tsx index a445262e034..1328d20528a 100644 --- a/packages/design-system/src/components/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/design-system/src/components/Breadcrumbs/Breadcrumbs.tsx @@ -3,12 +3,12 @@ import { useTranslation } from 'react-i18next'; import classnames from 'classnames'; import styles from './Breadcrumbs.module.scss'; -import Link from '../Link'; -import Dropdown from '../Dropdown/Dropdown'; +import { Link } from '../Link'; +import { Dropdown } from '../Dropdown/Dropdown'; import { ButtonTertiary } from '../Button'; import { StackHorizontal } from '../Stack'; -import Divider from '../Divider'; -import VisuallyHidden from '../VisuallyHidden'; +import { Divider } from '../Divider'; +import { VisuallyHidden } from '../VisuallyHidden'; import { I18N_DOMAIN_DESIGN_SYSTEM } from '../constants'; type BreadcrumbsLink = { @@ -25,7 +25,7 @@ type BreadcrumbsRouterLink = { type BreadcrumbsItem = (BreadcrumbsRouterLink | BreadcrumbsLink)[]; -type BreadCrumbsProps = Omit, 'className' | 'style'> & { +export type BreadCrumbsProps = Omit, 'className' | 'style'> & { items: BreadcrumbsItem; }; @@ -61,82 +61,84 @@ function BreadcrumbLink({ ); } -const Breadcrumbs = forwardRef(({ items, ...rest }: BreadCrumbsProps, ref: Ref) => { - const { t } = useTranslation(I18N_DOMAIN_DESIGN_SYSTEM); - const buildEntries = () => { - if (items.length > maxBreadcrumbsItemLength) { - const origin = items[0]; - const suffix = items.slice(-2); - const collapsed = items.slice(1, items.length - 2); +export const Breadcrumbs = forwardRef( + ({ items, ...rest }: BreadCrumbsProps, ref: Ref) => { + const { t } = useTranslation(I18N_DOMAIN_DESIGN_SYSTEM); + const buildEntries = () => { + if (items.length > maxBreadcrumbsItemLength) { + const origin = items[0]; + const suffix = items.slice(-2); + const collapsed = items.slice(1, items.length - 2); - return ( - <> - + return ( + <> + -
  • - - { - const refinedProp = - 'href' in collapsedLinks - ? { href: collapsedLinks.href } - : { as: collapsedLinks.as }; - return { - label: collapsedLinks.label, - target: collapsedLinks.target, - type: 'link', - ...refinedProp, - }; - })} - > - {}}> - - {t('COLLAPSED_LINKS_BUTTON', 'Display collapsed links')} - - … - - - - - - -
  • +
  • + + { + const refinedProp = + 'href' in collapsedLinks + ? { href: collapsedLinks.href } + : { as: collapsedLinks.as }; + return { + label: collapsedLinks.label, + target: collapsedLinks.target, + type: 'link', + ...refinedProp, + }; + })} + > + {}}> + + {t('COLLAPSED_LINKS_BUTTON', 'Display collapsed links')} + + … + + + + + + +
  • - {suffix.map((entry, index) => { - const isLastEntry = index === suffix.length - 1; - return ( - - ); - })} - - ); - } + {suffix.map((entry, index) => { + const isLastEntry = index === suffix.length - 1; + return ( + + ); + })} + + ); + } - return items.map((entry, index) => { - const isLastEntry = index === items.length - 1; - return ( - - ); - }); - }; + return items.map((entry, index) => { + const isLastEntry = index === items.length - 1; + return ( + + ); + }); + }; - return ( - - ); -}); + return ( + + ); + }, +); -export default Breadcrumbs; +Breadcrumbs.displayName = 'Breadcrumbs'; diff --git a/packages/design-system/src/components/Breadcrumbs/__snapshots__/Breadcrumbs.test.tsx.snap b/packages/design-system/src/components/Breadcrumbs/__snapshots__/Breadcrumbs.test.tsx.snap new file mode 100644 index 00000000000..e52fe526530 --- /dev/null +++ b/packages/design-system/src/components/Breadcrumbs/__snapshots__/Breadcrumbs.test.tsx.snap @@ -0,0 +1,195 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Breadcrumbs should render a11y html 1`] = ` +
    + +
    +`; diff --git a/packages/design-system/src/components/Breadcrumbs/index.ts b/packages/design-system/src/components/Breadcrumbs/index.ts new file mode 100644 index 00000000000..ce977548b14 --- /dev/null +++ b/packages/design-system/src/components/Breadcrumbs/index.ts @@ -0,0 +1 @@ +export * from './Breadcrumbs'; diff --git a/packages/design-system/src/components/Button/Button.cy.tsx b/packages/design-system/src/components/Button/Button.cy.tsx index f596d2996d4..28e77422666 100644 --- a/packages/design-system/src/components/Button/Button.cy.tsx +++ b/packages/design-system/src/components/Button/Button.cy.tsx @@ -5,7 +5,7 @@ import { useState } from 'react'; import ButtonPrimitive from './Primitive/ButtonPrimitive'; import { ButtonPrimary } from './'; -import Tooltip from '../../components/Tooltip'; +import { Tooltip } from '../../components/Tooltip'; const Loading = ({ 'data-testid': dataTestId }: { 'data-testid': string }) => { // eslint-disable-next-line react-hooks/rules-of-hooks @@ -55,21 +55,20 @@ context(' + + + + + + , + ); + expect(container.firstChild).toMatchSnapshot(); + const results = await axe(document.body); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/packages/design-system/src/components/Button/Button.tsx b/packages/design-system/src/components/Button/Button.tsx index e43bc7aee53..73a3cce3e13 100644 --- a/packages/design-system/src/components/Button/Button.tsx +++ b/packages/design-system/src/components/Button/Button.tsx @@ -1,6 +1,6 @@ import { forwardRef, Ref } from 'react'; -import ButtonPrimary, { ButtonPrimaryPropsType } from './variations/ButtonPrimary'; +import { ButtonPrimary, ButtonPrimaryPropsType } from './variations/ButtonPrimary'; import ButtonSecondary, { ButtonSecondaryPropsType } from './variations/ButtonSecondary'; import ButtonTertiary, { ButtonTertiaryPropsType } from './variations/ButtonTertiary'; import ButtonDestructive, { ButtonDestructivePropsType } from './variations/ButtonDestructive'; @@ -54,8 +54,6 @@ function ButtonPlatform( } } -const Button = forwardRef(ButtonPlatform) as ( +export const Button = forwardRef(ButtonPlatform) as ( props: ButtonType & { ref?: Ref }, ) => ReturnType; - -export default Button; diff --git a/packages/design-system/src/components/Button/Primitive/ButtonPrimitive.tsx b/packages/design-system/src/components/Button/Primitive/ButtonPrimitive.tsx index 8c93d9a65b7..8781866ee89 100644 --- a/packages/design-system/src/components/Button/Primitive/ButtonPrimitive.tsx +++ b/packages/design-system/src/components/Button/Primitive/ButtonPrimitive.tsx @@ -2,15 +2,16 @@ import { forwardRef, ReactElement, Ref } from 'react'; // eslint-disable-next-line @talend/import-depth import { IconNameWithSize } from '@talend/icons/dist/typeUtils'; import classnames from 'classnames'; -import Clickable, { ClickableProps } from '../../Clickable'; +import { ClickableProps } from '../../Clickable'; import { DataAttributes, DeprecatedIconNames } from '../../../types'; import { StackHorizontal } from '../../Stack'; -import Loading from '../../Loading'; +import { Loading } from '../../Loading'; import { getIconWithDeprecatedSupport } from '../../Icon/DeprecatedIconHelper'; import styles from './ButtonStyles.module.scss'; import { SizedIcon } from '../../Icon'; +import { Clickable } from '../../Clickable/Clickable'; export type AvailableVariantsTypes = 'primary' | 'destructive' | 'secondary' | 'tertiary'; export type AvailableSizes = 'M' | 'S'; @@ -22,9 +23,7 @@ export type SharedButtonTypes = { isLoading?: boolean; isDropdown?: boolean; size?: S; - icon?: S extends 'S' - ? IconNameWithSize<'S'> - : DeprecatedIconNames | ReactElement | IconNameWithSize<'M'>; + icon?: IconNameWithSize<'S'> | DeprecatedIconNames | ReactElement | IconNameWithSize<'M'>; }; export type BaseButtonProps = Omit & @@ -33,7 +32,7 @@ export type BaseButtonProps = Omit( props: BaseButtonProps, - ref?: Ref, + ref: Ref, ) { const { className, @@ -45,11 +44,12 @@ function ButtonPrimitiveInner( isDropdown = false, ...rest } = props; + const cls = { + [styles['size-S']]: size === 'S', + }; return ( ( )} {!isLoading && icon && ( - {getIconWithDeprecatedSupport({ iconSrc: icon, size: size === 'S' ? 'S' : 'M' })} + {getIconWithDeprecatedSupport({ iconSrc: icon, size: size || 'M' })} )} {children} diff --git a/packages/design-system/src/components/Button/__snapshots__/Button.test.tsx.snap b/packages/design-system/src/components/Button/__snapshots__/Button.test.tsx.snap new file mode 100644 index 00000000000..b9d45e891cf --- /dev/null +++ b/packages/design-system/src/components/Button/__snapshots__/Button.test.tsx.snap @@ -0,0 +1,132 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Button should render a11y html 1`] = ` +
    + + + + + + +
    +`; diff --git a/packages/design-system/src/components/Button/index.ts b/packages/design-system/src/components/Button/index.ts index 3fd70bef5f7..030cc6dd7c1 100644 --- a/packages/design-system/src/components/Button/index.ts +++ b/packages/design-system/src/components/Button/index.ts @@ -1,8 +1,8 @@ -import ButtonPrimary from './variations/ButtonPrimary'; +import { ButtonPrimary } from './variations/ButtonPrimary'; import ButtonSecondary from './variations/ButtonSecondary'; import ButtonTertiary from './variations/ButtonTertiary'; import ButtonDestructive from './variations/ButtonDestructive'; -import Button from './Button'; +import { Button } from './Button'; import { AvailableSizes, BaseButtonProps } from './Primitive/ButtonPrimitive'; diff --git a/packages/design-system/src/components/Button/variations/ButtonDestructive.tsx b/packages/design-system/src/components/Button/variations/ButtonDestructive.tsx index 366b8cdd187..aa06b878985 100644 --- a/packages/design-system/src/components/Button/variations/ButtonDestructive.tsx +++ b/packages/design-system/src/components/Button/variations/ButtonDestructive.tsx @@ -5,10 +5,8 @@ import styles from './ButtonDestructive.module.scss'; export type ButtonDestructivePropsType = Omit< BaseButtonProps, - 'className' | 'size' -> & { - size?: S; -}; + 'className' +>; function Destructive( props: ButtonDestructivePropsType, diff --git a/packages/design-system/src/components/Button/variations/ButtonPrimary.tsx b/packages/design-system/src/components/Button/variations/ButtonPrimary.tsx index 5488c61273b..f842f596661 100644 --- a/packages/design-system/src/components/Button/variations/ButtonPrimary.tsx +++ b/packages/design-system/src/components/Button/variations/ButtonPrimary.tsx @@ -5,10 +5,8 @@ import styles from './ButtonPrimary.module.scss'; export type ButtonPrimaryPropsType = Omit< BaseButtonProps, - 'className' | 'size' -> & { - size?: S; -}; + 'className' +>; function Primary( props: ButtonPrimaryPropsType, @@ -17,8 +15,6 @@ function Primary( return ; } -const ButtonPrimary = forwardRef(Primary) as ( +export const ButtonPrimary = forwardRef(Primary) as ( props: ButtonPrimaryPropsType & { ref?: Ref }, ) => ReturnType; - -export default ButtonPrimary; diff --git a/packages/design-system/src/components/Button/variations/ButtonSecondary.tsx b/packages/design-system/src/components/Button/variations/ButtonSecondary.tsx index e8bed12f18e..c27a1555ba1 100644 --- a/packages/design-system/src/components/Button/variations/ButtonSecondary.tsx +++ b/packages/design-system/src/components/Button/variations/ButtonSecondary.tsx @@ -6,10 +6,8 @@ import { ButtonDestructivePropsType } from './ButtonDestructive'; export type ButtonSecondaryPropsType = Omit< BaseButtonProps, - 'className' | 'size' -> & { - size?: S; -}; + 'className' +>; function Secondary( props: ButtonSecondaryPropsType, diff --git a/packages/design-system/src/components/Button/variations/ButtonTertiary.tsx b/packages/design-system/src/components/Button/variations/ButtonTertiary.tsx index ad960232b2e..b473653caaf 100644 --- a/packages/design-system/src/components/Button/variations/ButtonTertiary.tsx +++ b/packages/design-system/src/components/Button/variations/ButtonTertiary.tsx @@ -6,10 +6,8 @@ import { ButtonDestructivePropsType } from './ButtonDestructive'; export type ButtonTertiaryPropsType = Omit< BaseButtonProps, - 'className' | 'size' -> & { - size?: S; -}; + 'className' +>; function Tertiary( props: ButtonTertiaryPropsType, diff --git a/packages/design-system/src/components/ButtonAsLink/ButtonAsLink.test.tsx b/packages/design-system/src/components/ButtonAsLink/ButtonAsLink.test.tsx new file mode 100644 index 00000000000..b9e9a89349e --- /dev/null +++ b/packages/design-system/src/components/ButtonAsLink/ButtonAsLink.test.tsx @@ -0,0 +1,34 @@ +import { describe, it, expect } from '@jest/globals'; +import { axe } from 'jest-axe'; +import { render } from '@testing-library/react'; +import { ButtonAsLink } from './'; + +describe('ButtonAsLink', () => { + it('should render a11y html', async () => { + const { container } = render( +
    + + Primary + + + Destructive + + + Secondary + + + Tertiary submit + + + Tertiary + + + Tertiary + +
    , + ); + expect(container.firstChild).toMatchSnapshot(); + const results = await axe(document.body); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/packages/design-system/src/components/ButtonAsLink/ButtonAsLink.tsx b/packages/design-system/src/components/ButtonAsLink/ButtonAsLink.tsx index e6445c965b3..addf27ac47a 100644 --- a/packages/design-system/src/components/ButtonAsLink/ButtonAsLink.tsx +++ b/packages/design-system/src/components/ButtonAsLink/ButtonAsLink.tsx @@ -65,8 +65,6 @@ function ButtonPlatform(props: ButtonType, ref: Ref } } -const ButtonAsLink = forwardRef(ButtonPlatform) as ( +export const ButtonAsLink = forwardRef(ButtonPlatform) as ( props: ButtonType & { ref?: Ref }, ) => ReturnType; - -export default ButtonAsLink; diff --git a/packages/design-system/src/components/ButtonAsLink/Primitive/ButtonPrimitiveAsLink.tsx b/packages/design-system/src/components/ButtonAsLink/Primitive/ButtonPrimitiveAsLink.tsx index e9aa471706e..ed18b9b21d8 100644 --- a/packages/design-system/src/components/ButtonAsLink/Primitive/ButtonPrimitiveAsLink.tsx +++ b/packages/design-system/src/components/ButtonAsLink/Primitive/ButtonPrimitiveAsLink.tsx @@ -1,6 +1,6 @@ import { forwardRef, Ref } from 'react'; import classnames from 'classnames'; -import Linkable, { LinkableType } from '../../Linkable'; +import { Linkable, LinkableType } from '../../Linkable'; import { StackHorizontal } from '../../Stack'; diff --git a/packages/design-system/src/components/ButtonAsLink/__snapshots__/ButtonAsLink.test.tsx.snap b/packages/design-system/src/components/ButtonAsLink/__snapshots__/ButtonAsLink.test.tsx.snap new file mode 100644 index 00000000000..ff0c2f7bf25 --- /dev/null +++ b/packages/design-system/src/components/ButtonAsLink/__snapshots__/ButtonAsLink.test.tsx.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ButtonAsLink should render a11y html 1`] = ` +
    + + + Primary + + + + + + + + Destructive + + + + + Secondary + + + + + Tertiary submit + + + + + Tertiary + + + + + Tertiary + + +
    +`; diff --git a/packages/design-system/src/components/ButtonAsLink/index.ts b/packages/design-system/src/components/ButtonAsLink/index.ts index 567ee15986f..42c7a3b6cf2 100644 --- a/packages/design-system/src/components/ButtonAsLink/index.ts +++ b/packages/design-system/src/components/ButtonAsLink/index.ts @@ -2,10 +2,9 @@ import ButtonPrimaryAsLink from './variations/ButtonPrimaryAsLink'; import ButtonSecondaryAsLink from './variations/ButtonSecondaryAsLink'; import ButtonTertiaryAsLink from './variations/ButtonTertiaryAsLink'; import ButtonDestructiveAsLink from './variations/ButtonDestructiveAsLink'; -import ButtonAsLink from './ButtonAsLink'; +export * from './ButtonAsLink'; export { - ButtonAsLink, ButtonPrimaryAsLink, ButtonSecondaryAsLink, ButtonTertiaryAsLink, diff --git a/packages/design-system/src/components/ButtonAsLink/variations/ButtonDestructiveAsLink.tsx b/packages/design-system/src/components/ButtonAsLink/variations/ButtonDestructiveAsLink.tsx index 493cc0e1551..b634bb30536 100644 --- a/packages/design-system/src/components/ButtonAsLink/variations/ButtonDestructiveAsLink.tsx +++ b/packages/design-system/src/components/ButtonAsLink/variations/ButtonDestructiveAsLink.tsx @@ -17,7 +17,7 @@ function DestructiveAsLink( } const ButtonDestructiveAsLink = forwardRef(DestructiveAsLink) as ( - props: ButtonDestructiveAsLinkPropsType & { ref?: Ref }, + props: ButtonDestructiveAsLinkPropsType & { ref: Ref }, ) => ReturnType; export default ButtonDestructiveAsLink; diff --git a/packages/design-system/src/components/ButtonIcon/ButtonIcon.test.tsx b/packages/design-system/src/components/ButtonIcon/ButtonIcon.test.tsx new file mode 100644 index 00000000000..4dde3253ff2 --- /dev/null +++ b/packages/design-system/src/components/ButtonIcon/ButtonIcon.test.tsx @@ -0,0 +1,20 @@ +import { describe, it, expect } from '@jest/globals'; +import { axe } from 'jest-axe'; +import { render } from '@testing-library/react'; +import { ButtonIcon } from './'; + +describe('ButtonIcon', () => { + it('should render accessible html', async () => { + // note we need to add the aria-label to be accessible + // TODO: make it required + const { container } = render( + {}}> + children is considered as description + , + ); + expect(container.firstChild).toMatchSnapshot(); + const results = await axe(document.body); + expect(results).toHaveNoViolations(); + expect(container.firstChild).toHaveAttribute('aria-describedby', 'id-42'); + }); +}); diff --git a/packages/design-system/src/components/ButtonIcon/Primitive/ButtonIconPrimitive.tsx b/packages/design-system/src/components/ButtonIcon/Primitive/ButtonIconPrimitive.tsx index bfaac7ec1b4..3f815731b0d 100644 --- a/packages/design-system/src/components/ButtonIcon/Primitive/ButtonIconPrimitive.tsx +++ b/packages/design-system/src/components/ButtonIcon/Primitive/ButtonIconPrimitive.tsx @@ -2,14 +2,16 @@ import { forwardRef } from 'react'; import type { MouseEvent, ReactElement, ButtonHTMLAttributes, Ref } from 'react'; import classnames from 'classnames'; + // eslint-disable-next-line @talend/import-depth import { IconNameWithSize } from '@talend/icons/dist/typeUtils'; +import { mergeRefs } from '../../../mergeRef'; import { DeprecatedIconNames } from '../../../types'; -import Button from '../../Clickable'; -import Tooltip, { TooltipPlacement } from '../../Tooltip'; -import Loading from '../../Loading'; +import { Clickable } from '../../Clickable'; import { getIconWithDeprecatedSupport } from '../../Icon/DeprecatedIconHelper'; +import { Loading } from '../../Loading'; +import { Tooltip, TooltipPlacement } from '../../Tooltip'; import styles from './ButtonIcon.module.scss'; @@ -18,7 +20,7 @@ export type PossibleVariants = 'toggle' | 'floating' | 'default'; type CommonTypes> = Omit< ButtonHTMLAttributes, - 'className' | 'style' + 'className' | 'style' | 'aria-label' > & { children: string; isLoading?: boolean; @@ -35,10 +37,12 @@ export type ToggleTypes> = CommonTypes & { export type FloatingTypes> = CommonTypes & { variant: 'floating'; + isActive?: never; }; export type DefaultTypes = CommonTypes & { variant: 'default'; + isActive?: never; }; export type ButtonIconProps = @@ -58,30 +62,36 @@ function Primitive( icon, disabled, tooltipPlacement, + isActive = false, ...rest } = props; - const activeButtonIconPrimitive = props.variant === 'toggle' ? props.isActive : false; + const activeButtonIconPrimitive = props.variant === 'toggle' ? isActive : false; return ( - + {(triggerProps, triggerRef) => ( + + + {!isLoading && + getIconWithDeprecatedSupport({ iconSrc: icon, size: size === 'XS' ? 'S' : 'M' })} + {isLoading && } + + + )} ); } diff --git a/packages/design-system/src/components/ButtonIcon/__snapshots__/ButtonIcon.test.tsx.snap b/packages/design-system/src/components/ButtonIcon/__snapshots__/ButtonIcon.test.tsx.snap new file mode 100644 index 00000000000..1b5824609da --- /dev/null +++ b/packages/design-system/src/components/ButtonIcon/__snapshots__/ButtonIcon.test.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ButtonIcon should render accessible html 1`] = ` + +`; diff --git a/packages/design-system/src/components/ButtonIcon/index.ts b/packages/design-system/src/components/ButtonIcon/index.ts index 1efae09741a..e9296793e6b 100644 --- a/packages/design-system/src/components/ButtonIcon/index.ts +++ b/packages/design-system/src/components/ButtonIcon/index.ts @@ -1,4 +1,4 @@ -import ButtonIcon from './variations/ButtonIcon'; +import { ButtonIcon } from './variations/ButtonIcon'; import ButtonIconToggle from './variations/ButtonIconToggle'; import ButtonIconFloating from './variations/ButtonIconFloating'; diff --git a/packages/design-system/src/components/ButtonIcon/variations/ButtonIcon.tsx b/packages/design-system/src/components/ButtonIcon/variations/ButtonIcon.tsx index 56b4069e6ba..c8a83ad2ade 100644 --- a/packages/design-system/src/components/ButtonIcon/variations/ButtonIcon.tsx +++ b/packages/design-system/src/components/ButtonIcon/variations/ButtonIcon.tsx @@ -10,8 +10,6 @@ function Button(props: ButtonIconType, ref: Ref; } -const ButtonIcon = forwardRef(Button) as ( +export const ButtonIcon = forwardRef(Button) as ( props: ButtonIconType & { ref?: Ref }, ) => ReturnType; - -export default ButtonIcon; diff --git a/packages/design-system/src/components/WIP/Card/Card.module.scss b/packages/design-system/src/components/Card/Card.module.scss similarity index 100% rename from packages/design-system/src/components/WIP/Card/Card.module.scss rename to packages/design-system/src/components/Card/Card.module.scss diff --git a/packages/design-system/src/components/Card/Card.test.tsx b/packages/design-system/src/components/Card/Card.test.tsx new file mode 100644 index 00000000000..bc57648b6d1 --- /dev/null +++ b/packages/design-system/src/components/Card/Card.test.tsx @@ -0,0 +1,19 @@ +import { describe, it, expect } from '@jest/globals'; +import { axe } from 'jest-axe'; +import { render } from '@testing-library/react'; +import { Card } from './'; + +describe('Card', () => { + it('should render a11y html', async () => { + const { container } = render( +
    + +

    Content

    +
    +
    , + ); + expect(container.firstChild).toMatchSnapshot(); + const results = await axe(document.body); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/packages/design-system/src/components/WIP/Card/Card.tsx b/packages/design-system/src/components/Card/Card.tsx similarity index 71% rename from packages/design-system/src/components/WIP/Card/Card.tsx rename to packages/design-system/src/components/Card/Card.tsx index f78038563cf..041fcf6d9a3 100644 --- a/packages/design-system/src/components/WIP/Card/Card.tsx +++ b/packages/design-system/src/components/Card/Card.tsx @@ -1,15 +1,15 @@ import type { ReactNode } from 'react'; -import { StackVertical } from '../../Stack'; +import { StackVertical } from '../Stack'; import theme from './Card.module.scss'; -interface CardPropsType { +export type CardPropsType = { header?: ReactNode; children: ReactNode; -} +}; -function Card({ header, children }: CardPropsType) { +export function Card({ header, children }: CardPropsType) { return (
    @@ -19,5 +19,3 @@ function Card({ header, children }: CardPropsType) {
    ); } - -export default Card; diff --git a/packages/design-system/src/components/Card/__snapshots__/Card.test.tsx.snap b/packages/design-system/src/components/Card/__snapshots__/Card.test.tsx.snap new file mode 100644 index 00000000000..74f84a4c604 --- /dev/null +++ b/packages/design-system/src/components/Card/__snapshots__/Card.test.tsx.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Card should render a11y html 1`] = ` +
    +
    +
    +
    + header +
    +
    +

    + Content +

    +
    +
    +
    +
    +`; diff --git a/packages/design-system/src/components/Card/index.ts b/packages/design-system/src/components/Card/index.ts new file mode 100644 index 00000000000..ca0b060473a --- /dev/null +++ b/packages/design-system/src/components/Card/index.ts @@ -0,0 +1 @@ +export * from './Card'; diff --git a/packages/design-system/src/components/Clickable/Clickable.tsx b/packages/design-system/src/components/Clickable/Clickable.tsx index 0f03ce8dee4..64ee5630fb6 100644 --- a/packages/design-system/src/components/Clickable/Clickable.tsx +++ b/packages/design-system/src/components/Clickable/Clickable.tsx @@ -1,23 +1,22 @@ -import { forwardRef, MouseEvent, ReactNode, Ref } from 'react'; +import { forwardRef, MouseEvent, ReactNode, Ref, HTMLProps } from 'react'; import classnames from 'classnames'; -import { - Clickable as ReakitClickable, - ClickableProps as ReakitClickableProps, -} from 'reakit/Clickable'; import styles from './Clickable.module.scss'; -export type ClickableProps = Omit & { +export type ClickableProps = Omit< + HTMLProps, + 'size' | 'ref' | 'wrap' | 'loop' +> & { type?: 'button' | 'submit' | 'reset'; - children: ReactNode | ReactNode[]; + children: string | ReactNode | ReactNode[]; onClick?: (event: MouseEvent | KeyboardEvent) => void; }; -const Clickable = forwardRef( +export const Clickable = forwardRef( // Ref: Clickable is polymorphic. Could be any HTML element ({ type = 'button', className, ...props }: ClickableProps, ref: Ref) => { return ( - { + it('should render a11y html', async () => { + const { container } = render( +
    + +

    Content

    +
    +
    , + ); + expect(container.firstChild).toMatchSnapshot(); + const results = await axe(document.body); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/packages/design-system/src/components/Clickable/__snapshots__/Clikable.test.tsx.snap b/packages/design-system/src/components/Clickable/__snapshots__/Clikable.test.tsx.snap new file mode 100644 index 00000000000..267fd8f5839 --- /dev/null +++ b/packages/design-system/src/components/Clickable/__snapshots__/Clikable.test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Clickable should render a11y html 1`] = ` +
    + +
    +`; diff --git a/packages/design-system/src/components/Clickable/index.ts b/packages/design-system/src/components/Clickable/index.ts index c5458624f5b..cc29524ac92 100644 --- a/packages/design-system/src/components/Clickable/index.ts +++ b/packages/design-system/src/components/Clickable/index.ts @@ -1,4 +1 @@ -import Clickable, { ClickableProps } from './Clickable'; - -export type { ClickableProps }; -export default Clickable; +export * from './Clickable'; diff --git a/packages/design-system/src/components/WIP/Combobox/Combobox.module.scss b/packages/design-system/src/components/Combobox/Combobox.module.scss similarity index 91% rename from packages/design-system/src/components/WIP/Combobox/Combobox.module.scss rename to packages/design-system/src/components/Combobox/Combobox.module.scss index 7226052ce91..bab40e2e9ff 100644 --- a/packages/design-system/src/components/WIP/Combobox/Combobox.module.scss +++ b/packages/design-system/src/components/Combobox/Combobox.module.scss @@ -1,5 +1,5 @@ @use '~@talend/design-tokens/lib/tokens'; -@use '../../Form/Primitives/mixins' as Form; +@use '../Form/Primitives/mixins' as Form; .combobox { &__input { @@ -9,6 +9,7 @@ } [role='listbox'] { + margin-top: tokens.$coral-spacing-xs; background: tokens.$coral-color-neutral-background; border-radius: tokens.$coral-radius-s; box-shadow: tokens.$coral-elevation-shadow-neutral-m; diff --git a/packages/design-system/src/components/Combobox/Combobox.test.tsx b/packages/design-system/src/components/Combobox/Combobox.test.tsx new file mode 100644 index 00000000000..273da64d98a --- /dev/null +++ b/packages/design-system/src/components/Combobox/Combobox.test.tsx @@ -0,0 +1,27 @@ +import { describe, it, expect } from '@jest/globals'; +import { axe } from 'jest-axe'; +import { render } from '@testing-library/react'; +import { Combobox } from './'; + +jest.mock('@talend/utils', () => { + let i = 0; + return { + // we need stable but different uuid (is fixed to 42 by current mock) + randomUUID: () => `mocked-uuid-${i++}`, + }; +}); + +const fruits = ['Acerola', 'Apple', 'Apricots', 'Avocado']; + +describe('Combobox', () => { + it('should render a11y html', async () => { + const { container } = render( +
    + +
    , + ); + expect(container.firstChild).toMatchSnapshot(); + const results = await axe(document.body); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/packages/design-system/src/components/Combobox/Combobox.tsx b/packages/design-system/src/components/Combobox/Combobox.tsx new file mode 100644 index 00000000000..9e5cc23e516 --- /dev/null +++ b/packages/design-system/src/components/Combobox/Combobox.tsx @@ -0,0 +1,104 @@ +import { useState, useCallback, useEffect, useRef, FocusEvent } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useId } from '../../useId'; +import { I18N_DOMAIN_DESIGN_SYSTEM } from '../constants'; + +import styles from './Combobox.module.scss'; + +export type ComboboxProps = { + id?: string; + values?: string[]; + initialValue?: string; +}; + +export const Combobox = ({ values, ...rest }: ComboboxProps) => { + const { t } = useTranslation(I18N_DOMAIN_DESIGN_SYSTEM); + const [show, setShow] = useState(false); + const [options, setOptions] = useState(values || []); + const [value, setValue] = useState(rest.initialValue || ''); + const id = useId(rest.id); + const boxId = useId(); + const noValue = t('COMBOBOX_NOT_RESULT', 'No results found'); + const onKeydown = useCallback(e => { + if (e.key === 'Escape') { + setShow(false); + } + }, []); + + const comboboxRef = useRef(null); + + // sync options with values and value + useEffect(() => { + if (value) { + setOptions( + (values || []).filter(option => option.toLowerCase().includes(value.toLowerCase())), + ); + } else { + setOptions(values || []); + } + }, [value, values]); + + const onFocusOut = (event: FocusEvent) => { + // Check if where user clicks out is part of combobox + let relatedTarget = event.relatedTarget; + while (relatedTarget !== null && relatedTarget !== comboboxRef.current) { + relatedTarget = relatedTarget.parentElement; + } + + // If relatedTarget is null, it means we reach top level component without finding combobox div + // So we are sure user doesn't click in part of combobox + if (relatedTarget === null) { + setShow(false); + } + }; + + return ( +
    + setShow(true)} + onClick={() => { + if (!show) setShow(true); + }} + aria-controls={boxId} + value={value} + placeholder={t('COMBOBOX_PLACEHOLDER', 'Search')} + onChange={e => setValue(e.target.value)} + /> + +
    + ); +}; diff --git a/packages/design-system/src/components/Combobox/__snapshots__/Combobox.test.tsx.snap b/packages/design-system/src/components/Combobox/__snapshots__/Combobox.test.tsx.snap new file mode 100644 index 00000000000..6bbb872a4af --- /dev/null +++ b/packages/design-system/src/components/Combobox/__snapshots__/Combobox.test.tsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Combobox should render a11y html 1`] = ` +
    +
    + + +
    +
    +`; diff --git a/packages/design-system/src/components/Combobox/index.ts b/packages/design-system/src/components/Combobox/index.ts new file mode 100644 index 00000000000..814b172cb41 --- /dev/null +++ b/packages/design-system/src/components/Combobox/index.ts @@ -0,0 +1 @@ +export * from './Combobox'; diff --git a/packages/design-system/src/components/Disclosure/Disclosure.tsx b/packages/design-system/src/components/Disclosure/Disclosure.tsx new file mode 100644 index 00000000000..280362b18b5 --- /dev/null +++ b/packages/design-system/src/components/Disclosure/Disclosure.tsx @@ -0,0 +1,24 @@ +import type { Ref } from 'react'; + +import { ChildOrGenerator, renderOrClone } from '../../renderOrClone'; + +export type DisclosureProps = { + children: ChildOrGenerator; + popref?: Ref; +}; + +type DisclosureButtonProps = { + ref?: Ref; +}; + +export type DisclosureFnProps = { + ref?: Ref; +}; + +export function Disclosure({ children, popref, ...rest }: DisclosureProps) { + const props: DisclosureButtonProps = { + ref: popref, + }; + + return renderOrClone(children, { ...props, ...rest }) || null; +} diff --git a/packages/design-system/src/components/Disclosure/DisclosureContext.ts b/packages/design-system/src/components/Disclosure/DisclosureContext.ts new file mode 100644 index 00000000000..dca6bb2ccc0 --- /dev/null +++ b/packages/design-system/src/components/Disclosure/DisclosureContext.ts @@ -0,0 +1,14 @@ +import { createContext, useContext } from 'react'; + +interface DisclosureContextValue { + disclosureId?: string | number; + onSelect?: () => void; + open?: boolean; + panelId?: string; +} + +export const DisclosureContext = createContext({}); + +export function useDisclosureCtx() { + return useContext(DisclosureContext); +} diff --git a/packages/design-system/src/components/Divider/Divider.test.tsx b/packages/design-system/src/components/Divider/Divider.test.tsx new file mode 100644 index 00000000000..2762a8524f3 --- /dev/null +++ b/packages/design-system/src/components/Divider/Divider.test.tsx @@ -0,0 +1,26 @@ +import { describe, it, expect } from '@jest/globals'; +import { axe } from 'jest-axe'; +import { render } from '@testing-library/react'; +import { Divider } from './'; + +describe('Divider', () => { + it('should render a11y html', async () => { + const { container } = render( +
    +
    +

    test

    + +

    lorem ipsum

    +
    + +
    +

    Side

    +

    lorem ipsum content

    +
    +
    , + ); + expect(container.firstChild).toMatchSnapshot(); + const results = await axe(document.body); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/packages/design-system/src/components/Divider/Divider.tsx b/packages/design-system/src/components/Divider/Divider.tsx index 4aed009ab53..65ce1e5acd3 100644 --- a/packages/design-system/src/components/Divider/Divider.tsx +++ b/packages/design-system/src/components/Divider/Divider.tsx @@ -1,12 +1,22 @@ -import { forwardRef, Ref } from 'react'; -import { Separator as ReakitSeparator, SeparatorProps as ReakitSeparatorProps } from 'reakit'; +import { forwardRef, Ref, HTMLAttributes, RefAttributes } from 'react'; import style from './Divider.module.scss'; -type DividerProps = Omit; +export type DividerOptions = { + /** + * Separator's orientation. + */ + orientation?: 'horizontal' | 'vertical'; +}; -const Divider = forwardRef((props: DividerProps, ref: Ref) => { - return ; +export type DividerHTMLProps = HTMLAttributes & RefAttributes; + +export type DividerProps = DividerOptions & DividerHTMLProps; + +export const Divider = forwardRef((props: DividerProps, ref: Ref) => { + const ruleOrientation = props.orientation || 'horizontal'; + + return
    ; }); -export default Divider; +Divider.displayName = 'Divider'; diff --git a/packages/design-system/src/components/Divider/__snapshots__/Divider.test.tsx.snap b/packages/design-system/src/components/Divider/__snapshots__/Divider.test.tsx.snap new file mode 100644 index 00000000000..c8e87f167b3 --- /dev/null +++ b/packages/design-system/src/components/Divider/__snapshots__/Divider.test.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Divider should render a11y html 1`] = ` +
    +
    +

    + test +

    +
    +

    + lorem ipsum +

    +
    +
    +
    +

    + Side +

    +

    + lorem ipsum content +

    +
    +
    +`; diff --git a/packages/design-system/src/components/Divider/index.ts b/packages/design-system/src/components/Divider/index.ts index c8ee123153b..1f84888dc70 100644 --- a/packages/design-system/src/components/Divider/index.ts +++ b/packages/design-system/src/components/Divider/index.ts @@ -1,3 +1 @@ -import Divider from './Divider'; - -export default Divider; +export * from './Divider'; diff --git a/packages/design-system/src/components/Drawer/Drawer.test.tsx b/packages/design-system/src/components/Drawer/Drawer.test.tsx new file mode 100644 index 00000000000..4219cb4522c --- /dev/null +++ b/packages/design-system/src/components/Drawer/Drawer.test.tsx @@ -0,0 +1,22 @@ +import { describe, it, expect } from '@jest/globals'; +import { axe } from 'jest-axe'; +import { render } from '@testing-library/react'; +import { FloatingDrawer } from './'; + +describe('FloatingDrawer', () => { + it('should render a11y html', async () => { + const { container } = render( +
    + +

    test

    + +

    content of the drawer

    +
    +
    +
    , + ); + expect(container.firstChild).toMatchSnapshot(); + const results = await axe(document.body); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/packages/design-system/src/components/WIP/Drawer/Primitive/PrimitiveDrawer.module.scss b/packages/design-system/src/components/Drawer/Primitive/PrimitiveDrawer.module.scss similarity index 90% rename from packages/design-system/src/components/WIP/Drawer/Primitive/PrimitiveDrawer.module.scss rename to packages/design-system/src/components/Drawer/Primitive/PrimitiveDrawer.module.scss index 2916b7943fa..e18430463d6 100644 --- a/packages/design-system/src/components/WIP/Drawer/Primitive/PrimitiveDrawer.module.scss +++ b/packages/design-system/src/components/Drawer/Primitive/PrimitiveDrawer.module.scss @@ -1,12 +1,12 @@ @use '~@talend/design-tokens/lib/tokens'; -.primitive-drawer { +.drawer { width: tokens.$coral-sizing-maximal; height: 100%; background: tokens.$coral-color-neutral-background; display: flex; - &__header { + .header { flex-grow: 0; flex-shrink: 0; flex-basis: tokens.$coral-sizing-l; @@ -15,7 +15,7 @@ display: flex; } - &__body { + .body { flex-grow: 1; flex-shrink: 1; flex-basis: 0; @@ -24,7 +24,7 @@ display: flex; } - &__footer { + .footer { flex-grow: 0; flex-shrink: 0; flex-basis: calc(tokens.$coral-sizing-l + 2 * tokens.$coral-spacing-xs); diff --git a/packages/design-system/src/components/WIP/Drawer/Primitive/PrimitiveDrawer.tsx b/packages/design-system/src/components/Drawer/Primitive/PrimitiveDrawer.tsx similarity index 59% rename from packages/design-system/src/components/WIP/Drawer/Primitive/PrimitiveDrawer.tsx rename to packages/design-system/src/components/Drawer/Primitive/PrimitiveDrawer.tsx index 94d46eb0d20..3d85e2affde 100644 --- a/packages/design-system/src/components/WIP/Drawer/Primitive/PrimitiveDrawer.tsx +++ b/packages/design-system/src/components/Drawer/Primitive/PrimitiveDrawer.tsx @@ -1,6 +1,6 @@ import { forwardRef } from 'react'; import type { Ref, ReactNode } from 'react'; -import { StackVertical } from '../../../Stack'; +import { StackVertical } from '../../Stack'; import theme from './PrimitiveDrawer.module.scss'; @@ -12,11 +12,11 @@ export type DrawerProps = { export const PrimitiveDrawer = forwardRef( ({ header, children, footer }: DrawerProps, ref: Ref) => ( -
    +
    - {header &&
    {header}
    } -
    {children}
    - {footer &&
    {footer}
    } + {header &&
    {header}
    } +
    {children}
    + {footer &&
    {footer}
    }
    ), diff --git a/packages/design-system/src/components/Drawer/__snapshots__/Drawer.test.tsx.snap b/packages/design-system/src/components/Drawer/__snapshots__/Drawer.test.tsx.snap new file mode 100644 index 00000000000..5a47831a2d7 --- /dev/null +++ b/packages/design-system/src/components/Drawer/__snapshots__/Drawer.test.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FloatingDrawer should render a11y html 1`] = ` +
    +
    +

    + test +

    + +
    +
    +`; diff --git a/packages/design-system/src/components/WIP/Drawer/index.ts b/packages/design-system/src/components/Drawer/index.ts similarity index 100% rename from packages/design-system/src/components/WIP/Drawer/index.ts rename to packages/design-system/src/components/Drawer/index.ts diff --git a/packages/design-system/src/components/WIP/Drawer/variants/FloatingDrawer/FloatingDrawer.module.scss b/packages/design-system/src/components/Drawer/variants/FloatingDrawer/FloatingDrawer.module.scss similarity index 72% rename from packages/design-system/src/components/WIP/Drawer/variants/FloatingDrawer/FloatingDrawer.module.scss rename to packages/design-system/src/components/Drawer/variants/FloatingDrawer/FloatingDrawer.module.scss index fdb13afc15a..d9c9eaf78ca 100644 --- a/packages/design-system/src/components/WIP/Drawer/variants/FloatingDrawer/FloatingDrawer.module.scss +++ b/packages/design-system/src/components/Drawer/variants/FloatingDrawer/FloatingDrawer.module.scss @@ -1,6 +1,6 @@ @use '~@talend/design-tokens/lib/tokens'; -.floating-drawer { +.drawer { position: absolute; top: 0; right: 0; @@ -8,9 +8,4 @@ box-shadow: tokens.$coral-elevation-shadow-neutral-m; z-index: tokens.$coral-elevation-layer-standard-front; transition: transform tokens.$coral-transition-fast; - transform: translateX(100%); - - &[data-enter] { - transform: translateX(0%); - } } diff --git a/packages/design-system/src/components/Drawer/variants/FloatingDrawer/FloatingDrawer.tsx b/packages/design-system/src/components/Drawer/variants/FloatingDrawer/FloatingDrawer.tsx new file mode 100644 index 00000000000..3157baf9cca --- /dev/null +++ b/packages/design-system/src/components/Drawer/variants/FloatingDrawer/FloatingDrawer.tsx @@ -0,0 +1,129 @@ +import { useEffect, useState, cloneElement } from 'react'; +import type { CSSProperties, HTMLAttributes, ReactElement, ReactNode } from 'react'; +import { Transition } from 'react-transition-group'; +import { PrimitiveDrawer } from '../../Primitive/PrimitiveDrawer'; + +import theme from './FloatingDrawer.module.scss'; +import { useId } from '../../../../useId'; + +type WithDisclosure = { + disclosure: ReactElement; + visible?: never; +}; + +type Controlled = { + disclosure?: never; + visible: boolean; +}; + +type DialogFnProps = { + onClose: () => void; +}; + +export type FloatingDrawerProps = { + id?: string; + header?: ((dialog: DialogFnProps) => ReactNode) | ReactNode; + children: ((dialog: DialogFnProps) => ReactNode) | ReactNode; + footer?: ((dialog: DialogFnProps) => ReactNode) | ReactNode; + withTransition?: boolean; + onClose?: () => void; + ['aria-label']: string; +} & (WithDisclosure | Controlled); + +// backward compatibility +export type DrawerProps = FloatingDrawerProps; + +const STYLES = { + entering: { transform: 'translateX(100%)' }, + entered: { transform: 'translateX(0%)' }, + exiting: { transform: 'translateX(100%)' }, + exited: { transform: 'translateX(100%)' }, + unmounted: { transform: 'translateX(100%)' }, +}; + +export const FloatingDrawer = ({ + id, + withTransition = true, + disclosure, + header, + children, + footer, + visible, + onClose, + ...props +}: DrawerProps) => { + const uuid = useId(id, 'drawer'); + const [isVisible, setVisible] = useState(visible === undefined ? false : visible); + + // sync with the props + useEffect(() => { + if (visible !== undefined && visible !== isVisible) { + setVisible(visible); + } + }, [visible, isVisible]); + const onCloseHandler = () => { + if (onClose) { + onClose(); + } else { + setVisible(false); + } + }; + + const disclosureProps = { + onClick: () => setVisible(!isVisible), + ['aria-expanded']: isVisible, + ['aria-controls']: uuid, + }; + return ( + <> + {disclosure &&
    {cloneElement(disclosure, disclosureProps)}
    } + {isVisible && ( + + {transitionState => { + const style = { + ...STYLES[transitionState], + }; + const childrenProps = { + onClose: onCloseHandler, + }; + return ( + + ); + }} + + )} + + ); +}; + +FloatingDrawer.displayName = 'FloatingDrawer'; + +type ContainerPropTypes = HTMLAttributes & { + children: ReactNode | ReactNode[]; + style?: Omit; +}; + +const Container = ({ children, style, ...props }: ContainerPropTypes) => ( +
    + {children} +
    +); + +Container.displayName = 'FloatingDrawer.Container'; + +FloatingDrawer.Container = Container; diff --git a/packages/design-system/src/components/Dropdown/Dropdown.cy.tsx b/packages/design-system/src/components/Dropdown/Dropdown.cy.tsx index 4738d3de3b6..346476c4e0d 100644 --- a/packages/design-system/src/components/Dropdown/Dropdown.cy.tsx +++ b/packages/design-system/src/components/Dropdown/Dropdown.cy.tsx @@ -1,7 +1,7 @@ /* eslint-disable testing-library/prefer-screen-queries */ /* eslint-disable testing-library/await-async-query */ /* eslint-disable no-console */ -import Dropdown from './'; +import { Dropdown } from './'; import { ButtonTertiary } from '../../components/Button'; import { IconsProvider } from '../../components/IconsProvider'; @@ -83,7 +83,7 @@ context('', () => { it('should display menu', () => { cy.mount(); - cy.findByTestId('dropdown.button').click(); + cy.findByTestId('dropdown.button').click({ force: true }); cy.findByTestId('dropdown.menu').should('be.visible'); cy.findByTestId('dropdown.menuitem.Link with icon-0').should('be.visible'); cy.findByTestId('dropdown.menuitem.Button with icon-1').should('be.visible'); @@ -96,7 +96,7 @@ context('', () => { cy.mount(); cy.findByTestId('dropdown.button').click(); cy.findByTestId('dropdown.menu').should('be.visible'); - cy.findByTestId('dropdown.menuitem.Button with icon-1').click(); + cy.findByTestId('dropdown.menuitem.Button with icon-1').click({ force: true }); cy.findByTestId('dropdown.menu').should('not.be.visible'); }); diff --git a/packages/design-system/src/components/Dropdown/Dropdown.test.tsx b/packages/design-system/src/components/Dropdown/Dropdown.test.tsx new file mode 100644 index 00000000000..ecf34846dd6 --- /dev/null +++ b/packages/design-system/src/components/Dropdown/Dropdown.test.tsx @@ -0,0 +1,56 @@ +import { describe, it, expect } from '@jest/globals'; +import { axe } from 'jest-axe'; +import { render } from '@testing-library/react'; +import { Dropdown } from './'; +import { ButtonTertiary } from '../Button'; + +jest.mock('@talend/utils', () => { + let i = 0; + return { + // we need stable but different uuid (is fixed to 42 by current mock) + randomUUID: () => `mocked-uuid-${i++}`, + }; +}); + +describe('Dropdown', () => { + it('should render a11y html', async () => { + const { container } = render( +
    + + {}}> + Dropdown + + +
    , + ); + expect(container.firstChild).toMatchSnapshot(); + const results = await axe(document.body); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/packages/design-system/src/components/Dropdown/Dropdown.tsx b/packages/design-system/src/components/Dropdown/Dropdown.tsx index c307ff8f2bc..590bc0c249d 100644 --- a/packages/design-system/src/components/Dropdown/Dropdown.tsx +++ b/packages/design-system/src/components/Dropdown/Dropdown.tsx @@ -1,16 +1,25 @@ -import { cloneElement, forwardRef, MouseEvent, ReactElement, Ref } from 'react'; -import { Menu, MenuButton, useMenuState } from 'reakit'; -// eslint-disable-next-line @talend/import-depth -import DropdownButton from './Primitive/DropdownButton'; +import { cloneElement, MouseEvent, ReactElement, useEffect, useState } from 'react'; + +import { + useDismiss, + useInteractions, + useFloating, + autoUpdate, + flip, + shift, +} from '@floating-ui/react'; + +import { DataAttributes, DeprecatedIconNames } from '../../types'; +import { Clickable, ClickableProps } from '../Clickable'; +import { LinkableType } from '../Linkable'; +import DropdownDivider from './Primitive/DropdownDivider'; import DropdownLink from './Primitive/DropdownLink'; import DropdownShell from './Primitive/DropdownShell'; import DropdownTitle from './Primitive/DropdownTitle'; -import DropdownDivider from './Primitive/DropdownDivider'; -import Clickable, { ClickableProps } from '../Clickable'; -import { LinkableType } from '../Linkable'; -import { DataAttributes, DeprecatedIconNames } from '../../types'; +import { DropdownButton } from './Primitive/DropdownButton'; +import { randomUUID } from '@talend/utils'; -type DropdownButtonType = Omit & { +type DropdownButtonType = Omit & { label: string; onClick: () => void; icon?: DeprecatedIconNames; @@ -44,119 +53,134 @@ export type DropdownPropsType = { 'aria-label': string; } & Partial; -const Dropdown = forwardRef( - ( - { - children, - 'data-test': dataTest, - 'data-testid': dataTestId, - items, - ...rest - }: DropdownPropsType, - ref: Ref, - ) => { - const menu = useMenuState({ - animated: 250, - gutter: 4, - loop: true, - }); - - const menuButtonTestId = dataTestId ? `${dataTestId}.dropdown.button` : 'dropdown.button'; - const menuTestId = dataTestId ? `${dataTestId}.dropdown.menu` : 'dropdown.menu'; - const menuItemTestId = dataTestId ? `${dataTestId}.dropdown.menuitem` : 'dropdown.menuitem'; - const menuButtonTest = dataTest ? `${dataTest}.dropdown.button` : 'dropdown.button'; - const menuTest = dataTest ? `${dataTest}.dropdown.menu` : 'dropdown.menu'; - const menuItemTest = dataTest ? `${dataTest}.dropdown.menuitem` : 'dropdown.menuitem'; - - return ( - <> - - {disclosureProps => cloneElement(children, disclosureProps)} - - - {items.map((entry, index) => { - if (entry.type === 'button') { - const { label, ...entryRest } = entry; - const id = `${label}-${index}`; - return ( - | KeyboardEvent) => { - menu.hide(); - entry.onClick(event); - }} - key={id} - id={id} - data-testid={`${menuItemTestId}.${id}`} - data-test={`${menuItemTest}.${id}`} - > - {label} - - ); - } - - if (entry.type === 'title') { - const { label } = entry; - const id = `${label}-${index}`; - return ( - - {label} - - ); - } - - if (entry.type === 'divider') { - const id = `divider-${index}`; - return ( - - ); - } - - const { label, as, type, ...entryRest } = entry; +export const Dropdown = ({ + children, + 'data-test': dataTest, + 'data-testid': dataTestId, + items, + ...rest +}: DropdownPropsType) => { + const [isOpen, setIsOpen] = useState(false); + const [itemIds, setItemIds] = useState(items.map(() => randomUUID())); + + useEffect(() => { + setItemIds(items.map(() => randomUUID())); + }, [items]); + + const floating = useFloating({ + placement: 'bottom-start', + open: isOpen, + onOpenChange: setIsOpen, + middleware: [flip(), shift()], + whileElementsMounted: autoUpdate, + }); + const dismiss = useDismiss(floating.context, { + escapeKey: true, + outsidePressEvent: 'mousedown', + }); + const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]); + const menuButtonTestId = dataTestId ? `${dataTestId}.dropdown.button` : 'dropdown.button'; + const menuTestId = dataTestId ? `${dataTestId}.dropdown.menu` : 'dropdown.menu'; + const menuItemTestId = dataTestId ? `${dataTestId}.dropdown.menuitem` : 'dropdown.menuitem'; + const menuButtonTest = dataTest ? `${dataTest}.dropdown.button` : 'dropdown.button'; + const menuTest = dataTest ? `${dataTest}.dropdown.menu` : 'dropdown.menu'; + const menuItemTest = dataTest ? `${dataTest}.dropdown.menuitem` : 'dropdown.menuitem'; + + return ( + <> + {cloneElement(children as any, { + onClick: () => setIsOpen(!isOpen), + 'aria-pressed': `${isOpen}`, + 'data-testid': menuButtonTestId, + 'data-test': menuButtonTest, + 'aria-expanded': `${isOpen}`, + ref: floating.refs.setReference, + ...getReferenceProps(), + })} + setIsOpen(false)} + style={{ ...floating.floatingStyles, display: isOpen ? 'block' : 'none' }} + data-testid={menuTestId} + data-test={menuTest} + {...getFloatingProps()} + > + {items.map((entry, index) => { + const uuid = itemIds[index]; + if (entry.type === 'button') { + const { label, ...entryRest } = entry; const id = `${label}-${index}`; return ( - ) => { - menu.hide(); - if (entry.onClick) { - entry.onClick(event); - } + // {...menu} + onClick={(event: MouseEvent | KeyboardEvent) => { + entry.onClick(event); + setIsOpen(false); }} + key={uuid} + tabIndex={0} + id={uuid} data-testid={`${menuItemTestId}.${id}`} data-test={`${menuItemTest}.${id}`} > {label} - + ); - })} - - - ); - }, -); + } -Dropdown.displayName = 'Dropdown'; + if (entry.type === 'title') { + const { label } = entry; + const id = `${label}-${index}`; + return ( + + {label} + + ); + } -export default Dropdown; + if (entry.type === 'divider') { + const id = `divider-${index}`; + return ( + + ); + } + + const { label, as, type, ...entryRest } = entry; + const id = `${label}-${index}`; + return ( + ) => { + setIsOpen(false); + if (entry.onClick) { + entry.onClick(event); + } + }} + data-testid={`${menuItemTestId}.${id}`} + data-test={`${menuItemTest}.${id}`} + > + {label} + + ); + })} + + + ); +}; + +Dropdown.displayName = 'Dropdown'; diff --git a/packages/design-system/src/components/Dropdown/Primitive/DropdownButton.tsx b/packages/design-system/src/components/Dropdown/Primitive/DropdownButton.tsx index f0bbfbdd1f7..940bef4240c 100644 --- a/packages/design-system/src/components/Dropdown/Primitive/DropdownButton.tsx +++ b/packages/design-system/src/components/Dropdown/Primitive/DropdownButton.tsx @@ -1,52 +1,50 @@ import { forwardRef, Ref } from 'react'; -import classNames from 'classnames'; -import { MenuItem, MenuItemProps } from 'reakit'; + // eslint-disable-next-line @talend/import-depth import { IconNameWithSize } from '@talend/icons/dist/typeUtils'; import { DeprecatedIconNames } from '../../../types'; -import Clickable, { ClickableProps } from '../../Clickable'; +import { Clickable, ClickableProps } from '../../Clickable'; import { SizedIcon } from '../../Icon'; import { getIconWithDeprecatedSupport } from '../../Icon/DeprecatedIconHelper'; import styles from './DropdownEntry.module.scss'; -export type DropdownButtonType = Omit & - MenuItemProps & { icon?: DeprecatedIconNames | IconNameWithSize<'M'>; checked?: boolean }; +/** + * This is in fact a Menu Item + */ + +export type DropdownButtonType = ClickableProps & { + icon?: DeprecatedIconNames | IconNameWithSize<'M'>; +}; -const DropdownButton = forwardRef( +export const DropdownButton = forwardRef( ({ children, icon, checked, ...props }: DropdownButtonType, ref: Ref) => { return ( - - {icon && ( - - {getIconWithDeprecatedSupport({ - iconSrc: icon, - size: 'M', - ['data-test']: 'button.icon.before', - })} - - )} - {children} - {checked && ( - - )} - + +
    + {icon && ( + + {getIconWithDeprecatedSupport({ + iconSrc: icon, + size: 'M', + ['data-test']: 'button.icon.before', + })} + + )} + {children} + {checked && ( + + )} +
    +
    ); }, ); DropdownButton.displayName = 'DropdownButton'; - -export default DropdownButton; diff --git a/packages/design-system/src/components/Dropdown/Primitive/DropdownDivider.tsx b/packages/design-system/src/components/Dropdown/Primitive/DropdownDivider.tsx index 9cfb4854d47..62785bb0081 100644 --- a/packages/design-system/src/components/Dropdown/Primitive/DropdownDivider.tsx +++ b/packages/design-system/src/components/Dropdown/Primitive/DropdownDivider.tsx @@ -1,12 +1,13 @@ -import { forwardRef, Ref } from 'react'; -import { MenuItemProps, MenuSeparator } from 'reakit'; +import { forwardRef, Ref, HTMLAttributes } from 'react'; import styles from './DropdownDivider.module.scss'; -export type DropdownDividerType = MenuItemProps; +export type DropdownDividerType = HTMLAttributes; const DropdownDivider = forwardRef((props: DropdownDividerType, ref: Ref) => { - return ; + return
    ; }); +DropdownDivider.displayName = 'DropdownDivider'; + export default DropdownDivider; diff --git a/packages/design-system/src/components/Dropdown/Primitive/DropdownLink.tsx b/packages/design-system/src/components/Dropdown/Primitive/DropdownLink.tsx index bb4ce9e81e2..2f1cffa3786 100644 --- a/packages/design-system/src/components/Dropdown/Primitive/DropdownLink.tsx +++ b/packages/design-system/src/components/Dropdown/Primitive/DropdownLink.tsx @@ -1,10 +1,9 @@ import { forwardRef, ReactElement, Ref } from 'react'; -import { MenuItem, MenuItemProps } from 'reakit'; -import Linkable, { LinkableType } from '../../Linkable'; +import { Linkable, LinkableType } from '../../Linkable'; import styles from './DropdownEntry.module.scss'; -export type DropdownLinkType = LinkableType & MenuItemProps; +export type DropdownLinkType = LinkableType; // Extend Linkable with props from Dropdown // Since MenuItem and Linkable both have an `as` prop, this enables passing `as` to Linkable. @@ -17,15 +16,16 @@ const LocalLink = forwardRef( return ; }, ); +LocalLink.displayName = 'LocalLink'; const DropdownLink = forwardRef( ({ children, as, ...props }: DropdownLinkType, ref: Ref) => { return ( - + {children} - + ); }, ); - +DropdownLink.displayName = 'DropdownLink'; export default DropdownLink; diff --git a/packages/design-system/src/components/Dropdown/Primitive/DropdownShell.module.scss b/packages/design-system/src/components/Dropdown/Primitive/DropdownShell.module.scss index 87b0e5d6fdc..ce144eb6d44 100644 --- a/packages/design-system/src/components/Dropdown/Primitive/DropdownShell.module.scss +++ b/packages/design-system/src/components/Dropdown/Primitive/DropdownShell.module.scss @@ -2,6 +2,10 @@ .dropdownShell { z-index: tokens.$coral-elevation-layer-interactive-front; + position: absolute; + width: max-content; + top: 0; + left: 0; .animatedZone { background: tokens.$coral-color-neutral-background; @@ -12,14 +16,5 @@ min-width: calc(#{tokens.$coral-sizing-maximal} / 2); max-width: tokens.$coral-sizing-maximal; transition: tokens.$coral-transition-fast; - opacity: 0; - transform: translateY(-10%); - } - - &[data-enter] { - .animatedZone { - opacity: 1; - transform: translateY(0%); - } } } diff --git a/packages/design-system/src/components/Dropdown/Primitive/DropdownShell.tsx b/packages/design-system/src/components/Dropdown/Primitive/DropdownShell.tsx index d881c1af2dc..23240ef5345 100644 --- a/packages/design-system/src/components/Dropdown/Primitive/DropdownShell.tsx +++ b/packages/design-system/src/components/Dropdown/Primitive/DropdownShell.tsx @@ -1,21 +1,35 @@ -import { forwardRef, Ref } from 'react'; -import { MenuProps } from 'reakit'; - +import { forwardRef } from 'react'; +import type { HTMLAttributes, ReactNode } from 'react'; import styles from './DropdownShell.module.scss'; import { StackVertical } from '../../Stack'; -type ShellProps = MenuProps; +type ShellProps = HTMLAttributes & { + onClick: () => void; + children: ReactNode; +}; -const DropdownShell = forwardRef(({ children, ...rest }: ShellProps, ref: Ref) => { - return ( -
    -
    - - {children} - +const DropdownShell = forwardRef( + ({ children, onClick, ...rest }, ref) => { + return ( +
    +
    + + {children} + +
    -
    - ); -}); + ); + }, +); + +DropdownShell.displayName = 'DropdownShell'; export default DropdownShell; diff --git a/packages/design-system/src/components/Dropdown/Primitive/DropdownTitle.tsx b/packages/design-system/src/components/Dropdown/Primitive/DropdownTitle.tsx index 7f37a516b04..c8edaf2b05b 100644 --- a/packages/design-system/src/components/Dropdown/Primitive/DropdownTitle.tsx +++ b/packages/design-system/src/components/Dropdown/Primitive/DropdownTitle.tsx @@ -14,4 +14,5 @@ const DropdownTitle = forwardRef( }, ); +DropdownTitle.displayName = 'DropdownTitle'; export default DropdownTitle; diff --git a/packages/design-system/src/components/Dropdown/__snapshots__/Dropdown.test.tsx.snap b/packages/design-system/src/components/Dropdown/__snapshots__/Dropdown.test.tsx.snap new file mode 100644 index 00000000000..35241e8e71c --- /dev/null +++ b/packages/design-system/src/components/Dropdown/__snapshots__/Dropdown.test.tsx.snap @@ -0,0 +1,121 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Dropdown should render a11y html 1`] = ` +
    + + +
    +`; diff --git a/packages/design-system/src/components/Dropdown/index.ts b/packages/design-system/src/components/Dropdown/index.ts index 293f190383c..5bd11eca0bd 100644 --- a/packages/design-system/src/components/Dropdown/index.ts +++ b/packages/design-system/src/components/Dropdown/index.ts @@ -1,6 +1,2 @@ -import Dropdown from './Dropdown'; -import DropdownButton from './Primitive/DropdownButton'; - -export default Dropdown; - -export { DropdownButton }; +export * from './Dropdown'; +export * from './Primitive/DropdownButton'; diff --git a/packages/design-system/src/components/EmptyState/EmptyState.test.tsx b/packages/design-system/src/components/EmptyState/EmptyState.test.tsx new file mode 100644 index 00000000000..f24c60f457c --- /dev/null +++ b/packages/design-system/src/components/EmptyState/EmptyState.test.tsx @@ -0,0 +1,40 @@ +import { describe, it, expect } from '@jest/globals'; +import { axe } from 'jest-axe'; +import { render } from '@testing-library/react'; +import { EmptyState } from './'; + +describe('EmptyState', () => { + it('should render a11y html', async () => { + const { container } = render( +
    + + +
    + +
    +
    , + ); + expect(container.firstChild).toMatchSnapshot(); + const results = await axe(document.body); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/packages/design-system/src/components/EmptyState/EmptyState.tsx b/packages/design-system/src/components/EmptyState/EmptyState.tsx index d8e31be3f09..811e28745fb 100644 --- a/packages/design-system/src/components/EmptyState/EmptyState.tsx +++ b/packages/design-system/src/components/EmptyState/EmptyState.tsx @@ -15,7 +15,7 @@ type Small = VariantType<'S', EmptyStateSmallProps>; export type EmptyStateProps = Large | Medium | Small; -const EmptyState = forwardRef((props: EmptyStateProps, ref: Ref) => { +export const EmptyState = forwardRef((props: EmptyStateProps, ref: Ref) => { switch (props.variant) { case 'L': { const { variant, ...rest } = props; @@ -39,5 +39,3 @@ const EmptyState = forwardRef((props: EmptyStateProps, ref: Ref) => }); EmptyState.displayName = 'EmptyState'; - -export default EmptyState; diff --git a/packages/design-system/src/components/EmptyState/__snapshots__/EmptyState.test.tsx.snap b/packages/design-system/src/components/EmptyState/__snapshots__/EmptyState.test.tsx.snap new file mode 100644 index 00000000000..c9dde18e7b6 --- /dev/null +++ b/packages/design-system/src/components/EmptyState/__snapshots__/EmptyState.test.tsx.snap @@ -0,0 +1,434 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EmptyState should render a11y html 1`] = ` +
    + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    + No preparations yet +

    +

    + Add a preparation to clean, format, and transform data prior to processing. +

    +
    + + + + Learn more + + + + + +
    +
    +
    +
    +`; diff --git a/packages/design-system/src/components/EmptyState/index.ts b/packages/design-system/src/components/EmptyState/index.ts index b9cf05cfe80..0c2df29071e 100644 --- a/packages/design-system/src/components/EmptyState/index.ts +++ b/packages/design-system/src/components/EmptyState/index.ts @@ -1,7 +1,6 @@ -import EmptyState from './EmptyState'; +export * from './EmptyState'; import EmptyStateLarge from './variants/EmptyStateLarge'; import EmptyStateMedium from './variants/EmptyStateMedium'; import EmptyStateSmall from './variants/EmptyStateSmall'; -export default EmptyState; export { EmptyStateLarge, EmptyStateMedium, EmptyStateSmall }; diff --git a/packages/design-system/src/components/EmptyState/primitive/EmptyStatePrimitive.tsx b/packages/design-system/src/components/EmptyState/primitive/EmptyStatePrimitive.tsx index a65f7f4587d..d5baab619c4 100644 --- a/packages/design-system/src/components/EmptyState/primitive/EmptyStatePrimitive.tsx +++ b/packages/design-system/src/components/EmptyState/primitive/EmptyStatePrimitive.tsx @@ -2,7 +2,7 @@ import { forwardRef, HTMLAttributes, ReactElement, Ref } from 'react'; import { useTranslation } from 'react-i18next'; import { StackVertical } from '../../Stack'; -import Link from '../../Link'; +import { Link } from '../../Link'; import { ButtonPrimary } from '../../Button'; import { ButtonPrimaryAsLink } from '../../ButtonAsLink'; import { ButtonPrimaryPropsType } from '../../Button/variations/ButtonPrimary'; @@ -61,5 +61,5 @@ const EmptyStatePrimitive = forwardRef((props: EmptyStatePrimitiveProps, ref: Re ); }); - +EmptyStatePrimitive.displayName = 'EmptyStatePrimitive'; export default EmptyStatePrimitive; diff --git a/packages/design-system/src/components/EmptyState/variants/EmptyStateLarge.tsx b/packages/design-system/src/components/EmptyState/variants/EmptyStateLarge.tsx index b126eb6b68b..f783501c01a 100644 --- a/packages/design-system/src/components/EmptyState/variants/EmptyStateLarge.tsx +++ b/packages/design-system/src/components/EmptyState/variants/EmptyStateLarge.tsx @@ -1,5 +1,5 @@ import { forwardRef, Ref } from 'react'; -import Illustration from '../../illustrations'; +import { Illustration } from '../../illustrations'; import EmptyStatePrimitive, { EmptyStatePrimitiveProps } from '../primitive/EmptyStatePrimitive'; export type EmptyStateLargeProps = Omit & { @@ -17,4 +17,5 @@ const EmptyStateLarge = forwardRef((props: EmptyStateLargeProps, ref: Ref; }); +EmptyStateSmall.displayName = 'EmptyStateSmall'; export default EmptyStateSmall; diff --git a/packages/design-system/src/components/ErrorState/ErrorState.test.tsx b/packages/design-system/src/components/ErrorState/ErrorState.test.tsx new file mode 100644 index 00000000000..b0d09adfc52 --- /dev/null +++ b/packages/design-system/src/components/ErrorState/ErrorState.test.tsx @@ -0,0 +1,17 @@ +import { describe, it, expect } from '@jest/globals'; +import { axe } from 'jest-axe'; +import { render } from '@testing-library/react'; +import { ErrorState } from './'; + +describe('ErrorState', () => { + it('should render a11y html', async () => { + const { container } = render( +
    + +
    , + ); + expect(container.firstChild).toMatchSnapshot(); + const results = await axe(document.body); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/packages/design-system/src/components/ErrorState/ErrorState.tsx b/packages/design-system/src/components/ErrorState/ErrorState.tsx index fac2fd14083..655c47c3c33 100644 --- a/packages/design-system/src/components/ErrorState/ErrorState.tsx +++ b/packages/design-system/src/components/ErrorState/ErrorState.tsx @@ -2,7 +2,7 @@ import { ReactElement, isValidElement } from 'react'; import { ButtonPrimary } from '../Button'; import { ButtonPrimaryPropsType } from '../Button/variations/ButtonPrimary'; -import Link from '../Link'; +import { Link } from '../Link'; import { LinkProps } from '../Link/Link'; import { StackVertical } from '../Stack'; @@ -10,14 +10,14 @@ import ErrorIllustration from './illutstrations/ErrorIllustration'; import styles from './ErrorState.module.scss'; -type ErrorStatePropTypes = { +export type ErrorStatePropTypes = { title: string; description: string; action?: Omit, 'size'>; link?: ReactElement | LinkProps; }; -function ErrorState({ title, description, action, link }: ErrorStatePropTypes) { +export function ErrorState({ title, description, action, link }: ErrorStatePropTypes) { return (
    @@ -35,5 +35,3 @@ function ErrorState({ title, description, action, link }: ErrorStatePropTypes) {
    ); } - -export default ErrorState; diff --git a/packages/design-system/src/components/ErrorState/__snapshots__/ErrorState.test.tsx.snap b/packages/design-system/src/components/ErrorState/__snapshots__/ErrorState.test.tsx.snap new file mode 100644 index 00000000000..4eb0eba62d7 --- /dev/null +++ b/packages/design-system/src/components/ErrorState/__snapshots__/ErrorState.test.tsx.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ErrorState should render a11y html 1`] = ` +
    +
    +
    + + + + + + + + +
    +

    + small +

    +

    + what happens +

    +
    +
    +
    +
    +`; diff --git a/packages/design-system/src/components/ErrorState/index.ts b/packages/design-system/src/components/ErrorState/index.ts index b17df79da3f..c8a7981cdb2 100644 --- a/packages/design-system/src/components/ErrorState/index.ts +++ b/packages/design-system/src/components/ErrorState/index.ts @@ -1 +1 @@ -export { default } from './ErrorState'; +export * from './ErrorState'; diff --git a/packages/design-system/src/components/Form/Affix/variations/AffixButton.tsx b/packages/design-system/src/components/Form/Affix/variations/AffixButton.tsx index b7ef75b4881..17efeee1ebd 100644 --- a/packages/design-system/src/components/Form/Affix/variations/AffixButton.tsx +++ b/packages/design-system/src/components/Form/Affix/variations/AffixButton.tsx @@ -5,13 +5,14 @@ import classnames from 'classnames'; import { IconNameWithSize } from '@talend/icons/dist/typeUtils'; import { DeprecatedIconNames } from '../../../../types'; -import Tooltip from '../../../Tooltip'; +import { Tooltip, TooltipChildrenFnProps, TooltipChildrenFnRef } from '../../../Tooltip'; import { StackHorizontal } from '../../../Stack'; -import Clickable, { ClickableProps } from '../../../Clickable'; +import { Clickable, ClickableProps } from '../../../Clickable'; import { getIconWithDeprecatedSupport } from '../../../Icon/DeprecatedIconHelper'; import { SizedIcon } from '../../../Icon'; import styles from '../AffixStyles.module.scss'; +import { mergeRefs } from '../../../../mergeRef'; type CommonAffixButtonPropsType = { children: string; @@ -34,7 +35,7 @@ export type AffixButtonPropsType = Omit( ( { children, @@ -47,11 +48,12 @@ const AffixButton = forwardRef( }: AffixButtonPropsType, ref: Ref, ) => { - const element = ( + const element = (subProps: TooltipChildrenFnProps, subRef: TooltipChildrenFnRef) => ( @@ -74,12 +76,14 @@ const AffixButton = forwardRef( if (hideText) { return ( - {element} + {(triggerProps: TooltipChildrenFnProps, triggerRef: TooltipChildrenFnRef) => + element(triggerProps, mergeRefs([triggerRef, ref])) + } ); } - return element; + return element({}, ref); }, ); diff --git a/packages/design-system/src/components/Form/Affix/variations/AffixReadOnly.tsx b/packages/design-system/src/components/Form/Affix/variations/AffixReadOnly.tsx index 0b577f34743..7ed1f178320 100644 --- a/packages/design-system/src/components/Form/Affix/variations/AffixReadOnly.tsx +++ b/packages/design-system/src/components/Form/Affix/variations/AffixReadOnly.tsx @@ -4,7 +4,7 @@ import { IconNameWithSize } from '@talend/icons'; import { DeprecatedIconNames } from '../../../../types'; import { StackHorizontal } from '../../../Stack'; -import VisuallyHidden from '../../../VisuallyHidden'; +import { VisuallyHidden } from '../../../VisuallyHidden'; import { getIconWithDeprecatedSupport } from '../../../Icon/DeprecatedIconHelper'; import styles from '../AffixStyles.module.scss'; diff --git a/packages/design-system/src/components/Form/Affix/variations/AffixSelect.tsx b/packages/design-system/src/components/Form/Affix/variations/AffixSelect.tsx index ab2464fa676..f3e4542bfa0 100644 --- a/packages/design-system/src/components/Form/Affix/variations/AffixSelect.tsx +++ b/packages/design-system/src/components/Form/Affix/variations/AffixSelect.tsx @@ -1,7 +1,7 @@ import { forwardRef, Ref } from 'react'; -import { unstable_useId as useId } from 'reakit'; import FieldPrimitive, { FieldPropsPrimitive } from '../../Primitives/Field/Field'; import SelectNoWrapper, { SelectNoWrapperProps } from '../../Primitives/Select/SelectNoWrapper'; +import { useId } from '../../../../useId'; export type AffixSelectPropsType = Omit & Omit & { @@ -12,8 +12,7 @@ const AffixSelect = forwardRef((props: AffixSelectPropsType, ref: Ref, diff --git a/packages/design-system/src/components/Form/Field/Datalist/Datalist.tsx b/packages/design-system/src/components/Form/Field/Datalist/Datalist.tsx index 9e2bbc70084..24797eae43e 100644 --- a/packages/design-system/src/components/Form/Field/Datalist/Datalist.tsx +++ b/packages/design-system/src/components/Form/Field/Datalist/Datalist.tsx @@ -1,11 +1,11 @@ import { forwardRef, Key, Ref } from 'react'; -import { unstable_useId as useId } from 'reakit'; import { FieldPrimitive, FieldPropsPrimitive, InputPrimitive, InputPrimitiveProps, -} from '../../Primitives/index'; +} from '../../Primitives'; +import { useId } from '../../../../useId'; export type DatalistProps = { values: string[]; @@ -36,9 +36,8 @@ const Datalist = forwardRef( }: DatalistProps, ref: Ref | undefined, ) => { - const { id: reakitId } = useId(); - const datalistId = id || `datalist--${reakitId}`; - const datalistListId = `datalist__list--${reakitId}`; + const datalistId = useId(id, 'datalist-'); + const datalistListId = useId(undefined, 'datalist__list-'); return ( <> diff --git a/packages/design-system/src/components/Form/Field/Input/Input.Checkbox.tsx b/packages/design-system/src/components/Form/Field/Input/Input.Checkbox.tsx index 9d784dfb3c4..4160e07a676 100644 --- a/packages/design-system/src/components/Form/Field/Input/Input.Checkbox.tsx +++ b/packages/design-system/src/components/Form/Field/Input/Input.Checkbox.tsx @@ -1,7 +1,12 @@ import { forwardRef, Ref } from 'react'; -import { CheckboxPrimitive, CheckboxPrimitiveType } from '../../Primitives/index'; -export type CheckboxProps = Omit; +import { Mandatory } from '../../../../types'; +import { CheckboxPrimitive, CheckboxPrimitiveType } from '../../Primitives'; + +export type CheckboxProps = Mandatory< + Omit, + 'onChange' +>; const Checkbox = forwardRef((props: CheckboxProps, ref: Ref) => { return ; diff --git a/packages/design-system/src/components/Form/Field/Input/Input.Copy.tsx b/packages/design-system/src/components/Form/Field/Input/Input.Copy.tsx index 36cc4dac070..21e1242c2b7 100644 --- a/packages/design-system/src/components/Form/Field/Input/Input.Copy.tsx +++ b/packages/design-system/src/components/Form/Field/Input/Input.Copy.tsx @@ -7,7 +7,7 @@ import { FieldPropsPrimitive, InputPrimitive, InputPrimitiveProps, -} from '../../Primitives/index'; +} from '../../Primitives'; type InputCopyProps = Omit & Omit; diff --git a/packages/design-system/src/components/Form/Field/Input/Input.File.tsx b/packages/design-system/src/components/Form/Field/Input/Input.File.tsx index 596ffd0c21b..318d1e1666a 100644 --- a/packages/design-system/src/components/Form/Field/Input/Input.File.tsx +++ b/packages/design-system/src/components/Form/Field/Input/Input.File.tsx @@ -1,20 +1,20 @@ import { forwardRef, Key, Ref, useEffect, useRef, useState } from 'react'; -import { unstable_useId as useId } from 'reakit'; import { Trans, useTranslation } from 'react-i18next'; import { TFunction } from 'i18next'; import classnames from 'classnames'; import { I18N_DOMAIN_DESIGN_SYSTEM } from '../../../constants'; import { ButtonIcon } from '../../../ButtonIcon'; import { SizedIcon } from '../../../Icon'; -import VisuallyHidden from '../../../VisuallyHidden'; +import { VisuallyHidden } from '../../../VisuallyHidden'; import { FieldPrimitive, FieldPropsPrimitive, InputPrimitive, InputPrimitiveProps, -} from '../../Primitives/index'; +} from '../../Primitives'; import styles from './Input.File.module.scss'; +import { useId } from '../../../../useId'; function getFileSize(size: number, t: TFunction) { if (size < 1024) { @@ -93,8 +93,7 @@ const InputFile = forwardRef((props: FileProps, ref: Ref) => { } }; }, []); - const { id: reakitId } = useId(); - const fileInfoId = `info--${reakitId}`; + const fileInfoId = useId(undefined, 'info-'); const filesValue = () => { if (files) { diff --git a/packages/design-system/src/components/Form/Field/Input/Input.Radio.tsx b/packages/design-system/src/components/Form/Field/Input/Input.Radio.tsx index 2b5600f7617..27da5a606c2 100644 --- a/packages/design-system/src/components/Form/Field/Input/Input.Radio.tsx +++ b/packages/design-system/src/components/Form/Field/Input/Input.Radio.tsx @@ -1,5 +1,5 @@ import { forwardRef, Ref } from 'react'; -import { RadioPrimitive, RadioPrimitiveType } from '../../Primitives/index'; +import { RadioPrimitive, RadioPrimitiveType } from '../../Primitives'; export type RadioProps = Omit; diff --git a/packages/design-system/src/components/Form/Field/Input/Input.ToggleSwitch.tsx b/packages/design-system/src/components/Form/Field/Input/Input.ToggleSwitch.tsx index 7791a42a443..c6ee13c2bc1 100644 --- a/packages/design-system/src/components/Form/Field/Input/Input.ToggleSwitch.tsx +++ b/packages/design-system/src/components/Form/Field/Input/Input.ToggleSwitch.tsx @@ -1,17 +1,21 @@ import { forwardRef } from 'react'; import type { Ref } from 'react'; -import { Checkbox as ReakitCheckbox, unstable_useId as useId } from 'reakit'; import classnames from 'classnames'; -import useCheckboxState from './hooks/useCheckboxState'; -import { CheckboxProps } from './Input.Checkbox'; +import { useId } from '../../../../useId'; +import { CheckboxPrimitiveType } from '../../Primitives'; +import { useControl } from '../../../../useControl'; import styles from './Input.ToggleSwitch.module.scss'; -const ToggleSwitch = forwardRef( - ( - { +export type ToggleSwitchPropTypes = Omit & { + onChange?: (checked: boolean) => void; +}; + +export const ToggleSwitch = forwardRef( + (props: Omit, ref: Ref) => { + const { id, label, defaultChecked, @@ -21,34 +25,38 @@ const ToggleSwitch = forwardRef( required, children, isInline, + onChange, ...rest - }: Omit, - ref: Ref, - ) => { - const { id: reakitId } = useId(); - const switchId = id || `switch--${reakitId}`; - const checkbox = useCheckboxState({ state: defaultChecked || checked, readOnly }); + } = props; + const switchId = useId(id, 'switch-'); + const controlled = useControl(props, { + onChangeKey: 'onChange', + valueKey: 'checked', + defaultValueKey: 'defaultChecked', + selector: e => e.target.checked, + defaultValue: false, + }); return (