From ef016b0ef5ac5288adc808bbd7ee09e8d8fd0ee0 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sat, 14 May 2022 21:57:25 +0200 Subject: [PATCH] client: Do not use spectrum for colorpicker It is unmaintained, depends on jQuery and Parcel no longer wants to parse its CSS (asterisk hack before display property for IE 7 support). We need to use popper/floating-ui to correctly position the popup since the sidebar it is in has `position: fixed` and `overflow: hidden`. --- NEWS.md | 1 + README.md | 1 - assets/js/helpers/color.js | 20 ++++ assets/js/icons.js | 2 + assets/js/index.js | 1 - assets/js/templates/ColorChooser.jsx | 130 ++++++++++++++++++---- assets/js/templates/NavTags.jsx | 2 +- assets/locale/cs.json | 1 + assets/locale/en.json | 1 + assets/package-lock.json | 79 +++++++++++-- assets/package.json | 2 +- assets/styles/color-chooser.scss | 48 ++++++++ assets/styles/main.scss | 8 +- assets/styles/mixins/visually-hidden.scss | 18 +++ 14 files changed, 276 insertions(+), 38 deletions(-) create mode 100644 assets/js/helpers/color.js create mode 100644 assets/styles/color-chooser.scss create mode 100644 assets/styles/mixins/visually-hidden.scss diff --git a/NEWS.md b/NEWS.md index 5190bf50e5..b8ca7ccb89 100644 --- a/NEWS.md +++ b/NEWS.md @@ -20,6 +20,7 @@ - YouTube spout now supports following playlists. ([#1260](https://github.com/fossar/selfoss/pull/1260)) - Confirmation is now required when leaving the setting page with unsaved source changes. ([#1300](https://github.com/fossar/selfoss/pull/1300)) - Add link from settings page to individual sources. ([#1329](https://github.com/fossar/selfoss/pull/1329)) +- Tag colour can be now changed using keyboard. ([#1335](https://github.com/fossar/selfoss/pull/1335)) - Translations into several new languages were added: - English (United Kingdom): `en-GB` - French (Canada): `fr-CA` diff --git a/README.md b/README.md index cc466a9fac..d1221e00a2 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,6 @@ Special thanks to the great programmers of these libraries used by selfoss: * [PHP Universal Feed Generator](https://github.com/ajaxray/FeedWriter) * [Elphin IcoFileLoader](https://github.com/lordelph/icofileloader) * [jQuery hotkeys](https://github.com/tzuryby/jquery.hotkeys) -* [Spectrum Colorpicker](https://github.com/bgrins/spectrum) * [Graby](https://github.com/j0k3r/graby) * [FullTextRSS filters](http://help.fivefilters.org/customer/portal/articles/223153-site-patterns) diff --git a/assets/js/helpers/color.js b/assets/js/helpers/color.js new file mode 100644 index 0000000000..a52db53f8c --- /dev/null +++ b/assets/js/helpers/color.js @@ -0,0 +1,20 @@ +/** + * Get dark OR bright color depending the color contrast. + * + * @param string hexColor color (hex) value + * @param string darkColor dark color value + * @param string brightColor bright color value + * + * @return string dark OR bright color value + * + * @see https://24ways.org/2010/calculating-color-contrast/ + */ +export function colorByBrightness(hexColor, darkColor = '#555', brightColor = '#EEE') { + // Strip hash sign. + const color = hexColor.substr(1); + const r = parseInt(color.substr(0, 2), 16); + const g = parseInt(color.substr(2, 2), 16); + const b = parseInt(color.substr(4, 2), 16); + const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000; + return yiq >= 128 ? darkColor : brightColor; +} diff --git a/assets/js/icons.js b/assets/js/icons.js index 6ea266f4e2..58f696116e 100644 --- a/assets/js/icons.js +++ b/assets/js/icons.js @@ -11,6 +11,7 @@ import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons/faS import { faTimesCircle } from '@fortawesome/free-regular-svg-icons/faTimesCircle'; import { faArrowAltCircleDown } from '@fortawesome/free-solid-svg-icons/faArrowAltCircleDown'; import { faArrowRight } from '@fortawesome/free-solid-svg-icons/faArrowRight'; +import { faCheck } from '@fortawesome/free-solid-svg-icons/faCheck'; import { faCheckCircle } from '@fortawesome/free-solid-svg-icons/faCheckCircle'; import { faCaretDown } from '@fortawesome/free-solid-svg-icons/faCaretDown'; import { faCaretRight } from '@fortawesome/free-solid-svg-icons/faCaretRight'; @@ -54,6 +55,7 @@ export { faArrowRight as next, faCaretDown as arrowExpanded, faCaretRight as arrowCollapsed, + faCheck as check, faCheckCircle as markRead, faCloudUploadAlt as settings, faCog as menu, diff --git a/assets/js/index.js b/assets/js/index.js index 45ce375cbb..96c03021b9 100644 --- a/assets/js/index.js +++ b/assets/js/index.js @@ -1,6 +1,5 @@ import 'regenerator-runtime/runtime'; import './jquery'; -import 'spectrum-colorpicker'; import selfoss from './selfoss-base'; import './selfoss-shares'; import './selfoss-db-online'; diff --git a/assets/js/templates/ColorChooser.jsx b/assets/js/templates/ColorChooser.jsx index 213bb5b162..084440b940 100644 --- a/assets/js/templates/ColorChooser.jsx +++ b/assets/js/templates/ColorChooser.jsx @@ -1,33 +1,119 @@ -import React from 'react'; +import React, { useCallback, useContext, useMemo } from 'react'; +import { useFloating, autoUpdate, flip, offset, shift } from '@floating-ui/react-dom'; import PropTypes from 'prop-types'; +import { Button as MenuButton, Wrapper as MenuWrapper, Menu, MenuItem } from 'react-aria-menubutton'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { colorByBrightness } from '../helpers/color'; +import { LocalizationContext } from '../helpers/i18n'; +import * as icons from '../icons'; + +const palette = ['#ffccc9', '#ffce93', '#fffc9e', '#ffffc7', '#9aff99', '#96fffb', '#cdffff', '#cbcefb', '#fffe65', '#cfcfcf', '#fd6864', '#fe996b', '#fcff2f', '#67fd9a', '#38fff8', '#68fdff', '#9698ed', '#c0c0c0', '#fe0000', '#f8a102', '#ffcc67', '#f8ff00', '#34ff34', '#68cbd0', '#34cdf9', '#6665cd', '#9b9b9b', '#cb0000', '#f56b00', '#ffcb2f', '#ffc702', '#32cb00', '#00d2cb', '#3166ff', '#6434fc', '#656565', '#9a0000', '#ce6301', '#cd9934', '#999903', '#009901', '#329a9d', '#3531ff', '#6200c9', '#343434', '#680100', '#963400', '#986536', '#646809', '#036400', '#34696d', '#00009b', '#303498', '#000000', '#330001', '#643403', '#663234', '#343300', '#013300', '#003532', '#010066', '#340096']; + +function ColorButton({tag, color}) { + const style = useMemo( + () => ({ + backgroundColor: color, + color: colorByBrightness(color), + }), + [color] + ); + + const selected = color === tag.color; + + return ( + + {selected + ? + : ' ' + } + + ); +} + +ColorButton.propTypes = { + tag: PropTypes.object.isRequired, + color: PropTypes.string.isRequired, +}; + export default function ColorChooser({tag, onChange}) { - const colorChooser = React.useRef(null); - - React.useLayoutEffect(() => { - // init colorpicker - const picker = colorChooser.current; - $(picker).spectrum({ - showPaletteOnly: true, - color: tag.color, - palette: [ - ['#ffccc9', '#ffce93', '#fffc9e', '#ffffc7', '#9aff99', '#96fffb', '#cdffff', '#cbcefb', '#fffe65', '#cfcfcf', '#fd6864', '#fe996b', '#fcff2f', '#67fd9a', '#38fff8', '#68fdff', '#9698ed', '#c0c0c0', '#fe0000', '#f8a102', '#ffcc67', '#f8ff00', '#34ff34', '#68cbd0', '#34cdf9', '#6665cd', '#9b9b9b', '#cb0000', '#f56b00', '#ffcb2f', '#ffc702', '#32cb00', '#00d2cb', '#3166ff', '#6434fc', '#656565', '#9a0000', '#ce6301', '#cd9934', '#999903', '#009901', '#329a9d', '#3531ff', '#6200c9', '#343434', '#680100', '#963400', '#986536', '#646809', '#036400', '#34696d', '#00009b', '#303498', '#000000', '#330001', '#643403', '#663234', '#343300', '#013300', '#003532', '#010066', '#340096'] - ], - change: onChange, - }); - - return () => { - $(picker).spectrum('destroy'); - }; - }, [onChange, tag.color]); - - const style = React.useMemo( + const style = useMemo( () => ({ backgroundColor: tag.color }), [tag.color] ); + const preventDefault = useCallback( + (event) => { + event.preventDefault(); + // Prevent closing navigation on mobile. + event.stopPropagation(); + }, + ); + + const { + x: menuX, + y: menuY, + reference: buttonRef, + floating: floatingRef, + strategy: positionStrategy, + } = useFloating({ + placement: 'right-start', + strategy: 'fixed', + middleware: [ + offset({ mainAxis: 16 }), + shift(), + flip(), + ], + whileElementsMounted: autoUpdate, + }); + + const _ = useContext(LocalizationContext); + return ( - + + + + {_('tag_change_color_button_title')} + + + + {palette.map((color) => ( + + ))} + {!palette.includes(tag.color) && ( + + )} + + ); } diff --git a/assets/js/templates/NavTags.jsx b/assets/js/templates/NavTags.jsx index e538c02d31..e24f7ee36b 100644 --- a/assets/js/templates/NavTags.jsx +++ b/assets/js/templates/NavTags.jsx @@ -20,7 +20,7 @@ function Tag({ tag, active, collapseNav }) { (color) => { updateTag( tagName, - color.toHexString() + color ).then(() => { selfoss.entriesPage?.reload(); }).catch((error) => { diff --git a/assets/locale/cs.json b/assets/locale/cs.json index ab20057fdf..b985ac13a5 100644 --- a/assets/locale/cs.json +++ b/assets/locale/cs.json @@ -50,6 +50,7 @@ "lang_source_last_post": "Poslední příspěvek spatřen", "lang_source_refresh": "Obnovit tento zdroj", "lang_sources_leaving_unsaved_prompt": "Na stránce jsou neuložené změny. Opravdu chcete opustit nastavení?", + "lang_tag_change_color_button_title": "Změnit barvu", "lang_no_entries": "Žádný záznam", "lang_more": "Více", "lang_login": "Přihlásit", diff --git a/assets/locale/en.json b/assets/locale/en.json index 950f719e27..a48618b6df 100644 --- a/assets/locale/en.json +++ b/assets/locale/en.json @@ -62,6 +62,7 @@ "lang_source_last_post": "Last post seen", "lang_source_refresh": "Refresh this source", "lang_sources_leaving_unsaved_prompt": "Leave settings with unsaved source changes?", + "lang_tag_change_color_button_title": "Change color", "lang_no_entries": "No entries found", "lang_more": "More", "lang_login": "Log in", diff --git a/assets/package-lock.json b/assets/package-lock.json index ef377870f5..cd616bff30 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -6,6 +6,7 @@ "": { "dependencies": { "@fancyapps/fancybox": "^3.1.20", + "@floating-ui/react-dom": "^0.7.0", "@fortawesome/fontawesome-svg-core": "^1.2.35", "@fortawesome/free-brands-svg-icons": "^6.0.0", "@fortawesome/free-regular-svg-icons": "^6.0.0", @@ -30,7 +31,6 @@ "react-router-dom": "^5.2.0", "reset-css": "^5.0.1", "rooks": "^5.0.2", - "spectrum-colorpicker": "^1.8.1", "tinykeys": "^1.1.1", "unreset-css": "^1.0.1", "use-state-with-deps": "^1.1.1" @@ -213,6 +213,32 @@ "jquery": ">=1.9.0" } }, + "node_modules/@floating-ui/core": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.0.tgz", + "integrity": "sha512-W7+i5Suhhvv97WDTW//KqUA43f/2a4abprM1rWqtLM9lIlJ29tbFI8h232SvqunXon0WmKNEKVjbOsgBhTnbLw==" + }, + "node_modules/@floating-ui/dom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.0.tgz", + "integrity": "sha512-PS75dnMg4GdWjDFOiOs15cDzYJpukRNHqQn0ugrBlsrpk2n+y8bwZ24XrsdLSL7kxshmxxr2nTNycLnmRIvV7g==", + "dependencies": { + "@floating-ui/core": "^0.7.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.7.0.tgz", + "integrity": "sha512-mpYGykTqwtBYT+ZTQQ2OfZ6wXJNuUgmqqD9ooCgbMRgvul6InFOTtWYvtujps439hmOFiVPm4PoBkEEn5imidg==", + "dependencies": { + "@floating-ui/dom": "^0.5.0", + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz", @@ -6065,11 +6091,6 @@ "specificity": "bin/specificity" } }, - "node_modules/spectrum-colorpicker": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/spectrum-colorpicker/-/spectrum-colorpicker-1.8.1.tgz", - "integrity": "sha512-x1picQ5giVso71ESII7jZ3+ZFdit8WthNkzwJqLNdPDPzrltKUQGpTohWyPfSAID+bK1zGdO6bDbSh1S6GoLYA==" - }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -6628,6 +6649,19 @@ "punycode": "^2.1.0" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-state-with-deps": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/use-state-with-deps/-/use-state-with-deps-1.1.1.tgz", @@ -6892,6 +6926,28 @@ "integrity": "sha512-rcEtu8t+WnmqIDV/Wfm1yvy/nDdwc7YV25j9HLxGC2/WOsUhk9rcWg2nB8g1BrjRt9zaoADdjHTU6ILYTJzBBg==", "requires": {} }, + "@floating-ui/core": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.0.tgz", + "integrity": "sha512-W7+i5Suhhvv97WDTW//KqUA43f/2a4abprM1rWqtLM9lIlJ29tbFI8h232SvqunXon0WmKNEKVjbOsgBhTnbLw==" + }, + "@floating-ui/dom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.0.tgz", + "integrity": "sha512-PS75dnMg4GdWjDFOiOs15cDzYJpukRNHqQn0ugrBlsrpk2n+y8bwZ24XrsdLSL7kxshmxxr2nTNycLnmRIvV7g==", + "requires": { + "@floating-ui/core": "^0.7.0" + } + }, + "@floating-ui/react-dom": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.7.0.tgz", + "integrity": "sha512-mpYGykTqwtBYT+ZTQQ2OfZ6wXJNuUgmqqD9ooCgbMRgvul6InFOTtWYvtujps439hmOFiVPm4PoBkEEn5imidg==", + "requires": { + "@floating-ui/dom": "^0.5.0", + "use-isomorphic-layout-effect": "^1.1.1" + } + }, "@fortawesome/fontawesome-common-types": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz", @@ -11078,11 +11134,6 @@ "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", "dev": true }, - "spectrum-colorpicker": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/spectrum-colorpicker/-/spectrum-colorpicker-1.8.1.tgz", - "integrity": "sha512-x1picQ5giVso71ESII7jZ3+ZFdit8WthNkzwJqLNdPDPzrltKUQGpTohWyPfSAID+bK1zGdO6bDbSh1S6GoLYA==" - }, "stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -11522,6 +11573,12 @@ "punycode": "^2.1.0" } }, + "use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "requires": {} + }, "use-state-with-deps": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/use-state-with-deps/-/use-state-with-deps-1.1.1.tgz", diff --git a/assets/package.json b/assets/package.json index 3d2a88e943..855317c0f0 100644 --- a/assets/package.json +++ b/assets/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@fancyapps/fancybox": "^3.1.20", + "@floating-ui/react-dom": "^0.7.0", "@fortawesome/fontawesome-svg-core": "^1.2.35", "@fortawesome/free-brands-svg-icons": "^6.0.0", "@fortawesome/free-regular-svg-icons": "^6.0.0", @@ -25,7 +26,6 @@ "react-router-dom": "^5.2.0", "reset-css": "^5.0.1", "rooks": "^5.0.2", - "spectrum-colorpicker": "^1.8.1", "tinykeys": "^1.1.1", "unreset-css": "^1.0.1", "use-state-with-deps": "^1.1.1" diff --git a/assets/styles/color-chooser.scss b/assets/styles/color-chooser.scss new file mode 100644 index 0000000000..a161507e24 --- /dev/null +++ b/assets/styles/color-chooser.scss @@ -0,0 +1,48 @@ +.color-chooser-button { + display: block; + + /* Span the whole wrapper */ + height: 100%; + width: 100%; +} + +.color { + .popup-menu { + width: 15em; + display: flex; + flex-flow: wrap; + + /* Popper does not like margins */ + margin: 0; + } + + .popup-menu-item { + width: 1.3em; + height: 1.3em; + display: flex; + margin: 0.2em; + border: 1px solid black; + padding: 0; + border-radius: 0.2em; + flex-shrink: 0; /* preserve aspect ratio */ + + svg { + margin: auto; + display: block; + } + } +} + +@media screen and (max-width: 1024px) { + .color { + .popup-menu { + max-width: 26em; + width: 100%; + } + + .popup-menu-item { + width: 2em; + height: 2em; + } + } +} diff --git a/assets/styles/main.scss b/assets/styles/main.scss index 0b40ba6f17..993422c216 100644 --- a/assets/styles/main.scss +++ b/assets/styles/main.scss @@ -1,8 +1,8 @@ @use 'sass:color'; @import 'npm:@fancyapps/fancybox/dist/jquery.fancybox.css'; -@import 'npm:spectrum-colorpicker/spectrum.css'; @import 'npm:reset-css/sass/reset'; +@import 'mixins/visually-hidden'; /* base */ @@ -22,6 +22,8 @@ $text-color: black; --background-color: white; } +@import 'color-chooser'; + html, body { height: 100%; @@ -61,6 +63,10 @@ button::-moz-focus-inner { @import 'popup-menu'; +.visually-hidden { + @include visually-hidden; +} + #js-loading-message { margin: 1em; text-align: center; diff --git a/assets/styles/mixins/visually-hidden.scss b/assets/styles/mixins/visually-hidden.scss new file mode 100644 index 0000000000..2f6b91d044 --- /dev/null +++ b/assets/styles/mixins/visually-hidden.scss @@ -0,0 +1,18 @@ +// stylelint-disable declaration-no-important + +// Hide content visually while keeping it accessible to assistive technologies +// +// See: https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content/ +// See: https://kittygiraudel.com/2016/10/13/css-hide-and-seek/ + +@mixin visually-hidden() { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; // Fix for https://github.com/twbs/bootstrap/issues/25686 + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +}