diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 78025d0890dc..876180ab74cc 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -3935,6 +3935,16 @@ Map { }, "render": [Function], }, + "IdPrefix" => Object { + "propTypes": Object { + "children": Object { + "type": "node", + }, + "prefix": Object { + "type": "string", + }, + }, + }, "InlineLoading" => Object { "propTypes": Object { "className": Object { @@ -5879,6 +5889,9 @@ Map { ], "type": "oneOf", }, + "readOnly": Object { + "type": "bool", + }, "valueSelected": Object { "args": Array [ Array [ @@ -9984,6 +9997,7 @@ Map { "unstable_useFeatureFlag" => Object {}, "unstable_useFeatureFlags" => Object {}, "unstable_useLayoutDirection" => Object {}, + "useIdPrefix" => Object {}, "useLayer" => Object {}, "usePrefix" => Object {}, "useTheme" => Object {}, diff --git a/packages/react/src/__tests__/index-test.js b/packages/react/src/__tests__/index-test.js index b9c972bbd041..de21c298ff5f 100644 --- a/packages/react/src/__tests__/index-test.js +++ b/packages/react/src/__tests__/index-test.js @@ -87,6 +87,7 @@ describe('Carbon Components React', () => { "IconButton", "IconSkeleton", "IconTab", + "IdPrefix", "InlineLoading", "InlineNotification", "Layer", @@ -248,6 +249,7 @@ describe('Carbon Components React', () => { "unstable_useFeatureFlag", "unstable_useFeatureFlags", "unstable_useLayoutDirection", + "useIdPrefix", "useLayer", "usePrefix", "useTheme", diff --git a/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap b/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap index b1c00a31dcf8..ccf053c71244 100644 --- a/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap +++ b/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap @@ -587,10 +587,10 @@ exports[`DataTable selection -- radio buttons should render 1`] = ` className="cds--radio-button__appearance" /> Select row @@ -664,10 +664,10 @@ exports[`DataTable selection -- radio buttons should render 1`] = ` className="cds--radio-button__appearance" /> Select row @@ -741,10 +741,10 @@ exports[`DataTable selection -- radio buttons should render 1`] = ` className="cds--radio-button__appearance" /> Select row diff --git a/packages/react/src/components/IdPrefix/IdPrefix.mdx b/packages/react/src/components/IdPrefix/IdPrefix.mdx new file mode 100644 index 000000000000..0c46810f3ab3 --- /dev/null +++ b/packages/react/src/components/IdPrefix/IdPrefix.mdx @@ -0,0 +1,56 @@ +import { Story, Props, Source, Preview } from '@storybook/addon-docs/blocks'; +import { IdPrefix } from '../IdPrefix'; + +# Prefix + +[Source code](https://github.com/carbon-design-system/carbon/tree/main/packages/react/src/components/ClassPrefix) + + + + +## Table of Contents + +- [Overview](#overview) +- [Component API](#component-api) +- [Feedback](#feedback) + + + +## Overview + +The `IdPrefix` component is used to change the prefix applied to the +automatically generated `id` attributes placed on certain DOM elements. + + + + + +This component is used intended to be used in limited cases, primarily only if +you have id conflics when using v10 and v11 packages at the same time during +migration. + +In React, you can use `IdPrefix` anywhere in your component tree and specify the +prefix with the `prefix` prop. Most often it's used in the project root wrapping +the entire project: + +```jsx +import { IdPrefix } from '@carbon/react'; + +export default function MyApp() { + return ( + + + + ); +} +``` + +## Component API + + + +## Feedback + +Help us improve this component by providing feedback, asking questions on Slack, +or updating this file on +[GitHub](https://github.com/carbon-design-system/carbon/edit/main/packages/react/src/components/ClassPrefix/ClassPrefix.mdx). diff --git a/packages/react/src/components/IdPrefix/IdPrefix.stories.js b/packages/react/src/components/IdPrefix/IdPrefix.stories.js new file mode 100644 index 000000000000..542daceadb99 --- /dev/null +++ b/packages/react/src/components/IdPrefix/IdPrefix.stories.js @@ -0,0 +1,37 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { IdPrefix } from '.'; +import { useIdPrefix } from '../../internal/useIdPrefix'; +import mdx from './IdPrefix.mdx'; + +export default { + title: 'Components/IdPrefix', + component: IdPrefix, + parameters: { + docs: { + page: mdx, + }, + }, +}; + +export const Default = () => { + function ExampleComponent() { + const idPrefix = useIdPrefix(); + return

The current id prefix is: {idPrefix}

; + } + + return ( + <> + + + + + + ); +}; diff --git a/packages/react/src/components/IdPrefix/__tests__/IdPrefix-test.js b/packages/react/src/components/IdPrefix/__tests__/IdPrefix-test.js new file mode 100644 index 000000000000..74bb90faaeb0 --- /dev/null +++ b/packages/react/src/components/IdPrefix/__tests__/IdPrefix-test.js @@ -0,0 +1,31 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { IdPrefix } from '../../IdPrefix'; +import { useIdPrefix } from '../../../internal/useIdPrefix'; + +describe('IdPrefix', () => { + it('should set the prefix value used by usePrefix', () => { + const calls = []; + + function TestComponent() { + const prefix = useIdPrefix(); + calls.push(prefix); + return null; + } + + render( + + + + ); + + expect(calls).toEqual(['custom']); + }); +}); diff --git a/packages/react/src/components/IdPrefix/index.js b/packages/react/src/components/IdPrefix/index.js new file mode 100644 index 000000000000..da32bdb3a7ec --- /dev/null +++ b/packages/react/src/components/IdPrefix/index.js @@ -0,0 +1,29 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import PropTypes from 'prop-types'; +import React from 'react'; +import { IdPrefixContext } from '../../internal/useIdPrefix'; + +function IdPrefix({ children, prefix }) { + return ( + + {children} + + ); +} + +IdPrefix.propTypes = { + children: PropTypes.node, + + /** + * The value used to prefix the auto-generated id placed on some DOM elements + */ + prefix: PropTypes.string, +}; + +export { IdPrefix }; diff --git a/packages/react/src/components/RadioButton/RadioButton.js b/packages/react/src/components/RadioButton/RadioButton.js index 9f9424334d37..d3725bcf6ae7 100644 --- a/packages/react/src/components/RadioButton/RadioButton.js +++ b/packages/react/src/components/RadioButton/RadioButton.js @@ -35,7 +35,7 @@ const RadioButton = React.forwardRef(function RadioButton( onChange(value, name, event); } - const innerLabelClasses = classNames({ + const innerLabelClasses = classNames(`${prefix}--radio-button__label-text`, { [`${prefix}--visually-hidden`]: hideLabel, }); diff --git a/packages/react/src/components/RadioButton/RadioButton.stories.js b/packages/react/src/components/RadioButton/RadioButton.stories.js index 780a1f5a7836..f5c9ef146f7a 100644 --- a/packages/react/src/components/RadioButton/RadioButton.stories.js +++ b/packages/react/src/components/RadioButton/RadioButton.stories.js @@ -23,6 +23,14 @@ export default { page: mdx, }, }, + argTypes: { + readOnly: { + description: 'Specify whether the RadioButtonGroup is read-only', + control: { + type: 'boolean', + }, + }, + }, }; export const Default = () => { diff --git a/packages/react/src/components/RadioButtonGroup/RadioButtonGroup-test.js b/packages/react/src/components/RadioButtonGroup/RadioButtonGroup-test.js index 5c9060c7ad6c..2227e9967899 100644 --- a/packages/react/src/components/RadioButtonGroup/RadioButtonGroup-test.js +++ b/packages/react/src/components/RadioButtonGroup/RadioButtonGroup-test.js @@ -94,6 +94,31 @@ describe('RadioButtonGroup', () => { expect(fieldset).toBeDisabled(); }); + it('should support readonly to prevent changes', () => { + render( + + + + + ); + + const radio1 = screen.getByLabelText('test-1'); + const radio2 = screen.getByLabelText('test-2'); + + expect(radio1).toBeChecked(); + expect(radio2).not.toBeChecked(); + + userEvent.click(radio2); + + // no change + expect(radio1).toBeChecked(); + expect(radio2).not.toBeChecked(); + }); + it('should support `defaultSelected` as a way to select a radio button', () => { render( {}, orientation = 'horizontal', + readOnly, valueSelected, }, ref @@ -63,9 +66,11 @@ const RadioButtonGroup = React.forwardRef(function RadioButtonGroup( } function handleOnChange(newSelection, value, evt) { - if (newSelection !== selected) { - setSelected(newSelection); - onChange(newSelection, name, evt); + if (!readOnly) { + if (newSelection !== selected) { + setSelected(newSelection); + onChange(newSelection, name, evt); + } } } @@ -73,13 +78,17 @@ const RadioButtonGroup = React.forwardRef(function RadioButtonGroup( [`${prefix}--radio-button-group--${orientation}`]: orientation === 'vertical', [`${prefix}--radio-button-group--label-${labelPosition}`]: labelPosition, + [`${prefix}--radio-button-group--readonly`]: readOnly, }); const wrapperClasses = classNames(`${prefix}--form-item`, className); return (
-
+
{legendText && ( {legendText} )} @@ -137,6 +146,11 @@ RadioButtonGroup.propTypes = { */ orientation: PropTypes.oneOf(['horizontal', 'vertical']), + /** + * Whether the RadioButtonGroup should be read-only + */ + readOnly: PropTypes.bool, + /** * Specify the value that is currently selected in the group */ diff --git a/packages/react/src/index.js b/packages/react/src/index.js index af64c6560312..57f33606e848 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -71,6 +71,7 @@ export FormGroup from './components/FormGroup'; export FormItem from './components/FormItem'; export FormLabel from './components/FormLabel'; export { Grid, Row, Column, ColumnHang, FlexGrid } from './components/Grid'; +export { IdPrefix } from './components/IdPrefix'; export InlineLoading from './components/InlineLoading'; export Link from './components/Link'; export ListItem from './components/ListItem'; @@ -271,3 +272,4 @@ export { export { DefinitionTooltip } from './components/Tooltip/next/DefinitionTooltip'; export { GlobalTheme, Theme, useTheme } from './components/Theme'; export { usePrefix } from './internal/usePrefix'; +export { useIdPrefix } from './internal/useIdPrefix'; diff --git a/packages/react/src/internal/__tests__/useIdPrefix-test.js b/packages/react/src/internal/__tests__/useIdPrefix-test.js new file mode 100644 index 000000000000..0f1b91d3c5bc --- /dev/null +++ b/packages/react/src/internal/__tests__/useIdPrefix-test.js @@ -0,0 +1,41 @@ +/** + * Copyright IBM Corp. 2020 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { cleanup, render } from '@testing-library/react'; +import React from 'react'; +import { useIdPrefix, IdPrefixContext } from '../useIdPrefix'; + +describe('usePrefix', () => { + afterEach(cleanup); + + it('should emit the default prefix without context', () => { + let value = null; + + function TestComponent() { + value = useIdPrefix(); + return null; + } + + render(); + expect(value).toBe(null); + }); + + it('should emit the prefix in context', () => { + function TestComponent() { + const contextValue = useIdPrefix(); + return {contextValue}; + } + + const { getByTestId } = render( + + + + ); + + expect(getByTestId('test')).toHaveTextContent('test'); + }); +}); diff --git a/packages/react/src/internal/__tests__/usePrefix-test.js b/packages/react/src/internal/__tests__/usePrefix-test.js index 4d7afab6278b..a855dd3dda0c 100644 --- a/packages/react/src/internal/__tests__/usePrefix-test.js +++ b/packages/react/src/internal/__tests__/usePrefix-test.js @@ -26,7 +26,8 @@ describe('usePrefix', () => { it('should emit the prefix in context', () => { function TestComponent() { - return test; + const contextValue = usePrefix(); + return {contextValue}; } const { getByTestId } = render( diff --git a/packages/react/src/internal/useId.js b/packages/react/src/internal/useId.js index 483716c0868b..bdd5e977ad77 100644 --- a/packages/react/src/internal/useId.js +++ b/packages/react/src/internal/useId.js @@ -26,6 +26,7 @@ import { useEffect, useLayoutEffect, useState } from 'react'; import setupGetInstanceId from '../tools/setupGetInstanceId'; import { canUseDOM } from './environment'; +import { useIdPrefix } from './useIdPrefix'; const getId = setupGetInstanceId(); const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect; @@ -38,16 +39,18 @@ let serverHandoffCompleted = false; * @returns {string} */ export function useId(prefix = 'id') { + const _prefix = useIdPrefix(); + const [id, setId] = useState(() => { if (serverHandoffCompleted) { - return `${prefix}-${getId()}`; + return `${_prefix ? `${_prefix}-` : ``}${prefix}-${getId()}`; } return null; }); useIsomorphicLayoutEffect(() => { if (id === null) { - setId(`${prefix}-${getId()}`); + setId(`${_prefix ? `${_prefix}-` : ``}${prefix}-${getId()}`); } }, [getId]); diff --git a/packages/react/src/internal/useIdPrefix.js b/packages/react/src/internal/useIdPrefix.js new file mode 100644 index 000000000000..15c603dbb704 --- /dev/null +++ b/packages/react/src/internal/useIdPrefix.js @@ -0,0 +1,14 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; + +export const IdPrefixContext = React.createContext(null); + +export function useIdPrefix() { + return React.useContext(IdPrefixContext); +} diff --git a/packages/styles/scss/components/radio-button/_radio-button.scss b/packages/styles/scss/components/radio-button/_radio-button.scss index eb2bd627a11d..45f43ba08c1e 100644 --- a/packages/styles/scss/components/radio-button/_radio-button.scss +++ b/packages/styles/scss/components/radio-button/_radio-button.scss @@ -135,6 +135,24 @@ $radio-border-width: 1px !default; } } + // readonly + .#{$prefix}--radio-button-group--readonly + .#{$prefix}--radio-button + + .#{$prefix}--radio-button__label + .#{$prefix}--radio-button__appearance { + border-color: $icon-disabled; + } + + .#{$prefix}--radio-button-group--readonly .#{$prefix}--radio-button__label { + cursor: default; + } + + .#{$prefix}--radio-button-group--readonly + .#{$prefix}--radio-button__label-text { + cursor: text; + user-select: text; + } + // Focus .#{$prefix}--radio-button:focus