Skip to content

Fix ProductOption variants to correctly display disabled options #68

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

Merged
merged 2 commits into from
Apr 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-storefront",
"version": "7.11.1",
"version": "7.11.2",
"description": "Build and deploy e-commerce progressive web apps (PWAs) in record time.",
"module": "./index.js",
"license": "Apache-2.0",
Expand Down
2 changes: 1 addition & 1 deletion src/menu/Menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const Menu = React.memo(props => {
})
}

// it is implortant to memoize the context, otherwise it will cause all consumers rerender
// it is important to memoize the context, otherwise it will cause all consumers to rerender
// every time Menu rerenders
const context = useMemo(
() => ({
Expand Down
1 change: 1 addition & 0 deletions src/option/ProductOption.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export default function ProductOption(props) {
label={showLabel ? value && value.text : undefined}
selected={selected}
onClick={handleClick}
disabled={get(value, 'disabled')}
/>
</div>
)
Expand Down
8 changes: 8 additions & 0 deletions src/option/ProductOptionSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default function ProductOptionSelector({
value,
onChange,
variant,
strikeThroughDisabled,
OptionComponent,
}) {
classes = useStyles({ classes })
Expand All @@ -58,6 +59,7 @@ export default function ProductOptionSelector({
imageProps={option.image}
value={option}
skeleton={skeleton != null}
strikeThroughDisabled={strikeThroughDisabled}
/>
)
})}
Expand Down Expand Up @@ -103,6 +105,11 @@ ProductOptionSelector.propTypes = {
*/
value: PropTypes.object,

/**
* If `true`, disabled options will have a line through them.
*/
strikeThroughDisabled: PropTypes.bool,

/**
* Allows you to override the default component which is used to render a product option.
*/
Expand All @@ -114,4 +121,5 @@ ProductOptionSelector.defaultProps = {
optionProps: {},
imageProps: {},
OptionComponent: ProductOption,
strikeThroughDisabled: false,
}
87 changes: 85 additions & 2 deletions src/option/SwatchProductOption.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ export const styles = theme => ({
outline: 0,
},
},
/**
* Styles applied to the button element when [`disabled`](#prop-disabled) is `true`.
*/
buttonDisabled: {
cursor: 'default',
borderColor: theme.palette.grey['A100'],
},
/**
* Styles applied to the image element.
*/
Expand Down Expand Up @@ -118,6 +125,47 @@ export const styles = theme => ({
width: 12,
},
},
/**
* Styles applied to the image element when [`disabled`](#prop-disabled) is `true`.
*/
disabled: {
opacity: 0.3,
},
/**
* Styles applied to the element used as a strikethrough when [`disabled`](#prop-disabled) and
* [`strikeThroughDisabled`](#prop-disabled) are both `true`.
*/
strikeThrough: {
height: '7px',
borderWidth: '2px 0',
borderStyle: 'solid',
borderColor: '#f2f2f2',
backgroundColor: '#666',
position: 'relative',
width: '100%',
borderRadius: 10,
},
/**
* Styles applied to the element used as a strikethrough when [`disabled`](#prop-disabled) and
* [`strikeThroughDisabled`](#prop-disabled) are both `true`, and [`size`](#prop-size) is `'default'`.
*/
defaultStrikeThrough: {
top: -24,
},
/**
* Styles applied to the element used as a strikethrough when [`disabled`](#prop-disabled) and
* [`strikeThroughDisabled`](#prop-disabled) are both `true`, and [`size`](#prop-size) is `'small'`.
*/
smallStrikeThrough: {
top: -16,
},
/**
* Styles applied to the element used as a strikethrough when [`disabled`](#prop-disabled) and
* [`strikeThroughDisabled`](#prop-disabled) are both `true`, and [`size`](#prop-size) is `'tiny'`.
*/
tinyStrikeThrough: {
top: -12,
},
})
const useStyles = makeStyles(styles, { name: 'RSFSwatchProductOption' })

Expand All @@ -137,6 +185,9 @@ export default function SwatchProductOption({
ImageComponent,
className,
buttonProps,
disabled,
strikeThroughDisabled,
strikeThroughAngle,
}) {
classes = useStyles({ classes })

Expand All @@ -158,11 +209,12 @@ export default function SwatchProductOption({
<button
{...buttonProps}
type="button"
onClick={onClick}
onClick={disabled ? Function.prototype : onClick}
className={clsx({
[className]: className != null,
[classes.button]: true,
[classes[size]]: true,
[classes.buttonDisabled]: disabled,
})}
>
<div
Expand All @@ -173,7 +225,24 @@ export default function SwatchProductOption({
>
<SelectedIcon className={classes.icon} />
</div>
<ImageComponent classes={{ image: classes.image }} fill aspectRatio={1} {...imageProps} />
<ImageComponent
className={clsx({
[classes.disabled]: disabled,
})}
classes={{ image: classes.image }}
fill
aspectRatio={1}
{...imageProps}
/>
{disabled && strikeThroughDisabled && (
<div
className={clsx({
[classes.strikeThrough]: true,
[classes[`${size}StrikeThrough`]]: disabled,
})}
style={{ transform: `rotate(${strikeThroughAngle}deg)` }}
/>
)}
</button>
{label && (
<Typography variant="caption" className={clsx({ [classes.selectedLabel]: selected })}>
Expand Down Expand Up @@ -213,6 +282,18 @@ SwatchProductOption.propTypes = {
* If `true`, this option is selected.
*/
selected: PropTypes.bool,
/**
* Set to `true` to make the option disabled.
*/
disabled: PropTypes.bool,
/**
* Set to `true` to show a slash through the item when disabled.
*/
strikeThroughDisabled: PropTypes.bool,
/**
* The angle in degrees for the disabled indicator.
*/
strikeThroughAngle: PropTypes.number,
/**
* A function to call when this option is clicked.
*/
Expand All @@ -237,4 +318,6 @@ SwatchProductOption.defaultProps = {
ImageComponent: Image,
size: 'default',
buttonProps: {},
strikeThroughDisabled: false,
strikeThroughAngle: 45,
}
60 changes: 51 additions & 9 deletions src/option/TextProductOption.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ export const styles = theme => ({
},
},
},
/**
* Styles applied to the element used as a strikethrough when [`disabled`](#prop-disabled) and
* [`strikeThroughDisabled`](#prop-disabled) are both `true`.
*/
strikeThrough: {
height: '7px',
borderWidth: '2px 0',
borderStyle: 'solid',
borderColor: '#f2f2f2',
backgroundColor: '#666',
position: 'relative',
width: '100%',
top: 'calc(-50% - 2px)',
left: -2,
borderRadius: 10,
},
})

const useStyles = makeStyles(styles, { name: 'RSFTextProductOption' })
Expand All @@ -44,6 +60,9 @@ export default function TextProductOption({
skeleton,
buttonProps,
onClick,
disabled,
strikeThroughDisabled,
strikeThroughAngle,
}) {
classes = useStyles({ classes })

Expand All @@ -52,15 +71,24 @@ export default function TextProductOption({
}

return (
<Button
{...buttonProps}
className={clsx(className, classes.root)}
variant={selected ? 'contained' : 'outlined'}
color={selected ? 'primary' : 'default'}
onClick={onClick}
>
{label}
</Button>
<>
<Button
{...buttonProps}
disabled={disabled}
className={clsx(className, classes.root)}
variant={selected ? 'contained' : 'outlined'}
color={selected ? 'primary' : 'default'}
onClick={onClick}
>
{label}
</Button>
{disabled && strikeThroughDisabled && (
<div
className={classes.strikeThrough}
style={{ transform: `rotate(${strikeThroughAngle}deg)` }}
/>
)}
</>
)
}

Expand All @@ -85,6 +113,18 @@ TextProductOption.propTypes = {
* Set to `true` to mark the option as selected.
*/
selected: PropTypes.bool,
/**
* Set to `true` to make the option disabled.
*/
disabled: PropTypes.bool,
/**
* Set to `true` to show a slash through the item when disabled.
*/
strikeThroughDisabled: PropTypes.bool,
/**
* The angle in degrees for the disabled indicator.
*/
strikeThroughAngle: PropTypes.number,
/**
* This prop is intentionally ignored so that `TextProductOption` can be used interchangeably with
* `SwatchProductOption without` displaying a warning.
Expand All @@ -102,4 +142,6 @@ TextProductOption.propTypes = {

TextProductOption.defaultProps = {
selected: false,
strikeThroughDisabled: false,
strikeThroughAngle: 30,
}
66 changes: 66 additions & 0 deletions test/option/ProductOption.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,70 @@ describe('ProductOption', () => {

expect(onClickSpy).toHaveBeenCalledWith(option)
})

it('should not call onSelectedOptionChange function if option is disabled', () => {
const option = { id: 'test', text: 'test', disabled: true }
let setter = undefined
const onClickSpy = jest.fn().mockImplementation(value => setter(value))

const Test = () => {
const [selected, setSelected] = useState(option)
setter = setSelected

return (
<ProductOption
value={option}
selectedOption={selected}
onSelectedOptionChange={value => onClickSpy(value)}
variant="swatch"
/>
)
}

wrapper = mount(<Test />)
wrapper.find('button').simulate('click')
expect(onClickSpy).not.toHaveBeenCalled()
})

it('should have a strike-through element if strikeThroughDisabled is true', () => {
const option = { id: 'test', text: 'test', disabled: true }

wrapper = mount(
<ProductOption
value={option}
selectedOption={option}
showLabel={false}
variant="swatch"
strikeThroughDisabled
/>,
)

expect(
wrapper
.find('button')
.children()
.someWhere(child => child.hasClass(/strikeThrough/)),
).toEqual(true)

// also try for TextOption:
wrapper = mount(
<ProductOption
value={option}
selectedOption={option}
showLabel
variant="text"
strikeThroughDisabled
/>,
)

expect(
wrapper
.find('div')
.children()
.first()
.find('div')
.first()
.hasClass(/strikeThrough/),
).toEqual(true)
})
})