[Glitch] Increase clickable area around collection items, refactor ListItem component
Port 2b93a2211f to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
@@ -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<WrapperProps> = ({
|
||||
icon,
|
||||
iconEnd,
|
||||
sideContent,
|
||||
children,
|
||||
className,
|
||||
...otherProps
|
||||
@@ -29,69 +31,68 @@ export const ListItemWrapper: React.FC<WrapperProps> = ({
|
||||
<div {...otherProps} className={classNames(classes.wrapper, className)}>
|
||||
{icon}
|
||||
<div>{children}</div>
|
||||
{iconEnd && <span className={classes.iconEnd}>{iconEnd}</span>}
|
||||
{sideContent && (
|
||||
<span className={classes.sideContent}>{sideContent}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface WithSubtitle {
|
||||
interface ContentProps {
|
||||
subtitle?: React.ReactNode;
|
||||
subtitleId?: string;
|
||||
}
|
||||
|
||||
interface ContentProps
|
||||
extends React.ComponentPropsWithoutRef<'h3'>, WithSubtitle {}
|
||||
|
||||
export const ListItemContent: React.FC<ContentProps> = ({
|
||||
subtitle,
|
||||
children,
|
||||
...otherProps
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<h3 className={classes.title} {...otherProps}>
|
||||
{children}
|
||||
</h3>
|
||||
{subtitle && <div className={classes.subtitle}>{subtitle}</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const ListItemContent = polymorphicForwardRef<'h3', ContentProps>(
|
||||
(
|
||||
{ as: Component = 'h3', subtitle, subtitleId, children, ...otherProps },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<>
|
||||
<Component className={classes.title} ref={ref} {...otherProps}>
|
||||
{children}
|
||||
</Component>
|
||||
{subtitle && (
|
||||
<div className={classes.subtitle} id={subtitleId}>
|
||||
{subtitle}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
interface LinkProps
|
||||
extends React.ComponentPropsWithoutRef<typeof Link>, WithSubtitle {}
|
||||
extends React.ComponentPropsWithoutRef<typeof Link>, ContentProps {}
|
||||
|
||||
export const ListItemLink: React.FC<LinkProps> = ({
|
||||
subtitle,
|
||||
children,
|
||||
className,
|
||||
...otherProps
|
||||
}) => {
|
||||
return (
|
||||
<ListItemContent subtitle={subtitle}>
|
||||
<Link className={classNames(className, 'focusable')} {...otherProps}>
|
||||
{children}
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
);
|
||||
};
|
||||
export const ListItemLink = polymorphicForwardRef<'h3', LinkProps>(
|
||||
({ as, subtitle, children, className, ...otherProps }, ref) => {
|
||||
return (
|
||||
<ListItemContent ref={ref} as={as} subtitle={subtitle}>
|
||||
<Link className={classNames(className, 'focusable')} {...otherProps}>
|
||||
{children}
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
interface ButtonProps
|
||||
extends React.ComponentPropsWithoutRef<'button'>, WithSubtitle {}
|
||||
extends React.ComponentPropsWithoutRef<'button'>, ContentProps {}
|
||||
|
||||
export const ListItemButton: React.FC<ButtonProps> = ({
|
||||
subtitle,
|
||||
children,
|
||||
className,
|
||||
...otherProps
|
||||
}) => {
|
||||
return (
|
||||
<ListItemContent subtitle={subtitle}>
|
||||
<button
|
||||
type='button'
|
||||
className={classNames(className, 'focusable')}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</ListItemContent>
|
||||
);
|
||||
};
|
||||
export const ListItemButton = polymorphicForwardRef<'h3', ButtonProps>(
|
||||
({ as, subtitle, children, className, ...otherProps }, ref) => {
|
||||
return (
|
||||
<ListItemContent as={as} ref={ref} subtitle={subtitle}>
|
||||
<button
|
||||
type='button'
|
||||
className={classNames(className, 'focusable')}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</ListItemContent>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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: () => (
|
||||
<ListItemWrapper
|
||||
icon={<Icon icon={VisibilityOffIcon} id='visibility' />}
|
||||
iconEnd={<Icon icon={KeyboardArrowDownIcon} id='down' />}
|
||||
sideContent={<Icon icon={KeyboardArrowDownIcon} id='down' />}
|
||||
>
|
||||
<ListItemButton subtitle='You’ve blocked or muted these users'>
|
||||
3 hidden accounts
|
||||
@@ -49,9 +51,22 @@ export const WithLink: Story = {
|
||||
render: () => (
|
||||
<ListItemWrapper
|
||||
icon={<Icon icon={VisibilityIcon} id='visibility' />}
|
||||
iconEnd={<Icon icon={ChevronRightIcon} id='right' />}
|
||||
sideContent={<Icon icon={ChevronRightIcon} id='right' />}
|
||||
>
|
||||
<ListItemLink to='/'>View more</ListItemLink>
|
||||
</ListItemWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithInteractiveSideContent: Story = {
|
||||
render: () => (
|
||||
<ListItemWrapper
|
||||
icon={<AvatarById accountId='1' size={40} />}
|
||||
sideContent={<Button compact>Follow</Button>}
|
||||
>
|
||||
<ListItemLink to='/' subtitle='@test@example.com'>
|
||||
Test account
|
||||
</ListItemLink>
|
||||
</ListItemWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useId, useState } from 'react';
|
||||
|
||||
import { Article } from '@/flavours/glitch/components/scrollable_list/components';
|
||||
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
|
||||
@@ -42,6 +42,8 @@ export const TruncatedListItems = <TListItem,>({
|
||||
toggleButton,
|
||||
renderListItem,
|
||||
}: TruncatedListProps<TListItem>) => {
|
||||
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 = <TListItem,>({
|
||||
});
|
||||
})}
|
||||
{hasHiddenAccounts && (
|
||||
<Article aria-posinset={initialListSize} aria-setsize={totalListLength}>
|
||||
<Article
|
||||
aria-posinset={initialListSize}
|
||||
aria-setsize={totalListLength}
|
||||
aria-labelledby={toggleButtonId}
|
||||
aria-describedby={toggleButtonDescId}
|
||||
>
|
||||
<ListItemWrapper
|
||||
icon={
|
||||
toggleButton.icon && (
|
||||
<Icon id='toggle-icon' icon={toggleButton.icon} />
|
||||
)
|
||||
}
|
||||
iconEnd={
|
||||
sideContent={
|
||||
<Icon
|
||||
id='open-status'
|
||||
icon={
|
||||
@@ -84,6 +91,8 @@ export const TruncatedListItems = <TListItem,>({
|
||||
}
|
||||
>
|
||||
<ListItemButton
|
||||
id={toggleButtonId}
|
||||
subtitleId={toggleButtonDescId}
|
||||
aria-expanded={showTruncatedItems}
|
||||
onClick={toggleTruncatedItems}
|
||||
subtitle={toggleButton.subtitle}
|
||||
|
||||
Reference in New Issue
Block a user