Skip to content

Commit

Permalink
feat(imagegallerymodal): new component created to select an image
Browse files Browse the repository at this point in the history
  • Loading branch information
bjornalm committed Nov 27, 2020
1 parent 595ae6e commit 71d2d08
Show file tree
Hide file tree
Showing 21 changed files with 2,651 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .storybook/__snapshots__/Welcome.story.storyshot
Original file line number Diff line number Diff line change
Expand Up @@ -2868,6 +2868,18 @@ exports[`Storybook Snapshot tests and console checks Storyshots 0/Getting Starte
IconDropdown
</div>
</div>
<div
className="bx--structured-list-row"
>
<div
className="bx--structured-list-td"
/>
<div
className="bx--structured-list-td"
>
ImageGalleryModal
</div>
</div>
<div
className="bx--structured-list-row"
>
Expand Down
14 changes: 14 additions & 0 deletions src/components/IconSwitch/_icon-switch.scss
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,17 @@ $icon-switch-size-large: $spacing-09;
.#{$iot-prefix}--content-switcher--icon {
justify-content: center;
}

html[dir='rtl'] {
.#{$iot-prefix}--icon-switch {
&--small {
padding: 0 $icon-switch-size-small/4 0 0;
}
&--default {
padding: 0 $icon-switch-size-default/4 0 0;
}
&--large {
padding: 0 $icon-switch-size-large/4 0 0;
}
}
}
200 changes: 200 additions & 0 deletions src/components/ImageGalleryModal/ImageGalleryModal.jsx
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 src/components/ImageGalleryModal/ImageGalleryModal.story.jsx
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',
};
Loading

0 comments on commit 71d2d08

Please sign in to comment.