Skip to content

Commit a2de6cd

Browse files
feat(Dropdown): add support for images, icons and flags in selected values (#4003)
* updating icon shown when option selected * updated icons to show with selected item in dropdown elements. * finalize changes * fix UT Co-authored-by: reefman001 <[email protected]>
1 parent 50ec408 commit a2de6cd

File tree

7 files changed

+184
-53
lines changed

7 files changed

+184
-53
lines changed

Diff for: docs/src/examples/modules/Dropdown/Types/DropdownExampleClearableMultiple.js

+23-23
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,29 @@ import React from 'react'
22
import { Dropdown } from 'semantic-ui-react'
33

44
const countryOptions = [
5-
{ key: 'af', value: 'af', text: 'Afghanistan' },
6-
{ key: 'ax', value: 'ax', text: 'Aland Islands' },
7-
{ key: 'al', value: 'al', text: 'Albania' },
8-
{ key: 'dz', value: 'dz', text: 'Algeria' },
9-
{ key: 'as', value: 'as', text: 'American Samoa' },
10-
{ key: 'ad', value: 'ad', text: 'Andorra' },
11-
{ key: 'ao', value: 'ao', text: 'Angola' },
12-
{ key: 'ai', value: 'ai', text: 'Anguilla' },
13-
{ key: 'ag', value: 'ag', text: 'Antigua' },
14-
{ key: 'ar', value: 'ar', text: 'Argentina' },
15-
{ key: 'am', value: 'am', text: 'Armenia' },
16-
{ key: 'aw', value: 'aw', text: 'Aruba' },
17-
{ key: 'au', value: 'au', text: 'Australia' },
18-
{ key: 'at', value: 'at', text: 'Austria' },
19-
{ key: 'az', value: 'az', text: 'Azerbaijan' },
20-
{ key: 'bs', value: 'bs', text: 'Bahamas' },
21-
{ key: 'bh', value: 'bh', text: 'Bahrain' },
22-
{ key: 'bd', value: 'bd', text: 'Bangladesh' },
23-
{ key: 'bb', value: 'bb', text: 'Barbados' },
24-
{ key: 'by', value: 'by', text: 'Belarus' },
25-
{ key: 'be', value: 'be', text: 'Belgium' },
26-
{ key: 'bz', value: 'bz', text: 'Belize' },
27-
{ key: 'bj', value: 'bj', text: 'Benin' },
5+
{ key: 'af', value: 'af', flag: 'af', text: 'Afghanistan' },
6+
{ key: 'ax', value: 'ax', flag: 'ax', text: 'Aland Islands' },
7+
{ key: 'al', value: 'al', flag: 'al', text: 'Albania' },
8+
{ key: 'dz', value: 'dz', flag: 'dz', text: 'Algeria' },
9+
{ key: 'as', value: 'as', flag: 'as', text: 'American Samoa' },
10+
{ key: 'ad', value: 'ad', flag: 'ad', text: 'Andorra' },
11+
{ key: 'ao', value: 'ao', flag: 'ao', text: 'Angola' },
12+
{ key: 'ai', value: 'ai', flag: 'ai', text: 'Anguilla' },
13+
{ key: 'ag', value: 'ag', flag: 'ag', text: 'Antigua' },
14+
{ key: 'ar', value: 'ar', flag: 'ar', text: 'Argentina' },
15+
{ key: 'am', value: 'am', flag: 'am', text: 'Armenia' },
16+
{ key: 'aw', value: 'aw', flag: 'aw', text: 'Aruba' },
17+
{ key: 'au', value: 'au', flag: 'au', text: 'Australia' },
18+
{ key: 'at', value: 'at', flag: 'at', text: 'Austria' },
19+
{ key: 'az', value: 'az', flag: 'az', text: 'Azerbaijan' },
20+
{ key: 'bs', value: 'bs', flag: 'bs', text: 'Bahamas' },
21+
{ key: 'bh', value: 'bh', flag: 'bh', text: 'Bahrain' },
22+
{ key: 'bd', value: 'bd', flag: 'bd', text: 'Bangladesh' },
23+
{ key: 'bb', value: 'bb', flag: 'bb', text: 'Barbados' },
24+
{ key: 'by', value: 'by', flag: 'by', text: 'Belarus' },
25+
{ key: 'be', value: 'be', flag: 'be', text: 'Belgium' },
26+
{ key: 'bz', value: 'bz', flag: 'bz', text: 'Belize' },
27+
{ key: 'bj', value: 'bj', flag: 'bj', text: 'Benin' },
2828
]
2929

3030
const DropdownExampleClearableMultiple = () => (

Diff for: src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export DropdownHeader from './modules/Dropdown/DropdownHeader'
134134
export DropdownItem from './modules/Dropdown/DropdownItem'
135135
export DropdownMenu from './modules/Dropdown/DropdownMenu'
136136
export DropdownSearchInput from './modules/Dropdown/DropdownSearchInput'
137+
export DropdownText from './modules/Dropdown/DropdownText'
137138

138139
export Embed from './modules/Embed'
139140

Diff for: src/modules/Dropdown/Dropdown.js

+34-8
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ import {
2121
} from '../../lib'
2222
import Icon from '../../elements/Icon'
2323
import Label from '../../elements/Label'
24+
import Flag from '../../elements/Flag'
25+
import Image from '../../elements/Image'
2426
import DropdownDivider from './DropdownDivider'
2527
import DropdownItem from './DropdownItem'
2628
import DropdownHeader from './DropdownHeader'
2729
import DropdownMenu from './DropdownMenu'
2830
import DropdownSearchInput from './DropdownSearchInput'
31+
import DropdownText from './DropdownText'
2932
import getMenuOptions from './utils/getMenuOptions'
3033
import getSelectedIndex from './utils/getSelectedIndex'
3134

@@ -35,6 +38,27 @@ const getKeyOrValue = (key, value) => (_.isNil(key) ? value : key)
3538
const getKeyAndValues = (options) =>
3639
options ? options.map((option) => _.pick(option, ['key', 'value'])) : options
3740

41+
function renderItemContent(item) {
42+
const { flag, image, text } = item
43+
44+
// TODO: remove this in v2
45+
// This maintains compatibility with Shorthand API in v1 as this might be called in "Label.create()"
46+
if (React.isValidElement(text) || _.isFunction(text)) {
47+
return text
48+
}
49+
50+
return {
51+
content: (
52+
<>
53+
{Flag.create(flag)}
54+
{Image.create(image)}
55+
56+
{text}
57+
</>
58+
),
59+
}
60+
}
61+
3862
/**
3963
* A dropdown allows a user to select a value from a series of options.
4064
* @see Form
@@ -842,20 +866,21 @@ export default class Dropdown extends Component {
842866
search && searchQuery && 'filtered',
843867
)
844868
let _text = placeholder
869+
let selectedItem
845870

846871
if (text) {
847872
_text = text
848873
} else if (open && !multiple) {
849-
_text = _.get(this.getSelectedItem(selectedIndex), 'text')
874+
selectedItem = this.getSelectedItem(selectedIndex)
850875
} else if (hasValue) {
851-
_text = _.get(this.getItemByValue(value), 'text')
876+
selectedItem = this.getItemByValue(value)
852877
}
853878

854-
return (
855-
<div className={classes} role='alert' aria-live='polite' aria-atomic>
856-
{_text}
857-
</div>
858-
)
879+
return DropdownText.create(selectedItem ? renderItemContent(selectedItem) : _text, {
880+
defaultProps: {
881+
className: classes,
882+
},
883+
})
859884
}
860885

861886
renderSearchInput = () => {
@@ -1398,7 +1423,7 @@ Dropdown.defaultProps = {
13981423
minCharacters: 1,
13991424
noResultsMessage: 'No results found.',
14001425
openOnFocus: true,
1401-
renderLabel: ({ text }) => text,
1426+
renderLabel: renderItemContent,
14021427
searchInput: 'text',
14031428
selectOnBlur: true,
14041429
selectOnNavigation: true,
@@ -1412,3 +1437,4 @@ Dropdown.Header = DropdownHeader
14121437
Dropdown.Item = DropdownItem
14131438
Dropdown.Menu = DropdownMenu
14141439
Dropdown.SearchInput = DropdownSearchInput
1440+
Dropdown.Text = DropdownText

Diff for: src/modules/Dropdown/DropdownText.d.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as React from 'react'
2+
import { SemanticShorthandContent } from '../../generic'
3+
4+
export interface DropdownTextProps extends StrictDropdownTextProps {
5+
[key: string]: any
6+
}
7+
8+
export interface StrictDropdownTextProps {
9+
/** An element type to render as (string or function). */
10+
as?: any
11+
12+
/** Primary content. */
13+
children?: React.ReactNode
14+
15+
/** Additional classes. */
16+
className?: string
17+
18+
/** Shorthand for primary content. */
19+
content?: SemanticShorthandContent
20+
}
21+
22+
declare const DropdownText: React.FunctionComponent<DropdownTextProps>
23+
24+
export default DropdownText

Diff for: src/modules/Dropdown/DropdownText.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import cx from 'classnames'
2+
import PropTypes from 'prop-types'
3+
import React from 'react'
4+
5+
import {
6+
childrenUtils,
7+
createShorthandFactory,
8+
customPropTypes,
9+
getElementType,
10+
getUnhandledProps,
11+
} from '../../lib'
12+
13+
/**
14+
* A dropdown contains a selected value.
15+
*/
16+
function DropdownText(props) {
17+
const { children, className, content } = props
18+
const classes = cx('divider', className)
19+
const rest = getUnhandledProps(DropdownText, props)
20+
const ElementType = getElementType(DropdownText, props)
21+
22+
return (
23+
<ElementType aria-atomic aria-live='polite' role='alert' {...rest} className={classes}>
24+
{childrenUtils.isNil(children) ? content : children}
25+
</ElementType>
26+
)
27+
}
28+
29+
DropdownText.propTypes = {
30+
/** An element type to render as (string or function). */
31+
as: PropTypes.elementType,
32+
33+
/** Primary content. */
34+
children: PropTypes.node,
35+
36+
/** Additional classes. */
37+
className: PropTypes.string,
38+
39+
/** Shorthand for primary content. */
40+
content: customPropTypes.contentShorthand,
41+
}
42+
43+
DropdownText.create = createShorthandFactory(DropdownText, (val) => ({ content: val }))
44+
45+
export default DropdownText

Diff for: test/specs/modules/Dropdown/Dropdown-test.js

+40-22
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import React from 'react'
44

55
import * as common from 'test/specs/commonTests'
66
import { consoleUtil, domEvent, sandbox } from 'test/utils'
7+
import Label from 'src/elements/Label/Label'
78
import Dropdown from 'src/modules/Dropdown/Dropdown'
89
import DropdownDivider from 'src/modules/Dropdown/DropdownDivider'
910
import DropdownHeader from 'src/modules/Dropdown/DropdownHeader'
1011
import DropdownItem from 'src/modules/Dropdown/DropdownItem'
1112
import DropdownMenu from 'src/modules/Dropdown/DropdownMenu'
1213
import DropdownSearchInput from 'src/modules/Dropdown/DropdownSearchInput'
14+
import DropdownText from 'src/modules/Dropdown/DropdownText'
1315

1416
let attachTo
1517
let options
@@ -92,6 +94,7 @@ describe('Dropdown', () => {
9294
DropdownItem,
9395
DropdownMenu,
9496
DropdownSearchInput,
97+
DropdownText,
9598
])
9699

97100
common.implementsIconProp(Dropdown, {
@@ -290,13 +293,6 @@ describe('Dropdown', () => {
290293
wrapperMount(<Dropdown />)
291294
wrapper.find('div').at(0).should.have.prop('role', 'listbox')
292295
})
293-
it('should render an aria-live region with aria-atomic', () => {
294-
wrapperMount(<Dropdown />)
295-
wrapper
296-
.find('div')
297-
.at(1)
298-
.should.have.props({ 'aria-live': 'polite', 'aria-atomic': true, role: 'alert' })
299-
})
300296
it('should label search dropdown as a combobox', () => {
301297
wrapperMount(<Dropdown search />)
302298
wrapper.find('div').at(0).should.have.prop('role', 'combobox')
@@ -1103,13 +1099,10 @@ describe('Dropdown', () => {
11031099
const nextItem = _.sample(_.without(options, initialItem))
11041100

11051101
wrapperMount(<Dropdown options={options} selection value={initialItem.value} />)
1106-
.find('div.text')
1107-
.should.contain.text(initialItem.text)
1102+
wrapper.find(DropdownText).should.contain.text(initialItem.text)
11081103

1109-
wrapper
1110-
.setProps({ value: nextItem.value })
1111-
.find('div.text')
1112-
.should.contain.text(nextItem.text)
1104+
wrapper.setProps({ value: nextItem.value })
1105+
wrapper.find(DropdownText).should.contain.text(nextItem.text)
11131106
})
11141107

11151108
it('updates value on down arrow', () => {
@@ -1487,18 +1480,43 @@ describe('Dropdown', () => {
14871480
it('filters active options out of the list', () => {
14881481
// make all the items active, expect to see none in the list
14891482
const value = _.map(options, 'value')
1490-
wrapperShallow(
1491-
<Dropdown options={options} selection value={value} multiple />,
1492-
).should.not.have.descendants('DropdownItem')
1483+
1484+
wrapperShallow(<Dropdown options={options} selection value={value} multiple />)
1485+
wrapper.should.not.have.descendants('DropdownItem')
14931486
})
14941487
it('displays a label for active items', () => {
14951488
// select a random item, expect a label with the item's text
1496-
const activeItem = _.sample(options)
1497-
wrapperShallow(
1498-
<Dropdown options={options} selection value={[activeItem.value]} multiple />,
1499-
).should.have.descendants('Label')
1489+
const testOptions = [
1490+
{ value: 'foo', text: 'foo' },
1491+
{ value: 'bar', text: 'bar', image: 'bar.jpg' },
1492+
{ value: 'baz', text: <span className='baz'>baz</span> },
1493+
{
1494+
value: 'qux',
1495+
text: () => (
1496+
<span className='qux' key='qux'>
1497+
qux
1498+
</span>
1499+
),
1500+
},
1501+
]
1502+
1503+
consoleUtil.disableOnce()
1504+
wrapperMount(
1505+
<Dropdown
1506+
multiple
1507+
options={testOptions}
1508+
selection
1509+
value={testOptions.map((option) => option.value)}
1510+
/>,
1511+
)
1512+
1513+
expect(wrapper.find(Label).at(0).getDOMNode().innerText).to.include('foo')
1514+
1515+
expect(wrapper.find(Label).at(1).getDOMNode().innerText).to.include('bar')
1516+
wrapper.find(Label).at(1).find('Image').should.have.prop('src', 'bar.jpg')
15001517

1501-
wrapper.find('Label').should.have.prop('content', activeItem.text)
1518+
wrapper.find('span.baz').should.contain.text('baz')
1519+
wrapper.find('span.qux').should.contain.text('qux')
15021520
})
15031521
it('keeps the selection within the range of remaining options', () => {
15041522
// items are removed as they are made active
@@ -2031,7 +2049,7 @@ describe('Dropdown', () => {
20312049
<Dropdown minCharacters={3} options={options} placeholder='foo' selection search />,
20322050
)
20332051

2034-
wrapper.find('.default.text').simulate('click')
2052+
wrapper.find(DropdownText).simulate('click')
20352053

20362054
const activeElement = document.activeElement
20372055
const searchIsFocused = activeElement === document.querySelector('input.search')

Diff for: test/specs/modules/Dropdown/DropdownText-test.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react'
2+
3+
import DropdownText from 'src/modules/Dropdown/DropdownText'
4+
import * as common from 'test/specs/commonTests'
5+
6+
describe('DropdownText', () => {
7+
common.isConformant(DropdownText)
8+
common.rendersChildren(DropdownText)
9+
10+
it('aria attributes', () => {
11+
const wrapper = shallow(<DropdownText />)
12+
13+
wrapper.should.have.prop('aria-live', 'polite')
14+
wrapper.should.have.prop('aria-atomic', true)
15+
wrapper.should.have.prop('role', 'alert')
16+
})
17+
})

0 commit comments

Comments
 (0)