diff --git a/app/javascript/mastodon/components/list_item/index.tsx b/app/javascript/mastodon/components/list_item/index.tsx index 263e206fb9c..3e8d4067526 100644 --- a/app/javascript/mastodon/components/list_item/index.tsx +++ b/app/javascript/mastodon/components/list_item/index.tsx @@ -1,6 +1,8 @@ import classNames from 'classnames'; import { Link } from 'react-router-dom'; +import { polymorphicForwardRef } from '@/types/polymorphic'; + import classes from './styles.module.scss'; interface WrapperProps extends Omit< @@ -8,7 +10,7 @@ interface WrapperProps extends Omit< 'title' > { icon?: React.ReactNode; - iconEnd?: React.ReactNode; + sideContent?: React.ReactNode; } /** @@ -20,7 +22,7 @@ interface WrapperProps extends Omit< */ export const ListItemWrapper: React.FC = ({ icon, - iconEnd, + sideContent, children, className, ...otherProps @@ -29,69 +31,68 @@ export const ListItemWrapper: React.FC = ({
{icon}
{children}
- {iconEnd && {iconEnd}} + {sideContent && ( + {sideContent} + )}
); }; -interface WithSubtitle { +interface ContentProps { subtitle?: React.ReactNode; + subtitleId?: string; } -interface ContentProps - extends React.ComponentPropsWithoutRef<'h3'>, WithSubtitle {} - -export const ListItemContent: React.FC = ({ - subtitle, - children, - ...otherProps -}) => { - return ( - <> -

- {children} -

- {subtitle &&
{subtitle}
} - - ); -}; +export const ListItemContent = polymorphicForwardRef<'h3', ContentProps>( + ( + { as: Component = 'h3', subtitle, subtitleId, children, ...otherProps }, + ref, + ) => { + return ( + <> + + {children} + + {subtitle && ( +
+ {subtitle} +
+ )} + + ); + }, +); interface LinkProps - extends React.ComponentPropsWithoutRef, WithSubtitle {} + extends React.ComponentPropsWithoutRef, ContentProps {} -export const ListItemLink: React.FC = ({ - subtitle, - children, - className, - ...otherProps -}) => { - return ( - - - {children} - - - ); -}; +export const ListItemLink = polymorphicForwardRef<'h3', LinkProps>( + ({ as, subtitle, children, className, ...otherProps }, ref) => { + return ( + + + {children} + + + ); + }, +); interface ButtonProps - extends React.ComponentPropsWithoutRef<'button'>, WithSubtitle {} + extends React.ComponentPropsWithoutRef<'button'>, ContentProps {} -export const ListItemButton: React.FC = ({ - subtitle, - children, - className, - ...otherProps -}) => { - return ( - - - - ); -}; +export const ListItemButton = polymorphicForwardRef<'h3', ButtonProps>( + ({ as, subtitle, children, className, ...otherProps }, ref) => { + return ( + + + + ); + }, +); diff --git a/app/javascript/mastodon/components/list_item/list_item.stories.tsx b/app/javascript/mastodon/components/list_item/list_item.stories.tsx index dd066683c93..a191f8e4ed3 100644 --- a/app/javascript/mastodon/components/list_item/list_item.stories.tsx +++ b/app/javascript/mastodon/components/list_item/list_item.stories.tsx @@ -5,6 +5,8 @@ import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react'; import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react'; +import { AvatarById } from '../avatar'; +import { Button } from '../button'; import { Icon } from '../icon'; import { @@ -36,7 +38,7 @@ export const WithButton: Story = { render: () => ( } - iconEnd={} + sideContent={} > 3 hidden accounts @@ -49,9 +51,22 @@ export const WithLink: Story = { render: () => ( } - iconEnd={} + sideContent={} > View more ), }; + +export const WithInteractiveSideContent: Story = { + render: () => ( + } + sideContent={} + > + + Test account + + + ), +}; diff --git a/app/javascript/mastodon/components/list_item/styles.module.scss b/app/javascript/mastodon/components/list_item/styles.module.scss index 42542d5a229..8a90234f931 100644 --- a/app/javascript/mastodon/components/list_item/styles.module.scss +++ b/app/javascript/mastodon/components/list_item/styles.module.scss @@ -1,11 +1,15 @@ .wrapper { + --list-item-padding: 16px; + --list-item-padding-block: var(--list-item-padding); + --list-item-gap: 12px; + box-sizing: border-box; position: relative; display: flex; align-items: center; width: 100%; - gap: 12px; - padding: 16px; + gap: var(--list-item-gap); + padding: var(--list-item-padding-block) var(--list-item-padding); font-size: 15px; color: var(--color-text-primary); } @@ -29,7 +33,7 @@ &::before { content: ''; position: absolute; - inset: 0; + inset: var(--clickable-area-spread, 0); } } } @@ -39,6 +43,32 @@ color: var(--color-text-secondary); } -.iconEnd { +.sideContent { + display: flex; + gap: var(--list-item-gap); + align-self: stretch; + align-items: center; margin-inline-start: auto; + + &:has(button, a) { + // If the sideContent has interactive children, move it + // above the clickable area of the .title content + position: relative; + z-index: 1; + + // Cover up the .title content's clickable area in the + // padding box around the sideContent + &::before { + content: ''; + position: absolute; + inset-block: calc(-1 * var(--list-item-padding-block)); + inset-inline: calc(-1 * var(--list-item-padding)); + z-index: -1; + } + } + + // Improve vertical alignment of slotted icons + & > .icon { + vertical-align: middle; + } } diff --git a/app/javascript/mastodon/components/truncated_list/index.tsx b/app/javascript/mastodon/components/truncated_list/index.tsx index 75e5a4ee250..898900fc296 100644 --- a/app/javascript/mastodon/components/truncated_list/index.tsx +++ b/app/javascript/mastodon/components/truncated_list/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useId, useState } from 'react'; import { Article } from '@/mastodon/components/scrollable_list/components'; import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react'; @@ -42,6 +42,8 @@ export const TruncatedListItems = ({ toggleButton, renderListItem, }: TruncatedListProps) => { + const toggleButtonId = useId(); + const toggleButtonDescId = `${toggleButtonId}-desc`; const [showTruncatedItems, setShowTruncatedItems] = useState(false); const toggleTruncatedItems = useCallback(() => { setShowTruncatedItems((prev) => !prev); @@ -65,14 +67,19 @@ export const TruncatedListItems = ({ }); })} {hasHiddenAccounts && ( -
+
) } - iconEnd={ + sideContent={ ({ } > { withoutBorder?: boolean; positionInList: number; listSize: number; @@ -20,6 +23,7 @@ export const CollectionListItem: React.FC = ({ withoutBorder, positionInList, listSize, + className, ...otherProps }) => { const uniqueId = useId(); @@ -29,21 +33,26 @@ export const CollectionListItem: React.FC = ({ return (
- - - + } + {...otherProps} />
); diff --git a/app/javascript/mastodon/features/collections/components/collection_lockup.module.scss b/app/javascript/mastodon/features/collections/components/collection_lockup.module.scss index b658b490de4..cdd5b183685 100644 --- a/app/javascript/mastodon/features/collections/components/collection_lockup.module.scss +++ b/app/javascript/mastodon/features/collections/components/collection_lockup.module.scss @@ -1,11 +1,5 @@ -.content { - position: relative; - flex-grow: 1; - display: flex; - align-items: center; - column-gap: 12px; - color: var(--color-text-primary); - line-height: 1.3; +.wrapper { + --list-item-padding: 0px; } .avatarGrid { @@ -36,27 +30,3 @@ fill: var(--color-text-primary); background: var(--color-bg-warning-softest); } - -.link { - display: block; - font-size: 15px; - font-weight: 500; - text-decoration: none; - color: var(--color-text-primary); - - &:hover { - color: var(--color-text-brand); - } - - &::after { - // Increase clickable area by extending link across parent - content: ''; - position: absolute; - inset: 0; - } -} - -.info { - font-size: 13px; - color: var(--color-text-secondary); -} diff --git a/app/javascript/mastodon/features/collections/components/collection_lockup.tsx b/app/javascript/mastodon/features/collections/components/collection_lockup.tsx index bbcde0c3bd3..9593f02e2d9 100644 --- a/app/javascript/mastodon/features/collections/components/collection_lockup.tsx +++ b/app/javascript/mastodon/features/collections/components/collection_lockup.tsx @@ -1,10 +1,8 @@ -import { useId } from 'react'; - import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; -import { Link } from 'react-router-dom'; +import { ListItemLink, ListItemWrapper } from '@/mastodon/components/list_item'; import WarningIcon from '@/material-icons/400-24px/warning.svg?react'; import type { ApiCollectionJSON } from 'mastodon/api_types/collections'; import { AvatarById } from 'mastodon/components/avatar'; @@ -47,71 +45,78 @@ export interface CollectionLockupProps { collection: ApiCollectionJSON; withAuthorHandle?: boolean; withTimestamp?: boolean; + sideContent?: React.ReactNode; + className?: string; } export const CollectionLockup: React.FC = ({ collection, withAuthorHandle = true, withTimestamp, + sideContent, + className, }) => { const { id, name } = collection; - const uniqueId = useId(); - const linkId = `${uniqueId}-link`; - const infoId = `${uniqueId}-info`; const authorAccount = useAccount(collection.account_id); const authorHandle = useAccountHandle(authorAccount, domain); - return ( -
- item.account_id)} - sensitive={collection.sensitive} - /> -
-

- - {name} - -

-
    - {collection.sensitive && ( -
  • - -
  • - )} - {withAuthorHandle && authorAccount && ( - - )} + const collectionInfo = ( +
      + {collection.sensitive && ( +
    • - {withTimestamp && ( - - ), - }} - tagName='li' - /> - )} -
    -
-
+ + )} + {withAuthorHandle && authorAccount && ( + + )} + + {withTimestamp && ( + , + }} + tagName='li' + /> + )} + + ); + + return ( + item.account_id)} + sensitive={collection.sensitive} + /> + } + sideContent={sideContent} + > + + {name} + + ); }; diff --git a/app/javascript/mastodon/features/collections/components/collection_preview_card.module.scss b/app/javascript/mastodon/features/collections/components/collection_preview_card.module.scss index 16b883b72b4..26eb44539e4 100644 --- a/app/javascript/mastodon/features/collections/components/collection_preview_card.module.scss +++ b/app/javascript/mastodon/features/collections/components/collection_preview_card.module.scss @@ -1,8 +1,10 @@ .wrapper { - display: flex; - align-items: start; - padding: 12px; - gap: 12px; + --list-item-padding: 12px; + border-radius: 12px; border: 1px solid var(--color-border-primary); } + +.removeButton { + align-self: start; +} diff --git a/app/javascript/mastodon/features/collections/components/collection_preview_card.tsx b/app/javascript/mastodon/features/collections/components/collection_preview_card.tsx index 204e2767c53..83dc3e8a61e 100644 --- a/app/javascript/mastodon/features/collections/components/collection_preview_card.tsx +++ b/app/javascript/mastodon/features/collections/components/collection_preview_card.tsx @@ -1,5 +1,9 @@ +import { useIntl } from 'react-intl'; + import classNames from 'classnames'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import { IconButton } from 'mastodon/components/icon_button'; import type { CollectionLockupProps } from 'mastodon/features/collections/components/collection_lockup'; import { CollectionLockup } from 'mastodon/features/collections/components/collection_lockup'; @@ -14,9 +18,26 @@ export const CollectionPreviewCard: React.FC = ({ onRemove, ...otherProps }) => { + const intl = useIntl(); + const removeButton = onRemove && ( + + ); + return ( -
- -
+ ); };