Skip to content

Commit

Permalink
react-card - Phase 2 (microsoft#20132)
Browse files Browse the repository at this point in the history
* Add useGroupper

* Change files

* Update API

* Rename hook

* Update API

* Rename enum

* Change options prop

* react-card - Migrate Card to new prop merging / root as a slot (microsoft#20111)

* Add focus keyboard interactions

* Update stories to have context menus

* Change files

* Remove context examples because Typescript hates me

* Update snapshots

* Use new hook name

* Formatting

* revert(react-card): remove tabster behavior from card footer

* fix: update snapshots

Co-authored-by: varholak-peter <[email protected]>
  • Loading branch information
2 people authored and Marion Le Pontois committed Jan 17, 2022
1 parent ede5f86 commit cbab338
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 73 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Add keyboard focus interactions",
"packageName": "@fluentui/react-card",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Add useFocusableGroup hook",
"packageName": "@fluentui/react-tabster",
"email": "[email protected]",
"dependentChangeType": "patch"
}
1 change: 1 addition & 0 deletions packages/react-card/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"dependencies": {
"@fluentui/react-make-styles": "9.0.0-beta.1",
"@fluentui/react-utilities": "9.0.0-beta.1",
"@fluentui/react-tabster": "9.0.0-beta.1",
"tslib": "^2.1.0"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ exports[`Card renders a default state 1`] = `
<div>
<div
class=""
data-tabster="{\\"groupper\\":{\\"tabbability\\":2}}"
role="group"
>
Default Card
Expand Down
6 changes: 6 additions & 0 deletions packages/react-card/src/components/Card/useCard.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { getNativeElementProps } from '@fluentui/react-utilities';
import type { CardProps, CardState } from './Card.types';
import { useFocusableGroup, FocusableGroupTabBehavior } from '@fluentui/react-tabster';

/**
* Create the state required to render Card.
Expand All @@ -12,12 +13,17 @@ import type { CardProps, CardState } from './Card.types';
* @param ref - reference to root HTMLElement of Card
*/
export const useCard = (props: CardProps, ref: React.Ref<HTMLElement>): CardState => {
const groupperAttrs = useFocusableGroup({
tabBehavior: FocusableGroupTabBehavior.LimitedTrapFocus,
});

return {
components: { root: 'div' },

root: getNativeElementProps(props.as || 'div', {
ref,
role: 'group',
...groupperAttrs,
...props,
}),
};
Expand Down
160 changes: 87 additions & 73 deletions packages/react-card/src/stories/Card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,25 @@ import { Button } from '@fluentui/react-button';
import {
Open16Regular,
ArrowReply16Regular,
DismissSquare20Regular,
MoreHorizontal16Regular,
MoreVertical20Regular,
Share16Regular,
} from '@fluentui/react-icons';
import { makeStyles } from '@fluentui/react-make-styles';
import { Card, CardFooter, CardHeader, CardPreview } from '../index';

const useStyles = makeStyles({
root: {
actionCard: {
minWidth: '368px',
},
gridViewCard: {
minWidth: '254px',
maxWidth: '368px',
},
gray: {
color: 'gray',
},
logo: {
background: 'white',
padding: '6px',
width: '20px',
Expand All @@ -27,90 +37,94 @@ const useStyles = makeStyles({
const LogoBackground = (props: React.HTMLAttributes<HTMLElement>) => {
const styles = useStyles();

return <div className={styles.root}>{props.children}</div>;
return <div className={styles.logo}>{props.children}</div>;
};

export const ActionCard = () => (
<>
<Card style={{ minWidth: '368px' }} onClick={() => console.log('Test action')}>
<CardHeader
image={<img src="./avatar_elvia.svg" alt="Face of a person" />}
header={
<Body>
<b>Elvia Atkins</b> mentioned you
</Body>
}
description={<Caption>5h ago · About us - Overview</Caption>}
/>
export const ActionCard = () => {
const styles = useStyles();

<CardPreview
logo={
<LogoBackground>
<img src="./word_logo.svg" alt="Microsoft Word logo" />
</LogoBackground>
}
>
<img src="./doc_template.png" alt="Preview of a Word document " />
</CardPreview>
return (
<>
<Card tabIndex={0} className={styles.actionCard}>
<CardHeader
image={<img src="./avatar_elvia.svg" alt="Face of a person" />}
header={
<Body>
<b>Elvia Atkins</b> mentioned you
</Body>
}
description={<Caption>5h ago · About us - Overview</Caption>}
/>

<CardFooter>
<Button icon={<ArrowReply16Regular />}>Reply</Button>
</CardFooter>
</Card>
<CardPreview
logo={
<LogoBackground>
<img src="./word_logo.svg" alt="Microsoft Word logo" />
</LogoBackground>
}
>
<img src="./doc_template.png" alt="Preview of a Word document " />
</CardPreview>

<CardFooter>
<Button icon={<ArrowReply16Regular />}>Reply</Button>
<Button icon={<Share16Regular />}>Share</Button>
</CardFooter>
</Card>

<br />
<br />

<Card style={{ minWidth: '368px' }}>
<Card tabIndex={0} className={styles.actionCard} onClick={() => console.log('Test action')}>
<CardHeader
image={<img src="./avatar_mauricio.svg" alt="Face of a person" />}
header={
<Body>
<b>Mauricio August</b> <span className={styles.gray}>+ 7 others edited</span>
</Body>
}
description={<Caption>Artificial Intelligence Deck</Caption>}
action={<Button appearance="transparent" icon={<MoreVertical20Regular />} />}
/>

<CardPreview
logo={
<LogoBackground>
<img src="./powerpoint_logo.svg" alt="Microsoft PowerPoint logo" />
</LogoBackground>
}
>
<img src="./ai_deck_template.png" alt="Preview of an artificial intelligence slide deck" />
</CardPreview>

<CardFooter>
<Button icon={<Open16Regular />}>View changes</Button>
</CardFooter>
</Card>
</>
);
};

export const GridviewCard = () => {
const styles = useStyles();

return (
<Card className={styles.gridViewCard}>
<CardPreview>
<img src="./sales_template.png" alt="Preview of a sales slide deck" />
</CardPreview>
<CardHeader
image={<img src="./avatar_mauricio.svg" alt="Face of a person" />}
image={<img src="./powerpoint_logo.svg" alt="Microsoft PowerPoint logo" />}
header={
<Body>
<b>Mauricio August</b> <span style={{ color: 'gray' }}>+ 7 others edited</span>
<b>Sales Analysis</b>
</Body>
}
description={<Caption>Artificial Intelligence Deck</Caption>}
action={<DismissSquare20Regular />}
description={<Caption className={styles.gray}>Elvia replied to a comment</Caption>}
action={<Button appearance="transparent" icon={<MoreHorizontal16Regular />} />}
/>

<CardPreview
logo={
<LogoBackground>
<img src="./powerpoint_logo.svg" alt="Microsoft PowerPoint logo" />
</LogoBackground>
}
>
<img src="./ai_deck_template.png" alt="Preview of an artificial intelligence slide deck" />
</CardPreview>

<CardFooter action={<Button appearance="transparent" icon={<MoreVertical20Regular />} />}>
<Button icon={<Open16Regular />}>View changes</Button>
</CardFooter>
</Card>
</>
);

export const GridviewCard = () => (
<Card
style={{
minWidth: '254px',
maxWidth: '368px',
}}
>
<CardPreview>
<img src="./sales_template.png" alt="Preview of a sales slide deck" />
</CardPreview>
<CardHeader
image={<img src="./powerpoint_logo.svg" alt="Microsoft PowerPoint logo" />}
header={
<Body>
<b>Sales Analysis</b>
</Body>
}
description={<Caption style={{ color: 'gray' }}>Elvia replied to a comment</Caption>}
action={<MoreHorizontal16Regular />}
/>
</Card>
);
);
};

export default {
title: 'Components/OfficeCard',
Expand Down
15 changes: 15 additions & 0 deletions packages/react-tabster/etc/react-tabster.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ export const createFocusOutlineStyle: (theme: Theme, options?: {
style: Partial<FocusOutlineStyleOptions>;
} & CreateFocusIndicatorStyleRuleOptions) => MakeStyles;

// @public (undocumented)
export enum FocusableGroupTabBehavior {
Limited,
LimitedTrapFocus,
Unlimited
}

// @public (undocumented)
export type FocusOutlineOffset = Record<'top' | 'bottom' | 'left' | 'right', string>;

Expand All @@ -45,6 +52,14 @@ export interface UseArrowNavigationGroupOptions {
memorizeCurrent?: boolean;
}

// @public
export const useFocusableGroup: (options?: UseFocusableGroupOptions | undefined) => Types.TabsterDOMAttribute;

// @public (undocumented)
export interface UseFocusableGroupOptions {
tabBehavior?: FocusableGroupTabBehavior;
}

// @public
export const useFocusFinders: () => {
findAllFocusable: (container: HTMLElement, acceptCondition: (el: HTMLElement) => boolean) => HTMLElement[];
Expand Down
1 change: 1 addition & 0 deletions packages/react-tabster/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './useModalAttributes';
export * from './useTabsterAttributes';
export * from './useFocusIndicatorStyle';
export * from './useKeyboardNavAttribute';
export * from './useFocusableGroup';
43 changes: 43 additions & 0 deletions packages/react-tabster/src/hooks/useFocusableGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Types, getGroupper } from 'tabster';
import { useTabsterAttributes } from './useTabsterAttributes';
import { useTabster } from './useTabster';

export enum FocusableGroupTabBehavior {
/**
* Tab will cycle into and out of the groupper content.
*/
Unlimited = Types.GroupperTabbabilities.Unlimited,
/**
* Tab will cycle out of the container, but not into it.
*/
Limited = Types.GroupperTabbabilities.Limited,
/**
* Tab only cycles the inner elements.
*/
LimitedTrapFocus = Types.GroupperTabbabilities.LimitedTrapFocus,
}

export interface UseFocusableGroupOptions {
/**
* Type of TAB key interaction.
*/
tabBehavior?: FocusableGroupTabBehavior;
}

/**
* A hook that returns the necessary tabster attributes to support groupping.
* @param options - Options to configure keyboard navigation
*/
export const useFocusableGroup = (options?: UseFocusableGroupOptions) => {
const tabster = useTabster();

if (tabster) {
getGroupper(tabster);
}

return useTabsterAttributes({
groupper: {
tabbability: options?.tabBehavior as Types.GroupperTabbability,
},
});
};

0 comments on commit cbab338

Please sign in to comment.