-
Notifications
You must be signed in to change notification settings - Fork 78
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(imagegallerymodal): new component created to select an image
- Loading branch information
Showing
21 changed files
with
2,651 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
import React, { useState } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import classNames from 'classnames'; | ||
import { Grid20, List20 } from '@carbon/icons-react'; | ||
|
||
import { settings } from '../../constants/Settings'; | ||
import ComposedModal from '../ComposedModal'; | ||
import IconSwitch from '../IconSwitch/IconSwitch'; | ||
import { Search } from '../Search'; | ||
import { ContentSwitcher } from '../ContentSwitcher'; | ||
import { ComposedModalPropTypes } from '../ComposedModal/ComposedModal'; | ||
|
||
import ImageTile from './ImageTile'; | ||
|
||
const GRID = 'grid'; | ||
const LIST = 'list'; | ||
|
||
const { iotPrefix } = settings; | ||
|
||
const propTypes = { | ||
...ComposedModalPropTypes, // eslint-disable-line react/forbid-foreign-prop-types | ||
/** Classname to be added to the root node */ | ||
className: PropTypes.string, | ||
/** Array of the images that should be shown */ | ||
content: PropTypes.arrayOf( | ||
PropTypes.shape({ | ||
id: PropTypes.string.isRequired, | ||
src: PropTypes.string.isRequired, | ||
/** The alt attribute of the image element */ | ||
alt: PropTypes.string, | ||
/** Title to be shown above image. Defaults to file name from src. */ | ||
title: PropTypes.string, | ||
}) | ||
), | ||
/** The name of the view to be selected by default */ | ||
defaultView: PropTypes.oneOf([GRID, LIST]), | ||
/** The footer prop of the ComposedModalPropTypes */ | ||
footer: ComposedModalPropTypes.footer, | ||
/** The text of the grid button in the grid list toggle */ | ||
gridButtonText: PropTypes.string, | ||
/** The text with instructions showing above the search */ | ||
instructionText: PropTypes.string, | ||
/** The text of the list button in the grid list toggle */ | ||
listButtonText: PropTypes.string, | ||
modalCloseIconDescriptionText: PropTypes.string, | ||
/** The small label text of the modal */ | ||
modalLabelText: PropTypes.string, | ||
/** The large title text of the modal */ | ||
modalTitleText: PropTypes.string, | ||
/** The primary button (select) text of the modal */ | ||
modalPrimaryButtonLabelText: PropTypes.string, | ||
/** The secondary button (cancel) text of the modal */ | ||
modalSecondaryButtonLabelText: PropTypes.string, | ||
/** Callback called with selected image props when modal submit button is pressed. */ | ||
onSubmit: PropTypes.func.isRequired, | ||
/** Callback called when modal close icon or cancel button is pressed */ | ||
onClose: PropTypes.func.isRequired, | ||
/** The text for the search input placeHolder */ | ||
searchPlaceHolderText: PropTypes.string, | ||
/** The image property to be included in the search */ | ||
searchProperty: PropTypes.string, | ||
}; | ||
|
||
const defaultProps = { | ||
className: '', | ||
content: [], | ||
defaultView: GRID, | ||
footer: {}, | ||
gridButtonText: 'Grid', | ||
instructionText: 'Select the image that you want to display on this card.', | ||
listButtonText: 'List', | ||
modalLabelText: 'New image card', | ||
modalTitleText: 'Image gallery', | ||
modalPrimaryButtonLabelText: 'Select', | ||
modalSecondaryButtonLabelText: 'Cancel', | ||
modalCloseIconDescriptionText: 'Close', | ||
searchPlaceHolderText: 'Search image by file name', | ||
searchProperty: 'src', | ||
}; | ||
|
||
const ImageGalleryModal = ({ | ||
className, | ||
content, | ||
defaultView, | ||
gridButtonText, | ||
instructionText, | ||
listButtonText, | ||
modalCloseIconDescriptionText, | ||
modalLabelText, | ||
modalTitleText, | ||
modalPrimaryButtonLabelText, | ||
modalSecondaryButtonLabelText, | ||
onSubmit, | ||
onClose, | ||
searchPlaceHolderText, | ||
searchProperty, | ||
footer, | ||
...composedModalProps | ||
}) => { | ||
const [activeView, setActiveView] = useState(defaultView); | ||
const [selectedImage, setSelectedImage] = useState(); | ||
const [filteredContent, setFilteredContent] = useState(content); | ||
|
||
const toggleImageSelection = (imageProps) => { | ||
setSelectedImage((currentSelected) => { | ||
return currentSelected?.id === imageProps.id ? undefined : imageProps; | ||
}); | ||
}; | ||
|
||
const filterContent = (evt) => { | ||
const searchTerm = evt.currentTarget.value.toLowerCase(); | ||
const filtered = content.filter((imageProps) => { | ||
const text = (imageProps[searchProperty] ?? '').toLowerCase(); | ||
return text.includes(searchTerm); | ||
}); | ||
setFilteredContent(filtered); | ||
}; | ||
|
||
const baseClass = `${iotPrefix}--image-gallery-modal`; | ||
return ( | ||
<ComposedModal | ||
type="normal" | ||
className={classNames(className, baseClass)} | ||
footer={{ | ||
isPrimaryButtonDisabled: !selectedImage, | ||
primaryButtonLabel: modalPrimaryButtonLabelText, | ||
secondaryButtonLabel: modalSecondaryButtonLabelText, | ||
...footer, | ||
}} | ||
header={{ | ||
label: modalLabelText, | ||
title: modalTitleText, | ||
}} | ||
isLarge | ||
iconDescription={modalCloseIconDescriptionText} | ||
onClose={onClose} | ||
onSubmit={() => { | ||
onSubmit(selectedImage); | ||
}} | ||
{...composedModalProps}> | ||
<div className={`${baseClass}__top-section`}> | ||
<p className={`${baseClass}__instruction-text`} alt={instructionText}> | ||
{instructionText} | ||
</p> | ||
<div className={`${baseClass}__search-list-view-container`}> | ||
<Search | ||
id={`${baseClass}--search`} | ||
onChange={filterContent} | ||
labelText="" | ||
light | ||
placeHolderText={searchPlaceHolderText} | ||
/> | ||
<ContentSwitcher | ||
className={`${baseClass}__content-switcher`} | ||
onChange={(selected) => { | ||
setActiveView(selected.name); | ||
}} | ||
selectedIndex={activeView === GRID ? 0 : 1}> | ||
<IconSwitch | ||
name={GRID} | ||
size="large" | ||
text={gridButtonText} | ||
renderIcon={Grid20} | ||
index={0} | ||
/> | ||
<IconSwitch | ||
name={LIST} | ||
size="large" | ||
text={listButtonText} | ||
renderIcon={List20} | ||
index={1} | ||
/> | ||
</ContentSwitcher> | ||
</div> | ||
</div> | ||
<div className={`${baseClass}__flex-wrapper`}> | ||
<div | ||
className={classNames(`${baseClass}__scroll-panel`, { | ||
[`${baseClass}__scroll-panel--grid`]: activeView === GRID, | ||
[`${baseClass}__scroll-panel--list`]: activeView === LIST, | ||
})}> | ||
{filteredContent.map((imageProps) => ( | ||
<ImageTile | ||
isWide={activeView === LIST} | ||
key={imageProps.id} | ||
{...imageProps} | ||
toggleImageSelection={() => toggleImageSelection(imageProps)} | ||
isSelected={selectedImage?.id === imageProps.id} | ||
/> | ||
))} | ||
</div> | ||
</div> | ||
</ComposedModal> | ||
); | ||
}; | ||
|
||
ImageGalleryModal.propTypes = propTypes; | ||
ImageGalleryModal.defaultProps = defaultProps; | ||
|
||
export default ImageGalleryModal; |
113 changes: 113 additions & 0 deletions
113
src/components/ImageGalleryModal/ImageGalleryModal.story.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import React from 'react'; | ||
import { action } from '@storybook/addon-actions'; | ||
import { text, select, object } from '@storybook/addon-knobs'; | ||
|
||
import ImageGalleryModal from './ImageGalleryModal'; | ||
import assemblyline from './images/assemblyline.jpg'; | ||
import floow_plan from './images/floow_plan.png'; // eslint-disable-line camelcase | ||
import manufacturing_plant from './images/Manufacturing_plant.png'; // eslint-disable-line camelcase | ||
import extra_wide_image from './images/extra-wide-image.png'; // eslint-disable-line camelcase | ||
import robot_arm from './images/robot_arm.png'; // eslint-disable-line camelcase | ||
import tankmodal from './images/tankmodal.png'; | ||
import turbines from './images/turbines.png'; | ||
import large from './images/large.png'; | ||
import large_portrait from './images/large_portrait.png'; // eslint-disable-line camelcase | ||
|
||
const content = [ | ||
{ | ||
id: 'assemblyline', | ||
src: assemblyline, | ||
alt: 'assemblyline', | ||
title: `custom title assemblyline that is very long a and must be managed. | ||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do | ||
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim | ||
ad minim veniam.`, | ||
}, | ||
{ id: 'floow_plan', src: floow_plan, alt: 'floow plan' }, | ||
{ | ||
id: 'manufacturing_plant', | ||
src: manufacturing_plant, | ||
alt: 'manufacturing plant', | ||
}, | ||
{ id: 'robot_arm', src: robot_arm, alt: 'robot arm' }, | ||
{ id: 'tankmodal', src: tankmodal, alt: 'tankmodal' }, | ||
{ id: 'turbines', src: turbines, alt: 'turbines' }, | ||
{ id: 'extra-wide-image', src: extra_wide_image, alt: 'extra wide image' }, | ||
{ id: 'large', src: large, alt: 'large image' }, | ||
{ id: 'large_portrait', src: large_portrait, alt: 'large image portrait' }, | ||
]; | ||
|
||
export default { | ||
title: 'Watson IoT/ImageGalleryModal', | ||
|
||
parameters: { | ||
component: ImageGalleryModal, | ||
}, | ||
}; | ||
|
||
export const Basic = () => { | ||
const defaultView = select('defaultView', ['list', 'grid'], 'grid'); | ||
const editableContent = object('content', content); | ||
const regenerationKey = `${defaultView}${JSON.stringify(editableContent)}`; | ||
|
||
return ( | ||
<div> | ||
<ImageGalleryModal | ||
key={regenerationKey} // Only used for story knob demo purpose | ||
onSubmit={action('onSubmit')} | ||
onClose={action('onClose')} | ||
content={editableContent} | ||
searchProperty={select( | ||
'searchProperty', | ||
['id', 'src', 'alt', 'title'], | ||
'src' | ||
)} | ||
defaultView={defaultView} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
Basic.story = { | ||
name: 'basic', | ||
}; | ||
|
||
export const WithI18n = () => { | ||
return ( | ||
<div> | ||
<ImageGalleryModal | ||
onSubmit={action('onSubmit')} | ||
onClose={action('onClose')} | ||
content={content} | ||
gridButtonText={text('gridButtonText', 'Grid')} | ||
instructionText={text( | ||
'instructionText', | ||
'Select the image that you want to display on this card.' | ||
)} | ||
listButtonText={text('listButtonText', 'List')} | ||
modalLabelText={text('modalLabelText', 'New image card')} | ||
modalTitleText={text('modalTitleText', 'Image gallery')} | ||
modalPrimaryButtonLabelText={text( | ||
'modalPrimaryButtonLabelText', | ||
'Select' | ||
)} | ||
modalSecondaryButtonLabelText={text( | ||
'modalSecondaryButtonLabelText', | ||
'Cancel' | ||
)} | ||
modalCloseIconDescriptionText={text( | ||
'modalCloseIconDescriptionText', | ||
'Close' | ||
)} | ||
searchPlaceHolderText={text( | ||
'searchPlaceHolderText', | ||
'Search image by file name' | ||
)} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
WithI18n.story = { | ||
name: 'With i18n', | ||
}; |
Oops, something went wrong.