diff --git a/RELEASE.md b/RELEASE.md index e701d0a37..40bc1864f 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -67,6 +67,7 @@ ### Fix +- I testi per screen reader della barra degli strumenti ora riflettono correttamente lo stato corrente (espandi/riduci). - I pulsanti sulla barra degli strumenti ora rispettano il contrasto minimo previsto per i colori. ## Versione 12.9.0 (08/01/2026) diff --git a/locales/de/LC_MESSAGES/volto.po b/locales/de/LC_MESSAGES/volto.po index 3b9f1a41e..df56e7c76 100644 --- a/locales/de/LC_MESSAGES/volto.po +++ b/locales/de/LC_MESSAGES/volto.po @@ -271,6 +271,11 @@ msgstr "" msgid "Etichetta path filter" msgstr "" +#: overrideTranslations +# defaultMessage: Expand toolbar +msgid "Expand toolbar" +msgstr "Toolbar vergrößern" + #: components/ItaliaTheme/View/FAQ/FaqFolder/FaqFolderView # defaultMessage: Non ho trovato la risposta che cercavi msgid "Faq Folder: Nessun risultato trovato" diff --git a/locales/en/LC_MESSAGES/volto.po b/locales/en/LC_MESSAGES/volto.po index faac3db80..55151afb1 100644 --- a/locales/en/LC_MESSAGES/volto.po +++ b/locales/en/LC_MESSAGES/volto.po @@ -256,6 +256,11 @@ msgstr "" msgid "Etichetta path filter" msgstr "" +#: overrideTranslations +# defaultMessage: Expand toolbar +msgid "Expand toolbar" +msgstr "" + #: components/ItaliaTheme/View/FAQ/FaqFolder/FaqFolderView # defaultMessage: Non ho trovato la risposta che cercavi msgid "Faq Folder: Nessun risultato trovato" diff --git a/locales/es/LC_MESSAGES/volto.po b/locales/es/LC_MESSAGES/volto.po index 4be655318..c7d27ba55 100644 --- a/locales/es/LC_MESSAGES/volto.po +++ b/locales/es/LC_MESSAGES/volto.po @@ -265,6 +265,11 @@ msgstr "Etiqueta" msgid "Etichetta path filter" msgstr "Etiqueta" +#: overrideTranslations +# defaultMessage: Expand toolbar +msgid "Expand toolbar" +msgstr "Expandir barra de herramientas" + #: components/ItaliaTheme/View/FAQ/FaqFolder/FaqFolderView # defaultMessage: Non ho trovato la risposta che cercavi msgid "Faq Folder: Nessun risultato trovato" diff --git a/locales/fr/LC_MESSAGES/volto.po b/locales/fr/LC_MESSAGES/volto.po index 57e7c9f20..8199dad72 100644 --- a/locales/fr/LC_MESSAGES/volto.po +++ b/locales/fr/LC_MESSAGES/volto.po @@ -273,6 +273,11 @@ msgstr "" msgid "Etichetta path filter" msgstr "" +#: overrideTranslations +# defaultMessage: Expand toolbar +msgid "Expand toolbar" +msgstr "Étendre la barre d’outils" + #: components/ItaliaTheme/View/FAQ/FaqFolder/FaqFolderView # defaultMessage: Non ho trovato la risposta che cercavi msgid "Faq Folder: Nessun risultato trovato" diff --git a/locales/it/LC_MESSAGES/volto.po b/locales/it/LC_MESSAGES/volto.po index 3ab910e82..8589c6799 100644 --- a/locales/it/LC_MESSAGES/volto.po +++ b/locales/it/LC_MESSAGES/volto.po @@ -256,6 +256,11 @@ msgstr "Etichetta" msgid "Etichetta path filter" msgstr "Etichetta" +#: overrideTranslations +# defaultMessage: Expand toolbar +msgid "Expand toolbar" +msgstr "Espandi barra degli strumenti" + #: components/ItaliaTheme/View/FAQ/FaqFolder/FaqFolderView # defaultMessage: Non ho trovato la risposta che cercavi msgid "Faq Folder: Nessun risultato trovato" diff --git a/locales/volto.pot b/locales/volto.pot index d2f95e520..0d2760cdc 100644 --- a/locales/volto.pot +++ b/locales/volto.pot @@ -1,7 +1,11 @@ msgid "" msgstr "" "Project-Id-Version: Plone\n" +<<<<<<< 50789-expand-shrink-toolbar-a11y +"POT-Creation-Date: 2026-01-23T14:00:57.840Z\n" +======= "POT-Creation-Date: 2026-02-23T13:41:44.207Z\n" +>>>>>>> main "Last-Translator: Plone i18n \n" "Language-Team: Plone i18n \n" "MIME-Version: 1.0\n" @@ -258,6 +262,11 @@ msgstr "" msgid "Etichetta path filter" msgstr "" +#: overrideTranslations +# defaultMessage: Expand toolbar +msgid "Expand toolbar" +msgstr "" + #: components/ItaliaTheme/View/FAQ/FaqFolder/FaqFolderView # defaultMessage: Non ho trovato la risposta che cercavi msgid "Faq Folder: Nessun risultato trovato" diff --git a/src/customizations/volto/components/manage/Toolbar/Toolbar.jsx b/src/customizations/volto/components/manage/Toolbar/Toolbar.jsx new file mode 100644 index 000000000..60034ab29 --- /dev/null +++ b/src/customizations/volto/components/manage/Toolbar/Toolbar.jsx @@ -0,0 +1,644 @@ +/** + * Toolbar component. + * @module components/manage/Toolbar/Toolbar + * + * Customization: Added aria-expanded and aria-controls to the toolbar shrink button for accessibility. + * this file can be removed when the customization is included in Volto core + * BACKPORT of https://github.com/plone/volto/pull/7500 + */ + +import React, { Component } from 'react'; +import { defineMessages, injectIntl } from 'react-intl'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import jwtDecode from 'jwt-decode'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { doesNodeContainClick } from 'semantic-ui-react/dist/commonjs/lib'; +import { withCookies } from 'react-cookie'; +import { filter, find } from 'lodash'; +import cx from 'classnames'; +import config from '@plone/volto/registry'; + +import More from '@plone/volto/components/manage/Toolbar/More'; +import PersonalTools from '@plone/volto/components/manage/Toolbar/PersonalTools'; +import Types from '@plone/volto/components/manage/Toolbar/Types'; +import PersonalInformation from '@plone/volto/components/manage/Preferences/PersonalInformation'; +import PersonalPreferences from '@plone/volto/components/manage/Preferences/PersonalPreferences'; +import StandardWrapper from '@plone/volto/components/manage/Toolbar/StandardWrapper'; +import { + getTypes, + listActions, + setExpandedToolbar, + unlockContent, +} from '@plone/volto/actions'; +import { Icon } from '@plone/volto/components'; +import { + BodyClass, + getBaseUrl, + getCookieOptions, + hasApiExpander, +} from '@plone/volto/helpers'; +import { Pluggable } from '@plone/volto/components/manage/Pluggable'; + +import penSVG from '@plone/volto/icons/pen.svg'; +import unlockSVG from '@plone/volto/icons/unlock.svg'; +import folderSVG from '@plone/volto/icons/folder.svg'; +import addSVG from '@plone/volto/icons/add-document.svg'; +import moreSVG from '@plone/volto/icons/more.svg'; +import userSVG from '@plone/volto/icons/user.svg'; +import backSVG from '@plone/volto/icons/back.svg'; +import clearSVG from '@plone/volto/icons/clear.svg'; + +const messages = defineMessages({ + edit: { + id: 'Edit', + defaultMessage: 'Edit', + }, + contents: { + id: 'Contents', + defaultMessage: 'Contents', + }, + add: { + id: 'Add', + defaultMessage: 'Add', + }, + more: { + id: 'More', + defaultMessage: 'More', + }, + personalTools: { + id: 'Personal tools', + defaultMessage: 'Personal tools', + }, + shrinkToolbar: { + id: 'Shrink toolbar', + defaultMessage: 'Shrink toolbar', + }, + expandToolbar: { + id: 'Expand toolbar', + defaultMessage: 'Expand toolbar', + }, + personalInformation: { + id: 'Personal Information', + defaultMessage: 'Personal Information', + }, + personalPreferences: { + id: 'Personal Preferences', + defaultMessage: 'Personal Preferences', + }, + collection: { + id: 'Collection', + defaultMessage: 'Collection', + }, + file: { + id: 'File', + defaultMessage: 'File', + }, + link: { + id: 'Link', + defaultMessage: 'Link', + }, + newsItem: { + id: 'News Item', + defaultMessage: 'News Item', + }, + page: { + id: 'Page', + defaultMessage: 'Page', + }, + back: { + id: 'Back', + defaultMessage: 'Back', + }, + unlock: { + id: 'Unlock', + defaultMessage: 'Unlock', + }, +}); + +let toolbarComponents = { + personalTools: { component: PersonalTools, wrapper: null }, + more: { component: More, wrapper: null }, + types: { component: Types, wrapper: null, contentAsProps: true }, + profile: { + component: PersonalInformation, + wrapper: StandardWrapper, + wrapperTitle: messages.personalInformation, + hideToolbarBody: true, + }, + preferences: { + component: PersonalPreferences, + wrapper: StandardWrapper, + wrapperTitle: messages.personalPreferences, + hideToolbarBody: true, + }, +}; + +/** + * Toolbar container class. + * @class Toolbar + * @extends Component + */ +class Toolbar extends Component { + /** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ + static propTypes = { + actions: PropTypes.shape({ + object: PropTypes.arrayOf(PropTypes.object), + object_buttons: PropTypes.arrayOf(PropTypes.object), + user: PropTypes.arrayOf(PropTypes.object), + }), + token: PropTypes.string, + userId: PropTypes.string, + pathname: PropTypes.string.isRequired, + content: PropTypes.shape({ + '@type': PropTypes.string, + is_folderish: PropTypes.bool, + review_state: PropTypes.string, + }), + getTypes: PropTypes.func.isRequired, + types: PropTypes.arrayOf( + PropTypes.shape({ + '@id': PropTypes.string, + addable: PropTypes.bool, + title: PropTypes.string, + }), + ), + listActions: PropTypes.func.isRequired, + unlockContent: PropTypes.func, + unlockRequest: PropTypes.objectOf(PropTypes.any), + inner: PropTypes.element.isRequired, + hideDefaultViewButtons: PropTypes.bool, + }; + + /** + * Default properties. + * @property {Object} defaultProps Default properties. + * @static + */ + static defaultProps = { + actions: null, + token: null, + userId: null, + content: null, + hideDefaultViewButtons: false, + types: [], + }; + + toolbarWindow = React.createRef(); + + constructor(props) { + super(props); + const { cookies } = props; + this.state = { + expanded: cookies.get('toolbar_expanded') !== 'false', + showMenu: false, + menuStyle: {}, + menuComponents: [], + loadedComponents: [], + hideToolbarBody: false, + }; + } + + /** + * Component will mount + * @method componentDidMount + * @returns {undefined} + */ + componentDidMount() { + // Do not trigger the actions action if the expander is present + if (!hasApiExpander('actions', getBaseUrl(this.props.pathname))) { + this.props.listActions(getBaseUrl(this.props.pathname)); + } + // Do not trigger the types action if the expander is present + if (!hasApiExpander('types', getBaseUrl(this.props.pathname))) { + this.props.getTypes(getBaseUrl(this.props.pathname)); + } + toolbarComponents = { + ...(config.settings + ? config.settings.additionalToolbarComponents || {} + : {}), + ...toolbarComponents, + }; + this.props.setExpandedToolbar(this.state.expanded); + document.addEventListener('mousedown', this.handleClickOutside, false); + } + + /** + * Component will receive props + * @method componentWillReceiveProps + * @param {Object} nextProps Next properties + * @returns {undefined} + */ + UNSAFE_componentWillReceiveProps(nextProps) { + if (nextProps.pathname !== this.props.pathname) { + // Do not trigger the actions action if the expander is present + if (!hasApiExpander('actions', getBaseUrl(nextProps.pathname))) { + this.props.listActions(getBaseUrl(nextProps.pathname)); + } + // Do not trigger the types action if the expander is present + if (!hasApiExpander('types', getBaseUrl(nextProps.pathname))) { + this.props.getTypes(getBaseUrl(nextProps.pathname)); + } + } + + // Unlock + if (this.props.unlockRequest.loading && nextProps.unlockRequest.loaded) { + this.props.listActions(getBaseUrl(nextProps.pathname)); + } + } + + /** + * Component will receive props + * @method componentWillUnmount + * @returns {undefined} + */ + componentWillUnmount() { + document.removeEventListener('mousedown', this.handleClickOutside, false); + } + + handleShrink = () => { + const { cookies } = this.props; + cookies.set('toolbar_expanded', !this.state.expanded, getCookieOptions()); + this.setState( + (state) => ({ expanded: !state.expanded }), + () => this.props.setExpandedToolbar(this.state.expanded), + ); + }; + + closeMenu = () => + this.setState(() => ({ showMenu: false, loadedComponents: [] })); + + loadComponent = (type) => { + const { loadedComponents } = this.state; + if (!this.state.loadedComponents.includes(type)) { + this.setState({ + loadedComponents: [...loadedComponents, type], + hideToolbarBody: toolbarComponents[type].hideToolbarBody || false, + }); + } + }; + + unloadComponent = () => { + this.setState((state) => ({ + loadedComponents: state.loadedComponents.slice(0, -1), + hideToolbarBody: + toolbarComponents[ + state.loadedComponents[state.loadedComponents.length - 2] + ].hideToolbarBody || false, + })); + }; + + toggleMenu = (e, selector) => { + if (this.state.showMenu) { + this.closeMenu(); + return; + } + // PersonalTools always shows at bottom + if (selector === 'personalTools') { + this.setState((state) => ({ + showMenu: !state.showMenu, + menuStyle: { bottom: 0 }, + })); + } else if (selector === 'more') { + this.setState((state) => ({ + showMenu: !state.showMenu, + menuStyle: { + overflow: 'visible', + top: 0, + }, + })); + } else { + this.setState((state) => ({ + showMenu: !state.showMenu, + menuStyle: { top: 0 }, + })); + } + this.loadComponent(selector); + }; + + handleClickOutside = (e) => { + if (this.pusher && doesNodeContainClick(this.pusher, e)) return; + this.closeMenu(); + }; + + unlock = (e) => { + this.props.unlockContent(getBaseUrl(this.props.pathname), true); + }; + + /** + * Render method. + * @method render + * @returns {string} Markup for the component. + */ + render() { + const path = getBaseUrl(this.props.pathname); + const lock = this.props.content?.lock; + const unlockAction = + lock?.locked && lock?.stealable && lock?.creator !== this.props.userId; + const editAction = + !unlockAction && find(this.props.actions.object, { id: 'edit' }); + const folderContentsAction = find(this.props.actions.object, { + id: 'folderContents', + }); + const { expanded } = this.state; + + return ( + this.props.token && ( + <> + +
+ {this.state.showMenu && ( + // This sets the scroll locker in the body tag in mobile + + )} +
(this.pusher = node)} + style={{ + transform: this.toolbarWindow.current + ? `translateX(-${ + (this.state.loadedComponents.length - 1) * + this.toolbarWindow.current.getBoundingClientRect().width + }px)` + : null, + }} + > + {this.state.loadedComponents.map((component, index) => + (() => { + const ToolbarComponent = + toolbarComponents[component].component; + const WrapperComponent = toolbarComponents[component].wrapper; + const haveActions = + toolbarComponents[component].hideToolbarBody; + const title = + toolbarComponents[component].wrapperTitle && + this.props.intl.formatMessage( + toolbarComponents[component].wrapperTitle, + ); + if (WrapperComponent) { + return ( + + + + ); + } else { + return ( + + ); + } + })(), + )} +
+
+
+
+
+ {this.props.hideDefaultViewButtons && this.props.inner && ( + <>{this.props.inner} + )} + {!this.props.hideDefaultViewButtons && ( + <> + {unlockAction && ( + + )} + + {editAction && ( + + + + )} + {this.props.content && + this.props.content.is_folderish && + folderContentsAction && + !this.props.pathname.endsWith('/contents') && ( + + + + )} + {this.props.content && + this.props.content.is_folderish && + folderContentsAction && + this.props.pathname.endsWith('/contents') && ( + + + + )} + {this.props.content && + ((this.props.content.is_folderish && + this.props.types.length > 0) || + (config.settings.isMultilingual && + this.props.content['@components']?.translations)) && ( + + )} +
+ + + )} + +
+
+ + {!this.props.hideDefaultViewButtons && ( + + )} +
+
+
+ +
+
+
+ + ) + ); + } +} + +export default compose( + injectIntl, + withCookies, + connect( + (state, props) => ({ + actions: state.actions.actions, + token: state.userSession.token, + userId: state.userSession.token + ? jwtDecode(state.userSession.token).sub + : '', + content: state.content.data, + pathname: props.pathname, + types: filter(state.types.types, 'addable'), + unlockRequest: state.content.unlock, + }), + { getTypes, listActions, setExpandedToolbar, unlockContent }, + ), +)(Toolbar); diff --git a/src/overrideTranslations.jsx b/src/overrideTranslations.jsx index cff4d1d4c..6fef4e136 100644 --- a/src/overrideTranslations.jsx +++ b/src/overrideTranslations.jsx @@ -177,4 +177,11 @@ defineMessages({ id: 'Published', defaultMessage: 'Published', }, + //BACKPORT of https://github.com/plone/volto/pull/7500 + //start + expandToolbar: { + id: 'Expand toolbar', + defaultMessage: 'Expand toolbar', + }, + //end });