diff --git a/packages/core/src/common/_color-aliases.scss b/packages/core/src/common/_color-aliases.scss index c81def3560..42c22a1244 100644 --- a/packages/core/src/common/_color-aliases.scss +++ b/packages/core/src/common/_color-aliases.scss @@ -43,8 +43,10 @@ $pt-dark-icon-color-disabled: $pt-dark-text-color-disabled !default; $pt-dark-icon-color-selected: $pt-intent-primary !default; $pt-divider-black: rgba($black, 0.15) !default; +$pt-divider-black-muted: rgba($black, 0.1) !default; $pt-dark-divider-black: rgba($black, 0.4) !default; $pt-dark-divider-white: rgba($white, 0.2) !default; +$pt-dark-divider-white-muted: rgba($white, 0.1) !default; $pt-code-text-color: $pt-text-color-muted !default; $pt-dark-code-text-color: $pt-dark-text-color-muted !default; diff --git a/packages/core/src/common/classes.ts b/packages/core/src/common/classes.ts index c17aec0142..b80af0d17f 100644 --- a/packages/core/src/common/classes.ts +++ b/packages/core/src/common/classes.ts @@ -114,6 +114,9 @@ export const CALLOUT_ICON = `${CALLOUT}-icon`; export const CARD = `${NS}-card`; +export const CARD_LIST = `${NS}-card-list`; +export const CARD_LIST_BORDERED = `${CARD_LIST}-bordered`; + export const COLLAPSE = `${NS}-collapse`; export const COLLAPSE_BODY = `${COLLAPSE}-body`; diff --git a/packages/core/src/components/_index.scss b/packages/core/src/components/_index.scss index b45a29ebf0..43483c3ba9 100644 --- a/packages/core/src/components/_index.scss +++ b/packages/core/src/components/_index.scss @@ -7,6 +7,7 @@ @import "button/button-group"; @import "callout/callout"; @import "card/card"; +@import "card-list/card-list"; @import "collapse/collapse"; @import "context-menu/context-menu"; @import "divider/divider"; diff --git a/packages/core/src/components/card-list/card-list.md b/packages/core/src/components/card-list/card-list.md new file mode 100644 index 0000000000..94ea125c36 --- /dev/null +++ b/packages/core/src/components/card-list/card-list.md @@ -0,0 +1,52 @@ +--- +tag: new +--- + +@# Card List + +__CardList__ is a lightweight wrapper around the [__Card__](#core/components/card) component. It can be used to +visually group together cards in a list without any excess visual weight around or between them. Long lists may +be styled with CSS to scroll vertically. + +@reactExample CardListExample + +@## Usage + +```tsx +import { Card, CardList } from "@blueprintjs/core"; + + + Basil + Olive oil + Kosher Salt + Garlic + Pine nuts + Parmigiano Reggiano + +``` + +@## Combining with Section + +__CardList__ may be used as content for the [__Section__](#core/components/section) component (inside a nested +__SectionCard__). This allows support for features like a title & description above the list. + +Set the same value for `` and `` (either `true` or `false` for both) to get two +different kinds of appearances. + +```tsx +import { Card, CardList, Section, SectionCard } from "@blueprintjs/core"; + +
+ + + Basil + Olive oil + {/* ... */} + + +
+``` + +@## Props interface + +@interface CardListProps diff --git a/packages/core/src/components/card-list/card-list.scss b/packages/core/src/components/card-list/card-list.scss new file mode 100644 index 0000000000..f368d5a723 --- /dev/null +++ b/packages/core/src/components/card-list/card-list.scss @@ -0,0 +1,64 @@ +@import "../../common/variables"; + +$card-list-border-width: 1px !default; + +// N.B. min-height is calculated as height of a button + vertical padding. We need to add an extra pixel to account for +// the bottom border. +$card-list-item-default-min-height: ($pt-grid-size * 5) + $card-list-border-width !default; +$card-list-item-default-padding: $pt-grid-size (2 * $pt-grid-size) !default; + +$card-list-item-small-min-height: ($pt-grid-size * 4) + $card-list-border-width !default; +$card-list-item-small-padding: 7px 15px !default; + +.#{$ns}-card-list { + overflow: auto; + padding: 0; + width: 100%; + + > .#{$ns}-card { + align-items: center; + border-radius: 0; + box-shadow: none; + display: flex; + min-height: $card-list-item-default-min-height; + padding: $card-list-item-default-padding; + + &.#{$ns}-interactive:hover, + &.#{$ns}-interactive:active { + background-color: $light-gray5; + box-shadow: none; + + .#{$ns}-dark & { + background-color: $dark-gray4; + } + } + + &:not(:last-child) { + border-bottom: $card-list-border-width solid $pt-divider-black-muted; + + .#{$ns}-dark & { + border-color: $pt-dark-divider-white-muted; + } + } + } + + &.#{$ns}-compact > .#{$ns}-card { + min-height: $card-list-item-small-min-height; + padding: $card-list-item-small-padding; + } + + .#{$ns}-dark & { + // card border is inset in dark theme, so we need to add padding to prevent items from going over it + padding: 1px; + } + + &:not(.#{$ns}-card-list-bordered) { + border-radius: 0; + box-shadow: none; + + .#{$ns}-dark & { + margin: 1px; + width: calc(100% - 2px); + } + } +} diff --git a/packages/core/src/components/card-list/cardList.tsx b/packages/core/src/components/card-list/cardList.tsx new file mode 100644 index 0000000000..2809e661ca --- /dev/null +++ b/packages/core/src/components/card-list/cardList.tsx @@ -0,0 +1,61 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import classNames from "classnames"; +import * as React from "react"; + +import { Classes, DISPLAYNAME_PREFIX, Elevation, HTMLDivProps, Props } from "../../common"; +import { Card } from "../card/card"; + +export interface CardListProps extends Props, HTMLDivProps, React.RefAttributes { + /** + * Whether this container element should have a visual border. + * + * Set this to `false` to remove elevation and border radius styles, which allows this element to be a child of + * another bordered container element without padding (like SectionCard). Note that this also sets a 1px margin + * _in dark theme_ to account for inset box shadows in that theme used across the design system. Be sure to test + * your UI in both light and dark theme if you modify this prop value. + * + * @default true + */ + bordered?: boolean; + + /** + * Whether this component should use compact styles with reduced visual padding. + * + * @default false + */ + compact?: boolean; +} + +export const CardList: React.FC = React.forwardRef((props, ref) => { + const { bordered, className, children, compact, ...htmlProps } = props; + + const classes = classNames(className, Classes.CARD_LIST, { + [Classes.CARD_LIST_BORDERED]: bordered, + [Classes.COMPACT]: compact, + }); + + return ( + + {children} + + ); +}); +CardList.defaultProps = { + compact: false, +}; +CardList.displayName = `${DISPLAYNAME_PREFIX}.CardList`; diff --git a/packages/core/src/components/components.md b/packages/core/src/components/components.md index c0805baeeb..db1b0fed18 100644 --- a/packages/core/src/components/components.md +++ b/packages/core/src/components/components.md @@ -7,6 +7,7 @@ @page button-group @page callout @page card +@page card-list @page collapse @page divider @page editable-text diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index 9da930f4ae..18d732ffc9 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -27,6 +27,7 @@ export type { export { ButtonGroup, ButtonGroupProps } from "./button/buttonGroup"; export { Callout, CalloutProps } from "./callout/callout"; export { Card, CardProps } from "./card/card"; +export { CardList, CardListProps } from "./card-list/cardList"; export { Collapse, CollapseProps } from "./collapse/collapse"; export { ContextMenu, diff --git a/packages/core/test/card-list/cardListTests.tsx b/packages/core/test/card-list/cardListTests.tsx new file mode 100644 index 0000000000..d2ecdaeefe --- /dev/null +++ b/packages/core/test/card-list/cardListTests.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert } from "chai"; +import { mount } from "enzyme"; +import * as React from "react"; + +import { Card, CardList, Classes } from "../../src"; + +describe("", () => { + it("supports className prop", () => { + const TEST_CLASS = "test-class"; + const wrapper = mount( + + first + second + , + ); + + assert.isTrue(wrapper.find(`.${Classes.CARD_LIST}`).hostNodes().hasClass(TEST_CLASS), TEST_CLASS); + }); + + it("supports HTML props", () => { + const cardList = mount().find("div"); + assert.strictEqual(cardList.prop("title"), "foo"); + }); + + it("supports ref prop", () => { + const elementRef = React.createRef(); + mount(); + assert.isDefined(elementRef.current); + }); +}); diff --git a/packages/core/test/index.ts b/packages/core/test/index.ts index 198d5c35f2..234eb75443 100644 --- a/packages/core/test/index.ts +++ b/packages/core/test/index.ts @@ -27,6 +27,7 @@ import "./breadcrumbs/breadcrumbTests"; import "./buttons/buttonTests"; import "./callout/calloutTests"; import "./card/cardTests"; +import "./card-list/cardListTests"; import "./collapse/collapseTests"; import "./context-menu/contextMenuTests"; import "./context-menu/contextMenuSingletonTests"; diff --git a/packages/docs-app/src/examples/core-examples/cardListExample.tsx b/packages/docs-app/src/examples/core-examples/cardListExample.tsx new file mode 100644 index 0000000000..8a02c536fb --- /dev/null +++ b/packages/docs-app/src/examples/core-examples/cardListExample.tsx @@ -0,0 +1,160 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import classNames from "classnames"; +import * as React from "react"; + +import { Button, Card, CardList, Classes, Code, H5, Section, SectionCard, Switch } from "@blueprintjs/core"; +import { Example, ExampleProps, handleBooleanChange } from "@blueprintjs/docs-theme"; +import { ChevronRight } from "@blueprintjs/icons"; + +import { PropCodeTooltip } from "../../common/propCodeTooltip"; + +export interface CardListExampleState { + isBordered: boolean; + isCompact: boolean; + useInteractiveCards: boolean; + useScrollableContainer: boolean; + useSectionCardPadding: boolean; + useSectionContainer: boolean; +} + +export class CardListExample extends React.PureComponent { + public state: CardListExampleState = { + isBordered: true, + isCompact: false, + useInteractiveCards: true, + useScrollableContainer: false, + useSectionCardPadding: false, + useSectionContainer: false, + }; + + private get isBordered() { + return this.state.useSectionContainer ? this.state.useSectionCardPadding : this.state.isBordered; + } + + public render() { + const { isCompact, useInteractiveCards, useScrollableContainer, useSectionCardPadding, useSectionContainer } = + this.state; + + const options = ( + <> +
CardList Props
+ + This example overrides isBordered when using a Section container + + } + > + + + +
Card Props
+ +
Layout
+ + Use Section container + + } + onChange={this.toggleUseSectionContainer} + /> +
SectionCard
+ + + + ); + + const sectionCardClasses = classNames("docs-section-card", { + "docs-section-card-limited-height": useScrollableContainer, + }); + + return ( + +
+ {useSectionContainer ? ( +
+ + {this.renderList()} + +
+ ) : ( + this.renderList() + )} +
+
+ ); + } + + private renderList() { + const { isCompact, useInteractiveCards } = this.state; + const ingredients = ["Basil", "Olive oil", "Kosher salt", "Garlic", "Pine nuts", "Parmigiano Reggiano"]; + + return ( + + {ingredients.map(ingredient => ( + + {ingredient} + {useInteractiveCards ? ( + + ) : ( +