Skip to content

Commit

Permalink
feat: Observed element hooks (#28291)
Browse files Browse the repository at this point in the history
* feat: Observed element hooks

Implements hooks that enable the use of `observedElement` API in
react-tabster

Fixes #28030

* changefiles

* fix md

* Add tabster documentation

* add useModalAttribute docs

* improve observable examples

* improve focusable group examples

* improve arrow navigation group

* add focus finders

* Update packages/react-components/react-components/stories/Concepts/FocusManagement/useFocusFinders/FindPrevious.stories.tsx

* fix type check

* improve styles

* improve docs

* Add utilities section
  • Loading branch information
ling1726 authored Jun 27, 2023
1 parent 56143ef commit 1c7e645
Show file tree
Hide file tree
Showing 37 changed files with 1,534 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: Observed element hooks",
"packageName": "@fluentui/react-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: Observed element hooks",
"packageName": "@fluentui/react-tabster",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,7 @@ import { useFluentProviderStyles_unstable } from '@fluentui/react-provider';
import { useFocusableGroup } from '@fluentui/react-tabster';
import { UseFocusableGroupOptions } from '@fluentui/react-tabster';
import { useFocusFinders } from '@fluentui/react-tabster';
import { useFocusObserved } from '@fluentui/react-tabster';
import { useFocusWithin } from '@fluentui/react-tabster';
import { useId } from '@fluentui/react-utilities';
import { useImage_unstable } from '@fluentui/react-image';
Expand Down Expand Up @@ -921,6 +922,7 @@ import { useMenuTriggerContext_unstable } from '@fluentui/react-menu';
import { useMergedRefs } from '@fluentui/react-utilities';
import { useModalAttributes } from '@fluentui/react-tabster';
import { UseModalAttributesOptions } from '@fluentui/react-tabster';
import { useObservedElement } from '@fluentui/react-tabster';
import { useOption_unstable } from '@fluentui/react-combobox';
import { useOptionGroup_unstable } from '@fluentui/react-combobox';
import { useOptionGroupStyles_unstable } from '@fluentui/react-combobox';
Expand Down Expand Up @@ -2757,6 +2759,8 @@ export { UseFocusableGroupOptions }

export { useFocusFinders }

export { useFocusObserved }

export { useFocusWithin }

export { useId }
Expand Down Expand Up @@ -2859,6 +2863,8 @@ export { useModalAttributes }

export { UseModalAttributesOptions }

export { useObservedElement }

export { useOption_unstable }

export { useOptionGroup_unstable }
Expand Down
2 changes: 2 additions & 0 deletions packages/react-components/react-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export {
useFocusWithin,
useKeyboardNavAttribute,
useModalAttributes,
useObservedElement,
useFocusObserved,
} from '@fluentui/react-tabster';
export type {
CreateCustomFocusIndicatorStyleOptions,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as React from 'react';
import {
TextBoldRegular,
TextUnderlineRegular,
TextItalicRegular,
TextAlignLeftRegular,
TextAlignCenterRegular,
TextAlignRightRegular,
CopyRegular,
ClipboardPasteRegular,
CutRegular,
} from '@fluentui/react-icons';
import {
Button,
useArrowNavigationGroup,
Field,
RadioGroup,
Radio,
UseArrowNavigationGroupOptions,
makeStyles,
shorthands,
mergeClasses,
} from '@fluentui/react-components';

const useStyles = makeStyles({
container: {
display: 'flex',
...shorthands.gap('5px'),
},

vertical: {
flexDirection: 'column',
},

both: {},
horizontal: {},
grid: {
display: 'grid',
gridTemplateColumns: 'repeat(3, max-content)',
},
['grid-linear']: {
display: 'grid',
gridTemplateColumns: 'repeat(3, max-content)',
},
});

export const Axis = () => {
const styles = useStyles();
const [axis, setAxis] = React.useState<UseArrowNavigationGroupOptions['axis']>('horizontal');
const atributes = useArrowNavigationGroup({ axis });

return (
<>
<Field label="Select an axis">
<RadioGroup value={axis} onChange={(e, data) => setAxis(data.value as UseArrowNavigationGroupOptions['axis'])}>
<Radio label="Horizontal" value="horizontal" />
<Radio label="Vertical" value="vertical" />
<Radio label="Both" value="both" />
<Radio label="Grid" value="grid" />
<Radio label="Linear Grid" value="grid-linear" />
</RadioGroup>
</Field>
<div
aria-label="Editor toolbar example"
role="toolbar"
{...atributes}
className={mergeClasses(styles.container, axis && styles[axis])}
>
<Button aria-label="Bold" icon={<TextBoldRegular />} />
<Button aria-label="Underline" icon={<TextUnderlineRegular />} />
<Button aria-label="Italic" icon={<TextItalicRegular />} />
<Button aria-label="Align Left" icon={<TextAlignLeftRegular />} />
<Button aria-label="Align Center" icon={<TextAlignCenterRegular />} />
<Button aria-label="Align Right" icon={<TextAlignRightRegular />} />
<Button aria-label="Copy" icon={<CopyRegular />} />
<Button aria-label="Cut" icon={<CutRegular />} />
<Button aria-label="Paste" icon={<ClipboardPasteRegular />} />
</div>
</>
);
};

Axis.parameters = {
docs: {
description: {
story: [
'Keyboard navigation can be configured for different axis:',
'- horizontal - navigation with left/right keys',
'- vertical - navigation with up/downkeys',
'- both - navigation with all arrow keys, left/down and right/up will navigate in the same direction',
'- grid - bidirectional navigation in a 2D grid',
'- grid-liear - same as grid navigation, but horizontal focus will continue to flow to the next row',
].join('\n'),
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as React from 'react';
import {
TextBoldRegular,
TextUnderlineRegular,
TextItalicRegular,
TextAlignLeftRegular,
TextAlignCenterRegular,
TextAlignRightRegular,
CopyRegular,
ClipboardPasteRegular,
CutRegular,
} from '@fluentui/react-icons';
import { Button, useArrowNavigationGroup, makeStyles, shorthands, Checkbox } from '@fluentui/react-components';

const useStyles = makeStyles({
container: {
display: 'flex',
...shorthands.gap('5px'),
},
});

export const CircularNavigation = () => {
const styles = useStyles();
const [checked, setChecked] = React.useState(true);
const attributes = useArrowNavigationGroup({ axis: 'horizontal', circular: checked });

return (
<>
<Checkbox label="Circular" checked={checked} onChange={(e, data) => setChecked(data.checked as boolean)} />
<div aria-label="Editor toolbar example" role="toolbar" {...attributes} className={styles.container}>
<Button aria-label="Bold" icon={<TextBoldRegular />} />
<Button aria-label="Underline" icon={<TextUnderlineRegular />} />
<Button aria-label="Italic" icon={<TextItalicRegular />} />
<Button aria-label="Align Left" icon={<TextAlignLeftRegular />} />
<Button aria-label="Align Center" icon={<TextAlignCenterRegular />} />
<Button aria-label="Align Right" icon={<TextAlignRightRegular />} />
<Button aria-label="Copy" icon={<CopyRegular />} />
<Button aria-label="Cut" icon={<CutRegular />} />
<Button aria-label="Paste" icon={<ClipboardPasteRegular />} />
</div>
</>
);
};

CircularNavigation.parameters = {
docs: {
description: {
story: [
'Circular navigation means that focus will keep circling around the collection of focusable elements.',
].join('\n'),
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from 'react';
import {
TextBoldRegular,
TextUnderlineRegular,
TextItalicRegular,
TextAlignLeftRegular,
TextAlignCenterRegular,
TextAlignRightRegular,
CopyRegular,
ClipboardPasteRegular,
CutRegular,
} from '@fluentui/react-icons';
import { Button, useArrowNavigationGroup, makeStyles, shorthands } from '@fluentui/react-components';

const useStyles = makeStyles({
container: {
display: 'flex',
...shorthands.gap('5px'),
},
});

export const Default = () => {
const styles = useStyles();
const attributes = useArrowNavigationGroup({ axis: 'horizontal' });

return (
<div aria-label="Editor toolbar example" role="toolbar" {...attributes} className={styles.container}>
<Button aria-label="Bold" icon={<TextBoldRegular />} />
<Button aria-label="Underline" icon={<TextUnderlineRegular />} />
<Button aria-label="Italic" icon={<TextItalicRegular />} />
<Button aria-label="Align Left" icon={<TextAlignLeftRegular />} />
<Button aria-label="Align Center" icon={<TextAlignCenterRegular />} />
<Button aria-label="Align Right" icon={<TextAlignRightRegular />} />
<Button aria-label="Copy" icon={<CopyRegular />} />
<Button aria-label="Cut" icon={<CutRegular />} />
<Button aria-label="Paste" icon={<ClipboardPasteRegular />} />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as React from 'react';
import {
TextBoldRegular,
TextUnderlineRegular,
TextItalicRegular,
TextAlignLeftRegular,
TextAlignCenterRegular,
TextAlignRightRegular,
CopyRegular,
ClipboardPasteRegular,
CutRegular,
} from '@fluentui/react-icons';
import { Button, useArrowNavigationGroup, makeStyles, shorthands } from '@fluentui/react-components';

const useStyles = makeStyles({
container: {
display: 'flex',
...shorthands.gap('5px'),
},
});

export const Memorize = () => {
const styles = useStyles();
const attributes = useArrowNavigationGroup({ axis: 'horizontal', circular: true, memorizeCurrent: true });

return (
<div aria-label="Editor toolbar example" role="toolbar" {...attributes} className={styles.container}>
<Button aria-label="Bold" icon={<TextBoldRegular />} />
<Button aria-label="Underline" icon={<TextUnderlineRegular />} />
<Button aria-label="Italic" icon={<TextItalicRegular />} />
<Button aria-label="Align Left" icon={<TextAlignLeftRegular />} />
<Button aria-label="Align Center" icon={<TextAlignCenterRegular />} />
<Button aria-label="Align Right" icon={<TextAlignRightRegular />} />
<Button aria-label="Copy" icon={<CopyRegular />} />
<Button aria-label="Cut" icon={<CutRegular />} />
<Button aria-label="Paste" icon={<ClipboardPasteRegular />} />
</div>
);
};

Memorize.parameters = {
docs: {
description: {
story: [
'When a users tabs out out of the collection of focusable elements, ',
'users will tab back into the last focused element.',
].join('\n'),
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useArrowNavigationGroup } from '@fluentui/react-components';
import descriptionMd from './useArrowNavigationGroupDescription.md';

export { Default } from './Default.stories';
export { Axis } from './Axis.stories';
export { CircularNavigation } from './CircularNavigation.stories';
export { Memorize } from './Memorize.stories';

export default {
title: 'Utilities/Focus Management/useArrowNavigationGroup',
component: useArrowNavigationGroup,
parameters: {
docs: {
description: {
component: [descriptionMd].join('\n'),
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
This hook enables keyboard navigation using the arrow keys (up/down/left/right), among a collection of
focusable elements. This hook is powered using the [Mover API in tabster](http://tabster.io/docs/mover/).
In addition to the arrow keys, Home and End keys will navigate to the first and last focusable element in the collection
respectively.

> NOTE: Elements with `tabindex="-1"` are considered unfocusable by tabster and will be skipped.
Loading

0 comments on commit 1c7e645

Please sign in to comment.