diff --git a/.yarn/cache/postcss-loader-npm-7.0.0-e0a0c61fcd-b8e51e9989.zip b/.yarn/cache/postcss-loader-npm-7.0.0-e0a0c61fcd-b8e51e9989.zip
new file mode 100644
index 000000000000..944aeea2dac3
Binary files /dev/null and b/.yarn/cache/postcss-loader-npm-7.0.0-e0a0c61fcd-b8e51e9989.zip differ
diff --git a/e2e/components/Notifications/Notifications-test.e2e.js b/e2e/components/Notifications/Notifications-test.e2e.js
index 5b12401a0328..595c41bec074 100644
--- a/e2e/components/Notifications/Notifications-test.e2e.js
+++ b/e2e/components/Notifications/Notifications-test.e2e.js
@@ -16,24 +16,24 @@ test.describe('Notifications', () => {
test.describe(theme, () => {
test('toast @vrt', async ({ page }) => {
await snapshotStory(page, {
- component: 'Notifications',
- id: 'components-notifications--toast',
+ component: 'Toast',
+ id: 'components-notifications-toast--default',
theme,
});
});
test('inline @vrt', async ({ page }) => {
await snapshotStory(page, {
- component: 'Notifications',
- id: 'components-notifications--inline',
+ component: 'Inline',
+ id: 'components-notifications-inline--default',
theme,
});
});
test('actionable @vrt', async ({ page }) => {
await snapshotStory(page, {
- component: 'Notifications',
- id: 'components-notifications--actionable',
+ component: 'Actionable',
+ id: 'components-notifications-actionable--default',
theme,
});
});
@@ -42,8 +42,8 @@ test.describe('Notifications', () => {
test('accessibility-checker @avt', async ({ page }) => {
await visitStory(page, {
- component: 'Notifications',
- id: 'components-notifications--toast',
+ component: 'Toast',
+ id: 'components-notifications-toast--default',
globals: {
theme: 'white',
},
diff --git a/packages/grid/scss/_css-grid.scss b/packages/grid/scss/_css-grid.scss
index bfb584b61ecd..471b4903ba20 100644
--- a/packages/grid/scss/_css-grid.scss
+++ b/packages/grid/scss/_css-grid.scss
@@ -247,21 +247,21 @@
$span: $columns * 0.75;
--cds-grid-columns: #{$span};
- grid-column: span #{$span} / span #{$span};
+ grid-column: span list.slash($span, span) #{$span};
}
.#{$prefix}--#{$name}\:col-span-50 {
$span: $columns * 0.5;
--cds-grid-columns: #{$span};
- grid-column: span #{$span} / span #{$span};
+ grid-column: span list.slash($span, span) #{$span};
}
.#{$prefix}--#{$name}\:col-span-25 {
$span: $columns * 0.25;
--cds-grid-columns: #{$span};
- grid-column: span #{$span} / span #{$span};
+ grid-column: span list.slash($span, span) #{$span};
}
} @else {
@include breakpoint($name) {
@@ -279,21 +279,21 @@
$span: $columns * 0.75;
--cds-grid-columns: #{$span};
- grid-column: span #{$span} / span #{$span};
+ grid-column: span list.slash($span, span) #{$span};
}
.#{$prefix}--#{$name}\:col-span-50 {
$span: $columns * 0.5;
--cds-grid-columns: #{$span};
- grid-column: span #{$span} / span #{$span};
+ grid-column: span list.slash($span, span) #{$span};
}
.#{$prefix}--#{$name}\:col-span-25 {
$span: $columns * 0.25;
--cds-grid-columns: #{$span};
- grid-column: span #{$span} / span #{$span};
+ grid-column: span list.slash($span, span) #{$span};
}
}
}
@@ -426,7 +426,7 @@
@if is-smallest-breakpoint($key, $breakpoints) {
--cds-grid-columns: #{$span};
- grid-column: span #{$span} / span #{$span};
+ grid-column: span list.slash($span, span) #{$span};
} @else {
$previous-breakpoint: breakpoint-prev($key, $breakpoints);
$previous-column-count: get-column-count(
@@ -439,7 +439,7 @@
@include breakpoint($key) {
--cds-grid-columns: #{$span};
- grid-column: span #{$span} / span #{$span};
+ grid-column: span list.slash($span, span) #{$span};
}
}
}
diff --git a/packages/grid/scss/_flex-grid.scss b/packages/grid/scss/_flex-grid.scss
index f9230c4a60bf..085c70f29d0f 100644
--- a/packages/grid/scss/_flex-grid.scss
+++ b/packages/grid/scss/_flex-grid.scss
@@ -70,13 +70,8 @@
// Add a `max-width` to ensure content within each column does not blow out
// the width of the column. Applies to IE10+ and Firefox. Chrome and Safari
// do not appear to require this.
- @if meta.function-exists('div', 'math') {
- max-width: math.percentage(math.div($span, $columns));
- flex: 0 0 math.percentage(math.div($span, $columns));
- } @else {
- max-width: math.percentage(($span / $columns));
- flex: 0 0 math.percentage(($span / $columns));
- }
+ max-width: math.percentage(math.div($span, $columns));
+ flex: 0 0 math.percentage(math.div($span, $columns));
}
}
@@ -87,11 +82,7 @@
/// @group @carbon/grid
@mixin -make-col-offset($span, $columns) {
$offset: 0;
- @if meta.function-exists('div', 'math') {
- $offset: math.div($span, $columns);
- } @else {
- $offset: ($span / $columns);
- }
+ $offset: math.div($span, $columns);
@if $offset == 0 {
margin-left: 0;
} @else {
diff --git a/packages/grid/scss/_mixins.scss b/packages/grid/scss/_mixins.scss
index d507e20df5c2..3ff2861dd3b4 100644
--- a/packages/grid/scss/_mixins.scss
+++ b/packages/grid/scss/_mixins.scss
@@ -64,13 +64,8 @@
// Add a `max-width` to ensure content within each column does not blow out
// the width of the column. Applies to IE10+ and Firefox. Chrome and Safari
// do not appear to require this.
- @if meta.function-exists('div', 'math') {
- max-width: math.percentage(math.div($span, $columns));
- flex: 0 0 math.percentage(math.div($span, $columns));
- } @else {
- max-width: math.percentage(($span / $columns));
- flex: 0 0 math.percentage(($span / $columns));
- }
+ max-width: math.percentage(math.div($span, $columns));
+ flex: 0 0 math.percentage(math.div($span, $columns));
}
}
@@ -81,11 +76,7 @@
/// @group @carbon/grid
@mixin -make-col-offset($span, $columns) {
$offset: 0;
- @if meta.function-exists('div', 'math') {
- $offset: math.div($span, $columns);
- } @else {
- $offset: ($span / $columns);
- }
+ $offset: math.div($span, $columns);
@if $offset == 0 {
margin-left: 0;
} @else {
diff --git a/packages/layout/scss/_convert.import.scss b/packages/layout/scss/_convert.import.scss
deleted file mode 100644
index 3ddfe00c476c..000000000000
--- a/packages/layout/scss/_convert.import.scss
+++ /dev/null
@@ -1,63 +0,0 @@
-//
-// Copyright IBM Corp. 2018, 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.
-//
-//-------------------------------------------
-// Compatibility notes (*.import.scss)
-// ------------------------------------------
-//
-// This file is intended to be consumed and processed with dart-sass.
-// It is incompatible with node-sass/libsass as it contains sass language features
-// or functions that are unavailable in node-sass/libsass, such as `math.div`.
-//
-// The non-`.import` suffixed version of this file eg. `_filename.scss`
-// is intended to be compatible with node-sass/libsass.
-//
-// Styles authored within this file must be duplicated to the corresponding
-// compatibility file to ensure we continue to support node-sass and dart-sass
-// in v10.
-
-@use 'sass:meta';
-@use 'sass:math';
-
-/// Default font size
-/// @type Number
-/// @access public
-/// @group @carbon/layout
-$carbon--base-font-size: 16px !default;
-
-/// Convert a given px unit to a rem unit
-/// @param {Number} $px - Number with px unit
-/// @return {Number} Number with rem unit
-/// @access public
-/// @group @carbon/layout
-@function carbon--rem($px) {
- @if unit($px) != 'px' {
- @error "Expected argument $px to be of type `px`, instead received: `#{unit($px)}`";
- }
-
- @if meta.function-exists('div', 'math') {
- @return math.div($px, $carbon--base-font-size) * 1rem;
- } @else {
- @return ($px / $carbon--base-font-size) * 1rem;
- }
-}
-
-/// Convert a given px unit to a em unit
-/// @param {Number} $px - Number with px unit
-/// @return {Number} Number with em unit
-/// @access public
-/// @group @carbon/layout
-@function carbon--em($px) {
- @if unit($px) != 'px' {
- @error "Expected argument $px to be of type `px`, instead received: `#{unit($px)}`";
- }
-
- @if meta.function-exists('div', 'math') {
- @return math.div($px, $carbon--base-font-size) * 1em;
- } @else {
- @return ($px / $carbon--base-font-size) * 1em;
- }
-}
diff --git a/packages/layout/scss/_convert.scss b/packages/layout/scss/_convert.scss
index c9e5313ffe94..40a24408f582 100644
--- a/packages/layout/scss/_convert.scss
+++ b/packages/layout/scss/_convert.scss
@@ -24,11 +24,7 @@ $base-font-size: 16px !default;
@error "Expected argument $px to be of type `px`, instead received: `#{unit($px)}`";
}
- @if meta.function-exists('div', 'math') {
- @return math.div($px, $base-font-size) * 1rem;
- } @else {
- @return ($px / $base-font-size) * 1rem;
- }
+ @return math.div($px, $base-font-size) * 1rem;
}
/// Convert a given px unit to a em unit
@@ -41,9 +37,5 @@ $base-font-size: 16px !default;
@error "Expected argument $px to be of type `px`, instead received: `#{unit($px)}`";
}
- @if meta.function-exists('div', 'math') {
- @return math.div($px, $base-font-size) * 1em;
- } @else {
- @return ($px / $base-font-size) * 1em;
- }
+ @return math.div($px, $base-font-size) * 1em;
}
diff --git a/packages/layout/scss/modules/_convert.scss b/packages/layout/scss/modules/_convert.scss
index c9e5313ffe94..40a24408f582 100644
--- a/packages/layout/scss/modules/_convert.scss
+++ b/packages/layout/scss/modules/_convert.scss
@@ -24,11 +24,7 @@ $base-font-size: 16px !default;
@error "Expected argument $px to be of type `px`, instead received: `#{unit($px)}`";
}
- @if meta.function-exists('div', 'math') {
- @return math.div($px, $base-font-size) * 1rem;
- } @else {
- @return ($px / $base-font-size) * 1rem;
- }
+ @return math.div($px, $base-font-size) * 1rem;
}
/// Convert a given px unit to a em unit
@@ -41,9 +37,5 @@ $base-font-size: 16px !default;
@error "Expected argument $px to be of type `px`, instead received: `#{unit($px)}`";
}
- @if meta.function-exists('div', 'math') {
- @return math.div($px, $base-font-size) * 1em;
- } @else {
- @return ($px / $base-font-size) * 1em;
- }
+ @return math.div($px, $base-font-size) * 1em;
}
diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
index f983c2520855..dc154b6235a1 100644
--- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
+++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
@@ -91,8 +91,83 @@ Map {
},
},
"ActionableNotification" => Object {
- "$$typeof": Symbol(react.forward_ref),
- "render": [Function],
+ "defaultProps": Object {
+ "closeOnEscape": true,
+ "hasFocus": true,
+ "hideCloseButton": false,
+ "inline": false,
+ "kind": "error",
+ "onCloseButtonClick": [Function],
+ "role": "alertdialog",
+ },
+ "propTypes": Object {
+ "actionButtonLabel": Object {
+ "isRequired": true,
+ "type": "string",
+ },
+ "ariaLabel": Object {
+ "type": "string",
+ },
+ "caption": Object {
+ "type": "string",
+ },
+ "children": Object {
+ "type": "node",
+ },
+ "className": Object {
+ "type": "string",
+ },
+ "closeOnEscape": Object {
+ "type": "bool",
+ },
+ "hasFocus": Object {
+ "type": "bool",
+ },
+ "hideCloseButton": Object {
+ "type": "bool",
+ },
+ "inline": Object {
+ "type": "bool",
+ },
+ "kind": Object {
+ "args": Array [
+ Array [
+ "error",
+ "info",
+ "info-square",
+ "success",
+ "warning",
+ "warning-alt",
+ ],
+ ],
+ "isRequired": true,
+ "type": "oneOf",
+ },
+ "lowContrast": Object {
+ "type": "bool",
+ },
+ "onActionButtonClick": Object {
+ "type": "func",
+ },
+ "onClose": Object {
+ "type": "func",
+ },
+ "onCloseButtonClick": Object {
+ "type": "func",
+ },
+ "role": Object {
+ "type": "string",
+ },
+ "statusIconDescription": Object {
+ "type": "string",
+ },
+ "subtitle": Object {
+ "type": "string",
+ },
+ "title": Object {
+ "type": "string",
+ },
+ },
},
"AspectRatio" => Object {
"propTypes": Object {
@@ -1154,9 +1229,7 @@ Map {
"render": [Function],
},
"ComposedModal" => Object {
- "contextType": Object {
- "$$typeof": Symbol(react.context),
- },
+ "$$typeof": Symbol(react.forward_ref),
"defaultProps": Object {
"onKeyDown": [Function],
"selectorPrimaryFocus": "[data-modal-primary-focus]",
@@ -1196,12 +1269,7 @@ Map {
"type": "string",
},
"selectorsFloatingMenus": Object {
- "args": Array [
- Object {
- "type": "string",
- },
- ],
- "type": "arrayOf",
+ "type": "string",
},
"size": Object {
"args": Array [
@@ -1215,6 +1283,7 @@ Map {
"type": "oneOf",
},
},
+ "render": [Function],
},
"Content" => Object {
"defaultProps": Object {
@@ -3694,8 +3763,64 @@ Map {
},
},
"InlineNotification" => Object {
- "$$typeof": Symbol(react.forward_ref),
- "render": [Function],
+ "defaultProps": Object {
+ "hideCloseButton": false,
+ "kind": "error",
+ "onCloseButtonClick": [Function],
+ "role": "status",
+ },
+ "propTypes": Object {
+ "children": Object {
+ "type": "node",
+ },
+ "className": Object {
+ "type": "string",
+ },
+ "hideCloseButton": Object {
+ "type": "bool",
+ },
+ "kind": Object {
+ "args": Array [
+ Array [
+ "error",
+ "info",
+ "info-square",
+ "success",
+ "warning",
+ "warning-alt",
+ ],
+ ],
+ "type": "oneOf",
+ },
+ "lowContrast": Object {
+ "type": "bool",
+ },
+ "onClose": Object {
+ "type": "func",
+ },
+ "onCloseButtonClick": Object {
+ "type": "func",
+ },
+ "role": Object {
+ "args": Array [
+ Array [
+ "alert",
+ "log",
+ "status",
+ ],
+ ],
+ "type": "oneOf",
+ },
+ "statusIconDescription": Object {
+ "type": "string",
+ },
+ "subtitle": Object {
+ "type": "string",
+ },
+ "title": Object {
+ "type": "string",
+ },
+ },
},
"Layer" => Object {
"propTypes": Object {
@@ -4157,9 +4282,7 @@ Map {
"MultiSelect" => Object {
"$$typeof": Symbol(react.forward_ref),
"Filterable": Object {
- "contextType": Object {
- "$$typeof": Symbol(react.context),
- },
+ "$$typeof": Symbol(react.forward_ref),
"defaultProps": Object {
"ariaLabel": "Choose an item",
"compareItems": [Function],
@@ -4362,7 +4485,6 @@ Map {
"type": "bool",
},
"placeholder": Object {
- "isRequired": true,
"type": "string",
},
"selectionFeedback": Object {
@@ -4402,6 +4524,7 @@ Map {
"type": "node",
},
},
+ "render": [Function],
},
"defaultProps": Object {
"clearSelectionDescription": "Total items selected: ",
@@ -4666,16 +4789,83 @@ Map {
"render": [Function],
},
"NotificationActionButton" => Object {
- "$$typeof": Symbol(react.forward_ref),
- "render": [Function],
+ "propTypes": Object {
+ "children": Object {
+ "type": "node",
+ },
+ "className": Object {
+ "type": "string",
+ },
+ "inline": Object {
+ "type": "bool",
+ },
+ "onClick": Object {
+ "type": "func",
+ },
+ },
},
"NotificationButton" => Object {
- "$$typeof": Symbol(react.forward_ref),
- "render": [Function],
- },
- "NotificationTextDetails" => Object {
- "$$typeof": Symbol(react.forward_ref),
- "render": [Function],
+ "defaultProps": Object {
+ "ariaLabel": "close notification",
+ "notificationType": "toast",
+ "renderIcon": Object {
+ "$$typeof": Symbol(react.forward_ref),
+ "propTypes": Object {
+ "size": Object {
+ "args": Array [
+ Array [
+ Object {
+ "type": "number",
+ },
+ Object {
+ "type": "string",
+ },
+ ],
+ ],
+ "type": "oneOfType",
+ },
+ },
+ "render": [Function],
+ },
+ "type": "button",
+ },
+ "propTypes": Object {
+ "ariaLabel": Object {
+ "type": "string",
+ },
+ "className": Object {
+ "type": "string",
+ },
+ "name": Object {
+ "type": "string",
+ },
+ "notificationType": Object {
+ "args": Array [
+ Array [
+ "toast",
+ "inline",
+ "actionable",
+ ],
+ ],
+ "type": "oneOf",
+ },
+ "renderIcon": Object {
+ "args": Array [
+ Array [
+ Object {
+ "type": "func",
+ },
+ Object {
+ "type": "object",
+ },
+ ],
+ ],
+ "type": "oneOfType",
+ },
+ "type": Object {
+ "type": "string",
+ },
+ },
},
"NumberInput" => Object {
"$$typeof": Symbol(react.forward_ref),
@@ -5326,7 +5516,6 @@ Map {
},
],
],
- "isRequired": true,
"type": "oneOfType",
},
},
@@ -7827,8 +8016,74 @@ Map {
"render": [Function],
},
"ToastNotification" => Object {
- "$$typeof": Symbol(react.forward_ref),
- "render": [Function],
+ "defaultProps": Object {
+ "hideCloseButton": false,
+ "kind": "error",
+ "onCloseButtonClick": [Function],
+ "role": "status",
+ "timeout": 0,
+ },
+ "propTypes": Object {
+ "ariaLabel": Object {
+ "type": "string",
+ },
+ "caption": Object {
+ "type": "string",
+ },
+ "children": Object {
+ "type": "node",
+ },
+ "className": Object {
+ "type": "string",
+ },
+ "hideCloseButton": Object {
+ "type": "bool",
+ },
+ "kind": Object {
+ "args": Array [
+ Array [
+ "error",
+ "info",
+ "info-square",
+ "success",
+ "warning",
+ "warning-alt",
+ ],
+ ],
+ "type": "oneOf",
+ },
+ "lowContrast": Object {
+ "type": "bool",
+ },
+ "onClose": Object {
+ "type": "func",
+ },
+ "onCloseButtonClick": Object {
+ "type": "func",
+ },
+ "role": Object {
+ "args": Array [
+ Array [
+ "alert",
+ "log",
+ "status",
+ ],
+ ],
+ "type": "oneOf",
+ },
+ "statusIconDescription": Object {
+ "type": "string",
+ },
+ "subtitle": Object {
+ "type": "string",
+ },
+ "timeout": Object {
+ "type": "number",
+ },
+ "title": Object {
+ "type": "string",
+ },
+ },
},
"Toggle" => Object {
"propTypes": Object {
diff --git a/packages/react/package.json b/packages/react/package.json
index 45942119174a..bba6d02578d9 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -100,7 +100,7 @@
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.5",
"postcss": "^8.4.5",
- "postcss-loader": "^6.2.1",
+ "postcss-loader": "^7.0.0",
"process": "^0.11.10",
"prop-types": "^15.7.2",
"react": "^17.0.2",
diff --git a/packages/react/src/__tests__/index-test.js b/packages/react/src/__tests__/index-test.js
index 7bd977da856a..48fc1f658f1d 100644
--- a/packages/react/src/__tests__/index-test.js
+++ b/packages/react/src/__tests__/index-test.js
@@ -99,7 +99,6 @@ describe('Carbon Components React', () => {
"MultiSelect",
"NotificationActionButton",
"NotificationButton",
- "NotificationTextDetails",
"NumberInput",
"NumberInputSkeleton",
"OrderedList",
diff --git a/packages/react/src/components/ComposedModal/ComposedModal-test.js b/packages/react/src/components/ComposedModal/ComposedModal-test.js
index 3881d8cbbbcf..cf0343c1a053 100644
--- a/packages/react/src/components/ComposedModal/ComposedModal-test.js
+++ b/packages/react/src/components/ComposedModal/ComposedModal-test.js
@@ -8,7 +8,8 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import Button from '../Button';
-import ComposedModal, {
+import {
+ ComposedModal,
ModalHeader,
ModalBody,
ModalFooter,
diff --git a/packages/react/src/components/ComposedModal/index.js b/packages/react/src/components/ComposedModal/index.js
index ab0019e7c102..1b483e9f4773 100644
--- a/packages/react/src/components/ComposedModal/index.js
+++ b/packages/react/src/components/ComposedModal/index.js
@@ -29,4 +29,3 @@ export const ComposedModal = FeatureFlags.enabled('enable-v11-release')
: ComposedModalClassic;
export { ModalBody };
-export default from './ComposedModal';
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 a4470d90c119..550867deb6e3 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
@@ -112,13 +112,13 @@ exports[`DataTable selection -- radio buttons should not have select-all checkbo
>
DataTable with selection
@@ -556,7 +556,7 @@ exports[`DataTable selection -- radio buttons should render 1`] = `
-
-
-
+
-
+ Select row
+
+
+
+
+
|
-
-
-
+
-
-
-
-
- Select row
-
-
-
-
-
-
+ Select row
+
+
+
+
+
-
-
-
+
-
-
-
-
- Select row
-
-
-
-
-
-
+ Select row
+
+
+
+
+
DataTable with toolbar
diff --git a/packages/react/src/components/MultiSelect/FilterableMultiSelect.js b/packages/react/src/components/MultiSelect/FilterableMultiSelect.js
index 1bb38db8d002..4454f3ab139a 100644
--- a/packages/react/src/components/MultiSelect/FilterableMultiSelect.js
+++ b/packages/react/src/components/MultiSelect/FilterableMultiSelect.js
@@ -10,7 +10,7 @@ import cx from 'classnames';
import Downshift from 'downshift';
import isEqual from 'lodash.isequal';
import PropTypes from 'prop-types';
-import React from 'react';
+import React, { useState, useRef } from 'react';
import { defaultFilterItems } from '../ComboBox/tools/filter';
import { sortingPropTypes } from './MultiSelectPropTypes';
import ListBox, { PropTypes as ListBoxPropTypes } from '../ListBox';
@@ -19,221 +19,127 @@ import { match, keys } from '../../internal/keyboard';
import Selection from '../../internal/Selection';
import { defaultItemToString } from './tools/itemToString';
import mergeRefs from '../../tools/mergeRefs';
-import setupGetInstanceId from '../../tools/setupGetInstanceId';
+import { useId } from '../../internal/useId';
import { defaultSortItems, defaultCompareItems } from './tools/sorting';
-import { FeatureFlagContext } from '../FeatureFlags';
-import { PrefixContext } from '../../internal/usePrefix';
-
-const getInstanceId = setupGetInstanceId();
-
-export default class FilterableMultiSelect extends React.Component {
- static propTypes = {
- /**
- * 'aria-label' of the ListBox component.
- */
- ariaLabel: PropTypes.string,
-
- /**
- * Specify the direction of the multiselect dropdown. Can be either top or bottom.
- */
- direction: PropTypes.oneOf(['top', 'bottom']),
-
- /**
- * Disable the control
- */
- disabled: PropTypes.bool,
-
- /**
- * Additional props passed to Downshift
- */
- downshiftProps: PropTypes.shape(Downshift.propTypes),
-
- /**
- * Specify whether the title text should be hidden or not
- */
- hideLabel: PropTypes.bool,
-
- /**
- * Specify a custom `id`
- */
- id: PropTypes.string.isRequired,
-
- /**
- * Allow users to pass in arbitrary items from their collection that are
- * pre-selected
- */
- initialSelectedItems: PropTypes.array,
-
- /**
- * Is the current selection invalid?
- */
- invalid: PropTypes.bool,
-
- /**
- * If invalid, what is the error?
- */
- invalidText: PropTypes.node,
-
- /**
- * Function to render items as custom components instead of strings.
- * Defaults to null and is overridden by a getter
- */
- itemToElement: PropTypes.func,
-
- /**
- * Helper function passed to downshift that allows the library to render a
- * given item to a string label. By default, it extracts the `label` field
- * from a given item to serve as the item label in the list.
- */
- itemToString: PropTypes.func,
-
- /**
- * We try to stay as generic as possible here to allow individuals to pass
- * in a collection of whatever kind of data structure they prefer
- */
- items: PropTypes.array.isRequired,
-
- /**
- * `true` to use the light version.
- */
- light: PropTypes.bool,
-
- /**
- * Specify the locale of the control. Used for the default `compareItems`
- * used for sorting the list of items in the control.
- */
- locale: PropTypes.string,
-
- /**
- * `onChange` is a utility for this controlled component to communicate to a
- * consuming component what kind of internal state changes are occurring.
- */
- onChange: PropTypes.func,
-
- /**
- * `onInputValueChange` is a utility for this controlled component to communicate to
- * the currently typed input.
- */
- onInputValueChange: PropTypes.func,
-
- /**
- * `onMenuChange` is a utility for this controlled component to communicate to a
- * consuming component that the menu was opened(`true`)/closed(`false`).
- */
- onMenuChange: PropTypes.func,
-
- /**
- * Initialize the component with an open(`true`)/closed(`false`) menu.
- */
- open: PropTypes.bool,
-
- /**
- * Generic `placeholder` that will be used as the textual representation of
- * what this field is for
- */
- placeholder: PropTypes.string.isRequired,
-
- /**
- * Specify feedback (mode) of the selection.
- * `top`: selected item jumps to top
- * `fixed`: selected item stays at it's position
- * `top-after-reopen`: selected item jump to top after reopen dropdown
- */
- selectionFeedback: PropTypes.oneOf(['top', 'fixed', 'top-after-reopen']),
-
- /**
- * Specify the size of the ListBox. Currently supports either `sm`, `md` or `lg` as an option.
- */
- size: ListBoxPropTypes.ListBoxSize,
-
- ...sortingPropTypes,
-
- /**
- * Callback function for translating ListBoxMenuIcon SVG title
- */
- translateWithId: PropTypes.func,
-
- /**
- * Specify title to show title on hover
- */
- useTitleInItem: PropTypes.bool,
-
- /**
- * Specify whether the control is currently in warning state
- */
- warn: PropTypes.bool,
-
- /**
- * Provide the text that is displayed when the control is in warning state
- */
- warnText: PropTypes.node,
- };
-
- static contextType = FeatureFlagContext;
-
- static getDerivedStateFromProps({ open }, state) {
- /**
- * programmatically control this `open` prop
- */
- const { prevOpen } = state;
- return prevOpen === open
- ? null
- : {
- isOpen: open,
- prevOpen: open,
- };
+import { useFeatureFlag } from '../FeatureFlags';
+import { usePrefix } from '../../internal/usePrefix';
+
+const FilterableMultiSelect = React.forwardRef(function FilterableMultiSelect(
+ {
+ ariaLabel,
+ className: containerClassName,
+ compareItems,
+ direction,
+ disabled,
+ downshiftProps,
+ filterItems,
+ helperText,
+ hideLabel,
+ id,
+ initialSelectedItems,
+ invalid,
+ invalidText,
+ items,
+ itemToElement: ItemToElement, // needs to be capitalized for react to render it correctly
+ itemToString,
+ light,
+ locale,
+ onInputValueChange,
+ open,
+ onChange,
+ onMenuChange,
+ placeholder,
+ titleText,
+ type,
+ selectionFeedback,
+ size,
+ sortItems,
+ translateWithId,
+ useTitleInItem,
+ warn,
+ warnText,
+ },
+ ref
+) {
+ const [isOpen, setIsOpen] = useState(open);
+ const [prevOpen, setPrevOpen] = useState(open);
+ const [inputValue, setInputValue] = useState('');
+ const [topItems, setTopItems] = useState([]);
+ const [inputFocused, setInputFocused] = useState(false);
+ const [highlightedIndex, setHighlightedIndex] = useState(null);
+ const textInput = useRef();
+ const filterableMultiSelectInstanceId = useId();
+
+ const enabled = useFeatureFlag('enable-v11-release');
+ const prefix = usePrefix();
+
+ if (prevOpen !== open) {
+ setIsOpen(open);
+ setPrevOpen(open);
}
- static defaultProps = {
- ariaLabel: 'Choose an item',
- compareItems: defaultCompareItems,
- direction: 'bottom',
- disabled: false,
- filterItems: defaultFilterItems,
- initialSelectedItems: [],
- itemToString: defaultItemToString,
- locale: 'en',
- sortItems: defaultSortItems,
- light: false,
- open: false,
- selectionFeedback: 'top-after-reopen',
- };
-
- constructor(props) {
- super(props);
- this.filterableMultiSelectInstanceId = getInstanceId();
- this.state = {
- isOpen: props.open,
- inputValue: '',
- topItems: [],
- inputFocused: false,
- highlightedIndex: null,
- };
- this.textInput = React.createRef();
+ const inline = type === 'inline';
+ const showWarning = !invalid && warn;
+
+ const wrapperClasses = cx(
+ `${prefix}--multi-select__wrapper`,
+ `${prefix}--list-box__wrapper`,
+ [enabled ? containerClassName : null],
+ {
+ [`${prefix}--multi-select__wrapper--inline`]: inline,
+ [`${prefix}--list-box__wrapper--inline`]: inline,
+ [`${prefix}--multi-select__wrapper--inline--invalid`]: inline && invalid,
+ [`${prefix}--list-box__wrapper--inline--invalid`]: inline && invalid,
+ [`${prefix}--list-box--up`]: direction === 'top',
+ }
+ );
+ const helperId = !helperText
+ ? undefined
+ : `filterablemultiselect-helper-text-${filterableMultiSelectInstanceId}`;
+ const labelId = `${id}-label`;
+ const titleClasses = cx({
+ [`${prefix}--label`]: true,
+ [`${prefix}--label--disabled`]: disabled,
+ [`${prefix}--visually-hidden`]: hideLabel,
+ });
+ const helperClasses = cx({
+ [`${prefix}--form__helper-text`]: true,
+ [`${prefix}--form__helper-text--disabled`]: disabled,
+ });
+ const inputClasses = cx({
+ [`${prefix}--text-input`]: true,
+ [`${prefix}--text-input--empty`]: !inputValue,
+ [`${prefix}--text-input--light`]: light,
+ });
+ const helper = helperText ? (
+
+ {helperText}
+
+ ) : null;
+ const menuId = `${id}__menu`;
+ const inputId = `${id}-input`;
+
+ function handleOnChange(changes) {
+ if (onChange) {
+ onChange(changes);
+ }
}
- handleOnChange = (changes) => {
- if (this.props.onChange) {
- this.props.onChange(changes);
+ function handleOnMenuChange(forceIsOpen) {
+ const nextIsOpen = forceIsOpen ?? !isOpen;
+ setIsOpen(nextIsOpen);
+ if (onMenuChange) {
+ onMenuChange(nextIsOpen);
}
- };
-
- handleOnMenuChange = (isOpen) => {
- this.setState((state) => ({
- isOpen: isOpen ?? !state.isOpen,
- }));
- if (this.props.onMenuChange) {
- this.props.onMenuChange(isOpen);
- }
- };
+ }
- handleOnOuterClick = () => {
- this.handleOnMenuChange(false);
- };
+ function handleOnOuterClick() {
+ handleOnMenuChange(false);
+ }
- handleOnStateChange = (changes, downshift) => {
- if (changes.isOpen && !this.state.isOpen) {
- this.setState({ topItems: downshift.selectedItem });
+ function handleOnStateChange(changes, downshift) {
+ if (changes.isOpen && !isOpen) {
+ setTopItems(downshift.selectedItem);
}
const { type } = changes;
@@ -244,397 +150,451 @@ export default class FilterableMultiSelect extends React.Component {
case stateChangeTypes.keyDownArrowUp:
case stateChangeTypes.keyDownHome:
case stateChangeTypes.keyDownEnd:
- this.setState({
- highlightedIndex:
- changes.highlightedIndex !== undefined
- ? changes.highlightedIndex
- : null,
- });
- if (stateChangeTypes.keyDownArrowDown === type && !this.state.isOpen) {
- this.handleOnMenuChange(true);
+ setHighlightedIndex(
+ changes.highlightedIndex !== undefined
+ ? changes.highlightedIndex
+ : null
+ );
+ if (stateChangeTypes.keyDownArrowDown === type && !isOpen) {
+ handleOnMenuChange(true);
}
break;
case stateChangeTypes.keyDownEscape:
- this.handleOnMenuChange(false);
+ handleOnMenuChange(false);
break;
}
- };
-
- handleOnInputKeyDown = (event) => {
- event.stopPropagation();
- };
+ }
- handleOnInputValueChange = (inputValue, { type }) => {
- if (this.props.onInputValueChange) {
- this.props.onInputValueChange(inputValue);
+ function handleOnInputValueChange(inputValue, { type }) {
+ if (onInputValueChange) {
+ onInputValueChange(inputValue);
}
if (type !== Downshift.stateChangeTypes.changeInput) {
return;
}
- this.setState(() => {
- if (Array.isArray(inputValue)) {
- return {
- inputValue: '',
- };
- }
- return {
- inputValue: inputValue || '',
- };
- });
-
- if (inputValue && !this.state.isOpen) {
- this.handleOnMenuChange(true);
- } else if (!inputValue && this.state.isOpen) {
- this.handleOnMenuChange(false);
+ if (Array.isArray(inputValue)) {
+ clearInputValue();
+ } else {
+ setInputValue(inputValue);
}
- };
-
- clearInputValue = () => {
- this.setState({ inputValue: '' }, () => {
- if (this.textInput.current) {
- this.textInput.current.focus();
- }
- });
- };
-
- render() {
- const { highlightedIndex, isOpen, inputValue } = this.state;
- const {
- ariaLabel,
- className: containerClassName,
- direction,
- disabled,
- filterItems,
- items,
- itemToElement,
- itemToString,
- titleText,
- hideLabel,
- helperText,
- type,
- initialSelectedItems,
- id,
- locale,
- size,
- placeholder,
- sortItems,
- compareItems,
- light,
- invalid,
- invalidText,
- warn,
- warnText,
- useTitleInItem,
- translateWithId,
- downshiftProps,
- } = this.props;
- const inline = type === 'inline';
- const showWarning = !invalid && warn;
-
- // needs to be capitalized for react to render it correctly
- const ItemToElement = itemToElement;
-
- const scope = this.context;
- let enabled;
-
- if (scope.enabled) {
- enabled = scope.enabled('enable-v11-release');
+
+ if (inputValue && !isOpen) {
+ handleOnMenuChange(true);
+ } else if (!inputValue && isOpen) {
+ handleOnMenuChange(false);
}
+ }
- return (
-
- {(prefix) => {
- const wrapperClasses = cx(
- `${prefix}--multi-select__wrapper`,
- `${prefix}--list-box__wrapper`,
- [enabled ? containerClassName : null],
- {
- [`${prefix}--multi-select__wrapper--inline`]: inline,
- [`${prefix}--list-box__wrapper--inline`]: inline,
- [`${prefix}--multi-select__wrapper--inline--invalid`]:
- inline && invalid,
- [`${prefix}--list-box__wrapper--inline--invalid`]:
- inline && invalid,
- [`${prefix}--list-box--up`]: direction === 'top',
+ function clearInputValue() {
+ setInputValue('');
+ if (textInput.current) {
+ textInput.current.focus();
+ }
+ }
+
+ return (
+ (
+ {
+ if (selectedItem !== null) {
+ onItemChange(selectedItem);
}
- );
- const helperId = !helperText
- ? undefined
- : `filterablemultiselect-helper-text-${this.filterableMultiSelectInstanceId}`;
- const labelId = `${id}-label`;
- const titleClasses = cx({
- [`${prefix}--label`]: true,
- [`${prefix}--label--disabled`]: disabled,
- [`${prefix}--visually-hidden`]: hideLabel,
- });
- const helperClasses = cx({
- [`${prefix}--form__helper-text`]: true,
- [`${prefix}--form__helper-text--disabled`]: disabled,
- });
- const inputClasses = cx({
- [`${prefix}--text-input`]: true,
- [`${prefix}--text-input--empty`]: !this.state.inputValue,
- [`${prefix}--text-input--light`]: light,
- });
- const helper = helperText ? (
-
- {helperText}
-
- ) : null;
- const menuId = `${id}__menu`;
- const inputId = `${id}-input`;
-
- return (
- (
-
+ {({
+ getInputProps,
+ getItemProps,
+ getLabelProps,
+ getMenuProps,
+ getRootProps,
+ getToggleButtonProps,
+ isOpen,
+ inputValue,
+ selectedItem,
+ }) => {
+ const className = cx(
+ `${prefix}--multi-select`,
+ `${prefix}--combo-box`,
+ `${prefix}--multi-select--filterable`,
+ [enabled ? null : containerClassName],
+ {
+ [`${prefix}--multi-select--invalid`]: invalid,
+ [`${prefix}--multi-select--open`]: isOpen,
+ [`${prefix}--multi-select--inline`]: inline,
+ [`${prefix}--multi-select--selected`]: selectedItem.length > 0,
+ [`${prefix}--multi-select--filterable--input-focused`]:
+ inputFocused,
+ }
+ );
+ const rootProps = getRootProps(
+ {},
+ {
+ suppressRefError: true,
+ }
+ );
+
+ const labelProps = getLabelProps();
+
+ const buttonProps = getToggleButtonProps({
+ disabled,
+ onClick: () => {
+ handleOnMenuChange(!isOpen);
+ if (textInput.current) {
+ textInput.current.focus();
+ }
+ },
+ // When we moved the "root node" of Downshift to the for
+ // ARIA 1.2 compliance, we unfortunately hit this branch for the
+ // "mouseup" event that downshift listens to:
+ // https://github.com/downshift-js/downshift/blob/v5.2.1/src/downshift.js#L1051-L1065
+ //
+ // As a result, it will reset the state of the component and so we
+ // stop the event from propagating to prevent this. This allows the
+ // toggleMenu behavior for the toggleButton to correctly open and
+ // close the menu.
+ onMouseUp(event) {
+ if (isOpen) {
+ event.stopPropagation();
+ }
+ },
+ });
+
+ const inputProps = getInputProps({
+ 'aria-controls': isOpen ? menuId : null,
+ 'aria-describedby': helperText ? helperId : null,
+ // Remove excess aria `aria-labelledby`. HTML
+ // provides this aria information.
+ 'aria-labelledby': null,
+ disabled,
+ placeholder,
+ onClick: () => {
+ handleOnMenuChange(true);
+ },
+ onKeyDown: (event) => {
+ if (match(event, keys.Space)) {
+ event.stopPropagation();
+ }
+ },
+ onFocus: () => {
+ setInputFocused(true);
+ },
+ onBlur: () => {
+ setInputFocused(false);
+ },
+ });
+
+ const menuProps = getMenuProps(
+ {
+ 'aria-label': ariaLabel,
+ },
+ {
+ suppressRefError: true,
+ }
+ );
+
+ return (
+
+ {titleText ? (
+
+ {titleText}
+
+ ) : null}
+
{
- if (selectedItem !== null) {
- onItemChange(selectedItem);
- }
- }}
- itemToString={itemToString}
- onStateChange={this.handleOnStateChange}
- onOuterClick={this.handleOnOuterClick}
- selectedItem={selectedItems}
- labelId={labelId}
- menuId={menuId}
- inputId={inputId}>
- {({
- getInputProps,
- getItemProps,
- getLabelProps,
- getMenuProps,
- getRootProps,
- getToggleButtonProps,
- isOpen,
- inputValue,
- selectedItem,
- }) => {
- const className = cx(
- `${prefix}--multi-select`,
- `${prefix}--combo-box`,
- `${prefix}--multi-select--filterable`,
- [enabled ? null : containerClassName],
- {
- [`${prefix}--multi-select--invalid`]: invalid,
- [`${prefix}--multi-select--open`]: isOpen,
- [`${prefix}--multi-select--inline`]: inline,
- [`${prefix}--multi-select--selected`]:
- selectedItem.length > 0,
- [`${prefix}--multi-select--filterable--input-focused`]:
- this.state.inputFocused,
- }
- );
- const rootProps = getRootProps(
- {},
- {
- suppressRefError: true,
- }
- );
- const labelProps = getLabelProps();
- const buttonProps = getToggleButtonProps({
- disabled,
- onClick: () => {
- this.handleOnMenuChange(!this.state.isOpen);
- if (this.textInput.current) {
- this.textInput.current.focus();
- }
- },
- // When we moved the "root node" of Downshift to the for
- // ARIA 1.2 compliance, we unfortunately hit this branch for the
- // "mouseup" event that downshift listens to:
- // https://github.com/downshift-js/downshift/blob/v5.2.1/src/downshift.js#L1051-L1065
- //
- // As a result, it will reset the state of the component and so we
- // stop the event from propagating to prevent this. This allows the
- // toggleMenu behavior for the toggleButton to correctly open and
- // close the menu.
- onMouseUp(event) {
- event.stopPropagation();
- },
- });
- const inputProps = getInputProps({
- 'aria-controls': isOpen ? menuId : null,
- 'aria-describedby': helperText ? helperId : null,
- // Remove excess aria `aria-labelledby`. HTML
- // provides this aria information.
- 'aria-labelledby': null,
- disabled,
- placeholder,
- onClick: () => {
- this.handleOnMenuChange(true);
- },
- onKeyDown: (event) => {
- if (match(event, keys.Space)) {
+ size={size}>
+
+ {selectedItem.length > 0 && (
+ {
+ clearSelection();
+ if (textInput.current) {
+ textInput.current.focus();
+ }
+ }}
+ selectionCount={selectedItem.length}
+ translateWithId={translateWithId}
+ disabled={disabled}
+ />
+ )}
+
+ {invalid && (
+
+ )}
+ {showWarning && (
+
+ )}
+ {inputValue && (
+ {
+ // If we do not stop this event from propagating,
+ // it seems like Downshift takes our event and
+ // prevents us from getting `onClick` /
+ // `clearSelection` from the underlying
+ {isOpen ? (
+
+ {sortItems(
+ filterItems(items, { itemToString, inputValue }),
+ {
+ selectedItems: {
+ top: selectedItems,
+ fixed: [],
+ 'top-after-reopen': topItems,
+ }[selectionFeedback],
+ itemToString,
+ compareItems,
+ locale,
}
- },
- onFocus: () => {
- this.setState({ inputFocused: true });
- },
- onBlur: () => {
- this.setState({ inputFocused: false });
- },
- });
- const menuProps = getMenuProps(
- {
- 'aria-label': ariaLabel,
- },
- {
- suppressRefError: true,
- }
- );
-
- return (
-
- {titleText ? (
-
- {titleText}
-
- ) : null}
-
-
- {selectedItem.length > 0 && (
- {
- clearSelection();
- if (this.textInput.current) {
- this.textInput.current.focus();
- }
- }}
- selectionCount={selectedItem.length}
- translateWithId={translateWithId}
- disabled={disabled}
- />
- )}
-
- {invalid && (
-
- )}
- {showWarning && (
-
- )}
- {inputValue && (
- {
- // If we do not stop this event from propagating,
- // it seems like Downshift takes our event and
- // prevents us from getting `onClick` /
- // `clearSelection` from the underlying
- {isOpen ? (
-
- {sortItems(
- filterItems(items, {
- itemToString,
- inputValue,
- }),
- {
- selectedItems: {
- top: selectedItems,
- fixed: [],
- 'top-after-reopen': this.state.topItems,
- }[this.props.selectionFeedback],
- itemToString,
- compareItems,
- locale,
- }
- ).map((item, index) => {
- const itemProps = getItemProps({
- item,
- disabled: item.disabled,
- });
- const itemText = itemToString(item);
- const isChecked =
- selectedItem.filter((selected) =>
- isEqual(selected, item)
- ).length > 0;
- return (
-
-
-
- {itemToElement ? (
-
- ) : (
- itemText
- )}
-
-
-
- );
- })}
-
- ) : null}
-
- {!inline && !invalid && !warn ? helper : null}
-
- );
- }}
-
- )}
- />
- );
- }}
-
- );
- }
-}
+ ).map((item, index) => {
+ const itemProps = getItemProps({
+ item,
+ disabled: item.disabled,
+ });
+ const itemText = itemToString(item);
+ const isChecked =
+ selectedItem.filter((selected) =>
+ isEqual(selected, item)
+ ).length > 0;
+ return (
+
+
+
+ {ItemToElement ? (
+
+ ) : (
+ itemText
+ )}
+
+
+
+ );
+ })}
+
+ ) : null}
+
+ {!inline && !invalid && !warn ? helper : null}
+
+ );
+ }}
+
+ )}
+ />
+ );
+});
+
+FilterableMultiSelect.propTypes = {
+ /**
+ * 'aria-label' of the ListBox component.
+ */
+ ariaLabel: PropTypes.string,
+
+ /**
+ * Specify the direction of the multiselect dropdown. Can be either top or bottom.
+ */
+ direction: PropTypes.oneOf(['top', 'bottom']),
+
+ /**
+ * Disable the control
+ */
+ disabled: PropTypes.bool,
+
+ /**
+ * Additional props passed to Downshift
+ */
+ downshiftProps: PropTypes.shape(Downshift.propTypes),
+
+ /**
+ * Specify whether the title text should be hidden or not
+ */
+ hideLabel: PropTypes.bool,
+
+ /**
+ * Specify a custom `id`
+ */
+ id: PropTypes.string.isRequired,
+
+ /**
+ * Allow users to pass in arbitrary items from their collection that are
+ * pre-selected
+ */
+ initialSelectedItems: PropTypes.array,
+
+ /**
+ * Is the current selection invalid?
+ */
+ invalid: PropTypes.bool,
+
+ /**
+ * If invalid, what is the error?
+ */
+ invalidText: PropTypes.node,
+
+ /**
+ * Function to render items as custom components instead of strings.
+ * Defaults to null and is overridden by a getter
+ */
+ itemToElement: PropTypes.func,
+
+ /**
+ * Helper function passed to downshift that allows the library to render a
+ * given item to a string label. By default, it extracts the `label` field
+ * from a given item to serve as the item label in the list.
+ */
+ itemToString: PropTypes.func,
+
+ /**
+ * We try to stay as generic as possible here to allow individuals to pass
+ * in a collection of whatever kind of data structure they prefer
+ */
+ items: PropTypes.array.isRequired,
+
+ /**
+ * `true` to use the light version.
+ */
+ light: PropTypes.bool,
+
+ /**
+ * Specify the locale of the control. Used for the default `compareItems`
+ * used for sorting the list of items in the control.
+ */
+ locale: PropTypes.string,
+
+ /**
+ * `onChange` is a utility for this controlled component to communicate to a
+ * consuming component what kind of internal state changes are occurring.
+ */
+ onChange: PropTypes.func,
+
+ /**
+ * `onInputValueChange` is a utility for this controlled component to communicate to
+ * the currently typed input.
+ */
+ onInputValueChange: PropTypes.func,
+
+ /**
+ * `onMenuChange` is a utility for this controlled component to communicate to a
+ * consuming component that the menu was opened(`true`)/closed(`false`).
+ */
+ onMenuChange: PropTypes.func,
+
+ /**
+ * Initialize the component with an open(`true`)/closed(`false`) menu.
+ */
+ open: PropTypes.bool,
+
+ /**
+ * Generic `placeholder` that will be used as the textual representation of
+ * what this field is for
+ */
+ placeholder: PropTypes.string,
+
+ /**
+ * Specify feedback (mode) of the selection.
+ * `top`: selected item jumps to top
+ * `fixed`: selected item stays at it's position
+ * `top-after-reopen`: selected item jump to top after reopen dropdown
+ */
+ selectionFeedback: PropTypes.oneOf(['top', 'fixed', 'top-after-reopen']),
+
+ /**
+ * Specify the size of the ListBox. Currently supports either `sm`, `md` or `lg` as an option.
+ */
+ size: ListBoxPropTypes.ListBoxSize,
+
+ ...sortingPropTypes,
+
+ /**
+ * Callback function for translating ListBoxMenuIcon SVG title
+ */
+ translateWithId: PropTypes.func,
+
+ /**
+ * Specify title to show title on hover
+ */
+ useTitleInItem: PropTypes.bool,
+
+ /**
+ * Specify whether the control is currently in warning state
+ */
+ warn: PropTypes.bool,
+
+ /**
+ * Provide the text that is displayed when the control is in warning state
+ */
+ warnText: PropTypes.node,
+};
+
+FilterableMultiSelect.defaultProps = {
+ ariaLabel: 'Choose an item',
+ compareItems: defaultCompareItems,
+ direction: 'bottom',
+ disabled: false,
+ filterItems: defaultFilterItems,
+ initialSelectedItems: [],
+ itemToString: defaultItemToString,
+ locale: 'en',
+ sortItems: defaultSortItems,
+ light: false,
+ open: false,
+ selectionFeedback: 'top-after-reopen',
+};
+
+export default FilterableMultiSelect;
diff --git a/packages/react/src/components/MultiSelect/MultiSelect-story.js b/packages/react/src/components/MultiSelect/MultiSelect-story.js
deleted file mode 100644
index e6a558005d32..000000000000
--- a/packages/react/src/components/MultiSelect/MultiSelect-story.js
+++ /dev/null
@@ -1,350 +0,0 @@
-/**
- * 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, { useCallback, useState } from 'react';
-import { action } from '@storybook/addon-actions';
-import {
- withKnobs,
- boolean,
- select,
- text,
- object,
-} from '@storybook/addon-knobs';
-import { withReadme } from 'storybook-readme';
-import readme from './README.md';
-import MultiSelect from '../MultiSelect';
-import FilterableMultiSelect from '../MultiSelect/FilterableMultiSelect';
-import Checkbox from '../Checkbox';
-import mdx from './MultiSelect.mdx';
-import Button from '../Button';
-
-const items = [
- {
- id: 'downshift-1-item-0',
- text: 'Option 1',
- },
- {
- id: 'downshift-1-item-1',
- text: 'Option 2',
- },
- {
- id: 'downshift-1-item-2',
- text: 'Option 3 - a disabled item',
- disabled: true,
- },
- {
- id: 'downshift-1-item-3',
- text: 'Option 4',
- },
- {
- id: 'downshift-1-item-4',
- text: 'An example option that is really long to show what should be done to handle long text',
- },
- {
- id: 'downshift-1-item-5',
- text: 'Option 5',
- },
-];
-
-const defaultLabel = 'MultiSelect Label';
-const defaultPlaceholder = 'Filter';
-
-const types = {
- 'Default (default)': 'default',
- 'Inline (inline)': 'inline',
-};
-
-const sizes = {
- 'Small (sm)': 'sm',
- 'Medium (md) - default': undefined,
- 'Large (lg)': 'lg',
-};
-
-const directions = {
- 'Bottom (default)': 'bottom',
- 'Top ': 'top',
-};
-
-const props = () => ({
- id: text('MultiSelect ID (id)', 'carbon-multiselect-example'),
- titleText: text('Title (titleText)', 'Multiselect title'),
- hideLabel: boolean('No title text shown (hideLabel)', false),
- helperText: text('Helper text (helperText)', 'This is helper text'),
- disabled: boolean('Disabled (disabled)', false),
- light: boolean('Light variant (light)', false),
- useTitleInItem: boolean('Show tooltip on hover', false),
- type: select('UI type (Only for ``) (type)', types, 'default'),
- size: select('Field size (size)', sizes, undefined) || undefined,
- direction: select('Dropdown direction (direction)', directions, 'bottom'),
- label: text('Label (label)', defaultLabel),
- invalid: boolean('Show form validation UI (invalid)', false),
- invalidText: text(
- 'Form validation UI content (invalidText)',
- 'Invalid Selection'
- ),
- warn: boolean('Show warning state (warn)', false),
- warnText: text(
- 'Warning state text (warnText)',
- 'Selecting more items may increase processing time'
- ),
- onChange: action('onChange'),
- onMenuChange: action('onMenuChange'),
- listBoxMenuIconTranslationIds: object(
- 'Listbox menu icon translation IDs (for translateWithId callback)',
- {
- 'close.menu': 'Close menu',
- 'open.menu': 'Open menu',
- 'clear.all': 'Clear all',
- 'clear.selection': 'Clear selection',
- }
- ),
- selectionFeedback: select(
- 'Selection feedback',
- ['top', 'fixed', 'top-after-reopen'],
- 'top-after-reopen'
- ),
-});
-
-export default {
- title: 'Components/MultiSelect',
- component: MultiSelect,
- subcomponents: {
- FilterableMultiSelect,
- },
- decorators: [withKnobs],
- parameters: {
- docs: {
- page: mdx,
- },
- },
-};
-
-export const Default = withReadme(readme, () => {
- const {
- listBoxMenuIconTranslationIds,
- selectionFeedback,
- ...multiSelectProps
- } = props();
- return (
-
- (item ? item.text : '')}
- translateWithId={(id) => listBoxMenuIconTranslationIds[id]}
- selectionFeedback={selectionFeedback}
- />
-
- );
-});
-
-export const Controlled = withReadme(readme, () => {
- const {
- listBoxMenuIconTranslationIds,
- selectionFeedback,
- ...multiSelectProps
- } = props();
- const [selectedItems, setSelectedItems] = useState([]);
- const onChange = useCallback(({ selectedItems: newSelectedItems }) => {
- setSelectedItems(newSelectedItems);
- }, []);
-
- return (
-
- (item ? item.text : '')}
- translateWithId={(id) => listBoxMenuIconTranslationIds[id]}
- selectionFeedback={selectionFeedback}
- onChange={onChange}
- selectedItems={selectedItems}
- />
-
-
-
- );
-});
-
-export const ItemToElement = withReadme(readme, () => {
- return (
-
- (item ? item.text : '')}
- itemToElement={(item) =>
- item ? (
-
- {item.text}{' '}
-
- {' '}
- 🔥
-
-
- ) : (
- ''
- )
- }
- />
-
- (item ? item.text : '')}
- itemToElement={(item) =>
- item ? (
-
- {item.text}{' '}
-
- {' '}
- 🔥
-
-
- ) : (
- ''
- )
- }
- />
-
- );
-});
-
-Default.storyName = 'default';
-
-Default.parameters = {
- info: {
- text: `
- MultiSelect
- `,
- },
-};
-
-export const WithInitialSelectedItems = withReadme(readme, () => {
- const {
- listBoxMenuIconTranslationIds,
- selectionFeedback,
- ...multiSelectProps
- } = props();
-
- return (
-
- (item ? item.text : '')}
- initialSelectedItems={[items[0], items[1]]}
- translateWithId={(id) => listBoxMenuIconTranslationIds[id]}
- selectionFeedback={selectionFeedback}
- />
-
- );
-});
-
-WithInitialSelectedItems.storyName = 'with initial selected items';
-
-WithInitialSelectedItems.parameters = {
- info: {
- text: `
- Provide a set of items to initially select in the control
- `,
- },
-};
-
-export const _Filterable = withReadme(readme, () => {
- const {
- listBoxMenuIconTranslationIds,
- selectionFeedback,
- ...multiSelectProps
- } = props();
-
- return (
-
- (item ? item.text : '')}
- placeholder={defaultPlaceholder}
- translateWithId={(id) => listBoxMenuIconTranslationIds[id]}
- selectionFeedback={selectionFeedback}
- onMenuChange={(e) => {
- multiSelectProps.onMenuChange(e);
- }}
- />
-
- );
-});
-
-_Filterable.storyName = 'filterable';
-
-_Filterable.parameters = {
- info: {
- text: `
- When a list contains more than 25 items, use \`MultiSelect.Filterable\` to help find options from the list.
- `,
- },
-};
-
-export const WithChangeOnClose = withReadme(readme, () => {
- const {
- listBoxMenuIconTranslationIds,
- selectionFeedback,
- ...multiSelectProps
- } = props();
-
- const [hasFocus, setHasFocus] = useState(false);
- const [active, setActive] = useState(false);
- const [selItems, setSelItems] = useState([items[0]]);
- if (!hasFocus && active && selItems.length == 0) {
- setActive(false);
- }
-
- return (
-
- {
- setActive(a);
- if (a) {
- setSelItems([items[0]]);
- }
- }}
- labelText="Active"
- />
- (item ? item.text : '')}
- translateWithId={(id) => listBoxMenuIconTranslationIds[id]}
- selectionFeedback={selectionFeedback}
- key={active}
- disabled={!active}
- initialSelectedItems={selItems}
- onMenuChange={(e) => {
- multiSelectProps.onMenuChange(e);
- setHasFocus(e);
- }}
- onChange={(e) => {
- setSelItems(e.selectedItems);
- }}
- />
-
- );
-});
diff --git a/packages/react/src/components/MultiSelect/next/MultiSelect.stories.js b/packages/react/src/components/MultiSelect/MultiSelect.stories.js
similarity index 98%
rename from packages/react/src/components/MultiSelect/next/MultiSelect.stories.js
rename to packages/react/src/components/MultiSelect/MultiSelect.stories.js
index b4d3c30cd369..b56d4e975ec4 100644
--- a/packages/react/src/components/MultiSelect/next/MultiSelect.stories.js
+++ b/packages/react/src/components/MultiSelect/MultiSelect.stories.js
@@ -6,9 +6,9 @@
*/
import React from 'react';
-import MultiSelect from '../';
+import MultiSelect from '.';
import FilterableMultiSelect from './FilterableMultiSelect';
-import { Layer } from '../../Layer';
+import { Layer } from '../Layer';
export default {
title: 'Components/MultiSelect',
diff --git a/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.e2e.js b/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.e2e.js
index bd77bfb9a7e0..2fdfd1deebef 100644
--- a/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.e2e.js
+++ b/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.e2e.js
@@ -5,12 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
-import '../../../../index.scss';
+import '../../../../../index.scss';
import React from 'react';
import { mount } from '@cypress/react';
import { generateItems, generateGenericItem } from '../../ListBox/test-helpers';
-import FilterableMultiSelect from '../../MultiSelect/FilterableMultiSelect';
+import FilterableMultiSelect from '../FilterableMultiSelect';
describe('FilterableMultiSelect', () => {
beforeEach(() => {
diff --git a/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js b/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js
index 7ccf35a41f55..633ffa3f5e76 100644
--- a/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js
+++ b/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js
@@ -7,7 +7,7 @@
import React from 'react';
import { mount } from 'enzyme';
-import FilterableMultiSelect from '../../MultiSelect/FilterableMultiSelect';
+import FilterableMultiSelect from '../FilterableMultiSelect';
import {
assertMenuOpen,
assertMenuClosed,
@@ -25,8 +25,6 @@ describe('FilterableMultiSelect', () => {
let mockProps;
beforeEach(() => {
- // jest.mock('../../../internal/deprecateFieldOnObject');
-
mockProps = {
id: 'test-filterable-multiselect',
disabled: false,
diff --git a/packages/react/src/components/MultiSelect/__tests__/__snapshots__/FilterableMultiSelect-test.js.snap b/packages/react/src/components/MultiSelect/__tests__/__snapshots__/FilterableMultiSelect-test.js.snap
index 3a83c2b00d57..166398a5594e 100644
--- a/packages/react/src/components/MultiSelect/__tests__/__snapshots__/FilterableMultiSelect-test.js.snap
+++ b/packages/react/src/components/MultiSelect/__tests__/__snapshots__/FilterableMultiSelect-test.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilterableMultiSelect should render 1`] = `
-
-
+
`;
diff --git a/packages/react/src/components/MultiSelect/index.js b/packages/react/src/components/MultiSelect/index.js
index 6a1bd6854598..1d5eedcd0511 100644
--- a/packages/react/src/components/MultiSelect/index.js
+++ b/packages/react/src/components/MultiSelect/index.js
@@ -5,22 +5,17 @@
* LICENSE file in the root directory of this source tree.
*/
-import * as FeatureFlags from '@carbon/feature-flags';
import { deprecateFieldOnObject } from '../../internal/deprecateFieldOnObject';
import MultiSelect from './MultiSelect';
-import { default as FilterableMultiSelectClassic } from './FilterableMultiSelect';
-import { default as FilterableMultiSelectNext } from './next/FilterableMultiSelect';
+import { default as FilterableMultiSelect } from './FilterableMultiSelect';
-FilterableMultiSelectNext.displayName = 'MultiSelect.Filterable';
-MultiSelect.Filterable = FilterableMultiSelectClassic;
-
-export const FilterableMultiSelect = FeatureFlags.enabled('enable-v11-release')
- ? FilterableMultiSelectNext
- : FilterableMultiSelectClassic;
+FilterableMultiSelect.displayName = 'MultiSelect.Filterable';
+MultiSelect.Filterable = FilterableMultiSelect;
if (__DEV__) {
deprecateFieldOnObject(MultiSelect, 'Filterable', FilterableMultiSelect);
}
+export { FilterableMultiSelect };
export default MultiSelect;
diff --git a/packages/react/src/components/MultiSelect/next/FilterableMultiSelect.js b/packages/react/src/components/MultiSelect/next/FilterableMultiSelect.js
deleted file mode 100644
index 6e57bd4343ed..000000000000
--- a/packages/react/src/components/MultiSelect/next/FilterableMultiSelect.js
+++ /dev/null
@@ -1,600 +0,0 @@
-/**
- * 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 { WarningFilled, WarningAltFilled } from '@carbon/icons-react';
-import cx from 'classnames';
-import Downshift from 'downshift';
-import isEqual from 'lodash.isequal';
-import PropTypes from 'prop-types';
-import React, { useState, useRef } from 'react';
-import { defaultFilterItems } from '../../ComboBox/tools/filter';
-import { sortingPropTypes } from '../MultiSelectPropTypes';
-import ListBox, { PropTypes as ListBoxPropTypes } from '../../ListBox';
-import { ListBoxTrigger, ListBoxSelection } from '../../ListBox/next';
-import { match, keys } from '../../../internal/keyboard';
-import Selection from '../../../internal/Selection';
-import { defaultItemToString } from '../tools/itemToString';
-import mergeRefs from '../../../tools/mergeRefs';
-import { useId } from '../../../internal/useId';
-import { defaultSortItems, defaultCompareItems } from '../tools/sorting';
-import { useFeatureFlag } from '../../FeatureFlags';
-import { usePrefix } from '../../../internal/usePrefix';
-
-const FilterableMultiSelect = React.forwardRef(function FilterableMultiSelect(
- {
- ariaLabel,
- className: containerClassName,
- compareItems,
- direction,
- disabled,
- downshiftProps,
- filterItems,
- helperText,
- hideLabel,
- id,
- initialSelectedItems,
- invalid,
- invalidText,
- items,
- itemToElement: ItemToElement, // needs to be capitalized for react to render it correctly
- itemToString,
- light,
- locale,
- onInputValueChange,
- open,
- onChange,
- onMenuChange,
- placeholder,
- titleText,
- type,
- selectionFeedback,
- size,
- sortItems,
- translateWithId,
- useTitleInItem,
- warn,
- warnText,
- },
- ref
-) {
- const [isOpen, setIsOpen] = useState(open);
- const [prevOpen, setPrevOpen] = useState(open);
- const [inputValue, setInputValue] = useState('');
- const [topItems, setTopItems] = useState([]);
- const [inputFocused, setInputFocused] = useState(false);
- const [highlightedIndex, setHighlightedIndex] = useState(null);
- const textInput = useRef();
- const filterableMultiSelectInstanceId = useId();
-
- const enabled = useFeatureFlag('enable-v11-release');
- const prefix = usePrefix();
-
- if (prevOpen !== open) {
- setIsOpen(open);
- setPrevOpen(open);
- }
-
- const inline = type === 'inline';
- const showWarning = !invalid && warn;
-
- const wrapperClasses = cx(
- `${prefix}--multi-select__wrapper`,
- `${prefix}--list-box__wrapper`,
- [enabled ? containerClassName : null],
- {
- [`${prefix}--multi-select__wrapper--inline`]: inline,
- [`${prefix}--list-box__wrapper--inline`]: inline,
- [`${prefix}--multi-select__wrapper--inline--invalid`]: inline && invalid,
- [`${prefix}--list-box__wrapper--inline--invalid`]: inline && invalid,
- [`${prefix}--list-box--up`]: direction === 'top',
- }
- );
- const helperId = !helperText
- ? undefined
- : `filterablemultiselect-helper-text-${filterableMultiSelectInstanceId}`;
- const labelId = `${id}-label`;
- const titleClasses = cx({
- [`${prefix}--label`]: true,
- [`${prefix}--label--disabled`]: disabled,
- [`${prefix}--visually-hidden`]: hideLabel,
- });
- const helperClasses = cx({
- [`${prefix}--form__helper-text`]: true,
- [`${prefix}--form__helper-text--disabled`]: disabled,
- });
- const inputClasses = cx({
- [`${prefix}--text-input`]: true,
- [`${prefix}--text-input--empty`]: !inputValue,
- [`${prefix}--text-input--light`]: light,
- });
- const helper = helperText ? (
-
- {helperText}
-
- ) : null;
- const menuId = `${id}__menu`;
- const inputId = `${id}-input`;
-
- function handleOnChange(changes) {
- if (onChange) {
- onChange(changes);
- }
- }
-
- function handleOnMenuChange(forceIsOpen) {
- const nextIsOpen = forceIsOpen ?? !isOpen;
- setIsOpen(nextIsOpen);
- if (onMenuChange) {
- onMenuChange(nextIsOpen);
- }
- }
-
- function handleOnOuterClick() {
- handleOnMenuChange(false);
- }
-
- function handleOnStateChange(changes, downshift) {
- if (changes.isOpen && !isOpen) {
- setTopItems(downshift.selectedItem);
- }
-
- const { type } = changes;
- const { stateChangeTypes } = Downshift;
-
- switch (type) {
- case stateChangeTypes.keyDownArrowDown:
- case stateChangeTypes.keyDownArrowUp:
- case stateChangeTypes.keyDownHome:
- case stateChangeTypes.keyDownEnd:
- setHighlightedIndex(
- changes.highlightedIndex !== undefined
- ? changes.highlightedIndex
- : null
- );
- if (stateChangeTypes.keyDownArrowDown === type && !isOpen) {
- handleOnMenuChange(true);
- }
- break;
- case stateChangeTypes.keyDownEscape:
- handleOnMenuChange(false);
- break;
- }
- }
-
- function handleOnInputValueChange(inputValue, { type }) {
- if (onInputValueChange) {
- onInputValueChange(inputValue);
- }
-
- if (type !== Downshift.stateChangeTypes.changeInput) {
- return;
- }
-
- if (Array.isArray(inputValue)) {
- clearInputValue();
- } else {
- setInputValue(inputValue);
- }
-
- if (inputValue && !isOpen) {
- handleOnMenuChange(true);
- } else if (!inputValue && isOpen) {
- handleOnMenuChange(false);
- }
- }
-
- function clearInputValue() {
- setInputValue('');
- if (textInput.current) {
- textInput.current.focus();
- }
- }
-
- return (
- (
- {
- if (selectedItem !== null) {
- onItemChange(selectedItem);
- }
- }}
- itemToString={itemToString}
- onStateChange={handleOnStateChange}
- onOuterClick={handleOnOuterClick}
- selectedItem={selectedItems}
- labelId={labelId}
- menuId={menuId}
- inputId={inputId}>
- {({
- getInputProps,
- getItemProps,
- getLabelProps,
- getMenuProps,
- getRootProps,
- getToggleButtonProps,
- isOpen,
- inputValue,
- selectedItem,
- }) => {
- const className = cx(
- `${prefix}--multi-select`,
- `${prefix}--combo-box`,
- `${prefix}--multi-select--filterable`,
- [enabled ? null : containerClassName],
- {
- [`${prefix}--multi-select--invalid`]: invalid,
- [`${prefix}--multi-select--open`]: isOpen,
- [`${prefix}--multi-select--inline`]: inline,
- [`${prefix}--multi-select--selected`]: selectedItem.length > 0,
- [`${prefix}--multi-select--filterable--input-focused`]:
- inputFocused,
- }
- );
- const rootProps = getRootProps(
- {},
- {
- suppressRefError: true,
- }
- );
-
- const labelProps = getLabelProps();
-
- const buttonProps = getToggleButtonProps({
- disabled,
- onClick: () => {
- handleOnMenuChange(!isOpen);
- if (textInput.current) {
- textInput.current.focus();
- }
- },
- // When we moved the "root node" of Downshift to the for
- // ARIA 1.2 compliance, we unfortunately hit this branch for the
- // "mouseup" event that downshift listens to:
- // https://github.com/downshift-js/downshift/blob/v5.2.1/src/downshift.js#L1051-L1065
- //
- // As a result, it will reset the state of the component and so we
- // stop the event from propagating to prevent this. This allows the
- // toggleMenu behavior for the toggleButton to correctly open and
- // close the menu.
- onMouseUp(event) {
- if (isOpen) {
- event.stopPropagation();
- }
- },
- });
-
- const inputProps = getInputProps({
- 'aria-controls': isOpen ? menuId : null,
- 'aria-describedby': helperText ? helperId : null,
- // Remove excess aria `aria-labelledby`. HTML
- // provides this aria information.
- 'aria-labelledby': null,
- disabled,
- placeholder,
- onClick: () => {
- handleOnMenuChange(true);
- },
- onKeyDown: (event) => {
- if (match(event, keys.Space)) {
- event.stopPropagation();
- }
- },
- onFocus: () => {
- setInputFocused(true);
- },
- onBlur: () => {
- setInputFocused(false);
- },
- });
-
- const menuProps = getMenuProps(
- {
- 'aria-label': ariaLabel,
- },
- {
- suppressRefError: true,
- }
- );
-
- return (
-
- {titleText ? (
-
- {titleText}
-
- ) : null}
-
-
- {selectedItem.length > 0 && (
- {
- clearSelection();
- if (textInput.current) {
- textInput.current.focus();
- }
- }}
- selectionCount={selectedItem.length}
- translateWithId={translateWithId}
- disabled={disabled}
- />
- )}
-
- {invalid && (
-
- )}
- {showWarning && (
-
- )}
- {inputValue && (
- {
- // If we do not stop this event from propagating,
- // it seems like Downshift takes our event and
- // prevents us from getting `onClick` /
- // `clearSelection` from the underlying
- {isOpen ? (
-
- {sortItems(
- filterItems(items, { itemToString, inputValue }),
- {
- selectedItems: {
- top: selectedItems,
- fixed: [],
- 'top-after-reopen': topItems,
- }[selectionFeedback],
- itemToString,
- compareItems,
- locale,
- }
- ).map((item, index) => {
- const itemProps = getItemProps({
- item,
- disabled: item.disabled,
- });
- const itemText = itemToString(item);
- const isChecked =
- selectedItem.filter((selected) =>
- isEqual(selected, item)
- ).length > 0;
- return (
-
-
-
- {ItemToElement ? (
-
- ) : (
- itemText
- )}
-
-
-
- );
- })}
-
- ) : null}
-
- {!inline && !invalid && !warn ? helper : null}
-
- );
- }}
-
- )}
- />
- );
-});
-
-FilterableMultiSelect.propTypes = {
- /**
- * 'aria-label' of the ListBox component.
- */
- ariaLabel: PropTypes.string,
-
- /**
- * Specify the direction of the multiselect dropdown. Can be either top or bottom.
- */
- direction: PropTypes.oneOf(['top', 'bottom']),
-
- /**
- * Disable the control
- */
- disabled: PropTypes.bool,
-
- /**
- * Additional props passed to Downshift
- */
- downshiftProps: PropTypes.shape(Downshift.propTypes),
-
- /**
- * Specify whether the title text should be hidden or not
- */
- hideLabel: PropTypes.bool,
-
- /**
- * Specify a custom `id`
- */
- id: PropTypes.string.isRequired,
-
- /**
- * Allow users to pass in arbitrary items from their collection that are
- * pre-selected
- */
- initialSelectedItems: PropTypes.array,
-
- /**
- * Is the current selection invalid?
- */
- invalid: PropTypes.bool,
-
- /**
- * If invalid, what is the error?
- */
- invalidText: PropTypes.node,
-
- /**
- * Function to render items as custom components instead of strings.
- * Defaults to null and is overridden by a getter
- */
- itemToElement: PropTypes.func,
-
- /**
- * Helper function passed to downshift that allows the library to render a
- * given item to a string label. By default, it extracts the `label` field
- * from a given item to serve as the item label in the list.
- */
- itemToString: PropTypes.func,
-
- /**
- * We try to stay as generic as possible here to allow individuals to pass
- * in a collection of whatever kind of data structure they prefer
- */
- items: PropTypes.array.isRequired,
-
- /**
- * `true` to use the light version.
- */
- light: PropTypes.bool,
-
- /**
- * Specify the locale of the control. Used for the default `compareItems`
- * used for sorting the list of items in the control.
- */
- locale: PropTypes.string,
-
- /**
- * `onChange` is a utility for this controlled component to communicate to a
- * consuming component what kind of internal state changes are occurring.
- */
- onChange: PropTypes.func,
-
- /**
- * `onInputValueChange` is a utility for this controlled component to communicate to
- * the currently typed input.
- */
- onInputValueChange: PropTypes.func,
-
- /**
- * `onMenuChange` is a utility for this controlled component to communicate to a
- * consuming component that the menu was opened(`true`)/closed(`false`).
- */
- onMenuChange: PropTypes.func,
-
- /**
- * Initialize the component with an open(`true`)/closed(`false`) menu.
- */
- open: PropTypes.bool,
-
- /**
- * Generic `placeholder` that will be used as the textual representation of
- * what this field is for
- */
- placeholder: PropTypes.string,
-
- /**
- * Specify feedback (mode) of the selection.
- * `top`: selected item jumps to top
- * `fixed`: selected item stays at it's position
- * `top-after-reopen`: selected item jump to top after reopen dropdown
- */
- selectionFeedback: PropTypes.oneOf(['top', 'fixed', 'top-after-reopen']),
-
- /**
- * Specify the size of the ListBox. Currently supports either `sm`, `md` or `lg` as an option.
- */
- size: ListBoxPropTypes.ListBoxSize,
-
- ...sortingPropTypes,
-
- /**
- * Callback function for translating ListBoxMenuIcon SVG title
- */
- translateWithId: PropTypes.func,
-
- /**
- * Specify title to show title on hover
- */
- useTitleInItem: PropTypes.bool,
-
- /**
- * Specify whether the control is currently in warning state
- */
- warn: PropTypes.bool,
-
- /**
- * Provide the text that is displayed when the control is in warning state
- */
- warnText: PropTypes.node,
-};
-
-FilterableMultiSelect.defaultProps = {
- ariaLabel: 'Choose an item',
- compareItems: defaultCompareItems,
- direction: 'bottom',
- disabled: false,
- filterItems: defaultFilterItems,
- initialSelectedItems: [],
- itemToString: defaultItemToString,
- locale: 'en',
- sortItems: defaultSortItems,
- light: false,
- open: false,
- selectionFeedback: 'top-after-reopen',
-};
-
-export default FilterableMultiSelect;
diff --git a/packages/react/src/components/MultiSelect/next/__tests__/FilterableMultiSelect-test.e2e.js b/packages/react/src/components/MultiSelect/next/__tests__/FilterableMultiSelect-test.e2e.js
deleted file mode 100644
index 4adee9dd266b..000000000000
--- a/packages/react/src/components/MultiSelect/next/__tests__/FilterableMultiSelect-test.e2e.js
+++ /dev/null
@@ -1,136 +0,0 @@
-/**
- * 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 '../../../../../index.scss';
-
-import React from 'react';
-import { mount } from '@cypress/react';
-import {
- generateItems,
- generateGenericItem,
-} from '../../../ListBox/test-helpers';
-import FilterableMultiSelect from '../FilterableMultiSelect';
-
-describe('FilterableMultiSelect', () => {
- beforeEach(() => {
- const items = generateItems(5, generateGenericItem);
- const placeholder = 'Placeholder...';
-
- // eslint-disable-next-line react/prop-types
- function WrappedFilterableMultiSelect({ marginBottom = '1rem', ...props }) {
- return (
-
-
-
- );
- }
-
- mount(
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
- });
-
- it('should render', () => {
- cy.findAllByPlaceholderText(/Placeholder.../)
- .should('have.length', 13)
- .last()
- .should('be.visible');
-
- // snapshots should always be taken _after_ an assertion that
- // a element/component should be visible. This is to ensure
- // the DOM has settled and the element has fully loaded.
- cy.percySnapshot();
- });
-
- it('should render listbox when clicked', () => {
- cy.findAllByPlaceholderText(/Placeholder.../)
- .first()
- .click();
-
- cy.findAllByText(/Item 0/)
- .first()
- .should('be.visible');
- cy.findAllByText(/Item 4/)
- .first()
- .should('be.visible');
-
- // snapshots should always be taken _after_ an assertion that
- // a element/component should be visible. This is to ensure
- // the DOM has settled and the element has fully loaded.
- cy.percySnapshot();
- });
-});
diff --git a/packages/react/src/components/MultiSelect/next/__tests__/FilterableMultiSelect-test.js b/packages/react/src/components/MultiSelect/next/__tests__/FilterableMultiSelect-test.js
deleted file mode 100644
index 9633bb65af72..000000000000
--- a/packages/react/src/components/MultiSelect/next/__tests__/FilterableMultiSelect-test.js
+++ /dev/null
@@ -1,166 +0,0 @@
-/**
- * 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 { mount } from 'enzyme';
-import FilterableMultiSelect from '../FilterableMultiSelect';
-import {
- assertMenuOpen,
- assertMenuClosed,
- findMenuIconNode,
- generateItems,
- generateGenericItem,
-} from '../../../ListBox/test-helpers';
-
-const listItemName = 'ListBoxMenuItem';
-const openMenu = (wrapper) => {
- wrapper.find(`[role="combobox"]`).simulate('click');
-};
-
-describe('FilterableMultiSelect', () => {
- let mockProps;
-
- beforeEach(() => {
- mockProps = {
- id: 'test-filterable-multiselect',
- disabled: false,
- items: generateItems(5, generateGenericItem),
- initialSelectedItems: [],
- onChange: jest.fn(),
- onMenuChange: jest.fn(),
- placeholder: 'Placeholder...',
- };
- });
-
- it('should render', () => {
- const wrapper = mount();
- expect(wrapper).toMatchSnapshot();
- });
-
- it('should display all items when the menu is open initially', () => {
- const wrapper = mount();
- openMenu(wrapper);
- expect(wrapper.find(listItemName).length).toBe(mockProps.items.length);
- });
-
- it('should initially have the menu open when open prop is provided', () => {
- const wrapper = mount();
- assertMenuOpen(wrapper, mockProps);
- });
-
- it('should open the menu with a down arrow', () => {
- const wrapper = mount();
- const menuIconNode = findMenuIconNode(wrapper);
-
- menuIconNode.simulate('keyDown', { key: 'ArrowDown' });
- assertMenuOpen(wrapper, mockProps);
- });
-
- it('should let the user toggle the menu by the menu icon', () => {
- const wrapper = mount();
- findMenuIconNode(wrapper).simulate('click');
- assertMenuOpen(wrapper, mockProps);
- findMenuIconNode(wrapper).simulate('click');
- assertMenuClosed(wrapper);
- });
-
- it('should not close the menu after a user makes a selection', () => {
- const wrapper = mount();
- openMenu(wrapper);
-
- const firstListItem = wrapper.find(listItemName).at(0);
-
- firstListItem.simulate('click');
- assertMenuOpen(wrapper, mockProps);
- });
-
- it('should filter a list of items by the input value', () => {
- const wrapper = mount();
- openMenu(wrapper);
- expect(wrapper.find(listItemName).length).toBe(mockProps.items.length);
-
- wrapper
- .find('[placeholder="Placeholder..."]')
- .at(1)
- .simulate('change', { target: { value: '3' } });
-
- expect(wrapper.find(listItemName).length).toBe(1);
- });
-
- it('should call `onChange` with each update to selected items', () => {
- const wrapper = mount(
-
- );
- openMenu(wrapper);
-
- // Select the first two items
- wrapper.find(listItemName).at(0).simulate('click');
-
- expect(mockProps.onChange).toHaveBeenCalledTimes(1);
- expect(mockProps.onChange).toHaveBeenCalledWith({
- selectedItems: [mockProps.items[0]],
- });
-
- wrapper.find(listItemName).at(1).simulate('click');
-
- expect(mockProps.onChange).toHaveBeenCalledTimes(2);
- expect(mockProps.onChange).toHaveBeenCalledWith({
- selectedItems: [mockProps.items[0], mockProps.items[1]],
- });
-
- // Un-select the next two items
- wrapper.find(listItemName).at(0).simulate('click');
- expect(mockProps.onChange).toHaveBeenCalledTimes(3);
- expect(mockProps.onChange).toHaveBeenCalledWith({
- selectedItems: [mockProps.items[0]],
- });
-
- wrapper.find(listItemName).at(0).simulate('click');
- expect(mockProps.onChange).toHaveBeenCalledTimes(4);
- expect(mockProps.onChange).toHaveBeenCalledWith({
- selectedItems: [],
- });
- });
-
- it('should let items stay at their position after selecting', () => {
- const wrapper = mount(
-
- );
- openMenu(wrapper);
-
- // Select the first two items
- wrapper.find(listItemName).at(1).simulate('click');
-
- expect(mockProps.onChange).toHaveBeenCalledTimes(1);
- expect(mockProps.onChange).toHaveBeenCalledWith({
- selectedItems: [mockProps.items[1]],
- });
-
- wrapper.find(listItemName).at(1).simulate('click');
-
- expect(mockProps.onChange).toHaveBeenCalledTimes(2);
- expect(mockProps.onChange).toHaveBeenCalledWith({
- selectedItems: [],
- });
- });
-
- it('should not clear input value after a user makes a selection', () => {
- const wrapper = mount();
- openMenu(wrapper);
-
- wrapper
- .find('[placeholder="Placeholder..."]')
- .at(1)
- .simulate('change', { target: { value: '3' } });
-
- wrapper.find(listItemName).at(0).simulate('click');
-
- expect(
- wrapper.find('[placeholder="Placeholder..."]').at(1).props().value
- ).toEqual('3');
- });
-});
diff --git a/packages/react/src/components/MultiSelect/next/__tests__/__snapshots__/FilterableMultiSelect-test.js.snap b/packages/react/src/components/MultiSelect/next/__tests__/__snapshots__/FilterableMultiSelect-test.js.snap
deleted file mode 100644
index 166398a5594e..000000000000
--- a/packages/react/src/components/MultiSelect/next/__tests__/__snapshots__/FilterableMultiSelect-test.js.snap
+++ /dev/null
@@ -1,187 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`FilterableMultiSelect should render 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/packages/react/src/components/Notification/Notification-story.js b/packages/react/src/components/Notification/Notification-story.js
deleted file mode 100644
index dfda68894760..000000000000
--- a/packages/react/src/components/Notification/Notification-story.js
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * 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 { action } from '@storybook/addon-actions';
-import {
- withKnobs,
- boolean,
- number,
- select,
- text,
-} from '@storybook/addon-knobs';
-import {
- ToastNotification,
- InlineNotification,
- NotificationActionButton,
-} from './Notification';
-import mdx from './Notification.mdx';
-
-const kinds = {
- 'Error (error)': 'error',
- 'Info (info)': 'info',
- 'Info square (info-square)': 'info-square',
- 'Success (success)': 'success',
- 'Warning (warning)': 'warning',
- 'Warning (warning-alt)': 'warning-alt',
-};
-const notificationProps = () => ({
- kind: select('The notification kind (kind)', kinds, 'info'),
- lowContrast: boolean('Use low contrast variant (lowContrast)', false),
- role: text('ARIA role (role)', 'alert'),
- title: text('Title (title)', 'Notification title'),
- subtitle: text('Subtitle (subtitle)', 'Subtitle text goes here.'),
- iconDescription: text(
- 'Icon description (iconDescription)',
- 'describes the close button'
- ),
- statusIconDescription: text(
- 'Status icon description (statusIconDescription)',
- 'describes the status icon'
- ),
- hideCloseButton: boolean('Hide close button (hideCloseButton)', false),
- onClose: action('onClose'),
- onCloseButtonClick: action('onCloseButtonClick'),
-});
-
-const toastNotificationProps = () => ({
- ...notificationProps(),
- timeout: number(
- 'Duration in milliseconds to display notification (timeout)',
- 0
- ),
-});
-
-export default {
- title: 'Components/Notifications',
- component: ToastNotification,
- decorators: [withKnobs],
- subcomponents: {
- InlineNotification,
- },
- parameters: {
- docs: {
- page: mdx,
- },
- },
-};
-
-export const Toast = () => (
-
-);
-
-export const Inline = () => (
-
- {text('Action (NotificationActionButton > children)', 'Action')}
-
- }
- />
-);
-
-Inline.storyName = 'inline';
diff --git a/packages/react/src/components/Notification/Notification-test.js b/packages/react/src/components/Notification/Notification-test.js
index 85b8f1c6a5e3..e09faebde6e1 100644
--- a/packages/react/src/components/Notification/Notification-test.js
+++ b/packages/react/src/components/Notification/Notification-test.js
@@ -6,356 +6,337 @@
*/
import React from 'react';
-import { Close, ErrorFilled, CheckmarkFilled } from '@carbon/icons-react';
+import { ErrorFilled } from '@carbon/icons-react';
import {
NotificationButton,
- NotificationTextDetails,
ToastNotification,
InlineNotification,
+ ActionableNotification,
} from './Notification';
-import { shallow, mount } from 'enzyme';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { it } from 'window-or-global';
const prefix = 'cds';
describe('NotificationButton', () => {
- describe('Renders as expected', () => {
- const wrapper = shallow();
-
- it('renders given className', () => {
- expect(wrapper.hasClass('some-class')).toBe(true);
- });
-
- it('renders only one Icon', () => {
- const icon = wrapper.find(Close);
- expect(icon.length).toEqual(1);
- });
+ it('should place the `className` prop on the outermost DOM node', () => {
+ const { container } = render();
+ expect(container.firstChild).toHaveClass('test');
+ });
- it('supports custom icon', () => {
- const iconButton = mount(
- }
- />
- );
- const originalIcon = mount().find('svg');
- const icon = iconButton.find('svg');
- expect(icon.find(':not(svg):not(title)').html()).toBe(
- originalIcon.children().html()
- );
- });
+ it('supports custom icon', () => {
+ const { rerender } = render();
+ const defaultIcon = screen.queryByRole('button').innerHTML;
- describe('When notificationType equals "toast"', () => {
- it('button should have correct className by default', () => {
- expect(
- wrapper.hasClass(`${prefix}--toast-notification__close-button`)
- ).toBe(true);
- });
-
- it('icon should have correct className by default', () => {
- const icon = wrapper.find(Close);
- expect(icon.hasClass(`${prefix}--toast-notification__close-icon`)).toBe(
- true
- );
- });
- });
+ rerender(
+ }
+ />
+ );
+ const customIcon = screen.queryByRole('button').innerHTML;
- describe('When notificationType equals "inline"', () => {
- it('button should have correct className', () => {
- wrapper.setProps({ notificationType: 'inline' });
- expect(
- wrapper.hasClass(`${prefix}--inline-notification__close-button`)
- ).toBe(true);
- });
-
- it('icon should have correct className', () => {
- const icon = wrapper.find(Close);
- expect(
- icon.hasClass(`${prefix}--inline-notification__close-icon`)
- ).toBe(true);
- });
- });
+ expect(defaultIcon).not.toEqual(customIcon);
});
-});
-
-describe('NotificationTextDetails', () => {
- describe('Renders as expected', () => {
- const wrapper = shallow();
- describe('When notificationType equals "toast"', () => {
- it('div should have correct className by default', () => {
- expect(wrapper.hasClass(`${prefix}--toast-notification__details`)).toBe(
- true
- );
- });
- });
+ it('interpolates matching className based on notificationType prop', () => {
+ const { rerender, container } = render();
+ const notificationTypes = ['toast', 'inline'];
- describe('When notificationType equals "inline"', () => {
- it('div should have correct className', () => {
- wrapper.setProps({ notificationType: 'inline' });
- expect(
- wrapper.hasClass(`${prefix}--inline-notification__text-wrapper`)
- ).toBe(true);
- });
+ notificationTypes.forEach((notificationType) => {
+ rerender();
+ expect(container.firstChild).toHaveClass(
+ `${prefix}--${notificationType}-notification__close-button`
+ );
+ expect(screen.queryByRole('button').firstChild).toHaveClass(
+ `${prefix}--${notificationType}-notification__close-icon`
+ );
});
});
});
describe('ToastNotification', () => {
- describe('Renders as expected', () => {
- const toast = shallow(
-
+ it('should have role=status by default', () => {
+ const { container } = render(
+
);
- it('renders itself', () => {
- expect(toast.length).toEqual(1);
- });
+ expect(container.firstChild).toHaveAttribute('role', 'status');
+ });
- it('renders HTML for toast notifications when caption exists', () => {
- expect(toast.hasClass(`${prefix}--toast-notification`)).toBe(true);
+ it('should place the `className` prop on the outermost DOM node', () => {
+ const { container } = render(
+
+ );
+ expect(container.firstChild).toHaveClass('test');
+ });
+
+ it('interpolates matching className based on kind prop', () => {
+ const { rerender } = render(
+
+ );
+ const kinds = [
+ 'error',
+ 'info',
+ 'info-square',
+ 'success',
+ 'warning',
+ 'warning-alt',
+ ];
+ kinds.forEach((kind) => {
+ rerender();
+ expect(screen.queryByRole('status')).toHaveClass(
+ `${prefix}--toast-notification--${kind}`
+ );
});
+ });
- it('adds extra classes via className', () => {
- toast.setProps({ className: 'extra-class' });
+ it('allows non-interactive elements as children', () => {
+ render(
+
+ Sample text
+
+ );
+ expect(screen.queryByText(/Sample text/i)).toBeInTheDocument();
+ });
- expect(toast.hasClass('extra-class')).toBe(true);
- });
+ it('does not allow interactive elements as children', () => {
+ const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
- it('interpolates matching className based on kind prop', () => {
- const kinds = ['error', 'info', 'success', 'warning'];
+ expect(() => {
+ render(
+
+
+
+ );
+ }).toThrow();
- kinds.forEach((kind) => {
- toast.setProps({ kind });
- expect(
- toast.hasClass(`${prefix}--toast-notification--${kind}`)
- ).toEqual(true);
- });
- });
+ expect(spy).toHaveBeenCalled();
+ spy.mockRestore();
+ });
- it('has [role="alert"] on wrapping ', () => {
- expect(toast.props().role).toEqual('alert');
- });
+ it('close button is rendered by default and includes aria-hidden=true', () => {
+ render(
);
- it('sets a new kind when passed in via props', () => {
- toast.setProps({ kind: 'success' });
- expect(toast.props().kind).toEqual('success');
+ const closeButton = screen.queryByRole('button', {
+ hidden: true,
});
- it('can render any node for the subtitle and caption', () => {
- toast.setProps({
- subtitle:
,
- caption:
,
- });
- expect(toast.length).toEqual(1);
- });
+ expect(closeButton).toBeInTheDocument();
+ expect(closeButton).toHaveAttribute('aria-hidden', 'true');
});
- describe('events and state', () => {
- it('initial open state set to true', () => {
- const wrapper = mount(
-
- );
- expect(wrapper.children().length > 0).toBe(true);
+ it('does not render close button when `hideCloseButton` is provided', () => {
+ render(
);
+ const closeButton = screen.queryByRole('button', {
+ hidden: true,
});
+ expect(closeButton).not.toBeInTheDocument();
+ });
- it('sets open state to false when close button is clicked', () => {
- const wrapper = mount(
-
- );
+ it('calls `onClose` when notification is closed', async () => {
+ const onClose = jest.fn();
+ render(
);
- wrapper.find('button').simulate('click');
- expect(wrapper.children().length).toBe(0);
+ const closeButton = screen.queryByRole('button', {
+ hidden: true,
});
-
- it('closes notification if `onClose` is provided', () => {
- const wrapper = mount(
-
{}}
- />
- );
-
- wrapper.find('button').simulate('click');
- expect(wrapper.children().length).toBe(0);
+ userEvent.click(closeButton);
+ expect(onClose).toHaveBeenCalledTimes(1);
+ await waitFor(() => {
+ expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
+ });
- it('keeps notification open if `onClose` returns false', () => {
- const wrapper = mount(
- false}
- />
- );
+ it('keeps notification open if `onClose` returns false', () => {
+ render(
+ false} />
+ );
- wrapper.find('button').simulate('click');
- expect(wrapper.children().length).not.toBe(0);
+ const closeButton = screen.queryByRole('button', {
+ hidden: true,
});
+ userEvent.click(closeButton);
+ expect(screen.queryByRole('status')).toBeInTheDocument();
+ });
- it('renders null when open state is false', () => {
- const wrapper = mount(
-
- );
+ it('calls `onCloseButtonClick` when notification is closed', () => {
+ const onCloseButtonClick = jest.fn();
+ render(
+
+ );
- wrapper.find('button').simulate('click');
- expect(wrapper.html()).toBeNull();
+ const closeButton = screen.queryByRole('button', {
+ hidden: true,
});
+ userEvent.click(closeButton);
+ expect(onCloseButtonClick).toHaveBeenCalledTimes(1);
});
});
describe('InlineNotification', () => {
- describe('Renders as expected', () => {
- const inline = mount(
-
+ it('should have role=status by default', () => {
+ const { container } = render(
+
);
+ expect(container.firstChild).toHaveAttribute('role', 'status');
+ });
- it('renders itself', () => {
- expect(inline.length).toEqual(1);
- });
+ it('should place the `className` prop on the outermost DOM node', () => {
+ const { container } = render(
+
+ );
+ expect(container.firstChild).toHaveClass('test');
+ });
- it('renders success notification with matching kind and values', () => {
- inline.setProps({ kind: 'success' });
- expect(inline.find(CheckmarkFilled).length).toBe(1);
+ it('interpolates matching className based on kind prop', () => {
+ const { rerender } = render(
+
+ );
+ const kinds = [
+ 'error',
+ 'info',
+ 'info-square',
+ 'success',
+ 'warning',
+ 'warning-alt',
+ ];
+ kinds.forEach((kind) => {
+ rerender();
+ expect(screen.queryByRole('status')).toHaveClass(
+ `${prefix}--inline-notification--${kind}`
+ );
});
+ });
- it('renders error notification with matching kind and values', () => {
- inline.setProps({ kind: 'error' });
- expect(inline.find(ErrorFilled).length).toBe(1);
- });
+ it('allows non-interactive elements as children', () => {
+ render(
+
+ Sample text
+
+ );
+ expect(screen.queryByText(/Sample text/i)).toBeInTheDocument();
+ });
- it('renders warning notification with matching kind and values', () => {
- inline.setProps({ kind: 'warning' });
- expect(
- inline.find(`.${prefix}--inline-notification__icon`).exists()
- ).toBe(true);
- });
+ it('does not allow interactive elements as children', () => {
+ const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
- it('renders HTML for inline notifications when caption does not exist', () => {
- expect(inline.find(`.${prefix}--inline-notification`).exists()).toBe(
- true
+ expect(() => {
+ render(
+
+
+
);
- });
+ }).toThrow();
- it('adds extra classes via className', () => {
- inline.setProps({ className: 'extra-class' });
- expect(inline.find('.extra-class').exists()).toBe(true);
- });
+ expect(spy).toHaveBeenCalled();
+ spy.mockRestore();
+ });
- it('interpolates matching className based on kind prop', () => {
- const kinds = ['error', 'info', 'success', 'warning'];
+ it('close button is rendered by default and includes aria-hidden=true', () => {
+ render();
- kinds.forEach((kind) => {
- inline.setProps({ kind });
- expect(
- inline.find(`.${prefix}--inline-notification--${kind}`).exists()
- ).toEqual(true);
- });
+ const closeButton = screen.queryByRole('button', {
+ hidden: true,
});
+ expect(closeButton).toBeInTheDocument();
+ expect(closeButton).toHaveAttribute('aria-hidden', 'true');
+ });
- it('has [role="alert"] on wrapping ', () => {
- expect(inline.props().role).toEqual('alert');
+ it('does not render close button when `hideCloseButton` is provided', () => {
+ render(
);
+ const closeButton = screen.queryByRole('button', {
+ hidden: true,
});
+ expect(closeButton).not.toBeInTheDocument();
+ });
- it('sets a new kind when passed in via props', () => {
- inline.setProps({ kind: 'success' });
- expect(inline.props().kind).toEqual('success');
- });
+ it('calls `onClose` when notification is closed', async () => {
+ const onClose = jest.fn();
+ render(
);
- it('can render any node for the subtitle', () => {
- inline.setProps({ subtitle:
});
- expect(inline.length).toEqual(1);
+ const closeButton = screen.queryByRole('button', {
+ hidden: true,
+ });
+ userEvent.click(closeButton);
+ expect(onClose).toHaveBeenCalledTimes(1);
+ await waitFor(() => {
+ expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
});
- describe('events and state', () => {
- it('initial open state set to true', () => {
- const wrapper = mount(
-
- );
+ it('keeps notification open if `onClose` returns false', () => {
+ render(
+
false} />
+ );
- expect(wrapper.children().length > 0).toBe(true);
+ const closeButton = screen.queryByRole('button', {
+ hidden: true,
});
+ userEvent.click(closeButton);
+ expect(screen.queryByRole('status')).toBeInTheDocument();
+ });
- it('sets open state to false when close button is clicked', () => {
- const wrapper = mount(
-
- );
+ it('calls `onCloseButtonClick` when notification is closed', () => {
+ const onCloseButtonClick = jest.fn();
+ render(
+
+ );
- wrapper.find('button').simulate('click');
- expect(wrapper.children().length).toBe(0);
+ const closeButton = screen.queryByRole('button', {
+ hidden: true,
});
+ userEvent.click(closeButton);
+ expect(onCloseButtonClick).toHaveBeenCalledTimes(1);
+ });
+});
- it('closes notification if `onClose` is provided', () => {
- const wrapper = mount(
- {}}
- />
- );
+describe('ActionableNotification', () => {
+ it('uses role=alertdialog', () => {
+ const { container } = render(
+
+ );
+
+ expect(container.firstChild).toHaveAttribute('role', 'alertdialog');
+ });
- wrapper.find('button').simulate('click');
- expect(wrapper.children().length).toBe(0);
+ it('renders correct action label', () => {
+ render();
+ const actionButton = screen.queryByRole('button', {
+ name: 'My custom action',
});
+ expect(actionButton).toBeInTheDocument();
+ });
- it('keeps notification open if `onClose` returns false', () => {
- const wrapper = mount(
- false}
- />
- );
+ it('closes notification via escape button when focus is placed on the notification', async () => {
+ const onCloseButtonClick = jest.fn();
+ const onClose = jest.fn();
+ render(
+
+ );
- wrapper.find('button').simulate('click');
- expect(wrapper.children().length).not.toBe(0);
- });
+ // without focus being on/in the notification, it should not close via escape
+ userEvent.keyboard('{Escape}');
+ expect(onCloseButtonClick).toHaveBeenCalledTimes(0);
+ expect(onClose).toHaveBeenCalledTimes(0);
- it('renders null when open state is false', () => {
- const wrapper = mount(
-
- );
+ // after focus is placed, the notification should close via escape
+ userEvent.tab();
+ userEvent.keyboard('{Escape}');
+ expect(onCloseButtonClick).toHaveBeenCalledTimes(1);
+ expect(onClose).toHaveBeenCalledTimes(1);
- wrapper.find('button').simulate('click');
- expect(wrapper.html()).toBeNull();
+ await waitFor(() => {
+ expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
});
});
});
-
-// Deprecated
diff --git a/packages/react/src/components/Notification/Notification.js b/packages/react/src/components/Notification/Notification.js
index ac312382a396..abac8ecaee39 100644
--- a/packages/react/src/components/Notification/Notification.js
+++ b/packages/react/src/components/Notification/Notification.js
@@ -17,27 +17,55 @@ import {
InformationFilled,
InformationSquareFilled,
} from '@carbon/icons-react';
+
import Button from '../Button';
+import useIsomorphicEffect from '../../internal/useIsomorphicEffect';
+import { useNoInteractiveChildren } from '../../internal/useNoInteractiveChildren';
+import { keys, matches } from '../../internal/keyboard';
import { usePrefix } from '../../internal/usePrefix';
+/**
+ * Conditionally call a callback when the escape key is pressed
+ * @param {node} ref - ref of the container element to scope the functionality to
+ * @param {func} callback - function to be called
+ * @param {bool} override - escape hatch to conditionally call the callback
+ */
+function useEscapeToClose(ref, callback, override = true) {
+ const handleKeyDown = (event) => {
+ // The callback should only be called when focus is on or within the container
+ const elementContainsFocus =
+ (ref.current && document.activeElement === ref.current) ||
+ ref.current.contains(document.activeElement);
+
+ if (matches(event, [keys.Escape]) && override && elementContainsFocus) {
+ callback(event);
+ }
+ };
+
+ useIsomorphicEffect(() => {
+ document.addEventListener('keydown', handleKeyDown, false);
+ return () => document.removeEventListener('keydown', handleKeyDown, false);
+ });
+}
+
export function NotificationActionButton({
children,
className: customClassName,
onClick,
+ inline,
...rest
}) {
const prefix = usePrefix();
- const className = cx(
- customClassName,
- `${prefix}--inline-notification__action-button`
- );
+ const className = cx(customClassName, {
+ [`${prefix}--actionable-notification__action-button`]: true,
+ });
return (
@@ -55,6 +83,11 @@ NotificationActionButton.propTypes = {
*/
className: PropTypes.string,
+ /**
+ * Specify if the visual treatment of the button should be for an inline notification
+ */
+ inline: PropTypes.bool,
+
/**
* Optionally specify a click handler for the notification action button.
*/
@@ -64,7 +97,6 @@ NotificationActionButton.propTypes = {
export function NotificationButton({
ariaLabel,
className,
- iconDescription,
type,
renderIcon: IconTag,
name,
@@ -86,12 +118,10 @@ export function NotificationButton({
{...rest}
// eslint-disable-next-line react/button-has-type
type={type}
- aria-label={iconDescription}
- title={iconDescription}
+ aria-label={ariaLabel}
+ title={ariaLabel}
className={buttonClassName}>
- {IconTag && (
-
- )}
+ {IconTag && }
);
}
@@ -107,11 +137,6 @@ NotificationButton.propTypes = {
*/
className: PropTypes.string,
- /**
- * Provide a description for "close" icon that can be read by screen readers
- */
- iconDescription: PropTypes.string,
-
/**
* Specify an optional icon for the Button through a string,
* if something but regular "close" icon is desirable
@@ -121,7 +146,7 @@ NotificationButton.propTypes = {
/**
* Specify the notification type
*/
- notificationType: PropTypes.oneOf(['toast', 'inline']),
+ notificationType: PropTypes.oneOf(['toast', 'inline', 'actionable']),
/**
* Optional prop to allow overriding the icon rendering.
@@ -136,80 +161,12 @@ NotificationButton.propTypes = {
};
NotificationButton.defaultProps = {
- ariaLabel: 'close notification', // TODO: deprecate this prop
+ ariaLabel: 'close notification',
notificationType: 'toast',
type: 'button',
- iconDescription: 'close icon',
renderIcon: Close,
};
-export function NotificationTextDetails({
- title,
- subtitle,
- caption,
- notificationType,
- children,
- ...rest
-}) {
- const prefix = usePrefix();
- if (notificationType === 'toast') {
- return (
-
-
{title}
-
- {subtitle}
-
- {caption && (
-
- {caption}
-
- )}
- {children}
-
- );
- }
-
- if (notificationType === 'inline') {
- return (
-
-
{title}
-
- {subtitle}
-
- {children}
-
- );
- }
-}
-
-NotificationTextDetails.propTypes = {
- /**
- * Specify the caption
- */
- caption: PropTypes.node,
- /**
- * Pass in the children that will be rendered in NotificationTextDetails
- */
- children: PropTypes.node,
- /**
- * Specify the notification type
- */
- notificationType: PropTypes.oneOf(['toast', 'inline']),
- /**
- * Specify the sub-title
- */
- subtitle: PropTypes.node,
- /**
- * Specify the title
- */
- title: PropTypes.string,
-};
-
-NotificationTextDetails.defaultProps = {
- title: 'title',
- notificationType: 'toast',
-};
-
const iconTypes = {
error: ErrorFilled,
success: CheckmarkFilled,
@@ -248,35 +205,37 @@ NotificationIcon.propTypes = {
export function ToastNotification({
role,
- notificationType,
onClose,
onCloseButtonClick,
- iconDescription,
statusIconDescription,
className,
- caption,
- subtitle,
- title,
+ children,
kind,
lowContrast,
hideCloseButton,
- children,
timeout,
+ title,
+ caption,
+ subtitle,
...rest
}) {
- const prefix = usePrefix();
const [isOpen, setIsOpen] = useState(true);
+ const prefix = usePrefix();
const containerClassName = cx(className, {
[`${prefix}--toast-notification`]: true,
[`${prefix}--toast-notification--low-contrast`]: lowContrast,
[`${prefix}--toast-notification--${kind}`]: kind,
});
+ const contentRef = useRef(null);
+ useNoInteractiveChildren(contentRef);
+
const handleClose = (evt) => {
if (!onClose || onClose(evt) !== false) {
setIsOpen(false);
}
};
+ const ref = useRef(null);
function handleCloseButtonClick(event) {
onCloseButtonClick(event);
@@ -310,24 +269,36 @@ export function ToastNotification({
}
return (
-
+
-
+
+ {title && (
+
{title}
+ )}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+ {caption && (
+
+ {caption}
+
+ )}
{children}
-
+
{!hideCloseButton && (
)}
@@ -335,13 +306,18 @@ export function ToastNotification({
}
ToastNotification.propTypes = {
+ /**
+ * Provide a description for "close" icon button that can be read by screen readers
+ */
+ ariaLabel: PropTypes.string,
+
/**
* Specify the caption
*/
- caption: PropTypes.node,
+ caption: PropTypes.string,
/**
- * Pass in the children that will be rendered within the ToastNotification
+ * Specify the content
*/
children: PropTypes.node,
@@ -355,11 +331,6 @@ ToastNotification.propTypes = {
*/
hideCloseButton: PropTypes.bool,
- /**
- * Provide a description for "close" icon that can be read by screen readers
- */
- iconDescription: PropTypes.string,
-
/**
* Specify what state the notification represents
*/
@@ -370,19 +341,13 @@ ToastNotification.propTypes = {
'success',
'warning',
'warning-alt',
- ]).isRequired,
+ ]),
/**
* Specify whether you are using the low contrast variant of the ToastNotification.
*/
lowContrast: PropTypes.bool,
- /**
- * By default, this value is "toast". You can also provide an alternate type
- * if it makes sense for the underlying `
` and ``
- */
- notificationType: PropTypes.string,
-
/**
* Provide a function that is called when menu is closed
*/
@@ -394,10 +359,10 @@ ToastNotification.propTypes = {
onCloseButtonClick: PropTypes.func,
/**
- * By default, this value is "alert". You can also provide an alternate
+ * By default, this value is "status". You can also provide an alternate
* role if it makes sense from the accessibility-side
*/
- role: PropTypes.string.isRequired,
+ role: PropTypes.oneOf(['alert', 'log', 'status']),
/**
* Provide a description for "status" icon that can be read by screen readers
@@ -407,7 +372,7 @@ ToastNotification.propTypes = {
/**
* Specify the sub-title
*/
- subtitle: PropTypes.node,
+ subtitle: PropTypes.string,
/**
* Specify an optional duration the notification should be closed in
@@ -417,39 +382,33 @@ ToastNotification.propTypes = {
/**
* Specify the title
*/
- title: PropTypes.string.isRequired,
+ title: PropTypes.string,
};
ToastNotification.defaultProps = {
kind: 'error',
- title: 'provide a title',
- role: 'alert',
- notificationType: 'toast',
- iconDescription: 'closes notification',
+ role: 'status',
onCloseButtonClick: () => {},
hideCloseButton: false,
timeout: 0,
};
export function InlineNotification({
- actions,
+ children,
+ title,
+ subtitle,
role,
- notificationType,
onClose,
onCloseButtonClick,
- iconDescription,
statusIconDescription,
className,
- subtitle,
- title,
kind,
lowContrast,
hideCloseButton,
- children,
...rest
}) {
- const prefix = usePrefix();
const [isOpen, setIsOpen] = useState(true);
+ const prefix = usePrefix();
const containerClassName = cx(className, {
[`${prefix}--inline-notification`]: true,
[`${prefix}--inline-notification--low-contrast`]: lowContrast,
@@ -457,11 +416,15 @@ export function InlineNotification({
[`${prefix}--inline-notification--hide-close-button`]: hideCloseButton,
});
+ const contentRef = useRef(null);
+ useNoInteractiveChildren(contentRef);
+
const handleClose = (evt) => {
if (!onClose || onClose(evt) !== false) {
setIsOpen(false);
}
};
+ const ref = useRef(null);
function handleCloseButtonClick(event) {
onCloseButtonClick(event);
@@ -473,26 +436,35 @@ export function InlineNotification({
}
return (
-
+
-
+
+ {title && (
+
+ {title}
+
+ )}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
{children}
-
+
- {actions}
{!hideCloseButton && (
)}
@@ -501,12 +473,187 @@ export function InlineNotification({
InlineNotification.propTypes = {
/**
- * Pass in the action nodes that will be rendered within the InlineNotification
+ * Specify the content
+ */
+ children: PropTypes.node,
+
+ /**
+ * Specify an optional className to be applied to the notification box
+ */
+ className: PropTypes.string,
+
+ /**
+ * Specify the close button should be disabled, or not
+ */
+ hideCloseButton: PropTypes.bool,
+
+ /**
+ * Specify what state the notification represents
+ */
+ kind: PropTypes.oneOf([
+ 'error',
+ 'info',
+ 'info-square',
+ 'success',
+ 'warning',
+ 'warning-alt',
+ ]),
+
+ /**
+ * Specify whether you are using the low contrast variant of the InlineNotification.
+ */
+ lowContrast: PropTypes.bool,
+
+ /**
+ * Provide a function that is called when menu is closed
+ */
+ onClose: PropTypes.func,
+
+ /**
+ * Provide a function that is called when the close button is clicked
+ */
+ onCloseButtonClick: PropTypes.func,
+
+ /**
+ * By default, this value is "status". You can also provide an alternate
+ * role if it makes sense from the accessibility-side.
+ */
+ role: PropTypes.oneOf(['alert', 'log', 'status']),
+
+ /**
+ * Provide a description for "status" icon that can be read by screen readers
+ */
+ statusIconDescription: PropTypes.string,
+
+ /**
+ * Specify the sub-title
+ */
+ subtitle: PropTypes.string,
+
+ /**
+ * Specify the title
+ */
+ title: PropTypes.string,
+};
+
+InlineNotification.defaultProps = {
+ kind: 'error',
+ role: 'status',
+ onCloseButtonClick: () => {},
+ hideCloseButton: false,
+};
+
+export function ActionableNotification({
+ actionButtonLabel,
+ ariaLabel,
+ children,
+ role,
+ onActionButtonClick,
+ onClose,
+ onCloseButtonClick,
+ statusIconDescription,
+ className,
+ inline,
+ kind,
+ lowContrast,
+ hideCloseButton,
+ hasFocus,
+ closeOnEscape,
+ title,
+ subtitle,
+ ...rest
+}) {
+ const [isOpen, setIsOpen] = useState(true);
+ const prefix = usePrefix();
+ const containerClassName = cx(className, {
+ [`${prefix}--actionable-notification`]: true,
+ [`${prefix}--actionable-notification--toast`]: !inline,
+ [`${prefix}--actionable-notification--low-contrast`]: lowContrast,
+ [`${prefix}--actionable-notification--${kind}`]: kind,
+ [`${prefix}--actionable-notification--hide-close-button`]: hideCloseButton,
+ });
+
+ const ref = useRef(null);
+ useIsomorphicEffect(() => {
+ if (ref.current && hasFocus) {
+ ref.current.focus();
+ }
+ });
+
+ const handleClose = (evt) => {
+ if (!onClose || onClose(evt) !== false) {
+ setIsOpen(false);
+ }
+ };
+ useEscapeToClose(ref, handleCloseButtonClick, closeOnEscape);
+
+ function handleCloseButtonClick(event) {
+ onCloseButtonClick(event);
+ handleClose(event);
+ }
+
+ if (!isOpen) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ {title && (
+
+ {title}
+
+ )}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+ {children}
+
+
+
+
+
+ {actionButtonLabel}
+
+
+ {!hideCloseButton && (
+
+ )}
+
+ );
+}
+
+ActionableNotification.propTypes = {
+ /**
+ * Pass in the action button label that will be rendered within the ActionableNotification.
+ */
+ actionButtonLabel: PropTypes.string.isRequired,
+
+ /**
+ * Provide a description for "close" icon button that can be read by screen readers
*/
- actions: PropTypes.node,
+ ariaLabel: PropTypes.string,
/**
- * Pass in the children that will be rendered within the InlineNotification
+ * Specify the caption
+ */
+ caption: PropTypes.string,
+
+ /**
+ * Specify the content
*/
children: PropTypes.node,
@@ -515,15 +662,25 @@ InlineNotification.propTypes = {
*/
className: PropTypes.string,
+ /**
+ * Specify if pressing the escape key should close notifications
+ */
+ closeOnEscape: PropTypes.bool,
+
+ /**
+ * Specify if focus should be moved to the component when the notification contains actions
+ */
+ hasFocus: PropTypes.bool,
+
/**
* Specify the close button should be disabled, or not
*/
hideCloseButton: PropTypes.bool,
- /**
- * Provide a description for "close" icon that can be read by screen readers
+ /*
+ * Specify if the notification should have inline styling applied instead of toast
*/
- iconDescription: PropTypes.string,
+ inline: PropTypes.bool,
/**
* Specify what state the notification represents
@@ -538,15 +695,14 @@ InlineNotification.propTypes = {
]).isRequired,
/**
- * Specify whether you are using the low contrast variant of the InlineNotification.
+ * Specify whether you are using the low contrast variant of the ActionableNotification.
*/
lowContrast: PropTypes.bool,
/**
- * By default, this value is "inline". You can also provide an alternate type
- * if it makes sense for the underlying `
` and ``
+ * Provide a function that is called when the action is clicked
*/
- notificationType: PropTypes.string,
+ onActionButtonClick: PropTypes.func,
/**
* Provide a function that is called when menu is closed
@@ -559,10 +715,10 @@ InlineNotification.propTypes = {
onCloseButtonClick: PropTypes.func,
/**
- * By default, this value is "alert". You can also provide an alternate
- * role if it makes sense from the accessibility-side
+ * By default, this value is "alertdialog". You can also provide an alternate
+ * role if it makes sense from the accessibility-side.
*/
- role: PropTypes.string.isRequired,
+ role: PropTypes.string,
/**
* Provide a description for "status" icon that can be read by screen readers
@@ -572,18 +728,20 @@ InlineNotification.propTypes = {
/**
* Specify the sub-title
*/
- subtitle: PropTypes.node,
+ subtitle: PropTypes.string,
/**
* Specify the title
*/
- title: PropTypes.string.isRequired,
+ title: PropTypes.string,
};
-InlineNotification.defaultProps = {
- role: 'alert',
- notificationType: 'inline',
- iconDescription: 'closes notification',
+ActionableNotification.defaultProps = {
+ kind: 'error',
+ role: 'alertdialog',
onCloseButtonClick: () => {},
hideCloseButton: false,
+ hasFocus: true,
+ closeOnEscape: true,
+ inline: false,
};
diff --git a/packages/react/src/components/Notification/Notification.mdx b/packages/react/src/components/Notification/Notification.mdx
index 477227b95415..eea2568a58f9 100644
--- a/packages/react/src/components/Notification/Notification.mdx
+++ b/packages/react/src/components/Notification/Notification.mdx
@@ -1,4 +1,4 @@
-import { Props } from '@storybook/addon-docs';
+import { Story, Preview, Props } from '@storybook/addon-docs';
# Notification
@@ -10,12 +10,39 @@ import { Props } from '@storybook/addon-docs';
## Table of Contents
+- [Overview](#overview)
+- [Component API](#component-api)
+- [Actionable](#actionable)
+- [References](#references)
+- [Feedback](#feedback)
+
## Overview
+There are 3 different types of notification components:
+`ActionableNotification`, `InlineNotification`, and `ToastNotification`.
+
## Component API
+## Actionable
+
+
+
+
+
+## Inline
+
+
+
+
+
+## Toast
+
+
+
+
+
## Feedback
Help us improve this component by providing feedback, asking questions on Slack,
diff --git a/packages/react/src/components/Notification/index.js b/packages/react/src/components/Notification/index.js
index 7d060a62edc0..867e40e34e7b 100644
--- a/packages/react/src/components/Notification/index.js
+++ b/packages/react/src/components/Notification/index.js
@@ -5,53 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
-import {
- NotificationActionButton as NotificationActionButtonNext,
- NotificationButton as NotificationButtonNext,
- ToastNotification as ToastNotificationNext,
- InlineNotification as InlineNotificationNext,
- ActionableNotification as ActionableNotificationNext,
-} from './next/Notification';
-import {
- NotificationActionButton as NotificationActionButtonClassic,
- NotificationTextDetails as NotificationTextDetailsClassic,
- NotificationButton as NotificationButtonClassic,
- ToastNotification as ToastNotificationClassic,
- InlineNotification as InlineNotificationClassic,
+export {
+ NotificationActionButton,
+ NotificationButton,
+ ToastNotification,
+ InlineNotification,
+ ActionableNotification,
} from './Notification';
-import { createComponentToggle } from '../../internal/ComponentToggle';
-
-export const NotificationActionButton = createComponentToggle({
- name: 'NotificationActionButton',
- next: NotificationActionButtonNext,
- classic: NotificationActionButtonClassic,
-});
-
-export const NotificationTextDetails = createComponentToggle({
- name: 'NotificationTextDetails',
- classic: NotificationTextDetailsClassic,
-});
-
-export const NotificationButton = createComponentToggle({
- name: 'NotificationButton',
- next: NotificationButtonNext,
- classic: NotificationButtonClassic,
-});
-
-export const ToastNotification = createComponentToggle({
- name: 'ToastNotification',
- next: ToastNotificationNext,
- classic: ToastNotificationClassic,
-});
-
-export const InlineNotification = createComponentToggle({
- name: 'InlineNotification',
- next: InlineNotificationNext,
- classic: InlineNotificationClassic,
-});
-
-export const ActionableNotification = createComponentToggle({
- name: 'ActionableNotification',
- next: ActionableNotificationNext,
- classic: null,
-});
diff --git a/packages/react/src/components/Notification/next/Notification-test.js b/packages/react/src/components/Notification/next/Notification-test.js
deleted file mode 100644
index eacac26e2c6c..000000000000
--- a/packages/react/src/components/Notification/next/Notification-test.js
+++ /dev/null
@@ -1,342 +0,0 @@
-/**
- * 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 { ErrorFilled } from '@carbon/icons-react';
-import {
- NotificationButton,
- ToastNotification,
- InlineNotification,
- ActionableNotification,
-} from '../next/Notification';
-import { render, screen, waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { it } from 'window-or-global';
-
-const prefix = 'cds';
-
-describe('NotificationButton', () => {
- it('should place the `className` prop on the outermost DOM node', () => {
- const { container } = render();
- expect(container.firstChild).toHaveClass('test');
- });
-
- it('supports custom icon', () => {
- const { rerender } = render();
- const defaultIcon = screen.queryByRole('button').innerHTML;
-
- rerender(
- }
- />
- );
- const customIcon = screen.queryByRole('button').innerHTML;
-
- expect(defaultIcon).not.toEqual(customIcon);
- });
-
- it('interpolates matching className based on notificationType prop', () => {
- const { rerender, container } = render();
- const notificationTypes = ['toast', 'inline'];
-
- notificationTypes.forEach((notificationType) => {
- rerender();
- expect(container.firstChild).toHaveClass(
- `${prefix}--${notificationType}-notification__close-button`
- );
- expect(screen.queryByRole('button').firstChild).toHaveClass(
- `${prefix}--${notificationType}-notification__close-icon`
- );
- });
- });
-});
-
-describe('ToastNotification', () => {
- it('should have role=status by default', () => {
- const { container } = render(
-
- );
- expect(container.firstChild).toHaveAttribute('role', 'status');
- });
-
- it('should place the `className` prop on the outermost DOM node', () => {
- const { container } = render(
-
- );
- expect(container.firstChild).toHaveClass('test');
- });
-
- it('interpolates matching className based on kind prop', () => {
- const { rerender } = render(
-
- );
- const kinds = [
- 'error',
- 'info',
- 'info-square',
- 'success',
- 'warning',
- 'warning-alt',
- ];
- kinds.forEach((kind) => {
- rerender();
- expect(screen.queryByRole('status')).toHaveClass(
- `${prefix}--toast-notification--${kind}`
- );
- });
- });
-
- it('allows non-interactive elements as children', () => {
- render(
-
- Sample text
-
- );
- expect(screen.queryByText(/Sample text/i)).toBeInTheDocument();
- });
-
- it('does not allow interactive elements as children', () => {
- const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
-
- expect(() => {
- render(
-
-
-
- );
- }).toThrow();
-
- expect(spy).toHaveBeenCalled();
- spy.mockRestore();
- });
-
- it('close button is rendered by default and includes aria-hidden=true', () => {
- render();
-
- const closeButton = screen.queryByRole('button', {
- hidden: true,
- });
-
- expect(closeButton).toBeInTheDocument();
- expect(closeButton).toHaveAttribute('aria-hidden', 'true');
- });
-
- it('does not render close button when `hideCloseButton` is provided', () => {
- render();
- const closeButton = screen.queryByRole('button', {
- hidden: true,
- });
- expect(closeButton).not.toBeInTheDocument();
- });
-
- it('calls `onClose` when notification is closed', async () => {
- const onClose = jest.fn();
- render();
-
- const closeButton = screen.queryByRole('button', {
- hidden: true,
- });
- userEvent.click(closeButton);
- expect(onClose).toHaveBeenCalledTimes(1);
- await waitFor(() => {
- expect(screen.queryByRole('status')).not.toBeInTheDocument();
- });
- });
-
- it('keeps notification open if `onClose` returns false', () => {
- render(
- false} />
- );
-
- const closeButton = screen.queryByRole('button', {
- hidden: true,
- });
- userEvent.click(closeButton);
- expect(screen.queryByRole('status')).toBeInTheDocument();
- });
-
- it('calls `onCloseButtonClick` when notification is closed', () => {
- const onCloseButtonClick = jest.fn();
- render(
-
- );
-
- const closeButton = screen.queryByRole('button', {
- hidden: true,
- });
- userEvent.click(closeButton);
- expect(onCloseButtonClick).toHaveBeenCalledTimes(1);
- });
-});
-
-describe('InlineNotification', () => {
- it('should have role=status by default', () => {
- const { container } = render(
-
- );
- expect(container.firstChild).toHaveAttribute('role', 'status');
- });
-
- it('should place the `className` prop on the outermost DOM node', () => {
- const { container } = render(
-
- );
- expect(container.firstChild).toHaveClass('test');
- });
-
- it('interpolates matching className based on kind prop', () => {
- const { rerender } = render(
-
- );
- const kinds = [
- 'error',
- 'info',
- 'info-square',
- 'success',
- 'warning',
- 'warning-alt',
- ];
- kinds.forEach((kind) => {
- rerender();
- expect(screen.queryByRole('status')).toHaveClass(
- `${prefix}--inline-notification--${kind}`
- );
- });
- });
-
- it('allows non-interactive elements as children', () => {
- render(
-
- Sample text
-
- );
- expect(screen.queryByText(/Sample text/i)).toBeInTheDocument();
- });
-
- it('does not allow interactive elements as children', () => {
- const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
-
- expect(() => {
- render(
-
-
-
- );
- }).toThrow();
-
- expect(spy).toHaveBeenCalled();
- spy.mockRestore();
- });
-
- it('close button is rendered by default and includes aria-hidden=true', () => {
- render();
-
- const closeButton = screen.queryByRole('button', {
- hidden: true,
- });
- expect(closeButton).toBeInTheDocument();
- expect(closeButton).toHaveAttribute('aria-hidden', 'true');
- });
-
- it('does not render close button when `hideCloseButton` is provided', () => {
- render();
- const closeButton = screen.queryByRole('button', {
- hidden: true,
- });
- expect(closeButton).not.toBeInTheDocument();
- });
-
- it('calls `onClose` when notification is closed', async () => {
- const onClose = jest.fn();
- render();
-
- const closeButton = screen.queryByRole('button', {
- hidden: true,
- });
- userEvent.click(closeButton);
- expect(onClose).toHaveBeenCalledTimes(1);
- await waitFor(() => {
- expect(screen.queryByRole('status')).not.toBeInTheDocument();
- });
- });
-
- it('keeps notification open if `onClose` returns false', () => {
- render(
- false} />
- );
-
- const closeButton = screen.queryByRole('button', {
- hidden: true,
- });
- userEvent.click(closeButton);
- expect(screen.queryByRole('status')).toBeInTheDocument();
- });
-
- it('calls `onCloseButtonClick` when notification is closed', () => {
- const onCloseButtonClick = jest.fn();
- render(
-
- );
-
- const closeButton = screen.queryByRole('button', {
- hidden: true,
- });
- userEvent.click(closeButton);
- expect(onCloseButtonClick).toHaveBeenCalledTimes(1);
- });
-});
-
-describe('ActionableNotification', () => {
- it('uses role=alertdialog', () => {
- const { container } = render(
-
- );
-
- expect(container.firstChild).toHaveAttribute('role', 'alertdialog');
- });
-
- it('renders correct action label', () => {
- render();
- const actionButton = screen.queryByRole('button', {
- name: 'My custom action',
- });
- expect(actionButton).toBeInTheDocument();
- });
-
- it('closes notification via escape button when focus is placed on the notification', async () => {
- const onCloseButtonClick = jest.fn();
- const onClose = jest.fn();
- render(
-
- );
-
- // without focus being on/in the notification, it should not close via escape
- userEvent.keyboard('{Escape}');
- expect(onCloseButtonClick).toHaveBeenCalledTimes(0);
- expect(onClose).toHaveBeenCalledTimes(0);
-
- // after focus is placed, the notification should close via escape
- userEvent.tab();
- userEvent.keyboard('{Escape}');
- expect(onCloseButtonClick).toHaveBeenCalledTimes(1);
- expect(onClose).toHaveBeenCalledTimes(1);
-
- await waitFor(() => {
- expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
- });
- });
-});
diff --git a/packages/react/src/components/Notification/next/Notification.js b/packages/react/src/components/Notification/next/Notification.js
deleted file mode 100644
index bf51f9941fef..000000000000
--- a/packages/react/src/components/Notification/next/Notification.js
+++ /dev/null
@@ -1,747 +0,0 @@
-/**
- * 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, { useEffect, useRef, useState } from 'react';
-import cx from 'classnames';
-import {
- Close,
- ErrorFilled,
- CheckmarkFilled,
- WarningFilled,
- WarningAltFilled,
- InformationFilled,
- InformationSquareFilled,
-} from '@carbon/icons-react';
-
-import Button from '../../Button';
-import useIsomorphicEffect from '../../../internal/useIsomorphicEffect';
-import { useNoInteractiveChildren } from '../../../internal/useNoInteractiveChildren';
-import { keys, matches } from '../../../internal/keyboard';
-import { usePrefix } from '../../../internal/usePrefix';
-
-/**
- * Conditionally call a callback when the escape key is pressed
- * @param {node} ref - ref of the container element to scope the functionality to
- * @param {func} callback - function to be called
- * @param {bool} override - escape hatch to conditionally call the callback
- */
-function useEscapeToClose(ref, callback, override = true) {
- const handleKeyDown = (event) => {
- // The callback should only be called when focus is on or within the container
- const elementContainsFocus =
- (ref.current && document.activeElement === ref.current) ||
- ref.current.contains(document.activeElement);
-
- if (matches(event, [keys.Escape]) && override && elementContainsFocus) {
- callback(event);
- }
- };
-
- useIsomorphicEffect(() => {
- document.addEventListener('keydown', handleKeyDown, false);
- return () => document.removeEventListener('keydown', handleKeyDown, false);
- });
-}
-
-export function NotificationActionButton({
- children,
- className: customClassName,
- onClick,
- inline,
- ...rest
-}) {
- const prefix = usePrefix();
- const className = cx(customClassName, {
- [`${prefix}--actionable-notification__action-button`]: true,
- });
-
- return (
-
- );
-}
-
-NotificationActionButton.propTypes = {
- /**
- * Specify the content of the notification action button.
- */
- children: PropTypes.node,
-
- /**
- * Specify an optional className to be applied to the notification action button
- */
- className: PropTypes.string,
-
- /**
- * Specify if the visual treatment of the button should be for an inline notification
- */
- inline: PropTypes.bool,
-
- /**
- * Optionally specify a click handler for the notification action button.
- */
- onClick: PropTypes.func,
-};
-
-export function NotificationButton({
- ariaLabel,
- className,
- type,
- renderIcon: IconTag,
- name,
- notificationType,
- ...rest
-}) {
- const prefix = usePrefix();
- const buttonClassName = cx(className, {
- [`${prefix}--${notificationType}-notification__close-button`]:
- notificationType,
- });
- const iconClassName = cx({
- [`${prefix}--${notificationType}-notification__close-icon`]:
- notificationType,
- });
-
- return (
-
- );
-}
-
-NotificationButton.propTypes = {
- /**
- * Specify a label to be read by screen readers on the notification button
- */
- ariaLabel: PropTypes.string,
-
- /**
- * Specify an optional className to be applied to the notification button
- */
- className: PropTypes.string,
-
- /**
- * Specify an optional icon for the Button through a string,
- * if something but regular "close" icon is desirable
- */
- name: PropTypes.string,
-
- /**
- * Specify the notification type
- */
- notificationType: PropTypes.oneOf(['toast', 'inline', 'actionable']),
-
- /**
- * Optional prop to allow overriding the icon rendering.
- * Can be a React component class
- */
- renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
-
- /**
- * Optional prop to specify the type of the Button
- */
- type: PropTypes.string,
-};
-
-NotificationButton.defaultProps = {
- ariaLabel: 'close notification',
- notificationType: 'toast',
- type: 'button',
- renderIcon: Close,
-};
-
-const iconTypes = {
- error: ErrorFilled,
- success: CheckmarkFilled,
- warning: WarningFilled,
- ['warning-alt']: WarningAltFilled,
- info: InformationFilled,
- ['info-square']: InformationSquareFilled,
-};
-
-function NotificationIcon({ iconDescription, kind, notificationType }) {
- const prefix = usePrefix();
- const IconForKind = iconTypes[kind];
- if (!IconForKind) {
- return null;
- }
- return (
-
- {iconDescription}
-
- );
-}
-
-NotificationIcon.propTypes = {
- iconDescription: PropTypes.string.isRequired,
- kind: PropTypes.oneOf([
- 'error',
- 'success',
- 'warning',
- 'warning-alt',
- 'info',
- 'info-square',
- ]).isRequired,
- notificationType: PropTypes.oneOf(['inline', 'toast']).isRequired,
-};
-
-export function ToastNotification({
- role,
- onClose,
- onCloseButtonClick,
- statusIconDescription,
- className,
- children,
- kind,
- lowContrast,
- hideCloseButton,
- timeout,
- title,
- caption,
- subtitle,
- ...rest
-}) {
- const [isOpen, setIsOpen] = useState(true);
- const prefix = usePrefix();
- const containerClassName = cx(className, {
- [`${prefix}--toast-notification`]: true,
- [`${prefix}--toast-notification--low-contrast`]: lowContrast,
- [`${prefix}--toast-notification--${kind}`]: kind,
- });
-
- const contentRef = useRef(null);
- useNoInteractiveChildren(contentRef);
-
- const handleClose = (evt) => {
- if (!onClose || onClose(evt) !== false) {
- setIsOpen(false);
- }
- };
- const ref = useRef(null);
-
- function handleCloseButtonClick(event) {
- onCloseButtonClick(event);
- handleClose(event);
- }
-
- const savedOnClose = useRef(onClose);
-
- useEffect(() => {
- savedOnClose.current = onClose;
- });
-
- useEffect(() => {
- if (!timeout) {
- return;
- }
-
- const timeoutId = window.setTimeout((event) => {
- setIsOpen(false);
- if (savedOnClose.current) {
- savedOnClose.current(event);
- }
- }, timeout);
- return () => {
- window.clearTimeout(timeoutId);
- };
- }, [timeout]);
-
- if (!isOpen) {
- return null;
- }
-
- return (
-
-
-
- {title && (
-
{title}
- )}
- {subtitle && (
-
- {subtitle}
-
- )}
- {caption && (
-
- {caption}
-
- )}
- {children}
-
- {!hideCloseButton && (
-
- )}
-
- );
-}
-
-ToastNotification.propTypes = {
- /**
- * Provide a description for "close" icon button that can be read by screen readers
- */
- ariaLabel: PropTypes.string,
-
- /**
- * Specify the caption
- */
- caption: PropTypes.string,
-
- /**
- * Specify the content
- */
- children: PropTypes.node,
-
- /**
- * Specify an optional className to be applied to the notification box
- */
- className: PropTypes.string,
-
- /**
- * Specify the close button should be disabled, or not
- */
- hideCloseButton: PropTypes.bool,
-
- /**
- * Specify what state the notification represents
- */
- kind: PropTypes.oneOf([
- 'error',
- 'info',
- 'info-square',
- 'success',
- 'warning',
- 'warning-alt',
- ]),
-
- /**
- * Specify whether you are using the low contrast variant of the ToastNotification.
- */
- lowContrast: PropTypes.bool,
-
- /**
- * Provide a function that is called when menu is closed
- */
- onClose: PropTypes.func,
-
- /**
- * Provide a function that is called when the close button is clicked
- */
- onCloseButtonClick: PropTypes.func,
-
- /**
- * By default, this value is "status". You can also provide an alternate
- * role if it makes sense from the accessibility-side
- */
- role: PropTypes.oneOf(['alert', 'log', 'status']),
-
- /**
- * Provide a description for "status" icon that can be read by screen readers
- */
- statusIconDescription: PropTypes.string,
-
- /**
- * Specify the sub-title
- */
- subtitle: PropTypes.string,
-
- /**
- * Specify an optional duration the notification should be closed in
- */
- timeout: PropTypes.number,
-
- /**
- * Specify the title
- */
- title: PropTypes.string,
-};
-
-ToastNotification.defaultProps = {
- kind: 'error',
- role: 'status',
- onCloseButtonClick: () => {},
- hideCloseButton: false,
- timeout: 0,
-};
-
-export function InlineNotification({
- children,
- title,
- subtitle,
- role,
- onClose,
- onCloseButtonClick,
- statusIconDescription,
- className,
- kind,
- lowContrast,
- hideCloseButton,
- ...rest
-}) {
- const [isOpen, setIsOpen] = useState(true);
- const prefix = usePrefix();
- const containerClassName = cx(className, {
- [`${prefix}--inline-notification`]: true,
- [`${prefix}--inline-notification--low-contrast`]: lowContrast,
- [`${prefix}--inline-notification--${kind}`]: kind,
- [`${prefix}--inline-notification--hide-close-button`]: hideCloseButton,
- });
-
- const contentRef = useRef(null);
- useNoInteractiveChildren(contentRef);
-
- const handleClose = (evt) => {
- if (!onClose || onClose(evt) !== false) {
- setIsOpen(false);
- }
- };
- const ref = useRef(null);
-
- function handleCloseButtonClick(event) {
- onCloseButtonClick(event);
- handleClose(event);
- }
-
- if (!isOpen) {
- return null;
- }
-
- return (
-
-
-
-
- {title && (
-
- {title}
-
- )}
- {subtitle && (
-
- {subtitle}
-
- )}
- {children}
-
-
- {!hideCloseButton && (
-
- )}
-
- );
-}
-
-InlineNotification.propTypes = {
- /**
- * Specify the content
- */
- children: PropTypes.node,
-
- /**
- * Specify an optional className to be applied to the notification box
- */
- className: PropTypes.string,
-
- /**
- * Specify the close button should be disabled, or not
- */
- hideCloseButton: PropTypes.bool,
-
- /**
- * Specify what state the notification represents
- */
- kind: PropTypes.oneOf([
- 'error',
- 'info',
- 'info-square',
- 'success',
- 'warning',
- 'warning-alt',
- ]),
-
- /**
- * Specify whether you are using the low contrast variant of the InlineNotification.
- */
- lowContrast: PropTypes.bool,
-
- /**
- * Provide a function that is called when menu is closed
- */
- onClose: PropTypes.func,
-
- /**
- * Provide a function that is called when the close button is clicked
- */
- onCloseButtonClick: PropTypes.func,
-
- /**
- * By default, this value is "status". You can also provide an alternate
- * role if it makes sense from the accessibility-side.
- */
- role: PropTypes.oneOf(['alert', 'log', 'status']),
-
- /**
- * Provide a description for "status" icon that can be read by screen readers
- */
- statusIconDescription: PropTypes.string,
-
- /**
- * Specify the sub-title
- */
- subtitle: PropTypes.string,
-
- /**
- * Specify the title
- */
- title: PropTypes.string,
-};
-
-InlineNotification.defaultProps = {
- kind: 'error',
- role: 'status',
- onCloseButtonClick: () => {},
- hideCloseButton: false,
-};
-
-export function ActionableNotification({
- actionButtonLabel,
- ariaLabel,
- children,
- role,
- onActionButtonClick,
- onClose,
- onCloseButtonClick,
- statusIconDescription,
- className,
- inline,
- kind,
- lowContrast,
- hideCloseButton,
- hasFocus,
- closeOnEscape,
- title,
- subtitle,
- ...rest
-}) {
- const [isOpen, setIsOpen] = useState(true);
- const prefix = usePrefix();
- const containerClassName = cx(className, {
- [`${prefix}--actionable-notification`]: true,
- [`${prefix}--actionable-notification--toast`]: !inline,
- [`${prefix}--actionable-notification--low-contrast`]: lowContrast,
- [`${prefix}--actionable-notification--${kind}`]: kind,
- [`${prefix}--actionable-notification--hide-close-button`]: hideCloseButton,
- });
-
- const ref = useRef(null);
- useIsomorphicEffect(() => {
- if (ref.current && hasFocus) {
- ref.current.focus();
- }
- });
-
- const handleClose = (evt) => {
- if (!onClose || onClose(evt) !== false) {
- setIsOpen(false);
- }
- };
- useEscapeToClose(ref, handleCloseButtonClick, closeOnEscape);
-
- function handleCloseButtonClick(event) {
- onCloseButtonClick(event);
- handleClose(event);
- }
-
- if (!isOpen) {
- return null;
- }
-
- return (
-
-
-
-
-
- {title && (
-
- {title}
-
- )}
- {subtitle && (
-
- {subtitle}
-
- )}
- {children}
-
-
-
-
-
- {actionButtonLabel}
-
-
- {!hideCloseButton && (
-
- )}
-
- );
-}
-
-ActionableNotification.propTypes = {
- /**
- * Pass in the action button label that will be rendered within the ActionableNotification.
- */
- actionButtonLabel: PropTypes.string.isRequired,
-
- /**
- * Provide a description for "close" icon button that can be read by screen readers
- */
- ariaLabel: PropTypes.string,
-
- /**
- * Specify the caption
- */
- caption: PropTypes.string,
-
- /**
- * Specify the content
- */
- children: PropTypes.node,
-
- /**
- * Specify an optional className to be applied to the notification box
- */
- className: PropTypes.string,
-
- /**
- * Specify if pressing the escape key should close notifications
- */
- closeOnEscape: PropTypes.bool,
-
- /**
- * Specify if focus should be moved to the component when the notification contains actions
- */
- hasFocus: PropTypes.bool,
-
- /**
- * Specify the close button should be disabled, or not
- */
- hideCloseButton: PropTypes.bool,
-
- /*
- * Specify if the notification should have inline styling applied instead of toast
- */
- inline: PropTypes.bool,
-
- /**
- * Specify what state the notification represents
- */
- kind: PropTypes.oneOf([
- 'error',
- 'info',
- 'info-square',
- 'success',
- 'warning',
- 'warning-alt',
- ]).isRequired,
-
- /**
- * Specify whether you are using the low contrast variant of the ActionableNotification.
- */
- lowContrast: PropTypes.bool,
-
- /**
- * Provide a function that is called when the action is clicked
- */
- onActionButtonClick: PropTypes.func,
-
- /**
- * Provide a function that is called when menu is closed
- */
- onClose: PropTypes.func,
-
- /**
- * Provide a function that is called when the close button is clicked
- */
- onCloseButtonClick: PropTypes.func,
-
- /**
- * By default, this value is "alertdialog". You can also provide an alternate
- * role if it makes sense from the accessibility-side.
- */
- role: PropTypes.string,
-
- /**
- * Provide a description for "status" icon that can be read by screen readers
- */
- statusIconDescription: PropTypes.string,
-
- /**
- * Specify the sub-title
- */
- subtitle: PropTypes.string,
-
- /**
- * Specify the title
- */
- title: PropTypes.string,
-};
-
-ActionableNotification.defaultProps = {
- kind: 'error',
- role: 'alertdialog',
- onCloseButtonClick: () => {},
- hideCloseButton: false,
- hasFocus: true,
- closeOnEscape: true,
- inline: false,
-};
diff --git a/packages/react/src/components/Notification/next/Notification.stories.js b/packages/react/src/components/Notification/next/Notification.stories.js
deleted file mode 100644
index 5767c7557924..000000000000
--- a/packages/react/src/components/Notification/next/Notification.stories.js
+++ /dev/null
@@ -1,136 +0,0 @@
-/**
- * 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 { FeatureFlags } from '../../FeatureFlags';
-import {
- ActionableNotification,
- ToastNotification,
- InlineNotification,
-} from './Notification';
-import React from 'react';
-import { action } from '@storybook/addon-actions';
-
-// eslint-disable-next-line storybook/csf-component
-export default {
- title: 'Components/Notifications',
- subscomponents: {
- ActionableNotification,
- InlineNotification,
- ToastNotification,
- },
- decorators: [
- (Story) => (
-
-
-
- ),
- ],
- argTypes: {
- kind: {
- options: [
- 'error',
- 'info',
- 'info-square',
- 'success',
- 'warning',
- 'warning-alt',
- ],
- control: {
- type: 'select',
- },
- },
- className: {
- control: {
- type: 'text',
- },
- },
- },
- args: {
- kind: 'error',
- lowContrast: false,
- hideCloseButton: false,
- ariaLabel: 'closes notification',
- statusIconDescription: 'notification',
- onClose: action('onClose'),
- onCloseButtonClick: action('onCloseButtonClick'),
- },
-};
-
-const ToastStory = (args) => ;
-
-export const Toast = ToastStory.bind({});
-
-Toast.argTypes = {
- role: {
- options: ['alert', 'log', 'status'],
- control: {
- type: 'select',
- },
- },
- caption: {
- control: {
- type: 'text',
- },
- },
- title: {
- control: {
- type: 'text',
- },
- },
- subtitle: {
- control: {
- type: 'text',
- },
- },
-};
-Toast.args = {
- role: 'status',
- caption: '00:00:00 AM',
- timeout: 0,
- title: 'Notification title',
- subtitle: 'Subtitle text goes here',
-};
-
-const InlineStory = (args) => {
- return ;
-};
-
-export const Inline = InlineStory.bind({});
-
-Inline.argTypes = {
- role: {
- options: ['alert', 'log', 'status'],
- control: {
- type: 'select',
- },
- },
- title: {
- control: {
- type: 'text',
- },
- },
- subtitle: {
- control: {
- type: 'text',
- },
- },
-};
-
-Inline.args = {
- title: 'Notification title',
- subtitle: 'Subtitle text goes here',
-};
-
-export const Actionable = (args) => ;
-
-Actionable.args = {
- actionButtonLabel: 'Action',
- inline: false,
- closeOnEscape: true,
- title: 'Notification title',
- subtitle: 'Subtitle text goes here',
-};
diff --git a/packages/react/src/components/Notification/stories/ActionableNotification.stories.js b/packages/react/src/components/Notification/stories/ActionableNotification.stories.js
new file mode 100644
index 000000000000..dd1a7fd85a25
--- /dev/null
+++ b/packages/react/src/components/Notification/stories/ActionableNotification.stories.js
@@ -0,0 +1,76 @@
+/**
+ * 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 { ActionableNotification } from '../../Notification';
+import { action } from '@storybook/addon-actions';
+import mdx from '../Notification.mdx';
+
+// eslint-disable-next-line storybook/csf-component
+export default {
+ title: 'Components/Notifications/Actionable',
+ component: ActionableNotification,
+ parameters: {
+ docs: {
+ page: mdx,
+ },
+ },
+ args: {
+ kind: 'error',
+ lowContrast: false,
+ hideCloseButton: false,
+ ariaLabel: 'closes notification',
+ statusIconDescription: 'notification',
+ onClose: action('onClose'),
+ onCloseButtonClick: action('onCloseButtonClick'),
+ },
+};
+
+export const Default = () => (
+
+);
+
+export const Playground = (args) => ;
+
+Playground.argTypes = {
+ ariaLabel: {
+ table: {
+ disable: true,
+ },
+ },
+ onActionButtonClick: {
+ action: 'clicked',
+ },
+ onClose: {
+ action: 'clicked',
+ },
+ onCloseButtonClick: {
+ action: 'clicked',
+ },
+ children: {
+ table: {
+ disable: true,
+ },
+ },
+ className: {
+ table: {
+ disable: true,
+ },
+ },
+};
+Playground.args = {
+ actionButtonLabel: 'Action',
+ inline: false,
+ title: 'Notification title',
+ subtitle: 'Subtitle text goes here',
+};
diff --git a/packages/react/src/components/Notification/stories/InlineNotification.stories.js b/packages/react/src/components/Notification/stories/InlineNotification.stories.js
new file mode 100644
index 000000000000..6aaa7264e124
--- /dev/null
+++ b/packages/react/src/components/Notification/stories/InlineNotification.stories.js
@@ -0,0 +1,80 @@
+/**
+ * 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 { InlineNotification } from '../../Notification';
+import { action } from '@storybook/addon-actions';
+import mdx from '../Notification.mdx';
+
+// eslint-disable-next-line storybook/csf-component
+export default {
+ title: 'Components/Notifications/Inline',
+ component: InlineNotification,
+ parameters: {
+ docs: {
+ page: mdx,
+ },
+ },
+ args: {
+ kind: 'error',
+ lowContrast: false,
+ hideCloseButton: false,
+ ariaLabel: 'closes notification',
+ statusIconDescription: 'notification',
+ onClose: action('onClose'),
+ onCloseButtonClick: action('onCloseButtonClick'),
+ },
+};
+
+export const Default = () => (
+
+);
+
+export const Playground = (args) => ;
+
+Playground.argTypes = {
+ actionButtonLabel: {
+ table: {
+ disable: true,
+ },
+ },
+ ariaLabel: {
+ table: {
+ disable: true,
+ },
+ },
+ onActionButtonClick: {
+ table: {
+ disable: true,
+ },
+ },
+ onClose: {
+ action: 'clicked',
+ },
+ onCloseButtonClick: {
+ action: 'clicked',
+ },
+ children: {
+ table: {
+ disable: true,
+ },
+ },
+ className: {
+ table: {
+ disable: true,
+ },
+ },
+};
+Playground.args = {
+ actionButtonLabel: 'Action',
+ inline: false,
+ title: 'Notification title',
+ subtitle: 'Subtitle text goes here',
+};
diff --git a/packages/react/src/components/Notification/stories/ToastNotification.stories.js b/packages/react/src/components/Notification/stories/ToastNotification.stories.js
new file mode 100644
index 000000000000..b0f1d8a3edcc
--- /dev/null
+++ b/packages/react/src/components/Notification/stories/ToastNotification.stories.js
@@ -0,0 +1,84 @@
+/**
+ * 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 { ToastNotification } from '../../Notification';
+import { action } from '@storybook/addon-actions';
+import mdx from '../Notification.mdx';
+
+// eslint-disable-next-line storybook/csf-component
+export default {
+ title: 'Components/Notifications/Toast',
+ component: ToastNotification,
+ parameters: {
+ docs: {
+ page: mdx,
+ },
+ },
+ args: {
+ kind: 'error',
+ lowContrast: false,
+ hideCloseButton: false,
+ ariaLabel: 'closes notification',
+ statusIconDescription: 'notification',
+ onClose: action('onClose'),
+ onCloseButtonClick: action('onCloseButtonClick'),
+ },
+};
+
+export const Default = () => (
+
+);
+
+export const Playground = (args) => ;
+
+Playground.argTypes = {
+ actionButtonLabel: {
+ table: {
+ disable: true,
+ },
+ },
+ ariaLabel: {
+ table: {
+ disable: true,
+ },
+ },
+ onActionButtonClick: {
+ table: {
+ disable: true,
+ },
+ },
+ onClose: {
+ action: 'clicked',
+ },
+ onCloseButtonClick: {
+ action: 'clicked',
+ },
+ children: {
+ table: {
+ disable: true,
+ },
+ },
+ className: {
+ table: {
+ disable: true,
+ },
+ },
+};
+Playground.args = {
+ role: 'status',
+ caption: '00:00:00 AM',
+ timeout: 0,
+ title: 'Notification title',
+ subtitle: 'Subtitle text goes here',
+};
diff --git a/packages/react/src/components/RadioButton/RadioButton-story.js b/packages/react/src/components/RadioButton/RadioButton-story.js
deleted file mode 100644
index d21143dbf169..000000000000
--- a/packages/react/src/components/RadioButton/RadioButton-story.js
+++ /dev/null
@@ -1,106 +0,0 @@
-/**
- * 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 { action } from '@storybook/addon-actions';
-
-import { withKnobs, boolean, select, text } from '@storybook/addon-knobs';
-import RadioButtonGroup from '../RadioButtonGroup';
-import RadioButton from '../RadioButton';
-import mdx from './RadioButton.mdx';
-
-const values = {
- 'Option 1': 'radio-1',
- 'Option 2': 'radio-2',
- 'Option 3': 'radio-3',
-};
-
-const orientations = {
- 'Horizontal (horizontal)': 'horizontal',
- 'Vertical (vertical)': 'vertical',
-};
-
-const labelPositions = {
- 'Left (left)': 'left',
- 'Right (right)': 'right',
-};
-
-const props = {
- group: () => ({
- hideLegend: boolean(
- 'Hide the legend (hideLegend) of the RadioButtonGroup (legendText)',
- false
- ),
- legendText: text(
- 'The label (legend) of the RadioButtonGroup (legendText)',
- 'Radio button heading'
- ),
- name: text(
- 'The form control name (name in )',
- 'radio-button-group'
- ),
- valueSelected: select(
- 'Value of the selected button (valueSelected in )',
- values,
- 'radio-3'
- ),
- orientation: select(
- 'Radio button orientation (orientation)',
- orientations,
- 'horizontal'
- ),
- labelPosition: select(
- 'Label position (labelPosition)',
- labelPositions,
- 'right'
- ),
- onChange: action('onChange'),
- }),
- radio: () => ({
- className: 'some-class',
- disabled: boolean('Disabled (disabled in )', false),
- labelText: text('The label of the RadioButton (labelText)', 'Option 1'),
- }),
-};
-
-export default {
- title: 'Components/RadioButton',
- component: RadioButtonGroup,
- decorators: [withKnobs],
- parameters: {
- docs: {
- page: mdx,
- },
- },
- subcomponents: {
- RadioButton,
- },
-};
-
-export const Default = () => {
- return (
-
-
-
-
-
- );
-};
-
-export const Playground = () => {
- const radioProps = props.radio();
- return (
-
-
-
-
-
- );
-};
diff --git a/packages/react/src/components/RadioButton/RadioButton-test.js b/packages/react/src/components/RadioButton/RadioButton-test.js
deleted file mode 100644
index 25dc7101a211..000000000000
--- a/packages/react/src/components/RadioButton/RadioButton-test.js
+++ /dev/null
@@ -1,160 +0,0 @@
-/**
- * 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 RadioButton from '../RadioButton';
-import RadioButtonSkeleton from '../RadioButton/RadioButton.Skeleton';
-import { mount, shallow } from 'enzyme';
-
-const prefix = 'cds';
-
-const render = (props) =>
- mount(
-
- );
-
-describe('RadioButton', () => {
- describe('renders as expected', () => {
- const wrapper = render({
- checked: true,
- });
-
- const input = wrapper.find('input');
- const label = wrapper.find('label');
- const div = wrapper.find('div');
-
- describe('input', () => {
- it('is of type radio', () => {
- expect(input.props().type).toEqual('radio');
- });
-
- it('has the expected class', () => {
- expect(input.hasClass(`${prefix}--radio-button`)).toEqual(true);
- });
-
- it('has a unique id set by default', () => {
- expect(input.props().id).toBeDefined();
- });
-
- it('should have checked set when checked is passed', () => {
- wrapper.setProps({ checked: true });
- expect(input.props().checked).toEqual(true);
- });
-
- it('should set the name prop as expected', () => {
- expect(input.props().name).toEqual('test-name');
- });
- });
-
- describe('label', () => {
- it('should set htmlFor', () => {
- expect(label.props().htmlFor).toEqual(input.props().id);
- });
-
- it('should set the correct class', () => {
- expect(label.props().className).toEqual(
- `${prefix}--radio-button__label`
- );
- });
-
- it('should render a span with the correct class', () => {
- const span = label.find('span');
- expect(
- span.at(0).hasClass(`${prefix}--radio-button__appearance`)
- ).toEqual(true);
- });
-
- it('should render a span for the label text', () => {
- const span = label.find('span');
- expect(span.at(1).hasClass('')).toEqual(true);
- expect(span.at(1).text()).toEqual('testlabel');
- });
-
- it('should render a span with hidden class name to hide label text', () => {
- wrapper.setProps({
- hideLabel: true,
- });
- const label = wrapper.find('span');
- const span = label.find('span');
- expect(span.at(1).hasClass(`${prefix}--visually-hidden`)).toEqual(true);
- expect(span.at(1).text()).toEqual('testlabel');
- });
-
- it('should render label text', () => {
- wrapper.setProps({ labelText: 'test label text' });
- expect(label.text()).toMatch(/test label text/);
- });
- });
-
- describe('wrapper', () => {
- it('should have the correct class', () => {
- expect(div.hasClass(`${prefix}--radio-button-wrapper`)).toEqual(true);
- });
-
- it('should have extra classes applied', () => {
- expect(div.hasClass('extra-class')).toEqual(true);
- });
- });
- });
-
- it('should set defaultChecked as expected', () => {
- const wrapper = render({
- defaultChecked: true,
- });
-
- const input = () => wrapper.find('input');
- expect(input().props().defaultChecked).toEqual(true);
- wrapper.setProps({ defaultChecked: false });
- expect(input().props().defaultChecked).toEqual(false);
- });
-
- it('should set id if one is passed in', () => {
- const wrapper = render({
- id: 'unique-id',
- });
-
- const input = wrapper.find('input');
- expect(input.props().id).toEqual('unique-id');
- });
-
- describe('events', () => {
- it('should invoke onChange with expected arguments', () => {
- const onChange = jest.fn();
- const wrapper = render({ onChange });
- const input = wrapper.find('input');
- const inputElement = input.instance();
-
- inputElement.checked = true;
- wrapper.find('input').simulate('change');
-
- const call = onChange.mock.calls[0];
-
- expect(call[0]).toEqual('test-value');
- expect(call[1]).toEqual('test-name');
- expect(call[2].target).toBe(inputElement);
- });
- });
-});
-
-describe('RadioButtonSkeleton', () => {
- describe('Renders as expected', () => {
- const wrapper = shallow();
-
- const label = wrapper.find('span');
-
- it('Has the expected classes', () => {
- expect(label.hasClass(`${prefix}--skeleton`)).toEqual(true);
- expect(label.hasClass(`${prefix}--radio-button__label`)).toEqual(true);
- });
- });
-});
diff --git a/packages/react/src/components/RadioButton/RadioButton.js b/packages/react/src/components/RadioButton/RadioButton.js
index 8050e5aa5955..9f9424334d37 100644
--- a/packages/react/src/components/RadioButton/RadioButton.js
+++ b/packages/react/src/components/RadioButton/RadioButton.js
@@ -5,147 +5,135 @@
* LICENSE file in the root directory of this source tree.
*/
-import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
-import { warning } from '../../internal/warning';
-import uid from '../../tools/uniqueId';
+import classNames from 'classnames';
import { Text } from '../Text';
-import { PrefixContext } from '../../internal/usePrefix';
-
-class RadioButton extends React.Component {
- static propTypes = {
- /**
- * Specify whether the is currently checked
- */
- checked: PropTypes.bool,
-
- /**
- * Provide an optional className to be applied to the containing node
- */
- className: PropTypes.string,
-
- /**
- * Specify whether the should be checked by default
- */
- defaultChecked: PropTypes.bool,
-
- /**
- * Specify whether the control is disabled
- */
- disabled: PropTypes.bool,
-
- /**
- * Specify whether the label should be hidden, or not
- */
- hideLabel: PropTypes.bool,
-
- /**
- * Provide a unique id for the underlying `` node
- */
- id: PropTypes.string,
-
- /**
- * Provide where label text should be placed
- * NOTE: `top`/`bottom` are deprecated
- */
- labelPosition: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
-
- /**
- * Provide label text to be read by screen readers when interacting with the
- * control
- */
- labelText: PropTypes.node.isRequired,
-
- /**
- * Provide a name for the underlying `` node
- */
- name: PropTypes.string,
-
- /**
- * Provide an optional `onChange` hook that is called each time the value of
- * the underlying `` changes
- */
- onChange: PropTypes.func,
-
- /**
- * Provide a handler that is invoked when a user clicks on the control
- */
- onClick: PropTypes.func,
-
- /**
- * Specify the value of the
- */
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- };
-
- static contextType = PrefixContext;
- prefix = this.context;
-
- static defaultProps = {
- labelText: '',
- labelPosition: 'right',
- onChange: () => {},
- value: '',
- };
-
- uid = this.props.id || uid();
-
- handleChange = (evt) => {
- this.props.onChange(this.props.value, this.props.name, evt);
- };
-
- render() {
- const prefix = this.prefix;
- const {
- className,
- labelText,
- labelPosition,
- // eslint-disable-next-line react/prop-types
- innerRef: ref,
- hideLabel,
- ...other
- } = this.props;
- if (__DEV__) {
- warning(
- labelPosition !== 'top' && labelPosition !== 'bottom',
- '`top`/`bottom` values for `labelPosition` property in the `RadioButton` component is deprecated ' +
- 'and being removed in the next release of `carbon-components-react`.'
- );
- }
- const innerLabelClasses = classNames({
- [`${prefix}--visually-hidden`]: hideLabel,
- });
- const wrapperClasses = classNames(
- className,
- `${prefix}--radio-button-wrapper`,
- {
- [`${prefix}--radio-button-wrapper--label-${labelPosition}`]:
- labelPosition !== 'right',
- }
- );
- return (
-
-
-
-
- {labelText && {labelText}}
-
-
- );
+import { usePrefix } from '../../internal/usePrefix';
+import { useId } from '../../internal/useId';
+
+const RadioButton = React.forwardRef(function RadioButton(
+ {
+ className,
+ disabled,
+ hideLabel,
+ id,
+ labelPosition = 'right',
+ labelText = '',
+ name,
+ onChange = () => {},
+ value = '',
+ ...rest
+ },
+ ref
+) {
+ const prefix = usePrefix();
+ const uid = useId('radio-button');
+ const uniqueId = id || uid;
+
+ function handleOnChange(event) {
+ onChange(value, name, event);
}
-}
-
-export { RadioButton };
-export default (() => {
- const forwardRef = (props, ref) => ;
- forwardRef.displayName = 'RadioButton';
- return React.forwardRef(forwardRef);
-})();
+
+ const innerLabelClasses = classNames({
+ [`${prefix}--visually-hidden`]: hideLabel,
+ });
+
+ const wrapperClasses = classNames(
+ className,
+ `${prefix}--radio-button-wrapper`,
+ {
+ [`${prefix}--radio-button-wrapper--label-${labelPosition}`]:
+ labelPosition !== 'right',
+ }
+ );
+
+ return (
+
+
+
+
+ {labelText && {labelText}}
+
+
+ );
+});
+
+RadioButton.displayName = 'RadioButton';
+
+RadioButton.propTypes = {
+ /**
+ * Specify whether the `` is currently checked
+ */
+ checked: PropTypes.bool,
+
+ /**
+ * Provide an optional className to be applied to the containing node
+ */
+ className: PropTypes.string,
+
+ /**
+ * Specify whether the `` should be checked by default
+ */
+ defaultChecked: PropTypes.bool,
+
+ /**
+ * Specify whether the control is disabled
+ */
+ disabled: PropTypes.bool,
+
+ /**
+ * Specify whether the label should be hidden, or not
+ */
+ hideLabel: PropTypes.bool,
+
+ /**
+ * Provide a unique id for the underlying `` node
+ */
+ id: PropTypes.string,
+
+ /**
+ * Provide where label text should be placed
+ * NOTE: `top`/`bottom` are deprecated
+ */
+ labelPosition: PropTypes.oneOf(['right', 'left']),
+
+ /**
+ * Provide label text to be read by screen readers when interacting with the
+ * control
+ */
+ labelText: PropTypes.node.isRequired,
+
+ /**
+ * Provide a name for the underlying `` node
+ */
+ name: PropTypes.string,
+
+ /**
+ * Provide an optional `onChange` hook that is called each time the value of
+ * the underlying `` changes
+ */
+ onChange: PropTypes.func,
+
+ /**
+ * Provide a handler that is invoked when a user clicks on the control
+ */
+ onClick: PropTypes.func,
+
+ /**
+ * Specify the value of the ``
+ */
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+};
+
+export default RadioButton;
diff --git a/packages/react/src/components/RadioButton/next/RadioButton.stories.js b/packages/react/src/components/RadioButton/RadioButton.stories.js
similarity index 52%
rename from packages/react/src/components/RadioButton/next/RadioButton.stories.js
rename to packages/react/src/components/RadioButton/RadioButton.stories.js
index 2882e29db5e8..0e5b71a09c16 100644
--- a/packages/react/src/components/RadioButton/next/RadioButton.stories.js
+++ b/packages/react/src/components/RadioButton/RadioButton.stories.js
@@ -5,15 +5,23 @@
* LICENSE file in the root directory of this source tree.
*/
-import RadioButton from '../';
-import RadioButtonGroup from '../../RadioButtonGroup';
+import RadioButton from './RadioButton';
+import RadioButtonGroup from '../RadioButtonGroup';
+import RadioButtonSkeleton from './RadioButton.Skeleton';
import React from 'react';
+import mdx from './RadioButton.mdx';
export default {
title: 'Components/RadioButton',
component: RadioButton,
subcomponents: {
RadioButtonGroup,
+ RadioButtonSkeleton,
+ },
+ parameters: {
+ docs: {
+ page: mdx,
+ },
},
};
@@ -29,3 +37,17 @@ export const Default = () => {
);
};
+
+export const Skeleton = () => {
+ return ;
+};
+
+export const Playground = (args) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/packages/react/src/components/RadioButton/__tests__/RadioButton-test.js b/packages/react/src/components/RadioButton/__tests__/RadioButton-test.js
new file mode 100644
index 000000000000..2c399fa22689
--- /dev/null
+++ b/packages/react/src/components/RadioButton/__tests__/RadioButton-test.js
@@ -0,0 +1,135 @@
+/**
+ * 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, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import RadioButton from '../RadioButton';
+
+describe('RadioButton', () => {
+ it('should render an input with type="radio"', () => {
+ render(
+
+ );
+ expect(screen.getByRole('radio')).toBeInTheDocument();
+ });
+
+ it('should set an id on the by default', () => {
+ render(
+
+ );
+ expect(screen.getByRole('radio')).toHaveAttribute('id');
+ });
+
+ it('should set checked on the when checked is provided', () => {
+ render(
+
+ );
+ expect(screen.getByRole('radio')).toBeChecked();
+ });
+
+ it('should label the with labelText', () => {
+ render(
+
+ );
+ expect(screen.getByRole('radio')).toEqual(
+ screen.getByLabelText('test-label')
+ );
+ });
+
+ it('should set defaultChecked as expected', () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('radio')).toBeChecked();
+ });
+
+ it('should set id on the if one is passed in', () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('radio')).toHaveAttribute('id', 'test-id');
+ });
+
+ it('should invoke onChange with expected arguments', () => {
+ const onChange = jest.fn();
+
+ render(
+
+ );
+
+ userEvent.click(screen.getByRole('radio'));
+
+ expect(onChange).toHaveBeenCalled();
+ expect(onChange).toHaveBeenCalledWith(
+ 'test-value',
+ 'test-name',
+ expect.objectContaining({
+ target: screen.getByRole('radio'),
+ })
+ );
+ });
+
+ it('should place className on the outermost element', () => {
+ const { container } = render(
+
+ );
+ expect(container.firstChild).toHaveClass('custom-class');
+ });
+
+ it('should spread additional props on the element', () => {
+ render(
+
+ );
+ expect(screen.getByRole('radio')).toHaveAttribute('data-testid', 'test');
+ });
+
+ it('should support a `ref` on the element', () => {
+ const ref = jest.fn();
+ render(
+
+ );
+ expect(ref).toHaveBeenCalledWith(screen.getByRole('radio'));
+ });
+});
diff --git a/packages/react/src/components/RadioButton/__tests__/RadioButtonSkeleton-test.js b/packages/react/src/components/RadioButton/__tests__/RadioButtonSkeleton-test.js
new file mode 100644
index 000000000000..d2277da5032f
--- /dev/null
+++ b/packages/react/src/components/RadioButton/__tests__/RadioButtonSkeleton-test.js
@@ -0,0 +1,24 @@
+/**
+ * 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 RadioButtonSkeleton from '../RadioButton.Skeleton';
+
+describe('RadioButtonSkeleton', () => {
+ it('should support `className` on the outermost element', () => {
+ const { container } = render(
+
+ );
+ expect(container.firstChild).toHaveClass('custom-class');
+ });
+
+ it('should spread props on the outermost element', () => {
+ const { container } = render();
+ expect(container.firstChild).toHaveAttribute('data-testid', 'test');
+ });
+});
diff --git a/packages/react/src/components/RadioButton/index.js b/packages/react/src/components/RadioButton/index.js
index 4dbf158df0c5..b7a2f5828e63 100644
--- a/packages/react/src/components/RadioButton/index.js
+++ b/packages/react/src/components/RadioButton/index.js
@@ -5,13 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
-import * as FeatureFlags from '@carbon/feature-flags';
-import { default as RadioButtonNext } from './next/RadioButton';
-import { default as RadioButtonClassic } from './RadioButton';
-
-const RadioButton = FeatureFlags.enabled('enable-v11-release')
- ? RadioButtonNext
- : RadioButtonClassic;
+import RadioButton from './RadioButton';
export default RadioButton;
-export { default as RadioButtonSkeleton } from './RadioButton.Skeleton';
diff --git a/packages/react/src/components/RadioButton/next/RadioButton-test.js b/packages/react/src/components/RadioButton/next/RadioButton-test.js
deleted file mode 100644
index 35c83db00c0a..000000000000
--- a/packages/react/src/components/RadioButton/next/RadioButton-test.js
+++ /dev/null
@@ -1,160 +0,0 @@
-/**
- * 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 RadioButton from '../RadioButton';
-import RadioButtonSkeleton from '../../RadioButton/RadioButton.Skeleton';
-import { mount, shallow } from 'enzyme';
-
-const prefix = 'cds';
-
-const render = (props) =>
- mount(
-
- );
-
-describe('RadioButton', () => {
- describe('renders as expected', () => {
- const wrapper = render({
- checked: true,
- });
-
- const input = wrapper.find('input');
- const label = wrapper.find('label');
- const div = wrapper.find('div');
-
- describe('input', () => {
- it('is of type radio', () => {
- expect(input.props().type).toEqual('radio');
- });
-
- it('has the expected class', () => {
- expect(input.hasClass(`${prefix}--radio-button`)).toEqual(true);
- });
-
- it('has a unique id set by default', () => {
- expect(input.props().id).toBeDefined();
- });
-
- it('should have checked set when checked is passed', () => {
- wrapper.setProps({ checked: true });
- expect(input.props().checked).toEqual(true);
- });
-
- it('should set the name prop as expected', () => {
- expect(input.props().name).toEqual('test-name');
- });
- });
-
- describe('label', () => {
- it('should set htmlFor', () => {
- expect(label.props().htmlFor).toEqual(input.props().id);
- });
-
- it('should set the correct class', () => {
- expect(label.props().className).toEqual(
- `${prefix}--radio-button__label`
- );
- });
-
- it('should render a span with the correct class', () => {
- const span = label.find('span');
- expect(
- span.at(0).hasClass(`${prefix}--radio-button__appearance`)
- ).toEqual(true);
- });
-
- it('should render a span for the label text', () => {
- const span = label.find('span');
- expect(span.at(1).hasClass('')).toEqual(true);
- expect(span.at(1).text()).toEqual('testlabel');
- });
-
- it('should render a span with hidden class name to hide label text', () => {
- wrapper.setProps({
- hideLabel: true,
- });
- const label = wrapper.find('span');
- const span = label.find('span');
- expect(span.at(1).hasClass(`${prefix}--visually-hidden`)).toEqual(true);
- expect(span.at(1).text()).toEqual('testlabel');
- });
-
- it('should render label text', () => {
- wrapper.setProps({ labelText: 'test label text' });
- expect(label.text()).toMatch(/test label text/);
- });
- });
-
- describe('wrapper', () => {
- it('should have the correct class', () => {
- expect(div.hasClass(`${prefix}--radio-button-wrapper`)).toEqual(true);
- });
-
- it('should have extra classes applied', () => {
- expect(div.hasClass('extra-class')).toEqual(true);
- });
- });
- });
-
- it('should set defaultChecked as expected', () => {
- const wrapper = render({
- defaultChecked: true,
- });
-
- const input = () => wrapper.find('input');
- expect(input().props().defaultChecked).toEqual(true);
- wrapper.setProps({ defaultChecked: false });
- expect(input().props().defaultChecked).toEqual(false);
- });
-
- it('should set id if one is passed in', () => {
- const wrapper = render({
- id: 'unique-id',
- });
-
- const input = wrapper.find('input');
- expect(input.props().id).toEqual('unique-id');
- });
-
- describe('events', () => {
- it('should invoke onChange with expected arguments', () => {
- const onChange = jest.fn();
- const wrapper = render({ onChange });
- const input = wrapper.find('input');
- const inputElement = input.instance();
-
- inputElement.checked = true;
- wrapper.find('input').simulate('change');
-
- const call = onChange.mock.calls[0];
-
- expect(call[0]).toEqual('test-value');
- expect(call[1]).toEqual('test-name');
- expect(call[2].target).toBe(inputElement);
- });
- });
-});
-
-describe('RadioButtonSkeleton', () => {
- describe('Renders as expected', () => {
- const wrapper = shallow();
-
- const label = wrapper.find('span');
-
- it('Has the expected classes', () => {
- expect(label.hasClass(`${prefix}--skeleton`)).toEqual(true);
- expect(label.hasClass(`${prefix}--radio-button__label`)).toEqual(true);
- });
- });
-});
diff --git a/packages/react/src/components/RadioButton/next/RadioButton.js b/packages/react/src/components/RadioButton/next/RadioButton.js
deleted file mode 100644
index 395bde53f647..000000000000
--- a/packages/react/src/components/RadioButton/next/RadioButton.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import classNames from 'classnames';
-import { usePrefix } from '../../../internal/usePrefix';
-import { useId } from '../../../internal/useId';
-
-import { Text } from '../../Text';
-
-const RadioButton = React.forwardRef(function RadioButton(
- {
- className,
- disabled,
- hideLabel,
- id,
- labelPosition = 'right',
- labelText = '',
- name,
- onChange = () => {},
- value = '',
- ...rest
- },
- ref
-) {
- const prefix = usePrefix();
- const uid = useId('radio-button');
- const uniqueId = id || uid;
-
- function handleOnChange(event) {
- onChange(value, name, event);
- }
-
- const innerLabelClasses = classNames({
- [`${prefix}--visually-hidden`]: hideLabel,
- });
-
- const wrapperClasses = classNames(
- className,
- `${prefix}--radio-button-wrapper`,
- {
- [`${prefix}--radio-button-wrapper--label-${labelPosition}`]:
- labelPosition !== 'right',
- }
- );
-
- return (
-
-
-
-
- {labelText && {labelText}}
-
-
- );
-});
-
-RadioButton.propTypes = {
- /**
- * Specify whether the `` is currently checked
- */
- checked: PropTypes.bool,
-
- /**
- * Provide an optional className to be applied to the containing node
- */
- className: PropTypes.string,
-
- /**
- * Specify whether the `` should be checked by default
- */
- defaultChecked: PropTypes.bool,
-
- /**
- * Specify whether the control is disabled
- */
- disabled: PropTypes.bool,
-
- /**
- * Specify whether the label should be hidden, or not
- */
- hideLabel: PropTypes.bool,
-
- /**
- * Provide a unique id for the underlying `` node
- */
- id: PropTypes.string,
-
- /**
- * Provide where label text should be placed
- * NOTE: `top`/`bottom` are deprecated
- */
- labelPosition: PropTypes.oneOf(['right', 'left']),
-
- /**
- * Provide label text to be read by screen readers when interacting with the
- * control
- */
- labelText: PropTypes.node.isRequired,
-
- /**
- * Provide a name for the underlying `` node
- */
- name: PropTypes.string,
-
- /**
- * Provide an optional `onChange` hook that is called each time the value of
- * the underlying `` changes
- */
- onChange: PropTypes.func,
-
- /**
- * Provide a handler that is invoked when a user clicks on the control
- */
- onClick: PropTypes.func,
-
- /**
- * Specify the value of the ``
- */
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
-};
-
-export default RadioButton;
diff --git a/packages/react/src/components/RadioButtonGroup/RadioButtonGroup-test.js b/packages/react/src/components/RadioButtonGroup/RadioButtonGroup-test.js
index 44a3720afd81..6616b7cad74e 100644
--- a/packages/react/src/components/RadioButtonGroup/RadioButtonGroup-test.js
+++ b/packages/react/src/components/RadioButtonGroup/RadioButtonGroup-test.js
@@ -6,9 +6,9 @@
*/
import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import React from 'react';
-import { shallow, mount } from 'enzyme';
-import RadioButtonGroup from '../RadioButtonGroup';
+import RadioButtonGroup from './RadioButtonGroup';
import RadioButton from '../RadioButton';
describe('RadioButtonGroup', () => {
@@ -60,8 +60,8 @@ describe('RadioButtonGroup', () => {
});
describe('Component API', () => {
- it('should support a custom className on the