-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add <Avatar> to display an image along with the text #208
Changes from 11 commits
8789a56
ba7e9d1
2ab1484
c34bbfe
b8df563
b425c33
225d756
927eb1e
e7d5c89
22ba40e
a0c7eee
edf8fee
847c092
ad3cee0
880938c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import classNames from 'classnames'; | ||
|
||
import './styles/Avatar.scss'; | ||
|
||
import icBEM from './utils/icBEM'; | ||
import prefixClass from './utils/prefixClass'; | ||
|
||
const COMPONENT_NAME = prefixClass('avatar'); | ||
const ROOT_BEM = icBEM(COMPONENT_NAME); | ||
|
||
const SQUARE = 'square'; | ||
const ROUNDED = 'rounded'; | ||
const CIRCLE = 'circle'; | ||
export const AVATAR_TYPE = { SQUARE, ROUNDED, CIRCLE }; | ||
|
||
function Avatar({ | ||
className, | ||
src, | ||
alt, | ||
type, | ||
...otherProps | ||
}) { | ||
const bemClass = ROOT_BEM.modifier(type); | ||
|
||
const rootClassName = classNames(className, `${bemClass}`); | ||
|
||
return ( | ||
<div className={rootClassName} {...otherProps}> | ||
<img alt={alt} src={src} /> | ||
</div> | ||
); | ||
} | ||
|
||
Avatar.propTypes = { | ||
src: PropTypes.string.isRequired, | ||
alt: PropTypes.string.isRequired, | ||
type: PropTypes.oneOf(Object.values(AVATAR_TYPE)), | ||
}; | ||
|
||
Avatar.defaultProps = { | ||
type: SQUARE, | ||
}; | ||
|
||
export default Avatar; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import React from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import { shallow } from 'enzyme'; | ||
|
||
import Avatar from '../Avatar'; | ||
|
||
describe('<Avatar>', () => { | ||
it('renders without crashing', () => { | ||
const div = document.createElement('div'); | ||
const element = <Avatar src="LINK" alt="ALT" />; | ||
|
||
ReactDOM.render(element, div); | ||
}); | ||
|
||
it('handles type modifiers', () => { | ||
let wrapper = shallow(<Avatar src="LINK" alt="ALT" />); | ||
expect(wrapper.hasClass('gyp-avatar--square')).toBeTruthy(); | ||
|
||
wrapper = shallow(<Avatar type="square" src="LINK" alt="ALT" />); | ||
expect(wrapper.hasClass('gyp-avatar--square')).toBeTruthy(); | ||
|
||
wrapper = shallow(<Avatar type="rounded" src="LINK" alt="ALT" />); | ||
expect(wrapper.hasClass('gyp-avatar--rounded')).toBeTruthy(); | ||
|
||
wrapper = shallow(<Avatar type="circle" src="LINK" alt="ALT" />); | ||
expect(wrapper.hasClass('gyp-avatar--circle')).toBeTruthy(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
@import "./mixins"; | ||
|
||
// ------------------------------------- | ||
// Component Block | ||
// ------------------------------------- | ||
.#{$prefix}-avatar { | ||
width: 2.2rem; | ||
height: 2.2rem; | ||
overflow: hidden; | ||
margin: .2rem .45rem .2rem .25rem; | ||
background-color: $c-gray; | ||
flex-shrink: 0; | ||
|
||
// ---------------------- | ||
// <Avatar> variants | ||
// ---------------------- | ||
&--square { | ||
border-radius: 0; | ||
} | ||
|
||
&--rounded { | ||
border-radius: 6px; | ||
} | ||
|
||
&--circle { | ||
border-radius: 50%; | ||
} | ||
|
||
// ---------------------- | ||
// Inner image | ||
// ---------------------- | ||
> img { | ||
width: 100%; | ||
height: 100%; | ||
text-align: center; | ||
object-fit: cover; | ||
} | ||
} | ||
|
||
// Override the default left margin on <ListRow> | ||
.#{$prefix}-list-row > .#{$prefix}-avatar { | ||
&:first-child { | ||
margin-left: 0; | ||
} | ||
} | ||
|
||
.#{$prefix}-list-row__body > .#{$prefix}-avatar { | ||
&:first-child { | ||
margin-left: 0; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ export const TYPE_SYMBOL = Symbol('SelectOption'); | |
function SelectOption({ | ||
label, | ||
value, | ||
avatar, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing type check here? |
||
readOnly, | ||
checked, | ||
onChange, | ||
|
@@ -32,6 +33,7 @@ function SelectOption({ | |
checked={checked} | ||
disabled={readOnly} | ||
basic={label} | ||
avatar={avatar} | ||
onChange={handleCheckboxChange} | ||
{...checkboxProps} /> | ||
</ListRow> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,6 +50,22 @@ function getValueLabelMap(fromChildren = []) { | |
return resultMap; | ||
} | ||
|
||
/** | ||
* Generate a value-avatar map from all `<SelectOption>`s. | ||
* | ||
* @param {array} fromOptions | ||
* @return {Map} | ||
*/ | ||
function getValueAvatarMap(fromChildren = []) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this be merged with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice suggestion. Thanks! |
||
const resultMap = new Map(); | ||
const options = parseSelectOptions(fromChildren); | ||
|
||
options.forEach( | ||
option => resultMap.set(option.value, option.avatar) | ||
); | ||
return resultMap; | ||
} | ||
|
||
class SelectRow extends PureComponent { | ||
static propTypes = { | ||
label: PropTypes.node.isRequired, | ||
|
@@ -85,12 +101,14 @@ class SelectRow extends PureComponent { | |
state = { | ||
popoverOpen: false, | ||
valueLabelMap: getValueLabelMap(this.props.children), | ||
avatarLabelMap: getValueAvatarMap(this.props.children), | ||
cachedValues: this.props.values || this.props.defaultValues, | ||
}; | ||
|
||
componentWillReceiveProps(nextProps) { | ||
this.setState({ | ||
valueLabelMap: getValueLabelMap(nextProps.children), | ||
avatarLabelMap: getValueAvatarMap(nextProps.children), | ||
}); | ||
|
||
if (this.getIsControlled(nextProps)) { | ||
|
@@ -156,6 +174,17 @@ class SelectRow extends PureComponent { | |
.join(asideSeparator); | ||
} | ||
|
||
renderAvatar() { | ||
const { cachedValues, avatarLabelMap } = this.state; | ||
|
||
if (cachedValues.length === 0) { | ||
return null; | ||
} | ||
|
||
return cachedValues | ||
.map(value => avatarLabelMap.get(value)); | ||
} | ||
|
||
render() { | ||
const { | ||
label, | ||
|
@@ -189,6 +218,7 @@ class SelectRow extends PureComponent { | |
|
||
return ( | ||
<ListRow className={wrapperClassName} {...rowProps}> | ||
{this.renderAvatar()} | ||
<Content minified={false} disabled={disabled} {...contentProps}> | ||
<Text | ||
bold={!ineditable} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import React from 'react'; | ||
|
||
import Avatar from '@ichef/gypcrete/src/Avatar'; | ||
import FlexRow from 'utils/FlexRow'; | ||
|
||
function BasicAvatarExample() { | ||
return ( | ||
<FlexRow> | ||
<Avatar alt="Avatar of Design" src="https://api.adorable.io/avatars/285/design@ichef.tw" /> | ||
<Avatar type="square" alt="Avatar of RD" src="https://api.adorable.io/avatars/285/rd@ichef.tw" /> | ||
<Avatar type="rounded" alt="Avatar of Marketing" src="https://api.adorable.io/avatars/285/marketing@ichef.tw" /> | ||
<Avatar type="circle" alt="Avatar of Customer Service" src="https://api.adorable.io/avatars/285/customer_service@ichef.tw" /> | ||
</FlexRow> | ||
); | ||
} | ||
|
||
export default BasicAvatarExample; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { storiesOf } from '@storybook/react'; | ||
import { withInfo } from '@storybook/addon-info'; | ||
|
||
import Avatar from '@ichef/gypcrete/src/Avatar'; | ||
import getPropTables from 'utils/getPropTables'; | ||
|
||
import BasicAvatar from './BasicAvatar'; | ||
|
||
storiesOf('@ichef/gypcrete|Avatar', module) | ||
.add('basic usage', withInfo()(BasicAvatar)) | ||
// Props table | ||
.add('props', getPropTables([Avatar])); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,12 @@ | ||
import React from 'react'; | ||
|
||
import Checkbox from '@ichef/gypcrete/src/Checkbox'; | ||
import Avatar from '@ichef/gypcrete/src/Avatar'; | ||
import DebugBox from 'utils/DebugBox'; | ||
|
||
function BasicCheckboxExample() { | ||
const rdAvatar = <Avatar type="square" alt="Avatar of RD" src="https://api.adorable.io/avatars/285/rd@ichef.tw" />; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we change the email here to more generic one, maybe |
||
|
||
return ( | ||
<div> | ||
<DebugBox> | ||
|
@@ -18,6 +21,13 @@ function BasicCheckboxExample() { | |
tag="New" /> | ||
</DebugBox> | ||
|
||
<DebugBox> | ||
<Checkbox | ||
defaultChecked | ||
basic="Join pilot program" | ||
avatar={rdAvatar} /> | ||
</DebugBox> | ||
|
||
<DebugBox> | ||
<Checkbox | ||
defaultChecked | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You probably need a
&
here?