diff --git a/app/javascript/mastodon/components/dropdown/redesign.module.scss b/app/javascript/mastodon/components/dropdown/redesign.module.scss index 1439df90f11..54f8eddc2e1 100644 --- a/app/javascript/mastodon/components/dropdown/redesign.module.scss +++ b/app/javascript/mastodon/components/dropdown/redesign.module.scss @@ -69,6 +69,11 @@ width: 100%; } +.menuItemIcon { + width: var(--space-lg); + height: var(--space-lg); +} + .menu > hr { height: 1px; background-color: var(--color-border-primary); diff --git a/app/javascript/mastodon/components/dropdown/redesign.stories.tsx b/app/javascript/mastodon/components/dropdown/redesign.stories.tsx index b56e8df3dcb..69847d2bedd 100644 --- a/app/javascript/mastodon/components/dropdown/redesign.stories.tsx +++ b/app/javascript/mastodon/components/dropdown/redesign.stories.tsx @@ -13,7 +13,6 @@ import { useToggle } from '@/mastodon/hooks/useToggle'; import { Button } from '../button/redesign'; import { ToggleField } from '../form_fields/redesign'; -import { Icon } from '../icon'; import type { DropdownProps } from './redesign'; import { @@ -47,13 +46,13 @@ export const Simple: Story = { return ( First item Second item @@ -81,13 +80,13 @@ export const Popover: Story = { {...args} > First item Second item @@ -109,12 +108,7 @@ export const Controls: Story = {
- - {sun ? ( - - ) : ( - - )} +
diff --git a/app/javascript/mastodon/components/dropdown/redesign.tsx b/app/javascript/mastodon/components/dropdown/redesign.tsx index 3587c01d664..9037a71c6ac 100644 --- a/app/javascript/mastodon/components/dropdown/redesign.tsx +++ b/app/javascript/mastodon/components/dropdown/redesign.tsx @@ -90,7 +90,15 @@ export const Dropdown = ({ }; type DropdownItemProps = Merge< - { as?: As; children?: React.ReactNode; className?: string; active?: boolean }, + { + as?: As; + children?: React.ReactNode; + className?: string; + active?: boolean; + leadingIcon?: IconProp; + trailingIcon?: IconProp; + iconClassName?: string; + }, React.ComponentPropsWithoutRef >; @@ -99,6 +107,9 @@ export const DropdownItem = ({ as: AsComp, children, className, + leadingIcon, + trailingIcon, + iconClassName, ...props }: DropdownItemProps) => { const Component = AsComp ?? 'div'; @@ -111,14 +122,30 @@ export const DropdownItem = ({ active && classes.menuItemActive, )} > + {leadingIcon && ( + + )} + {children} + + {trailingIcon && ( + + )} ); }; export const DropdownItemButton: React.FC< - { icon?: IconProp } & React.ComponentProps<'button'> -> = ({ icon, children, className, ...props }) => { + Omit, 'as'> +> = ({ children, className, ...props }) => { return ( - {icon && } {children} ); diff --git a/app/javascript/mastodon/components/form_fields/redesign.module.scss b/app/javascript/mastodon/components/form_fields/redesign.module.scss index af0d9c1ad2f..ca9b683ee2e 100644 --- a/app/javascript/mastodon/components/form_fields/redesign.module.scss +++ b/app/javascript/mastodon/components/form_fields/redesign.module.scss @@ -28,8 +28,8 @@ } .icon { - width: var(--fs-lg); - height: var(--fs-lg); + width: var(--space-lg); + height: var(--space-lg); } // Text Input diff --git a/app/javascript/mastodon/features/compose/redesign/styles.module.scss b/app/javascript/mastodon/features/compose/redesign/styles.module.scss index b35cb80f20c..1d8f35f965b 100644 --- a/app/javascript/mastodon/features/compose/redesign/styles.module.scss +++ b/app/javascript/mastodon/features/compose/redesign/styles.module.scss @@ -48,6 +48,12 @@ } } +.dropdownItemControl { + color: inherit; + width: 100%; + user-select: none; +} + .textarea > textarea { @include mixins.type-body-lg; diff --git a/app/javascript/mastodon/features/compose/redesign/upload.tsx b/app/javascript/mastodon/features/compose/redesign/upload.tsx index 2fc743f107b..6f2504a747d 100644 --- a/app/javascript/mastodon/features/compose/redesign/upload.tsx +++ b/app/javascript/mastodon/features/compose/redesign/upload.tsx @@ -111,7 +111,7 @@ export const ComposeUpload: React.FC<{ id?: string; className?: string }> = ({ offset={4} maxWidth={170} > - + = ({ > = ( const defaultPrivacy = useAppSelector( (state) => state.compose.get('default_privacy') as StatusVisibility, ); - const quotePolicy = useAppSelector( - (state) => - (state.compose.get('quote_policy') as ApiQuotePolicy | undefined) ?? - (state.compose.get('default_quote_policy') as ApiQuotePolicy), + const currentQuotePolicy = useAppSelector( + (state) => state.compose.get('quote_policy') as ApiQuotePolicy | undefined, ); + const defaultQuotePolicy = useAppSelector( + (state) => state.compose.get('default_quote_policy') as ApiQuotePolicy, + ); + const quotePolicy = currentQuotePolicy ?? defaultQuotePolicy; const dispatch = useAppDispatch(); const handlePrivacyChange: React.ChangeEventHandler = useCallback( (event) => { - const { name } = event.target; - if (name === 'private' && privacy !== 'private') { - dispatch(changeComposeVisibility(name)); - } else if (name === 'public' && privacy === 'private') { + const { value } = event.target; + if (value === 'private' && privacy !== 'private') { + dispatch(changeComposeVisibility(value)); + } else if (value === 'public' && privacy === 'private') { dispatch( changeComposeVisibility( defaultPrivacy === 'unlisted' ? 'unlisted' : 'public', ), ); - } else if (name === 'unlisted' && privacy !== 'private') { + } else if (value === 'unlisted' && privacy !== 'private') { dispatch( changeComposeVisibility( privacy === 'public' ? 'unlisted' : 'public', @@ -112,10 +123,27 @@ const ComposeVisibilityMenu: React.FC> = ( const handleQuotePolicyChange: React.ChangeEventHandler = useCallback( (event) => { - const checked = event.target.checked; - dispatch(setComposeQuotePolicy(checked ? 'public' : 'nobody')); + const { value, checked } = event.target; + let newQuotePolicy: ApiQuotePolicy = 'nobody'; + switch (value) { + case 'public': + newQuotePolicy = 'public'; + break; + case 'followers': + newQuotePolicy = 'followers'; + break; + case 'others': + // If it's not checked, then it's nobody. + if (checked) { + // Only use the default if it's not nobody, as then it'll never be enabled. + newQuotePolicy = + defaultQuotePolicy !== 'nobody' ? defaultQuotePolicy : 'public'; + } + break; + } + dispatch(setComposeQuotePolicy(newQuotePolicy)); }, - [dispatch], + [defaultQuotePolicy, dispatch], ); const handleSwitchToMessage: React.MouseEventHandler = useCallback(() => { @@ -134,71 +162,99 @@ const ComposeVisibilityMenu: React.FC> = ( } className={classes.visibilityFieldset} > - - - } - checked={privacy === 'public' || privacy === 'unlisted'} - onChange={handlePrivacyChange} - /> - + + + - - - } - checked={privacy === 'private'} - onChange={handlePrivacyChange} + + - +
- - - } - disabled={privacy === 'private'} - checked={privacy === 'public'} - onChange={handlePrivacyChange} - size='sm' + + - + - - + + + + {quotePolicy !== 'nobody' && privacy !== 'private' && ( +
} - disabled={privacy === 'private'} - checked={quotePolicy === 'public' && privacy !== 'private'} - onChange={handleQuotePolicyChange} - size='sm' - /> - + className={classes.visibilityFieldset} + > + + + + + + + +
+ )}
- + > = ( ); }; + +const DropdownRadioCheckField: React.FC< + Omit< + React.ComponentProps, + 'label' | 'icon' | 'iconClassName' + > & { + children: React.ReactNode; + } +> = ({ children, onClick, ...props }) => { + const { ref, onWrapperClick } = useDropdownControl(); + + return ( + + + + ); +}; + +const DropdownToggleField: React.FC< + Omit, 'label'> & { + children: React.ReactNode; + icon?: IconProp; + } +> = ({ children, icon, ...props }) => { + const { ref, onWrapperClick } = useDropdownControl(); + + return ( + + + + ); +}; + +function useDropdownControl() { + const ref = useRef(null); + const onWrapperClick: React.MouseEventHandler = useCallback((event) => { + const { target } = event; + if ( + target instanceof HTMLLabelElement || + target instanceof HTMLInputElement + ) { + return; + } + ref.current?.click(); + }, []); + return { ref, onWrapperClick }; +} diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 526671a61a1..f30c8b074da 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -518,6 +518,9 @@ "compose.upload.menu": "Add alt text or remove the image", "compose.upload.menu.add_alt": "Add alt text", "compose.upload.menu.delete": "Remove image", + "compose.visibility.quote_policy": "Who can quote", + "compose.visibility.quote_policy.anyone": "Anyone", + "compose.visibility.quote_policy.followers": "Followers", "compose.visibility.title": "Visibility", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any sensitive information over Mastodon.",