0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-17 23:44:39 -05:00

Improved hover styling for list items in Settings (#21387)

fixes
https://linear.app/ghost/issue/DES-804/implement-new-hover-styling-for-table-rows-and-lists-in-settings

This adds new hover styling for list items in Recommendations,
Newsletter and Integrations settings.

---------

Co-authored-by: Fabien 'egg' O'Carroll <fabien@allou.is>
This commit is contained in:
Daniël van der Winden 2024-10-26 09:00:14 +02:00 committed by GitHub
parent 9dff9cc364
commit 6b7932ad9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 97 additions and 64 deletions

View file

@ -42,28 +42,36 @@ const ListItem: React.FC<ListItemProps> = ({
}; };
const listItemClasses = clsx( const listItemClasses = clsx(
'group/list-item flex items-center justify-between', 'group/list-item relative flex items-center justify-between',
bgOnHover && 'hover:bg-gradient-to-r hover:from-white hover:to-grey-50 dark:hover:from-black dark:hover:to-grey-950', bgOnHover && 'hover:bg-grey-50 dark:hover:bg-grey-950',
separator ? 'border-b border-grey-100 last-of-type:border-b-transparent hover:border-grey-200 dark:border-grey-900 dark:hover:border-grey-800' : 'border-y border-transparent hover:border-grey-200 first-of-type:hover:border-t-transparent dark:hover:border-grey-800', separator ? 'border-b border-grey-100 last-of-type:border-b-transparent dark:border-grey-900' : 'border-y border-transparent',
onClick && 'cursor-pointer before:absolute before:inset-0 before:content-[""]',
'hover:z-10 hover:border-b-transparent',
'-mb-px pb-px',
className className
); );
return ( return (
<div className={listItemClasses} data-testid={testId}> <div className={listItemClasses} data-testid={testId} onClick={handleClick}>
{children ? children : {bgOnHover && (
<div className={`flex grow items-center gap-3 ${onClick && 'cursor-pointer'}`} onClick={handleClick}> <div className="absolute inset-0 -z-10 -mx-4 rounded-lg bg-grey-50 opacity-0 group-hover/list-item:opacity-100 dark:bg-grey-950" />
{avatar && avatar} )}
<div className={`flex grow flex-col py-3 pr-6`} id={id}> <div className="relative flex w-full items-center justify-between">
<span>{title}</span> {children ? children :
{detail && <span className='text-xs text-grey-700'>{detail}</span>} <div className={`flex grow items-center gap-3`}>
{avatar && avatar}
<div className={`flex grow flex-col py-3 pr-6`} id={id}>
<span>{title}</span>
{detail && <span className='text-xs text-grey-700'>{detail}</span>}
</div>
</div> </div>
</div> }
} {action &&
{action && <div className={`visible py-3 md:pl-2 ${paddingRight && 'md:pr-2'} ${hideActions ? 'group-hover/list-item:visible md:invisible' : ''}`}>
<div className={`visible py-3 md:pl-6 ${paddingRight && 'md:pr-6'} ${hideActions ? 'group-hover/list-item:visible md:invisible' : ''}`}> {action}
{action} </div>
</div> }
} </div>
</div> </div>
); );
}; };

View file

@ -78,7 +78,7 @@ export const TabList: React.FC<TabListProps> = ({
topRightContent topRightContent
}) => { }) => {
const containerClasses = clsx( const containerClasses = clsx(
'no-scrollbar flex w-full overflow-x-auto', 'no-scrollbar mb-px flex w-full overflow-x-auto',
width === 'narrow' && 'gap-3', width === 'narrow' && 'gap-3',
width === 'normal' && 'gap-5', width === 'normal' && 'gap-5',
width === 'wide' && 'gap-7', width === 'wide' && 'gap-7',

View file

@ -1,7 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import React, {forwardRef} from 'react'; import React, {forwardRef} from 'react';
export const tableRowHoverBgClasses = 'hover:bg-gradient-to-r hover:from-white hover:to-grey-50 dark:hover:from-black dark:hover:to-grey-950'; export const tableRowHoverBgClasses = 'before:absolute before:inset-x-[-16px] before:top-[-1px] before:bottom-0 before:bg-grey-50 before:opacity-0 hover:before:opacity-100 before:rounded-md before:transition-opacity dark:before:bg-grey-950 hover:z-10';
export interface TableRowProps { export interface TableRowProps {
id?: string; id?: string;
@ -28,23 +28,26 @@ const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(function TableRo
separator = (separator === undefined) ? true : separator; separator = (separator === undefined) ? true : separator;
const tableRowClasses = clsx( const tableRowClasses = clsx(
'group/table-row', 'group/table-row relative',
bgOnHover && tableRowHoverBgClasses, bgOnHover && tableRowHoverBgClasses,
onClick && 'cursor-pointer', onClick && 'cursor-pointer',
separator ? 'border-b border-grey-100 last-of-type:border-b-transparent hover:border-grey-200 dark:border-grey-950 dark:hover:border-grey-900' : 'border-y border-none first-of-type:hover:border-t-transparent', separator ? 'border-b border-grey-100 last-of-type:border-b-transparent dark:border-grey-950' : 'border-y border-none first-of-type:hover:border-t-transparent',
'hover:border-b-transparent',
className className
); );
return ( return (
<tr ref={ref} className={tableRowClasses} data-testid={testId} id={id} style={style} onClick={handleClick}> <tr ref={ref} className={tableRowClasses} data-testid={testId} id={id} style={style} onClick={handleClick}>
{children} <td className="p-0" colSpan={1000}>
{action && <div className="relative z-10 flex items-center">
<td className={`w-[1%] whitespace-nowrap p-0 hover:cursor-pointer`}> <div className="grow py-2">{children}</div>
<div className={`visible flex items-center justify-end py-3 pr-6 ${hideActions ? 'group-hover/table-row:visible md:invisible' : ''}`}> {action &&
{action} <div className={`flex items-center justify-end p-2${hideActions ? ' opacity-0 group-hover/table-row:opacity-100' : ''}`}>
</div> {action}
</td> </div>
} }
</div>
</td>
</tr> </tr>
); );
}); });

View file

@ -40,7 +40,10 @@ const IntegrationItem: React.FC<IntegrationItemProps> = ({
}) => { }) => {
const {updateRoute} = useRouting(); const {updateRoute} = useRouting();
const handleClick = () => { const handleClick = (e?: React.MouseEvent<HTMLElement>) => {
// Prevent the click event from bubbling up when clicking the delete button
e?.stopPropagation();
if (disabled) { if (disabled) {
updateRoute({route: 'pro', isExternal: true}); updateRoute({route: 'pro', isExternal: true});
} else { } else {
@ -48,8 +51,13 @@ const IntegrationItem: React.FC<IntegrationItemProps> = ({
} }
}; };
const handleDelete = (e?: React.MouseEvent<HTMLElement>) => {
e?.stopPropagation();
onDelete?.();
};
const buttons = custom ? const buttons = custom ?
<Button color='red' label='Delete' link onClick={onDelete} /> <Button color='red' label='Delete' link onClick={handleDelete} />
: :
(disabled ? (disabled ?
<Button icon='lock-locked' label='Upgrade' link onClick={handleClick} /> : <Button icon='lock-locked' label='Upgrade' link onClick={handleClick} /> :

View file

@ -26,12 +26,11 @@ const NewsletterItemContainer: React.FC<Partial<SortableItemContainerProps>> = (
}; };
const container = ( const container = (
<TableRow <TableRow ref={setRef}
ref={setRef} action={<Button color='green' data-testid="edit-newsletter-button" label='Edit' link onClick={showDetails} />}
action={<Button color='green' label='Edit' link onClick={showDetails} />}
className={isDragging ? 'opacity-75' : ''} className={isDragging ? 'opacity-75' : ''}
hideActions={false}
style={style} style={style}
hideActions
onClick={showDetails} onClick={showDetails}
> >
{(props.dragHandleAttributes || isDragging) && <TableCell className='w-10 !align-middle' > {(props.dragHandleAttributes || isDragging) && <TableCell className='w-10 !align-middle' >

View file

@ -53,25 +53,31 @@ const IncomingRecommendationItem: React.FC<{incomingRecommendation: IncomingReco
</div> </div>
) )
} testId='incoming-recommendation-list-item' hideActions> } testId='incoming-recommendation-list-item' hideActions>
<TableCell onClick={showDetails}> <TableCell className='w-80' onClick={showDetails}>
<div className='group flex items-center gap-3 hover:cursor-pointer'> <div className='group flex items-center gap-3 hover:cursor-pointer'>
<div className={`flex grow flex-col`}> <div className={`flex grow flex-col`}>
<div className="mb-0.5 flex items-center gap-3"> <div className="flex items-center gap-3">
<RecommendationIcon favicon={incomingRecommendation.favicon} featured_image={incomingRecommendation.featured_image} title={incomingRecommendation.title || incomingRecommendation.url} /> <RecommendationIcon favicon={incomingRecommendation.favicon} featured_image={incomingRecommendation.featured_image} title={incomingRecommendation.title || incomingRecommendation.url} />
<span className='line-clamp-1 font-medium'>{incomingRecommendation.title || incomingRecommendation.url}</span> <span className='line-clamp-1 font-medium'>{incomingRecommendation.title || incomingRecommendation.url}</span>
</div> </div>
</div> </div>
</div> </div>
</TableCell> </TableCell>
<TableCell className='hidden w-[1%] whitespace-nowrap !pr-1 pl-0 text-right align-middle md:!visible md:!table-cell' padding={false} onClick={showDetails}> <TableCell className='hidden w-auto whitespace-nowrap text-left align-middle md:!visible md:!table-cell' padding={false} onClick={showDetails}>
{(signups === 0) ? (<span className="text-grey-500 dark:text-grey-900">-</span>) : (<div className='-mt-px text-right'> {(signups === 0) ? (
<span className='text-right'>{numberWithCommas(signups)}</span> <span className="text-grey-500 dark:text-grey-900">-</span>
</div>)} ) : (
<div className='mr-2'>
<span>{numberWithCommas(signups)}</span>
</div>
)}
</TableCell> </TableCell>
<TableCell className='hidden w-[1%] whitespace-nowrap align-middle md:!visible md:!table-cell' onClick={showDetails}> <TableCell className='hidden w-[1%] whitespace-nowrap align-middle md:!visible md:!table-cell' onClick={showDetails}>
{(signups === 0) ? (null) : (<div className='-mt-px text-left'> {(signups === 0) ? (null) : (
<span className='-mb-px inline-block min-w-[60px] whitespace-nowrap text-left text-sm lowercase text-grey-700'>{freeMembersLabel}</span> <div className='-mt-px text-left'>
</div>)} <span className='-mb-px inline-block min-w-[60px] whitespace-nowrap text-left text-sm lowercase text-grey-700'>{freeMembersLabel}</span>
</div>
)}
</TableCell> </TableCell>
{incomingRecommendation.recommending_back && <TableCell className='w-[1%] whitespace-nowrap group-hover/table-row:visible md:invisible'><div className='mt-1 whitespace-nowrap text-right text-sm text-grey-700'>Recommending</div></TableCell>} {incomingRecommendation.recommending_back && <TableCell className='w-[1%] whitespace-nowrap group-hover/table-row:visible md:invisible'><div className='mt-1 whitespace-nowrap text-right text-sm text-grey-700'>Recommending</div></TableCell>}
</TableRow> </TableRow>

View file

@ -35,26 +35,31 @@ const RecommendationItem: React.FC<{recommendation: Recommendation}> = ({recomme
const clicks = count === 1 ? 'click' : 'clicks'; const clicks = count === 1 ? 'click' : 'clicks';
return ( return (
<TableRow testId='recommendation-list-item'> <TableRow className='group hover:cursor-pointer' testId='recommendation-list-item' onClick={showDetails}>
<TableCell onClick={showDetails}> <TableCell className='w-80'>
<div className='group flex items-center gap-3 hover:cursor-pointer'> <div className='flex items-center gap-3'>
<div className={`flex grow flex-col`}> <RecommendationIcon isGhostSite={isGhostSite} {...recommendation} />
<div className="mb-0.5 flex items-center gap-3"> <span className='line-clamp-1 font-medium'>{recommendation.title}</span>
<RecommendationIcon isGhostSite={isGhostSite} {...recommendation} />
<span className='line-clamp-1 font-medium'>{recommendation.title}</span>
</div>
</div>
</div> </div>
</TableCell> </TableCell>
<TableCell className='hidden w-[1%] whitespace-nowrap !pr-1 pl-0 text-right align-middle md:!visible md:!table-cell' padding={false} onClick={showDetails}> <TableCell
{(count === 0) ? (<span className="text-grey-500 dark:text-grey-900">-</span>) : (<div className='-mt-px items-end gap-1 text-right'> className='hidden w-auto whitespace-nowrap text-left align-middle md:!visible md:!table-cell'
<span className='text-right'>{numberWithCommas(count)}</span> >
</div>)} {count === 0 ? (
</TableCell> <span className="text-grey-500 dark:text-grey-900">-</span>
<TableCell className='hidden align-middle md:!visible md:!table-cell' onClick={showDetails}> ) : (
{(count === 0) ? (null) : (<div className=''> <>
<span className='min-w-[60px] whitespace-nowrap text-left text-sm lowercase text-grey-700'>{showSubscribers ? newMembers : clicks}</span><span className='whitespace-nowrap text-left text-sm lowercase text-grey-700 opacity-0 transition-opacity group-hover/table-row:opacity-100'> from you</span> <div className='flex items-center'>
</div>)} <div className='mr-2'>
<span>{numberWithCommas(count)}</span>
</div>
<div className='text-sm lowercase text-grey-700'>
<span>{showSubscribers ? newMembers : clicks}</span>
<span className='invisible group-hover:visible'> from you</span>
</div>
</div>
</>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
); );

View file

@ -342,9 +342,13 @@ test.describe('Newsletter settings', async () => {
}] }]
}} }}
}}); }});
const awesomeNewsletterRow = section.getByRole('row', {name: /Awesome newsletter/});
await awesomeNewsletterRow.hover();
await section.getByText('Awesome newsletter').hover(); const editButton = awesomeNewsletterRow.getByTestId('edit-newsletter-button');
await section.getByRole('button', {name: 'Edit'}).click(); await editButton.waitFor({state: 'visible', timeout: 5000});
await editButton.click();
const activeNewsletterModal = page.getByTestId('newsletter-modal'); const activeNewsletterModal = page.getByTestId('newsletter-modal');
await activeNewsletterModal.getByRole('button', {name: 'Archive newsletter'}).click(); await activeNewsletterModal.getByRole('button', {name: 'Archive newsletter'}).click();