Change behavior of privacy dropdown to only change value on validation (#29406)
This commit is contained in:
		
							parent
							
								
									9fa7338b6e
								
							
						
					
					
						commit
						90573c3abb
					
				| @ -141,6 +141,7 @@ class LanguageDropdownMenu extends PureComponent { | |||||||
|     case 'Escape': |     case 'Escape': | ||||||
|       onClose(); |       onClose(); | ||||||
|       break; |       break; | ||||||
|  |     case ' ': | ||||||
|     case 'Enter': |     case 'Enter': | ||||||
|       this.handleClick(e); |       this.handleClick(e); | ||||||
|       break; |       break; | ||||||
|  | |||||||
| @ -5,16 +5,16 @@ import { injectIntl, defineMessages } from 'react-intl'; | |||||||
| 
 | 
 | ||||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||||
| 
 | 
 | ||||||
| import { supportsPassiveEvents } from 'detect-passive-events'; |  | ||||||
| import Overlay from 'react-overlays/Overlay'; | import Overlay from 'react-overlays/Overlay'; | ||||||
| 
 | 
 | ||||||
| import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; | import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; | ||||||
| import InfoIcon from '@/material-icons/400-24px/info.svg?react'; |  | ||||||
| import LockIcon from '@/material-icons/400-24px/lock.svg?react'; | import LockIcon from '@/material-icons/400-24px/lock.svg?react'; | ||||||
| import PublicIcon from '@/material-icons/400-24px/public.svg?react'; | import PublicIcon from '@/material-icons/400-24px/public.svg?react'; | ||||||
| import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; | import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; | ||||||
| import { Icon }  from 'mastodon/components/icon'; | import { Icon }  from 'mastodon/components/icon'; | ||||||
| 
 | 
 | ||||||
|  | import { PrivacyDropdownMenu } from './privacy_dropdown_menu'; | ||||||
|  | 
 | ||||||
| const messages = defineMessages({ | const messages = defineMessages({ | ||||||
|   public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, |   public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, | ||||||
|   public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' }, |   public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' }, | ||||||
| @ -28,126 +28,6 @@ const messages = defineMessages({ | |||||||
|   unlisted_extra: { id: 'privacy.unlisted.additional', defaultMessage: 'This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.' }, |   unlisted_extra: { id: 'privacy.unlisted.additional', defaultMessage: 'This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.' }, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; |  | ||||||
| 
 |  | ||||||
| class PrivacyDropdownMenu extends PureComponent { |  | ||||||
| 
 |  | ||||||
|   static propTypes = { |  | ||||||
|     style: PropTypes.object, |  | ||||||
|     items: PropTypes.array.isRequired, |  | ||||||
|     value: PropTypes.string.isRequired, |  | ||||||
|     onClose: PropTypes.func.isRequired, |  | ||||||
|     onChange: PropTypes.func.isRequired, |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   handleDocumentClick = e => { |  | ||||||
|     if (this.node && !this.node.contains(e.target)) { |  | ||||||
|       this.props.onClose(); |  | ||||||
|       e.stopPropagation(); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   handleKeyDown = e => { |  | ||||||
|     const { items } = this.props; |  | ||||||
|     const value = e.currentTarget.getAttribute('data-index'); |  | ||||||
|     const index = items.findIndex(item => { |  | ||||||
|       return (item.value === value); |  | ||||||
|     }); |  | ||||||
|     let element = null; |  | ||||||
| 
 |  | ||||||
|     switch(e.key) { |  | ||||||
|     case 'Escape': |  | ||||||
|       this.props.onClose(); |  | ||||||
|       break; |  | ||||||
|     case 'Enter': |  | ||||||
|       this.handleClick(e); |  | ||||||
|       break; |  | ||||||
|     case 'ArrowDown': |  | ||||||
|       element = this.node.childNodes[index + 1] || this.node.firstChild; |  | ||||||
|       break; |  | ||||||
|     case 'ArrowUp': |  | ||||||
|       element = this.node.childNodes[index - 1] || this.node.lastChild; |  | ||||||
|       break; |  | ||||||
|     case 'Tab': |  | ||||||
|       if (e.shiftKey) { |  | ||||||
|         element = this.node.childNodes[index - 1] || this.node.lastChild; |  | ||||||
|       } else { |  | ||||||
|         element = this.node.childNodes[index + 1] || this.node.firstChild; |  | ||||||
|       } |  | ||||||
|       break; |  | ||||||
|     case 'Home': |  | ||||||
|       element = this.node.firstChild; |  | ||||||
|       break; |  | ||||||
|     case 'End': |  | ||||||
|       element = this.node.lastChild; |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (element) { |  | ||||||
|       element.focus(); |  | ||||||
|       this.props.onChange(element.getAttribute('data-index')); |  | ||||||
|       e.preventDefault(); |  | ||||||
|       e.stopPropagation(); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   handleClick = e => { |  | ||||||
|     const value = e.currentTarget.getAttribute('data-index'); |  | ||||||
| 
 |  | ||||||
|     e.preventDefault(); |  | ||||||
| 
 |  | ||||||
|     this.props.onClose(); |  | ||||||
|     this.props.onChange(value); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   componentDidMount () { |  | ||||||
|     document.addEventListener('click', this.handleDocumentClick, { capture: true }); |  | ||||||
|     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); |  | ||||||
|     if (this.focusedItem) this.focusedItem.focus({ preventScroll: true }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   componentWillUnmount () { |  | ||||||
|     document.removeEventListener('click', this.handleDocumentClick, { capture: true }); |  | ||||||
|     document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   setRef = c => { |  | ||||||
|     this.node = c; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   setFocusRef = c => { |  | ||||||
|     this.focusedItem = c; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   render () { |  | ||||||
|     const { style, items, value } = this.props; |  | ||||||
| 
 |  | ||||||
|     return ( |  | ||||||
|       <div style={{ ...style }} role='listbox' ref={this.setRef}> |  | ||||||
|         {items.map(item => ( |  | ||||||
|           <div role='option' tabIndex={0} key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}> |  | ||||||
|             <div className='privacy-dropdown__option__icon'> |  | ||||||
|               <Icon id={item.icon} icon={item.iconComponent} /> |  | ||||||
|             </div> |  | ||||||
| 
 |  | ||||||
|             <div className='privacy-dropdown__option__content'> |  | ||||||
|               <strong>{item.text}</strong> |  | ||||||
|               {item.meta} |  | ||||||
|             </div> |  | ||||||
| 
 |  | ||||||
|             {item.extra && ( |  | ||||||
|               <div className='privacy-dropdown__option__additional' title={item.extra}> |  | ||||||
|                 <Icon id='info-circle' icon={InfoIcon} /> |  | ||||||
|               </div> |  | ||||||
|             )} |  | ||||||
|           </div> |  | ||||||
|         ))} |  | ||||||
|       </div> |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class PrivacyDropdown extends PureComponent { | class PrivacyDropdown extends PureComponent { | ||||||
| 
 | 
 | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|  | |||||||
| @ -0,0 +1,128 @@ | |||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | import { useCallback, useEffect, useRef, useState } from 'react'; | ||||||
|  | 
 | ||||||
|  | import classNames from 'classnames'; | ||||||
|  | 
 | ||||||
|  | import { supportsPassiveEvents } from 'detect-passive-events'; | ||||||
|  | 
 | ||||||
|  | import InfoIcon from '@/material-icons/400-24px/info.svg?react'; | ||||||
|  | import { Icon } from 'mastodon/components/icon'; | ||||||
|  | 
 | ||||||
|  | const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; | ||||||
|  | 
 | ||||||
|  | export const PrivacyDropdownMenu = ({ style, items, value, onClose, onChange }) => { | ||||||
|  |   const nodeRef = useRef(null); | ||||||
|  |   const focusedItemRef = useRef(null); | ||||||
|  |   const [currentValue, setCurrentValue] = useState(value); | ||||||
|  | 
 | ||||||
|  |   const handleDocumentClick = useCallback((e) => { | ||||||
|  |     if (nodeRef.current && !nodeRef.current.contains(e.target)) { | ||||||
|  |       onClose(); | ||||||
|  |       e.stopPropagation(); | ||||||
|  |     } | ||||||
|  |   }, [nodeRef, onClose]); | ||||||
|  | 
 | ||||||
|  |   const handleClick = useCallback((e) => { | ||||||
|  |     const value = e.currentTarget.getAttribute('data-index'); | ||||||
|  | 
 | ||||||
|  |     e.preventDefault(); | ||||||
|  | 
 | ||||||
|  |     onClose(); | ||||||
|  |     onChange(value); | ||||||
|  |   }, [onClose, onChange]); | ||||||
|  | 
 | ||||||
|  |   const handleKeyDown = useCallback((e) => { | ||||||
|  |     const value = e.currentTarget.getAttribute('data-index'); | ||||||
|  |     const index = items.findIndex(item => (item.value === value)); | ||||||
|  | 
 | ||||||
|  |     let element = null; | ||||||
|  | 
 | ||||||
|  |     switch (e.key) { | ||||||
|  |     case 'Escape': | ||||||
|  |       onClose(); | ||||||
|  |       break; | ||||||
|  |     case ' ': | ||||||
|  |     case 'Enter': | ||||||
|  |       handleClick(e); | ||||||
|  |       break; | ||||||
|  |     case 'ArrowDown': | ||||||
|  |       element = nodeRef.current.childNodes[index + 1] || nodeRef.current.firstChild; | ||||||
|  |       break; | ||||||
|  |     case 'ArrowUp': | ||||||
|  |       element = nodeRef.current.childNodes[index - 1] || nodeRef.current.lastChild; | ||||||
|  |       break; | ||||||
|  |     case 'Tab': | ||||||
|  |       if (e.shiftKey) { | ||||||
|  |         element = nodeRef.current.childNodes[index + 1] || nodeRef.current.firstChild; | ||||||
|  |       } else { | ||||||
|  |         element = nodeRef.current.childNodes[index - 1] || nodeRef.current.lastChild; | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     case 'Home': | ||||||
|  |       element = nodeRef.current.firstChild; | ||||||
|  |       break; | ||||||
|  |     case 'End': | ||||||
|  |       element = nodeRef.current.lastChild; | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (element) { | ||||||
|  |       element.focus(); | ||||||
|  |       setCurrentValue(element.getAttribute('data-index')); | ||||||
|  |       e.preventDefault(); | ||||||
|  |       e.stopPropagation(); | ||||||
|  |     } | ||||||
|  |   }, [nodeRef, items, onClose, handleClick, setCurrentValue]); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     document.addEventListener('click', handleDocumentClick, { capture: true }); | ||||||
|  |     document.addEventListener('touchend', handleDocumentClick, listenerOptions); | ||||||
|  |     focusedItemRef.current?.focus({ preventScroll: true }); | ||||||
|  | 
 | ||||||
|  |     return () => { | ||||||
|  |       document.removeEventListener('click', handleDocumentClick, { capture: true }); | ||||||
|  |       document.removeEventListener('touchend', handleDocumentClick, listenerOptions); | ||||||
|  |     }; | ||||||
|  |   }, [handleDocumentClick]); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <ul style={{ ...style }} role='listbox' ref={nodeRef}> | ||||||
|  |       {items.map(item => ( | ||||||
|  |         <li | ||||||
|  |           role='option' | ||||||
|  |           tabIndex={0} | ||||||
|  |           key={item.value} | ||||||
|  |           data-index={item.value} | ||||||
|  |           onKeyDown={handleKeyDown} | ||||||
|  |           onClick={handleClick} | ||||||
|  |           className={classNames('privacy-dropdown__option', { active: item.value === currentValue })} | ||||||
|  |           aria-selected={item.value === currentValue} | ||||||
|  |           ref={item.value === currentValue ? focusedItemRef : null} | ||||||
|  |         > | ||||||
|  |           <div className='privacy-dropdown__option__icon'> | ||||||
|  |             <Icon id={item.icon} icon={item.iconComponent} /> | ||||||
|  |           </div> | ||||||
|  | 
 | ||||||
|  |           <div className='privacy-dropdown__option__content'> | ||||||
|  |             <strong>{item.text}</strong> | ||||||
|  |             {item.meta} | ||||||
|  |           </div> | ||||||
|  | 
 | ||||||
|  |           {item.extra && ( | ||||||
|  |             <div className='privacy-dropdown__option__additional' title={item.extra}> | ||||||
|  |               <Icon id='info-circle' icon={InfoIcon} /> | ||||||
|  |             </div> | ||||||
|  |           )} | ||||||
|  |         </li> | ||||||
|  |       ))} | ||||||
|  |     </ul> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | PrivacyDropdownMenu.propTypes = { | ||||||
|  |   style: PropTypes.object, | ||||||
|  |   items: PropTypes.array.isRequired, | ||||||
|  |   value: PropTypes.string.isRequired, | ||||||
|  |   onClose: PropTypes.func.isRequired, | ||||||
|  |   onChange: PropTypes.func.isRequired, | ||||||
|  | }; | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user