Skip to content

Commit

Permalink
feat(react-keytips): initial implementation (#221)
Browse files Browse the repository at this point in the history
Co-authored-by: viktorgenaev <[email protected]>
Co-authored-by: Dmytro Kirpa <[email protected]>
Co-authored-by: Martin Hochel <[email protected]>
  • Loading branch information
4 people authored Oct 16, 2024
1 parent a7e7aaa commit 813ebd1
Show file tree
Hide file tree
Showing 47 changed files with 3,297 additions and 21 deletions.
6 changes: 4 additions & 2 deletions .syncpackrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
},
{
"packages": [
"@fluentui-contrib/react-interactive-tab"
"@fluentui-contrib/react-interactive-tab",
"@fluentui-contrib/react-keytips"
],
"dependencies": [
"@fluentui/react-jsx-runtime"
"@fluentui/react-jsx-runtime",
"@fluentui/react-utilities"
],
"dependencyTypes": [
"prod"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: initial implementation",
"packageName": "@fluentui-contrib/react-keytips",
"email": "[email protected]",
"dependentChangeType": "patch"
}
5 changes: 5 additions & 0 deletions packages/react-keytips/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
"rules": {
"@nx/enforce-module-boundaries": "off"
}
},
{
"files": ["src/**/*.spec.{ts,js,tsx,jsx}"],
"extends": ["plugin:playwright/recommended"],
"rules": {}
}
]
}
109 changes: 103 additions & 6 deletions packages/react-keytips/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,108 @@
# react-keytips
# @fluentui-contrib/react-keytips

This library was generated with [Nx](https://nx.dev).
## Installation

## Building
```bash
yarn add @fluentui-contrib/react-keytips
```

Run `nx build react-keytips` to build the library.
## Usage

## Running unit tests
```tsx
import * as React from 'react';
import { Keytips, useKeytipRef } from '@fluentui-contrib/react-keytips';
import { Button } from '@fluentui/react-components';

Run `nx test react-keytips` to execute the unit tests via [Jest](https://jestjs.io).
export const App = () => {
const onExecute = (_, ({ targetElement })) => {
targetElement.click();
};

const keytipRefA = useKeytipRef({ keySequences: ['a'], content: 'A', onExecute });
const keytipRefB = useKeytipRef({ keySequences: ['b'], content: 'B', onExecute });
const keytipRefC = useKeytipRef({ keySequences: ['c'], content: 'C', onExecute });

return (
<>
{/* Keytips must be added once at the root level of the app */}
<Keytips />
<Button ref={keytipRefA}>Button A</Button>
<Button ref={keytipRefB}>Button B</Button>
<Button ref={keytipRefC}>Button C</Button>
</>
);
};
```

### Handling keytips with dynamic content

If a Keytip triggers dynamic content that includes its own keytips, you must add the `hasDynamicChildren` prop to the `useKeytipRef` for the relevant component. Additionally, the child keytips should include the parent's key sequence in their key sequences.

Here's an example using a Tab component:

```tsx
import * as React from 'react';
import { Keytips, useKeytipRef } from '@fluentui-contrib/react-keytips';

const TabExample = () => {
const [selectedValue, setSelectedValue] = React.useState<TabValue>('1');

const onTabSelect = (_: SelectTabEvent, data: SelectTabData) => {
setSelectedValue(data.value);
};

const refFirstTab = useKeytipRef({
keySequences: ['a'],
content: 'A',
hasDynamicChildren: true,
onExecute: btnExecute,
});

const refSecondTab = useKeytipRef({
keySequences: ['b'],
content: 'B',
hasDynamicChildren: true,
onExecute: btnExecute,
});

const checkBoxRef = useKeytipRef<HTMLInputElement>({
keySequences: ['a', '1'],
content: '1',
onExecute: btnExecute,
});

const btnRef = useKeytipRef({
keySequences: ['b', '1'],
content: 'B1',
onExecute: btnExecute,
});

return (
<>
<Keytips {...props} />
<TabList onTabSelect={onTabSelect}>
<Tab id="1" ref={refFirstTab} value="1">
First Tab
</Tab>
<Tab id="2" ref={refSecondTab} value="2">
Second Tab
</Tab>
</TabList>
<div className={classes.panels}>
{selectedValue === '1' && (
<div role="tabpanel" className={classes.row}>
<Checkbox ref={checkBoxRef} label="Checkbox" />
</div>
)}
{selectedValue === '2' && (
<div role="tabpanel">
<Button ref={btnRef}>Button 2</Button>
</div>
)}
</div>
</>
);
};
```

Follow up on the [Storybook](https://microsoft.github.io/fluentui-contrib/react-keytips) for examples on how to use the components provided by this package.
5 changes: 2 additions & 3 deletions packages/react-keytips/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ export default {
displayName: 'react-keytips',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': ['@swc/jest', swcJestConfig],
'^.+\\.[tj]sx?$': ['@swc/jest', swcJestConfig],
},
moduleFileExtensions: ['ts', 'js', 'html'],
testEnvironment: 'jsdom',
moduleFileExtensions: ['js', 'ts', 'tsx', 'html'],
coverageDirectory: '../../coverage/packages/react-keytips',
};
10 changes: 7 additions & 3 deletions packages/react-keytips/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
{
"name": "@fluentui-contrib/react-keytips",
"version": "0.0.1",
"dependencies": {
"@swc/helpers": "~0.5.2"
},
"main": "./src/index.js",
"typings": "./src/index.d.ts",
"private": true,
Expand All @@ -13,5 +10,12 @@
"@types/react-dom": ">=16.8.0 <19.0.0",
"react": ">=16.8.0 <19.0.0",
"react-dom": ">=16.8.0 <19.0.0"
},
"dependencies": {
"@fluentui/react-jsx-runtime": "^9.0.29",
"@fluentui/react-utilities": "^9.16.0",
"@fluentui/react-positioning": "^9.16.0",
"@fluentui/keyboard-keys": "~9.0.6",
"@swc/helpers": "~0.5.2"
}
}
54 changes: 54 additions & 0 deletions packages/react-keytips/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { defineConfig, devices } from '@playwright/experimental-ct-react';

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './src',
/* Glob patterns or regular expressions that match test files. */
testMatch: '**/*.component-browser-@(spec|test).tsx',
/* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */
snapshotDir: './__snapshots__',
/* Maximum time one test can run for. */
timeout: 10 * 1000,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'list',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',

/* Port to use for Playwright component endpoint. */
ctPort: 3100,
},

/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
12 changes: 12 additions & 0 deletions packages/react-keytips/playwright/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Testing Page</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./index.tsx"></script>
</body>
</html>
1 change: 1 addition & 0 deletions packages/react-keytips/playwright/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Import styles, initialize component theme here.
8 changes: 8 additions & 0 deletions packages/react-keytips/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@
"quiet": true
}
}
},
"component-test": {
"executor": "@fluentui-contrib/nx-plugin:playwright",
"options": {
"testingType": "component",
"output": "{workspaceRoot}/dist/.playwright/packages/react-keytips",
"config": "packages/react-keytips/playwright.config.ts"
}
}
}
}
12 changes: 12 additions & 0 deletions packages/react-keytips/src/components/Keytip/Keytip.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import { Keytip, keytipClassNames } from './index';

describe('Keytip', () => {
it('renders a default state', () => {
render(<Keytip content="A" keySequences={['a']} visible />);
const keytip = screen.getByRole('tooltip');
expect(keytip.classList.contains(keytipClassNames.content)).toBeTruthy();
expect(keytip.id).toBe('keytip-ktp-a');
});
});
18 changes: 18 additions & 0 deletions packages/react-keytips/src/components/Keytip/Keytip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useKeytip_unstable } from './useKeytip';
import { renderKeytip_unstable } from './renderKeytip';
import type { KeytipProps } from './Keytip.types';
import { useKeytipStyles_unstable } from './useKeytipStyles.styles';

/**
* Keytip component. Responsible for rendering an individual keytip,
* is not supposed to be used directly, but is used by the Keytips component.
*
*/
export const Keytip = (props: KeytipProps) => {
const state = useKeytip_unstable(props);
useKeytipStyles_unstable(state);

return renderKeytip_unstable(state);
};

Keytip.displayName = 'Keytip';
71 changes: 71 additions & 0 deletions packages/react-keytips/src/components/Keytip/Keytip.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { EventData, EventHandler } from '@fluentui/react-utilities';
import type {
PositioningProps,
ComponentProps,
ComponentState,
Slot,
} from '@fluentui/react-components';

/**
* Slot properties for Keytip
*/
export type KeytipSlots = {
/**
* The text or JSX content of the Keytip.
*/
content: NonNullable<Slot<'div'>>;
};

export type ExecuteKeytipEventHandler<E = HTMLElement> = EventHandler<
EventData<'keydown', KeyboardEvent> & {
targetElement: E;
}
>;

export type ReturnKeytipEventHandler<E = HTMLElement> = EventHandler<
EventData<'keydown', KeyboardEvent> & {
targetElement: E;
}
>;

export type KeytipProps = ComponentProps<KeytipSlots> & {
/**
* Positioning props to be passed to Keytip.
* @default { align: 'center', position: 'below' }
*/
positioning?: PositioningProps;
/**
* Whether the keytip is visible.
*/
visible?: boolean;
/**
* Function to call when this keytip is activated.
*/
onExecute?: ExecuteKeytipEventHandler;
/**
* Function to call when the keytip is the currentKeytip and a return sequence is pressed.
*/
onReturn?: ReturnKeytipEventHandler;
/**
* Array of KeySequences which is the full key sequence to trigger this keytip
* Should not include initial 'start' key sequence
*/
keySequences: string[];
/**
* Whether or not this keytip will have children keytips that are dynamically created (DOM is generated on * keytip activation).
* Common cases are a Tabs or Modal. Or if the keytip opens a menu.
*/
dynamic?: boolean;
};

export type KeytipWithId = KeytipProps & {
uniqueId: string;
};

export type KeytipState = ComponentState<KeytipSlots> &
Required<Pick<KeytipProps, 'visible' | 'positioning' | 'content'>> & {
/**
* Whether the Keytip should be rendered to the DOM.
*/
shouldRenderKeytip?: boolean;
};
5 changes: 5 additions & 0 deletions packages/react-keytips/src/components/Keytip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './Keytip';
export * from './Keytip.types';
export * from './renderKeytip';
export * from './useKeytip';
export * from './useKeytipStyles.styles';
16 changes: 16 additions & 0 deletions packages/react-keytips/src/components/Keytip/renderKeytip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** @jsxRuntime classic */
/** @jsx createElement */

import type { KeytipSlots, KeytipState } from './Keytip.types';
import { assertSlots } from '@fluentui/react-utilities';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { createElement } from '@fluentui/react-jsx-runtime';

/**
* Render the final JSX of Keytip
*/
export const renderKeytip_unstable = (state: KeytipState) => {
assertSlots<KeytipSlots>(state);
if (!state.shouldRenderKeytip) return null;
return <state.content>{state.content.children}</state.content>;
};
Loading

0 comments on commit 813ebd1

Please sign in to comment.