From b15a915f04b8d5da93c76fb35458c6001fd0d60f Mon Sep 17 00:00:00 2001 From: Denis Yakshov Date: Fri, 9 Oct 2026 12:12:54 +0200 Subject: [PATCH] fix(react)!: produce valid html markup --- .../components/AudioPlayer/AudioPlayer.tsx | 2 +- .../AutocompleteMenu/AutocompleteMenu.tsx | 7 ++++- .../react/src/components/Banner/Banner.tsx | 4 +-- .../src/components/ButtonBase/ButtonBase.tsx | 30 +++++++++++++++++-- .../Checkbox/CheckboxIcon/CheckboxIcon.tsx | 4 +-- .../Dialog/DialogClose/DialogClose.tsx | 2 +- .../react/src/components/Divider/Divider.tsx | 9 ++++-- .../src/components/Divider/Divider.types.ts | 10 ++++++- .../src/components/Dropzone/Dropzone.tsx | 16 +++++----- .../FormControlLabel/FormControlLabel.tsx | 2 +- .../FormControlLabel.types.ts | 2 +- .../src/components/MenuGroup/MenuGroup.tsx | 12 ++++++-- .../PaginationPages/PaginationPages.tsx | 6 ++-- .../components/Radio/RadioIcon/RadioIcon.tsx | 8 ++--- .../components/SFS/SFSFilters/SFSFilters.tsx | 6 ++-- .../components/SFS/SFSSorting/SFSSorting.tsx | 10 +++---- .../Sidebar/SidebarItem/SidebarItem.tsx | 3 +- .../Sidenav/SidenavItem/SidenavItem.tsx | 1 + .../react/src/components/Slider/useSlider.ts | 4 +-- .../components/SortingMenu/SortingMenu.tsx | 4 +-- .../react/src/components/SvgIcon/SvgIcon.tsx | 4 +-- .../src/components/SvgIcon/SvgIcon.types.ts | 2 +- .../SwiperPaginationItem.tsx | 2 +- .../react/src/components/Switch/Switch.tsx | 2 +- .../components/TouchRipple/TouchRipple.tsx | 4 +-- .../components/TouchRipple/useTouchRipple.ts | 2 +- .../theme/lib/components/_breadcrumbs.scss | 3 +- .../theme/lib/components/_pagination.scss | 5 ++++ packages/theme/lib/components/_switch.scss | 1 + .../theme/lib/components/_touch-ripple.scss | 1 + 30 files changed, 112 insertions(+), 56 deletions(-) diff --git a/packages/react/src/components/AudioPlayer/AudioPlayer.tsx b/packages/react/src/components/AudioPlayer/AudioPlayer.tsx index ecd5f74fb..f79994a35 100644 --- a/packages/react/src/components/AudioPlayer/AudioPlayer.tsx +++ b/packages/react/src/components/AudioPlayer/AudioPlayer.tsx @@ -762,7 +762,7 @@ export const AudioPlayer = ({ ref, ...inProps }: AudioPlayerProps & RefAttribute {iconBack} - + {rates.map((r) => ( 0) { groupedOptions.push( - + ); } diff --git a/packages/react/src/components/Banner/Banner.tsx b/packages/react/src/components/Banner/Banner.tsx index 6dc01ea44..e117cf768 100644 --- a/packages/react/src/components/Banner/Banner.tsx +++ b/packages/react/src/components/Banner/Banner.tsx @@ -51,9 +51,7 @@ export const Banner = ({ ref, ...inProps }: BannerProps & RefAttributes {icon !== false &&
{icon || iconMapping[severity]}
}
-
- {children} -
+
{children}
{actions}
{!!action &&
{action}
} diff --git a/packages/react/src/components/ButtonBase/ButtonBase.tsx b/packages/react/src/components/ButtonBase/ButtonBase.tsx index f30c652d4..063816ed3 100644 --- a/packages/react/src/components/ButtonBase/ButtonBase.tsx +++ b/packages/react/src/components/ButtonBase/ButtonBase.tsx @@ -11,6 +11,29 @@ import { useDefaultProps } from '../../theming'; import { OverridableComponent } from '../../types'; import { TouchRipple, useTouchRipple } from '../TouchRipple'; +const FORM_ELEMENTS = ['button', 'fieldset', 'input', 'optgroup', 'option', 'select', 'textarea']; + +const FLOW_CONTENT_ELEMENTS = [ + 'address', + 'article', + 'aside', + 'blockquote', + 'dd', + 'div', + 'fieldset', + 'figcaption', + 'figure', + 'footer', + 'form', + 'header', + 'li', + 'main', + 'nav', + 'section', + 'td', + 'th', +]; + /** * The Button allows users to take actions, and make choices, with a single tap. */ @@ -36,6 +59,9 @@ export const ButtonBase = (({ ref, ...inProps }: ButtonBaseProps) => { name: 'ESButtonBase', }); + const Wrapper = typeof Component === 'string' && FLOW_CONTENT_ELEMENTS.includes(Component) ? 'div' : 'span'; + const isDisabledSupported = typeof Component !== 'string' || FORM_ELEMENTS.includes(Component); + const buttonRef = useRef(null); const handleRef = useForkRef(ref, buttonRef); @@ -94,12 +120,12 @@ export const ButtonBase = (({ ref, ...inProps }: ButtonBaseProps) => { disableTouchRipple && 'es-button-base--disable-touch-ripple', pressed && 'es-button-base--pressed' )} - disabled={disabled} + disabled={isDisabledSupported ? disabled : undefined} type={type} {...props} {...bind} > -
{children}
+ {children} ); diff --git a/packages/react/src/components/Checkbox/CheckboxIcon/CheckboxIcon.tsx b/packages/react/src/components/Checkbox/CheckboxIcon/CheckboxIcon.tsx index 180fbfa75..a53528742 100644 --- a/packages/react/src/components/Checkbox/CheckboxIcon/CheckboxIcon.tsx +++ b/packages/react/src/components/Checkbox/CheckboxIcon/CheckboxIcon.tsx @@ -11,8 +11,8 @@ import { useDefaultProps } from '../../../theming'; /** * @see `Checkbox` */ -export const CheckboxIcon = ({ ref, ...inProps }: CheckboxIconProps & RefAttributes) => { +export const CheckboxIcon = ({ ref, ...inProps }: CheckboxIconProps & RefAttributes) => { const { className, ...props } = useDefaultProps({ props: inProps, name: 'ESCheckboxIcon' }); - return
; + return ; }; diff --git a/packages/react/src/components/Dialog/DialogClose/DialogClose.tsx b/packages/react/src/components/Dialog/DialogClose/DialogClose.tsx index 26dd53ab5..5ba9b0484 100644 --- a/packages/react/src/components/Dialog/DialogClose/DialogClose.tsx +++ b/packages/react/src/components/Dialog/DialogClose/DialogClose.tsx @@ -30,7 +30,7 @@ export const DialogClose = ({ ref, ...inProps }: DialogCloseProps & RefAttribute
); diff --git a/packages/react/src/components/Divider/Divider.tsx b/packages/react/src/components/Divider/Divider.tsx index b86b6e239..f03fa614d 100644 --- a/packages/react/src/components/Divider/Divider.tsx +++ b/packages/react/src/components/Divider/Divider.tsx @@ -11,8 +11,9 @@ import { useDefaultProps } from '../../theming'; /** * The Divider is a thin line that groups content in lists and layouts. */ -export const Divider = ({ ref, ...inProps }: DividerProps & RefAttributes) => { +export const Divider = ({ ref, ...inProps }: DividerProps & RefAttributes) => { const { + component: Component = 'div', className, style, children, @@ -21,13 +22,14 @@ export const Divider = ({ ref, ...inProps }: DividerProps & RefAttributes {children ? {children} : null} -
+ ); }; diff --git a/packages/react/src/components/Divider/Divider.types.ts b/packages/react/src/components/Divider/Divider.types.ts index 6a2d81eea..ab043340f 100644 --- a/packages/react/src/components/Divider/Divider.types.ts +++ b/packages/react/src/components/Divider/Divider.types.ts @@ -1,8 +1,16 @@ -import { CSSProperties, ReactNode } from 'react'; +import { CSSProperties, ElementType, ReactNode } from 'react'; export interface DividerProps { children?: ReactNode; + /** + * The component used for the root node. + */ + component?: ElementType; + + /** The aria role of the root element. */ + role?: string; + /** Class applied to the root element. */ className?: string; /** Style applied to the root element. */ diff --git a/packages/react/src/components/Dropzone/Dropzone.tsx b/packages/react/src/components/Dropzone/Dropzone.tsx index f24761223..e37957623 100644 --- a/packages/react/src/components/Dropzone/Dropzone.tsx +++ b/packages/react/src/components/Dropzone/Dropzone.tsx @@ -142,20 +142,20 @@ export const Dropzone = (inProps: DropzoneProps): React.JSX.Element => { onDragOver={onDragOver} onDrop={onDropzoneDrop} > -
- {!!icon && !(isDragOverDocument && !!dragHeading) &&
{icon}
} + + {!!icon && !(isDragOverDocument && !!dragHeading) && {icon}} {isDragOverDocument && !!dragHeading ? ( -
{dragHeading}
+ {dragHeading} ) : ( - !!heading &&
{heading}
+ !!heading && {heading} )} -
+ {isDragOverDocument && !!dragSubheading ? ( -
{dragSubheading}
+ {dragSubheading} ) : ( - !!subheading &&
{subheading}
+ !!subheading && {subheading} )} @@ -166,7 +166,7 @@ export const Dropzone = (inProps: DropzoneProps): React.JSX.Element => { )} >; + typography?: Partial>; }; /** * The components used for each slot inside. diff --git a/packages/react/src/components/MenuGroup/MenuGroup.tsx b/packages/react/src/components/MenuGroup/MenuGroup.tsx index 2951be78b..21fd5378d 100644 --- a/packages/react/src/components/MenuGroup/MenuGroup.tsx +++ b/packages/react/src/components/MenuGroup/MenuGroup.tsx @@ -12,7 +12,7 @@ import { useDefaultProps } from '../../theming'; * A heading that labels a group of items inside a dropdown menu. It can stick to the top of the menu while the list * scrolls. */ -export const MenuGroup = ({ ref, ...inProps }: MenuGroupProps & RefAttributes) => { +export const MenuGroup = ({ ref, ...inProps }: MenuGroupProps & RefAttributes) => { const { children, @@ -27,7 +27,7 @@ export const MenuGroup = ({ ref, ...inProps }: MenuGroupProps & RefAttributes {children} - + ); }; + +/** + * The group is a heading, not an item, so `MenuList` must not make it focusable. + */ +MenuGroup.esSkipListHighlight = true; diff --git a/packages/react/src/components/Pagination/PaginationPages/PaginationPages.tsx b/packages/react/src/components/Pagination/PaginationPages/PaginationPages.tsx index a7d591080..7f9b0a8bc 100644 --- a/packages/react/src/components/Pagination/PaginationPages/PaginationPages.tsx +++ b/packages/react/src/components/Pagination/PaginationPages/PaginationPages.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Fragment, memo, RefAttributes, useState } from 'react'; +import { memo, RefAttributes, useState } from 'react'; import { PaginationPagesProps } from './PaginationPages.types'; @@ -111,7 +111,7 @@ export const PaginationPages = memo(function PaginationPages({ } = slotProps?.[item.type as keyof typeof slotProps]?.(item) || {}; return ( - +
  • {item.type === 'previous' || item.type === 'next' ? (
  • ); })} diff --git a/packages/react/src/components/Radio/RadioIcon/RadioIcon.tsx b/packages/react/src/components/Radio/RadioIcon/RadioIcon.tsx index b356840e3..75478af1a 100644 --- a/packages/react/src/components/Radio/RadioIcon/RadioIcon.tsx +++ b/packages/react/src/components/Radio/RadioIcon/RadioIcon.tsx @@ -11,12 +11,12 @@ import { useDefaultProps } from '../../../theming'; /** * @see `Radio` */ -export const RadioIcon = ({ ref, ...inProps }: RadioIconProps & RefAttributes) => { +export const RadioIcon = ({ ref, ...inProps }: RadioIconProps & RefAttributes) => { const { className, style } = useDefaultProps({ props: inProps, name: 'ESRadioIcon' }); return ( -
    -
    -
    + + + ); }; diff --git a/packages/react/src/components/SFS/SFSFilters/SFSFilters.tsx b/packages/react/src/components/SFS/SFSFilters/SFSFilters.tsx index ea0baf0fc..44ad56ba9 100644 --- a/packages/react/src/components/SFS/SFSFilters/SFSFilters.tsx +++ b/packages/react/src/components/SFS/SFSFilters/SFSFilters.tsx @@ -54,14 +54,14 @@ export const SFSFilters = memo(function SFSFilters({ }; return ( -
    +
    {button ? ( button({ open: isOpen, setOpen }) ) : ( -
    {labelButton}
    + {labelButton} {iconFilters} - {!!count &&
    {count}
    } + {!!count && {count}}
    )} diff --git a/packages/react/src/components/SFS/SFSSorting/SFSSorting.tsx b/packages/react/src/components/SFS/SFSSorting/SFSSorting.tsx index 0b403268f..2caf751ae 100644 --- a/packages/react/src/components/SFS/SFSSorting/SFSSorting.tsx +++ b/packages/react/src/components/SFS/SFSSorting/SFSSorting.tsx @@ -50,17 +50,17 @@ export const SFSSorting = memo(function SFSSorting({ return (
    -
    + {values.length === 1 ? options.find((o) => o.value === values[0].value)?.label : labelButton} -
    + {iconSort} {values.length === 1 && ( -
    + {values[0].direction === 'asc' ? iconAsc : iconDesc} -
    + )} {values.length > 1 && ( -
    {values.length}
    + {values.length} )}
    = ({ ref: inR {text} - {!!children && } + {!!children && } {!!children && React.Children.map(children, (child: any, idx: number) => { const { text, inset, onClick, ...rest } = child.props; @@ -263,6 +263,7 @@ export const SidebarItem: OverridableComponent = ({ ref: inR ref={itemRef} button className={clsx('es-sidebar-item__button', `es-sidebar-item__button--color--${color}`)} + {...({ component: 'div' } as any)} onClick={onItemClick} onKeyDown={onItemKeyDown} onTouchStart={onItemTouchStart} diff --git a/packages/react/src/components/Sidenav/SidenavItem/SidenavItem.tsx b/packages/react/src/components/Sidenav/SidenavItem/SidenavItem.tsx index 875ff7791..f7c383242 100644 --- a/packages/react/src/components/Sidenav/SidenavItem/SidenavItem.tsx +++ b/packages/react/src/components/Sidenav/SidenavItem/SidenavItem.tsx @@ -139,6 +139,7 @@ export const SidenavItem: OverridableComponent = ({ ref: inR ref={itemRef} button className={clsx('es-sidenav-item', `es-sidenav-item--color--${color}`, className)} + {...({ component: 'div' } as any)} data-id={id} selected={selected} onClick={onItemClick} diff --git a/packages/react/src/components/Slider/useSlider.ts b/packages/react/src/components/Slider/useSlider.ts index b230a475d..cd2863e20 100644 --- a/packages/react/src/components/Slider/useSlider.ts +++ b/packages/react/src/components/Slider/useSlider.ts @@ -664,8 +664,8 @@ export function useSlider(parameters: UseSliderParameters): UseSliderReturnValue tabIndex, 'aria-labelledby': ariaLabelledby, 'aria-orientation': orientation, - 'aria-valuemax': scale(max), - 'aria-valuemin': scale(min), + 'aria-valuemax': scale(max) === max ? undefined : scale(max), + 'aria-valuemin': scale(min) === min ? undefined : scale(min), name, type: 'range', min: parameters.min, diff --git a/packages/react/src/components/SortingMenu/SortingMenu.tsx b/packages/react/src/components/SortingMenu/SortingMenu.tsx index 9881e1914..2add00fdc 100644 --- a/packages/react/src/components/SortingMenu/SortingMenu.tsx +++ b/packages/react/src/components/SortingMenu/SortingMenu.tsx @@ -229,10 +229,10 @@ export const SortingMenu = memo(function SortingMenu({ ? sortMap[item.value].labelAsc || labelAsc : sortMap[item.value].labelDesc || labelDesc} -
    + {item.direction === 'asc' ? iconItemAsc : iconItemDesc} {isMultiple && {i + 1}} -
    + )} diff --git a/packages/react/src/components/SvgIcon/SvgIcon.tsx b/packages/react/src/components/SvgIcon/SvgIcon.tsx index 6f9b4fca4..f9bdb4692 100644 --- a/packages/react/src/components/SvgIcon/SvgIcon.tsx +++ b/packages/react/src/components/SvgIcon/SvgIcon.tsx @@ -40,7 +40,7 @@ export const SvgIcon = (inProps: SvgIconProps) => { if (container) { return ( -
    { }} > {svg} -
    + ); } diff --git a/packages/react/src/components/SvgIcon/SvgIcon.types.ts b/packages/react/src/components/SvgIcon/SvgIcon.types.ts index fe2318081..fe98fad2a 100644 --- a/packages/react/src/components/SvgIcon/SvgIcon.types.ts +++ b/packages/react/src/components/SvgIcon/SvgIcon.types.ts @@ -30,5 +30,5 @@ export type SvgIconProps = { title?: string; /** Props applied to the container element. */ - ContainerProps?: DetailedHTMLProps, HTMLDivElement>; + ContainerProps?: DetailedHTMLProps, HTMLSpanElement>; } & SVGProps; diff --git a/packages/react/src/components/Swiper/SwiperPaginationItem/SwiperPaginationItem.tsx b/packages/react/src/components/Swiper/SwiperPaginationItem/SwiperPaginationItem.tsx index 6bc40fb74..fe771552e 100644 --- a/packages/react/src/components/Swiper/SwiperPaginationItem/SwiperPaginationItem.tsx +++ b/packages/react/src/components/Swiper/SwiperPaginationItem/SwiperPaginationItem.tsx @@ -49,7 +49,7 @@ export const SwiperPaginationItem = ({ value={index} onChange={onSlideChange} /> -
    -
    +
    diff --git a/packages/react/src/components/TouchRipple/TouchRipple.tsx b/packages/react/src/components/TouchRipple/TouchRipple.tsx index cc4246296..577a946fa 100644 --- a/packages/react/src/components/TouchRipple/TouchRipple.tsx +++ b/packages/react/src/components/TouchRipple/TouchRipple.tsx @@ -11,11 +11,11 @@ import { useDefaultProps } from '../../theming'; /** * The touch ripple for ButtonBase component. */ -export const TouchRipple = ({ ref, ...inProps }: TouchRippleProps & RefAttributes) => { +export const TouchRipple = ({ ref, ...inProps }: TouchRippleProps & RefAttributes) => { const { className, style } = useDefaultProps({ props: inProps, name: 'ESTouchRipple', }); - return
    ; + return ; }; diff --git a/packages/react/src/components/TouchRipple/useTouchRipple.ts b/packages/react/src/components/TouchRipple/useTouchRipple.ts index 25e40d790..d7af948c9 100644 --- a/packages/react/src/components/TouchRipple/useTouchRipple.ts +++ b/packages/react/src/components/TouchRipple/useTouchRipple.ts @@ -71,7 +71,7 @@ export const useTouchRipple = ({ ...rest }: TouchRippleParams) => { - const ref = useRef(null); + const ref = useRef(null); const [pressed, setPressed] = useState(false); diff --git a/packages/theme/lib/components/_breadcrumbs.scss b/packages/theme/lib/components/_breadcrumbs.scss index 0b28bf047..643f5c1e3 100644 --- a/packages/theme/lib/components/_breadcrumbs.scss +++ b/packages/theme/lib/components/_breadcrumbs.scss @@ -74,7 +74,8 @@ } &--disabled { - & div { + & div, + & .es-button-base__wrapper { cursor: not-allowed; overflow: hidden; text-overflow: ellipsis; diff --git a/packages/theme/lib/components/_pagination.scss b/packages/theme/lib/components/_pagination.scss index b0b3c8335..b391cb643 100644 --- a/packages/theme/lib/components/_pagination.scss +++ b/packages/theme/lib/components/_pagination.scss @@ -31,6 +31,11 @@ padding: 0; } + &__item { + align-items: center; + display: flex; + } + &__button { border-radius: 50%; diff --git a/packages/theme/lib/components/_switch.scss b/packages/theme/lib/components/_switch.scss index ff4a67995..761f447c5 100644 --- a/packages/theme/lib/components/_switch.scss +++ b/packages/theme/lib/components/_switch.scss @@ -320,6 +320,7 @@ background-color: var(--es-common-switch); border: 2px solid currentcolor; border-radius: 50%; + display: block; transition: 150ms; transition-property: width, height, border, border-radius; } diff --git a/packages/theme/lib/components/_touch-ripple.scss b/packages/theme/lib/components/_touch-ripple.scss index 898e240a3..619ba6f1b 100644 --- a/packages/theme/lib/components/_touch-ripple.scss +++ b/packages/theme/lib/components/_touch-ripple.scss @@ -1,6 +1,7 @@ @mixin include() { .es-touch-ripple { border-radius: inherit; + display: block; inset: 0; overflow: hidden; pointer-events: none;