diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index fb3d438c5ef4..3c08f604347b 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -8498,6 +8498,9 @@ Map { }, "Toggle" => Object { "propTypes": Object { + "aria-labelledby": Object { + "type": "string", + }, "className": Object { "type": "string", }, @@ -8520,10 +8523,7 @@ Map { "labelB": Object { "type": "node", }, - "labelText": Object { - "isRequired": true, - "type": "node", - }, + "labelText": [Function], "onClick": Object { "type": "func", }, diff --git a/packages/react/src/components/Toggle/Toggle-test.js b/packages/react/src/components/Toggle/Toggle-test.js index d652d1019184..8a7ec415028f 100644 --- a/packages/react/src/components/Toggle/Toggle-test.js +++ b/packages/react/src/components/Toggle/Toggle-test.js @@ -88,10 +88,28 @@ describe('Toggle', () => { .classList.contains(`${prefix}--visually-hidden`) ).toBe(true); expect( - wrapper.container.querySelector(`.${prefix}--toggle__label-text`) - .textContent + wrapper.container.querySelector(`.${prefix}--toggle__text`).textContent ).toBe(props.labelText); }); + + it("doesn't render sideLabel if props.hideLabel and props['aria-labelledby'] are provided", () => { + const externalElementId = 'external-element-id'; + wrapper.rerender( + + ); + + expect( + wrapper.container.querySelector(`.${prefix}--toggle__text`) + ).toBeNull(); + + expect(wrapper.getByRole('switch').getAttribute('aria-labelledby')).toBe( + externalElementId + ); + + expect( + wrapper.container.querySelector(`.${prefix}--toggle__label`).tagName + ).toBe('DIV'); + }); }); describe('behaves as expected', () => { diff --git a/packages/react/src/components/Toggle/Toggle.js b/packages/react/src/components/Toggle/Toggle.js index b66c3bbaa12e..1d1072091506 100644 --- a/packages/react/src/components/Toggle/Toggle.js +++ b/packages/react/src/components/Toggle/Toggle.js @@ -12,6 +12,7 @@ import { useControllableState } from '../../internal/useControllableState'; import { usePrefix } from '../../internal/usePrefix'; export function Toggle({ + 'aria-labelledby': ariaLabelledby, className, defaultToggled = false, disabled = false, @@ -45,6 +46,8 @@ export function Toggle({ const isSm = size === 'sm'; const sideLabel = hideLabel ? labelText : checked ? labelB : labelA; + const renderSideLabel = !(hideLabel && ariaLabelledby); + const LabelComponent = ariaLabelledby ? 'div' : 'label'; const wrapperClasses = classNames( `${prefix}--toggle`, @@ -76,11 +79,14 @@ export function Toggle({ role="switch" type="button" aria-checked={checked} + aria-labelledby={ariaLabelledby} disabled={disabled} onClick={handleClick} /> - + ); } Toggle.propTypes = { + /** + * Specify another element's id to be used as the label for this toggle + */ + 'aria-labelledby': PropTypes.string, + /** * Specify a custom className to apply to the form-item node */ @@ -140,9 +153,17 @@ Toggle.propTypes = { /** * Provide the text that will be read by a screen reader when visiting this - * control + * control. This is required unless 'aria-labelledby' is provided instead */ - labelText: PropTypes.node.isRequired, + labelText: (props, ...rest) => { + if (!props['aria-labelledby'] && !props.labelText) { + return new Error( + 'labelText property is required if no aria-labelledby is provided.' + ); + } + + return PropTypes.node(props, ...rest); + }, /** * Provide an event listener that is called when the control is clicked diff --git a/packages/react/src/components/Toggle/Toggle.stories.js b/packages/react/src/components/Toggle/Toggle.stories.js index e017b4e749aa..a7b6a7990f33 100644 --- a/packages/react/src/components/Toggle/Toggle.stories.js +++ b/packages/react/src/components/Toggle/Toggle.stories.js @@ -35,14 +35,7 @@ export const SmallToggle = () => ( ); export const Playground = (args) => ( - + ); Playground.argTypes = {