diff --git a/packages/core/story-structure.js b/packages/core/story-structure.js index ce26c9c6f8..8763918132 100644 --- a/packages/core/story-structure.js +++ b/packages/core/story-structure.js @@ -54,6 +54,7 @@ const s = [ 'c/Datagrid/Extensions/EditableCell', 'c/Datagrid/Extensions/ColumnCustomization', 'c/Datagrid/Extensions/Skeleton', + 'c/Datagrid/Extensions/Slug', ], }, { n: 'DelimitedList', s: ['c/DelimitedList'] }, diff --git a/packages/ibm-products-styles/src/components/Datagrid/styles/_datagrid.scss b/packages/ibm-products-styles/src/components/Datagrid/styles/_datagrid.scss index 06d0ce4719..70690c91bd 100644 --- a/packages/ibm-products-styles/src/components/Datagrid/styles/_datagrid.scss +++ b/packages/ibm-products-styles/src/components/Datagrid/styles/_datagrid.scss @@ -6,6 +6,7 @@ // @use '@carbon/styles/scss/theme' as *; +@use '@carbon/styles/scss/utilities' as *; @use '@carbon/layout/scss/convert' as *; @use '@carbon/styles/scss/spacing' as *; @use '@carbon/react/scss/components/button/tokens' as *; @@ -155,6 +156,16 @@ background-color: $background; } +.#{$block-class} th.#{$block-class}__with-slug { + @include ai-gradient('top', 100%); +} + +.#{$block-class} + th.#{$block-class}__with-slug + .#{c4p-settings.$carbon-prefix}--slug { + margin-left: $spacing-03; +} + .#{$block-class}__grid-container { display: block; width: 100%; @@ -235,6 +246,10 @@ white-space: initial; } + .#{$block-class}__defaultStringRenderer.#{$block-class}__defaultStringRenderer--slug { + width: fit-content; + } + .#{$block-class}__expanded-row { display: flex; overflow: hidden; diff --git a/packages/ibm-products/src/components/Datagrid/Datagrid/DatagridHeaderRow.js b/packages/ibm-products/src/components/Datagrid/Datagrid/DatagridHeaderRow.js index 04e3674103..eb7a904413 100644 --- a/packages/ibm-products/src/components/Datagrid/Datagrid/DatagridHeaderRow.js +++ b/packages/ibm-products/src/components/Datagrid/Datagrid/DatagridHeaderRow.js @@ -1,6 +1,6 @@ /* eslint-disable react/prop-types */ /** - * Copyright IBM Corp. 2020, 2023 + * Copyright IBM Corp. 2020, 2024 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -17,6 +17,7 @@ import { handleColumnResizingEvent, } from './addons/stateReducer'; import { getNodeTextContent } from '../../../global/js/utils/getNodeTextContent'; +import { ColumnHeaderSlug } from './addons/Slug/ColumnHeaderSlug'; const blockClass = `${pkg.prefix}--datagrid`; @@ -92,7 +93,7 @@ const ResizeHeader = ({ }; const HeaderRow = (datagridState, headRef, headerGroup) => { - const { resizerAriaLabel } = datagridState; + const { resizerAriaLabel, isTableSortable } = datagridState; // Used to measure the height of the table and uses that value // to display a vertical line to indicate the column you are resizing useEffect(() => { @@ -149,6 +150,13 @@ const HeaderRow = (datagridState, headRef, headerGroup) => { ...headerGroupProps } = headerGroup.getHeaderGroupProps(); + const renderSlug = (slug) => { + if (isTableSortable) { + return; + } + return ; + }; + return ( { datagridState.isTableSortable && header.id !== 'spacer', [`${blockClass}__isSorted`]: header.isSorted, [`${blockClass}__header-actions-column`]: header.isAction, + [`${blockClass}__with-slug`]: + header.slug && React.isValidElement(header.slug), })} key={header.id} aria-hidden={header.id === 'spacer' && 'true'} {...getAccessibilityProps(header)} > {header.render('Header')} + {renderSlug(header.slug)} {resizerProps && !header.isAction && ( { + if (slug && isValidElement(slug)) { + const normalizedSlug = React.cloneElement(slug, { + size: 'mini', + ref, + }); + return normalizedSlug; + } + return; +}); + +ColumnHeaderSlug.propTypes = { + /** + * Specify the AI slug to be displayed + */ + slug: PropTypes.node, +}; diff --git a/packages/ibm-products/src/components/Datagrid/Extensions/Slug/Slug.stories.js b/packages/ibm-products/src/components/Datagrid/Extensions/Slug/Slug.stories.js new file mode 100644 index 0000000000..1a410b2f3f --- /dev/null +++ b/packages/ibm-products/src/components/Datagrid/Extensions/Slug/Slug.stories.js @@ -0,0 +1,274 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +//cspell: disable +/** + * Copyright IBM Corp. 2024, 2024 + * + * 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, { useState } from 'react'; +import { Edit, TrashCan, FolderOpen, View, Folders } from '@carbon/react/icons'; +import { + unstable__Slug as Slug, + unstable__SlugContent as SlugContent, + unstable__SlugActions as SlugActions, + IconButton, + Button, +} from '@carbon/react'; +import { action } from '@storybook/addon-actions'; +import { + getStoryTitle, + prepareStory, +} from '../../../../global/js/utils/story-helper'; +import { Datagrid, useDatagrid, useSortableColumns } from '../../index'; +import styles from '../../_storybook-styles.scss'; +import { DatagridActions } from '../../utils/DatagridActions'; +import { makeData } from '../../utils/makeData'; +import { ARG_TYPES } from '../../utils/getArgTypes'; +import { StoryDocsPage } from '../../../../global/js/utils/StoryDocsPage'; + +export default { + title: `${getStoryTitle(Datagrid.displayName)}/Extensions/Slug`, + component: Datagrid, + tags: ['autodocs'], + parameters: { + styles, + docs: { + page: () => , + }, + layout: 'fullscreen', + }, + argTypes: { + featureFlags: { + table: { + disable: true, + }, + }, + }, +}; + +const columnSlug = ( + + + + AI Explained + 84% + Confidence score + + Lorem ipsum dolor sit amet, di os consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut fsil labore et dolore magna aliqua. + + + Model type + Foundation model + + + + + + + + + + + + View details + + + +); + +const defaultHeader = [ + { + Header: 'Row Index', + accessor: (row, i) => i, + sticky: 'left', + id: 'rowIndex', // id is required when accessor is a function. + }, + { + Header: 'First Name', + accessor: 'firstName', + }, + { + Header: 'Last Name', + accessor: 'lastName', + }, + { + Header: 'Age', + accessor: 'age', + width: 60, + }, + { + Header: 'Visits', + accessor: 'visits', + width: 120, + slug: columnSlug, + }, + { + Header: 'Someone 1', + accessor: 'someone1', + slug: columnSlug, + width: 200, + }, + { + Header: 'Someone 2', + accessor: 'someone2', + }, + { + Header: 'Someone 3', + accessor: 'someone3', + }, + { + Header: 'Someone 4', + accessor: 'someone4', + }, + { + Header: 'Someone 5', + accessor: 'someone5', + }, + { + Header: 'Someone 6', + accessor: 'someone6', + }, + { + Header: 'Someone 7', + accessor: 'someone7', + }, + { + Header: 'Someone 8', + accessor: 'someone8', + }, + { + Header: 'Someone 9', + accessor: 'someone9', + }, + { + Header: 'Someone 10', + accessor: 'someone10', + }, +]; + +const sharedDatagridProps = { + emptyStateTitle: 'Empty state title', + emptyStateDescription: 'Description text explaining why table is empty', + emptyStateSize: 'lg', + gridTitle: 'Data table title', + gridDescription: 'Additional information if needed', + useDenseHeader: false, + rowSize: 'lg', + rowSizes: [ + { + value: 'xl', + labelText: 'Extra large', + }, + { + value: 'lg', + labelText: 'Large', + }, + { + value: 'md', + labelText: 'Medium', + }, + { + value: 'xs', + labelText: 'Small', + }, + ], + onRowSizeChange: (value) => { + console.log('row size changed to: ', value); + }, + rowActions: [ + { + id: 'edit', + itemText: 'Edit', + icon: Edit, + onClick: action('Clicked row action: edit'), + }, + + { + id: 'delete', + itemText: 'Delete', + icon: TrashCan, + isDelete: true, + onClick: action('Clicked row action: delete'), + }, + ], +}; + +const controlProps = { + gridTitle: sharedDatagridProps.gridTitle, + gridDescription: sharedDatagridProps.gridDescription, + useDenseHeader: sharedDatagridProps.useDenseHeader, + rowSize: sharedDatagridProps.rowSize, + rowSizes: sharedDatagridProps.rowSizes, + onRowSizeChange: sharedDatagridProps.onRowSizeChange, +}; + +const GridWithSlugColumnHeader = ({ withSorting, ...args }) => { + const columns = React.useMemo(() => defaultHeader, []); + const [data] = useState(makeData(10, 2)); + const datagridState = useDatagrid( + { + columns, + data, + DatagridActions, + ...args.defaultGridProps, + }, + withSorting ? useSortableColumns : '' + ); + + return ; +}; + +const GridWithSlugColumnHeaderWrapper = ({ withSorting, ...args }) => { + return ( + + ); +}; + +const slugColumnHeaderStoryName = 'Column slug'; +export const SlugColumnHeaderStory = prepareStory( + GridWithSlugColumnHeaderWrapper, + { + storyName: slugColumnHeaderStoryName, + argTypes: { + gridTitle: ARG_TYPES.gridTitle, + gridDescription: ARG_TYPES.gridDescription, + useDenseHeader: ARG_TYPES.useDenseHeader, + rowSize: ARG_TYPES.rowSize, + rowSizes: ARG_TYPES.rowSizes, + onRowSizeChange: ARG_TYPES.onRowSizeChange, + expanderButtonTitleExpanded: 'Collapse row', + expanderButtonTitleCollapsed: 'Expand row', + }, + args: { + ...controlProps, + }, + } +); + +const slugSortableColumnHeaderStoryName = 'Column slug sort'; +export const SlugSortableColumnHeaderStory = prepareStory( + GridWithSlugColumnHeaderWrapper, + { + storyName: slugSortableColumnHeaderStoryName, + argTypes: { + gridTitle: ARG_TYPES.gridTitle, + gridDescription: ARG_TYPES.gridDescription, + useDenseHeader: ARG_TYPES.useDenseHeader, + rowSize: ARG_TYPES.rowSize, + rowSizes: ARG_TYPES.rowSizes, + onRowSizeChange: ARG_TYPES.onRowSizeChange, + expanderButtonTitleExpanded: 'Collapse row', + expanderButtonTitleCollapsed: 'Expand row', + }, + args: { + ...controlProps, + withSorting: true, + }, + } +); diff --git a/packages/ibm-products/src/components/Datagrid/useDefaultStringRenderer.js b/packages/ibm-products/src/components/Datagrid/useDefaultStringRenderer.js index 7091a88791..17379563f5 100644 --- a/packages/ibm-products/src/components/Datagrid/useDefaultStringRenderer.js +++ b/packages/ibm-products/src/components/Datagrid/useDefaultStringRenderer.js @@ -23,21 +23,24 @@ const useDefaultStringRenderer = (hooks) => { ); - const HeaderRenderer = (header) => ( + const HeaderRenderer = (header, slug) => { + return ( {header} - ); + )}; const visibleColumns = (columns) => { const columnsWithDefaultCells = columns.map((column) => ({ Cell: StringRenderer, ...column, - Header: column.HeaderRenderer || HeaderRenderer(column.Header), + Header: column.HeaderRenderer || HeaderRenderer(column.Header, column.slug), })); return [...columnsWithDefaultCells]; }; diff --git a/packages/ibm-products/src/components/Datagrid/useSortableColumns.js b/packages/ibm-products/src/components/Datagrid/useSortableColumns.js index 581c827912..bed00c267a 100644 --- a/packages/ibm-products/src/components/Datagrid/useSortableColumns.js +++ b/packages/ibm-products/src/components/Datagrid/useSortableColumns.js @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2020, 2023 + * Copyright IBM Corp. 2020, 2024 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -11,6 +11,7 @@ import { pkg, carbon } from '../../settings'; import { Button } from '@carbon/react'; import { ArrowUp, ArrowDown, ArrowsVertical } from '@carbon/react/icons'; import { SelectAll } from './Datagrid/DatagridSelectAll'; +import { ColumnHeaderSlug } from './Datagrid/addons/Slug/ColumnHeaderSlug'; const blockClass = `${pkg.prefix}--datagrid`; @@ -62,7 +63,15 @@ const useSortableColumns = (hooks) => { descendingSortableLabelText, defaultSortableLabelText, } = instance; - const onSortClick = (column) => { + const onSortClick = (event, column) => { + const slug = + event.target.classList.contains(`${carbon.prefix}--slug`) || + event.target.closest(`.${carbon.prefix}--slug`); + // Do not continue with sorting if we find a slug + if (slug) { + event.stopPropagation(); + return; + } const key = column.id; const sortDesc = column.isSortedDesc; const { newSortDesc, newOrder } = getNewSortOrder(sortDesc); @@ -105,9 +114,16 @@ const useSortableColumns = (hooks) => { defaultSortableLabelText, })} aria-pressed={getAriaPressedValue(headerProp?.column)} - onClick={() => onSortClick(headerProp?.column)} + onClick={(event) => onSortClick(event, headerProp?.column)} kind="ghost" - renderIcon={(props) => icon(headerProp?.column, props)} + renderIcon={(props) => { + return ( + <> + + {icon(headerProp?.column, props)} + > + ); + }} className={cx( `${carbon.prefix}--table-sort ${blockClass}--table-sort`, {
AI Explained
Confidence score
+ Lorem ipsum dolor sit amet, di os consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut fsil labore et dolore magna aliqua. +
Model type
Foundation model