diff --git a/packages/react/src/components/Table/TableCellRenderer/TableCellRenderer.jsx b/packages/react/src/components/Table/TableCellRenderer/TableCellRenderer.jsx index dcac20791d..f44f52a937 100644 --- a/packages/react/src/components/Table/TableCellRenderer/TableCellRenderer.jsx +++ b/packages/react/src/components/Table/TableCellRenderer/TableCellRenderer.jsx @@ -1,11 +1,12 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { DefinitionTooltip, Tooltip } from '@carbon/react'; +import { Tooltip } from '@carbon/react'; import warning from 'warning'; import { settings } from '../../../constants/Settings'; import { WrapCellTextPropTypes } from '../../../constants/SharedPropTypes'; +import { DefinitionTooltip } from '../../Tooltip'; const { iotPrefix } = settings; @@ -91,6 +92,8 @@ const TableCellRenderer = ({ id="table-header-tooltip" align={tooltipDirection} openOnHover + tooltipText={tooltip} + as="a" > {element} diff --git a/packages/react/src/components/Table/TableHead/ColumnResize.jsx b/packages/react/src/components/Table/TableHead/ColumnResize.jsx index 97a34484d2..1cb3f01b10 100644 --- a/packages/react/src/components/Table/TableHead/ColumnResize.jsx +++ b/packages/react/src/components/Table/TableHead/ColumnResize.jsx @@ -19,11 +19,6 @@ const propTypes = { paddingExtra: PropTypes.number.isRequired, preserveColumnWidths: PropTypes.bool.isRequired, showExpanderColumn: PropTypes.bool.isRequired, - resizeColumnText: PropTypes.string, -}; - -const defaultProps = { - resizeColumnText: 'Resize column', }; const dragHandleWidth = 4; @@ -96,7 +91,6 @@ const ColumnResize = React.forwardRef((props, ref) => { paddingExtra, showExpanderColumn, preserveColumnWidths, - resizeColumnText, } = props; const [startX, setStartX] = useState(0); const [leftPosition, setLeftPosition] = useState(0); @@ -175,11 +169,8 @@ const ColumnResize = React.forwardRef((props, ref) => { })); return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events + // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
e.stopPropagation()} onMouseDown={(e) => onMouseDown(e)} style={{ @@ -194,6 +185,5 @@ const ColumnResize = React.forwardRef((props, ref) => { }); ColumnResize.propTypes = propTypes; -ColumnResize.defaultProps = defaultProps; export default ColumnResize; diff --git a/packages/react/src/components/Table/TableHead/_column-resize.scss b/packages/react/src/components/Table/TableHead/_column-resize.scss index 8926a99b4b..0d28d8dff0 100644 --- a/packages/react/src/components/Table/TableHead/_column-resize.scss +++ b/packages/react/src/components/Table/TableHead/_column-resize.scss @@ -14,6 +14,11 @@ &:hover { background-color: $layer-selected-inverse; } + &:focus { + background-color: $layer-selected-inverse; + outline: $spacing-01 solid $focus; + outline-offset: -$spacing-01; + } } .#{$iot-prefix}--column-resize-handle--dragging { diff --git a/packages/react/src/components/Table/TableHead/_table-head.scss b/packages/react/src/components/Table/TableHead/_table-head.scss index bc8d78de12..d1f456b99e 100644 --- a/packages/react/src/components/Table/TableHead/_table-head.scss +++ b/packages/react/src/components/Table/TableHead/_table-head.scss @@ -56,10 +56,6 @@ } } - th[aria-sort] span.#{$prefix}--popover-container { - padding-left: 1rem; - } - th button.#{$prefix}--definition-term { font-weight: 600; } @@ -255,16 +251,15 @@ } .#{$prefix}--table-sort { - padding-left: 0; - padding-right: 0; + padding-inline-start: $spacing-05; .#{$prefix}--table-header-label, .#{$prefix}--tooltip--definition { - padding-left: $spacing-05; - padding-right: 0; + padding-inline-start: 0; + padding-inline-end: 0; + text-decoration: none; [dir='rtl'] & { - padding-left: unset; padding-right: $spacing-05; } } diff --git a/packages/react/src/components/Table/TableToolbar/TableToolbar.jsx b/packages/react/src/components/Table/TableToolbar/TableToolbar.jsx index b6a5b17ffd..6b503af266 100644 --- a/packages/react/src/components/Table/TableToolbar/TableToolbar.jsx +++ b/packages/react/src/components/Table/TableToolbar/TableToolbar.jsx @@ -328,73 +328,6 @@ const TableToolbar = ({ className={classnames(`${iotPrefix}--table-toolbar`, className)} aria-label={i18n.toolbarLabelAria} > - {hasBatchActionToolbar ? ( - tableTranslateWithId(i18n, ...args)} - > - {hasVisibleBatchActions && - visibleBatchActions.map(({ id, labelText, disabled, ...others }) => ( - onApplyBatchAction(id)} - tabIndex={shouldShowBatchActions ? 0 : -1} - disabled={!shouldShowBatchActions || disabled} - {...others} - > - {labelText} - - ))} - {hasVisibleOverflowBatchActions ? ( - e.stopPropagation()} - renderIcon={(props) => } - tabIndex={shouldShowBatchActions ? 0 : -1} - size="md" - menuOptionsClass={`${iotPrefix}--table-overflow-batch-actions__menu`} - withCarbonTooltip - tooltipPosition="bottom" - buttonLabel={i18n.batchActionsOverflowMenuText} - > - {visibleOverflowBatchActions.map( - ({ - id, - labelText, - disabled, - hasDivider, - isDelete, - renderIcon, - iconDescription, - }) => ( - onApplyBatchAction(id)} - key={`table-batch-actions-overflow-menu-${id}`} - requireTitle={!renderIcon} - hasDivider={hasDivider} - isDelete={isDelete} - aria-label={labelText} - /> - ) - )} - - ) : null} - - ) : null} {secondaryTitle ? ( // eslint-disable-next-line jsx-a11y/label-has-associated-control, jsx-a11y/label-has-for @@ -613,6 +546,73 @@ const TableToolbar = ({ } )} + {hasBatchActionToolbar ? ( + tableTranslateWithId(i18n, ...args)} + > + {hasVisibleBatchActions && + visibleBatchActions.map(({ id, labelText, disabled, ...others }) => ( + onApplyBatchAction(id)} + tabIndex={shouldShowBatchActions ? 0 : -1} + disabled={!shouldShowBatchActions || disabled} + {...others} + > + {labelText} + + ))} + {hasVisibleOverflowBatchActions ? ( + e.stopPropagation()} + renderIcon={(props) => } + tabIndex={shouldShowBatchActions ? 0 : -1} + size="md" + menuOptionsClass={`${iotPrefix}--table-overflow-batch-actions__menu`} + withCarbonTooltip + tooltipPosition="bottom" + buttonLabel={i18n.batchActionsOverflowMenuText} + > + {visibleOverflowBatchActions.map( + ({ + id, + labelText, + disabled, + hasDivider, + isDelete, + renderIcon, + iconDescription, + }) => ( + onApplyBatchAction(id)} + key={`table-batch-actions-overflow-menu-${id}`} + requireTitle={!renderIcon} + hasDivider={hasDivider} + isDelete={isDelete} + aria-label={labelText} + /> + ) + )} + + ) : null} + + ) : null} ); }; diff --git a/packages/react/src/components/Table/TableToolbar/_table-toolbar.scss b/packages/react/src/components/Table/TableToolbar/_table-toolbar.scss index ac8945f93e..e180465474 100644 --- a/packages/react/src/components/Table/TableToolbar/_table-toolbar.scss +++ b/packages/react/src/components/Table/TableToolbar/_table-toolbar.scss @@ -62,6 +62,10 @@ div.#{$prefix}--toolbar-action.#{$prefix}--toolbar-search-container-expandable { will-change: transform; } +.#{$prefix}--batch-actions.#{$prefix}--batch-actions--active.#{$iot-prefix}--table-batch-actions { + z-index: 2; +} + .#{$iot-prefix}--table-toolbar-content { flex: 1; font-size: 0.875rem; @@ -166,15 +170,13 @@ html[dir='rtl'] { } } -.#{$iot-prefix}--table-overflow-batch-actions { +.#{$prefix}--table-toolbar + .#{$prefix}--tooltip-trigger__wrapper + .#{$iot-prefix}--table-overflow-batch-actions { &.#{$prefix}--overflow-menu--open, &.#{$prefix}--overflow-menu--open:hover, - &:hover { - // background-color: $hover-primary;//$$hover-primary; - } - &:focus { - outline: 2px solid $layer-01; + outline: $spacing-01 solid $layer-01; outline-offset: -($spacing-01); } diff --git a/packages/react/src/components/Tooltip/DefinitionTooltip.jsx b/packages/react/src/components/Tooltip/DefinitionTooltip.jsx new file mode 100644 index 0000000000..1d3c3a06a7 --- /dev/null +++ b/packages/react/src/components/Tooltip/DefinitionTooltip.jsx @@ -0,0 +1,236 @@ +/** + * Copied from Carbon Design System + * Copyright IBM Corp. 2016, 2025 + * + * 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 cx from 'classnames'; +import PropTypes from 'prop-types'; +import React, { useState } from 'react'; +import { Popover, PopoverContent } from '@carbon/react'; + +import { match, keys } from '../../internal/keyboard'; +import { settings } from '../../constants/Settings'; + +const { prefix: carbonPrefix } = settings; + +const DefinitionTooltip = ({ + align = 'bottom', + autoAlign, + className, + children, + definition, + defaultOpen = false, + id, + openOnHover, + tooltipText, + triggerClassName, + as = 'button', // NEW: Element type or custom component + renderTrigger, // NEW: Custom render function (alternative) + ...rest +}) => { + const [isOpen, setOpen] = useState(defaultOpen); + const prefix = carbonPrefix || 'cds'; + + // Generate a unique ID if not provided + const tooltipId = id || `definition-tooltip-${Math.random().toString(36).substr(2, 9)}`; + + function onKeyDown(event) { + if (isOpen && match(event, keys.Escape)) { + event.stopPropagation(); + setOpen(false); + } + } + + // Common trigger props + const triggerProps = { + className: cx(`${prefix}--definition-term`, triggerClassName), + 'aria-controls': tooltipId, + 'aria-describedby': tooltipId, + 'aria-expanded': isOpen, + tabIndex: 0, + onBlur: () => { + setOpen(false); + }, + onMouseDown: (event) => { + // We use onMouseDown rather than onClick to make sure this triggers + // before onFocus. + if (event.button === 0) { + // Prevent default for anchor tags + if (as === 'a' || (typeof as === 'string' && as.toLowerCase() === 'a')) { + event.preventDefault(); + } + setOpen(!isOpen); + } + }, + onKeyDown, + ...rest, + }; + + // Add onClick for anchor tags + if (as === 'a' || (typeof as === 'string' && as.toLowerCase() === 'a')) { + triggerProps.onClick = (event) => { + event.preventDefault(); + setOpen(!isOpen); + }; + // Add href for anchor tags if not provided + if (!rest.href) { + triggerProps.href = '#'; + } + } + + // Add type for button + if (as === 'button' || (typeof as === 'string' && as.toLowerCase() === 'button')) { + triggerProps.type = rest.type || 'button'; + } + + // Determine the trigger element + let TriggerElement; + + if (renderTrigger) { + // Option 1: Custom render function + TriggerElement = () => renderTrigger(triggerProps, children, isOpen, setOpen); + } else if (typeof as === 'string') { + // Option 2: HTML element string ('button', 'a', 'span', etc.) + TriggerElement = () => React.createElement(as, triggerProps, children); + } else { + // Option 3: Custom React component + TriggerElement = () => React.createElement(as, triggerProps, children); + } + + return ( + { + setOpen(false); + }} + onMouseEnter={() => { + if (openOnHover) { + setOpen(true); + } + }} + onFocus={() => { + setOpen(true); + }} + open={isOpen} + > + + + {tooltipText ?? definition} + + + ); +}; + +DefinitionTooltip.propTypes = { + /** + * Specify how the trigger should align with the tooltip + */ + align: PropTypes.oneOf([ + 'top', + 'top-left', + 'top-right', + 'bottom', + 'bottom-left', + 'bottom-right', + 'left', + 'left-bottom', + 'left-top', + 'right', + 'right-bottom', + 'right-top', + 'top-start', + 'top-end', + 'bottom-start', + 'bottom-end', + 'left-end', + 'left-start', + 'right-end', + 'right-start', + ]), + + /** + * Will auto-align the popover. This prop is currently experimental and is + * subject to future changes. Requires React v17+ + */ + autoAlign: PropTypes.bool, + + /** + * The element type or custom component to render as the trigger. + * Can be 'button', 'a', 'span', or a custom React component. + */ + as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]), + + /** + * The `children` prop will be used as the value that is being defined + */ + children: PropTypes.node.isRequired, + + /** + * Specify an optional className to be applied to the container node + */ + className: PropTypes.string, + + /** + * Specify whether the tooltip should be open when it first renders + */ + defaultOpen: PropTypes.bool, + + /** + * The `definition` prop is used as the content inside of the tooltip that + * appears when a user interacts with the element rendered by the `children` + * prop + */ + definition: PropTypes.node.isRequired, + + /** + * Alternative to definition prop for tooltip content + */ + tooltipText: PropTypes.node, + + /** + * Provide a value that will be assigned as the id of the tooltip + */ + id: PropTypes.string, + + /** + * Specifies whether or not the `DefinitionTooltip` should open on hover or not + */ + openOnHover: PropTypes.bool, + + /** + * Custom render function for the trigger element. + * Receives (props, children, isOpen, setOpen) as arguments. + * If provided, this takes precedence over the `as` prop. + */ + renderTrigger: PropTypes.func, + + /** + * The CSS class name of the trigger element + */ + triggerClassName: PropTypes.string, +}; + +DefinitionTooltip.defaultProps = { + align: 'bottom', + as: 'button', + autoAlign: false, + className: undefined, + defaultOpen: false, + tooltipText: '', + id: undefined, + openOnHover: false, + renderTrigger: undefined, + triggerClassName: undefined, +}; + +export { DefinitionTooltip }; +export default DefinitionTooltip; + +// Made with Bob diff --git a/packages/react/src/components/Tooltip/_definition-tooltip.scss b/packages/react/src/components/Tooltip/_definition-tooltip.scss new file mode 100644 index 0000000000..39e475d601 --- /dev/null +++ b/packages/react/src/components/Tooltip/_definition-tooltip.scss @@ -0,0 +1,22 @@ +@use '@carbon/react/scss/components/popover' as *; +@use '@carbon/react/scss/config' as *; +@use '@carbon/react/scss/spacing' as *; +@use '@carbon/react/scss/type' as *; +@use '@carbon/react/scss/colors' as *; +@use '@carbon/react/scss/theme' as *; +@use '../../globals/vars' as *; + +// Definition Tooltip styles +.#{$prefix}--popover-container .#{$prefix}--definition-term { + margin-inline-start: 0; + text-decoration: none; + .#{$iot-prefix}--table__cell-text--truncate { + overflow: hidden; + } +} +.#{$prefix}--popover-container .#{$prefix}--definition-term > span:first-child { + padding-inline-start: 0; +} +.#{$prefix}--popover-container .#{$prefix}--definition-term > span.cds--popover-container { + padding: 0; +} diff --git a/packages/react/src/components/Tooltip/_tooltip.scss b/packages/react/src/components/Tooltip/_tooltip.scss index de235a8eec..a79dd2f5b0 100644 --- a/packages/react/src/components/Tooltip/_tooltip.scss +++ b/packages/react/src/components/Tooltip/_tooltip.scss @@ -7,6 +7,9 @@ @use '@carbon/react/scss/colors' as *; @use '../../globals/vars' as *; +// Import DefinitionTooltip styles +@use './definition-tooltip'; + .#{$iot-prefix}--tooltip { display: inline-flex; align-items: center; diff --git a/packages/react/src/components/Tooltip/index.jsx b/packages/react/src/components/Tooltip/index.jsx index 5564752f76..18e36acbea 100644 --- a/packages/react/src/components/Tooltip/index.jsx +++ b/packages/react/src/components/Tooltip/index.jsx @@ -88,4 +88,5 @@ Tooltip.propTypes = { showIcon: PropTypes.bool, }; +export { DefinitionTooltip } from './DefinitionTooltip'; export default Tooltip; diff --git a/packages/react/src/internal/keyboard.js b/packages/react/src/internal/keyboard.js new file mode 100644 index 0000000000..9989e92f09 --- /dev/null +++ b/packages/react/src/internal/keyboard.js @@ -0,0 +1,42 @@ +/** + * Copied from Carbon Design System + * Copyright IBM Corp. 2016, 2025 + * + * 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 const keys = { + Escape: 'Escape', + Enter: 'Enter', + Space: ' ', + ArrowUp: 'ArrowUp', + ArrowDown: 'ArrowDown', + ArrowLeft: 'ArrowLeft', + ArrowRight: 'ArrowRight', + Tab: 'Tab', +}; + +/** + * Check if the given event matches the provided key or keys + * @param {KeyboardEvent} event - The keyboard event to check + * @param {string|string[]} keyOrKeys - A key or array of keys to match against + * @returns {boolean} - True if the event key matches + */ +export function match(event, keyOrKeys) { + if (Array.isArray(keyOrKeys)) { + return keyOrKeys.some((key) => event.key === key); + } + return event.key === keyOrKeys; +} + +/** + * Get the character from a keyboard event + * @param {KeyboardEvent} event - The keyboard event + * @returns {string} - The character from the event + */ +export function getCharacterFor(event) { + return event.key; +} + +// Made with Bob