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': | ||||
|       onClose(); | ||||
|       break; | ||||
|     case ' ': | ||||
|     case 'Enter': | ||||
|       this.handleClick(e); | ||||
|       break; | ||||
|  | ||||
| @ -5,16 +5,16 @@ import { injectIntl, defineMessages } from 'react-intl'; | ||||
| 
 | ||||
| import classNames from 'classnames'; | ||||
| 
 | ||||
| import { supportsPassiveEvents } from 'detect-passive-events'; | ||||
| import Overlay from 'react-overlays/Overlay'; | ||||
| 
 | ||||
| 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 PublicIcon from '@/material-icons/400-24px/public.svg?react'; | ||||
| import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; | ||||
| import { Icon }  from 'mastodon/components/icon'; | ||||
| 
 | ||||
| import { PrivacyDropdownMenu } from './privacy_dropdown_menu'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, | ||||
|   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.' }, | ||||
| }); | ||||
| 
 | ||||
| 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 { | ||||
| 
 | ||||
|   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