Fix for scrollable body section
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -30,12 +30,29 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/*
|
||||
* Cursor and hover tint are applied at the card level so they cover the
|
||||
* entire surface — including CardBody which sits above the overlay.
|
||||
*/
|
||||
.bui-Card[data-interactive] {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: color-mix(in srgb, currentColor 5%, transparent);
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-CardOverlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
@@ -48,7 +65,10 @@
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
&:hover::after,
|
||||
/*
|
||||
* Keep focus tint for keyboard navigation (hover tint has moved to the
|
||||
* card container above).
|
||||
*/
|
||||
&[data-focused]::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -60,13 +80,24 @@
|
||||
}
|
||||
|
||||
/*
|
||||
* Interactive elements inside the card must sit above the overlay so they
|
||||
* remain independently clickable. The overlay handles the rest of the surface.
|
||||
* CardBody and interactive elements must sit above the overlay (z-index: 1)
|
||||
* so that:
|
||||
* - scroll events reach CardBody's overflow container
|
||||
* - buttons/links/inputs remain independently clickable
|
||||
* Text clicks in CardBody that bypass the overlay are caught by the
|
||||
* container-level onClick in Card.tsx.
|
||||
*/
|
||||
.bui-Card[data-interactive]
|
||||
:is(button, a[href], [role='button'], [role='link'], input, select, textarea):not(
|
||||
.bui-CardOverlay
|
||||
) {
|
||||
:is(
|
||||
button,
|
||||
a[href],
|
||||
[role='button'],
|
||||
[role='link'],
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
.bui-CardBody
|
||||
):not(.bui-CardOverlay) {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -231,6 +231,39 @@ export const BgOnProviders = meta.story({
|
||||
),
|
||||
});
|
||||
|
||||
export const InteractiveWithScrollableBody = meta.story({
|
||||
render: () => (
|
||||
<Card
|
||||
style={{ width: '300px', height: '220px' }}
|
||||
onPress={() => alert('Card pressed')}
|
||||
label="View details"
|
||||
>
|
||||
<CardHeader>
|
||||
<Text weight="bold">Scrollable Interactive Card</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Text>
|
||||
This is the first paragraph of a long body text that demonstrates how
|
||||
the Card component handles extensive content. The card should adjust
|
||||
accordingly to display all the text properly while maintaining its
|
||||
structure.
|
||||
</Text>
|
||||
<Text>
|
||||
Here's a second paragraph that adds more content to our card body.
|
||||
Having multiple paragraphs helps to visualize how spacing works within
|
||||
the card component.
|
||||
</Text>
|
||||
<Text>
|
||||
This third paragraph continues to add more text to ensure we have a
|
||||
proper demonstration of a card with significant content. This makes it
|
||||
easier to test scrolling behavior and overall layout when content
|
||||
exceeds the initial view.
|
||||
</Text>
|
||||
</CardBody>
|
||||
</Card>
|
||||
),
|
||||
});
|
||||
|
||||
export const Interactive = meta.story({
|
||||
render: () => (
|
||||
<Card
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { forwardRef } from 'react';
|
||||
import { forwardRef, useCallback } from 'react';
|
||||
import { Button as RAButton, Link as RALink } from 'react-aria-components';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import {
|
||||
@@ -46,6 +46,29 @@ export const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
|
||||
const { classes, children, onPress, href, label } = ownProps;
|
||||
const isInteractive = !!(onPress || href);
|
||||
|
||||
// CardBody sits above the overlay (z-index: 2) so that scroll events reach
|
||||
// its overflow container. As a result, text clicks inside CardBody bypass
|
||||
// the overlay and bubble up to here instead. We fire the card action only
|
||||
// when the click did not originate from a nested interactive element.
|
||||
const { onClick: userOnClick, ...restPropsWithoutClick } =
|
||||
restProps as React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const handleContainerClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
userOnClick?.(e);
|
||||
if (!isInteractive) return;
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target.closest?.(
|
||||
'button, a[href], input, select, textarea, [role="button"], [role="link"]',
|
||||
)
|
||||
)
|
||||
return;
|
||||
onPress?.();
|
||||
},
|
||||
[userOnClick, isInteractive, onPress],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
bg="neutral"
|
||||
@@ -53,16 +76,23 @@ export const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
|
||||
className={classes.root}
|
||||
data-interactive={isInteractive || undefined}
|
||||
{...dataAttributes}
|
||||
{...restProps}
|
||||
{...restPropsWithoutClick}
|
||||
onClick={handleContainerClick}
|
||||
>
|
||||
{href && (
|
||||
<RALink className={classes.overlay} href={href} aria-label={label} />
|
||||
<RALink
|
||||
className={classes.overlay}
|
||||
href={href}
|
||||
aria-label={label}
|
||||
onClick={e => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
{onPress && !href && (
|
||||
<RAButton
|
||||
className={classes.overlay}
|
||||
onPress={onPress}
|
||||
aria-label={label}
|
||||
onClick={e => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user