diff --git a/pages/cards/selection.page.tsx b/pages/cards/selection.page.tsx index 3ec389b931..3a3db17187 100644 --- a/pages/cards/selection.page.tsx +++ b/pages/cards/selection.page.tsx @@ -2,16 +2,28 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useState } from 'react'; import range from 'lodash/range'; -import Cards, { CardsProps } from '~components/cards/index'; -import Header from '~components/header/index'; +import Cards, { CardsProps } from '~components/cards'; +import Header from '~components/header'; +import Toggle from '~components/toggle'; +import { EmptyState, getMatchesCountText, paginationLabels } from '../table/shared-configs'; +import { useCollection } from '@cloudscape-design/collection-hooks'; +import Pagination from '~components/pagination'; +import TextFilter from '~components/text-filter'; interface Item { number: number; text: string; } +const renderAriaLive: CardsProps['renderAriaLive'] = ({ firstIndex, lastIndex, totalItemsCount }) => + `Displaying items ${firstIndex} to ${lastIndex} of ${totalItemsCount}`; + +const getHeaderCounterText = (items: ReadonlyArray, selectedItems: ReadonlyArray | undefined) => { + return selectedItems && selectedItems?.length > 0 ? `(${selectedItems.length}/${items.length})` : `(${items.length})`; +}; + const ariaLabels: CardsProps['ariaLabels'] = { - selectionGroupLabel: 'group label', + selectionGroupLabel: 'Resource selection', itemSelectionLabel: ({ selectedItems }, item) => `${item.text} is ${!selectedItems.includes(item) ? 'not ' : ''}selected`, }; @@ -37,21 +49,57 @@ const cardDefinition: CardsProps.CardDefinition = { ], }; -const items = createSimpleItems(4); +const allItems = createSimpleItems(50); export default function () { - const [selectedItems, setSelectedItems] = useState([]); + const [selectionType, setSelectionType] = useState('multi'); + const [entireCard, setEntireCard] = useState(false); + const [someDisabled, setSomeDisabled] = useState(false); + + const { items, filteredItemsCount, collectionProps, filterProps, paginationProps } = useCollection(allItems, { + filtering: { + empty: , + noMatch: , + }, + pagination: { pageSize: 10 }, + selection: {}, + }); + return ( <>

Cards selection

+ setEntireCard(event.detail.checked)}> + Allow clicking entire card to select + + setSomeDisabled(event.detail.checked)}> + Make some card elements inactive + + setSelectionType(event.detail.checked ? 'multi' : 'single')} + > + Use multi selection + + {...collectionProps} items={items} cardDefinition={cardDefinition} - header={
Cards header
} - selectionType="multi" - selectedItems={selectedItems} - onSelectionChange={({ detail }) => setSelectedItems(detail.selectedItems)} + header={
Resources
} + selectionType={selectionType} ariaLabels={ariaLabels} + renderAriaLive={renderAriaLive} + entireCardClickable={entireCard} + isItemDisabled={item => someDisabled && !item.text.includes('o')} + pagination={} + filter={ + + } /> ); diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 8fb978643a..185a64bc46 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -3827,6 +3827,13 @@ Default value: "optional": true, "type": "string", }, + Object { + "description": "Activating this property makes the entire card clickable to select it. +Don't use this property if the card has any other interactive elements.", + "name": "entireCardClickable", + "optional": true, + "type": "boolean", + }, Object { "description": " Use this property to inform screen readers which range of cards is currently displayed. It specifies the index (1-based) of the first card. diff --git a/src/cards/__tests__/selection.test.tsx b/src/cards/__tests__/selection.test.tsx index 4c2092fdb4..8dda9aadf7 100644 --- a/src/cards/__tests__/selection.test.tsx +++ b/src/cards/__tests__/selection.test.tsx @@ -66,6 +66,21 @@ describe('Cards selection', () => { ).wrapper; expect(getSelectedCardsText()).toEqual(['0']); }); + + it('cannot select items by clicking anywhere on the card by default', () => { + wrapper = renderCards( {...props} selectionType={selectionType} />).wrapper; + getCard(1).findCardHeader()?.click(); + expect(handleSelectionChange).not.toHaveBeenCalled(); + }); + + it('can select items by clicking anywhere on the card when entireCardClickable is enabled', () => { + wrapper = renderCards( + {...props} selectionType={selectionType} entireCardClickable={true} /> + ).wrapper; + getCard(1).findCardHeader()?.click(); + expectSelected([{ description: '1' }]); + expect(getCardSelectionArea(1)?.find('input')?.getElement()).toHaveFocus(); + }); }); describe('Single selection', () => { diff --git a/src/cards/index.tsx b/src/cards/index.tsx index d26dfc0f61..78c5eda26a 100644 --- a/src/cards/index.tsx +++ b/src/cards/index.tsx @@ -59,6 +59,7 @@ const Cards = React.forwardRef(function ( renderAriaLive, firstIndex, totalItemsCount, + entireCardClickable, ...rest }: CardsProps, ref: React.Ref @@ -187,6 +188,7 @@ const Cards = React.forwardRef(function ( onFocus={onCardFocus} ariaLabel={ariaLabels?.cardsLabel} ariaLabelledby={isLabelledByHeader && headerIdRef.current ? headerIdRef.current : undefined} + entireCardClickable={entireCardClickable} /> )} @@ -212,7 +214,11 @@ const CardsList = ({ onFocus, ariaLabelledby, ariaLabel, -}: Pick, 'items' | 'cardDefinition' | 'trackBy' | 'selectionType' | 'visibleSections'> & { + entireCardClickable, +}: Pick< + CardsProps, + 'items' | 'cardDefinition' | 'trackBy' | 'selectionType' | 'visibleSections' | 'entireCardClickable' +> & { columns: number | null; isItemSelected: (item: T) => boolean; getItemSelectionProps: (item: T) => SelectionControlProps; @@ -223,6 +229,7 @@ const CardsList = ({ ariaDescribedby?: string; }) => { const selectable = !!selectionType; + const canClickEntireCard = selectable && entireCardClickable; const { moveFocusDown, moveFocusUp } = useSelectionFocusMove(selectionType, items.length); @@ -260,7 +267,18 @@ const CardsList = ({ {...(focusMarkers && focusMarkers.item)} role={listItemRole} > -
+
{ + getItemSelectionProps(item).onChange(); + // Manually move focus to the native input (checkbox or radio button) + event.currentTarget.querySelector('input')?.focus(); + } + : undefined + } + >
{cardDefinition.header ? cardDefinition.header(item) : ''} diff --git a/src/cards/interfaces.tsx b/src/cards/interfaces.tsx index acc07e8a87..5648e7493e 100644 --- a/src/cards/interfaces.tsx +++ b/src/cards/interfaces.tsx @@ -207,6 +207,12 @@ export interface CardsProps extends BaseComponentProps { * @visualrefresh `full-page` variant */ variant?: 'container' | 'full-page'; + + /** + * Activating this property makes the entire card clickable to select it. + * Don't use this property if the card has any other interactive elements. + */ + entireCardClickable?: boolean; } export namespace CardsProps {