diff --git a/app/soapbox/features/soapbox_config/components/color-picker.tsx b/app/soapbox/features/soapbox_config/components/color-picker.tsx new file mode 100644 index 000000000..8b203a901 --- /dev/null +++ b/app/soapbox/features/soapbox_config/components/color-picker.tsx @@ -0,0 +1,49 @@ +import { supportsPassiveEvents } from 'detect-passive-events'; +import React, { useEffect, useRef } from 'react'; +import { SketchPicker, ColorChangeHandler } from 'react-color'; + +import { isMobile } from 'soapbox/is_mobile'; + +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + +interface IColorPicker { + style?: React.CSSProperties, + value: string, + onChange: ColorChangeHandler, + onClose: () => void, +} + +const ColorPicker: React.FC = ({ style, value, onClose, onChange }) => { + const node = useRef(null); + + const handleDocumentClick = (e: MouseEvent | TouchEvent) => { + if (node.current && !node.current.contains(e.target as HTMLElement)) { + onClose(); + } + }; + + useEffect(() => { + document.addEventListener('click', handleDocumentClick, false); + document.addEventListener('touchend', handleDocumentClick, listenerOptions); + + return () => { + document.removeEventListener('click', handleDocumentClick, false); + document.removeEventListener('touchend', handleDocumentClick); + }; + }); + + const pickerStyle: React.CSSProperties = { + ...style, + marginLeft: isMobile(window.innerWidth) ? '20px' : '12px', + position: 'absolute', + zIndex: 1000, + }; + + return ( +
+ +
+ ); +}; + +export default ColorPicker; diff --git a/app/soapbox/features/soapbox_config/components/color-with-picker.tsx b/app/soapbox/features/soapbox_config/components/color-with-picker.tsx new file mode 100644 index 000000000..6178140e6 --- /dev/null +++ b/app/soapbox/features/soapbox_config/components/color-with-picker.tsx @@ -0,0 +1,61 @@ +import React, { useState, useRef } from 'react'; +// @ts-ignore: TODO: upgrade react-overlays. v3.1 and above have TS definitions +import Overlay from 'react-overlays/lib/Overlay'; + +import { isMobile } from 'soapbox/is_mobile'; + +import ColorPicker from './color-picker'; + +import type { ColorChangeHandler } from 'react-color'; + +interface IColorWithPicker { + buttonId: string, + label: React.ReactNode, + value: string, + onChange: ColorChangeHandler, +} + +const ColorWithPicker: React.FC = ({ buttonId, label, value, onChange }) => { + const node = useRef(null); + const [active, setActive] = useState(false); + const [placement, setPlacement] = useState(null); + + const hidePicker = () => { + setActive(false); + }; + + const showPicker = () => { + setActive(true); + setPlacement(isMobile(window.innerWidth) ? 'bottom' : 'right'); + }; + + const onToggle: React.MouseEventHandler = () => { + if (active) { + hidePicker(); + } else { + showPicker(); + } + }; + + return ( +
+ + + + ); +}; + +export default ColorWithPicker; diff --git a/app/soapbox/features/soapbox_config/components/icon-picker.tsx b/app/soapbox/features/soapbox_config/components/icon-picker.tsx new file mode 100644 index 000000000..bb0c18eff --- /dev/null +++ b/app/soapbox/features/soapbox_config/components/icon-picker.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import IconPickerDropdown from './icon_picker_dropdown'; + +interface IIconPicker { + label: React.ReactNode, + value: string, + onChange: React.ChangeEventHandler, +} + +const IconPicker: React.FC = ({ onChange, value, label }) => { + return ( +
+
+ {label && ()} +
+ +
+
+
+ ); +}; + +export default IconPicker; diff --git a/app/soapbox/features/soapbox_config/index.js b/app/soapbox/features/soapbox_config/index.js index 44317a1b7..b6fe7f17f 100644 --- a/app/soapbox/features/soapbox_config/index.js +++ b/app/soapbox/features/soapbox_config/index.js @@ -1,12 +1,9 @@ -import { supportsPassiveEvents } from 'detect-passive-events'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import PropTypes from 'prop-types'; import React from 'react'; -import { SketchPicker } from 'react-color'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import Overlay from 'react-overlays/lib/Overlay'; import { connect } from 'react-redux'; import { updateConfig } from 'soapbox/actions/admin'; @@ -24,12 +21,12 @@ import { Checkbox, } from 'soapbox/features/forms'; import ThemeToggle from 'soapbox/features/ui/components/theme-toggle'; -import { isMobile } from 'soapbox/is_mobile'; import { normalizeSoapboxConfig } from 'soapbox/normalizers'; import Accordion from '../ui/components/accordion'; -import IconPickerDropdown from './components/icon_picker_dropdown'; +import ColorWithPicker from './components/color-with-picker'; +import IconPicker from './components/icon-picker'; import SitePreview from './components/site_preview'; const messages = defineMessages({ @@ -60,8 +57,6 @@ const messages = defineMessages({ singleUserModeProfileHint: { id: 'soapbox_config.single_user_mode_profile_hint', defaultMessage: '@handle' }, }); -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; - const templates = { promoPanelItem: ImmutableMap({ icon: '', text: '', url: '' }), footerItem: ImmutableMap({ title: '', url: '' }), @@ -461,121 +456,3 @@ class SoapboxConfig extends ImmutablePureComponent { } } - -class ColorPicker extends React.PureComponent { - - static propTypes = { - style: PropTypes.object, - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - onClose: PropTypes.func, - } - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - } - - componentDidMount() { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - } - - render() { - const { style, value, onChange } = this.props; - const margin_left_picker = isMobile(window.innerWidth) ? '20px' : '12px'; - - return ( -
- -
- ); - } - -} - -class ColorWithPicker extends ImmutablePureComponent { - - static propTypes = { - buttonId: PropTypes.string.isRequired, - label: PropTypes.node, - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - } - - onToggle = (e) => { - if (!e.key || e.key === 'Enter') { - if (this.state.active) { - this.onHidePicker(); - } else { - this.onShowPicker(e); - } - } - } - - state = { - active: false, - placement: null, - } - - onHidePicker = () => { - this.setState({ active: false }); - } - - onShowPicker = ({ target }) => { - this.setState({ active: true }); - this.setState({ placement: isMobile(window.innerWidth) ? 'bottom' : 'right' }); - } - - render() { - const { buttonId, label, value, onChange } = this.props; - const { active, placement } = this.state; - - return ( -
- - - ); - } - -} - -export class IconPicker extends ImmutablePureComponent { - - static propTypes = { - icons: PropTypes.object, - label: PropTypes.node, - value: PropTypes.string, - onChange: PropTypes.func.isRequired, - } - - render() { - const { onChange, value, label } = this.props; - - return ( -
-
- {label && ()} -
- -
-
-
- ); - } - -} diff --git a/package.json b/package.json index 5eed7b932..5d19be83b 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@types/object-assign": "^4.0.30", "@types/object-fit-images": "^3.2.3", "@types/qrcode.react": "^1.0.2", + "@types/react-color": "^3.0.6", "@types/react-datepicker": "^4.4.0", "@types/react-helmet": "^6.1.5", "@types/react-motion": "^0.0.32", diff --git a/yarn.lock b/yarn.lock index 39d12627f..9445a64a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2184,6 +2184,14 @@ dependencies: "@types/react" "*" +"@types/react-color@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.6.tgz#602fed023802b2424e7cd6ff3594ccd3d5055f9a" + integrity sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w== + dependencies: + "@types/react" "*" + "@types/reactcss" "*" + "@types/react-datepicker@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-4.4.0.tgz#0072e18536ad305fd57786f9b6f9e499eed2b475" @@ -2272,6 +2280,13 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/reactcss@*": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.6.tgz#133c1e7e896f2726370d1d5a26bf06a30a038bcc" + integrity sha512-qaIzpCuXNWomGR1Xq8SCFTtF4v8V27Y6f+b9+bzHiv087MylI/nTCqqdChNeWS7tslgROmYB7yeiruWX7WnqNg== + dependencies: + "@types/react" "*" + "@types/redux-mock-store@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@types/redux-mock-store/-/redux-mock-store-1.0.3.tgz#895de4a364bc4836661570aec82f2eef5989d1fb"