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:
parent
9dff9cc364
commit
6b7932ad9e
8 changed files with 97 additions and 64 deletions
|
@ -42,28 +42,36 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||
};
|
||||
|
||||
const listItemClasses = clsx(
|
||||
'group/list-item 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',
|
||||
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',
|
||||
'group/list-item relative flex items-center justify-between',
|
||||
bgOnHover && 'hover:bg-grey-50 dark:hover:bg-grey-950',
|
||||
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
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={listItemClasses} data-testid={testId}>
|
||||
{children ? children :
|
||||
<div className={`flex grow items-center gap-3 ${onClick && 'cursor-pointer'}`} onClick={handleClick}>
|
||||
{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 className={listItemClasses} data-testid={testId} onClick={handleClick}>
|
||||
{bgOnHover && (
|
||||
<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" />
|
||||
)}
|
||||
<div className="relative flex w-full items-center justify-between">
|
||||
{children ? children :
|
||||
<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>
|
||||
}
|
||||
{action &&
|
||||
<div className={`visible py-3 md:pl-6 ${paddingRight && 'md:pr-6'} ${hideActions ? 'group-hover/list-item:visible md:invisible' : ''}`}>
|
||||
{action}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
{action &&
|
||||
<div className={`visible py-3 md:pl-2 ${paddingRight && 'md:pr-2'} ${hideActions ? 'group-hover/list-item:visible md:invisible' : ''}`}>
|
||||
{action}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -78,7 +78,7 @@ export const TabList: React.FC<TabListProps> = ({
|
|||
topRightContent
|
||||
}) => {
|
||||
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 === 'normal' && 'gap-5',
|
||||
width === 'wide' && 'gap-7',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
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 {
|
||||
id?: string;
|
||||
|
@ -28,23 +28,26 @@ const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(function TableRo
|
|||
|
||||
separator = (separator === undefined) ? true : separator;
|
||||
const tableRowClasses = clsx(
|
||||
'group/table-row',
|
||||
'group/table-row relative',
|
||||
bgOnHover && tableRowHoverBgClasses,
|
||||
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
|
||||
);
|
||||
|
||||
return (
|
||||
<tr ref={ref} className={tableRowClasses} data-testid={testId} id={id} style={style} onClick={handleClick}>
|
||||
{children}
|
||||
{action &&
|
||||
<td className={`w-[1%] whitespace-nowrap p-0 hover:cursor-pointer`}>
|
||||
<div className={`visible flex items-center justify-end py-3 pr-6 ${hideActions ? 'group-hover/table-row:visible md:invisible' : ''}`}>
|
||||
{action}
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
<td className="p-0" colSpan={1000}>
|
||||
<div className="relative z-10 flex items-center">
|
||||
<div className="grow py-2">{children}</div>
|
||||
{action &&
|
||||
<div className={`flex items-center justify-end p-2${hideActions ? ' opacity-0 group-hover/table-row:opacity-100' : ''}`}>
|
||||
{action}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -40,7 +40,10 @@ const IntegrationItem: React.FC<IntegrationItemProps> = ({
|
|||
}) => {
|
||||
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) {
|
||||
updateRoute({route: 'pro', isExternal: true});
|
||||
} else {
|
||||
|
@ -48,8 +51,13 @@ const IntegrationItem: React.FC<IntegrationItemProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleDelete = (e?: React.MouseEvent<HTMLElement>) => {
|
||||
e?.stopPropagation();
|
||||
onDelete?.();
|
||||
};
|
||||
|
||||
const buttons = custom ?
|
||||
<Button color='red' label='Delete' link onClick={onDelete} />
|
||||
<Button color='red' label='Delete' link onClick={handleDelete} />
|
||||
:
|
||||
(disabled ?
|
||||
<Button icon='lock-locked' label='Upgrade' link onClick={handleClick} /> :
|
||||
|
|
|
@ -26,12 +26,11 @@ const NewsletterItemContainer: React.FC<Partial<SortableItemContainerProps>> = (
|
|||
};
|
||||
|
||||
const container = (
|
||||
<TableRow
|
||||
ref={setRef}
|
||||
action={<Button color='green' label='Edit' link onClick={showDetails} />}
|
||||
<TableRow ref={setRef}
|
||||
action={<Button color='green' data-testid="edit-newsletter-button" label='Edit' link onClick={showDetails} />}
|
||||
className={isDragging ? 'opacity-75' : ''}
|
||||
hideActions={false}
|
||||
style={style}
|
||||
hideActions
|
||||
onClick={showDetails}
|
||||
>
|
||||
{(props.dragHandleAttributes || isDragging) && <TableCell className='w-10 !align-middle' >
|
||||
|
|
|
@ -53,25 +53,31 @@ const IncomingRecommendationItem: React.FC<{incomingRecommendation: IncomingReco
|
|||
</div>
|
||||
)
|
||||
} 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={`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} />
|
||||
<span className='line-clamp-1 font-medium'>{incomingRecommendation.title || incomingRecommendation.url}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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}>
|
||||
{(signups === 0) ? (<span className="text-grey-500 dark:text-grey-900">-</span>) : (<div className='-mt-px text-right'>
|
||||
<span className='text-right'>{numberWithCommas(signups)}</span>
|
||||
</div>)}
|
||||
<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='mr-2'>
|
||||
<span>{numberWithCommas(signups)}</span>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<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'>
|
||||
<span className='-mb-px inline-block min-w-[60px] whitespace-nowrap text-left text-sm lowercase text-grey-700'>{freeMembersLabel}</span>
|
||||
</div>)}
|
||||
{(signups === 0) ? (null) : (
|
||||
<div className='-mt-px text-left'>
|
||||
<span className='-mb-px inline-block min-w-[60px] whitespace-nowrap text-left text-sm lowercase text-grey-700'>{freeMembersLabel}</span>
|
||||
</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>
|
||||
|
|
|
@ -35,26 +35,31 @@ const RecommendationItem: React.FC<{recommendation: Recommendation}> = ({recomme
|
|||
const clicks = count === 1 ? 'click' : 'clicks';
|
||||
|
||||
return (
|
||||
<TableRow testId='recommendation-list-item'>
|
||||
<TableCell onClick={showDetails}>
|
||||
<div className='group flex items-center gap-3 hover:cursor-pointer'>
|
||||
<div className={`flex grow flex-col`}>
|
||||
<div className="mb-0.5 flex items-center gap-3">
|
||||
<RecommendationIcon isGhostSite={isGhostSite} {...recommendation} />
|
||||
<span className='line-clamp-1 font-medium'>{recommendation.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
<TableRow className='group hover:cursor-pointer' testId='recommendation-list-item' onClick={showDetails}>
|
||||
<TableCell className='w-80'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<RecommendationIcon isGhostSite={isGhostSite} {...recommendation} />
|
||||
<span className='line-clamp-1 font-medium'>{recommendation.title}</span>
|
||||
</div>
|
||||
</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}>
|
||||
{(count === 0) ? (<span className="text-grey-500 dark:text-grey-900">-</span>) : (<div className='-mt-px items-end gap-1 text-right'>
|
||||
<span className='text-right'>{numberWithCommas(count)}</span>
|
||||
</div>)}
|
||||
</TableCell>
|
||||
<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>)}
|
||||
<TableCell
|
||||
className='hidden w-auto whitespace-nowrap text-left align-middle md:!visible md:!table-cell'
|
||||
>
|
||||
{count === 0 ? (
|
||||
<span className="text-grey-500 dark:text-grey-900">-</span>
|
||||
) : (
|
||||
<>
|
||||
<div className='flex items-center'>
|
||||
<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>
|
||||
</TableRow>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
await section.getByRole('button', {name: 'Edit'}).click();
|
||||
const editButton = awesomeNewsletterRow.getByTestId('edit-newsletter-button');
|
||||
await editButton.waitFor({state: 'visible', timeout: 5000});
|
||||
await editButton.click();
|
||||
|
||||
const activeNewsletterModal = page.getByTestId('newsletter-modal');
|
||||
await activeNewsletterModal.getByRole('button', {name: 'Archive newsletter'}).click();
|
||||
|
|
Loading…
Add table
Reference in a new issue