From 86dad78ebb98407cc4dff5d3faee18beea04b561 Mon Sep 17 00:00:00 2001 From: Marcos Moura Date: Tue, 29 Nov 2022 16:13:01 +0100 Subject: [PATCH] feat: implement accessibility features for selectable cards --- ...-08d5a2c2-97f3-4d5a-ad91-bc9410d5a749.json | 6 + ...-eb4bcf69-423b-47f8-854a-a214ede6a002.json | 7 + .../react-card/etc/react-card.api.md | 14 +- .../src/components/Card/Card.cy.tsx | 46 +++---- .../react-card/src/components/Card/Card.tsx | 4 +- .../src/components/Card/Card.types.ts | 23 ++-- .../src/components/Card/renderCard.tsx | 2 +- .../react-card/src/components/Card/useCard.ts | 37 +++-- .../src/components/Card/useCardSelectable.ts | 41 +++--- .../src/components/Card/useCardStyles.ts | 109 +++++++-------- .../react-components/react-card/src/index.ts | 2 +- .../stories/Card/CardBestPractices.md | 10 ++ .../stories/Card/CardDescription.md | 13 -- .../stories/Card/CardInteractive.stories.tsx | 129 ------------------ .../stories/Card/CardOrientation.stories.tsx | 5 +- .../react-card/stories/Card/CardPreview.md | 12 ++ .../stories/Card/CardSelectable.stories.tsx | 4 +- .../react-card/stories/Card/index.stories.tsx | 5 +- 18 files changed, 191 insertions(+), 278 deletions(-) create mode 100644 change/@fluentui-react-card-08d5a2c2-97f3-4d5a-ad91-bc9410d5a749.json create mode 100644 change/@fluentui-react-card-eb4bcf69-423b-47f8-854a-a214ede6a002.json create mode 100644 packages/react-components/react-card/stories/Card/CardBestPractices.md delete mode 100644 packages/react-components/react-card/stories/Card/CardInteractive.stories.tsx create mode 100644 packages/react-components/react-card/stories/Card/CardPreview.md diff --git a/change/@fluentui-react-card-08d5a2c2-97f3-4d5a-ad91-bc9410d5a749.json b/change/@fluentui-react-card-08d5a2c2-97f3-4d5a-ad91-bc9410d5a749.json new file mode 100644 index 0000000000000..38e19fc73980a --- /dev/null +++ b/change/@fluentui-react-card-08d5a2c2-97f3-4d5a-ad91-bc9410d5a749.json @@ -0,0 +1,6 @@ +{ + "type": "prerelease", + "packageName": "@fluentui/react-card", + "email": "marcosvmmoura@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-card-eb4bcf69-423b-47f8-854a-a214ede6a002.json b/change/@fluentui-react-card-eb4bcf69-423b-47f8-854a-a214ede6a002.json new file mode 100644 index 0000000000000..4492bdf15639b --- /dev/null +++ b/change/@fluentui-react-card-eb4bcf69-423b-47f8-854a-a214ede6a002.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "fix selectable card accessibility", + "packageName": "@fluentui/react-card", + "email": "marcosvmmoura@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-card/etc/react-card.api.md b/packages/react-components/react-card/etc/react-card.api.md index 90acb0348e8ac..5b77a4a1effeb 100644 --- a/packages/react-components/react-card/etc/react-card.api.md +++ b/packages/react-components/react-card/etc/react-card.api.md @@ -67,6 +67,9 @@ export type CardHeaderSlots = { // @public export type CardHeaderState = ComponentState; +// @public +export type CardOnSelectionChangeEvent = React_2.MouseEvent | React_2.KeyboardEvent | React_2.ChangeEvent; + // @public export const CardPreview: ForwardRefComponent; @@ -93,12 +96,13 @@ export type CardProps = ComponentProps & { size?: 'small' | 'medium' | 'large'; selected?: boolean; defaultSelected?: boolean; - onSelectionChange?: (event: CarOnSelectionChangeEvent, data: CardOnSelectData) => void; + onSelectionChange?: (event: CardOnSelectionChangeEvent, data: CardOnSelectData) => void; + selectableProps?: React_2.InputHTMLAttributes; }; // @public export type CardSlots = { - root: Slot<'div', 'a' | 'button'>; + root: Slot<'div'>; select?: Slot<'div', 'input'>; }; @@ -108,11 +112,9 @@ export type CardState = ComponentState & Required; -// @public -export type CarOnSelectionChangeEvent = React_2.MouseEvent | React_2.KeyboardEvent | React_2.ChangeEvent; - // @public export const renderCard_unstable: (state: CardState) => JSX.Element; @@ -126,7 +128,7 @@ export const renderCardHeader_unstable: (state: CardHeaderState) => JSX.Element; export const renderCardPreview_unstable: (state: CardPreviewState) => JSX.Element; // @public -export const useCard_unstable: (props: CardProps, ref: React_2.Ref) => CardState; +export const useCard_unstable: (props: CardProps, ref: React_2.Ref) => CardState; // @public export const useCardFooter_unstable: (props: CardFooterProps, ref: React_2.Ref) => CardFooterState; diff --git a/packages/react-components/react-card/src/components/Card/Card.cy.tsx b/packages/react-components/react-card/src/components/Card/Card.cy.tsx index 0c0e461aa57fa..15da57eec8499 100644 --- a/packages/react-components/react-card/src/components/Card/Card.cy.tsx +++ b/packages/react-components/react-card/src/components/Card/Card.cy.tsx @@ -268,19 +268,19 @@ describe('Card', () => { cy.get(`.${cardClassNames.select}`).should('not.exist'); }); - it('should be checked when prop is present - selected prop', () => { + it('should render select slot - selected prop', () => { mountFluent(); - cy.get('#card').should('have.attr', 'aria-checked', 'true'); + cy.get(`.${cardClassNames.select}`).should('exist'); }); - it('should be checked when prop is present - defaultSelected prop', () => { + it('should render select slot - defaultSelected prop', () => { mountFluent(); - cy.get('#card').should('have.attr', 'aria-checked', 'true'); + cy.get(`.${cardClassNames.select}`).should('exist'); }); - it('should not be checked when prop is present - onSelectionChange prop', () => { + it('should render select slot - onSelectionChange prop', () => { const Example = () => { const onSelectionChange = React.useCallback(() => null, []); @@ -289,15 +289,21 @@ describe('Card', () => { mountFluent(); - cy.get('#card').should('have.attr', 'aria-checked', 'false'); + cy.get(`.${cardClassNames.select}`).should('exist'); }); - it('should have internal checkbox when selectable - select prop', () => { + it('should render select slot custom JSX is provided', () => { mountFluent(} />); cy.get(`.${cardClassNames.select}`).should('exist'); }); + it('should have internal checkbox when selectable - no select slot', () => { + mountFluent(); + + cy.get(`.${cardClassNames.select}`).should('exist'); + }); + it('should render custom select slot', () => { mountFluent(} />); @@ -307,46 +313,38 @@ describe('Card', () => { it('should select with a mouse click', () => { mountFluent(); - cy.get(`.${cardClassNames.root}`).should('have.attr', 'aria-checked', 'false'); + cy.get(`.${cardClassNames.select}`).should('not.be.checked'); cy.get(`.${cardClassNames.root}`).realClick(); - cy.get(`.${cardClassNames.root}`).should('have.attr', 'aria-checked', 'true'); + cy.get(`.${cardClassNames.select}`).should('be.checked'); }); it('should have checkbox pre-selected and toggle its value', () => { mountFluent(); - cy.get(`.${cardClassNames.root}`).should('have.attr', 'aria-checked', 'true'); + cy.get(`.${cardClassNames.select}`).should('be.checked'); cy.get(`.${cardClassNames.root}`).realClick(); - cy.get(`.${cardClassNames.root}`).should('have.attr', 'aria-checked', 'false'); + cy.get(`.${cardClassNames.select}`).should('not.be.checked'); }); it('should select with the Space key', () => { mountFluent(); - cy.get(`.${cardClassNames.root}`).focus().realPress('Space'); - cy.get(`.${cardClassNames.root}`).should('have.attr', 'aria-checked', 'true'); - }); - - it('should NOT select with the Enter key if card has any actions inside', () => { - mountFluent(); - - cy.get(`.${cardClassNames.root}`).focus().realPress('Enter'); - cy.get(`.${cardClassNames.root}`).should('have.attr', 'aria-checked', 'false'); - cy.get(`.${cardClassNames.root} button`).first().should('be.focused'); + cy.get(`.${cardClassNames.select}`).focus().realPress('Space'); + cy.get(`.${cardClassNames.select}`).should('be.checked'); }); it('should NOT select when focused on an action', () => { mountFluent(); cy.get(`.${cardClassNames.root} button`).first().focus().realPress('Enter'); - cy.get(`.${cardClassNames.root}`).should('have.attr', 'aria-checked', 'false'); + cy.get(`.${cardClassNames.select}`).should('not.be.checked'); }); it('should select with the Enter key if card does not have any actions inside', () => { mountFluent(); - cy.get(`.${cardClassNames.root}`).focus().realPress('Enter'); - cy.get(`.${cardClassNames.root}`).should('have.attr', 'aria-checked', 'true'); + cy.get(`.${cardClassNames.select}`).focus().realPress('Enter'); + cy.get(`.${cardClassNames.select}`).should('be.checked'); }); it('should sync selected value with custom slot', () => { diff --git a/packages/react-components/react-card/src/components/Card/Card.tsx b/packages/react-components/react-card/src/components/Card/Card.tsx index 689224d62e2c8..b1ba61861dadf 100644 --- a/packages/react-components/react-card/src/components/Card/Card.tsx +++ b/packages/react-components/react-card/src/components/Card/Card.tsx @@ -2,13 +2,13 @@ import * as React from 'react'; import { useCard_unstable } from './useCard'; import { renderCard_unstable } from './renderCard'; import { useCardStyles_unstable } from './useCardStyles'; -import type { CardProps, CardRefElement } from './Card.types'; +import type { CardProps } from './Card.types'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; /** * A card provides scaffolding for hosting actions and content for a single topic. */ -export const Card: ForwardRefComponent = React.forwardRef((props, ref) => { +export const Card: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useCard_unstable(props, ref); useCardStyles_unstable(state); diff --git a/packages/react-components/react-card/src/components/Card/Card.types.ts b/packages/react-components/react-card/src/components/Card/Card.types.ts index e58baf745065c..b7921ee816ea5 100644 --- a/packages/react-components/react-card/src/components/Card/Card.types.ts +++ b/packages/react-components/react-card/src/components/Card/Card.types.ts @@ -1,17 +1,12 @@ import * as React from 'react'; import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; -/** - * Card refs to the root element slot. - */ -export type CardRefElement = HTMLDivElement | HTMLButtonElement | HTMLAnchorElement; - /** * Card selected event type * * This event is fired when a selectable card changes its selection state. */ -export type CarOnSelectionChangeEvent = React.MouseEvent | React.KeyboardEvent | React.ChangeEvent; +export type CardOnSelectionChangeEvent = React.MouseEvent | React.KeyboardEvent | React.ChangeEvent; /** * Data sent from the selection events on a selectable card. @@ -27,7 +22,7 @@ export type CardSlots = { /** * Root element of the component. */ - root: Slot<'div', 'a' | 'button'>; + root: Slot<'div'>; /** * Select element represents a checkbox. @@ -111,7 +106,12 @@ export type CardProps = ComponentProps & { /** * Callback to be called when the selected state value changes. */ - onSelectionChange?: (event: CarOnSelectionChangeEvent, data: CardOnSelectData) => void; + onSelectionChange?: (event: CardOnSelectionChangeEvent, data: CardOnSelectData) => void; + + /** + * Properties passed to the internal checkbox element. + */ + selectableProps?: React.InputHTMLAttributes; }; /** @@ -147,5 +147,12 @@ export type CardState = ComponentState & * @default false */ selected: boolean; + + /** + * Defines whether the card internal checkbox is currently focused. + * + * @default false + */ + selectFocused: boolean; } >; diff --git a/packages/react-components/react-card/src/components/Card/renderCard.tsx b/packages/react-components/react-card/src/components/Card/renderCard.tsx index 992f29e80c1d8..e36e88773b0db 100644 --- a/packages/react-components/react-card/src/components/Card/renderCard.tsx +++ b/packages/react-components/react-card/src/components/Card/renderCard.tsx @@ -10,8 +10,8 @@ export const renderCard_unstable = (state: CardState) => { return ( + {slots.select && state.selectable ? : null} {slotProps.root.children} - {state.hasSelectSlot && slots.select ? : null} ); }; diff --git a/packages/react-components/react-card/src/components/Card/useCard.ts b/packages/react-components/react-card/src/components/Card/useCard.ts index ca10f3bbfa22e..7815213be719a 100644 --- a/packages/react-components/react-card/src/components/Card/useCard.ts +++ b/packages/react-components/react-card/src/components/Card/useCard.ts @@ -1,8 +1,8 @@ import * as React from 'react'; -import { getNativeElementProps } from '@fluentui/react-utilities'; -import { useFocusableGroup } from '@fluentui/react-tabster'; +import { getNativeElementProps, useMergedRefs } from '@fluentui/react-utilities'; +import { useFocusableGroup, useFocusWithin } from '@fluentui/react-tabster'; -import type { CardProps, CardRefElement, CardState } from './Card.types'; +import type { CardProps, CardState } from './Card.types'; import { useCardSelectable } from './useCardSelectable'; const focusMap = { @@ -42,16 +42,24 @@ const useCardFocusAttributes = ({ focusMode = 'off' }: CardProps, { interactive * @param props - props from this instance of Card * @param ref - reference to the root element of Card */ -export const useCard_unstable = (props: CardProps, ref: React.Ref): CardState => { - const { appearance = 'filled', orientation = 'vertical', size = 'medium', as = 'div' } = props; - const cardRef = React.useRef(null); +export const useCard_unstable = (props: CardProps, ref: React.Ref): CardState => { + const { appearance = 'filled', orientation = 'vertical', size = 'medium' } = props; - const { selectable, hasSelectSlot, selected, selectableSlot, selectableProps } = useCardSelectable(props, cardRef); + const cardBaseRef = useFocusWithin(); + const { + selectable, + hasSelectSlot, + selected, + selectableSlot, + selectableProps, + selectableTag, + selectFocused, + } = useCardSelectable(props, cardBaseRef); + + const cardRef = useMergedRefs(cardBaseRef, ref); const interactive = Boolean( - selectable || - ['a', 'button'].includes(as) || - props.onClick || + props.onClick || props.onDoubleClick || props.onMouseUp || props.onMouseDown || @@ -70,15 +78,16 @@ export const useCard_unstable = (props: CardProps, ref: React.Ref) => { +export const useCardSelectable = (props: CardProps, cardRef: React.RefObject) => { const { select, selected, defaultSelected, onSelectionChange } = props; const { findAllFocusable } = useFocusFinders(); @@ -15,9 +15,11 @@ export const useCardSelectable = (props: CardProps, cardRef: React.RefObject { + (event: CardOnSelectionChangeEvent) => { if (!cardRef.current) { return false; } @@ -29,10 +31,10 @@ export const useCardSelectable = (props: CardProps, cardRef: React.RefObject { + (event: CardOnSelectionChangeEvent) => { if (shouldRestrictTriggerAction(event)) { return; } @@ -64,24 +66,23 @@ export const useCardSelectable = (props: CardProps, cardRef: React.RefObject { if (!hasSelectSlot) { - return undefined; + return getNativeElementProps>('input', { + ...props.selectableProps, + ref: selectableRef, + type: 'checkbox', + checked: isCardSelected, + onChange: (event: React.ChangeEvent) => onChangeHandler(event), + onFocus: () => setIsSelectFocused(true), + onBlur: () => setIsSelectFocused(false), + }); } return resolveShorthand(select, { @@ -89,7 +90,7 @@ export const useCardSelectable = (props: CardProps, cardRef: React.RefObject setIsCardSelected(Boolean(defaultSelected ?? selected)), [ defaultSelected, @@ -100,6 +101,8 @@ export const useCardSelectable = (props: CardProps, cardRef: React.RefObject :not(.${cardPreviewClassNames.root}):not(.${cardHeaderClassNames.root}):not(.${cardFooterClassNames.root})`]: { flexGrow: 1, }, - - ...createFocusOutlineStyle({ - style: { - outlineRadius: `var(${cardCSSVars.cardBorderRadiusVar})`, - outlineWidth: tokens.strokeWidthThick, - }, - selector: 'focus', - }), }, + selectableFocused: createFocusOutlineStyle({ + style: focusOutlineStyle, + selector: 'focus-within', + }), + orientationHorizontal: { flexDirection: 'row', alignItems: 'center', @@ -80,9 +83,15 @@ const useStyles = makeStyles({ // 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"]):first-of-type.${cardPreviewClassNames.root}`]: { + [`> :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)`, + }, }, orientationVertical: { flexDirection: 'column', @@ -92,12 +101,23 @@ const useStyles = makeStyles({ 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"]):first-of-type.${cardPreviewClassNames.root}`]: { + [`> :not([aria-hidden="true"]).${cardPreviewClassNames.root}:first-of-type`]: { marginTop: `calc(var(${cardCSSVars.cardSizeVar}) * -1)`, }, + [`> .${cardClassNames.select} + .${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: { @@ -113,21 +133,6 @@ const useStyles = makeStyles({ [cardCSSVars.cardBorderRadiusVar]: tokens.borderRadiusLarge, }, - interactiveLink: { - textDecorationLine: 'none', - }, - interactiveButton: { - ...shorthands.border('0'), - width: '100%', - alignItems: 'normal', - appearance: 'none', - lineHeight: 'inherit', - fontFamily: 'inherit', - fontSize: 'inherit', - fontWeight: 'inherit', - textAlign: 'start', - }, - filled: { backgroundColor: tokens.colorNeutralBackground1, boxShadow: tokens.shadow4, @@ -286,43 +291,20 @@ const useStyles = makeStyles({ position: 'absolute', top: '4px', right: '4px', + zIndex: 1, }, selectHidden: { - width: 0, - height: 0, + ...shorthands.overflow('hidden'), + width: '1px', + height: '1px', position: 'absolute', - top: 0, - right: 0, + clip: 'rect(0 0 0 0)', + clipPath: 'inset(50%)', + whiteSpace: 'nowrap', }, }); -const getInteractiveClassnames = (state: CardState, styles: ReturnType) => { - 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 baseClass = mergeClasses(interactiveMap[state.appearance], state.selected && selectedMap[state.appearance]); - - if (state.components.root === 'button') { - return mergeClasses(baseClass, styles.interactiveButton); - } - - if (state.components.root === 'a') { - return mergeClasses(baseClass, styles.interactiveLink); - } - - return baseClass; -}; - /** * Apply styling to the Card slots based on the state. */ @@ -347,13 +329,28 @@ export const useCardStyles_unstable = (state: CardState): CardState => { 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, + }; + state.root.className = mergeClasses( cardClassNames.root, styles.root, orientationMap[state.orientation], sizeMap[state.size], appearanceMap[state.appearance], - state.interactive && getInteractiveClassnames(state, styles), + (state.interactive || state.selectable) && interactiveMap[state.appearance], + state.selected && selectedMap[state.appearance], + state.selectFocused && styles.selectableFocused, state.root.className, ); diff --git a/packages/react-components/react-card/src/index.ts b/packages/react-components/react-card/src/index.ts index 668b1ab45b78f..16fbcf0ea3443 100644 --- a/packages/react-components/react-card/src/index.ts +++ b/packages/react-components/react-card/src/index.ts @@ -6,7 +6,7 @@ export { useCardStyles_unstable, useCard_unstable, } from './Card'; -export type { CardProps, CardSlots, CardState, CarOnSelectionChangeEvent } from './Card'; +export type { CardProps, CardSlots, CardState, CardOnSelectionChangeEvent } from './Card'; export { CardFooter, cardFooterClassNames, diff --git a/packages/react-components/react-card/stories/Card/CardBestPractices.md b/packages/react-components/react-card/stories/Card/CardBestPractices.md new file mode 100644 index 0000000000000..124400955ebb8 --- /dev/null +++ b/packages/react-components/react-card/stories/Card/CardBestPractices.md @@ -0,0 +1,10 @@ +
+ + Best Practices + + +### Accessibility + +- By default, each card is of role="group". +- Provide meaningful `aria-label`, `aria-describedby` and `aria-labelledby` whenever needed, specially for selectable cards. +-
diff --git a/packages/react-components/react-card/stories/Card/CardDescription.md b/packages/react-components/react-card/stories/Card/CardDescription.md index 01d3bcaa45bdd..4f7a02021958c 100644 --- a/packages/react-components/react-card/stories/Card/CardDescription.md +++ b/packages/react-components/react-card/stories/Card/CardDescription.md @@ -1,14 +1 @@ The Card component is a framework for organizing content within the confines of a container. It's main function is to provide the scaffolding for hosting actions and content for a single topic within a card. - - - -> **⚠️ Preview components are considered unstable:** -> -> ```jsx -> -> import { Card } from '@fluentui/react-components/unstable'; -> -> ``` -> -> - Features and APIs may change before final release -> - Please contact us if you intend to use this in your product diff --git a/packages/react-components/react-card/stories/Card/CardInteractive.stories.tsx b/packages/react-components/react-card/stories/Card/CardInteractive.stories.tsx deleted file mode 100644 index 97216137ed73e..0000000000000 --- a/packages/react-components/react-card/stories/Card/CardInteractive.stories.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import * as React from 'react'; -import { - makeStyles, - shorthands, - tokens, - Caption1, - Subtitle1, - Avatar, - Text, - mergeClasses, -} from '@fluentui/react-components'; -import { Card, CardHeader, CardPreview, CardProps } from '@fluentui/react-card'; -import { Comment16Regular } from '@fluentui/react-icons'; - -const resolveAsset = (asset: string) => { - const ASSET_URL = - 'https://raw.githubusercontent.com/microsoft/fluentui/master/packages/react-components/react-card/stories/assets/'; - - return `${ASSET_URL}${asset}`; -}; - -const useStyles = makeStyles({ - main: { - ...shorthands.gap('36px'), - display: 'flex', - flexDirection: 'column', - flexWrap: 'wrap', - }, - - title: { - ...shorthands.margin(0, 0, '12px'), - }, - - card: { - width: '300px', - maxWidth: '100%', - height: 'fit-content', - }, - - caption: { - color: tokens.colorNeutralForeground3, - }, - - flexContainer: { - columnGap: '4px', - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }, - - appIcon: { - ...shorthands.borderRadius('4px'), - height: '32px', - }, - logoBadge: { - ...shorthands.padding('5px'), - ...shorthands.borderRadius(tokens.borderRadiusSmall), - backgroundColor: '#FFF', - boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12)', - }, -}); - -const Title = ({ children }: React.PropsWithChildren<{}>) => { - const styles = useStyles(); - - return ( - - {children} - - ); -}; - -const CardExample = (props: CardProps) => { - const styles = useStyles(); - - return ( - - }> - file preview - - - } - header={Classroom Collaboration} - description={ - - - Colin replied to a comment - - } - /> - - ); -}; - -export const Interactive = () => { - const styles = useStyles(); - - const onClickEvent = () => { - console.log('Interactive when has onClick event'); - }; - - return ( -
-
- As a link - -
- -
- As a button - -
- -
- With a MouseEvent - -
-
- ); -}; - -Interactive.parameters = { - docs: { - description: { - story: 'The card surface can be used as an interactive target, either with mouse or keyboard interaction.', - }, - }, -}; diff --git a/packages/react-components/react-card/stories/Card/CardOrientation.stories.tsx b/packages/react-components/react-card/stories/Card/CardOrientation.stories.tsx index 297271444ee41..420c289d58e8e 100644 --- a/packages/react-components/react-card/stories/Card/CardOrientation.stories.tsx +++ b/packages/react-components/react-card/stories/Card/CardOrientation.stories.tsx @@ -26,7 +26,8 @@ const useStyles = makeStyles({ ...shorthands.margin(0, 0, '12px'), }, - horizontalCard: { + horizontalCardImage: { + width: '60px', height: '60px', }, @@ -87,7 +88,7 @@ export const Orientation = () => { 'horizontal' - + Company Logo diff --git a/packages/react-components/react-card/stories/Card/CardPreview.md b/packages/react-components/react-card/stories/Card/CardPreview.md new file mode 100644 index 0000000000000..a5fac42c5f4ca --- /dev/null +++ b/packages/react-components/react-card/stories/Card/CardPreview.md @@ -0,0 +1,12 @@ + + +> **⚠️ Preview components are considered unstable:** +> +> ```jsx +> +> import { Card } from '@fluentui/react-components/unstable'; +> +> ``` +> +> - Features and APIs may change before final release +> - Please contact us if you intend to use this in your product diff --git a/packages/react-components/react-card/stories/Card/CardSelectable.stories.tsx b/packages/react-components/react-card/stories/Card/CardSelectable.stories.tsx index eb518c25896dc..055991d826412 100644 --- a/packages/react-components/react-card/stories/Card/CardSelectable.stories.tsx +++ b/packages/react-components/react-card/stories/Card/CardSelectable.stories.tsx @@ -58,7 +58,7 @@ const CardExample = (props: CardProps) => { iOS App Prototype} description={You created 53m ago} - action={