From c63f92b302acbd9ed50dc28544fefc9fd69b40a1 Mon Sep 17 00:00:00 2001 From: Valentyna Date: Tue, 30 May 2023 12:42:11 +0200 Subject: [PATCH 1/7] Added `tab` focus mode --- .../react-breadcrumb/cypress.config.ts | 3 + .../react-breadcrumb/docs/Spec.md | 1 + .../react-breadcrumb/package.json | 3 + .../components/Breadcrumb/Breadcrumb.cy.tsx | 215 ++++++++++++++++++ .../components/Breadcrumb/Breadcrumb.types.ts | 20 +- .../Breadcrumb/BreadcrumbContext.ts | 1 + .../components/Breadcrumb/useBreadcrumb.ts | 5 +- .../Breadcrumb/useBreadcrumbContextValue.ts | 6 +- .../BreadcrumbButton/useBreadcrumbButton.ts | 3 +- .../BreadcrumbLink/useBreadcrumbLink.ts | 3 +- .../Breadcrumb/BreadcrumbFocusMode.tsx | 103 +++++++++ .../stories/Breadcrumb/index.stories.tsx | 1 + 12 files changed, 352 insertions(+), 12 deletions(-) create mode 100644 packages/react-components/react-breadcrumb/cypress.config.ts create mode 100644 packages/react-components/react-breadcrumb/src/components/Breadcrumb/Breadcrumb.cy.tsx create mode 100644 packages/react-components/react-breadcrumb/stories/Breadcrumb/BreadcrumbFocusMode.tsx diff --git a/packages/react-components/react-breadcrumb/cypress.config.ts b/packages/react-components/react-breadcrumb/cypress.config.ts new file mode 100644 index 00000000000000..ca52cf041bbf2c --- /dev/null +++ b/packages/react-components/react-breadcrumb/cypress.config.ts @@ -0,0 +1,3 @@ +import { baseConfig } from '@fluentui/scripts-cypress'; + +export default baseConfig; diff --git a/packages/react-components/react-breadcrumb/docs/Spec.md b/packages/react-components/react-breadcrumb/docs/Spec.md index e49373e5993d8a..ca15356523aeb3 100644 --- a/packages/react-components/react-breadcrumb/docs/Spec.md +++ b/packages/react-components/react-breadcrumb/docs/Spec.md @@ -176,6 +176,7 @@ Dropdown contains collapsed items. | ------------ | -------------------------- | ------------- | -------------------------------- | | appearance | `transparent`, `subtle` | `transparent` | Sets appearance | | dividerType | `chevron`, `slash` | `chevron` | Sets type of divider | +| focusMode | `tab`, `arrow`, `off` | `tab` | Sets focus mode | | iconPosition | `before`, `after` | `before` | Sets icon position for all items | | size | `small`, `medium`, `large` | `medium` | Defines size of the Breadcrumb | diff --git a/packages/react-components/react-breadcrumb/package.json b/packages/react-components/react-breadcrumb/package.json index 707fe49009ddd6..d6b2173d077069 100644 --- a/packages/react-components/react-breadcrumb/package.json +++ b/packages/react-components/react-breadcrumb/package.json @@ -16,6 +16,8 @@ "build": "just-scripts build", "clean": "just-scripts clean", "code-style": "just-scripts code-style", + "e2e": "cypress run --component", + "e2e:local": "cypress open --component", "just": "just-scripts", "lint": "just-scripts lint", "test": "jest --passWithNoTests", @@ -30,6 +32,7 @@ "@fluentui/react-conformance": "*", "@fluentui/react-conformance-griffel": "9.0.0-beta.22", "@fluentui/scripts-api-extractor": "*", + "@fluentui/scripts-cypress": "*", "@fluentui/scripts-tasks": "*" }, "dependencies": { diff --git a/packages/react-components/react-breadcrumb/src/components/Breadcrumb/Breadcrumb.cy.tsx b/packages/react-components/react-breadcrumb/src/components/Breadcrumb/Breadcrumb.cy.tsx new file mode 100644 index 00000000000000..e5ca97b6b34d5c --- /dev/null +++ b/packages/react-components/react-breadcrumb/src/components/Breadcrumb/Breadcrumb.cy.tsx @@ -0,0 +1,215 @@ +import * as React from 'react'; +import { mount } from '@cypress/react'; +import type {} from '@cypress/react'; +import { FluentProvider } from '@fluentui/react-provider'; +import { webLightTheme } from '@fluentui/react-theme'; +import { Breadcrumb, BreadcrumbItem, BreadcrumbButton, BreadcrumbLink } from '@fluentui/react-breadcrumb'; +import type { BreadcrumbProps } from '@fluentui/react-breadcrumb'; + +const mountFluent = (element: JSX.Element) => { + mount({element}); +}; + +const BreadcrumbSampleWithButton = (props: BreadcrumbProps) => ( + <> +

+ Before +

+ + + + Item 1 + + + Item 2 + + + + Item 3 + + + + +

+ After +

+ +); + +const BreadcrumbSampleWithLink = (props: BreadcrumbProps) => ( + <> +

+ Before +

+ + + + + Item 1 + + + + + Item 2 + + + + + Item 3 + + + + +

+ After +

+ +); + +const NonInteractiveBreadcrumbSample = (props: BreadcrumbProps) => ( + <> +

+ Before +

+ + + Item 1 + Item 2 + + Item 3 + + + +

+ After +

+ +); + +describe('Breadcrumb', () => { + describe('focus behaviors for BreadcrumbButton', () => { + describe('focusMode="tab"(default)', () => { + it('should be focusable', () => { + mountFluent(); + + cy.get('#before').focus(); + + cy.get('#breadcrumb-button-1').should('not.be.focused'); + + cy.realPress('Tab'); + + cy.get('#breadcrumb-button-1').should('be.focused'); + cy.realPress('Tab'); + cy.get('#breadcrumb-button-2').should('be.focused'); + cy.realPress('Tab'); + cy.get('#breadcrumb-button-3').should('be.focused'); + }); + }); + + describe('focusMode="arrow"', () => { + it('should be focusable', () => { + mountFluent(); + + cy.get('#before').focus(); + + cy.get('#breadcrumb-button-1').should('not.be.focused'); + + cy.realPress('Tab'); + + cy.get('#breadcrumb-button-1').should('be.focused'); + cy.realPress('ArrowRight'); + cy.get('#breadcrumb-button-2').should('be.focused'); + cy.realPress('ArrowRight'); + cy.get('#breadcrumb-button-3').should('be.focused'); + cy.realPress('ArrowRight'); + cy.get('#breadcrumb-button-1').should('be.focused'); + }); + }); + + describe('focusMode="off" (default)', () => { + it('should not be focusable', () => { + mountFluent(); + + cy.get('#before').focus(); + + cy.get('#breadcrumb-button-1').should('not.be.focused'); + cy.get('#before').should('be.focused'); + + cy.realPress('Tab'); + + cy.get('#breadcrumb-button-1').should('not.be.focused'); + cy.get('#after').should('be.focused'); + }); + }); + }); + describe('focus behaviors for BreadcrumbLink', () => { + describe('focusMode="tab"(default)', () => { + it('should be focusable', () => { + mountFluent(); + + cy.get('#before').focus(); + + cy.get('#breadcrumb-link-1').should('not.be.focused'); + + cy.realPress('Tab'); + + cy.get('#breadcrumb-link-1').should('be.focused'); + cy.realPress('Tab'); + cy.get('#breadcrumb-link-2').should('be.focused'); + cy.realPress('Tab'); + cy.get('#breadcrumb-link-3').should('be.focused'); + }); + }); + + describe('focusMode="arrow"', () => { + it('should be focusable', () => { + mountFluent(); + + cy.get('#before').focus(); + + cy.get('#breadcrumb-link-1').should('not.be.focused'); + + cy.realPress('Tab'); + + cy.get('#breadcrumb-link-1').should('be.focused'); + cy.realPress('ArrowRight'); + cy.get('#breadcrumb-link-2').should('be.focused'); + cy.realPress('ArrowRight'); + cy.get('#breadcrumb-link-3').should('be.focused'); + cy.realPress('ArrowRight'); + cy.get('#breadcrumb-link-1').should('be.focused'); + }); + }); + + describe('focusMode="off" (default)', () => { + it('should not be focusable', () => { + mountFluent(); + + cy.get('#before').focus(); + + cy.get('#breadcrumb-link-1').should('not.be.focused'); + cy.get('#before').should('be.focused'); + + cy.realPress('Tab'); + + cy.get('#breadcrumb-link-1').should('not.be.focused'); + cy.get('#after').should('be.focused'); + }); + }); + }); + describe('focus behaviors for BreadcrumbItem', () => { + it('should not be focusable', () => { + mountFluent(); + + cy.get('#before').focus(); + + cy.get('#breadcrumb-item-1').should('not.be.focused'); + cy.get('#before').should('be.focused'); + + cy.realPress('Tab'); + + cy.get('#breadcrumb-item-1').should('not.be.focused'); + cy.get('#after').should('be.focused'); + }); + }); +}); diff --git a/packages/react-components/react-breadcrumb/src/components/Breadcrumb/Breadcrumb.types.ts b/packages/react-components/react-breadcrumb/src/components/Breadcrumb/Breadcrumb.types.ts index 59e859d6cc7679..4b2b946fb024ad 100644 --- a/packages/react-components/react-breadcrumb/src/components/Breadcrumb/Breadcrumb.types.ts +++ b/packages/react-components/react-breadcrumb/src/components/Breadcrumb/Breadcrumb.types.ts @@ -4,7 +4,7 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utili * Data shared between breadcrumb components */ export type BreadcrumbContextValue = Required< - Pick + Pick >; export type BreadcrumbContextValues = { @@ -34,11 +34,21 @@ export type BreadcrumbProps = ComponentProps & { appearance?: 'transparent' | 'subtle'; /** - * Makes Breadcrumb not focusable. + * Sets the focus behavior for the Breadcrumb. * - * @default false + * `tab` + * This behaviour will cycle through all elements inside of the Breadcrumb when pressing the Tab key and then release focus + * after the last inner element. + * + * `arrow` + * This behaviour will cycle through all elements inside of the Breadcrumb when pressing the Arrow key. + * + * `off` + * The Breadcrumb will not focusable. + * + * @default 'tab' */ - disableFocus?: boolean; + focusMode?: 'arrow' | 'tab' | 'off'; /** * Controls type of the divider. @@ -66,4 +76,4 @@ export type BreadcrumbProps = ComponentProps & { * State used in rendering Breadcrumb */ export type BreadcrumbState = ComponentState & - Required>; + Required>; diff --git a/packages/react-components/react-breadcrumb/src/components/Breadcrumb/BreadcrumbContext.ts b/packages/react-components/react-breadcrumb/src/components/Breadcrumb/BreadcrumbContext.ts index 2a189e0efbcebb..01e89ea51c351c 100644 --- a/packages/react-components/react-breadcrumb/src/components/Breadcrumb/BreadcrumbContext.ts +++ b/packages/react-components/react-breadcrumb/src/components/Breadcrumb/BreadcrumbContext.ts @@ -11,6 +11,7 @@ export const breadcrumbDefaultValue: BreadcrumbContextValue = { iconPosition: 'before', size: 'medium', dividerType: 'chevron', + focusMode: 'tab', }; /** diff --git a/packages/react-components/react-breadcrumb/src/components/Breadcrumb/useBreadcrumb.ts b/packages/react-components/react-breadcrumb/src/components/Breadcrumb/useBreadcrumb.ts index d0d5628eb8ae5e..a4ca1b43dafe19 100644 --- a/packages/react-components/react-breadcrumb/src/components/Breadcrumb/useBreadcrumb.ts +++ b/packages/react-components/react-breadcrumb/src/components/Breadcrumb/useBreadcrumb.ts @@ -15,7 +15,7 @@ import { useArrowNavigationGroup } from '@fluentui/react-tabster'; export const useBreadcrumb_unstable = (props: BreadcrumbProps, ref: React.Ref): BreadcrumbState => { const { appearance = 'transparent', - disableFocus, + focusMode = 'tab', dividerType = 'chevron', iconPosition = 'before', size = 'medium', @@ -37,11 +37,12 @@ export const useBreadcrumb_unstable = (props: BreadcrumbProps, ref: React.Ref { - return { appearance, dividerType, iconPosition, size }; - }, [appearance, dividerType, iconPosition, size]); + return { appearance, dividerType, focusMode, iconPosition, size }; + }, [appearance, dividerType, focusMode, iconPosition, size]); return { breadcrumb }; } diff --git a/packages/react-components/react-breadcrumb/src/components/BreadcrumbButton/useBreadcrumbButton.ts b/packages/react-components/react-breadcrumb/src/components/BreadcrumbButton/useBreadcrumbButton.ts index 8587d83a2003bd..c11fb4aed18597 100644 --- a/packages/react-components/react-breadcrumb/src/components/BreadcrumbButton/useBreadcrumbButton.ts +++ b/packages/react-components/react-breadcrumb/src/components/BreadcrumbButton/useBreadcrumbButton.ts @@ -16,7 +16,7 @@ export const useBreadcrumbButton_unstable = ( props: BreadcrumbButtonProps, ref: React.Ref, ): BreadcrumbButtonState => { - const { appearance, iconPosition, size } = useBreadcrumbContext_unstable(); + const { appearance, iconPosition, size, focusMode } = useBreadcrumbContext_unstable(); const { current = false, icon, ...rest } = props; return { @@ -26,6 +26,7 @@ export const useBreadcrumbButton_unstable = ( appearance: props.appearance || appearance, iconPosition: props.iconPosition || iconPosition, icon, + tabIndex: focusMode === 'off' ? -1 : undefined, 'aria-current': current ? props['aria-current'] ?? 'page' : undefined, }, ref, diff --git a/packages/react-components/react-breadcrumb/src/components/BreadcrumbLink/useBreadcrumbLink.ts b/packages/react-components/react-breadcrumb/src/components/BreadcrumbLink/useBreadcrumbLink.ts index b6097b6fa8c880..e87d6045a13254 100644 --- a/packages/react-components/react-breadcrumb/src/components/BreadcrumbLink/useBreadcrumbLink.ts +++ b/packages/react-components/react-breadcrumb/src/components/BreadcrumbLink/useBreadcrumbLink.ts @@ -16,7 +16,7 @@ export const useBreadcrumbLink_unstable = ( props: BreadcrumbLinkProps, ref: React.Ref, ): BreadcrumbLinkState => { - const { appearance, iconPosition, size } = useBreadcrumbContext_unstable(); + const { appearance, iconPosition, focusMode, size } = useBreadcrumbContext_unstable(); const { current = false, disabled = false, icon, overflow = false, ...rest } = props; const linkAppearance = props.appearance || appearance; @@ -30,6 +30,7 @@ export const useBreadcrumbLink_unstable = ( root: getNativeElementProps('a', { 'aria-current': current ? props['aria-current'] ?? 'page' : undefined, ref, + tabIndex: focusMode === 'off' ? -1 : undefined, ...rest, }), appearance: linkAppearance === 'subtle' ? 'subtle' : 'default', diff --git a/packages/react-components/react-breadcrumb/stories/Breadcrumb/BreadcrumbFocusMode.tsx b/packages/react-components/react-breadcrumb/stories/Breadcrumb/BreadcrumbFocusMode.tsx new file mode 100644 index 00000000000000..1e32548ed25b03 --- /dev/null +++ b/packages/react-components/react-breadcrumb/stories/Breadcrumb/BreadcrumbFocusMode.tsx @@ -0,0 +1,103 @@ +import * as React from 'react'; +import { Breadcrumb, BreadcrumbItem, BreadcrumbButton, BreadcrumbDivider } from '@fluentui/react-breadcrumb'; +import { bundleIcon, CalendarMonth20Filled, CalendarMonth20Regular, GridDots20Regular } from '@fluentui/react-icons'; +import { ButtonProps } from '@fluentui/react-components'; + +const CalendarMonth = bundleIcon(CalendarMonth20Filled, CalendarMonth20Regular); + +type Item = { + key: number; + item?: string; + buttonProps?: { + 'aria-label'?: string; + onClick?: () => void; + icon?: ButtonProps['icon']; + disabled?: boolean; + iconPosition?: 'before' | 'after'; + }; +}; + +const buttonItems: Item[] = [ + { + key: 0, + item: 'Item 0', + buttonProps: { + onClick: () => console.log('item 0 was clicked'), + }, + }, + { + key: 1, + buttonProps: { + icon: , + 'aria-label': 'Item 1', + onClick: () => console.log('item 1 was clicked'), + }, + }, + { + key: 2, + item: 'Item 2', + buttonProps: { + icon: , + onClick: () => console.log('item 2 was clicked'), + }, + }, + { + key: 3, + item: 'Item 3', + buttonProps: { + onClick: () => console.log('item 3 was clicked'), + }, + }, + { + key: 4, + item: 'Item 4', + buttonProps: { + icon: , + iconPosition: 'after', + onClick: () => console.log('item 4 was clicked'), + }, + }, + { + key: 5, + item: 'Item 5', + buttonProps: { + onClick: () => console.log('item 5 was clicked'), + }, + }, +]; + +function renderButton(el: Item, isLastItem: boolean = false) { + return ( + + + + {el.item} + + + {!isLastItem && } + + ); +} +export const BreadcrumbFocusMode = () => ( + <> +

`tab` - default

+ Navigation using `tab` key. + + {buttonItems.map(el => renderButton(el, el.key === buttonItems.length - 1))} + +

`arrow`

+ Navigation using arrow keys + + {buttonItems.map(el => renderButton(el, el.key === buttonItems.length - 1))} + +

`off`

+ Breadcrumb is not focusable + + {buttonItems.map(el => renderButton(el, el.key === buttonItems.length - 1))} + + +); diff --git a/packages/react-components/react-breadcrumb/stories/Breadcrumb/index.stories.tsx b/packages/react-components/react-breadcrumb/stories/Breadcrumb/index.stories.tsx index e8f0fd5aad0f75..8a02b840e9e63f 100644 --- a/packages/react-components/react-breadcrumb/stories/Breadcrumb/index.stories.tsx +++ b/packages/react-components/react-breadcrumb/stories/Breadcrumb/index.stories.tsx @@ -5,6 +5,7 @@ import bestPracticesMd from './BreadcrumbBestPractices.md'; export { Default } from './BreadcrumbDefault.stories'; export { BreadcrumbWithOverflow } from './BreadcrumbWithOverflow.stories'; +export { BreadcrumbFocusMode } from './BreadcrumbFocusMode'; export default { title: 'Preview Components/Breadcrumb', From 3df6eb93015caabded01f8a5d7bd16882ca83dd7 Mon Sep 17 00:00:00 2001 From: Valentyna Date: Thu, 1 Jun 2023 15:46:45 +0200 Subject: [PATCH 2/7] updated cy config --- .../react-components/react-breadcrumb/tsconfig.cy.json | 9 +++++++++ packages/react-components/react-breadcrumb/tsconfig.json | 3 +++ .../react-components/react-breadcrumb/tsconfig.lib.json | 4 +++- 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 packages/react-components/react-breadcrumb/tsconfig.cy.json diff --git a/packages/react-components/react-breadcrumb/tsconfig.cy.json b/packages/react-components/react-breadcrumb/tsconfig.cy.json new file mode 100644 index 00000000000000..93a140885851da --- /dev/null +++ b/packages/react-components/react-breadcrumb/tsconfig.cy.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "isolatedModules": false, + "types": ["node", "cypress", "cypress-storybook/cypress", "cypress-real-events"], + "lib": ["ES2019", "dom"] + }, + "include": ["**/*.cy.ts", "**/*.cy.tsx"] +} diff --git a/packages/react-components/react-breadcrumb/tsconfig.json b/packages/react-components/react-breadcrumb/tsconfig.json index 1941a041d46c19..1317f81620ca5e 100644 --- a/packages/react-components/react-breadcrumb/tsconfig.json +++ b/packages/react-components/react-breadcrumb/tsconfig.json @@ -20,6 +20,9 @@ }, { "path": "./.storybook/tsconfig.json" + }, + { + "path": "./tsconfig.cy.json" } ] } diff --git a/packages/react-components/react-breadcrumb/tsconfig.lib.json b/packages/react-components/react-breadcrumb/tsconfig.lib.json index 6f90cf95c005bd..e17f808c039339 100644 --- a/packages/react-components/react-breadcrumb/tsconfig.lib.json +++ b/packages/react-components/react-breadcrumb/tsconfig.lib.json @@ -16,7 +16,9 @@ "**/*.test.ts", "**/*.test.tsx", "**/*.stories.ts", - "**/*.stories.tsx" + "**/*.stories.tsx", + "**/*.cy.ts", + "**/*.cy.tsx" ], "include": ["./src/**/*.ts", "./src/**/*.tsx"] } From 1083ada9489d82d2996833048db90dcc8300e232 Mon Sep 17 00:00:00 2001 From: Valentyna Date: Thu, 1 Jun 2023 16:17:40 +0200 Subject: [PATCH 3/7] generated api --- .../react-breadcrumb/etc/react-breadcrumb.api.md | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/react-components/react-breadcrumb/etc/react-breadcrumb.api.md b/packages/react-components/react-breadcrumb/etc/react-breadcrumb.api.md index 486f705ec9af82..eeff29cca1b240 100644 --- a/packages/react-components/react-breadcrumb/etc/react-breadcrumb.api.md +++ b/packages/react-components/react-breadcrumb/etc/react-breadcrumb.api.md @@ -6,17 +6,7 @@ /// -import { ButtonProps } from '@fluentui/react-button'; -import { ButtonSlots } from '@fluentui/react-button'; -import { ButtonState } from '@fluentui/react-button'; -import type { ComponentProps } from '@fluentui/react-utilities'; -import type { ComponentState } from '@fluentui/react-utilities'; -import type { ForwardRefComponent } from '@fluentui/react-utilities'; -import { Link } from '@fluentui/react-link'; -import { LinkProps } from '@fluentui/react-link'; import * as React_2 from 'react'; -import type { Slot } from '@fluentui/react-utilities'; -import type { SlotClassNames } from '@fluentui/react-utilities'; // @public export const Breadcrumb: ForwardRefComponent; @@ -105,7 +95,7 @@ export type BreadcrumbLinkState = ComponentState & Partial< // @public export type BreadcrumbProps = ComponentProps & { appearance?: 'transparent' | 'subtle'; - disableFocus?: boolean; + focusMode?: 'arrow' | 'tab' | 'off'; dividerType?: 'chevron' | 'slash'; iconPosition?: 'before' | 'after'; size?: 'small' | 'medium' | 'large'; @@ -118,7 +108,7 @@ export type BreadcrumbSlots = { }; // @public -export type BreadcrumbState = ComponentState & Required>; +export type BreadcrumbState = ComponentState & Required>; // @public (undocumented) export type PartitionBreadcrumbItems = { From 65088a7a7cd5a68debe7098095acd64f3146ba74 Mon Sep 17 00:00:00 2001 From: Valentyna Date: Thu, 1 Jun 2023 16:36:46 +0200 Subject: [PATCH 4/7] updated snapshots --- .../src/components/Breadcrumb/Breadcrumb.test.tsx | 3 --- .../components/BreadcrumbDivider/BreadcrumbDivider.test.tsx | 2 -- 2 files changed, 5 deletions(-) diff --git a/packages/react-components/react-breadcrumb/src/components/Breadcrumb/Breadcrumb.test.tsx b/packages/react-components/react-breadcrumb/src/components/Breadcrumb/Breadcrumb.test.tsx index 9f0cc13830edb8..a12d4346ecb4aa 100644 --- a/packages/react-components/react-breadcrumb/src/components/Breadcrumb/Breadcrumb.test.tsx +++ b/packages/react-components/react-breadcrumb/src/components/Breadcrumb/Breadcrumb.test.tsx @@ -28,7 +28,6 @@ describe('Breadcrumb', () => {