Skip to content

Commit efc31e5

Browse files
feat(Popover): add isTabTip prop to Popover (#13283)
* feat(tabTipPopover): add tabTip to Popover * feat(tabTipPopover): add tab-tip styles, story * test(Popover): add e2e tests * style(Popover): adjust storybook styles * fix(Popover): add new prop to proptypes * chore(snapshot): update snapshots * docs(Popover): add close on esc to story * docs(Popover): fix typo --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 13d5afc commit efc31e5

File tree

8 files changed

+288
-16
lines changed

8 files changed

+288
-16
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Copyright IBM Corp. 2022
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
'use strict';
9+
10+
const { expect, test } = require('@playwright/test');
11+
const { themes } = require('../../test-utils/env');
12+
const { snapshotStory, visitStory } = require('../../test-utils/storybook');
13+
14+
test.describe('Popover', () => {
15+
themes.forEach((theme) => {
16+
test.describe(theme, () => {
17+
test('Popover - auto align @vrt', async ({ page }) => {
18+
await snapshotStory(page, {
19+
component: 'Popover',
20+
id: 'components-popover--auto-align',
21+
theme,
22+
});
23+
});
24+
25+
test('Popover - isTabTip @vrt', async ({ page }) => {
26+
await snapshotStory(page, {
27+
component: 'Popover',
28+
id: 'components-popover--tab-tip',
29+
theme,
30+
});
31+
});
32+
});
33+
});
34+
35+
test('accessibility-checker @avt', async ({ page }) => {
36+
await visitStory(page, {
37+
component: 'Popover',
38+
id: 'components-popover--auto-align',
39+
globals: {
40+
theme: 'white',
41+
},
42+
});
43+
await expect(page).toHaveNoACViolations('Popover');
44+
});
45+
});

packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5669,6 +5669,9 @@ Map {
56695669
"highContrast": Object {
56705670
"type": "bool",
56715671
},
5672+
"isTabTip": Object {
5673+
"type": "bool",
5674+
},
56725675
"open": Object {
56735676
"isRequired": true,
56745677
"type": "bool",

packages/react/src/components/Popover/Popover.stories.js

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@
66
*/
77

88
import './story.scss';
9-
import { Checkbox } from '@carbon/icons-react';
9+
import { Checkbox as CheckboxIcon } from '@carbon/icons-react';
1010
import React, { useState } from 'react';
1111
import { Popover, PopoverContent } from '../Popover';
12+
import RadioButton from '../RadioButton';
13+
import RadioButtonGroup from '../RadioButtonGroup';
14+
import { default as Checkbox } from '../Checkbox';
1215
import mdx from './Popover.mdx';
16+
import { Settings } from '@carbon/icons-react';
17+
import { keys, match } from '../../internal/keyboard';
18+
19+
const prefix = 'cds';
1320

1421
export default {
1522
title: 'Components/Popover',
@@ -59,7 +66,7 @@ const PlaygroundStory = (props) => {
5966
highContrast={highContrast}
6067
open={open}>
6168
<div className="playground-trigger">
62-
<Checkbox />
69+
<CheckboxIcon />
6370
</div>
6471
<PopoverContent className="p-3">
6572
<p className="popover-title">Available storage</p>
@@ -71,6 +78,85 @@ const PlaygroundStory = (props) => {
7178
);
7279
};
7380

81+
export const TabTip = () => {
82+
const [open, setOpen] = useState(true);
83+
const [openTwo, setOpenTwo] = useState(false);
84+
return (
85+
<div className="popover-tabtip-story" style={{ display: 'flex' }}>
86+
<Popover
87+
open={open}
88+
onKeyDown={(evt) => {
89+
if (match(evt, keys.Escape)) {
90+
setOpen(false);
91+
}
92+
}}
93+
isTabTip>
94+
<button
95+
aria-label="Settings"
96+
type="button"
97+
onClick={() => {
98+
setOpen(!open);
99+
}}>
100+
<Settings />
101+
</button>
102+
<PopoverContent className="p-3">
103+
<RadioButtonGroup
104+
style={{ alignItems: 'flex-start', flexDirection: 'column' }}
105+
legendText="Row height"
106+
name="radio-button-group"
107+
defaultSelected="small">
108+
<RadioButton labelText="Small" value="small" id="radio-small" />
109+
<RadioButton labelText="Large" value="large" id="radio-large" />
110+
</RadioButtonGroup>
111+
<hr />
112+
<fieldset className={`${prefix}--fieldset`}>
113+
<legend className={`${prefix}--label`}>Edit columns</legend>
114+
<Checkbox defaultChecked labelText="Name" id="checkbox-label-1" />
115+
<Checkbox defaultChecked labelText="Type" id="checkbox-label-2" />
116+
<Checkbox
117+
defaultChecked
118+
labelText="Location"
119+
id="checkbox-label-3"
120+
/>
121+
</fieldset>
122+
</PopoverContent>
123+
</Popover>
124+
125+
<Popover open={openTwo} isTabTip align="bottom-right">
126+
<button
127+
aria-label="Settings"
128+
type="button"
129+
onClick={() => {
130+
setOpenTwo(!openTwo);
131+
}}>
132+
<Settings />
133+
</button>
134+
<PopoverContent className="p-3">
135+
<RadioButtonGroup
136+
style={{ alignItems: 'flex-start', flexDirection: 'column' }}
137+
legendText="Row height"
138+
name="radio-button-group-2"
139+
defaultSelected="small-2">
140+
<RadioButton labelText="Small" value="small-2" id="radio-small-2" />
141+
<RadioButton labelText="Large" value="large-2" id="radio-large-2" />
142+
</RadioButtonGroup>
143+
<hr />
144+
<fieldset className={`${prefix}--fieldset`}>
145+
<legend className={`${prefix}--label`}>Edit columns</legend>
146+
<Checkbox defaultChecked labelText="Name" id="checkbox-label-8" />
147+
<Checkbox defaultChecked labelText="Type" id="checkbox-label-9" />
148+
<Checkbox
149+
defaultChecked
150+
labelText="Location"
151+
id="checkbox-label-10"
152+
/>
153+
</fieldset>
154+
</PopoverContent>
155+
</Popover>
156+
</div>
157+
);
158+
};
159+
74160
export const Playground = PlaygroundStory.bind({});
75161

76162
Playground.argTypes = {
@@ -141,7 +227,7 @@ export const AutoAlign = () => {
141227
}}>
142228
<Popover open={open} autoAlign>
143229
<div className="playground-trigger">
144-
<Checkbox
230+
<CheckboxIcon
145231
onClick={() => {
146232
setOpen(!open);
147233
}}
@@ -157,7 +243,7 @@ export const AutoAlign = () => {
157243
</div>
158244
<Popover open autoAlign>
159245
<div className="playground-trigger">
160-
<Checkbox />
246+
<CheckboxIcon />
161247
</div>
162248
<PopoverContent className="p-3">
163249
<p className="popover-title">Available storage</p>
@@ -169,7 +255,7 @@ export const AutoAlign = () => {
169255
<div style={{ position: 'absolute', top: 0, right: 0, margin: '3rem' }}>
170256
<Popover open autoAlign>
171257
<div className="playground-trigger">
172-
<Checkbox />
258+
<CheckboxIcon />
173259
</div>
174260
<PopoverContent className="p-3">
175261
<p className="popover-title">Available storage</p>
@@ -183,7 +269,7 @@ export const AutoAlign = () => {
183269
style={{ position: 'absolute', bottom: 0, right: 0, margin: '3rem' }}>
184270
<Popover open autoAlign>
185271
<div className="playground-trigger">
186-
<Checkbox />
272+
<CheckboxIcon />
187273
</div>
188274
<PopoverContent className="p-3">
189275
<p className="popover-title">Available storage</p>
@@ -196,7 +282,7 @@ export const AutoAlign = () => {
196282
<div style={{ position: 'absolute', bottom: 0, left: 0, margin: '3rem' }}>
197283
<Popover open autoAlign>
198284
<div className="playground-trigger">
199-
<Checkbox />
285+
<CheckboxIcon />
200286
</div>
201287
<PopoverContent className="p-3">
202288
<p className="popover-title">Available storage</p>

packages/react/src/components/Popover/__tests__/Popover-test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { render, screen } from '@testing-library/react';
99
import React from 'react';
1010
import { Popover, PopoverContent } from '../../Popover';
1111

12+
const prefix = 'cds';
13+
1214
describe('Popover', () => {
1315
it('should support a ref on the outermost element', () => {
1416
const ref = jest.fn();
@@ -75,5 +77,31 @@ describe('Popover', () => {
7577
);
7678
expect(container.firstChild).toHaveAttribute('id', 'test');
7779
});
80+
81+
// Tab Tip tests
82+
it('should respect isTabTip prop', () => {
83+
const { container } = render(
84+
<Popover open isTabTip>
85+
<button type="button">Settings</button>
86+
<PopoverContent>test</PopoverContent>
87+
</Popover>
88+
);
89+
expect(container.firstChild).toHaveClass(`${prefix}--popover--tab-tip`);
90+
});
91+
92+
it('should not allow other alignments than bottom-left or bottom-right when isTabTip is present', () => {
93+
const { container } = render(
94+
<Popover open isTabTip align="top-left">
95+
<button type="button">Settings</button>
96+
<PopoverContent data-testid="test">test</PopoverContent>
97+
</Popover>
98+
);
99+
expect(container.firstChild).not.toHaveClass(
100+
`${prefix}--popover--top-left`
101+
);
102+
expect(container.firstChild).toHaveClass(
103+
`${prefix}--popover--bottom-left`
104+
);
105+
});
78106
});
79107
});

packages/react/src/components/Popover/index.tsx

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ interface PopoverBaseProps {
4444
align?: PopoverAlignment;
4545

4646
/**
47-
* Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to futurue changes.
47+
* Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to future changes.
4848
*/
4949
autoAlign?: boolean;
5050

@@ -74,6 +74,11 @@ interface PopoverBaseProps {
7474
*/
7575
highContrast?: boolean;
7676

77+
/**
78+
* Render the component using the tab tip variant
79+
*/
80+
isTabTip?: boolean;
81+
7782
/**
7883
* Specify whether the component is currently open or closed
7984
*/
@@ -88,10 +93,11 @@ export type PopoverProps<T extends React.ElementType> = PolymorphicProps<
8893
const Popover = React.forwardRef(
8994
<T extends React.ElementType>(
9095
{
91-
align = 'bottom',
96+
isTabTip,
97+
align = isTabTip ? 'bottom-left' : 'bottom',
9298
as,
9399
autoAlign = false,
94-
caret = true,
100+
caret = isTabTip ? false : true,
95101
className: customClassName,
96102
children,
97103
dropShadow = true,
@@ -111,6 +117,17 @@ const Popover = React.forwardRef(
111117
};
112118
}, []);
113119

120+
if (isTabTip) {
121+
const tabTipAlignments: PopoverAlignment[] = [
122+
'bottom-left',
123+
'bottom-right',
124+
];
125+
126+
if (!tabTipAlignments.includes(align)) {
127+
align = 'bottom-left';
128+
}
129+
}
130+
114131
const ref = useMergedRefs([forwardRef, popover]);
115132
const [autoAligned, setAutoAligned] = useState(false);
116133
const [autoAlignment, setAutoAlignment] = useState(align);
@@ -121,8 +138,9 @@ const Popover = React.forwardRef(
121138
[`${prefix}--popover--drop-shadow`]: dropShadow,
122139
[`${prefix}--popover--high-contrast`]: highContrast,
123140
[`${prefix}--popover--open`]: open,
124-
[`${prefix}--popover--${autoAlignment}`]: autoAligned,
141+
[`${prefix}--popover--${autoAlignment}`]: autoAligned && !isTabTip,
125142
[`${prefix}--popover--${align}`]: !autoAligned,
143+
[`${prefix}--popover--tab-tip`]: isTabTip,
126144
},
127145
customClassName
128146
);
@@ -132,7 +150,7 @@ const Popover = React.forwardRef(
132150
return;
133151
}
134152

135-
if (!autoAlign) {
153+
if (!autoAlign || isTabTip) {
136154
setAutoAligned(false);
137155
return;
138156
}
@@ -251,13 +269,31 @@ const Popover = React.forwardRef(
251269
setAutoAligned(true);
252270
setAutoAlignment(alignment);
253271
}
254-
}, [autoAligned, align, autoAlign, prefix, open]);
272+
}, [autoAligned, align, autoAlign, prefix, open, isTabTip]);
255273

256274
const BaseComponent: React.ElementType<any> = as ?? 'span';
275+
276+
const mappedChildren = React.Children.map(children, (child) => {
277+
const item = child as any;
278+
279+
if (item?.type === 'button') {
280+
const { className } = item.props;
281+
const tabTipClasses = cx(
282+
`${prefix}--popover--tab-tip__button`,
283+
className
284+
);
285+
return React.cloneElement(item, {
286+
className: tabTipClasses,
287+
});
288+
} else {
289+
return item;
290+
}
291+
});
292+
257293
return (
258294
<PopoverContext.Provider value={value}>
259295
<BaseComponent {...rest} className={className} ref={ref}>
260-
{children}
296+
{isTabTip ? mappedChildren : children}
261297
</BaseComponent>
262298
</PopoverContext.Provider>
263299
);
@@ -299,7 +335,7 @@ Popover.propTypes = {
299335
as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]),
300336

301337
/**
302-
* Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to futurue changes.
338+
* Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to future changes.
303339
*/
304340
autoAlign: PropTypes.bool,
305341

@@ -329,6 +365,11 @@ Popover.propTypes = {
329365
*/
330366
highContrast: PropTypes.bool,
331367

368+
/**
369+
* Render the component using the tab tip variant
370+
*/
371+
isTabTip: PropTypes.bool,
372+
332373
/**
333374
* Specify whether the component is currently open or closed
334375
*/

0 commit comments

Comments
 (0)