Skip to content

Commit 1a25539

Browse files
Merge pull request #2243 from kevinparkerson/dropdown-tooltip
Dropdown: Add tooltip menu item ability
2 parents a979ddb + 7224d43 commit 1a25539

File tree

11 files changed

+232
-18
lines changed

11 files changed

+232
-18
lines changed

components/component-docs.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10852,7 +10852,7 @@
1085210852
"name": "array"
1085310853
},
1085410854
"required": false,
10855-
"description": "An array of menu item objects. `className` and `id` object keys are applied to the `li` DOM node. `divider` key can have a value of `top` or `bottom`. `rightIcon` and `leftIcon` are not actually `Icon` components, but prop objects that get passed to an `Icon` component. The `href` key will be added to the `a` and its default click event will be prevented. Here is a sample:\n```\n[{\n className: 'custom-li-class',\n divider: 'bottom',\n label: 'A Header',\n type: 'header'\n }, {\n href: 'http://sfdc.co/',\n id: 'custom-li-id',\n label: 'Has a value',\n leftIcon: {\n name: 'settings',\n category: 'utility'\n },\n rightIcon: {\n name: 'settings',\n category: 'utility'\n },\n type: 'item',\n value: 'B0'\n }, {\n type: 'divider'\n}]\n```"
10855+
"description": "An array of menu item objects. `className` and `id` object keys are applied to the `li` DOM node. `divider` key can have a value of `top` or `bottom`. `rightIcon` and `leftIcon` are not actually `Icon` components, but prop objects that get passed to an `Icon` component. The `href` key will be added to the `a` and its default click event will be prevented. Here is a sample:\n```\n[{\n className: 'custom-li-class',\n divider: 'bottom',\n label: 'A Header',\n type: 'header'\n }, {\n href: 'http://sfdc.co/',\n id: 'custom-li-id',\n label: 'Has a value',\n leftIcon: {\n name: 'settings',\n category: 'utility'\n },\n rightIcon: {\n name: 'settings',\n category: 'utility'\n },\n type: 'item',\n value: 'B0'\n }, {\n tooltipContent: 'Displays a tooltip when hovered over with this content. The `tooltipMenuItem` prop must be set for this to work.'\n type: 'divider'\n}]\n```"
1085610856
},
1085710857
"style": {
1085810858
"type": {
@@ -10908,6 +10908,13 @@
1090810908
"required": false,
1090910909
"description": "This prop is passed onto the triggering `Button`. It creates a tooltip with the content of the `node` provided."
1091010910
},
10911+
"tooltipMenuItem": {
10912+
"type": {
10913+
"name": "node"
10914+
},
10915+
"required": false,
10916+
"description": "Accepts a `Tooltip` component to be used as the template for menu item tooltips that appear via the `tooltipContent` options object attribute. Must be present for `tooltipContent` to work"
10917+
},
1091110918
"triggerClassName": {
1091210919
"type": {
1091310920
"name": "union",

components/menu-dropdown/__docs__/__snapshots__/storybook-stories.storyshot

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4705,3 +4705,43 @@ exports[`DOM snapshots SLDSMenuDropdown Two Hovers 1`] = `
47054705
</div>
47064706
</div>
47074707
`;
4708+
4709+
exports[`DOM snapshots SLDSMenuDropdown With tooltips 1`] = `
4710+
<div
4711+
className="slds-p-around_medium slds-text-align_center"
4712+
>
4713+
<div
4714+
className="slds-dropdown-trigger slds-dropdown-trigger_click"
4715+
id="dropdown-with-tooltips"
4716+
onClick={[Function]}
4717+
onFocus={null}
4718+
onKeyDown={[Function]}
4719+
onMouseEnter={null}
4720+
onMouseLeave={null}
4721+
>
4722+
<button
4723+
aria-expanded={false}
4724+
aria-haspopup={true}
4725+
className="slds-button slds-button_icon-border-filled ignore-click-dropdown-with-tooltips"
4726+
disabled={false}
4727+
onClick={[Function]}
4728+
tabIndex="0"
4729+
type="button"
4730+
>
4731+
<svg
4732+
aria-hidden="true"
4733+
className="slds-button__icon"
4734+
>
4735+
<use
4736+
xlinkHref="/assets/icons/utility-sprite/svg/symbols.svg#down"
4737+
/>
4738+
</svg>
4739+
<span
4740+
className="slds-assistive-text"
4741+
>
4742+
More Options
4743+
</span>
4744+
</button>
4745+
</div>
4746+
</div>
4747+
`;

components/menu-dropdown/__docs__/storybook-stories.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import IconSettings from '../../icon-settings';
1010
import { MENU_DROPDOWN } from '../../../utilities/constants';
1111
import Dropdown from '../../menu-dropdown';
1212
import { DropdownNubbinPositions } from '../../menu-dropdown/menu-dropdown';
13+
import DropdownWithTooltips from '../__examples__/with-tooltips';
1314
import List from '../../utilities/menu-list';
1415
import Button from '../../button';
1516
import Trigger from '../../menu-dropdown/button-trigger';
@@ -480,4 +481,5 @@ storiesOf(MENU_DROPDOWN, module)
480481
label="Dropdown Click"
481482
options={options}
482483
/>
483-
));
484+
))
485+
.add('With tooltips', () => <DropdownWithTooltips />);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from 'react';
2+
3+
import IconSettings from '~/components/icon-settings';
4+
import Dropdown from '~/components/menu-dropdown'; // `~` is replaced with design-system-react at runtime
5+
import Tooltip from '~/components/tooltip';
6+
7+
class Example extends React.Component {
8+
static displayName = 'DropdownWithTooltipsExample';
9+
10+
render() {
11+
return (
12+
<IconSettings iconPath="/assets/icons">
13+
<Dropdown
14+
assistiveText={{ icon: 'More Options' }}
15+
iconCategory="utility"
16+
iconName="down"
17+
iconVariant="border-filled"
18+
id="dropdown-with-tooltips"
19+
options={[
20+
{ label: 'Header', type: 'header' },
21+
{ label: 'Menu Item One', value: 'A0' },
22+
{
23+
label: 'Menu Item Two',
24+
value: 'B0',
25+
tooltipContent: 'Just a friendly tooltip',
26+
},
27+
{ label: 'Menu Item Three', value: 'C0' },
28+
{ type: 'divider' },
29+
{ label: 'Menu Item Four', value: 'D0' },
30+
{ label: 'Menu Item Five', value: 'E0' },
31+
{
32+
label: 'Menu Item Six',
33+
value: 'F0',
34+
tooltipContent: 'Another friendly tooltip',
35+
},
36+
{ type: 'divider' },
37+
{ label: 'Menu Item Seven', value: 'G0' },
38+
]}
39+
tooltipMenuItem={<Tooltip />}
40+
{...this.props}
41+
/>
42+
</IconSettings>
43+
);
44+
}
45+
}
46+
47+
export default Example; // export is replaced with `ReactDOM.render(<Example />, mountNode);` at runtime

components/menu-dropdown/__tests__/dropdown.browser-test.jsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
// Import your internal dependencies (for example):
2525
import Dropdown from '../../menu-dropdown';
2626
import IconSettings from '../../icon-settings';
27+
import Tooltip from '../../tooltip';
2728
import List from '../../utilities/menu-list';
2829
import { keyObjects } from '../../../utilities/key-code';
2930

@@ -548,4 +549,37 @@ describe('SLDSMenuDropdown', function() {
548549
}, 2);
549550
});
550551
});
552+
553+
describe('Tooltips function as expected', () => {
554+
beforeEach(
555+
mountComponent(
556+
<DemoComponent
557+
options={[
558+
{ label: 'Test item A', value: 'A0' },
559+
{
560+
label: 'Test item B',
561+
value: 'B0',
562+
tooltipContent: 'Testing tooltip content',
563+
},
564+
{ label: 'Test item C', value: 'C0' },
565+
]}
566+
tooltipMenuItem={<Tooltip />}
567+
/>
568+
)
569+
);
570+
571+
afterEach(unmountComponent);
572+
573+
it('Tooltip component shows when focused on menu item.', function() {
574+
const nodes = getNodes({ wrapper: this.wrapper });
575+
nodes.trigger.simulate('focus');
576+
nodes.trigger.simulate('keyDown', keyObjects.ENTER);
577+
nodes.trigger.simulate('keyDown', keyObjects.DOWN);
578+
579+
const tooltip = this.wrapper
580+
.find('#sample-dropdown-item-1-tooltip')
581+
.hostNodes();
582+
expect(tooltip.length).to.equal(1);
583+
});
584+
});
551585
});

components/menu-dropdown/component.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"/__examples__/default-right-to-left.jsx",
2121
"/__examples__/sub-heading.jsx",
2222
"/__examples__/custom-trigger.jsx",
23-
"/__examples__/checkmark.jsx"
23+
"/__examples__/checkmark.jsx",
24+
"/__examples__/with-tooltips.jsx"
2425
],
2526
"url-slug": "menu-dropdowns"
2627
}

components/menu-dropdown/menu-dropdown.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ const propTypes = {
345345
* type: 'item',
346346
* value: 'B0'
347347
* }, {
348+
* tooltipContent: 'Displays a tooltip when hovered over with this content. The `tooltipMenuItem` prop must be set for this to work.'
348349
* type: 'divider'
349350
* }]
350351
* ```
@@ -374,6 +375,10 @@ const propTypes = {
374375
* This prop is passed onto the triggering `Button`. It creates a tooltip with the content of the `node` provided.
375376
*/
376377
tooltip: PropTypes.node,
378+
/**
379+
* Accepts a `Tooltip` component to be used as the template for menu item tooltips that appear via the `tooltipContent` options object attribute. Must be present for `tooltipContent` to work
380+
*/
381+
tooltipMenuItem: PropTypes.node,
377382
/**
378383
* CSS classes to be added to wrapping trigger `div` around the button.
379384
*/
@@ -848,6 +853,7 @@ class MenuDropdown extends React.Component {
848853
selectedIndices={
849854
this.props.multiple ? this.state.selectedIndices : undefined
850855
}
856+
tooltipMenuItem={this.props.tooltipMenuItem}
851857
triggerId={this.getId()}
852858
length={this.props.length}
853859
{...customListProps}

components/site-stories.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ const documentationSiteLiveExamples = {
180180
require('raw-loader!@salesforce/design-system-react/components/menu-dropdown/__examples__/sub-heading.jsx'),
181181
require('raw-loader!@salesforce/design-system-react/components/menu-dropdown/__examples__/custom-trigger.jsx'),
182182
require('raw-loader!@salesforce/design-system-react/components/menu-dropdown/__examples__/checkmark.jsx'),
183+
require('raw-loader!@salesforce/design-system-react/components/menu-dropdown/__examples__/with-tooltips.jsx'),
183184
],
184185
modal: [
185186
require('raw-loader!@salesforce/design-system-react/components/modal/__examples__/menu-contents.jsx'),

components/utilities/menu-list/index.jsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ class List extends React.Component {
6262
* The index of the currently selected item in the list.
6363
*/
6464
selectedIndex: PropTypes.number,
65+
/**
66+
* Accepts a `Tooltip` component to be used as the template for menu item tooltips that appear via the `tooltipContent` options object attribute
67+
*/
68+
tooltipMenuItem: PropTypes.node,
6569
/**
6670
* The id of the element which triggered this list (in a menu context).
6771
*/
@@ -76,10 +80,13 @@ class List extends React.Component {
7680

7781
render() {
7882
let lengthClassName;
83+
let list;
84+
7985
if (this.props.length) {
8086
lengthClassName = `slds-dropdown_length-${this.props.length}`;
8187
}
82-
return (
88+
89+
list = (
8390
<ul
8491
aria-labelledby={this.props.triggerId}
8592
className={classNames(
@@ -110,11 +117,54 @@ class List extends React.Component {
110117
labelRenderer={this.props.itemRenderer}
111118
onSelect={this.props.onSelect}
112119
ref={(listItem) => this.props.itemRefs(listItem, index)}
120+
tooltipTemplate={this.props.tooltipMenuItem}
113121
/>
114122
);
115123
})}
116124
</ul>
117125
);
126+
127+
if (this.props.tooltipMenuItem) {
128+
/* eslint-disable react/no-danger */
129+
list = (
130+
<React.Fragment>
131+
<style
132+
dangerouslySetInnerHTML={{
133+
__html: `.slds-dropdown__item > .slds-tooltip-trigger > a {
134+
position: relative;
135+
display: -ms-flexbox;
136+
display: flex;
137+
-ms-flex-pack: justify;
138+
justify-content: space-between;
139+
-ms-flex-align: center;
140+
align-items: center;
141+
padding: 0.5rem 0.75rem;
142+
color: #080707;
143+
white-space: nowrap;
144+
cursor: pointer;
145+
}
146+
147+
.slds-dropdown__item > .slds-tooltip-trigger > a:active {
148+
text-decoration: none;
149+
background-color: #ecebea;
150+
}
151+
152+
.slds-dropdown__item > .slds-tooltip-trigger > a:hover,
153+
.slds-dropdown__item > .slds-tooltip-trigger > a:focus {
154+
outline: 0;
155+
text-decoration: none;
156+
background-color: #f3f2f2;
157+
}
158+
`,
159+
}}
160+
/>
161+
{list}
162+
</React.Fragment>
163+
);
164+
/* eslint-enable react/no-danger */
165+
}
166+
167+
return list;
118168
}
119169
}
120170

components/utilities/menu-list/item.jsx

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ class ListItem extends React.Component {
5959
category: PropTypes.string,
6060
name: PropTypes.string,
6161
}),
62+
tooltipContent: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
63+
tooltipTemplate: PropTypes.node,
6264
type: PropTypes.string,
6365
value: PropTypes.any,
6466
};
@@ -172,6 +174,41 @@ class ListItem extends React.Component {
172174
case 'link':
173175
case 'item':
174176
default: {
177+
/* eslint-disable jsx-a11y/role-supports-aria-props */
178+
let itemContents = (
179+
<a
180+
aria-checked={this.props.checkmark && this.props.isSelected}
181+
aria-disabled={this.props['aria-disabled']}
182+
href={this.props.href}
183+
data-index={this.props.index}
184+
onClick={this.handleClick}
185+
role={this.props.checkmark ? 'menuitemcheckbox' : 'menuitem'}
186+
tabIndex="-1"
187+
>
188+
{this.getLabel()}
189+
{this.getIcon('right')}
190+
</a>
191+
);
192+
193+
if (this.props.tooltipContent && this.props.tooltipTemplate) {
194+
const {
195+
...userDefinedTooltipProps
196+
} = this.props.tooltipTemplate.props;
197+
const tooltipProps = {
198+
align: 'top',
199+
content: this.props.tooltipContent, // either use specific content defined on option or content defined on tooltip component.
200+
id: `${this.props.id}-tooltip`,
201+
position: 'absolute',
202+
triggerStyle: { width: '100%' },
203+
...userDefinedTooltipProps, // we want to allow user defined tooltip pros to overwrite default props, if need be.
204+
};
205+
itemContents = React.cloneElement(
206+
this.props.tooltipTemplate,
207+
tooltipProps,
208+
itemContents
209+
);
210+
}
211+
175212
return (
176213
/* eslint-disable jsx-a11y/role-supports-aria-props */
177214
// disabled eslint, but using aria-selected on presentation role seems suspicious...
@@ -190,19 +227,7 @@ class ListItem extends React.Component {
190227
onMouseDown={this.handleMouseDown}
191228
role="presentation"
192229
>
193-
{/* eslint-disable jsx-a11y/role-supports-aria-props */}
194-
<a
195-
aria-checked={this.props.checkmark && this.props.isSelected}
196-
aria-disabled={this.props['aria-disabled']}
197-
href={this.props.href}
198-
data-index={this.props.index}
199-
onClick={this.handleClick}
200-
role={this.props.checkmark ? 'menuitemcheckbox' : 'menuitem'}
201-
tabIndex="-1"
202-
>
203-
{this.getLabel()}
204-
{this.getIcon('right')}
205-
</a>
230+
{itemContents}
206231
</li>
207232
);
208233
}

0 commit comments

Comments
 (0)