diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 72b9156562868a..54ddf4e44d8537 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -83,6 +83,7 @@ apps/test-bundles @microsoft/fluentui-react apps/public-docsite-v9 @microsoft/cxe-red @microsoft/cxe-coastal @microsoft/fluentui-react-build apps/theming-designer @microsoft/fluentui-react apps/ssr-tests-v9 @microsoft/fluentui-react-build +apps/stress-test @microsoft/cxe-red @spmonahan @micahgodbolt #### Packages packages/azure-themes @hyoshis @Jacqueline-ms diff --git a/apps/stress-test/.gitignore b/apps/stress-test/.gitignore index 2b77931853383a..8756e1fe4b6642 100644 --- a/apps/stress-test/.gitignore +++ b/apps/stress-test/.gitignore @@ -1,4 +1,5 @@ node_modules/ +dev-build/ dist/ benchmarks/**/*.json diff --git a/apps/stress-test/README.md b/apps/stress-test/README.md index 5726399518aa6b..7f4184db4d448a 100644 --- a/apps/stress-test/README.md +++ b/apps/stress-test/README.md @@ -26,16 +26,16 @@ $ yarn workspace @fluentui/stress-test stress-test build --build-deps $ yarn workspace @fluentui/stress-test stress-test build ``` -> NOTE: `build:local` is much slower, but is required the first time you're building the application or if you've pulled in lots of changes. Use `build:app` if you don't need to build dependencies like `@fluentui/react` as it's much faster. +> NOTE: `build --build-deps` is much slower, but is required the first time you're building the application or if you've pulled in lots of changes. Use `build` if you don't need to build dependencies like `@fluentui/react` as it's much faster. ### Examples ```shell -# Run the "simple-stress" scenario with the "mount" and "prop-update" test cases against Firefox with small sizes and low sample size -$ yarn workspace @fluentui/stress-test stress-test run --scenario simple-stress --sample-size 2 --test-cases mount prop-update --browsers firefox --sizes xs s +# Run your "my-scenario" with the "mount" and "inject-styles" test cases against Firefox with small sizes and low sample size. Target the "stress-tree" page with "button" renderers. +$ yarn workspace @fluentui/stress-test stress-test run --scenario my-scenario --test-cases mount inject-styles --browsers chrome firefox --sizes xs s --targets v8/stress-test?r=button v9/stress-test?r=button wc/stress-test?r=button -# Run the "simple-stress" scenario with the "mount" and "prop-update" test cases against the default browsers at the default sizes and sample size -$ yarn workspace @fluentui/stress-test stress-test run --scenario simple-stress --test-cases mount prop-update +# Run your "my-scenario" scenario with the "mount" and "inject-styles" test cases against the default browsers at the default sizes and sample size. Target the "stress-tree" page with "button" renderers. +$ yarn workspace @fluentui/stress-test stress-test run --scenario my-scenario --test-cases mount inject-styles --targets v8/stress-test?r=button v9/stress-test?r=button wc/stress-test?r=button ``` > NOTE: Tests should be run against production builds. While tests can be run against development builds, and this is useful for gathering quick results and debugging, the performance characteristics of development and production builds can differ quite a bit. @@ -43,8 +43,9 @@ $ yarn workspace @fluentui/stress-test stress-test run --scenario simple-stress ## Glossary - **scenario**: A testing scenario for a specific line of investigation. For example, if you wanted to compare the performance of different `Button` implementations you might create a "button-test" scenario for various targets. -- **targets**: Different implementation targets. For example: "v8" for Fluent UI v8, "v9" for Fluent UI v9, "wc" for Fluent UI Web Components. +- **targets**: Different implementation targets. These are URLs that can accept query parameters. For example "v8/stress-tree?r=button", "wc/stress-tree?r=button". - **test cases**: Different test cases to run against a given scenario. For example, you might want to test mounting performance for a scenario. +- **renderers**: Simple components that can be plugged into targets allowing easy customization of the component under test. ## Development @@ -65,6 +66,45 @@ The `benchmarks` folder houses Tachometer configurations and test results; and h The `scripts` folder house the Stress Test CLI app that is used to run tests. -### Adding test cases +### Creating your own tests -Add tests cases to the appropriate `src/pages` sub-folder. For example to create a new test, "my test" for Fluent v9 add it to `src/pages/v9/my-test`. Use an existing page as a guide for the files you need to add. Pages are automatically picked up by Webpack when the dev server is started. +There are a few way you can create your own tests depending on what you want to do. + +#### Adding a new test with a renderer + +The simplest thing to do is use an existing test page like "stress-tree" but with your own renderer. A renderer is just a function that renders a component to be rendered. Renderers are specific to the UI library being tested so a Web Component renderer is different from a React one but conceptually they are the same. + +An example React renderer: + +```tsx +// myButton.tsx +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; +``` + +> Note: renderers need to be saved in the appropriate `src/renderers` subfolder. `v8` for Fluent V8, `v9` for Fluent V9, `wc` for Fluent Web Components + +In your test specify the renderer with the `r` query parameter: + +```shell +yarn workspace @fluentui/stress-test stress-test run my-button-scenarion --test-cases mount --targets v9/stress-tree?r=myButton +``` + +#### Adding new test fixtures + +Test fixtures are used to drive tests. In the "stress-tree` page fixtures are used to control different sizes of DOM tree so tests can be run against "simple" to "complex" DOM trees. + +Fixtures can be generated with the `build-fixture` command: + +```shell +yarn workspace @fluentui/stress-test stress-test build-fixture --name my-fixture --option option1=value1 option2=value2 +``` + +Fixtures are saved in the `src/fixtures` folder. + +> Note: fixtures can become quite large so they are excluded from git. If you need others to be able to generate your fixture be sure to keep track of the options you use to create the fixture. + +#### Adding a new test page + +A page is a way to control the entire structure of your test. Currently, we ship a "stress-tree" page which loads a broad and deep DOM tree to simulate a complex real-world web application. If you need something different create a page to represent the situation you need to test. See `src/pages` for examples of how to create a page. diff --git a/apps/stress-test/scripts/commands/buildTestConfig.ts b/apps/stress-test/scripts/commands/buildTestConfig.ts index 71dd499b51e414..fa5213f226a6a3 100644 --- a/apps/stress-test/scripts/commands/buildTestConfig.ts +++ b/apps/stress-test/scripts/commands/buildTestConfig.ts @@ -72,7 +72,6 @@ const makeConfigJson: MakeConfigJson = (_scenario, browser, testCase, sampleSize ], expand: targets.map(target => { - // const params = querystring.stringify({ test: testCase, ...testOptions }); const targetParams = target.includes('?') ? querystring.parse(target.substring(target.indexOf('?') + 1)) : {}; const params = querystring.stringify({ ...targetParams, diff --git a/apps/stress-test/scripts/utils/configureYargs.ts b/apps/stress-test/scripts/utils/configureYargs.ts index f7947958443850..f7c11e432b3bda 100644 --- a/apps/stress-test/scripts/utils/configureYargs.ts +++ b/apps/stress-test/scripts/utils/configureYargs.ts @@ -45,9 +45,6 @@ const cliOptions = { describe: 'Optimization mode for Griffel.', default: 'buildtime', }, - renderers: { - describe: 'Renderers to use for testing. This determines what is actually tested.', - }, 'test-options': { describe: 'Options to apply to each test. E.g., option1=value1 option2=value2', coerce: (arg: string[]) => { @@ -164,7 +161,6 @@ const configureYargs: CongfigureYargs = (command, y) => { targets, 'test-options': testOptions, port, - renderers, } = cliOptions; configure(y, { scenario, @@ -175,7 +171,6 @@ const configureYargs: CongfigureYargs = (command, y) => { targets, 'test-options': testOptions, port, - renderers, }); break; } @@ -192,7 +187,6 @@ const configureYargs: CongfigureYargs = (command, y) => { 'process-results': processResults, port, root, - renderers, } = cliOptions; configure(y, { scenario, @@ -205,7 +199,6 @@ const configureYargs: CongfigureYargs = (command, y) => { 'process-results': processResults, port, root, - renderers, }); break; } diff --git a/apps/stress-test/scripts/utils/fixtures.ts b/apps/stress-test/scripts/utils/fixtures.ts index c8feeb1f0a12a0..745a66f3f7bd37 100644 --- a/apps/stress-test/scripts/utils/fixtures.ts +++ b/apps/stress-test/scripts/utils/fixtures.ts @@ -1,8 +1,9 @@ import fs from 'fs-extra'; import { join } from 'path'; import { getFixturesDir, remove } from './paths.js'; -import { RandomTree } from '../../src/shared/tree/RandomTree.js'; -import { RandomSelectorTreeNode, selectorTreeCreator } from '../../src/shared/tree/RandomSelectorTreeNode.js'; +import { RandomTree } from './tree/RandomTree.js'; +import { RandomSelectorTreeNode } from '../../src/shared/tree/types.js'; +import { selectorTreeCreator } from './tree/RandomSelectorTreeNode.js'; import glob from 'glob'; type BuildTreeFixture = (name: string, options: { [key: string]: string }) => void; @@ -20,77 +21,87 @@ const defaultFixtureOptions: DefaultFixtureOptions = { // eslint-disable-next-line @typescript-eslint/naming-convention xs_1: { minBreadth: '1', - maxBreadth: '3', + maxBreadth: '5', minDepth: '1', - maxDepth: '3', + maxDepth: '5', + targetSize: '250', }, // eslint-disable-next-line @typescript-eslint/naming-convention xs_2: { minBreadth: '1', - maxBreadth: '3', + maxBreadth: '5', minDepth: '1', - maxDepth: '3', - seed: '4212021', + maxDepth: '5', + targetSize: '250', + seed: '7032017', }, // eslint-disable-next-line @typescript-eslint/naming-convention s_1: { minBreadth: '2', - maxBreadth: '5', + maxBreadth: '10', minDepth: '2', - maxDepth: '5', + maxDepth: '10', + targetSize: '500', }, // eslint-disable-next-line @typescript-eslint/naming-convention s_2: { minBreadth: '2', - maxBreadth: '5', + maxBreadth: '10', minDepth: '2', - maxDepth: '5', - seed: '4212021', + maxDepth: '10', + targetSize: '500', + seed: '7032017', }, // eslint-disable-next-line @typescript-eslint/naming-convention m_1: { - minBreadth: '3', - maxBreadth: '8', - minDepth: '3', - maxDepth: '8', + minBreadth: '4', + maxBreadth: '20', + minDepth: '4', + maxDepth: '20', + targetSize: '1000', }, // eslint-disable-next-line @typescript-eslint/naming-convention m_2: { - minBreadth: '3', - maxBreadth: '8', - minDepth: '3', - maxDepth: '8', - seed: '4212021', + minBreadth: '4', + maxBreadth: '20', + minDepth: '4', + maxDepth: '20', + targetSize: '1000', + seed: '7032017', }, // eslint-disable-next-line @typescript-eslint/naming-convention l_1: { - minBreadth: '5', - maxBreadth: '15', - minDepth: '4', - maxDepth: '15', + minBreadth: '8', + maxBreadth: '40', + minDepth: '8', + maxDepth: '40', + targetSize: '2000', }, // eslint-disable-next-line @typescript-eslint/naming-convention l_2: { - minBreadth: '5', - maxBreadth: '15', - minDepth: '4', - maxDepth: '15', - seed: '4212021', + minBreadth: '8', + maxBreadth: '40', + minDepth: '8', + maxDepth: '40', + targetSize: '2000', + seed: '7032017', }, // eslint-disable-next-line @typescript-eslint/naming-convention xl_1: { - minBreadth: '10', - maxBreadth: '20', - minDepth: '10', - maxDepth: '20', + minBreadth: '16', + maxBreadth: '80', + minDepth: '16', + maxDepth: '80', + targetSize: '4000', }, // eslint-disable-next-line @typescript-eslint/naming-convention xl_2: { - minBreadth: '10', - maxBreadth: '20', - minDepth: '10', - maxDepth: '20', - seed: '4212021', + minBreadth: '16', + maxBreadth: '80', + minDepth: '16', + maxDepth: '80', + targetSize: '4000', + seed: '7032017', }, }; @@ -123,7 +134,7 @@ export const buildTreeFixture: BuildTreeFixture = (name, options) => { const data = { tree: JSON.parse(jsonTree), - selectors, + selectors: Array.from(new Set(selectors)), }; const js = `export default ${JSON.stringify(data, null, 2)};`; diff --git a/apps/stress-test/scripts/utils/tachometer.ts b/apps/stress-test/scripts/utils/tachometer.ts index c465709a70acf1..c4e40789caf559 100644 --- a/apps/stress-test/scripts/utils/tachometer.ts +++ b/apps/stress-test/scripts/utils/tachometer.ts @@ -1,12 +1,30 @@ import { ConfigResult } from './types'; -import { exec } from 'child_process'; -import { promisify } from 'util'; +import { spawn } from 'child_process'; import { ensureClean } from './paths.js'; -const execAsync = promisify(exec); - type RunTachometer = (testConfigs: ConfigResult[]) => void; +const maxAttempts = 3; + +const executeCommand = (cmd: string) => { + return new Promise((resolve, reject) => { + const tach = spawn(cmd, { shell: true }); + + tach.stdout.on('data', data => { + console.log(data.toString()); + }); + + tach.on('close', code => { + resolve(code); + }); + + tach.on('error', err => { + tach.kill(); + reject(err); + }); + }); +}; + const runTachometer: RunTachometer = async testConfigs => { // Everything will be going to the same results folder for a test run const { resultsDir } = testConfigs[0]; @@ -17,13 +35,30 @@ const runTachometer: RunTachometer = async testConfigs => { console.log('\n--------------------\n'); console.log(`Running test case "${testFile}...`); const cmd = `tach --config ${testFile} --json-file ${resultsFile}`; + console.log(`Test command: ${cmd}`); + let attempt = 0; + let done = false; // Make sure we run these in sequence. // We don't want to run several tests at once as they // may unduly influence each other. - const res = await execAsync(cmd); - console.log(res.stdout); - console.log(`Test complete! Results written to ${resultsFile}.`); + while (!done) { + try { + await executeCommand(cmd); + console.log(`Test complete! Results written to ${resultsFile}.`); + done = true; + } catch (err) { + if (attempt < maxAttempts) { + attempt++; + console.error(`Error running Tachometer!`); + console.error((err as Error).message); + console.log('----------'); + console.log(`Trying again. Attempt ${attempt} of ${maxAttempts}.`); + } else { + throw err; + } + } + } } }; diff --git a/apps/stress-test/scripts/utils/tree/RandomSelectorTreeNode.ts b/apps/stress-test/scripts/utils/tree/RandomSelectorTreeNode.ts new file mode 100644 index 00000000000000..3481dc455e8fdf --- /dev/null +++ b/apps/stress-test/scripts/utils/tree/RandomSelectorTreeNode.ts @@ -0,0 +1,192 @@ +import { LCG } from 'random-seedable'; +import { RandomSelector } from '../../../src/shared/css/RandomSelector.js'; +import { Attribute, RandomSelectorTreeNode, TreeNode, TreeNodeCreateCallback } from '../../../src/shared/tree/types'; + +const rando: LCG = new LCG(4212021); + +const coin = (pTrue: number = 0.5): boolean => rando.coin(pTrue); +const choice = (choices: T[]): T => rando.choice(choices); + +const randomSelector = new RandomSelector(); + +const chances: { [key: string]: number } = { + not: 0.05, + addClassName: 0.5, + addAttribute: 0.2, + buildDescendentSelector: 0.5, + addSibling: 0.1, + addPseudo: 0.1, + useDescendantCombinator: 0.2, + useNonMatchingSelector: 0.5, +}; + +const nonMatchingSelector = '.non-matching-selector'; + +const buildDescendentSelector = ( + node: TreeNode | null, + selector?: string, +): string => { + if (!node) { + return selector ?? ''; + } + + if (!selector) { + selector = choice(getSelectorsFromNode(node, { pseudos: false })); + } + + const parent = node.parent; + + if (!parent) { + return selector; + } + + let selectorChoices = getSelectorsFromNode(parent, { pseudos: false }); + if (coin(chances.useNonMatchingSelector)) { + selectorChoices = [...selectorChoices, nonMatchingSelector]; + } + + selector = ( + maybeNot(choice(selectorChoices)) + + (coin(chances.useDescendantCombinator) ? ' > ' : ' ') + + selector + ).trim(); + + if (coin(chances.buildDescendentSelector)) { + selector = buildDescendentSelector(node.parent, selector); + } + + return selector; +}; + +const getNodeClassNames = () => { + const nodeSelectors = [randomSelector.randomSelector(['class'])]; + if (coin(chances.addClassName)) { + nodeSelectors.push(randomSelector.randomSelector(['class'])); + } + + return nodeSelectors; +}; + +const maybeNot = (selector: string): string => { + if (coin(chances.not)) { + return `:not(${selector})`; + } + + return selector; +}; + +const getAttributes = () => { + const attributes = [] as Attribute[]; + if (coin(chances.addAttribute)) { + const selector = randomSelector.randomSelector(['attribute-name', 'attribute-value']); + const [key, value] = selector.replace(/(\[|\])/g, '').split('='); + attributes.push({ key, value, selector }); + } + + return attributes; +}; + +const getSiblingSelectors = ( + parent: TreeNode | null, + node: TreeNode, +) => { + const siblings = [] as string[]; + + if (parent && coin(chances.addSibling)) { + const combinator = choice(['nth-child', '~', '+']); + if (combinator === 'nth-child') { + siblings.push(randomSelector.randomSelector(['nth-child'])); + } else { + const sibling = choice(parent.children); + if (!sibling) { + return siblings; + } + const siblingSelectorType = choice(['classNames', 'attribute']); + let siblingSelector; + if (siblingSelectorType === 'classNames') { + siblingSelector = choice(sibling.value.classNames) ?? '*'; + } else { + siblingSelector = choice(sibling.value.attributes)?.selector ?? '*'; + } + + const nodeSelectorType = choice(['classNames', 'attribute']); + let nodeSelector; + if (nodeSelectorType === 'classNames') { + nodeSelector = choice(node.value.classNames) ?? '*'; + } else { + nodeSelector = choice(node.value.attributes)?.selector ?? '*'; + } + siblings.push(`${siblingSelector} ${combinator} ${nodeSelector}`); + } + } + + return siblings; +}; + +const getPseudoSelectors = () => { + const pseudo = [] as string[]; + + if (coin(chances.addPsuedo)) { + pseudo.push(randomSelector.randomSelector(['pseudo-element'])); + } + + return pseudo; +}; + +type IncludeOptions = { + [k in keyof RandomSelectorTreeNode]?: boolean; +}; +const getSelectorsFromNode = (node: TreeNode, include?: IncludeOptions): string[] => { + const { classNames = true, attributes = true, siblings = true, pseudos = true } = include ?? {}; + + let selectors = [] as string[]; + + if (classNames) { + selectors = selectors.concat(...node.value.classNames); + } + + if (attributes) { + selectors = selectors.concat(...node.value.attributes.map(attr => attr.selector)); + } + + if (siblings) { + selectors = selectors.concat(...node.value.siblings); + } + + if (pseudos) { + selectors = selectors.concat(...node.value.pseudos); + } + + return selectors; +}; + +export type RandomSelectorTreeCreator = (selectors: string[]) => TreeNodeCreateCallback; + +export const selectorTreeCreator: RandomSelectorTreeCreator = selectors => { + const createSelectorTree: TreeNodeCreateCallback = (parent, depth, breadth) => { + const node = { + value: { + name: `${depth}-${breadth}`, + classNames: getNodeClassNames(), + attributes: getAttributes(), + siblings: [] as string[], + pseudos: getPseudoSelectors(), + }, + children: [], + parent, + }; + + node.value.siblings = getSiblingSelectors(parent, node); + + if (coin(chances.buildDescendentSelector)) { + const descendentSelector = buildDescendentSelector(node); + selectors.push(descendentSelector); + } else { + selectors.push(...getSelectorsFromNode(node)); + } + + return node; + }; + + return createSelectorTree; +}; diff --git a/apps/stress-test/scripts/utils/tree/RandomTree.ts b/apps/stress-test/scripts/utils/tree/RandomTree.ts new file mode 100644 index 00000000000000..6c83d6922b9e2e --- /dev/null +++ b/apps/stress-test/scripts/utils/tree/RandomTree.ts @@ -0,0 +1,99 @@ +import { TreeNode, TreeNodeCreateCallback } from '../../../src/shared/tree/types'; +import { TestTreeFixture } from '../../../src/shared/utils/testUtils.js'; +import { LCG } from 'random-seedable'; + +const defaultSeed = 4212021; + +type TreeParams = { + minDepth?: number; + maxDepth?: number; + minBreadth?: number; + maxBreadth?: number; + seed?: number; + targetSize?: number; +}; + +export class RandomTree { + private numNodes: number; + private minDepth: number; + private maxDepth: number; + private minBreadth: number; + private maxBreadth: number; + private targetSize: number; + + private rando: LCG; + + constructor({ + minDepth = 1, + maxDepth = 15, + minBreadth = 1, + maxBreadth = 15, + seed = defaultSeed, + targetSize, + }: TreeParams = {}) { + this.minDepth = Number(minDepth); + this.maxDepth = Number(maxDepth); + this.minBreadth = Number(minBreadth); + this.maxBreadth = Number(maxBreadth); + this.targetSize = targetSize ? Number(targetSize) : Infinity; + + this.rando = new LCG(seed); + this.numNodes = 0; + } + + public build = (createNode: TreeNodeCreateCallback): TreeNode => { + this.numNodes = 1; + const root = createNode(null, 0, 0); + let tree = this._doBuild(createNode, root, 1); + while (this.numNodes < this.targetSize) { + tree = this._doBuild(createNode, root, 1); + } + + return tree; + }; + + public fromFixture = (fixture: TestTreeFixture['tree'], parent: TreeNode | null = null): TreeNode => { + const root: TreeNode = { + value: fixture.value as T, + children: [], + parent, + }; + + for (const child of fixture.children) { + root.children.push(this.fromFixture(child, root)); + } + + return root; + }; + + private _randomDepth = (max?: number): number => { + return this.rando.randRange(this.minDepth, max ?? this.maxDepth); + }; + + private _randomBreadth = (max?: number): number => { + return this.rando.randRange(this.minBreadth, max ?? this.maxBreadth); + }; + + private _doBuild = ( + createNode: TreeNodeCreateCallback, + parent: TreeNode, + currentDepth: number, + currentBreadth: number = this.maxBreadth, + ): TreeNode => { + const breadth = this._randomBreadth(currentBreadth); + const depth = this._randomDepth(Math.max(this.maxDepth - currentDepth, this.minDepth)); + + for (let i = 0; i < breadth && this.numNodes < this.targetSize; i++) { + this.numNodes++; + const node = createNode(parent, currentDepth, breadth); + + parent.children.push(node); + + if (currentDepth < depth && this.numNodes < this.targetSize) { + this._doBuild(createNode, node, currentDepth + 1); + } + } + + return parent; + }; +} diff --git a/apps/stress-test/src/components/v8/StressApp.tsx b/apps/stress-test/src/components/v8/StressApp.tsx index df6eadaf54f5a0..6e3da22b363d7b 100644 --- a/apps/stress-test/src/components/v8/StressApp.tsx +++ b/apps/stress-test/src/components/v8/StressApp.tsx @@ -1,19 +1,19 @@ import * as React from 'react'; -import { getTestParams } from '../../shared/testParams'; -import { performanceMeasure } from '../../shared/performanceMeasure'; +import { getTestOptions } from '../../shared/utils/testOptions'; +import { performanceMeasure } from '../../shared/utils/performanceMeasure'; import { StressContainer } from './StressContainer'; export const StressApp = () => { - const [numChildren, setNumChildren] = React.useState(getTestParams().numStartNodes); + const [numChildren, setNumChildren] = React.useState(Number(getTestOptions().numStartNodes)); React.useEffect(() => { - const { test, numStartNodes, numAddNodes, numRemoveNodes } = getTestParams(); + const { test, numStartNodes, numAddNodes, numRemoveNodes } = getTestOptions(); if (test === 'add-node') { performanceMeasure('stress', 'start'); - setNumChildren(numStartNodes + numAddNodes); + setNumChildren(Number(numStartNodes) + Number(numAddNodes)); } else if (test === 'removeNode') { performanceMeasure('stress', 'start'); - setNumChildren(numStartNodes - numRemoveNodes); + setNumChildren(Number(numStartNodes) - Number(numRemoveNodes)); } }, []); diff --git a/apps/stress-test/src/components/v8/StressContainer.tsx b/apps/stress-test/src/components/v8/StressContainer.tsx index 26ef7d3ae0c169..03d06fda61889f 100644 --- a/apps/stress-test/src/components/v8/StressContainer.tsx +++ b/apps/stress-test/src/components/v8/StressContainer.tsx @@ -1,8 +1,8 @@ import { mergeStyleSets } from '@fluentui/react'; import * as React from 'react'; -import { injectGlobalCss } from '../../shared/injectStyles'; -import { getTestParams } from '../../shared/testParams'; -import { performanceMeasure } from '../../shared/performanceMeasure'; +import { injectGlobalCss } from '../../shared/css/injectStyles'; +import { getTestOptions } from '../../shared/utils/testOptions'; +import { performanceMeasure } from '../../shared/utils/performanceMeasure'; import { StressComponent } from './StressComponent'; const styles = mergeStyleSets({ @@ -21,7 +21,7 @@ export const StressContainer: React.FC = ({ numChildren = const [checked, setChecked] = React.useState(false); React.useEffect(() => { - const { test } = getTestParams(); + const { test } = getTestOptions(); if (test === 'mount') { performanceMeasure('stress', 'start'); } else if (test === 'inject-styles') { diff --git a/apps/stress-test/src/components/v9/StressApp.tsx b/apps/stress-test/src/components/v9/StressApp.tsx index 89116b36a7c470..32e02e406e22f3 100644 --- a/apps/stress-test/src/components/v9/StressApp.tsx +++ b/apps/stress-test/src/components/v9/StressApp.tsx @@ -1,20 +1,20 @@ import * as React from 'react'; import { FluentProvider, webLightTheme } from '@fluentui/react-components'; -import { getTestParams } from '../../shared/testParams'; -import { performanceMeasure } from '../../shared/performanceMeasure'; +import { getTestOptions } from '../../shared/utils/testOptions'; +import { performanceMeasure } from '../../shared/utils/performanceMeasure'; import { StressContainer } from './StressContainer'; export const StressApp = () => { - const [numChildren, setNumChildren] = React.useState(getTestParams().numStartNodes); + const [numChildren, setNumChildren] = React.useState(Number(getTestOptions().numStartNodes)); React.useEffect(() => { - const { test, numStartNodes, numAddNodes, numRemoveNodes } = getTestParams(); + const { test, numStartNodes, numAddNodes, numRemoveNodes } = getTestOptions(); if (test === 'add-node') { performanceMeasure('stress', 'start'); - setNumChildren(numStartNodes + numAddNodes); + setNumChildren(Number(numStartNodes) + Number(numAddNodes)); } else if (test === 'removeNode') { performanceMeasure('stress', 'start'); - setNumChildren(numStartNodes - numRemoveNodes); + setNumChildren(Number(numStartNodes) - Number(numRemoveNodes)); } }, []); diff --git a/apps/stress-test/src/components/v9/StressContainer.tsx b/apps/stress-test/src/components/v9/StressContainer.tsx index 5d5ed9b956fa64..caeb16cd5b7444 100644 --- a/apps/stress-test/src/components/v9/StressContainer.tsx +++ b/apps/stress-test/src/components/v9/StressContainer.tsx @@ -1,8 +1,8 @@ import { makeStyles, shorthands } from '@fluentui/react-components'; import * as React from 'react'; -import { injectGlobalCss } from '../../shared/injectStyles'; -import { getTestParams } from '../../shared/testParams'; -import { performanceMeasure } from '../../shared/performanceMeasure'; +import { injectGlobalCss } from '../../shared/css/injectStyles'; +import { getTestOptions } from '../../shared/utils/testOptions'; +import { performanceMeasure } from '../../shared/utils/performanceMeasure'; import { StressComponent } from './StressComponent'; const useStyles = makeStyles({ @@ -21,7 +21,7 @@ export const StressContainer: React.FC = ({ numChildren = const [checked, setChecked] = React.useState(false); React.useEffect(() => { - const { test } = getTestParams(); + const { test } = getTestOptions(); if (test === 'mount') { performanceMeasure('stress', 'start'); } else if (test === 'inject-styles') { diff --git a/apps/stress-test/src/components/wc/stressApp.wc.ts b/apps/stress-test/src/components/wc/stressApp.wc.ts index 30132397f0de0e..c4c993d473ae72 100644 --- a/apps/stress-test/src/components/wc/stressApp.wc.ts +++ b/apps/stress-test/src/components/wc/stressApp.wc.ts @@ -1,6 +1,6 @@ import { FASTElement, customElement, attr, html, css, repeat, ValueConverter } from '@microsoft/fast-element'; -import { getTestParams } from '../../shared/testParams'; -import { performanceMeasure } from '../../shared/performanceMeasure'; +import { getTestOptions } from '../../shared/utils/testOptions'; +import { performanceMeasure } from '../../shared/utils/performanceMeasure'; import { StressComponent } from './stressComponent.wc'; const styles = css` @@ -43,7 +43,7 @@ export class StressApp extends FASTElement { public connectedCallback(): void { super.connectedCallback(); - const { test, numStartNodes, numAddNodes, numRemoveNodes } = getTestParams(); + const { test, numStartNodes, numAddNodes, numRemoveNodes } = getTestOptions(); if (test === 'prop-update') { setTimeout(() => { @@ -53,12 +53,12 @@ export class StressApp extends FASTElement { } else if (test === 'add-node') { setTimeout(() => { performanceMeasure('stress', 'start'); - this.numchildren = numStartNodes + numAddNodes; + this.numchildren = Number(numStartNodes) + Number(numAddNodes); }, 2000); } else if (test === 'remove-node') { setTimeout(() => { performanceMeasure('stress', 'start'); - this.numchildren = numStartNodes - numRemoveNodes; + this.numchildren = Number(numStartNodes) - Number(numRemoveNodes); }, 2000); } } diff --git a/apps/stress-test/src/components/wc/stressContainer.wc.ts b/apps/stress-test/src/components/wc/stressContainer.wc.ts index 26a3df9100a5d2..49507d8294c140 100644 --- a/apps/stress-test/src/components/wc/stressContainer.wc.ts +++ b/apps/stress-test/src/components/wc/stressContainer.wc.ts @@ -1,7 +1,7 @@ import { FASTElement, customElement, html, css } from '@microsoft/fast-element'; -import { injectGlobalCss } from '../../shared/injectStyles'; -import { getTestParams } from '../../shared/testParams'; -import { performanceMeasure } from '../../shared/performanceMeasure'; +import { injectGlobalCss } from '../../shared/css/injectStyles'; +import { getTestOptions } from '../../shared/utils/testOptions'; +import { performanceMeasure } from '../../shared/utils/performanceMeasure'; const styles = css` :host { @@ -26,7 +26,7 @@ export class StressContainer extends FASTElement { public connectedCallback(): void { super.connectedCallback(); - const { test } = getTestParams(); + const { test } = getTestOptions(); if (test === 'mount') { performance.mark('start'); diff --git a/apps/stress-test/src/pages/v8/stress-tree/index.html b/apps/stress-test/src/pages/v8/stress-tree/index.html new file mode 100644 index 00000000000000..9ff4031b962193 --- /dev/null +++ b/apps/stress-test/src/pages/v8/stress-tree/index.html @@ -0,0 +1,11 @@ + + + + + + v8: Stress Tree + + +
+ + diff --git a/apps/stress-test/src/pages/v8/stress-tree/index.tsx b/apps/stress-test/src/pages/v8/stress-tree/index.tsx new file mode 100644 index 00000000000000..055cce6a621342 --- /dev/null +++ b/apps/stress-test/src/pages/v8/stress-tree/index.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { initializeIcons, ThemeProvider } from '@fluentui/react'; +import { getTestOptions } from '../../../shared/utils/testOptions'; +import { ReactTest } from '../../../shared/react/ReactTest'; + +initializeIcons(); + +const { fixtureName, rendererName, r } = getTestOptions(); +document.title += ' | ' + r ?? rendererName; +ReactDOM.render( + + + , + document.getElementById('root'), +); diff --git a/apps/stress-test/src/pages/v9/stress-tree/index.html b/apps/stress-test/src/pages/v9/stress-tree/index.html new file mode 100644 index 00000000000000..5d99e8898db678 --- /dev/null +++ b/apps/stress-test/src/pages/v9/stress-tree/index.html @@ -0,0 +1,11 @@ + + + + + + v9: Stress Tree + + +
+ + diff --git a/apps/stress-test/src/pages/v9/stress-tree/index.tsx b/apps/stress-test/src/pages/v9/stress-tree/index.tsx new file mode 100644 index 00000000000000..c8b0001571dc11 --- /dev/null +++ b/apps/stress-test/src/pages/v9/stress-tree/index.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { FluentProvider, webLightTheme } from '@fluentui/react-components'; +import { ReactTest } from '../../../shared/react/ReactTest'; +import { getTestOptions } from '../../../shared/utils/testOptions'; + +const { fixtureName, rendererName, r } = getTestOptions(); +document.title += ' | ' + r ?? rendererName; +ReactDOM.render( + + + , + document.getElementById('root'), +); diff --git a/apps/stress-test/src/pages/wc/simple-stress/index.wc.ts b/apps/stress-test/src/pages/wc/simple-stress/index.wc.ts index 951bb24195e7ae..3f28e7f78fb9cf 100644 --- a/apps/stress-test/src/pages/wc/simple-stress/index.wc.ts +++ b/apps/stress-test/src/pages/wc/simple-stress/index.wc.ts @@ -9,11 +9,11 @@ import { import { StressApp } from '../../../components/wc/stressApp.wc'; import { StressComponent } from '../../../components/wc/stressComponent.wc'; import { StressContainer } from '../../../components/wc/stressContainer.wc'; -import { getTestParams } from '../../../shared/testParams'; +import { getTestOptions } from '../../../shared/utils/testOptions'; -const testParams = getTestParams(); +const testOptions = getTestOptions(); -document.querySelector('stress-app')?.setAttribute('numchildren', testParams.numStartNodes.toString()); +document.querySelector('stress-app')?.setAttribute('numchildren', testOptions.numStartNodes.toString()); provideFluentDesignSystem().register( fluentButton(), diff --git a/apps/stress-test/src/pages/wc/stress-tree/index.html b/apps/stress-test/src/pages/wc/stress-tree/index.html new file mode 100644 index 00000000000000..c20b268b0cb78d --- /dev/null +++ b/apps/stress-test/src/pages/wc/stress-tree/index.html @@ -0,0 +1,11 @@ + + + + + + WC: Stress Tree + + +
+ + diff --git a/apps/stress-test/src/pages/wc/stress-tree/index.wc.ts b/apps/stress-test/src/pages/wc/stress-tree/index.wc.ts new file mode 100644 index 00000000000000..09d14b03269fe6 --- /dev/null +++ b/apps/stress-test/src/pages/wc/stress-tree/index.wc.ts @@ -0,0 +1,9 @@ +import { getTestOptions } from '../../../shared/utils/testOptions'; +import { wcTest } from '../../../shared/wc/WCTest'; + +wcTest().then(testNode => { + const { r, rendererName } = getTestOptions(); + document.title += ' | ' + r ?? rendererName; + + document.querySelector('#app')?.appendChild(testNode!); +}); diff --git a/apps/stress-test/src/renderers/v8/button.tsx b/apps/stress-test/src/renderers/v8/button.tsx new file mode 100644 index 00000000000000..74ca75be0686dd --- /dev/null +++ b/apps/stress-test/src/renderers/v8/button.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { DefaultButton } from '@fluentui/react'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v8/checkbox.tsx b/apps/stress-test/src/renderers/v8/checkbox.tsx new file mode 100644 index 00000000000000..d52032c41e0917 --- /dev/null +++ b/apps/stress-test/src/renderers/v8/checkbox.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { Checkbox } from '@fluentui/react'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v8/spinButton.tsx b/apps/stress-test/src/renderers/v8/spinButton.tsx new file mode 100644 index 00000000000000..1f0db14c83fb58 --- /dev/null +++ b/apps/stress-test/src/renderers/v8/spinButton.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { SpinButton } from '@fluentui/react'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v8/stress.tsx b/apps/stress-test/src/renderers/v8/stress.tsx new file mode 100644 index 00000000000000..4bbd00d65c589d --- /dev/null +++ b/apps/stress-test/src/renderers/v8/stress.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { + mergeStyleSets, + DefaultButton, + Separator, + Checkbox, + SpinButton, + Spinner, + DefaultPalette, +} from '@fluentui/react'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const styles = mergeStyleSets({ + stressComponent: { + display: 'flex', + flexDirection: 'column', + rowGap: '10px', + maxWidth: '300px', + border: `1px solid ${DefaultPalette.neutralLight}`, + borderRadius: '4px', + padding: '10px', + }, +}); + +export type StressComponentProps = { + id?: string; + checked: boolean; +}; + +const StressComponent: React.FC = ({ id = '', checked }) => { + return ( +
+ A button + + + + + + + +
+ ); +}; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v8/textField.tsx b/apps/stress-test/src/renderers/v8/textField.tsx new file mode 100644 index 00000000000000..1d0d855cfae7af --- /dev/null +++ b/apps/stress-test/src/renderers/v8/textField.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { TextField } from '@fluentui/react'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v8/textarea.tsx b/apps/stress-test/src/renderers/v8/textarea.tsx new file mode 100644 index 00000000000000..2dbd15db5085ea --- /dev/null +++ b/apps/stress-test/src/renderers/v8/textarea.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { TextField } from '@fluentui/react'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/btn/basic.tsx b/apps/stress-test/src/renderers/v9/btn/basic.tsx new file mode 100644 index 00000000000000..0c30dbc0592ac8 --- /dev/null +++ b/apps/stress-test/src/renderers/v9/btn/basic.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import { useRootStyles } from './styles'; +import { ReactSelectorTreeComponentRenderer } from '../../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + const rootStyles = useRootStyles(); + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/btn/idiomatic.tsx b/apps/stress-test/src/renderers/v9/btn/idiomatic.tsx new file mode 100644 index 00000000000000..8d5e66f36fc800 --- /dev/null +++ b/apps/stress-test/src/renderers/v9/btn/idiomatic.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { + useButton_unstable, + renderButton_unstable, + ForwardRefComponent, + ButtonProps, + ButtonState, + mergeClasses, + buttonClassNames, +} from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../../shared/react/types'; +import { useRootStyles, useRootDisabledStyles } from './styles'; + +const useButtonIdiomaticOverrideStyles = (state: ButtonState): ButtonState => { + const rootStyles = useRootStyles(); + const rootDisabledStyles = useRootDisabledStyles(); + + const { disabled, disabledFocusable } = state; + + state.root.className = mergeClasses( + buttonClassNames.root, + rootStyles.base, + rootStyles.highContrast, + (disabled || disabledFocusable) && rootDisabledStyles.base, + (disabled || disabledFocusable) && rootDisabledStyles.highContrast, + state.root.className, + ); + + return state; +}; + +const ButtonIdiomaticOverride: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useButton_unstable(props, ref); + + useButtonIdiomaticOverrideStyles(state); + + return renderButton_unstable(state); +}) as ForwardRefComponent; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return {`${node.value.name}, ${index}`}; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/btn/override.tsx b/apps/stress-test/src/renderers/v9/btn/override.tsx new file mode 100644 index 00000000000000..1e4daeeed1d3ee --- /dev/null +++ b/apps/stress-test/src/renderers/v9/btn/override.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { Button, ButtonProps, makeStyles, mergeClasses } from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../../shared/react/types'; +import { useRootStyles, useRootDisabledStyles } from './styles'; + +const useButtonOverridesStyles = makeStyles({ + base: { + fontWeight: 'initial', + minWidth: 'initial', + fontSize: 'unset', + lineHeight: 'normal', + }, +}); + +const ButtonOverride: React.FC = props => { + const rootStyles = useRootStyles(); + const rootDisabledStyles = useRootDisabledStyles(); + const buttonOverrideStyles = useButtonOverridesStyles(); + + const { className, disabled, disabledFocusable } = props; + const applyDisabledStyles = disabled || disabledFocusable; + + return ( + ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/checkbox.tsx b/apps/stress-test/src/renderers/v9/checkbox.tsx new file mode 100644 index 00000000000000..976203251a208a --- /dev/null +++ b/apps/stress-test/src/renderers/v9/checkbox.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { Checkbox } from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/input.tsx b/apps/stress-test/src/renderers/v9/input.tsx new file mode 100644 index 00000000000000..3a36f5c647e3c5 --- /dev/null +++ b/apps/stress-test/src/renderers/v9/input.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { Input } from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/spinButton.tsx b/apps/stress-test/src/renderers/v9/spinButton.tsx new file mode 100644 index 00000000000000..c32fa1ac1785b7 --- /dev/null +++ b/apps/stress-test/src/renderers/v9/spinButton.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { SpinButton } from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/spinButton/noPseudos.tsx b/apps/stress-test/src/renderers/v9/spinButton/noPseudos.tsx new file mode 100644 index 00000000000000..c4ffd670a77531 --- /dev/null +++ b/apps/stress-test/src/renderers/v9/spinButton/noPseudos.tsx @@ -0,0 +1,483 @@ +import * as React from 'react'; +import { + useSpinButton_unstable, + renderSpinButton_unstable, + ForwardRefComponent, + SpinButtonProps, + SpinButtonState, + makeStyles, + mergeClasses, + shorthands, + spinButtonClassNames, + tokens, + useInputStyles_unstable, +} from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../../shared/react/types'; + +const spinButtonExtraClassNames = { + buttonActive: 'fui-SpinButton__button_active', +}; + +const useRootStyles = makeStyles({ + base: { + display: 'inline-grid', + gridTemplateColumns: `1fr 24px`, + gridTemplateRows: '1fr 1fr', + columnGap: tokens.spacingHorizontalXS, + rowGap: 0, + paddingRight: 0, + position: 'relative', + // Remove the border styles from react-input + ...shorthands.border('0'), + isolation: 'isolate', + + // Apply border styles on the ::before pseudo element. + // We cannot use ::after since react-input uses that + // for the selector styles. + // Using the pseudo element allows us to place the border + // above content in the component which ensures the buttons + // line up visually with the border as expected. Without this + // there is a bit of a gap which can become very noticeable + // at high zoom or when OS zoom levels are not divisible by 2 + // (e.g., 150% on Windows in Firefox) + // This is most noticeable on the "outline" appearance which is + // also the default so it feels worth the extra ceremony to get right. + '::before': { + content: '""', + boxSizing: 'border-box', + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + ...shorthands.borderRadius(tokens.borderRadiusMedium), + pointerEvents: 'none', + zIndex: 10, + }, + + '::after': { + right: 0, + bottom: 0, + left: 0, + zIndex: 20, + }, + }, + + outline: { + '::before': { + ...shorthands.border('1px', 'solid', tokens.colorNeutralStroke1), + borderBottomColor: tokens.colorNeutralStrokeAccessible, + }, + }, + + outlineInteractive: { + ':hover': { + '::before': { + ...shorthands.borderColor(tokens.colorNeutralStroke1Hover), + borderBottomColor: tokens.colorNeutralStrokeAccessibleHover, + }, + }, + // DO NOT add a space between the selectors! It changes the behavior of make-styles. + ':active,:focus-within': { + '::before': { + ...shorthands.borderColor(tokens.colorNeutralStroke1Pressed), + borderBottomColor: tokens.colorNeutralStrokeAccessiblePressed, + }, + }, + }, + + underline: { + '::before': { + ...shorthands.borderRadius(0), // corners look strange if rounded + ...shorthands.borderBottom('1px', 'solid', tokens.colorNeutralStrokeAccessible), + }, + }, + + underlineInteractive: { + ':hover': { + '::before': { + borderBottomColor: tokens.colorNeutralStrokeAccessibleHover, + }, + }, + // DO NOT add a space between the selectors! It changes the behavior of make-styles. + ':active,:focus-within': { + '::before': { + borderBottomColor: tokens.colorNeutralStrokeAccessiblePressed, + }, + }, + }, + + filled: { + '::before': { + ...shorthands.border('1px', 'solid', tokens.colorTransparentStroke), + }, + }, + + filledInteractive: { + // DO NOT add a space between the selectors! It changes the behavior of make-styles. + ':hover,:focus-within': { + '::before': { + // also handles pressed border color (:active) + ...shorthands.borderColor(tokens.colorTransparentStrokeInteractive), + }, + }, + }, + + disabled: { + '@media (forced-colors: active)': { + ...shorthands.borderColor('GrayText'), + }, + }, + + outlineDisabled: { + '::before': { + ...shorthands.border('1px', 'solid', tokens.colorNeutralStrokeDisabled), + ...shorthands.borderRadius(tokens.borderRadiusMedium), // because underline doesn't usually have a radius + }, + }, + + underlineDisabled: { + '::before': { + ...shorthands.borderBottom('1px', 'solid', tokens.colorTransparentStrokeDisabled), + }, + }, + + filledDisabled: { + '::before': { + ...shorthands.border('1px', 'solid', tokens.colorTransparentStrokeDisabled), + }, + }, +}); + +const useInputStyles = makeStyles({ + base: { + gridColumnStart: '1', + gridColumnEnd: '2', + gridRowStart: '1', + gridRowEnd: '3', + outlineStyle: 'none', + ...shorthands.padding(0), + }, +}); + +const useButtonStyles = makeStyles({ + base: { + display: 'inline-flex', + width: '24px', + alignItems: 'center', + justifyContent: 'center', + ...shorthands.border(0), + position: 'absolute', + + outlineStyle: 'none', + height: '100%', + + ':enabled:hover': { + cursor: 'pointer', + }, + + ':active': { + outlineStyle: 'none', + }, + + ':disabled': { + cursor: 'not-allowed', + }, + }, + + incrementButton: { + gridColumnStart: '2', + gridColumnEnd: '3', + gridRowStart: '1', + gridRowEnd: '2', + ...shorthands.borderRadius(0, tokens.borderRadiusMedium, 0, 0), + }, + + // TODO: revisit these padding numbers for aligning the icon. + // Padding values aren't perfect. + // The icon doesn't align perfectly with the Figma designs. + // It's set in a 16x16px square but the artwork is inset from that + // so I've had to compute the numbers by handle. + // Additionally the design uses fractional values so these are + // rounded to the nearest integer. + incrementButtonSmall: { + ...shorthands.padding('3px', '5px', '0px', '5px'), + }, + + incrementButtonMedium: { + ...shorthands.padding('4px', '5px', '1px', '5px'), + }, + + decrementButton: { + gridColumnStart: '2', + gridColumnEnd: '3', + gridRowStart: '2', + gridRowEnd: '3', + ...shorthands.borderRadius(0, 0, tokens.borderRadiusMedium, 0), + }, + + decrementButtonSmall: { + ...shorthands.padding('0px', '5px', '3px', '5px'), + }, + + decrementButtonMedium: { + ...shorthands.padding('1px', '5px', '4px', '5px'), + }, + + outline: { + backgroundColor: 'transparent', + color: tokens.colorNeutralForeground3, + ':enabled': { + ':hover': { + color: tokens.colorNeutralForeground3Hover, + backgroundColor: tokens.colorSubtleBackgroundHover, + }, + ':active': { + color: tokens.colorNeutralForeground3Pressed, + backgroundColor: tokens.colorSubtleBackgroundPressed, + }, + [`&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForeground3Pressed, + backgroundColor: tokens.colorSubtleBackgroundPressed, + }, + }, + ':disabled': { + color: tokens.colorNeutralForegroundDisabled, + }, + }, + + underline: { + backgroundColor: 'transparent', + color: tokens.colorNeutralForeground3, + ':enabled': { + ':hover': { + color: tokens.colorNeutralForeground3Hover, + backgroundColor: tokens.colorSubtleBackgroundHover, + }, + ':active': { + color: tokens.colorNeutralForeground3Pressed, + backgroundColor: tokens.colorSubtleBackgroundPressed, + }, + [`&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForeground3Pressed, + backgroundColor: tokens.colorSubtleBackgroundPressed, + }, + }, + ':disabled': { + color: tokens.colorNeutralForegroundDisabled, + }, + }, + 'filled-darker': { + backgroundColor: 'transparent', + color: tokens.colorNeutralForeground3, + + ':enabled': { + ':hover': { + color: tokens.colorNeutralForeground3Hover, + backgroundColor: tokens.colorNeutralBackground3Hover, + }, + ':active': { + color: tokens.colorNeutralForeground3Pressed, + backgroundColor: tokens.colorNeutralBackground3Pressed, + }, + [`&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForeground3Pressed, + backgroundColor: tokens.colorNeutralBackground3Pressed, + }, + }, + ':disabled': { + color: tokens.colorNeutralForegroundDisabled, + }, + }, + 'filled-lighter': { + backgroundColor: 'transparent', + color: tokens.colorNeutralForeground3, + + ':enabled': { + ':hover': { + color: tokens.colorNeutralForeground3Hover, + backgroundColor: tokens.colorNeutralBackground1Hover, + }, + [`:active,&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForeground3Pressed, + backgroundColor: tokens.colorNeutralBackground1Pressed, + }, + }, + ':disabled': { + color: tokens.colorNeutralForegroundDisabled, + }, + }, +}); + +// Cannot just disable button as they need to remain +// exposed to ATs like screen readers. +const useButtonDisabledStyles = makeStyles({ + base: { + cursor: 'not-allowed', + + ':hover': { + cursor: 'not-allowed', + }, + }, + + outline: { + color: tokens.colorNeutralForegroundDisabled, + ':enabled': { + ':hover': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + ':active': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + [`&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + }, + }, + + underline: { + color: tokens.colorNeutralForegroundDisabled, + ':enabled': { + ':hover': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + ':active': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + [`&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + }, + }, + + 'filled-darker': { + color: tokens.colorNeutralForegroundDisabled, + ':enabled': { + ':hover': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + ':active': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + [`&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + }, + }, + + 'filled-lighter': { + color: tokens.colorNeutralForegroundDisabled, + ':enabled': { + ':hover': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + ':active': { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + [`&.${spinButtonExtraClassNames.buttonActive}`]: { + color: tokens.colorNeutralForegroundDisabled, + backgroundColor: 'transparent', + }, + }, + }, +}); + +/** + * Apply styling to the SpinButton slots based on the state + */ +export const useSpinButtonStyles_unstable = (state: SpinButtonState): SpinButtonState => { + const { appearance, atBound, spinState, size } = state; + const disabled = state.input.disabled; + const filled = appearance.startsWith('filled'); + + const rootStyles = useRootStyles(); + const buttonStyles = useButtonStyles(); + const buttonDisabledStyles = useButtonDisabledStyles(); + const inputStyles = useInputStyles(); + + // Grab the root className here so we can be sure to merge is last + const rootClassName = state.root.className; + state.root.className = undefined; + // Reuse react-input's styles without re-using the Input component. + useInputStyles_unstable({ + size, + appearance, + input: state.input, + root: state.root, + components: { + root: 'span', + input: 'input', + contentBefore: 'span', + contentAfter: 'span', + }, + }); + + state.root.className = mergeClasses( + state.root.className, // Get the classes from useInputStyles_unstable + spinButtonClassNames.root, + rootStyles.base, + appearance === 'outline' && rootStyles.outline, + appearance === 'underline' && rootStyles.underline, + filled && rootStyles.filled, + !disabled && appearance === 'outline' && rootStyles.outlineInteractive, + !disabled && appearance === 'underline' && rootStyles.underlineInteractive, + !disabled && filled && rootStyles.filledInteractive, + disabled && rootStyles.disabled, + disabled && appearance === 'outline' && rootStyles.outlineDisabled, + disabled && appearance === 'underline' && rootStyles.underlineDisabled, + disabled && filled && rootStyles.filledDisabled, + rootClassName, // Make sure any original class name is applied last + ); + + state.incrementButton.className = mergeClasses( + spinButtonClassNames.incrementButton, + spinState === 'up' && `${spinButtonExtraClassNames.buttonActive}`, + buttonStyles.base, + buttonStyles.incrementButton, + buttonStyles[appearance], + size === 'small' ? buttonStyles.incrementButtonSmall : buttonStyles.incrementButtonMedium, + (atBound === 'max' || atBound === 'both') && buttonDisabledStyles.base, + (atBound === 'max' || atBound === 'both') && buttonDisabledStyles[appearance], + state.incrementButton.className, + ); + state.decrementButton.className = mergeClasses( + spinButtonClassNames.decrementButton, + spinState === 'down' && `${spinButtonExtraClassNames.buttonActive}`, + buttonStyles.base, + buttonStyles.decrementButton, + buttonStyles[appearance], + size === 'small' ? buttonStyles.decrementButtonSmall : buttonStyles.decrementButtonMedium, + (atBound === 'min' || atBound === 'both') && buttonDisabledStyles.base, + (atBound === 'min' || atBound === 'both') && buttonDisabledStyles[appearance], + state.decrementButton.className, + ); + + state.input.className = mergeClasses(spinButtonClassNames.input, state.input.className, inputStyles.base); + + return state; +}; + +const SpinButtonNoPseudos: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useSpinButton_unstable(props, ref); + + useSpinButtonStyles_unstable(state); + + return renderSpinButton_unstable(state); +}) as ForwardRefComponent; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/spinButton/vanilla.tsx b/apps/stress-test/src/renderers/v9/spinButton/vanilla.tsx new file mode 100644 index 00000000000000..02451c886d9f03 --- /dev/null +++ b/apps/stress-test/src/renderers/v9/spinButton/vanilla.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { ReactSelectorTreeComponentRenderer } from '../../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/stress.tsx b/apps/stress-test/src/renderers/v9/stress.tsx new file mode 100644 index 00000000000000..3915d1cee00489 --- /dev/null +++ b/apps/stress-test/src/renderers/v9/stress.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { + makeStyles, + tokens, + shorthands, + Button, + Divider, + Checkbox, + Label, + SpinButton, + Spinner, +} from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const useStyles = makeStyles({ + stressComponent: { + display: 'flex', + flexDirection: 'column', + rowGap: '10px', + maxWidth: '300px', + ...shorthands.border('1px', 'solid', tokens.colorNeutralStroke1), + ...shorthands.borderRadius(tokens.borderRadiusMedium), + ...shorthands.padding('10px'), + }, +}); + +export type StressComponentProps = { + id?: string; + checked: boolean; +}; + +const StressComponent: React.FC = ({ id = '', checked }) => { + const styles = useStyles(); + + return ( +
+ + + + + + + + + +
+ ); +}; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return ; +}; + +export default componentRenderer; diff --git a/apps/stress-test/src/renderers/v9/textarea.tsx b/apps/stress-test/src/renderers/v9/textarea.tsx new file mode 100644 index 00000000000000..74e67548d06c9f --- /dev/null +++ b/apps/stress-test/src/renderers/v9/textarea.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { Textarea } from '@fluentui/react-components'; +import { ReactSelectorTreeComponentRenderer } from '../../shared/react/types'; + +const componentRenderer: ReactSelectorTreeComponentRenderer = (node, depth, index) => { + return