405 lines
15 KiB
JavaScript
405 lines
15 KiB
JavaScript
'use client';
|
|
import * as React from 'react';
|
|
import { shorthands, makeStyles, mergeClasses, makeResetStyles } from '@griffel/react';
|
|
import { tokens } from '@fluentui/react-theme';
|
|
import { textClassNames } from '@fluentui/react-text';
|
|
import { createFocusOutlineStyle } from '@fluentui/react-tabster';
|
|
import { cardPreviewClassNames } from '../CardPreview/useCardPreviewStyles.styles';
|
|
import { cardHeaderClassNames } from '../CardHeader/useCardHeaderStyles.styles';
|
|
import { cardFooterClassNames } from '../CardFooter/useCardFooterStyles.styles';
|
|
/**
|
|
* Static CSS class names used internally for the component slots.
|
|
*/ export const cardClassNames = {
|
|
root: 'fui-Card',
|
|
floatingAction: 'fui-Card__floatingAction',
|
|
checkbox: 'fui-Card__checkbox'
|
|
};
|
|
/**
|
|
* CSS variable names used internally for uniform styling in Card.
|
|
*/ export const cardCSSVars = {
|
|
cardSizeVar: '--fui-Card--size',
|
|
cardBorderRadiusVar: '--fui-Card--border-radius'
|
|
};
|
|
const focusOutlineStyle = {
|
|
outlineRadius: `var(${cardCSSVars.cardBorderRadiusVar})`,
|
|
outlineWidth: tokens.strokeWidthThick,
|
|
outlineOffset: '-2px'
|
|
};
|
|
const useCardResetStyles = makeResetStyles({
|
|
overflow: 'hidden',
|
|
borderRadius: `var(${cardCSSVars.cardBorderRadiusVar})`,
|
|
padding: `var(${cardCSSVars.cardSizeVar})`,
|
|
gap: `var(${cardCSSVars.cardSizeVar})`,
|
|
display: 'flex',
|
|
position: 'relative',
|
|
boxSizing: 'border-box',
|
|
color: tokens.colorNeutralForeground1,
|
|
// Border setting using after pseudo element to allow CardPreview to render behind it.
|
|
'::after': {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
content: '""',
|
|
pointerEvents: 'none',
|
|
...shorthands.borderStyle('solid'),
|
|
...shorthands.borderWidth(tokens.strokeWidthThin),
|
|
borderRadius: `var(${cardCSSVars.cardBorderRadiusVar})`
|
|
},
|
|
// Prevents CardHeader and CardFooter from shrinking.
|
|
[`> .${cardHeaderClassNames.root}, > .${cardFooterClassNames.root}`]: {
|
|
flexShrink: 0
|
|
}
|
|
});
|
|
const disabledStyles = {
|
|
cursor: 'not-allowed',
|
|
userSelect: 'none',
|
|
color: tokens.colorNeutralForegroundDisabled,
|
|
backgroundColor: tokens.colorNeutralBackgroundDisabled,
|
|
boxShadow: tokens.shadow2,
|
|
...shorthands.borderColor(tokens.colorNeutralStrokeDisabled),
|
|
'::before': {
|
|
content: '""',
|
|
position: 'absolute',
|
|
inset: 0,
|
|
zIndex: `calc(${tokens.zIndexContent} + 1)`
|
|
},
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorNeutralStrokeDisabled)
|
|
}
|
|
};
|
|
const useCardStyles = makeStyles({
|
|
focused: {
|
|
...createFocusOutlineStyle({
|
|
style: focusOutlineStyle,
|
|
selector: 'focus'
|
|
})
|
|
},
|
|
selectableFocused: createFocusOutlineStyle({
|
|
style: focusOutlineStyle,
|
|
selector: 'focus-within'
|
|
}),
|
|
orientationHorizontal: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
// Remove vertical padding to keep CardPreview content flush with Card's borders.
|
|
[`> .${cardPreviewClassNames.root}`]: {
|
|
marginTop: `calc(var(${cardCSSVars.cardSizeVar}) * -1)`,
|
|
marginBottom: `calc(var(${cardCSSVars.cardSizeVar}) * -1)`
|
|
},
|
|
// Due to Tabster's "Groupper" focus functionality, hidden elements are injected before and after Card's content.
|
|
// As such, the code below targets a CardPreview, when it's the first element.
|
|
// Since this is on horizontal cards, the left padding is removed to keep the content flush with the border.
|
|
[`> :not([aria-hidden="true"]).${cardPreviewClassNames.root}:first-of-type`]: {
|
|
marginLeft: `calc(var(${cardCSSVars.cardSizeVar}) * -1)`
|
|
},
|
|
// Due to Tabster's "Groupper" focus functionality, hidden elements are injected before and after Card's content.
|
|
// As such, the code below targets a CardPreview, when it's the last element.
|
|
// Since this is on horizontal cards, the right padding is removed to keep the content flush with the border.
|
|
[`> :not([aria-hidden="true"]).${cardPreviewClassNames.root}:last-of-type`]: {
|
|
marginRight: `calc(var(${cardCSSVars.cardSizeVar}) * -1)`
|
|
},
|
|
// If the last child is a CardHeader or CardFooter, allow it to grow to fill the available space.
|
|
[`> .${cardHeaderClassNames.root}:last-of-type, > .${cardFooterClassNames.root}:last-of-type`]: {
|
|
flexGrow: 1
|
|
}
|
|
},
|
|
orientationVertical: {
|
|
flexDirection: 'column',
|
|
// Remove lateral padding to keep CardPreview content flush with Card's borders.
|
|
[`> .${cardPreviewClassNames.root}`]: {
|
|
marginLeft: `calc(var(${cardCSSVars.cardSizeVar}) * -1)`,
|
|
marginRight: `calc(var(${cardCSSVars.cardSizeVar}) * -1)`
|
|
},
|
|
// Due to Tabster's "Groupper" focus functionality, hidden elements are injected before and after Card's content.
|
|
// As such, the code below targets a CardPreview, when it's the first element.
|
|
// Since this is on vertical cards, the top padding is removed to keep the content flush with the border.
|
|
[`> :not([aria-hidden="true"]).${cardPreviewClassNames.root}:first-of-type`]: {
|
|
marginTop: `calc(var(${cardCSSVars.cardSizeVar}) * -1)`
|
|
},
|
|
[`> .${cardClassNames.floatingAction} + .${cardPreviewClassNames.root}`]: {
|
|
marginTop: `calc(var(${cardCSSVars.cardSizeVar}) * -1)`
|
|
},
|
|
// Due to Tabster's "Groupper" focus functionality, hidden elements are injected before and after Card's content.
|
|
// As such, the code below targets a CardPreview, when it's the first element.
|
|
// Since this is on vertical cards, the bottom padding is removed to keep the content flush with the border.
|
|
[`> :not([aria-hidden="true"]).${cardPreviewClassNames.root}:last-of-type`]: {
|
|
marginBottom: `calc(var(${cardCSSVars.cardSizeVar}) * -1)`
|
|
}
|
|
},
|
|
sizeSmall: {
|
|
[cardCSSVars.cardSizeVar]: '8px',
|
|
[cardCSSVars.cardBorderRadiusVar]: tokens.borderRadiusSmall
|
|
},
|
|
sizeMedium: {
|
|
[cardCSSVars.cardSizeVar]: '12px',
|
|
[cardCSSVars.cardBorderRadiusVar]: tokens.borderRadiusMedium
|
|
},
|
|
sizeLarge: {
|
|
[cardCSSVars.cardSizeVar]: '16px',
|
|
[cardCSSVars.cardBorderRadiusVar]: tokens.borderRadiusLarge
|
|
},
|
|
interactive: {
|
|
[`& .${textClassNames.root}`]: {
|
|
color: 'currentColor'
|
|
}
|
|
},
|
|
filled: {
|
|
backgroundColor: tokens.colorNeutralBackground1,
|
|
boxShadow: tokens.shadow4,
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorTransparentStroke)
|
|
}
|
|
},
|
|
filledInteractive: {
|
|
cursor: 'pointer',
|
|
backgroundColor: tokens.colorNeutralBackground1,
|
|
boxShadow: tokens.shadow4,
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorTransparentStroke)
|
|
},
|
|
':hover': {
|
|
color: tokens.colorNeutralForeground1Hover,
|
|
backgroundColor: tokens.colorNeutralBackground1Hover,
|
|
boxShadow: tokens.shadow8
|
|
},
|
|
':active': {
|
|
backgroundColor: tokens.colorNeutralBackground1Pressed
|
|
}
|
|
},
|
|
filledInteractiveSelected: {
|
|
backgroundColor: tokens.colorNeutralBackground1Selected,
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorNeutralStroke1Selected)
|
|
},
|
|
':hover': {
|
|
color: tokens.colorNeutralForeground1Selected,
|
|
backgroundColor: tokens.colorNeutralBackground1Selected
|
|
}
|
|
},
|
|
filledAlternative: {
|
|
backgroundColor: tokens.colorNeutralBackground2,
|
|
boxShadow: tokens.shadow4,
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorTransparentStroke)
|
|
}
|
|
},
|
|
filledAlternativeInteractive: {
|
|
cursor: 'pointer',
|
|
backgroundColor: tokens.colorNeutralBackground2,
|
|
boxShadow: tokens.shadow4,
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorTransparentStroke)
|
|
},
|
|
':hover': {
|
|
color: tokens.colorNeutralForeground2Hover,
|
|
backgroundColor: tokens.colorNeutralBackground2Hover,
|
|
boxShadow: tokens.shadow8
|
|
},
|
|
':active': {
|
|
backgroundColor: tokens.colorNeutralBackground2Pressed
|
|
}
|
|
},
|
|
filledAlternativeInteractiveSelected: {
|
|
backgroundColor: tokens.colorNeutralBackground2Selected,
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorNeutralStroke1Selected)
|
|
},
|
|
':hover': {
|
|
color: tokens.colorNeutralForeground2Selected,
|
|
backgroundColor: tokens.colorNeutralBackground2Selected
|
|
}
|
|
},
|
|
outline: {
|
|
backgroundColor: tokens.colorTransparentBackground,
|
|
boxShadow: 'none',
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorNeutralStroke1)
|
|
}
|
|
},
|
|
outlineInteractive: {
|
|
cursor: 'pointer',
|
|
backgroundColor: tokens.colorTransparentBackground,
|
|
boxShadow: 'none',
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorNeutralStroke1)
|
|
},
|
|
':hover': {
|
|
color: tokens.colorNeutralForeground1Hover,
|
|
backgroundColor: tokens.colorTransparentBackgroundHover,
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorNeutralStroke1Hover)
|
|
}
|
|
},
|
|
':active': {
|
|
backgroundColor: tokens.colorTransparentBackgroundPressed,
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorNeutralStroke1Pressed)
|
|
}
|
|
}
|
|
},
|
|
outlineInteractiveSelected: {
|
|
backgroundColor: tokens.colorTransparentBackgroundSelected,
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorNeutralStroke1Selected)
|
|
},
|
|
':hover': {
|
|
color: tokens.colorNeutralForeground1Selected,
|
|
backgroundColor: tokens.colorTransparentBackgroundSelected
|
|
}
|
|
},
|
|
outlineDisabled: {
|
|
backgroundColor: tokens.colorTransparentBackground,
|
|
boxShadow: 'none',
|
|
...shorthands.borderColor(tokens.colorNeutralStrokeDisabled),
|
|
'&:hover, &:active': {
|
|
backgroundColor: tokens.colorTransparentBackground,
|
|
boxShadow: 'none'
|
|
},
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorNeutralStrokeDisabled)
|
|
}
|
|
},
|
|
subtle: {
|
|
backgroundColor: tokens.colorSubtleBackground,
|
|
boxShadow: 'none',
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorTransparentStroke)
|
|
}
|
|
},
|
|
subtleInteractive: {
|
|
cursor: 'pointer',
|
|
backgroundColor: tokens.colorSubtleBackground,
|
|
boxShadow: 'none',
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorTransparentStroke)
|
|
},
|
|
':hover': {
|
|
color: tokens.colorNeutralForeground1Hover,
|
|
backgroundColor: tokens.colorSubtleBackgroundHover
|
|
},
|
|
':active': {
|
|
backgroundColor: tokens.colorSubtleBackgroundPressed
|
|
}
|
|
},
|
|
subtleInteractiveSelected: {
|
|
backgroundColor: tokens.colorSubtleBackgroundSelected,
|
|
'::after': {
|
|
...shorthands.borderColor(tokens.colorNeutralStroke1Selected)
|
|
},
|
|
':hover': {
|
|
color: tokens.colorNeutralForeground1Selected,
|
|
backgroundColor: tokens.colorSubtleBackgroundSelected
|
|
}
|
|
},
|
|
highContrastSelected: {
|
|
'@media (forced-colors: active)': {
|
|
forcedColorAdjust: 'none',
|
|
backgroundColor: 'Highlight',
|
|
color: 'HighlightText',
|
|
[`& .${cardPreviewClassNames.root}, & .${cardFooterClassNames.root}`]: {
|
|
forcedColorAdjust: 'auto'
|
|
},
|
|
'::after': {
|
|
...shorthands.borderColor('Highlight')
|
|
}
|
|
}
|
|
},
|
|
highContrastInteractive: {
|
|
'@media (forced-colors: active)': {
|
|
':hover, :active': {
|
|
forcedColorAdjust: 'none',
|
|
backgroundColor: 'Highlight',
|
|
color: 'HighlightText',
|
|
[`& .${cardPreviewClassNames.root}, & .${cardFooterClassNames.root}`]: {
|
|
forcedColorAdjust: 'auto'
|
|
}
|
|
},
|
|
'::after': {
|
|
...shorthands.borderColor('Highlight')
|
|
}
|
|
}
|
|
},
|
|
select: {
|
|
position: 'absolute',
|
|
top: '4px',
|
|
right: '4px',
|
|
zIndex: tokens.zIndexContent
|
|
},
|
|
hiddenCheckbox: {
|
|
overflow: 'hidden',
|
|
width: '1px',
|
|
height: '1px',
|
|
position: 'absolute',
|
|
clip: 'rect(0 0 0 0)',
|
|
clipPath: 'inset(50%)',
|
|
whiteSpace: 'nowrap'
|
|
},
|
|
disabled: {
|
|
...disabledStyles,
|
|
'&:hover, &:active': disabledStyles
|
|
}
|
|
});
|
|
/**
|
|
* Apply styling to the Card slots based on the state.
|
|
*/ export const useCardStyles_unstable = (state)=>{
|
|
'use no memo';
|
|
const resetStyles = useCardResetStyles();
|
|
const styles = useCardStyles();
|
|
const orientationMap = {
|
|
horizontal: styles.orientationHorizontal,
|
|
vertical: styles.orientationVertical
|
|
};
|
|
const sizeMap = {
|
|
small: styles.sizeSmall,
|
|
medium: styles.sizeMedium,
|
|
large: styles.sizeLarge
|
|
};
|
|
const appearanceMap = {
|
|
filled: styles.filled,
|
|
'filled-alternative': styles.filledAlternative,
|
|
outline: styles.outline,
|
|
subtle: styles.subtle
|
|
};
|
|
const selectedMap = {
|
|
filled: styles.filledInteractiveSelected,
|
|
'filled-alternative': styles.filledAlternativeInteractiveSelected,
|
|
outline: styles.outlineInteractiveSelected,
|
|
subtle: styles.subtleInteractiveSelected
|
|
};
|
|
const interactiveMap = {
|
|
filled: styles.filledInteractive,
|
|
'filled-alternative': styles.filledAlternativeInteractive,
|
|
outline: styles.outlineInteractive,
|
|
subtle: styles.subtleInteractive
|
|
};
|
|
const isSelectableOrInteractive = !state.disabled && (state.interactive || state.selectable);
|
|
const focusedClassName = React.useMemo(()=>{
|
|
if (state.disabled) {
|
|
return '';
|
|
}
|
|
if (state.selectable) {
|
|
if (state.selectFocused) {
|
|
return styles.selectableFocused;
|
|
}
|
|
return '';
|
|
}
|
|
return styles.focused;
|
|
}, [
|
|
state.disabled,
|
|
state.selectFocused,
|
|
state.selectable,
|
|
styles.focused,
|
|
styles.selectableFocused
|
|
]);
|
|
state.root.className = mergeClasses(cardClassNames.root, resetStyles, orientationMap[state.orientation], sizeMap[state.size], appearanceMap[state.appearance], isSelectableOrInteractive && styles.interactive, isSelectableOrInteractive && interactiveMap[state.appearance], state.selected && selectedMap[state.appearance], focusedClassName, isSelectableOrInteractive && styles.highContrastInteractive, state.selected && styles.highContrastSelected, state.disabled && styles.disabled, state.disabled && state.appearance === 'outline' && styles.outlineDisabled, state.root.className);
|
|
if (state.floatingAction) {
|
|
state.floatingAction.className = mergeClasses(cardClassNames.floatingAction, styles.select, state.floatingAction.className);
|
|
}
|
|
if (state.checkbox) {
|
|
state.checkbox.className = mergeClasses(cardClassNames.checkbox, styles.hiddenCheckbox, state.checkbox.className);
|
|
}
|
|
return state;
|
|
};
|