diff --git a/e2e/components/Popover/Popover-test.e2e.js b/e2e/components/Popover/Popover-test.e2e.js new file mode 100644 index 000000000000..fcedb5ed4a8a --- /dev/null +++ b/e2e/components/Popover/Popover-test.e2e.js @@ -0,0 +1,45 @@ +/** + * 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 strict'; + +const { expect, test } = require('@playwright/test'); +const { themes } = require('../../test-utils/env'); +const { snapshotStory, visitStory } = require('../../test-utils/storybook'); + +test.describe('Popover', () => { + themes.forEach((theme) => { + test.describe(theme, () => { + test('Popover - auto align @vrt', async ({ page }) => { + await snapshotStory(page, { + component: 'Popover', + id: 'components-popover--auto-align', + theme, + }); + }); + + test('Popover - isTabTip @vrt', async ({ page }) => { + await snapshotStory(page, { + component: 'Popover', + id: 'components-popover--tab-tip', + theme, + }); + }); + }); + }); + + test('accessibility-checker @avt', async ({ page }) => { + await visitStory(page, { + component: 'Popover', + id: 'components-popover--auto-align', + globals: { + theme: 'white', + }, + }); + await expect(page).toHaveNoACViolations('Popover'); + }); +}); diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 4f969ad71b21..a72fca38f3fe 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -5669,6 +5669,9 @@ Map { "highContrast": Object { "type": "bool", }, + "isTabTip": Object { + "type": "bool", + }, "open": Object { "isRequired": true, "type": "bool", diff --git a/packages/react/src/components/Popover/Popover.stories.js b/packages/react/src/components/Popover/Popover.stories.js index 46ba2e9b402a..735fa35c9678 100644 --- a/packages/react/src/components/Popover/Popover.stories.js +++ b/packages/react/src/components/Popover/Popover.stories.js @@ -6,10 +6,17 @@ */ import './story.scss'; -import { Checkbox } from '@carbon/icons-react'; +import { Checkbox as CheckboxIcon } from '@carbon/icons-react'; import React, { useState } from 'react'; import { Popover, PopoverContent } from '../Popover'; +import RadioButton from '../RadioButton'; +import RadioButtonGroup from '../RadioButtonGroup'; +import { default as Checkbox } from '../Checkbox'; import mdx from './Popover.mdx'; +import { Settings } from '@carbon/icons-react'; +import { keys, match } from '../../internal/keyboard'; + +const prefix = 'cds'; export default { title: 'Components/Popover', @@ -59,7 +66,7 @@ const PlaygroundStory = (props) => { highContrast={highContrast} open={open}>
- +

Available storage

@@ -71,6 +78,85 @@ const PlaygroundStory = (props) => { ); }; +export const TabTip = () => { + const [open, setOpen] = useState(true); + const [openTwo, setOpenTwo] = useState(false); + return ( +
+ { + if (match(evt, keys.Escape)) { + setOpen(false); + } + }} + isTabTip> + + + + + + +
+
+ Edit columns + + + +
+
+
+ + + + + + + + +
+
+ Edit columns + + + +
+
+
+
+ ); +}; + export const Playground = PlaygroundStory.bind({}); Playground.argTypes = { @@ -141,7 +227,7 @@ export const AutoAlign = () => { }}>
- { setOpen(!open); }} @@ -157,7 +243,7 @@ export const AutoAlign = () => {
- +

Available storage

@@ -169,7 +255,7 @@ export const AutoAlign = () => {
- +

Available storage

@@ -183,7 +269,7 @@ export const AutoAlign = () => { style={{ position: 'absolute', bottom: 0, right: 0, margin: '3rem' }}>
- +

Available storage

@@ -196,7 +282,7 @@ export const AutoAlign = () => {
- +

Available storage

diff --git a/packages/react/src/components/Popover/__tests__/Popover-test.js b/packages/react/src/components/Popover/__tests__/Popover-test.js index e93fa35adda7..1c616d0e3323 100644 --- a/packages/react/src/components/Popover/__tests__/Popover-test.js +++ b/packages/react/src/components/Popover/__tests__/Popover-test.js @@ -9,6 +9,8 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { Popover, PopoverContent } from '../../Popover'; +const prefix = 'cds'; + describe('Popover', () => { it('should support a ref on the outermost element', () => { const ref = jest.fn(); @@ -75,5 +77,31 @@ describe('Popover', () => { ); expect(container.firstChild).toHaveAttribute('id', 'test'); }); + + // Tab Tip tests + it('should respect isTabTip prop', () => { + const { container } = render( + + + test + + ); + expect(container.firstChild).toHaveClass(`${prefix}--popover--tab-tip`); + }); + + it('should not allow other alignments than bottom-left or bottom-right when isTabTip is present', () => { + const { container } = render( + + + test + + ); + expect(container.firstChild).not.toHaveClass( + `${prefix}--popover--top-left` + ); + expect(container.firstChild).toHaveClass( + `${prefix}--popover--bottom-left` + ); + }); }); }); diff --git a/packages/react/src/components/Popover/index.tsx b/packages/react/src/components/Popover/index.tsx index c71acef12f24..3bccc34eead7 100644 --- a/packages/react/src/components/Popover/index.tsx +++ b/packages/react/src/components/Popover/index.tsx @@ -44,7 +44,7 @@ interface PopoverBaseProps { align?: PopoverAlignment; /** - * Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to futurue changes. + * Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to future changes. */ autoAlign?: boolean; @@ -74,6 +74,11 @@ interface PopoverBaseProps { */ highContrast?: boolean; + /** + * Render the component using the tab tip variant + */ + isTabTip?: boolean; + /** * Specify whether the component is currently open or closed */ @@ -88,10 +93,11 @@ export type PopoverProps = PolymorphicProps< const Popover = React.forwardRef( ( { - align = 'bottom', + isTabTip, + align = isTabTip ? 'bottom-left' : 'bottom', as, autoAlign = false, - caret = true, + caret = isTabTip ? false : true, className: customClassName, children, dropShadow = true, @@ -111,6 +117,17 @@ const Popover = React.forwardRef( }; }, []); + if (isTabTip) { + const tabTipAlignments: PopoverAlignment[] = [ + 'bottom-left', + 'bottom-right', + ]; + + if (!tabTipAlignments.includes(align)) { + align = 'bottom-left'; + } + } + const ref = useMergedRefs([forwardRef, popover]); const [autoAligned, setAutoAligned] = useState(false); const [autoAlignment, setAutoAlignment] = useState(align); @@ -121,8 +138,9 @@ const Popover = React.forwardRef( [`${prefix}--popover--drop-shadow`]: dropShadow, [`${prefix}--popover--high-contrast`]: highContrast, [`${prefix}--popover--open`]: open, - [`${prefix}--popover--${autoAlignment}`]: autoAligned, + [`${prefix}--popover--${autoAlignment}`]: autoAligned && !isTabTip, [`${prefix}--popover--${align}`]: !autoAligned, + [`${prefix}--popover--tab-tip`]: isTabTip, }, customClassName ); @@ -132,7 +150,7 @@ const Popover = React.forwardRef( return; } - if (!autoAlign) { + if (!autoAlign || isTabTip) { setAutoAligned(false); return; } @@ -251,13 +269,31 @@ const Popover = React.forwardRef( setAutoAligned(true); setAutoAlignment(alignment); } - }, [autoAligned, align, autoAlign, prefix, open]); + }, [autoAligned, align, autoAlign, prefix, open, isTabTip]); const BaseComponent: React.ElementType = as ?? 'span'; + + const mappedChildren = React.Children.map(children, (child) => { + const item = child as any; + + if (item?.type === 'button') { + const { className } = item.props; + const tabTipClasses = cx( + `${prefix}--popover--tab-tip__button`, + className + ); + return React.cloneElement(item, { + className: tabTipClasses, + }); + } else { + return item; + } + }); + return ( - {children} + {isTabTip ? mappedChildren : children} ); @@ -299,7 +335,7 @@ Popover.propTypes = { as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]), /** - * Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to futurue changes. + * Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to future changes. */ autoAlign: PropTypes.bool, @@ -329,6 +365,11 @@ Popover.propTypes = { */ highContrast: PropTypes.bool, + /** + * Render the component using the tab tip variant + */ + isTabTip: PropTypes.bool, + /** * Specify whether the component is currently open or closed */ diff --git a/packages/react/src/components/Popover/story.scss b/packages/react/src/components/Popover/story.scss index 08aea270b5f4..83432e1f0076 100644 --- a/packages/react/src/components/Popover/story.scss +++ b/packages/react/src/components/Popover/story.scss @@ -98,3 +98,22 @@ display: flex; flex-direction: column; } + +.popover-tabtip-story .cds--popover-content { + width: 16rem; +} + +.popover-tabtip-story .cds--radio-button-wrapper { + margin-bottom: 0.5rem; +} + +.popover-tabtip-story hr { + border: none; + background: theme.$border-subtle; + height: 1px; + margin: 8px 0 16px; +} + +.popover-tabtip-story .cds--popover-container:last-of-type { + margin-left: 15rem; +} diff --git a/packages/react/src/components/RadioButtonGroup/RadioButtonGroup.js b/packages/react/src/components/RadioButtonGroup/RadioButtonGroup.js index b65419125200..0c0172461ba9 100644 --- a/packages/react/src/components/RadioButtonGroup/RadioButtonGroup.js +++ b/packages/react/src/components/RadioButtonGroup/RadioButtonGroup.js @@ -26,6 +26,7 @@ const RadioButtonGroup = React.forwardRef(function RadioButtonGroup( orientation = 'horizontal', readOnly, valueSelected, + ...rest }, ref ) { @@ -88,7 +89,8 @@ const RadioButtonGroup = React.forwardRef(function RadioButtonGroup(
+ aria-readonly={readOnly} + {...rest}> {legendText && ( {legendText} )} diff --git a/packages/styles/scss/components/popover/_popover.scss b/packages/styles/scss/components/popover/_popover.scss index 11cb48ebfede..e88e0c1c97b6 100644 --- a/packages/styles/scss/components/popover/_popover.scss +++ b/packages/styles/scss/components/popover/_popover.scss @@ -8,6 +8,7 @@ @use '../../config' as *; @use '../../theme'; @use '../../utilities/box-shadow' as *; +@use '../../utilities/button-reset'; @use '../../utilities/custom-property'; @use '../../utilities/high-contrast-mode' as *; @use '../../utilities/focus-outline' as *; @@ -370,4 +371,51 @@ $popover-caret-height: custom-property.get-var( clip-path: polygon(0% 0%, 100% 50%, 0% 100%); transform: translate(calc(-1 * $popover-offset + 100%), -50%); } + + //----------------------------------------------------------------------------- + // Tab Tip Variant + //----------------------------------------------------------------------------- + .#{$prefix}--popover--tab-tip .#{$prefix}--popover-content { + border-radius: 0; + } + + .#{$prefix}--popover--tab-tip__button { + @include button-reset.reset; + + position: relative; + display: inline-flex; + width: rem(32px); + height: rem(32px); + align-items: center; + justify-content: center; + + &:focus { + @include focus-outline('outline'); + } + + &:hover { + background-color: theme.$layer-hover; + } + } + + .#{$prefix}--popover--tab-tip.#{$prefix}--popover--open + .#{$prefix}--popover--tab-tip__button { + background: theme.$layer; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); + } + + .#{$prefix}--popover--tab-tip.#{$prefix}--popover--open + .#{$prefix}--popover--tab-tip__button:not(:focus)::after { + position: absolute; + z-index: z('floating') + 1; + bottom: 0; + width: 100%; + height: 2px; + background: theme.$layer; + content: ''; + } + + .#{$prefix}--popover--tab-tip__button svg { + fill: theme.$icon-primary; + } }