From e346d01e48bdeef2d06eec83bbe47660be3f2e03 Mon Sep 17 00:00:00 2001 From: Jan Hassel <jan.hassel@ibm.com> Date: Thu, 6 Oct 2022 17:41:14 +0200 Subject: [PATCH] feat: add contained-list component (#11969) * feat(contained-list): scaffold new component * feat(contained-list-item): scaffold new component * feat(contained-list-item): add support for clickable items * feat(contained-list): add support for disabled list items * feat(contained-list): add support for actions in header and item * docs(contained-list): build stories * feat(contained-list-item): add support for icons * test(contained-list): add tests * refactor(contained-list): rename variants * docs(contained-list): change "Heading" to "List title" * fix(contained-list-item): add extra padding when action is pased * docs(contained-list): change "Heading" to "List title" Co-authored-by: Lauren Rice <43969356+laurenmrice@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../__snapshots__/PublicAPI-test.js.snap | 100 +++++++ packages/react/src/__tests__/index-test.js | 2 + .../components/ContainedList/ContainedList.js | 74 +++++ .../ContainedListItem/ContainedListItem.js | 96 ++++++ .../ContainedList/ContainedListItem/index.js | 8 + .../__tests__/ContainedList-test.js | 137 +++++++++ .../src/components/ContainedList/index.js | 14 + .../next/ContainedList.stories.js | 280 ++++++++++++++++++ packages/react/src/index.js | 3 + packages/styles/scss/components/_index.scss | 1 + .../contained-list/_contained-list.scss | 174 +++++++++++ .../components/contained-list/_index.scss | 11 + 12 files changed, 900 insertions(+) create mode 100644 packages/react/src/components/ContainedList/ContainedList.js create mode 100644 packages/react/src/components/ContainedList/ContainedListItem/ContainedListItem.js create mode 100644 packages/react/src/components/ContainedList/ContainedListItem/index.js create mode 100644 packages/react/src/components/ContainedList/__tests__/ContainedList-test.js create mode 100644 packages/react/src/components/ContainedList/index.js create mode 100644 packages/react/src/components/ContainedList/next/ContainedList.stories.js create mode 100644 packages/styles/scss/components/contained-list/_contained-list.scss create mode 100644 packages/styles/scss/components/contained-list/_index.scss diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index b078a5b06886..396eaf5a04a0 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -8639,6 +8639,106 @@ Map { "$$typeof": Symbol(react.forward_ref), "render": [Function], }, + "unstable_ContainedList" => Object { + "ContainedListItem": Object { + "propTypes": Object { + "action": Object { + "type": "node", + }, + "children": Object { + "type": "node", + }, + "className": Object { + "type": "string", + }, + "disabled": Object { + "type": "bool", + }, + "onClick": Object { + "type": "func", + }, + "renderIcon": Object { + "args": Array [ + Array [ + Object { + "type": "func", + }, + Object { + "type": "object", + }, + ], + ], + "type": "oneOfType", + }, + }, + }, + "propTypes": Object { + "action": Object { + "type": "node", + }, + "children": Object { + "type": "node", + }, + "className": Object { + "type": "string", + }, + "kind": Object { + "args": Array [ + Array [ + "on-page", + "disclosed", + ], + ], + "type": "oneOf", + }, + "label": Object { + "args": Array [ + Array [ + Object { + "type": "string", + }, + Object { + "type": "node", + }, + ], + ], + "isRequired": true, + "type": "oneOfType", + }, + }, + }, + "unstable_ContainedListItem" => Object { + "propTypes": Object { + "action": Object { + "type": "node", + }, + "children": Object { + "type": "node", + }, + "className": Object { + "type": "string", + }, + "disabled": Object { + "type": "bool", + }, + "onClick": Object { + "type": "func", + }, + "renderIcon": Object { + "args": Array [ + Array [ + Object { + "type": "func", + }, + Object { + "type": "object", + }, + ], + ], + "type": "oneOfType", + }, + }, + }, "unstable_FeatureFlags" => Object { "propTypes": Object { "children": Object { diff --git a/packages/react/src/__tests__/index-test.js b/packages/react/src/__tests__/index-test.js index 856fc691e02f..3490c5d4f003 100644 --- a/packages/react/src/__tests__/index-test.js +++ b/packages/react/src/__tests__/index-test.js @@ -214,6 +214,8 @@ describe('Carbon Components React', () => { "TreeView", "UnorderedList", "VStack", + "unstable_ContainedList", + "unstable_ContainedListItem", "unstable_FeatureFlags", "unstable_LayoutDirection", "unstable_Menu", diff --git a/packages/react/src/components/ContainedList/ContainedList.js b/packages/react/src/components/ContainedList/ContainedList.js new file mode 100644 index 000000000000..85440bdf5807 --- /dev/null +++ b/packages/react/src/components/ContainedList/ContainedList.js @@ -0,0 +1,74 @@ +/** + * Copyright IBM Corp. 2022 + * + * 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 PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { useId } from '../../internal/useId'; +import { usePrefix } from '../../internal/usePrefix'; + +const variants = ['on-page', 'disclosed']; + +function ContainedList({ + action, + children, + className, + kind = variants[0], + label, +}) { + const labelId = `${useId('contained-list')}-header`; + const prefix = usePrefix(); + + const classes = classNames( + `${prefix}--contained-list`, + `${prefix}--contained-list--${kind}`, + className + ); + + return ( + <div className={classes}> + <div className={`${prefix}--contained-list__header`}> + <div id={labelId} className={`${prefix}--contained-list__label`}> + {label} + </div> + {action && ( + <div className={`${prefix}--contained-list__action`}>{action}</div> + )} + </div> + <ul aria-labelledby={labelId}>{children}</ul> + </div> + ); +} + +ContainedList.propTypes = { + /** + * A slot for a possible interactive element to render. + */ + action: PropTypes.node, + + /** + * A collection of ContainedListItems to be rendered in the ContainedList + */ + children: PropTypes.node, + + /** + * Additional CSS class names. + */ + className: PropTypes.string, + + /** + * The kind of ContainedList you want to display + */ + kind: PropTypes.oneOf(variants), + + /** + * A label describing the contained list. + */ + label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, +}; + +export default ContainedList; diff --git a/packages/react/src/components/ContainedList/ContainedListItem/ContainedListItem.js b/packages/react/src/components/ContainedList/ContainedListItem/ContainedListItem.js new file mode 100644 index 000000000000..718f90ec285a --- /dev/null +++ b/packages/react/src/components/ContainedList/ContainedListItem/ContainedListItem.js @@ -0,0 +1,96 @@ +/** + * Copyright IBM Corp. 2022 + * + * 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 PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { usePrefix } from '../../../internal/usePrefix'; + +function ContainedListItem({ + action, + children, + className, + disabled = false, + onClick, + renderIcon: IconElement, +}) { + const prefix = usePrefix(); + + const isClickable = onClick !== undefined; + + const classes = classNames(`${prefix}--contained-list-item`, className, { + [`${prefix}--contained-list-item--clickable`]: isClickable, + [`${prefix}--contained-list-item--with-icon`]: IconElement, + [`${prefix}--contained-list-item--with-action`]: action, + }); + + const content = ( + <> + {IconElement && ( + <div className={`${prefix}--contained-list-item__icon`}> + <IconElement /> + </div> + )} + <div>{children}</div> + </> + ); + + return ( + <li className={classes}> + {isClickable ? ( + <button + className={`${prefix}--contained-list-item__content`} + type="button" + disabled={disabled} + onClick={onClick}> + {content} + </button> + ) : ( + <div className={`${prefix}--contained-list-item__content`}> + {content} + </div> + )} + {action && ( + <div className={`${prefix}--contained-list-item__action`}>{action}</div> + )} + </li> + ); +} + +ContainedListItem.propTypes = { + /** + * A slot for a possible interactive element to render within the item. + */ + action: PropTypes.node, + + /** + * The content of this item. Must not contain any interactive elements. Use props.action to include those. + */ + children: PropTypes.node, + + /** + * Additional CSS class names. + */ + className: PropTypes.string, + + /** + * Whether this item is disabled. + */ + disabled: PropTypes.bool, + + /** + * Provide an optional function to be called when the item is clicked. + */ + onClick: PropTypes.func, + + /** + * Provide an optional icon to render in front of the item's content. + */ + renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), +}; + +export default ContainedListItem; diff --git a/packages/react/src/components/ContainedList/ContainedListItem/index.js b/packages/react/src/components/ContainedList/ContainedListItem/index.js new file mode 100644 index 000000000000..65d76124448c --- /dev/null +++ b/packages/react/src/components/ContainedList/ContainedListItem/index.js @@ -0,0 +1,8 @@ +/** + * Copyright IBM Corp. 2022 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +export default from './ContainedListItem'; diff --git a/packages/react/src/components/ContainedList/__tests__/ContainedList-test.js b/packages/react/src/components/ContainedList/__tests__/ContainedList-test.js new file mode 100644 index 000000000000..0aa7ea9cc90b --- /dev/null +++ b/packages/react/src/components/ContainedList/__tests__/ContainedList-test.js @@ -0,0 +1,137 @@ +/** + * 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 ContainedList, { ContainedListItem } from '../'; +import { render } from '@testing-library/react'; + +const prefix = 'cds'; + +const defaultProps = { + list: { + label: 'Heading', + }, + item: { + children: 'List item', + }, +}; +let wrapper; + +function TestComponent({ list, item }) { + const props = { + list: { + ...defaultProps.list, + ...list, + }, + item: { + ...defaultProps.item, + ...item, + }, + }; + + return ( + <ContainedList {...props.list}> + <ContainedListItem {...props.item} /> + </ContainedList> + ); +} + +beforeEach(() => { + wrapper = render(<TestComponent />); +}); + +async function a11y(label) { + it('should have no Axe violations', async () => { + await expect(wrapper.container).toHaveNoAxeViolations(); + }); + + it('should have no Accessibility Checker violations', async () => { + await expect(wrapper.container).toHaveNoACViolations(label); + }); +} + +describe('ContainedList', () => { + it('list and label ids match', () => { + const list = wrapper.getByRole('list'); + const label = wrapper.container.querySelector( + `.${prefix}--contained-list__label` + ); + + expect(list.getAttribute('aria-labelledby')).toBe(label.id); + }); + + it('renders props.label', () => { + const label = wrapper.container.querySelector( + `.${prefix}--contained-list__label` + ); + + expect(label.textContent).toBe(defaultProps.list.label); + }); + + it('supports additional css class names', () => { + const className = 'some-class'; + wrapper.rerender(<TestComponent list={{ className }} />); + + expect(wrapper.container.firstChild.classList.contains(className)).toBe( + true + ); + }); + + a11y('ContainedList'); +}); + +describe('ContainedListItem', () => { + it('renders props.children', () => { + const content = wrapper.getByRole('listitem'); + + expect(content.textContent).toBe(defaultProps.item.children); + }); + + it('supports additional css class names', () => { + const className = 'some-class'; + wrapper.rerender(<TestComponent item={{ className }} />); + + expect(wrapper.getByRole('listitem').classList.contains(className)).toBe( + true + ); + }); + + it('renders props.action adjacent to content', () => { + wrapper.rerender( + <TestComponent item={{ action: <div data-testid="action" /> }} /> + ); + const contentEl = wrapper.container.querySelector( + `.${prefix}--contained-list-item__content` + ); + + expect(contentEl.nextSibling.firstChild.dataset['testid']).toBe('action'); + }); + + it('supports props.renderIcon', () => { + wrapper.rerender( + <TestComponent item={{ renderIcon: () => <svg data-testid="svg" /> }} /> + ); + + expect(wrapper.container.querySelector('svg').dataset['testid']).toBe( + 'svg' + ); + }); + + describe('interactive', () => { + beforeEach(() => { + wrapper.rerender(<TestComponent item={{ onClick: () => {} }} />); + }); + + it('renders content as button', () => { + const content = wrapper.getByRole('listitem').firstChild; + + expect(content.tagName).toBe('BUTTON'); + }); + + a11y('ContainedListItem, interactive'); + }); +}); diff --git a/packages/react/src/components/ContainedList/index.js b/packages/react/src/components/ContainedList/index.js new file mode 100644 index 000000000000..15087cf386d9 --- /dev/null +++ b/packages/react/src/components/ContainedList/index.js @@ -0,0 +1,14 @@ +/** + * Copyright IBM Corp. 2022 + * + * 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 ContainedList from './ContainedList'; +import ContainedListItem from './ContainedListItem'; + +ContainedList.ContainedListItem = ContainedListItem; + +export { ContainedListItem }; +export default ContainedList; diff --git a/packages/react/src/components/ContainedList/next/ContainedList.stories.js b/packages/react/src/components/ContainedList/next/ContainedList.stories.js new file mode 100644 index 000000000000..6a2541bb3cef --- /dev/null +++ b/packages/react/src/components/ContainedList/next/ContainedList.stories.js @@ -0,0 +1,280 @@ +/** + * Copyright IBM Corp. 2022 + * + * 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 { + Apple, + Fish, + Information, + Strawberry, + SubtractAlt, + Wheat, +} from '@carbon/icons-react'; +import { VStack } from '../../Stack'; +import Button from '../../Button'; +import ExpandableSearch from '../../ExpandableSearch'; +import Tag from '../../Tag'; +import { Tooltip } from '../../Tooltip/next'; + +import ContainedList, { ContainedListItem } from '../'; + +export default { + title: 'Experimental/unstable_ContainedList', + component: ContainedList, +}; + +export const OnPage = () => ( + <> + <ContainedList label="List title" kind="on-page"> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + </ContainedList> + <ContainedList label="List title" kind="on-page"> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + </ContainedList> + </> +); + +export const Disclosed = () => ( + <> + <ContainedList label="List title" kind="disclosed"> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + </ContainedList> + <ContainedList label="List title" kind="disclosed"> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + </ContainedList> + </> +); + +export const Interactive = () => { + const onClick = action('onClick (ContainedListItem)'); + + return ( + <VStack gap={12}> + <ContainedList label="List title" kind="on-page"> + <ContainedListItem onClick={onClick}>List item</ContainedListItem> + <ContainedListItem onClick={onClick} disabled> + List item + </ContainedListItem> + <ContainedListItem onClick={onClick}>List item</ContainedListItem> + <ContainedListItem onClick={onClick}>List item</ContainedListItem> + </ContainedList> + <ContainedList label="List title" kind="disclosed"> + <ContainedListItem onClick={onClick}>List item</ContainedListItem> + <ContainedListItem onClick={onClick} disabled> + List item + </ContainedListItem> + <ContainedListItem onClick={onClick}>List item</ContainedListItem> + <ContainedListItem onClick={onClick}>List item</ContainedListItem> + </ContainedList> + </VStack> + ); +}; + +export const Actions = () => { + const itemAction = ( + <Button + kind="ghost" + iconDescription="Dismiss" + hasIconOnly + renderIcon={SubtractAlt} + /> + ); + + return ( + <VStack gap={12}> + <ContainedList + label="List title" + kind="on-page" + action={<ExpandableSearch placeholder="Find item" size="lg" />}> + <ContainedListItem action={itemAction}>List item</ContainedListItem> + <ContainedListItem action={itemAction}>List item</ContainedListItem> + <ContainedListItem action={itemAction}>List item</ContainedListItem> + <ContainedListItem action={itemAction}>List item</ContainedListItem> + </ContainedList> + <ContainedList + label="List title" + kind="disclosed" + action={ + <Button kind="ghost" size="sm"> + Dismiss all + </Button> + }> + <ContainedListItem action={itemAction}>List item</ContainedListItem> + <ContainedListItem action={itemAction} disabled> + List item + </ContainedListItem> + <ContainedListItem action={itemAction}>List item</ContainedListItem> + <ContainedListItem action={itemAction}>List item</ContainedListItem> + </ContainedList> + </VStack> + ); +}; + +export const ActionsInteractive = () => { + const onClick = action('onClick (ContainedListItem)'); + const itemAction = ( + <Button + kind="ghost" + iconDescription="Dismiss" + hasIconOnly + renderIcon={SubtractAlt} + /> + ); + + return ( + <VStack gap={12}> + <ContainedList + label="List title" + kind="on-page" + action={<ExpandableSearch placeholder="Find item" size="lg" />}> + <ContainedListItem action={itemAction} onClick={onClick}> + List item + </ContainedListItem> + <ContainedListItem action={itemAction} onClick={onClick}> + List item + </ContainedListItem> + <ContainedListItem action={itemAction} onClick={onClick}> + List item + </ContainedListItem> + <ContainedListItem action={itemAction} onClick={onClick}> + List item + </ContainedListItem> + </ContainedList> + <ContainedList + label="List title" + kind="disclosed" + action={ + <Button kind="ghost" size="sm"> + Dismiss all + </Button> + }> + <ContainedListItem action={itemAction} onClick={onClick}> + List item + </ContainedListItem> + <ContainedListItem action={itemAction} onClick={onClick}> + List item + </ContainedListItem> + <ContainedListItem action={itemAction} onClick={onClick}> + List item + </ContainedListItem> + <ContainedListItem action={itemAction} onClick={onClick}> + List item + </ContainedListItem> + </ContainedList> + </VStack> + ); +}; + +export const ListTitleDecorators = () => { + return ( + <VStack gap={12}> + <ContainedList + label={ + <div + style={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + }}> + <span>List title</span> + <Tag size="sm">4</Tag> + </div> + } + kind="on-page"> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + </ContainedList> + <ContainedList + label={ + <div style={{ display: 'flex', alignItems: 'center' }}> + <span>List title</span> + <Tooltip align="top" label="Tooltip content"> + <button + className="sb-tooltip-trigger" + style={{ color: 'inherit', border: 'none' }} + type="button"> + <Information style={{ fill: 'currentColor' }} /> + </button> + </Tooltip> + </div> + } + kind="disclosed"> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + <ContainedListItem>List item</ContainedListItem> + </ContainedList> + </VStack> + ); +}; + +export const Icons = () => { + return ( + <VStack gap={12}> + <ContainedList label="List title" kind="on-page"> + <ContainedListItem renderIcon={Apple}>List item</ContainedListItem> + <ContainedListItem renderIcon={Wheat}>List item</ContainedListItem> + <ContainedListItem renderIcon={Strawberry}>List item</ContainedListItem> + <ContainedListItem renderIcon={Fish}>List item</ContainedListItem> + </ContainedList> + <ContainedList label="List title" kind="disclosed"> + <ContainedListItem renderIcon={Apple}>List item</ContainedListItem> + <ContainedListItem renderIcon={Wheat}>List item</ContainedListItem> + <ContainedListItem renderIcon={Strawberry}>List item</ContainedListItem> + <ContainedListItem renderIcon={Fish}>List item</ContainedListItem> + </ContainedList> + </VStack> + ); +}; + +const PlaygroundStory = (args) => ( + <> + {[...Array(4)].map((_, i) => ( + <ContainedList key={i} {...args}> + {[...Array(8)].map((_, j) => ( + <ContainedListItem key={`${i}-${j}`}>List item</ContainedListItem> + ))} + </ContainedList> + ))} + </> +); + +export const Playground = PlaygroundStory.bind({}); + +Playground.argTypes = { + action: { + control: false, + }, + children: { + control: false, + }, + className: { + control: false, + }, + label: { + defaultValue: 'List title', + }, + kind: { + defaultValue: 'on-page', + }, +}; diff --git a/packages/react/src/index.js b/packages/react/src/index.js index 8a6fc7082c96..da42916bf17d 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -205,6 +205,9 @@ export { } from './components/UIShell'; // Experimental +export unstable_ContainedList, { + ContainedListItem as unstable_ContainedListItem, +} from './components/ContainedList'; export { useContextMenu as unstable_useContextMenu } from './components/ContextMenu'; export { FeatureFlags as unstable_FeatureFlags, diff --git a/packages/styles/scss/components/_index.scss b/packages/styles/scss/components/_index.scss index 5992543d7791..c9dd4183726d 100644 --- a/packages/styles/scss/components/_index.scss +++ b/packages/styles/scss/components/_index.scss @@ -12,6 +12,7 @@ @use 'checkbox'; @use 'code-snippet'; @use 'combo-box'; +@use 'contained-list'; @use 'content-switcher'; @use 'copy-button'; @use 'data-table'; diff --git a/packages/styles/scss/components/contained-list/_contained-list.scss b/packages/styles/scss/components/contained-list/_contained-list.scss new file mode 100644 index 000000000000..8d8958bf86cf --- /dev/null +++ b/packages/styles/scss/components/contained-list/_contained-list.scss @@ -0,0 +1,174 @@ +// +// Copyright IBM Corp. 2022 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@use '../../config' as *; +@use '../../motion' as *; +@use '../../spacing' as *; +@use '../../theme' as *; +@use '../../type' as *; +@use '../../utilities/convert' as *; +@use '../../utilities/focus-outline' as *; +@use '../../utilities/button-reset'; + +/// Contained List styles +/// @access public +/// @group contained-list +@mixin contained-list { + .#{$prefix}--contained-list__header { + position: sticky; + z-index: 1; + top: 0; + display: flex; + align-items: center; + padding-inline: $spacing-05; + } + + .#{$prefix}--contained-list__label { + width: 100%; + } + + // "On Page" variant + + .#{$prefix}--contained-list--on-page + .#{$prefix}--contained-list--on-page { + margin-block-start: $spacing-05; + } + + .#{$prefix}--contained-list--on-page .#{$prefix}--contained-list__header { + @include type-style('heading-compact-01'); + + height: $spacing-09; + border-bottom: 1px solid $border-subtle; + background-color: $background; + color: $text-primary; + } + + .#{$prefix}--layer-two + .#{$prefix}--contained-list--on-page + .#{$prefix}--contained-list__header { + background-color: $layer-01; + } + + .#{$prefix}--layer-three + .#{$prefix}--contained-list--on-page + .#{$prefix}--contained-list__header { + background-color: $layer-02; + } + + // "Disclosed" variant + + .#{$prefix}--contained-list--disclosed .#{$prefix}--contained-list__header { + @include type-style('label-01'); + + height: $spacing-07; + background-color: $layer; + color: $text-secondary; + } + + // List item + + .#{$prefix}--contained-list-item { + position: relative; + } + + .#{$prefix}--contained-list-item:not(:first-of-type) { + margin-top: -1px; + } + + .#{$prefix}--contained-list-item--clickable + .#{$prefix}--contained-list-item__content { + @include button-reset.reset; + + text-align: start; + transition: background-color $duration-moderate-01 + motion(standard, productive); + } + + .#{$prefix}--contained-list-item__content, + .#{$prefix}--contained-list-item--clickable + .#{$prefix}--contained-list-item__content { + @include type-style('body-01'); + + padding: calc(#{$spacing-05} - #{rem(2px)}) $spacing-05; + color: $text-primary; + } + + .#{$prefix}--contained-list-item:not(:last-of-type)::before { + position: absolute; + right: $spacing-05; + bottom: 0; + left: $spacing-05; + height: 1px; + background-color: $border-subtle; + content: ''; + } + + .#{$prefix}--contained-list-item--clickable + .#{$prefix}--contained-list-item__content:not(:disabled):hover { + background-color: $layer-hover; + } + + .#{$prefix}--contained-list-item--clickable + .#{$prefix}--contained-list-item__content:not(:disabled):active { + background-color: $layer-active; + } + + .#{$prefix}--contained-list-item--clickable + .#{$prefix}--contained-list-item__content:disabled { + color: $text-disabled; + cursor: not-allowed; + } + + .#{$prefix}--contained-list-item--clickable + .#{$prefix}--contained-list-item__content:focus { + outline: none; + } + + .#{$prefix}--contained-list-item--clickable + .#{$prefix}--contained-list-item__content:focus::after { + @include focus-outline('outline'); + + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + content: ''; + } + + .#{$prefix}--contained-list-item--with-action + .#{$prefix}--contained-list-item__content { + padding-inline-end: $spacing-10; + } + + .#{$prefix}--contained-list__action, + .#{$prefix}--contained-list-item__action { + position: absolute; + top: 0; + right: 0; + left: 0; + display: flex; + justify-content: flex-end; + pointer-events: none; + } + + .#{$prefix}--contained-list__action > *, + .#{$prefix}--contained-list-item__action > * { + pointer-events: all; + } + + .#{$prefix}--contained-list-item--with-icon + .#{$prefix}--contained-list-item__content { + display: grid; + column-gap: $spacing-04; + grid-template-columns: 1rem 1fr; + } + + .#{$prefix}--contained-list-item__icon { + display: inline-flex; + padding-top: $spacing-01; + } +} diff --git a/packages/styles/scss/components/contained-list/_index.scss b/packages/styles/scss/components/contained-list/_index.scss new file mode 100644 index 000000000000..1ac61680c5dd --- /dev/null +++ b/packages/styles/scss/components/contained-list/_index.scss @@ -0,0 +1,11 @@ +// +// Copyright IBM Corp. 2022 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@forward 'contained-list'; +@use 'contained-list'; + +@include contained-list.contained-list;