-
Notifications
You must be signed in to change notification settings - Fork 510
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
Feat: Percentage field color customization #692
Changes from 15 commits
8644367
40e259e
ad586e7
57e758d
f9e5be7
4882048
f8c4269
976dd7f
9bd1a4c
52964a8
d74ac13
8854965
13db993
c25f420
5afd5c7
7ca47fa
591ccb2
5069a75
531b996
aa8d086
5161c7c
8fac4b2
21443cd
9fd88b3
2c1fbca
bb73673
18aa406
e2bfef2
0bd6f63
d015144
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,92 @@ | ||||||||||||||||
import { | ||||||||||||||||
useState, | ||||||||||||||||
useEffect, | ||||||||||||||||
useRef, | ||||||||||||||||
MutableRefObject, | ||||||||||||||||
useLayoutEffect, | ||||||||||||||||
} from "react"; | ||||||||||||||||
import { Box } from "@mui/material"; | ||||||||||||||||
|
||||||||||||||||
import { fieldSx } from "@src/components/SideDrawer/utils"; | ||||||||||||||||
import { Color, ColorPicker } from "react-color-palette"; | ||||||||||||||||
import { useDebouncedCallback } from "use-debounce"; | ||||||||||||||||
|
||||||||||||||||
const useResponsiveWidth = (): [ | ||||||||||||||||
width: number, | ||||||||||||||||
setRef: MutableRefObject<HTMLElement | null> | ||||||||||||||||
] => { | ||||||||||||||||
const ref = useRef(null); | ||||||||||||||||
const [width, setWidth] = useState(0); | ||||||||||||||||
|
||||||||||||||||
useLayoutEffect(() => { | ||||||||||||||||
if (!ref || !ref.current) { | ||||||||||||||||
return; | ||||||||||||||||
} | ||||||||||||||||
const resizeObserver = new ResizeObserver((targets) => { | ||||||||||||||||
const { width: currentWidth } = targets[0].contentRect; | ||||||||||||||||
setWidth(currentWidth); | ||||||||||||||||
}); | ||||||||||||||||
|
||||||||||||||||
resizeObserver.observe(ref.current); | ||||||||||||||||
|
||||||||||||||||
return () => { | ||||||||||||||||
resizeObserver.disconnect(); | ||||||||||||||||
}; | ||||||||||||||||
}, []); | ||||||||||||||||
|
||||||||||||||||
return [width, ref]; | ||||||||||||||||
}; | ||||||||||||||||
|
||||||||||||||||
export interface IColorPickerProps { | ||||||||||||||||
value: Color; | ||||||||||||||||
handleOnChangeComplete: (color: Color) => void; | ||||||||||||||||
disabled?: boolean; | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
export default function ColorPickerInput({ | ||||||||||||||||
value, | ||||||||||||||||
handleOnChangeComplete, | ||||||||||||||||
disabled = false, | ||||||||||||||||
}: IColorPickerProps) { | ||||||||||||||||
const [localValue, setLocalValue] = useState(value); | ||||||||||||||||
const [width, setRef] = useResponsiveWidth(); | ||||||||||||||||
|
||||||||||||||||
const debouncedOnChangeComplete = useDebouncedCallback((color) => { | ||||||||||||||||
handleOnChangeComplete(color); | ||||||||||||||||
}, 100); | ||||||||||||||||
|
||||||||||||||||
useEffect(() => { | ||||||||||||||||
debouncedOnChangeComplete(localValue); | ||||||||||||||||
}, [debouncedOnChangeComplete, localValue]); | ||||||||||||||||
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. I’m not sure why this is done this way. react-color-picker already has a If you need to debounce (when writing to db), it should be handled in the parent component. If the reason for debounce is to improve performance, you can use React 18’s
Suggested change
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. The problem here is every color changes frame(100 times+ just for a little drag) appends lots of styles into head as we're generating styles regarding. I'm aware of this problem for now. This also makes hard to reuse this component I already tried it for Color field. I'm actually planning to work on another related issue after this. I've decided to think through this reusability problems during slider customization implementation. I'll research how we can use startTransition btw. Also, a similar implementation used in Color field's PopoverCell. 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.
It only re-renders this ColorPickerInput component, which is expected and necessary. This is why we use
This only happens when the user stops dragging (
This is because when Additionally, now that you’ve accepted the suggestion to pass
I’ve added another change request and when you click accept on that in GitHub, I’ll merge this PR. 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. This is obviously better! |
||||||||||||||||
|
||||||||||||||||
return ( | ||||||||||||||||
<Box | ||||||||||||||||
ref={setRef} | ||||||||||||||||
sx={[ | ||||||||||||||||
fieldSx, | ||||||||||||||||
{ | ||||||||||||||||
marginTop: "1rem", | ||||||||||||||||
padding: "1rem", | ||||||||||||||||
borderColor: "divider", | ||||||||||||||||
transitionDuration: 0, | ||||||||||||||||
"& .rcp": { | ||||||||||||||||
border: "none", | ||||||||||||||||
"& .rcp-saturation": { | ||||||||||||||||
borderRadius: "4px", | ||||||||||||||||
htuerker marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
}, | ||||||||||||||||
"& .rcp-body": { | ||||||||||||||||
boxSizing: "unset", | ||||||||||||||||
htuerker marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
}, | ||||||||||||||||
}, | ||||||||||||||||
}, | ||||||||||||||||
]} | ||||||||||||||||
> | ||||||||||||||||
<ColorPicker | ||||||||||||||||
width={width} | ||||||||||||||||
height={100} | ||||||||||||||||
color={localValue} | ||||||||||||||||
onChange={(color) => setLocalValue(color)} | ||||||||||||||||
htuerker marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
/> | ||||||||||||||||
</Box> | ||||||||||||||||
); | ||||||||||||||||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
import { useState } from "react"; | ||
|
||
import { | ||
Box, | ||
ButtonBase, | ||
Checkbox, | ||
Collapse, | ||
InputLabel, | ||
useTheme, | ||
} from "@mui/material"; | ||
import ColorPickerInput from "@src/components/ColorPickerInput"; | ||
import { ISettingsProps } from "@src/components/fields/types"; | ||
|
||
import { Color, toColor } from "react-color-palette"; | ||
import { fieldSx } from "@src/components/SideDrawer/utils"; | ||
import { ChevronDown } from "mdi-material-ui"; | ||
import { ReactElement } from "react-markdown/lib/react-markdown"; | ||
import { resultColorsScale } from "@src/utils/color"; | ||
|
||
const ColorPickerCollapse = ({ | ||
colorKey, | ||
color, | ||
active, | ||
setActive, | ||
disabled, | ||
children, | ||
}: { | ||
colorKey: string; | ||
color: Color; | ||
active: boolean; | ||
setActive: (activeKey: string | null) => void; | ||
disabled?: boolean; | ||
children?: ReactElement; | ||
}) => { | ||
const toggleCollapse = () => !disabled && setActive(active ? null : colorKey); | ||
htuerker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const toggleState = (() => { | ||
if (disabled && active) { | ||
setActive(null); | ||
return false; | ||
} | ||
return active; | ||
})(); | ||
|
||
return ( | ||
<Box sx={{ width: "100%" }}> | ||
<ButtonBase | ||
htuerker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
onClick={toggleCollapse} | ||
component={ButtonBase} | ||
focusRipple | ||
disabled={disabled} | ||
sx={[ | ||
fieldSx, | ||
{ | ||
justifyContent: "flex-start", | ||
"&&": { pl: 0.75, pr: 0.5 }, | ||
color: color.hex, | ||
transition: (theme) => | ||
theme.transitions.create("border-radius", { | ||
delay: theme.transitions.duration.standard, | ||
}), | ||
"&.Mui-disabled": { color: "text.disabled" }, | ||
}, | ||
active && { | ||
transitionDelay: "0s", | ||
transitionDuration: "0s", | ||
}, | ||
]} | ||
> | ||
<Box | ||
htuerker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sx={{ | ||
backgroundColor: color?.hex, | ||
width: 15, | ||
height: 15, | ||
mr: 1.5, | ||
boxShadow: (theme) => `0 0 0 1px ${theme.palette.divider} inset`, | ||
borderRadius: 0.5, | ||
opacity: 0.5, | ||
}} | ||
/> | ||
<div style={{ flexGrow: 1 }}>{color.hex}</div> | ||
<ChevronDown | ||
color="action" | ||
sx={{ | ||
transition: (theme) => theme.transitions.create("transform"), | ||
transform: active ? "rotate(180deg)" : "none", | ||
}} | ||
/> | ||
</ButtonBase> | ||
<Collapse in={toggleState}>{children}</Collapse> | ||
</Box> | ||
); | ||
}; | ||
|
||
const defaultColors: { [key: string]: Color } = { | ||
htuerker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
default: toColor("hex", "#FFFFFF"), | ||
htuerker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
startColor: toColor("hex", "#ED4747"), | ||
midColor: toColor("hex", "#F3C900"), | ||
endColor: toColor("hex", "#1FAD5F"), | ||
}; | ||
|
||
const colorLabels: { [key: string]: string } = { | ||
startColor: "Start Color", | ||
midColor: "Middle Color", | ||
endColor: "End Color", | ||
}; | ||
|
||
export default function Settings({ onChange, config }: ISettingsProps) { | ||
const [checkStates, setCheckStates] = useState<{ [key: string]: boolean }>({ | ||
startColor: Boolean(config.startColor), | ||
midColor: Boolean(config.midColor), | ||
endColor: Boolean(config.endColor), | ||
}); | ||
const [activePicker, setActivePicker] = useState<string | null>(null); | ||
const theme = useTheme(); | ||
|
||
const onCheckboxChange = (key: string, checked: boolean) => { | ||
if (!checked) { | ||
onChange(key)(defaultColors.default); | ||
} else { | ||
onChange(key)(defaultColors[key]); | ||
} | ||
setCheckStates({ ...checkStates, [key]: checked }); | ||
}; | ||
|
||
return ( | ||
<> | ||
{Object.keys(checkStates).map((colorKey) => { | ||
const color = config[colorKey] || defaultColors[colorKey]; | ||
return ( | ||
<Box key={colorKey} sx={{ display: "flex", flexDirection: "column" }}> | ||
{colorLabels[colorKey]} | ||
<Box | ||
sx={{ | ||
htuerker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
display: "flex", | ||
alignItems: "start", | ||
justifyContent: "center", | ||
}} | ||
> | ||
<Checkbox | ||
sx={[ | ||
fieldSx, | ||
{ width: "auto", marginRight: "0.5rem", boxShadow: "none" }, | ||
]} | ||
checked={checkStates[colorKey]} | ||
onChange={() => | ||
onCheckboxChange(colorKey, !checkStates[colorKey]) | ||
} | ||
/> | ||
<ColorPickerCollapse | ||
colorKey={colorKey} | ||
active={activePicker === colorKey} | ||
setActive={(activePicker) => setActivePicker(activePicker)} | ||
color={color} | ||
disabled={!checkStates[colorKey]} | ||
> | ||
<ColorPickerInput | ||
value={color} | ||
handleOnChangeComplete={(color) => onChange(colorKey)(color)} | ||
disabled={!checkStates[colorKey]} | ||
/> | ||
</ColorPickerCollapse> | ||
</Box> | ||
</Box> | ||
); | ||
})} | ||
<InputLabel> | ||
Preview: | ||
<Box sx={{ display: "flex", textAlign: "center" }}> | ||
{[0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1].map((value) => { | ||
const { startColor, midColor, endColor } = config; | ||
return ( | ||
<Box | ||
key={value} | ||
htuerker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sx={{ | ||
width: "100%", | ||
padding: "0.5rem 0", | ||
color: theme.palette.text.primary, | ||
backgroundColor: resultColorsScale(value, { | ||
startColor: startColor.hex, | ||
midColor: midColor.hex, | ||
endColor: endColor.hex, | ||
}).toHex(), | ||
opacity: 0.5, | ||
}} | ||
> | ||
{Math.floor(value * 100)}% | ||
</Box> | ||
); | ||
})} | ||
</Box> | ||
</InputLabel> | ||
</> | ||
); | ||
} |
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.
Pass the
onChangeComplete
prop to the<ColorPicker>
component directly. You also need to importColorPickerProps
from react-color-palette.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.
This is a nice trick I did not know it, thanks!
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.
I think it's better not to use this as onChangeComplete is optional in ColorPickerProps.
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 can use
NonNullable<ColorPickerProps['onChangeComplete']>
(TypeScript docs)