Add tooling packages from the FAST repository

# Pull Request

## 📖 Description

<!--- Provide some background and a description of your work. -->
This pull request adds the `@microsoft/fast-tooling`, `@microsoft/fast-tooling-react`, and `@microsoft/fast-tooling-wasm` packages to the repository using NPM 7 workspaces.

This should allow:
- Running the `npm run test` from the package root to run all tests
- In the individual `fast-tooling` and `fast-tooling-react` packages running `npm start` to kick off a build of the `webpack-dev-server`

## 👩‍💻 Reviewer Notes

<!---
Provide some notes for reviewers to help them provide targeted feedback and testing.
-->
Pull down the repository, use `npm i` or `npm install` to install dependencies, then use `npm run test` at the root and `npm start` in the `fast-tooling` and `fast-tooling-react` packages.

Keep in mind that the requirements for this repository are now NPM 7 and NodeJS 16, ensure these are both installed.

##  Checklist

### General

<!--- Review the list and put an x in the boxes that apply. -->

- [ ] I have added tests for my changes.
- [x] I have tested my changes.
- [x] I have updated the project documentation to reflect my changes.
This commit is contained in:
Jane Chu 2021-09-16 10:59:57 -07:00 коммит произвёл GitHub
Родитель 0f18e5debd
Коммит d39991bc75
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
485 изменённых файлов: 133431 добавлений и 4 удалений

11
.eslintignore Normal file
Просмотреть файл

@ -0,0 +1,11 @@
# Never lint test files
*.spec.ts
# Never lint node_modules
node_modules
# Never lint build output
dist
# Never lint coverage output
coverage

75
.eslintrc.js Normal file
Просмотреть файл

@ -0,0 +1,75 @@
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "import"],
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
],
settings: {
react: {
version: "latest",
},
},
rules: {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-empty-interface": [
"error",
{
allowSingleExtends: true,
},
],
"@typescript-eslint/interface-name-prefix": ["error", { prefixWithI: "never" }],
"import/order": "error",
"sort-imports": [
"error",
{
ignoreCase: true,
ignoreDeclarationSort: true,
},
],
"comma-dangle": "off",
"@typescript-eslint/typedef": [
"error",
{
arrowParameter: false,
arrayDestructuring: true,
parameter: true,
propertyDeclaration: true,
memberVariableDeclaration: true,
variableDeclarationIgnoreFunction: true,
variableDeclaration: false,
},
],
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/camelcase": "off",
"@typescript-eslint/naming-convention": [
"error",
{
selector: "default",
format: ["UPPER_CASE", "camelCase", "PascalCase"],
leadingUnderscore: "allow",
},
{
selector: "property",
format: null, // disable for property names because of our foo__expanded convention for JSS
// TODO: I think we can come up with a regex that ignores variables with __ in them
},
{
selector: "variable",
format: null, // disable for variable names because of our foo__expanded convention for JSS
// TODO: I think we can come up with a regex that ignores variables with __ in them
},
],
"@typescript-eslint/no-inferrable-types": "off",
"no-prototype-builtins": "off",
"no-fallthrough": "off",
"no-unexpected-multiline": "off",
"@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
"react/no-children-prop": "off",
"@typescript-eslint/no-explicit-any": "off",
},
};

3
.gitignore поставляемый
Просмотреть файл

@ -55,9 +55,6 @@ storybook-static
.tmp/
temp/
# npm package-locks
package-lock.json
# GitHub Actions Local Testing
.github/workflows/testing/*.json

9
.prettierignore Normal file
Просмотреть файл

@ -0,0 +1,9 @@
*.spec.ts
*.spec.tsx
**/__test-images__
**/__tests__
**/.tmp
**/bootstrap
**/coverage
**/dist
**/temp

13
.prettierrc Normal file
Просмотреть файл

@ -0,0 +1,13 @@
{
"printWidth": 90,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid",
"htmlWhitespaceSensitivity": "ignore",
"endOfLine": "auto"
}

Просмотреть файл

@ -4,7 +4,7 @@
### Machine setup
To work with the FAST Tooling [monorepo](https://en.wikipedia.org/wiki/Monorepo) you'll need Git, Node.js, and NPM setup on your machine.
To work with the FAST Tooling [monorepo](https://en.wikipedia.org/wiki/Monorepo) you'll need Git, Node.js@^16.0.0, and npm@^7.0.0 setup on your machine.
FAST Tooling uses Git as its source control system. If you haven't already installed it, you can download it [here](https://git-scm.com/downloads) or if you prefer a GUI-based approach, try [GitHub Desktop](https://desktop.github.com/).

35
build/clean.js Normal file
Просмотреть файл

@ -0,0 +1,35 @@
/* eslint-env node */
/* eslint-disable @typescript-eslint/explicit-function-return-type,@typescript-eslint/no-var-requires,@typescript-eslint/typedef */
/**
* Utility for cleaning directories.
* Usage: node build/clean.js %path%
*/
const path = require("path");
const rimraf = require("rimraf");
const argv = require("yargs").argv;
/**
* All paths passed to the clean script
*/
const paths = argv._;
/**
* Function to remove a given path
*/
function cleanPath(cleanPath) {
if (!cleanPath) {
console.error("No path specified.");
process.exit(1);
}
const removePath = path.resolve(process.cwd(), cleanPath);
rimraf(removePath, () => {
console.log(removePath, "cleaned");
});
}
/**
* Clean all paths
*/
if (Array.isArray(paths)) {
paths.forEach(cleanPath);
}

42720
package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

56
package.json Normal file
Просмотреть файл

@ -0,0 +1,56 @@
{
"name": "@microsoft/fast-tooling-repository",
"description": "A system of development tools, and utilities used à la carte or as a suite to build enterprise-grade websites and applications.",
"version": "0.1.0",
"author": {
"name": "Microsoft",
"url": "https://discord.gg/FcSNfg4"
},
"license": "MIT",
"private": true,
"workspaces": [
"./packages/fast-tooling-react",
"./packages/fast-tooling"
],
"repository": {
"type": "git",
"url": "git+https://github.com/microsoft/fast-tooling.git"
},
"bugs": {
"url": "https://github.com/microsoft/fast-tooling/issues/new/choose"
},
"scripts": {
"test:diff:error": "echo \"Untracked files exist, try running npm prepare to identify the culprit.\" && exit 1",
"test:diff": "git update-index --refresh && git diff-index --quiet HEAD -- || npm run test:diff:error",
"test": "npm run prettier --workspaces --if-present && npm run test:diff && npm run test --workspaces --if-present"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{ts,tsx,js,html}": [
"prettier --write"
]
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^2.23.0",
"@typescript-eslint/parser": "^2.23.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-react": "^7.19.0",
"chalk": "^2.4.2",
"copyfiles": "^2.4.1",
"dotenv": "^6.0.0",
"glob": "^7.1.2",
"husky": "^4.2.5",
"lint-staged": "^10.1.2",
"prettier": "2.0.2",
"rimraf": "^3.0.2",
"yargs": "^16.2.0"
},
"dependencies": {
}
}

Просмотреть файл

@ -0,0 +1,9 @@
# don't ever lint node_modules
node_modules
# don't lint build output (make sure it's set to your correct build folder name)
dist
# don't lint coverage output
coverage
# don't lint tests
__test__
__tests__

Просмотреть файл

@ -0,0 +1,3 @@
module.exports = {
extends: ["../../.eslintrc"],
};

Просмотреть файл

@ -0,0 +1,20 @@
# App files
app/
# Source files
src/
# Tests
__tests__/
coverage/
*.spec.*
*.test.*
# Config files
.prettierignore
.eslintignore
.eslintrc.cjs
babel.config.js
tsconfig.build.json
tsconfig.json
webpack.config.cjs

Просмотреть файл

@ -0,0 +1 @@
package-lock=false

Просмотреть файл

@ -0,0 +1,2 @@
coverage/*
dist/*

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,895 @@
# FAST Tooling React
The tooling available in FAST Tooling React can be used together to create UI for manipulating serializable data and viewing React components.
![JavaScript](https://img.shields.io/badge/ES6-Supported-yellow.svg?style=for-the-badge&logo=JavaScript) &nbsp; ![TypeScript](https://img.shields.io/badge/TypeScript-Supported-blue.svg?style=for-the-badge)
- [Benefits](#benefits)
- [Concepts](#concepts)
- [Ecosystem](#ecosystem)
- [Installation](#installation)
- [Requirements](#requirements)
- [Form](#form)
- [Validation](#validation)
- [Drag and drop](#drag-and-drop)
- [Using form control plugins](#using-form-control-plugins)
- [List of control types](#list-of-control-types)
- [Validation](#validation)
- [JSON schema metadata](#json-schema-metadata)
- [Title](#title)
- [Description](#description)
- [Disabled](#disabled)
- [Examples & default](#examples-&-default)
- [Badges](#badges)
- [Alias](#alias)
- [Dictionaries](#dictionaries)
- [Categories](#categories)
- [JSON schema keywords](#json-schema-keywords)
- [oneOf & anyOf](#oneof-&-anyof)
- [Enums](#enums)
- [allOf & $ref](#allof-&-ref)
- [Navigation](#navigation)
- [Include data types](#include-data-types)
- [Navigation Menu](#navigation-menu)
- [Menu structure](#menu-structure)
- [Expanding and Collapsing](#expanding-and-collapsing)
- [Controlling the location](#controlling-the-location)
- [Viewer](#viewer)
- [Setting width and height](#setting-width-and-height)
- [Sending custom messages](#sending-custom-messages)
- [Receiving custom messages](#receiving-custom-messages)
- [Select device](#select-device)
- [Devices](#devices)
- [Rotate](#rotate)
- [Data utilities](#data-utilities)
- [Transforming data](#transforming-data)
## Benefits
The FAST Tooling can be used in any combination for the following scenarios:
- Mapping serializable data to a React component in an application
- Editing data using a form generated from a JSON schema
- Viewing a React component in an isolated iframe environment
- Using a navigation generated from a components data
- All of the above to create a live editing UI
## Concepts
### Ecosystem
The following components are intended to work together as an ecosystem of components:
- `ModularForm` - see [Form](#form)
- `ModularViewer` - see [Viewer](#viewer)
- `ModularNavigation` - see [Navigation](#navigation)
Each of these components is provided as a standalone version and a version intended to work with another of the above components. If the `Form` is intended to be used with the `Viewer` then the `Modular` prefixed versions should be used. This enables them to share certain capabilities such as drag and drop.
Example:
```jsx
import { DndProvider } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import { ModularForm, ModularViewer } from "@microsoft/fast-tooling-react";
// See details on implementation from the standalone
// versions of Form and Viewer
<DndProvider backend={HTML5Backend}>
<ModularForm {...props} />
<ModularViewer {...props} />
</DndProvider>
```
## Installation
`npm i --save @microsoft/fast-tooling-react`
## Requirements
The `@microsoft/fast-tooling-react` package will be installed with `@microsoft/fast-tooling`. The `@microsoft/fast-tooling` package includes exports required for implementing the React specific components, namely the `MessageSystem` and the minified webworker which handles data manipulation. Please refer to the documentation for `@microsoft/fast-tooling` for a better understanding of these systems.
## Form
The required property is the `messageSystem`, see `@microsoft/fast-tooling` for details on setting this up.
Example:
```jsx
import { Form } from "@microsoft/fast-tooling-react";
/**
* Add to your render function
*/
<Form
messageSystem={fastMessageSystem}
/>
```
### Validation
Validation is treated as optional, there is a validation utility provided by the `@microsoft/fast-tooling` package that will give basic JSON schema validation errors. Refer to the `@microsoft/fast-tooling` README for details.
### Drag and drop
Drag and drop is provided to the `Form` using the `react-dnd` package as well as the `HTML5Backend`. If you are using `react-dnd` somewhere else and need to implement the backend once, use the secondary export `ModularForm`.
### Using form control plugins
All necessary form controls are built in by default but can be overriden either through the schema by adding a `formControlId` property with a string value or a control type defined [below](#list-of-control-types).
To make a custom control, use the secondary export `StandardControlPlugin` which will take care of all standard form actions such as setting default, resetting data, etc. You will need to provide the necessary functionality to the `control` as JSX.
When the plugin instance is passed to the `<Form />`
either the id or the type is then referenced and will cause the control to render.
A config is passed to the control, the specifications of this can be found [here](https://github.com/microsoft/fast/blob/master/packages/fast-tooling-react/src/form/templates/template.control.utilities.props.tsx). Note that the `ControlConfig` interface may include extra properties depending on the control type being used.
Example id plugin:
JSON Schema:
```json
{
"type": "object",
"properties": {
"foo": {
"type": "string",
"formControlId": "foo"
}
}
}
```
JSX:
```jsx
<Form
messageSystem={fastMessageSystem}
controls={[
new StandardControlPlugin({
id: "foo",
control: (config) => {
return (
<input
value={config.value}
/>
)
}
})
]}
/>
```
Example type plugin:
```jsx
<Form
messageSystem={fastMessageSystem}
controls={[
new StandardControlPlugin({
type: ControlType.textarea,
control: (config) => {
return (
<input
value={config.value}
/>
)
}
})
]}
/>
```
#### List of control types
Control types are available as an enum provided as a secondary export `ControlType` and consist of the following:
```js
import { ControlType } from "@microsoft/fast-tooling-react";
// Available types
ControlType.select
ControlType.array
ControlType.checkbox
ControlType.linkedData
ControlType.numberField
ControlType.sectionLink
ControlType.section
ControlType.display
ControlType.button
ControlType.textarea
```
These control types can be paired with our default controls, the following of which are available:
- `SelectControl`
- `ArrayControl`
- `CheckboxControl`
- `LinkedDataControl`
- `NumberFieldControl`
- `SectionLinkControl`
- `SectionControl`
- `DisplayControl`
- `ButtonControl`
- `TextareaControl`
**Note: If the id and type are not specified, all controls will be replaced with the control.**
Example of a replacement type:
```jsx
import { ControlType, TextareaControl } from "@microsoft/fast-tooling-react";
...
<Form
messageSystem={fastMessageSystem}
controls={[
new StandardControlPlugin({
type: ControlType.textarea,
control: (config) => {
return (
<React.Fragment>
<span>Hello world!</span>
<TextareaControl {...config} />
</React.Fragement>
);
}
})
]}
/>
```
Example of a replacement for all controls, using the component for the default control:
```jsx
<Form
messageSystem={fastMessageSystem}
controls={[
new StandardControlPlugin({
control: (config) => {
return (
<React.Fragment>
<span>Hello world!</span>
<config.component {...config} />
</React.Fragement>
);
}
})
]}
/>
```
#### Making your own control plugin
The `StandardControlPlugin` creates a standard template for expected functionality not specific to a control such as a `CheckboxControl`. This may include showing a button to set the value to the default value, an unset/reset button if the value represented in the control is optional, etc.
It is possible to create your own control plugin template; this section is for more advanced usage and should be done with caution.
To assist in the creation of a custom control plugin template, another secondary export is provided, `ControlTemplateUtilities`. This is an abstract class that can be extended, it includes all of the render methods for various actions that can be taken that are not control specific. It is possible to use this class to make your own template and include extra logic for when these items should render.
Example:
```jsx
import { ControlTemplateUtilities } from "@microsoft/fast-tooling-react";
export class MyControlTemplate extends ControlTemplateUtilities {
public render() {
return (
<div>
<label
htmlFor={this.props.dataLocation}
title={this.props.labelTooltip}
>
{this.props.label}
</label>
{this.renderConstValueIndicator("const-value-indicator-css-class")}
{this.renderDefaultValueIndicator("default-value-indicator-css-class")}
{this.renderBadge("badge-css-class")}
{this.renderControl(this.props.control(this.getConfig()))}
{this.renderSoftRemove("soft-remove-css-class")}
{this.renderInvalidMessage("invalid-message-css-class")}
</div>
);
}
}
export { StandardControlTemplate };
```
The following methods are available:
- `renderConstValueIndicator` - This will indicate that this value is not the const value and will set the value to the const value if clicked.
- `renderDefaultValueIndicator` - This will indicate that this value is not the default value and will set the value to the default if clicked.
- `renderBadge` - This renders a badge, as indicated by the [badge](#badges) section of this README.
- `renderControl` - This renders the control, such as `CheckboxControl` etc. or whatever control has been specified if the default controls are not being used. This must include an argument to execute the control with the `getConfig` method as an argument.
- `renderSoftRemove` - This allows for the rendering of an unset/reset button if the value of this control is optional.
- `renderInvalidMessage` - This method renders the invalid message for this control as specified when validating the data against the JSON schema.
Note that with the exception of `renderControl` method, the others require a string argument, this will be used as a class so that the generated HTML from the render method can be styled. At this point it is up to the implementer to include their own styling for these items.
It is recommended that the implementation also include the use of a label for accessibility.
### Validation
Form validation uses the [ajv](https://github.com/epoberezkin/ajv) package. The validation can be displayed inline or using the browser default HTML5 validation UI. This can be achieved through the `displayValidationBrowserDefault` which is `true` by default and `displayValidationInline` which will show validation messages below the associated form element.
### JSON schema metadata
The schema form generator can interpret most [JSON schemas](http://json-schema.org/), however there are some things to note when writing JSON schemas that make for a better UI.
#### Title
Using a title will add a label to the corresponding form element. All properties are required to have a title.
Example:
```json
{
"$schema": "http://json-schema.org/schema#",
"id": "my-component",
"title": "My component",
"type": "object",
"properties": {
"text": {
"title": "Text",
"type": "string",
"example": "Hello world"
},
"weight": {
"title": "Weight",
"type": "string",
"enum": [
"heavy"
]
}
},
"required": [
"text"
]
}
```
#### Description
Using a description will add a HTML attribute `title` to the label, resulting in a browser tooltip. This should be used for supplemental information that may not be apparent in the title.
Example:
```json
{
"$schema": "http://json-schema.org/schema#",
"id": "my-component",
"title": "My component",
"type": "object",
"properties": {
"text": {
"title": "Text",
"description": "The text appearing in the body",
"type": "string",
"example": "Hello world"
},
"weight": {
"title": "Weight",
"description": "The weight of the text",
"type": "string",
"enum": [
"heavy"
]
}
},
"required": [
"text"
]
}
```
#### Disabled
The disabled flag is optional and the form item representing this section of the schema will be disabled if flag is set to true.
Example:
```json
{
"$schema": "http://json-schema.org/schema#",
"id": "my-component",
"title": "My component",
"type": "object",
"properties": {
"text": {
"title": "Text",
"type": "string",
"example": "Hello world",
"disabled": true
}
},
"required": [
"text"
]
}
```
#### Examples & default
Providing an examples or default value will replace the placeholder 'example text' or randomly generated number. It is generally better to add this extra information in case the schema form generator needs to create a new set of data.
Example:
```json
{
"$schema": "http://json-schema.org/schema#",
"id": "my-component",
"title": "My component",
"type": "object",
"properties": {
"text": {
"title": "Text",
"type": "string",
"examples": [
"Hello world"
]
},
"style": {
"title": "Style",
"type": "object",
"properties": {
"color": {
"title": "HEX Color",
"type": "string",
"examples": [
"#FF0000"
]
}
},
"required": [
"color"
]
}
},
"required": [
"text"
]
}
```
Because the style is optional, you can toggle to add it. The schema form generator will see that color is a required piece of data and use the example given to fill in.
#### Badges
To allow more detail about a field two additional fields can be added to JSON schemas, `badge` and `badgeDescription`. The `badge` can have the values "info", "warning" and "locked" which will create the related icons. Adding a `badgeDescription` will add a native tooltip when the badge is hovered.
Example:
```json
{
"type": "string",
"badge": "warning",
"badgeDescription": "Setting this field will cause adverse effects"
}
```
#### Alias
Occasionally the `title` provided by the JSON schema may not be enough information for the `Form` component, if an additional `alias` property is provided, this will be used as the linked data control label. In Chromium based browsers this will show both, and both the `title` text and `alias` text will autocomplete. In Firefox however, this will result in only showing the `alias`, so ensure that the `alias` text contains enough information to be easily autocompleted by a user.
#### Dictionaries
The `additionalProperties` JSON schema keyword can be used to create a dictionary of user-input keys on an object. To give these keys a label add the keyword `propertyTitle`, this will create a label for the form element for editing the property key.
Example:
```json
{
"type": "object",
"additionalProperties": {
"title": "A dictionary of strings",
"propertyTitle": "A dictionary key",
"type": "string"
}
}
```
### Categories
For improved UI when there become too many properties in an object, categories can be specified on a per-schema basis.
Example:
```jsx
<Form
messageSystem={fastMessageSystem}
categories={{
"category-schema-id": {
"": [
{
title: "Style",
dataLocations: ["color", "outline", "font"],
},
{
title: "Content",
dataLocations: ["title", "body", "footer"],
},
{
title: "Advanced",
dataLocations: ["tracking", "accessibility"],
expandByDefault: false // default true
},
],
},
}}
/>
```
This shows the root object of a schema with `$id` of `category-schema-id` that has the properties `color`, `outline`, `font`, `title`, `body`, `footer`, `tracking` and `accessibility` and splits them into categories with the appropriate titles.
The "Advanced" category has its expand default set to `false`, this means that initially it will be collapsed.
### JSON schema keywords
Certain JSON schema keywords are interpreted to provide a better UI.
#### oneOf & anyOf
The oneOf and anyOf keywords can be used inside a property and at the root level of a schema. This will create a select dropdown so that the user can switch between them. If data has been provided, it will select the first oneOf/anyOf instance it can validate against. The contents of a 'title' property will be used for the contents of the dropdown.
Example:
```json
{
"$schema": "http://json-schema.org/schema#",
"id": "my-component",
"title": "My component",
"oneOf": [
{
"title": "color",
"type": "object",
"properties": {
"color": {
"title": "HEX Color",
"type": "string",
"example": "#FF0000"
}
}
},
{
"title": "text",
"type": "object",
"properties": {
"text": {
"title": "Text",
"type": "string",
"example": "Hello world"
}
}
}
]
}
```
#### Enums
Any enums will be converted to a select dropdown.
```json
{
"$schema": "http://json-schema.org/schema#",
"id": "my-component",
"title": "My component",
"type": "object",
"properties": {
"color": {
"title": "Color",
"type": "string",
"enum" : [
"red",
"green",
"blue",
"yellow"
],
"default": "red"
}
}
}
```
#### allOf & $ref
The `allOf` and `$ref` keywords cannot be interpreted by the schema form generator.
#### Categories
Any `object` in the `<Form />` may have categories with which to contain its properties. This can be achieved by passing the `categories` prop which is a dictionary of keys that match to a schemas `id`, and which contain a `dataLocation` key to indicate which object a form category belongs to. Each category can then specify the properties as a set of `dataLocation` strings and a `title`.
Example:
```tsx
<Form
messageSystem={fastMessageSystem}
categories={{
"https://my.schema.id": {
"": [ // The root level dataLocation
{
title: "Style",
dataLocations: ["border", "font"]
},
{
title: "Content",
dataLocations: ["text", "title"]
}
]
}
}}
/>
```
## Navigation
The required property is the `messageSystem`, see `@microsoft/fast-tooling` for details on setting this up.
Example:
```jsx
<Navigation
messageSystem={fastMessageSystem}
/>
```
### Include data types
By default all JSON schema data types are visible. If the optional `types` prop is included, the data types that are in the array will become the data types that are rendered. If a data type that is specified is contained inside a data type that is not, that will directly nest the visible data type with the parent of the unspecified data type.
Example:
```jsx
<Navigation
messageSystem={fastMessageSystem}
types={["object", "string"]} // only "object" and "string" data types are rendered
/>
```
## Navigation Menu
The `NavigationMenu` component creates a navigational menu from a provided data structure. This component is meant to serve as location routing in an application.
### Menu structure
Example menu structure:
```js
const menu = [
{
displayName: "red",
location: "/red"
},
{
displayName: "green",
items: [
{
displayName: "blue",
location: "/blue"
}
]
}
]
```
Each menu item requires a `displayName` to use in the generated UI.
Simple example:
```jsx
render() {
return (
<NavigationMenu
menu={menu}
/>
);
}
```
### Controlling the location
A `location` may optionally be provided in the menu data, if this is not accompanied by a `onLocationUpdate` callback, the generated UI for that menu item will be an anchor. If an `onLocationUpdate` callback is provided this results in a span, which when clicked will fire the callback with the associated location.
Example:
```jsx
render() {
return (
<NavigationMenu
menu={menu}
activeLocation={this.state.activeLocation}
onLocationUpdate={this.handleLocationUpdate}
/>
);
}
handleLocationUpdate = (location) => {
this.setState({
activeLocation: location
});
// do some route manipulation
}
```
## Viewer
The `Viewer` component creates an iframe, it can have a fixed or adjustable width and height and can be used independently or as a set with the `SelectDevice` and `Rotate` components.
The required property is the `messageSystem`, see `@microsoft/fast-tooling` for details on setting this up and the `iframeSrc`.
Example:
```jsx
<Viewer
messageSystem={fastMessageSystem}
iframeSrc={"/example-content"}
/>
```
### Setting width and height
The `width` and `height` can be set on the `Viewer` component which will be used as pixel values:
Example:
```jsx
<Viewer
messageSystem={fastMessageSystem}
iframeSrc={"/example-content"}
width={500}
height={300}
/>
```
To create a responsive width an height, the `width` and `height` can be tied to values in state and combined with the `onUpdateHeight`, `onUpdateWidth` and `responsive` props. This creates draggable borders around the iframe.
Example:
```jsx
<Viewer
messageSystem={fastMessageSystem}
iframeSrc={"/example-content"}
width={this.state.viewerWidth}
height={this.state.viewerHeight}
responsive={true}
onUpdateHeight={this.handleUpdateViewerHeight}
onUpdateWidth={this.handleUpdateViewerWidth}
/>
// handlers for the `onUpdateHeight` and `onUpdateWidth` callbacks
handleUpdateViewerHeight = (newViewerHeight) => {
this.setState({
viewerHeight: newViewerHeight
});
}
handleUpdateViewerWidth = (newViewerWidth) => {
this.setState({
viewerWidth: newViewerWidth
});
}
```
### Sending custom messages
Sending custom messages through from the iframe can be done with a `postMessage` to the iframe window. The custom message should define the `type` and `action`. The type should be `MessageSystemType.custom` imported from the `@microsoft/fast-tooling` package and the `action` is defined as the enum value `ViewerCustomAction.call` provided as an export.
Example:
```javascript
import { MessageSystemType } from "@microsoft/fast-tooling";
import { ViewerCustomAction } from "@microsoft/fast-tooling-react";
window.postMessage({
type: MessageSystemType.custom,
action: ViewerCustomAction.call,
data: myData
}, "*");
```
### Receiving custom messages
When a custom message is sent through the message system with a type of `ViewerCustomAction.call`, it will be passed to all registered callbacks with the message system using a modified `action` type of `ViewerCustomAction.response`. This way any further action that needs to be taken with the message data passed from the iframe can be done by looking for the response.
### Select device
Use the `SelectDevice` component to select from provided default devices or provide your own custom device configurations. This component accepts a list of configured devices via the `devices` prop, some default devices are included with the package as a secondary export. It also accepts an `activeDeviceId` prop which maps to the current device id of the provided devices. In addition there is a callback `onUpdateDevice` which will fire a provided function with the new device id selected.
Example:
```jsx
import {
defaultDevices,
SelectDevice,
} from "@microsoft/fast-tooling-react";
<SelectDevice
devices={defaultDevices}
onUpdateDevice={this.handleDeviceUpdate}
activeDeviceId={this.state.activeDevice.id}
/>
```
#### Devices
A device can be either "responsive" or "fixed", if it is responsive it does not take a width and height. The current active device can be used to activate the `responsive` prop on the `Viewer` component.
Example of custom devices passed to the `devices` prop:
```json
[
{
"id": "responsive",
"displayName": "Responsive display",
"display": "responsive"
},
{
"id": "phoneDevice",
"displayName": "Phone device",
"display": "fixed",
"height": 800,
"width": 320
}
]
```
### Rotate
Use the `Rotate` component to switch between landscape and portrait view. This component accepts an `orientation` prop which can be either "landscape" or "portrait". It also accepts an `onUpdateOrientation` callback which will fire a provided function with the new orientation selected.
Example:
```jsx
import {
Rotate,
} from "@microsoft/fast-tooling-react";
<Rotate
orientation={this.state.orientation}
onUpdateOrientation={this.handleOrientationUpdate}
/>
```
## Data utilities
### Transforming data
As data from the dictionary of data is intended to be mapped to JSON schema, it may need to be transformed to be useful as, for instance, a React component.
Assuming that each JSON schema represents React props for a given component, a mapper has been provided which can be used in conjunction with the `@microsoft/fast-tooling` export `mapDataDictionary`.
Example:
```js
import { mapDataDictionary } from "@microsoft/fast-tooling";
import { reactMapper } from "@microsoft/fast-tooling-react";
const componentDictionary = {
"button-schema-id": MyButton
}
const myComponentInstance = mapDataDictionary({
dataDictionary: {
foo: {
schemaId: "button-schema-id",
data: {
children: "Hello world",
},
},
},
dataDictionaryKey: "foo",
mapper: reactMapper(componentDictionary),
schemaDictionary: {
"button-schema-id": {
id: "button-schema-id",
type: "object",
properties: {
children: {
type: "string",
},
},
},
},
});
```
Expected result from the example above is an instance of `MyButton` with the text "Hello world". This is a simple mapper that assumes that any linked data is a React component that is nested.

Просмотреть файл

@ -0,0 +1,3 @@
# Background
This folder contains a React application used for testing. None of the files in this folder are intended for export, it should consume exports from the `./src` folder to create a variety of experiences and to validate the components working with each other.

Просмотреть файл

@ -0,0 +1,91 @@
import React from "react";
import { BrowserRouter, Link, Redirect, Route, Switch } from "react-router-dom";
import { NavigationTestPage } from "./pages/navigation";
import ViewerPage from "./pages/viewer";
import ViewerContentPage from "./pages/viewer/content";
import { FormTestPage } from "./pages/form";
import { FormAndNavigationTestPage } from "./pages/form-and-navigation";
import { NavigationMenuTestPage } from "./pages/navigation-menu";
import { WebComponentTestPage } from "./pages/web-components";
import WebComponentViewerContent from "./pages/web-components/web-component-viewer-content";
class App extends React.Component<{}, {}> {
public render(): React.ReactNode {
return (
<BrowserRouter>
<div>
{this.renderLinks()}
<Switch>
<Route
exact={true}
path={"/navigation-menu"}
component={NavigationMenuTestPage}
/>
<Route
exact={true}
path={"/navigation"}
component={NavigationTestPage}
/>
<Route exact={true} path={"/viewer"} component={ViewerPage} />
<Route
exact={true}
path={"/viewer/content"}
component={ViewerContentPage}
/>
<Route exact={true} path={"/form"} component={FormTestPage} />
<Route
exact={true}
path={"/form-and-navigation"}
component={FormAndNavigationTestPage}
/>
<Route
exact={true}
path={"/web-components"}
component={WebComponentTestPage}
/>
<Route
exact={true}
path={"/web-components/content"}
component={WebComponentViewerContent}
/>
<Route exact={true} path={"/"}>
<Redirect to={"/form"} />
</Route>
</Switch>
</div>
</BrowserRouter>
);
}
private renderLinks(): React.ReactNode {
if (window.location.pathname.slice(-7) !== "content") {
return (
<React.Fragment>
<ul>
<li>
<Link to="/navigation-menu">Navigation Menu</Link>
</li>
<li>
<Link to="/form">Form</Link>
</li>
<li>
<Link to="/navigation">Navigation</Link>
</li>
<li>
<Link to="/form-and-navigation">Form and Navigation</Link>
</li>
<li>
<Link to="/viewer">Viewer</Link>
</li>
<li>
<Link to="/web-components">Web Components</Link>
</li>
</ul>
<hr />
</React.Fragment>
);
}
}
}
export default App;

Просмотреть файл

@ -0,0 +1,23 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./app";
import HTML5Backend from "react-dnd-html5-backend";
import { DndProvider } from "react-dnd";
/**
* Create the root node
*/
const root: HTMLElement = document.createElement("div");
root.setAttribute("id", "root");
document.body.appendChild(root);
document.body.setAttribute("style", "margin: 0");
/**
* Primary render function for app. Called on store updates
*/
ReactDOM.render(
<DndProvider backend={HTML5Backend}>
<App />
</DndProvider>,
document.getElementById("root")
);

Просмотреть файл

@ -0,0 +1,311 @@
import React from "react";
import { DesignSystemProvider } from "@microsoft/fast-jss-manager-react";
import { ModularForm, ModularNavigation } from "../../src";
import { FormProps } from "../../src/form/form.props";
import {
FormAttributeSettingsMappingToPropertyNames,
FormChildOptionItem,
} from "../../src/form/types";
import * as testConfigs from "./form/";
import {
DataDictionary,
getDataFromSchema,
MessageSystem,
MessageSystemType,
SchemaDictionary,
} from "@microsoft/fast-tooling";
export type componentDataOnChange = (e: React.ChangeEvent<HTMLFormElement>) => void;
export interface FormTestPageState {
schema: any;
data: any;
dataDictionary: DataDictionary<unknown>;
navigation: any;
attributeAssignment?: FormAttributeSettingsMappingToPropertyNames;
showExtendedControls: boolean;
dataLocation: string;
defaultBrowserErrors?: boolean;
inlineErrors?: boolean;
}
export interface GroupItem {
items: any;
type: string;
}
const designSystemDefaults: any = {
foregroundColor: "#000",
backgroundColor: "#FFF",
brandColor: "#0078D4",
};
let fastMessageSystem: MessageSystem;
class FormAndNavigationTestPage extends React.Component<{}, FormTestPageState> {
/**
* These are the children that can be added
*/
private childOptions: FormChildOptionItem[];
constructor(props: {}) {
super(props);
this.childOptions = this.getChildOptions();
const exampleData: any = getDataFromSchema(testConfigs.textField.schema);
if ((window as any).Worker) {
fastMessageSystem = new MessageSystem({
webWorker: "message-system.js",
dataDictionary: [
{
"": {
schemaId: testConfigs.textField.schema.id,
data: exampleData,
},
},
"",
],
schemaDictionary: this.generateSchemaDictionary(),
});
fastMessageSystem.add({ onMessage: this.handleMessageSystem });
}
this.state = {
schema: testConfigs.textField.schema,
data: exampleData,
dataDictionary: [
{
"": {
schemaId: testConfigs.textField.schema.id,
data: exampleData,
},
},
"",
],
navigation: void 0,
showExtendedControls: false,
dataLocation: "",
inlineErrors: void 0,
defaultBrowserErrors: void 0,
};
}
public render(): JSX.Element {
return (
<DesignSystemProvider designSystem={designSystemDefaults}>
<div>
<div
style={{
width: "300px",
height: "100vh",
float: "left",
fontFamily:
"Segoe UI, SegoeUI, Helvetica Neue, Helvetica, Arial, sans-serif",
}}
>
{this.renderNavigation()}
</div>
<div
style={{
float: "left",
marginLeft: "8px",
}}
>
<div>
<select onChange={this.handleComponentUpdate}>
{this.getComponentOptions()}
</select>
<br />
<br />
<input
id={"showInlineErrors"}
type="checkbox"
value={(!!this.state.inlineErrors).toString()}
onChange={this.handleShowInlineErrors}
/>
<label htmlFor={"showInlineErrors"}>Show inline errors</label>
<br />
<input
id={"showBrowserErrors"}
type="checkbox"
value={(!!this.state.defaultBrowserErrors).toString()}
onChange={this.handleShowBrowserErrors}
/>
<label htmlFor={"showBrowserErrors"}>
Show default browser errors
</label>
<br />
</div>
<pre
style={{
padding: "12px",
background: "rgb(244, 245, 246)",
borderRadius: "4px",
}}
>
{JSON.stringify(this.state.dataDictionary, null, 2)}
</pre>
<pre>{this.state.dataLocation}</pre>
</div>
<div
style={{
width: "300px",
height: "100vh",
float: "left",
fontFamily:
"Segoe UI, SegoeUI, Helvetica Neue, Helvetica, Arial, sans-serif",
}}
>
<ModularForm {...this.coerceFormProps()} />
</div>
</div>
</DesignSystemProvider>
);
}
private renderNavigation(): React.ReactNode {
return <ModularNavigation messageSystem={fastMessageSystem} />;
}
private generateSchemaDictionary(): SchemaDictionary {
const schemaDictionary: SchemaDictionary = {};
Object.keys(testConfigs).forEach((testConfigKey: string) => {
schemaDictionary[testConfigs[testConfigKey].schema.id] =
testConfigs[testConfigKey].schema;
});
return schemaDictionary;
}
/**
* Gets the child options for the schema form
*/
private getChildOptions(): FormChildOptionItem[] {
const childOptions: FormChildOptionItem[] = [];
const groups: GroupItem[] = [
{
items: testConfigs,
type: "components",
},
];
for (const group of groups) {
Object.keys(group.items).map((itemName: any, key: number): void => {
if (typeof testConfigs[itemName].schema !== "undefined") {
const childObj: FormChildOptionItem = {
name: testConfigs[itemName].schema.title || "Untitled",
component: testConfigs[itemName].component,
schema: testConfigs[itemName].schema,
};
childOptions.push(childObj);
}
});
}
return childOptions;
}
private coerceFormProps(): FormProps {
const formProps: FormProps = {
messageSystem: fastMessageSystem,
};
if (typeof this.state.defaultBrowserErrors === "boolean") {
formProps.displayValidationBrowserDefault = this.state.defaultBrowserErrors;
}
if (typeof this.state.inlineErrors === "boolean") {
formProps.displayValidationInline = this.state.inlineErrors;
}
return formProps;
}
private handleShowInlineErrors = (e: React.ChangeEvent<HTMLInputElement>): void => {
if (e.target.value === "true") {
this.setState({
inlineErrors: false,
});
} else {
this.setState({
inlineErrors: true,
});
}
};
private handleShowBrowserErrors = (e: React.ChangeEvent<HTMLInputElement>): void => {
if (e.target.value === "true") {
this.setState({
defaultBrowserErrors: false,
});
} else {
this.setState({
defaultBrowserErrors: true,
});
}
};
private handleMessageSystem = (e: MessageEvent): void => {
switch (e.data.type) {
case MessageSystemType.initialize:
if (e.data.data && e.data.navigation) {
this.setState({
data: e.data.data,
dataDictionary: e.data.dataDictionary,
navigation: e.data.navigation,
});
}
break;
case MessageSystemType.data:
if (e.data.data) {
this.setState({
data: e.data.data,
dataDictionary: e.data.dataDictionary,
});
}
break;
case MessageSystemType.navigation:
if (e.data.navigation) {
this.setState({
data: e.data.navigation,
dataDictionary: e.data.dataDictionary,
});
}
break;
}
};
private handleComponentUpdate = (e: React.ChangeEvent<HTMLSelectElement>): void => {
const data: any = !!testConfigs[e.target.value].data
? testConfigs[e.target.value].data
: getDataFromSchema(testConfigs[e.target.value].schema);
if ((window as any).Worker && fastMessageSystem) {
fastMessageSystem.postMessage({
type: MessageSystemType.initialize,
data: [
{
"": {
schemaId: testConfigs[e.target.value].schema.id,
data,
},
},
"",
],
schemaDictionary: this.generateSchemaDictionary(),
});
}
};
private getComponentOptions(): JSX.Element[] {
return Object.keys(testConfigs).map((testComponentKey: any, index: number) => {
return <option key={index}>{testConfigs[testComponentKey].schema.id}</option>;
});
}
}
export { FormAndNavigationTestPage };

Просмотреть файл

@ -0,0 +1,585 @@
import * as testConfigs from "./form/";
import { AlignControl, Form } from "../../src";
import {
ControlConfig,
StandardControlPlugin,
TextAlignControl,
FileControl,
} from "../../src";
import CSSControl from "../../src/form/custom-controls/control.css";
import { properties as allCSSProperties } from "@microsoft/fast-tooling/dist/esm/css-data";
import { FormProps } from "../../src/form/form.props";
import {
FormAttributeSettingsMappingToPropertyNames,
FormChildOptionItem,
} from "../../src/form/types";
import React from "react";
import {
AjvMapper,
DataDictionary,
getDataFromSchema,
MessageSystem,
MessageSystemType,
SchemaDictionary,
} from "@microsoft/fast-tooling";
import {
accentColorName,
L1ColorName,
L4ColorName,
textColorName,
L3FillColorName,
errorColorName,
FloatingColorName,
} from "../../src/style";
import { CSSPropertiesDictionary } from "@microsoft/fast-tooling/dist/esm/data-utilities/mapping.mdn-data";
import { ControlContext } from "../../src/form/templates/types";
import { CSSStandardControlPlugin } from "../../src/form/custom-controls/css";
import { CSSControlConfig } from "../../src/form/custom-controls/css/css.template.control.standard.props";
import { DesignSystem } from "@microsoft/fast-foundation";
import {
fastButton,
fastCheckbox,
fastNumberField,
fastOption,
fastSelect,
fastTextField,
} from "@microsoft/fast-components";
import {
fastToolingColorPicker,
fastToolingFile,
fastToolingFileActionObjectUrl,
} from "@microsoft/fast-tooling/dist/esm/web-components";
DesignSystem.getOrCreate().register(
fastButton(),
fastCheckbox(),
fastNumberField(),
fastOption(),
fastSelect(),
fastTextField(),
fastToolingColorPicker({ prefix: "fast-tooling" }),
fastToolingFile({ prefix: "fast-tooling" }),
fastToolingFileActionObjectUrl({ prefix: "fast-tooling" })
);
export type componentDataOnChange = (e: React.ChangeEvent<HTMLFormElement>) => void;
export interface FormTestPageState {
schema: any;
data: any;
dataDictionary: DataDictionary<unknown>;
navigation: any;
attributeAssignment?: FormAttributeSettingsMappingToPropertyNames;
showExtendedControls: boolean;
defaultBrowserErrors?: boolean;
inlineErrors?: boolean;
dataSet?: any;
cssPropertyOverrides: boolean;
}
export interface GroupItem {
items: any;
type: string;
}
export interface DataSet {
displayName: string;
data: any;
}
const properties = {
"border-width": allCSSProperties["border-width"],
"border-style": allCSSProperties["border-style"],
"border-color": allCSSProperties["border-color"],
"outline-offset": allCSSProperties["outline-offset"],
"animation-delay": allCSSProperties["animation-delay"],
};
const dataSets: DataSet[] = [
{
displayName: "Data set 1 (all defined)",
data: {
textarea: "alpha",
"section-link": {},
checkbox: true,
button: null,
array: ["foo", "bar"],
"number-field": 42,
select: "foo",
},
},
{
displayName: "Data set 2 (select defined)",
data: {
textarea: "beta",
"section-link": {},
display: "foobar",
checkbox: false,
"number-field": 24,
select: "bar",
},
},
{
displayName: "Data set 3 (none defined)",
data: {},
},
];
const CSSpropertyOverrides = {
[accentColorName]: "blue",
[L1ColorName]: "white",
[L4ColorName]: "lightgray",
[textColorName]: "black",
[L3FillColorName]: "white",
[errorColorName]: "green",
[FloatingColorName]: "purple",
};
let fastMessageSystem: MessageSystem;
let ajvMapper: AjvMapper;
class FormTestPage extends React.Component<{}, FormTestPageState> {
/**
* These are the children that can be added
*/
private childOptions: FormChildOptionItem[];
/**
* The custom control plugins used in the form
*/
private controlPlugins: StandardControlPlugin[];
constructor(props: {}) {
super(props);
this.childOptions = this.getChildOptions();
this.controlPlugins = [
new StandardControlPlugin({
id: testConfigs.customControl.schema.properties.file.formControlId,
control: (config: ControlConfig): React.ReactNode => {
return (
<FileControl {...config} accept=".jpg,.jpeg,.png,.gif">
Add Image
</FileControl>
);
},
}),
new StandardControlPlugin({
id: testConfigs.customControl.schema.properties.textAlign.formControlId,
control: (config: ControlConfig): React.ReactNode => {
return <TextAlignControl {...config} />;
},
}),
new StandardControlPlugin({
id: testConfigs.customControl.schema.properties.align.formControlId,
control: (config: ControlConfig): React.ReactNode => {
return <AlignControl {...config} />;
},
}),
new StandardControlPlugin({
id: testConfigs.controlPluginCss.schema.properties.css.formControlId,
context: ControlContext.fill,
control: (config: ControlConfig): React.ReactNode => {
return (
<CSSControl
css={(properties as unknown) as CSSPropertiesDictionary}
{...config}
key={`${config.dictionaryId}::${config.dataLocation}`}
/>
);
},
}),
new StandardControlPlugin({
id:
testConfigs.controlPluginCssWithOverrides.schema.properties
.cssWithOverrides.formControlId,
control: (config: ControlConfig): React.ReactNode => {
return (
<CSSControl
css={(properties as unknown) as CSSPropertiesDictionary}
key={`${config.dictionaryId}::${config.dataLocation}`}
cssControls={[
new CSSStandardControlPlugin({
id: "foo",
propertyNames: ["align-content", "align-items"],
control: (config: CSSControlConfig) => {
const handleChange = (
propertyName: string
): ((
e: React.ChangeEvent<HTMLInputElement>
) => void) => {
return (
e: React.ChangeEvent<HTMLInputElement>
) => {
config.onChange({
[propertyName]: e.target.value,
});
};
};
return (
<div>
<div>
<label htmlFor={"align-content"}>
align-content
</label>
<br />
<input
id={"align-content"}
onChange={handleChange(
"align-content"
)}
/>
</div>
<div>
<label htmlFor={"align-items"}>
align-items
</label>
<br />
<input
id={"align-items"}
onChange={handleChange(
"align-items"
)}
/>
</div>
</div>
);
},
}),
]}
{...config}
/>
);
},
}),
];
const exampleData: any = getDataFromSchema(testConfigs.controlPluginCss.schema);
if ((window as any).Worker) {
fastMessageSystem = new MessageSystem({
webWorker: "message-system.js",
dataDictionary: [
{
foo: {
schemaId: testConfigs.controlPluginCss.schema.id,
data: exampleData,
},
},
"foo",
],
schemaDictionary: this.generateSchemaDictionary(),
});
ajvMapper = new AjvMapper({
messageSystem: fastMessageSystem,
});
fastMessageSystem.add({ onMessage: this.handleMessageSystem });
}
this.state = {
schema: testConfigs.controlPluginCss.schema,
data: exampleData,
dataDictionary: [
{
foo: {
schemaId: testConfigs.controlPluginCss.schema.id,
data: exampleData,
},
},
"foo",
],
navigation: void 0,
showExtendedControls: false,
inlineErrors: void 0,
defaultBrowserErrors: void 0,
dataSet: dataSets[0].data,
cssPropertyOverrides: false,
};
}
public render(): JSX.Element {
return (
<div style={this.state.cssPropertyOverrides ? CSSpropertyOverrides : {}}>
<div
style={{
width: "300px",
height: "100vh",
float: "left",
fontFamily:
"Segoe UI, SegoeUI, Helvetica Neue, Helvetica, Arial, sans-serif",
}}
>
<Form {...this.coerceFormProps()} />
</div>
<div
style={{
float: "left",
marginLeft: "8px",
}}
>
<div>
<select
onChange={this.handleComponentUpdate}
defaultValue={testConfigs.controlPluginCss.schema.id}
>
{this.getComponentOptions()}
</select>
{this.renderDataSetComponentOptions()}
<br />
<br />
<input
id={"useCSSOverrides"}
type={"checkbox"}
value={this.state.cssPropertyOverrides.toString()}
onChange={this.handleCSSOverrideUpdate}
/>
<label htmlFor={"useCSSOverrides"}>
Show CSS property overrides
</label>
<br />
<input
id={"showInlineErrors"}
type="checkbox"
value={(!!this.state.inlineErrors).toString()}
onChange={this.handleShowInlineErrors}
/>
<label htmlFor={"showInlineErrors"}>Show inline errors</label>
<br />
<input
id={"showBrowserErrors"}
type="checkbox"
value={(!!this.state.defaultBrowserErrors).toString()}
onChange={this.handleShowBrowserErrors}
/>
<label htmlFor={"showBrowserErrors"}>
Show default browser errors
</label>
<br />
</div>
<h2>Data Dictionary</h2>
<pre
style={{
padding: "12px",
background: "rgb(244, 245, 246)",
borderRadius: "4px",
}}
>
{JSON.stringify(this.state.dataDictionary, null, 2)}
</pre>
<h2>Navigation</h2>
<pre
style={{
padding: "12px",
background: "rgb(244, 245, 246)",
borderRadius: "4px",
}}
>
{JSON.stringify(this.state.navigation, null, 2)}
</pre>
</div>
</div>
);
}
private renderDataSetComponentOptions(): React.ReactNode {
if (this.state.schema.id === testConfigs.allControlTypes.schema.id) {
return (
<select onChange={this.handleDataSetUpdate}>
{this.getComponentDataSets()}
</select>
);
}
}
private generateSchemaDictionary(): SchemaDictionary {
const schemaDictionary: SchemaDictionary = {};
Object.keys(testConfigs).forEach((testConfigKey: string) => {
schemaDictionary[testConfigs[testConfigKey].schema.id] =
testConfigs[testConfigKey].schema;
});
return schemaDictionary;
}
/**
* Gets the child options for the schema form
*/
private getChildOptions(): FormChildOptionItem[] {
const childOptions: FormChildOptionItem[] = [];
const groups: GroupItem[] = [
{
items: testConfigs,
type: "components",
},
];
for (const group of groups) {
Object.keys(group.items).map((itemName: any, key: number): void => {
if (typeof testConfigs[itemName].schema !== "undefined") {
const childObj: FormChildOptionItem = {
name: testConfigs[itemName].schema.title || "Untitled",
component: testConfigs[itemName].component,
schema: testConfigs[itemName].schema,
};
childOptions.push(childObj);
}
});
}
return childOptions;
}
private getComponentDataSets(): React.ReactNode {
return dataSets.map((dataSet: DataSet, index: number) => {
return (
<option key={index} value={index}>
{dataSet.displayName}
</option>
);
});
}
private coerceFormProps(): FormProps {
const formProps: FormProps = {
messageSystem: fastMessageSystem,
controls: this.controlPlugins,
categories: {
category: {
"": [
{
title: "String & Boolean",
dataLocations: ["string", "boolean"],
},
{
title: "Empty",
dataLocations: [],
},
{
title: "No match",
dataLocations: ["foo", "bar"],
},
{
title: "Advanced",
dataLocations: ["array", "object"],
expandByDefault: false,
},
],
object: [
{
title: "Test",
dataLocations: ["object.string"],
},
],
},
},
};
if (typeof this.state.defaultBrowserErrors === "boolean") {
formProps.displayValidationBrowserDefault = this.state.defaultBrowserErrors;
}
if (typeof this.state.inlineErrors === "boolean") {
formProps.displayValidationInline = this.state.inlineErrors;
}
return formProps;
}
private handleMessageSystem = (e: MessageEvent): void => {
switch (e.data.type) {
case MessageSystemType.initialize:
if (e.data.data && e.data.navigation) {
this.setState({
data: e.data.data,
navigation: e.data.navigation,
});
}
case MessageSystemType.data:
if (e.data.data) {
this.setState({
data: e.data.data,
dataDictionary: e.data.dataDictionary,
});
}
case MessageSystemType.navigation:
if (e.data.navigation) {
this.setState({
navigation: e.data.navigation,
});
}
}
};
private handleCSSOverrideUpdate = (): void => {
this.setState({
cssPropertyOverrides: !this.state.cssPropertyOverrides,
});
};
private handleDataSetUpdate = (e: React.ChangeEvent<HTMLSelectElement>): void => {
this.setState({
data: dataSets[parseInt(e.target.value, 10)].data,
});
};
private handleShowInlineErrors = (e: React.ChangeEvent<HTMLInputElement>): void => {
if (e.target.value === "true") {
this.setState({
inlineErrors: false,
});
} else {
this.setState({
inlineErrors: true,
});
}
};
private handleShowBrowserErrors = (e: React.ChangeEvent<HTMLInputElement>): void => {
if (e.target.value === "true") {
this.setState({
defaultBrowserErrors: false,
});
} else {
this.setState({
defaultBrowserErrors: true,
});
}
};
private handleComponentUpdate = (e: React.ChangeEvent<HTMLSelectElement>): void => {
const data: any = !!testConfigs[e.target.value].data
? testConfigs[e.target.value].data
: testConfigs[e.target.value].schema.id ===
testConfigs.allControlTypes.schema.id
? this.state.dataSet
: getDataFromSchema(testConfigs[e.target.value].schema);
if ((window as any).Worker && fastMessageSystem) {
fastMessageSystem.postMessage({
type: MessageSystemType.initialize,
data: [
{
foo: {
schemaId: testConfigs[e.target.value].schema.id,
data,
},
},
"foo",
],
schemaDictionary: this.generateSchemaDictionary(),
});
}
};
private getComponentOptions(): JSX.Element[] {
return Object.keys(testConfigs).map((testComponentKey: any, index: number) => {
return <option key={index}>{testConfigs[testComponentKey].schema.id}</option>;
});
}
}
export { FormTestPage };

Просмотреть файл

@ -0,0 +1,7 @@
# App configs
The files in this folder are for testing the form.
There should be:
- A schema conforming to a sample set of data
- An optional React component as an `index.tsx` file
- An optional configuration file for extra options specified in the root README.mds

Просмотреть файл

@ -0,0 +1,143 @@
export interface ExampleComponent {
schema: any;
data?: any;
}
import {
allControlTypesSchema,
anyOfSchema,
arraysSchema,
badgeSchema,
categorySchema,
checkboxSchema,
childrenSchema,
constSchema as constKeywordSchema,
controlPluginCssSchema,
controlPluginCssWithOverridesSchema,
controlPluginSchema as customControlSchema,
defaultsSchema,
dictionarySchema,
disabledSchema,
generalSchema,
invalidDataSchema,
mergedOneOfSchema,
nestedOneOfSchema,
nullSchema as nullKeywordSchema,
numberFieldSchema,
objectsSchema,
oneOfDeeplyNestedSchema as oneOfArraysSchema,
oneOfSchema,
textareaSchema,
textSchema,
tooltipSchema,
} from "../../../src/__tests__/schemas";
export const category: ExampleComponent = {
schema: categorySchema,
};
export const customControl: ExampleComponent = {
schema: customControlSchema,
};
export const controlPluginCssWithOverrides: ExampleComponent = {
schema: controlPluginCssWithOverridesSchema,
};
export const controlPluginCss: ExampleComponent = {
schema: controlPluginCssSchema,
};
export const textField: ExampleComponent = {
schema: textareaSchema,
};
export const text: ExampleComponent = {
schema: textSchema,
};
export const numberField: ExampleComponent = {
schema: numberFieldSchema,
};
export const checkbox: ExampleComponent = {
schema: checkboxSchema,
};
export const anyOf: ExampleComponent = {
schema: anyOfSchema,
};
export const oneOf: ExampleComponent = {
schema: oneOfSchema,
};
export const nestedOneOf: ExampleComponent = {
schema: nestedOneOfSchema,
};
export const mergedOneOf: ExampleComponent = {
schema: mergedOneOfSchema,
};
export const objects: ExampleComponent = {
schema: objectsSchema,
};
export const arrays: ExampleComponent = {
schema: arraysSchema,
};
export const oneOfArrays: ExampleComponent = {
schema: oneOfArraysSchema,
};
export const children: ExampleComponent = {
schema: childrenSchema,
};
export const generalExample: ExampleComponent = {
schema: generalSchema,
};
export const badge: ExampleComponent = {
schema: badgeSchema,
};
export const constKeyword: ExampleComponent = {
schema: constKeywordSchema,
};
import InvalidDataDataSet from "../../../src/__tests__/datasets/invalid-data";
export const invalidData: ExampleComponent = {
schema: invalidDataSchema,
data: InvalidDataDataSet,
};
export const defaults: ExampleComponent = {
schema: defaultsSchema,
};
export const nullKeyword: ExampleComponent = {
schema: nullKeywordSchema,
};
export const allControlTypes: ExampleComponent = {
schema: allControlTypesSchema,
};
import DictionaryDataSet from "../../../src/__tests__/datasets/dictionary";
export const dictionary: ExampleComponent = {
schema: dictionarySchema,
data: DictionaryDataSet,
};
export const tooltip: ExampleComponent = {
schema: tooltipSchema,
};
export const disabled: ExampleComponent = {
schema: disabledSchema,
};

Просмотреть файл

@ -0,0 +1,165 @@
import React from "react";
import { MenuItem, NavigationMenu } from "../../src";
import {
accentColorName,
L1ColorName,
L4ColorName,
textColorName,
L3FillColorName,
inactiveTextColorName,
L3OutlineColorName,
} from "../../src/style";
const menu: MenuItem[] = [
{
displayName: "foo",
location: "foo",
},
{
displayName: "bar",
location: "bar",
items: [
{
displayName: "bar1",
location: "bar1",
},
{
displayName: "bar2",
items: [
{
displayName: "bar2_1",
location: "bar2_1",
items: [
{
displayName: "bar2_1_1",
location: "bar2_1_1",
},
],
},
],
},
],
},
{
displayName: "bat",
location: "bat",
},
];
const CSSpropertyOverrides = {
[accentColorName]: "blue",
[L1ColorName]: "white",
[L4ColorName]: "lightslategray",
[textColorName]: "darkred",
[L3FillColorName]: "white",
[inactiveTextColorName]: "orange",
[L3OutlineColorName]: "orange",
};
export interface NavigationMenuTestPageState {
expanded: boolean;
location?: string;
triggerLocationUpdate?: boolean;
cssPropertyOverrides: boolean;
}
class NavigationMenuTestPage extends React.Component<{}, NavigationMenuTestPageState> {
constructor(props: {}) {
super(props);
this.state = {
expanded: void 0,
triggerLocationUpdate: false,
cssPropertyOverrides: false,
};
}
public render(): React.ReactNode {
return (
<div style={this.state.cssPropertyOverrides ? CSSpropertyOverrides : {}}>
<button onClick={this.handleExpandClick}>expand</button>
<button onClick={this.handleCollapseClick}>collapse</button>
<button
onClick={this.handleTriggerLocationUpdate}
style={this.getStyle("triggerLocationUpdate")}
>
manually trigger location update
</button>
<input
id={"useCSSOverrides"}
type={"checkbox"}
value={this.state.cssPropertyOverrides.toString()}
onChange={this.handleCSSOverrideUpdate}
/>
<label htmlFor={"useCSSOverrides"}>Show CSS property overrides</label>
<NavigationMenu
menu={menu}
expanded={this.state.expanded}
activeLocation={this.state.location}
onLocationUpdate={
this.state.triggerLocationUpdate
? this.handleLocationUpdate
: void 0
}
/>
<pre>location - {this.state.location}</pre>
<pre>{JSON.stringify(menu, null, 2)}</pre>
</div>
);
}
private handleCSSOverrideUpdate = (): void => {
this.setState({
cssPropertyOverrides: !this.state.cssPropertyOverrides,
});
};
private handleExpandClick = (): void => {
this.setState(
{
expanded: true,
},
() => {
this.setState({
expanded: void 0,
});
}
);
};
private handleCollapseClick = (): void => {
this.setState(
{
expanded: false,
},
() => {
this.setState({
expanded: void 0,
});
}
);
};
private handleLocationUpdate = (location: string): void => {
this.setState({
location,
});
};
private handleTriggerLocationUpdate = (): void => {
this.setState({
triggerLocationUpdate: !this.state.triggerLocationUpdate,
});
};
private getStyle(stateKey: string): any {
if (this.state[stateKey]) {
return {
background: "#414141",
color: "white",
};
}
}
}
export { NavigationMenuTestPage };

Просмотреть файл

@ -0,0 +1,291 @@
import React from "react";
import { ModularNavigation } from "../../src";
import childrenSchema from "./navigation/children.schema";
import { children } from "./navigation/example.data";
import { DataType, MessageSystem, MessageSystemType } from "@microsoft/fast-tooling";
import noChildrenSchema from "./navigation/no-children.schema";
import {
accentColorName,
L1ColorName,
L3ColorName,
textColorName,
L3FillColorName,
inactiveTextColorName,
L3OutlineColorName,
} from "../../src/style";
import { Data } from "@microsoft/fast-tooling";
import { MessageSystemNavigationTypeAction } from "@microsoft/fast-tooling";
export interface NavigationTestPageState {
navigation: any;
cssPropertyOverrides: boolean;
types?: DataType[];
activeDictionaryId: string;
defaultLinkedDataDroppableDataLocation: boolean;
droppableBlocklist: string[];
}
const CSSpropertyOverrides = {
[accentColorName]: "blue",
[L1ColorName]: "white",
[L3ColorName]: "lightslategray",
[textColorName]: "darkred",
[L3FillColorName]: "white",
[inactiveTextColorName]: "orange",
[L3OutlineColorName]: "orange",
};
let fastMessageSystem: MessageSystem;
class NavigationTestPage extends React.Component<{}, NavigationTestPageState> {
constructor(props: {}) {
super(props);
if ((window as any).Worker) {
fastMessageSystem = new MessageSystem({
webWorker: "message-system.js",
dataDictionary: children,
schemaDictionary: {
[childrenSchema.id]: childrenSchema,
[noChildrenSchema.id]: noChildrenSchema,
},
});
}
fastMessageSystem.add({
onMessage: this.handleMessageSystem,
});
this.state = {
navigation: null,
cssPropertyOverrides: false,
types: undefined,
activeDictionaryId: children[1],
defaultLinkedDataDroppableDataLocation: false,
droppableBlocklist: [],
};
}
public render(): React.ReactNode {
return (
<div style={this.state.cssPropertyOverrides ? CSSpropertyOverrides : {}}>
<input
id={"useCSSOverrides"}
type={"checkbox"}
value={this.state.cssPropertyOverrides.toString()}
onChange={this.handleCSSOverrideUpdate}
/>
<label htmlFor={"useCSSOverrides"}>Show CSS property overrides</label>
<br />
<fieldset>
<legend>Allow types</legend>
<input
id={"allowArrays"}
type={"checkbox"}
value={`${
Array.isArray(this.state.types) &&
!!this.state.types.find(type => type === DataType.array)
}`}
onChange={this.handleIncludeType(DataType.array)}
/>
<label htmlFor={"allowArrays"}>Allow arrays</label>
<br />
<input
id={"allowObjects"}
type={"checkbox"}
value={`${
Array.isArray(this.state.types) &&
!!this.state.types.find(type => type === DataType.object)
}`}
onChange={this.handleIncludeType(DataType.object)}
/>
<label htmlFor={"allowArrays"}>Allow objects</label>
<br />
<input
id={"allowBooleans"}
type={"checkbox"}
value={`${
Array.isArray(this.state.types) &&
!!this.state.types.find(type => type === DataType.boolean)
}`}
onChange={this.handleIncludeType(DataType.boolean)}
/>
<label htmlFor={"allowBooleans"}>Allow booleans</label>
<br />
<input
id={"allowStrings"}
type={"checkbox"}
value={`${
Array.isArray(this.state.types) &&
!!this.state.types.find(type => type === DataType.string)
}`}
onChange={this.handleIncludeType(DataType.string)}
/>
<label htmlFor={"allowStrings"}>Allow strings</label>
<br />
<input
id={"allowNumbers"}
type={"checkbox"}
value={`${
Array.isArray(this.state.types) &&
!!this.state.types.find(type => type === DataType.number)
}`}
onChange={this.handleIncludeType(DataType.number)}
/>
<label htmlFor={"allowNumbers"}>Allow numbers</label>
<br />
<input
id={"allowNulls"}
type={"checkbox"}
value={`${
Array.isArray(this.state.types) &&
!!this.state.types.find(type => type === DataType.null)
}`}
onChange={this.handleIncludeType(DataType.null)}
/>
<label htmlFor={"allowNulls"}>Allow null</label>
</fieldset>
<fieldset>
<legend>Options</legend>
<input
type={"checkbox"}
id={"assignDefaultLinkedDataDatalocation"}
onChange={this.handleSetDefaultLinkedDataDatalocation}
value={this.state.defaultLinkedDataDroppableDataLocation.toString()}
/>
<label htmlFor={"assignDefaultLinkedDataDatalocation"}>
Assign "children" as default data location
</label>
<br />
<input
type={"checkbox"}
id={"assignDroppableBlocklist"}
onChange={this.handleSetDroppableBlocklist}
value={this.state.droppableBlocklist}
/>
<label htmlFor={"assignDroppableBlocklist"}>
Assign linked data without a default slot to the blocklist
</label>
</fieldset>
{this.renderAllLinkedData()}
<ModularNavigation
messageSystem={fastMessageSystem}
types={this.state.types}
defaultLinkedDataDroppableDataLocation={
this.state.defaultLinkedDataDroppableDataLocation
? "children"
: void 0
}
droppableBlocklist={this.state.droppableBlocklist}
/>
<pre>{JSON.stringify(this.state.types, null, 2)}</pre>
<pre>{JSON.stringify(this.state.navigation, null, 2)}</pre>
</div>
);
}
private renderAllLinkedData(): React.ReactNode {
return (
<fieldset>
<legend>Linked data IDs</legend>
{this.renderLinkedDataItems()}
</fieldset>
);
}
private renderLinkedDataItems(): React.ReactNode {
return Object.entries(children[0]).map(
([key, child]: [string, Data<unknown>], index: number) => {
return (
<div key={key}>
<label htmlFor={key}>{key}</label>
<input
id={key}
name={"linked-data"}
type={"radio"}
onChange={this.handleLinkedDataNavigationOnChange(key)}
value={key}
checked={this.state.activeDictionaryId === key}
/>
</div>
);
}
);
}
private handleSetDefaultLinkedDataDatalocation = (e: React.ChangeEvent): void => {
this.setState({
defaultLinkedDataDroppableDataLocation: !this.state
.defaultLinkedDataDroppableDataLocation,
});
};
private handleSetDroppableBlocklist = (e: React.ChangeEvent): void => {
this.setState({
droppableBlocklist:
this.state.droppableBlocklist.length > 0 ? [] : [noChildrenSchema.$id],
});
};
private handleLinkedDataNavigationOnChange = (linkedDataId: string): (() => void) => {
return () => {
fastMessageSystem.postMessage({
type: MessageSystemType.navigation,
action: MessageSystemNavigationTypeAction.update,
activeDictionaryId: linkedDataId,
activeNavigationConfigId: "",
});
};
};
private handleIncludeType = (
type: DataType
): ((e: React.ChangeEvent<HTMLInputElement>) => void) => {
return (e: React.ChangeEvent<HTMLInputElement>): void => {
let currentTypes: DataType[] = [].concat(this.state.types || []);
if (e.target.checked) {
currentTypes.push(type);
} else {
const indexOfType = currentTypes.findIndex(
currentType => currentType === type
);
currentTypes.splice(indexOfType, 1);
}
if (currentTypes.length === 0) {
currentTypes = undefined;
}
this.setState({
types: currentTypes,
});
};
};
private handleMessageSystem = (e: MessageEvent): void => {
if (
e.data &&
(e.data.type === MessageSystemType.initialize ||
e.data.type === MessageSystemType.data)
) {
this.setState({
navigation: e.data.navigationDictionary,
});
} else if (e.data && e.data.type === MessageSystemType.navigation) {
this.setState({
activeDictionaryId: e.data.activeDictionaryId,
});
}
};
private handleCSSOverrideUpdate = (): void => {
this.setState({
cssPropertyOverrides: !this.state.cssPropertyOverrides,
});
};
}
export { NavigationTestPage };

Просмотреть файл

@ -0,0 +1,40 @@
import { linkedDataSchema } from "@microsoft/fast-tooling";
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with children",
description: "A test component's schema definition.",
type: "object",
id: "children",
$id: "children",
properties: {
object: {
title: "Object",
type: "object",
properties: {
children: {
title: "Object Children",
...linkedDataSchema,
},
},
},
array: {
title: "Array",
type: "array",
items: {
title: "Object",
type: "object",
properties: {
children: {
title: "Array Children",
...linkedDataSchema,
},
},
},
},
children: {
title: "Children",
...linkedDataSchema,
},
},
};

Просмотреть файл

@ -0,0 +1,72 @@
import { DataDictionary } from "@microsoft/fast-tooling";
import noChildrenSchema from "./no-children.schema";
import childrenSchema from "./children.schema";
const noChildren: any = {
text: "Hello world",
};
const children: DataDictionary<any> = [
{
foo: {
schemaId: childrenSchema.id,
data: {
children: [
{
id: "bar",
},
{
id: "bat",
},
{
id: "baz",
},
],
},
},
bar: {
parent: {
id: "foo",
dataLocation: "children",
},
schemaId: noChildrenSchema.id,
data: {
text: "bar",
},
},
bat: {
parent: {
id: "foo",
dataLocation: "children",
},
schemaId: noChildrenSchema.id,
data: {
text: "bat",
},
},
baz: {
parent: {
id: "foo",
dataLocation: "children",
},
schemaId: childrenSchema.id,
data: {
children: [
{
id: "bax",
},
],
},
},
bax: {
parent: {
id: "baz",
dataLocation: "children",
},
schemaId: noChildrenSchema.id,
data: {},
},
},
"foo",
];
export { children, noChildren };

Просмотреть файл

@ -0,0 +1,14 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component without children",
description: "A test component's schema definition.",
type: "object",
id: "no-children",
$id: "no-children",
properties: {
text: {
title: "Text",
type: "string",
},
},
};

Просмотреть файл

@ -0,0 +1,51 @@
import React from "react";
import { MessageSystemType } from "@microsoft/fast-tooling";
import { ViewerCustomAction } from "../../../src";
interface ViewerContentState {
message: string;
}
class ViewerContent extends React.Component<{}, ViewerContentState> {
constructor(props: {}) {
super(props);
this.state = {
message: "Hello world",
};
window.addEventListener("message", this.handlePostMessage);
}
public render(): React.ReactNode {
return (
<React.Fragment>
<pre>{this.state.message}</pre>
<button onClick={this.handleReset}>reset</button>
</React.Fragment>
);
}
private handleReset = (): void => {
window.postMessage(
{
type: MessageSystemType.custom,
action: ViewerCustomAction.call,
value: "reset",
},
"*"
);
};
private handlePostMessage = (e: MessageEvent): void => {
if (e.origin === location.origin && typeof e.data === "string") {
try {
this.setState({
message: JSON.stringify(JSON.parse(e.data), null, 2),
});
} catch (e) {}
}
};
}
export default ViewerContent;

Просмотреть файл

@ -0,0 +1,160 @@
import React from "react";
import { MessageSystem, MessageSystemType } from "@microsoft/fast-tooling";
import {
defaultDevices,
Device,
Display,
Orientation,
Rotate,
SelectDevice,
Viewer,
} from "../../../src";
export interface PageState {
height: number;
width: number;
activeDevice: Device;
orientation: Orientation;
inputValue: string;
}
let fastMessageSystem: MessageSystem;
class ViewerPage extends React.Component<{}, PageState> {
constructor(props: {}) {
super(props);
if ((window as any).Worker) {
fastMessageSystem = new MessageSystem({
webWorker: "message-system.js",
dataDictionary: [
{
"": {
schemaId: "foo",
data: {},
},
},
"",
],
schemaDictionary: {
foo: {
type: "object",
properties: {},
},
},
});
fastMessageSystem.add({ onMessage: this.handleMessageSystem });
}
this.state = {
height: 800,
width: 800,
activeDevice: defaultDevices[0],
orientation: Orientation.portrait,
inputValue: "",
};
}
public render(): JSX.Element {
return (
<div style={{ width: "100%", height: "calc(100vh - 200px)" }}>
<div style={{ margin: "10px 0" }}>
<input
type="text"
onChange={this.handleInputUpdate}
value={this.state.inputValue}
/>
<br />
<br />
<SelectDevice
devices={defaultDevices}
onUpdateDevice={this.handleDeviceUpdate}
activeDeviceId={this.state.activeDevice.id}
jssStyleSheet={{
selectDevice: {
paddingRight: "10px",
},
}}
/>
<Rotate
orientation={this.state.orientation}
onUpdateOrientation={this.handleOrientationUpdate}
landscapeDisabled={this.isRotateDisabled()}
portraitDisabled={this.isRotateDisabled()}
/>
</div>
<Viewer
height={this.state.height}
width={this.state.width}
iframeSrc={"/viewer/content"}
responsive={this.state.activeDevice.display === Display.responsive}
onUpdateHeight={this.handleUpdatedHeight}
onUpdateWidth={this.handleUpdatedWidth}
messageSystem={fastMessageSystem}
/>
</div>
);
}
private isRotateDisabled(): boolean {
return !!!this.state.activeDevice.width && !!!this.state.activeDevice.height;
}
private handleMessageSystem = (e: MessageEvent): void => {
if (e.data.type === MessageSystemType.custom) {
this.setState({
inputValue: e.data.value,
});
}
};
private handleInputUpdate = (e: React.ChangeEvent<HTMLInputElement>): void => {
fastMessageSystem.postMessage({
type: MessageSystemType.custom,
value: e.target.value,
} as any);
};
private handleOrientationUpdate = (orientation: Orientation): void => {
if (!this.isRotateDisabled()) {
this.setState({
orientation,
width:
orientation === Orientation.portrait
? this.state.activeDevice.width
: this.state.activeDevice.height,
height:
orientation === Orientation.portrait
? this.state.activeDevice.height
: this.state.activeDevice.width,
});
}
};
private handleDeviceUpdate = (deviceId: string): void => {
const activeDevice: Device = defaultDevices.find((device: Device) => {
return deviceId === device.id;
});
this.setState({
activeDevice,
orientation: Orientation.portrait,
height: activeDevice.height ? activeDevice.height : this.state.height,
width: activeDevice.width ? activeDevice.width : this.state.width,
});
};
private handleUpdatedHeight = (height: number): void => {
this.setState({
height,
});
};
private handleUpdatedWidth = (width: number): void => {
this.setState({
width,
});
};
}
export default ViewerPage;

Просмотреть файл

@ -0,0 +1,213 @@
import { Form, Viewer } from "../../src";
import { FormProps } from "../../src/form/form.props";
import { DesignSystemProvider } from "@microsoft/fast-jss-manager-react";
import React from "react";
import {
getDataFromSchema,
MessageSystem,
MessageSystemType,
SchemaDictionary,
} from "@microsoft/fast-tooling";
import FancyButton from "./web-components/fancy-button";
import { webComponentSchemas } from "./web-components/index";
import fancyButtonSchema from "./web-components/fancy-button.schema";
export type componentDataOnChange = (e: React.ChangeEvent<HTMLFormElement>) => void;
export interface WebComponentTestPageState {
data: any;
navigation: any;
width: number;
height: number;
}
export interface GroupItem {
items: any;
type: string;
}
const designSystemDefaults: any = {
foregroundColor: "#000",
backgroundColor: "#FFF",
brandColor: "#0078D4",
};
let fastMessageSystem: MessageSystem;
class WebComponentTestPage extends React.Component<{}, WebComponentTestPageState> {
constructor(props: {}) {
super(props);
if ((window as any).Worker) {
fastMessageSystem = new MessageSystem({
webWorker: "message-system.js",
dataDictionary: [
{
foo: {
schemaId: fancyButtonSchema.id,
data: {},
},
},
"foo",
],
schemaDictionary: this.generateSchemaDictionary(),
});
fastMessageSystem.add({ onMessage: this.handleMessageSystem });
}
this.state = {
data: {},
navigation: void 0,
height: 500,
width: 500,
};
}
public render(): JSX.Element {
return (
<DesignSystemProvider designSystem={designSystemDefaults}>
<div>
<div
style={{
width: "250px",
height: "100vh",
float: "left",
fontFamily:
"Segoe UI, SegoeUI, Helvetica Neue, Helvetica, Arial, sans-serif",
}}
>
<Form {...this.coerceFormProps()} />
</div>
<div
style={{
float: "left",
marginLeft: "8px",
}}
>
<div>
<select onChange={this.handleComponentUpdate}>
{this.getComponentOptions()}
</select>
</div>
<Viewer
messageSystem={fastMessageSystem}
iframeSrc={"/web-components/content"}
responsive={true}
onUpdateHeight={this.handleViewerUpdateHeight}
onUpdateWidth={this.handleViewerUpdateWidth}
height={this.state.height}
width={this.state.width}
/>
<h2>Data</h2>
<pre
style={{
padding: "12px",
background: "rgb(244, 245, 246)",
borderRadius: "4px",
}}
>
{JSON.stringify(this.state.data, null, 2)}
</pre>
<h2>Navigation</h2>
<pre
style={{
padding: "12px",
background: "rgb(244, 245, 246)",
borderRadius: "4px",
}}
>
{JSON.stringify(this.state.navigation, null, 2)}
</pre>
</div>
</div>
</DesignSystemProvider>
);
}
private generateSchemaDictionary(): SchemaDictionary {
const schemaDictionary: SchemaDictionary = {};
Object.keys(webComponentSchemas).forEach((webComponentSchemasKey: string) => {
schemaDictionary[webComponentSchemas[webComponentSchemasKey].schema.id] =
webComponentSchemas[webComponentSchemasKey].schema;
});
return schemaDictionary;
}
private coerceFormProps(): FormProps {
const formProps: FormProps = {
messageSystem: fastMessageSystem,
};
return formProps;
}
private handleViewerUpdateHeight = (height: number): void => {
this.setState({ height });
};
private handleViewerUpdateWidth = (width: number): void => {
this.setState({ width });
};
private handleMessageSystem = (e: MessageEvent): void => {
switch (e.data.type) {
case MessageSystemType.initialize:
if (e.data.dataDictionary && e.data.navigationDictionary) {
this.setState({
data: e.data.dataDictionary,
navigation: e.data.navigationDictionary,
});
}
case MessageSystemType.data:
if (e.data.dataDictionary) {
this.setState({
data: e.data.dataDictionary,
});
}
case MessageSystemType.navigation:
if (e.data.navigationDictionary) {
this.setState({
navigation: e.data.navigationDictionary,
});
}
}
};
private handleComponentUpdate = (e: React.ChangeEvent<HTMLSelectElement>): void => {
const data: any = !!webComponentSchemas[e.target.value].data
? webComponentSchemas[e.target.value].data
: getDataFromSchema(webComponentSchemas[e.target.value].schema);
if ((window as any).Worker && fastMessageSystem) {
fastMessageSystem.postMessage({
type: MessageSystemType.initialize,
data: [
{
foo: {
schemaId: webComponentSchemas[e.target.value].schema.id,
data,
},
},
"foo",
],
schemaDictionary: this.generateSchemaDictionary(),
});
}
};
private getComponentOptions(): JSX.Element[] {
return Object.keys(webComponentSchemas).map(
(webComponentSchemasKey: any, index: number) => {
return (
<option key={index}>
{webComponentSchemas[webComponentSchemasKey].schema.id}
</option>
);
}
);
}
}
export { WebComponentTestPage };

Просмотреть файл

@ -0,0 +1,17 @@
import { linkedDataSchema, ReservedElementMappingKeyword } from "@microsoft/fast-tooling";
export default {
$schema: "http://json-schema.org/schema#",
title: "Fancy button",
[ReservedElementMappingKeyword.mapsToTagName]: "fancy-button",
description: "A test component's schema definition.",
type: "object",
id: "fancyButton",
properties: {
children: {
title: "Default slot",
[ReservedElementMappingKeyword.mapsToSlot]: "",
...linkedDataSchema,
},
},
};

Просмотреть файл

@ -0,0 +1,12 @@
export default class FancyButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
const wrapper = document.createElement("button");
const defaultSlot = document.createElement("slot");
wrapper.appendChild(defaultSlot);
shadow.appendChild(wrapper);
}
}

Просмотреть файл

@ -0,0 +1,22 @@
import fancyButtonSchema from "./fancy-button.schema";
import textSchema from "./text.schema";
export enum TestDataType {
element = "element",
string = "string",
}
const webComponentSchemas: any = {
[fancyButtonSchema.id]: {
data: {},
type: TestDataType.element,
schema: fancyButtonSchema,
},
[textSchema.id]: {
data: "foo",
type: TestDataType.string,
schema: textSchema,
},
};
export { webComponentSchemas };

Просмотреть файл

@ -0,0 +1,7 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Text",
description: "A test component's schema definition.",
type: "string",
id: "text",
};

Просмотреть файл

@ -0,0 +1,99 @@
import React from "react";
import {
DataDictionary,
htmlMapper,
htmlResolver,
mapDataDictionary,
MessageSystemType,
SchemaDictionary,
} from "@microsoft/fast-tooling";
import FancyButton from "./fancy-button";
interface WebComponentViewerContentState {
message: string;
dataDictionary: DataDictionary<unknown>;
schemaDictionary: SchemaDictionary;
activeDictionaryId: string;
}
class WebComponentViewerContent extends React.Component<
{},
WebComponentViewerContentState
> {
private ref: React.RefObject<HTMLDivElement>;
constructor(props: {}) {
super(props);
this.ref = React.createRef();
this.state = {
message: "Hello world",
dataDictionary: null,
schemaDictionary: {},
activeDictionaryId: null,
};
customElements.define("fancy-button", FancyButton);
window.addEventListener("message", this.handlePostMessage);
}
public render(): React.ReactNode {
return (
<React.Fragment>
<div ref={this.ref} />
<pre>{this.state.message}</pre>
</React.Fragment>
);
}
private handlePostMessage = (e: MessageEvent): void => {
if (e.origin === location.origin && typeof e.data === "string") {
try {
const parsedJSON = JSON.parse(e.data);
if (parsedJSON.type === MessageSystemType.initialize) {
this.setState({
message: JSON.stringify(parsedJSON, null, 2),
dataDictionary: parsedJSON.dataDictionary,
schemaDictionary: parsedJSON.schemaDictionary,
activeDictionaryId: parsedJSON.activeDictionaryId,
});
} else if (parsedJSON.type === MessageSystemType.data) {
const mappedData: HTMLElement = mapDataDictionary({
dataDictionary: parsedJSON.dataDictionary as DataDictionary<any>,
schemaDictionary: this.state.schemaDictionary,
mapper: htmlMapper({
version: 1,
tags: [
{
name: "fancy-button",
description: "A fancier button",
attributes: [],
slots: [
{
name: "",
description: "The default slot",
},
],
},
],
}),
resolver: htmlResolver,
});
this.ref.current.innerHTML = "";
this.ref.current.append(mappedData);
this.setState({
message: JSON.stringify(parsedJSON, null, 2),
dataDictionary: parsedJSON.dataDictionary,
});
}
} catch (e) {}
}
};
}
export default WebComponentViewerContent;

Просмотреть файл

@ -0,0 +1,13 @@
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current",
},
},
],
"@babel/react",
],
};

Просмотреть файл

@ -0,0 +1,132 @@
{
"name": "@microsoft/fast-tooling-react",
"description": "A React-specific set of components and utilities to assist in creating web UI",
"sideEffects": false,
"version": "2.11.4",
"author": {
"name": "Microsoft",
"url": "https://discord.gg/FcSNfg4"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/Microsoft/fast.git"
},
"bugs": {
"url": "https://github.com/Microsoft/fast/issues/new/choose"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -p ./tsconfig.json",
"build:app": "webpack --progress --mode=production",
"clean:dist": "node ../../build/clean.js dist",
"coverage": "jest --coverage",
"prepublishOnly": "npm run clean:dist && npm run build",
"postinstall": "npm run clean:dist && npm run build",
"prettier": "prettier --config ../../.prettierrc --write \"**/*.{ts,tsx,html}\"",
"prettier:diff": "prettier --config ../../.prettierrc \"**/*.{ts,tsx,html}\" --list-different",
"start": "webpack-dev-server --history-api-fallback --progress --config webpack.config.cjs",
"test": "npm run eslint && npm run unit-tests && npm run build",
"eslint": "eslint . --ext .ts",
"eslint:fix": "eslint . --ext .ts --fix",
"unit-tests": "jest --runInBand",
"watch": "npm run build -- -w --preserveWatchOutput"
},
"jest": {
"collectCoverage": true,
"coverageReporters": [
"json",
"text",
[
"lcov",
{
"projectRoot": "../../"
}
]
],
"coverageThreshold": {
"global": {
"statements": 80,
"branches": 59,
"functions": 74,
"lines": 80
}
},
"coveragePathIgnorePatterns": [
"/(.tmp|__tests__)/*"
],
"testURL": "http://localhost",
"transform": {
"^.+\\.tsx?$": "ts-jest",
"^.+\\.jsx?$": "babel-jest"
},
"transformIgnorePatterns": [
"!<rootDir>/node_modules/lodash-es"
],
"testRegex": "(\\.|/)(test|spec)\\.(jsx?|tsx?)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json"
],
"testEnvironment": "jest-environment-jsdom-sixteen"
},
"devDependencies": {
"@babel/core": "^7.12.13",
"@babel/preset-env": "^7.12.13",
"@babel/preset-react": "^7.12.13",
"@types/jest": "^25.2.1",
"@types/lodash-es": "^4.17.4",
"@types/node": "^9.6.7",
"@types/react": "^16.8.0",
"@types/react-router": "^4.0.0",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.7.0",
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-react": "^7.19.0",
"focus-visible": "^4.1.5",
"html-webpack-plugin": "^3.2.0",
"jest": "^25.4.0",
"jest-environment-jsdom-sixteen": "^2.0.0",
"lodash-es": "4.17.15",
"prettier": "2.0.2",
"react": "^16.8.0",
"react-dnd-html5-backend": "^9.0.0",
"react-dom": "^16.8.0",
"react-redux": "^5.0.7",
"react-router-dom": "^4.2.2",
"react-test-renderer": "^16.6.3",
"rimraf": "^3.0.2",
"ts-jest": "^25.4.0",
"ts-loader": "^4.0.1",
"typescript": "^3.9.0",
"webpack": "^4.44.0",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.11.0",
"yargs": "^16.2.0"
},
"peerDependencies": {
"@microsoft/fast-jss-manager-react": "^3.0.0 || ^4.0.0",
"lodash-es": "^4.0.0",
"react": "^16.8.0"
},
"dependencies": {
"@microsoft/fast-colors": "^5.1.3",
"@microsoft/fast-components": "^2.9.2",
"@microsoft/fast-components-class-name-contracts-base": "^4.8.0",
"@microsoft/fast-components-foundation-react": "^3.2.0",
"@microsoft/fast-element": "^1.5.1",
"@microsoft/fast-foundation": "^2.13.1",
"@microsoft/fast-jss-manager-react": "^3.0.0 || ^4.0.0",
"@microsoft/fast-jss-utilities": "^4.8.0",
"@microsoft/fast-tooling": "^0.29.0",
"@microsoft/fast-web-utilities": "^4.8.1",
"@skatejs/val": "^0.5.0",
"exenv-es6": "^1.0.0",
"raf-throttle": "^2.0.3",
"react-dnd": "^9.0.0"
}
}

Просмотреть файл

@ -0,0 +1,9 @@
export default {
additionalFalse: {
foo: 1,
bar: "bat",
},
ad: "foo",
b: "bar",
c: "bat",
};

Просмотреть файл

@ -0,0 +1,17 @@
export default {
validBooleanRequired: true,
invalidBooleanWrongType: "foo",
invalidNullWrongType: "bar",
invalidStringWrongType: false,
invalidNumberWrongType: "bar",
invalidEnumWrongType: "hello",
invalidObjectWrongType: true,
invalidObjectMissingProperty: {
foo: "bar",
},
invalidArrayWrongType: "world",
objectExample: {
invalidBooleanWrongType: "bat",
},
arrayExample: [true, "foo", false, "bar", 3],
};

Просмотреть файл

@ -0,0 +1,13 @@
Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});

Просмотреть файл

@ -0,0 +1,15 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with horizontal alignment",
description: "A test component's schema definition.",
type: "object",
id: "alignHorizontal",
properties: {
alignHorizontal: {
title: "Horizontal alignment",
type: "string",
enum: ["left", "center", "right"],
},
},
required: ["alignHorizontal"],
};

Просмотреть файл

@ -0,0 +1,50 @@
import { linkedDataSchema } from "@microsoft/fast-tooling";
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with all control types",
description: "A test component's schema definition.",
type: "object",
id: "allControlTypes",
properties: {
textarea: {
title: "textarea",
type: "string",
},
"section-link": {
title: "section-link",
type: "object",
},
display: {
title: "display",
const: "foobar",
},
checkbox: {
title: "checkbox",
type: "boolean",
},
button: {
title: "button",
type: "null",
},
array: {
title: "array",
items: {
title: "array item",
type: "string",
},
},
"number-field": {
title: "number-field",
type: "number",
},
select: {
title: "select",
type: "string",
enum: ["foo", "bar", "bat"],
},
children: {
...linkedDataSchema,
},
},
};

Просмотреть файл

@ -0,0 +1,170 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with anyOf",
description: "A test component's schema definition.",
type: "object",
id: "anyOf",
anyOf: [
{
description: "String",
type: "object",
additionalProperties: false,
properties: {
string: {
title: "String",
type: "string",
},
},
},
{
description: "Number",
type: "object",
additionalProperties: false,
properties: {
number: {
title: "Number",
type: "number",
},
},
},
{
description: "Sub-object alpha",
type: "object",
additionalProperties: false,
properties: {
subObjectAlpha: {
title: "Sub-object alpha",
type: "object",
properties: {
foo: {
title: "Sub-object alpha foo",
type: "string",
},
},
},
},
},
{
description: "Sub-object beta",
type: "object",
additionalProperties: false,
properties: {
subObjectBeta: {
title: "Sub-object beta",
type: "object",
properties: {
bar: {
title: "Sub-object alpha bar",
type: "string",
},
},
},
},
},
{
title: "Nested anyOf",
description: "Nested anyOf",
type: "object",
additionalProperties: false,
properties: {
nestedAnyOf: {
title: "Nested anyOf",
description: "Nested anyOf Configuration",
anyOf: [
{
description: "Object",
type: "object",
properties: {
object: {
title: "String",
type: "object",
properties: {
string: {
title: "String",
type: "string",
},
},
required: ["string"],
},
},
required: ["object"],
},
{
description: "String",
type: "object",
properties: {
string: {
title: "String",
type: "string",
},
},
required: ["string"],
},
{
description: "Number",
type: "object",
properties: {
number: {
title: "Number",
type: "number",
},
},
required: ["number"],
},
],
},
},
},
{
additionalProperties: false,
description: "Number or String",
type: "object",
properties: {
numberOrString: {
anyOf: [
{
title: "Number",
type: "number",
},
{
title: "String",
type: "string",
},
{
title: "Array",
type: "array",
items: {
title: "Array item",
type: "string",
},
},
{
title: "Array with anyOf in items",
type: "array",
items: {
anyOf: [
{
additionalProperties: false,
title: "Array item object",
type: "object",
properties: {
string: {
title: "Array item object string",
type: "string",
},
},
},
{
title: "Array item number",
type: "number",
},
],
},
},
],
},
},
required: ["numberOrString"],
},
],
};

Просмотреть файл

@ -0,0 +1,67 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with array",
description: "A test component's schema definition.",
type: "object",
id: "arrays",
properties: {
strings: {
title: "Array of strings",
type: "array",
items: {
title: "String",
type: "string",
},
minItems: 2,
maxItems: 5,
},
objects: {
title: "Array of objects",
type: "array",
items: {
title: "Object",
type: "object",
properties: {
string: {
title: "String",
type: "string",
},
},
required: ["string"],
},
},
stringsWithDefault: {
title: "Array of strings with default",
type: "array",
items: {
title: "String",
type: "string",
},
default: ["foo", "bar"],
},
objectsWithDefault: {
title: "Array of objects with default",
type: "array",
items: {
title: "Object",
type: "object",
properties: {
string: {
title: "String",
type: "string",
},
},
required: ["string"],
},
default: [
{
string: "foo",
},
{
string: "bar",
},
],
},
},
required: ["strings"],
};

Просмотреть файл

@ -0,0 +1,145 @@
import { linkedDataSchema } from "@microsoft/fast-tooling";
export default {
$schema: "http://json-schema.org/schema#",
title: "Badge",
description: "A test component's schema definition.",
type: "object",
id: "badge",
properties: {
string: {
title: "Textarea",
badge: "info",
badgeDescription: "More information is provided",
type: "string",
},
boolean: {
title: "Checkbox",
badge: "warning",
badgeDescription: "Warning message",
type: "boolean",
},
enum: {
title: "Select",
badge: "locked",
badgeDescription: "This field is locked",
type: "string",
enum: ["span", "button"],
},
number: {
title: "Numberfield",
badge: "info",
badgeDescription: "More information is provided",
type: "number",
},
object: {
title: "Object (no required items)",
badge: "warning",
badgeDescription: "Warning message",
type: "object",
properties: {
number: {
type: "number",
},
},
},
array: {
title: "Array",
type: "array",
badge: "warning",
badgeDescription: "Warning message",
items: {
title: "String item",
type: "string",
},
},
stringWithDefault: {
title: "Textarea",
badge: "info",
badgeDescription: "More information is provided",
type: "string",
default: "Hello world",
},
booleanWithDefault: {
title: "Checkbox",
badge: "warning",
badgeDescription: "Warning message",
type: "boolean",
default: true,
},
enumWithDefault: {
title: "Select",
badge: "locked",
badgeDescription: "This field is locked",
type: "string",
enum: ["span", "button"],
default: "button",
},
numberWithDefault: {
title: "Numberfield",
badge: "info",
badgeDescription: "More information is provided",
type: "number",
default: 42,
},
objectWithDefault: {
title: "Object (no required items)",
badge: "warning",
badgeDescription: "Warning message",
type: "object",
properties: {
number: {
type: "number",
},
},
default: {
number: 100,
},
},
arrayWithDefault: {
title: "Array",
type: "array",
badge: "warning",
badgeDescription: "Warning message",
items: {
title: "String item",
type: "string",
},
default: ["foo", "bar"],
},
constWithDefault: {
title: "Display using const",
badge: "locked",
badgeDescription: "This field is locked",
type: "string",
const: "A",
default: "B",
},
selectWithSingleItemWithDefault: {
title: "Display using single enum",
badge: "locked",
badgeDescription: "This field is locked",
type: "string",
enum: ["A"],
default: "B",
},
children: {
...linkedDataSchema,
badge: "warning",
badgeDescription: "Warning message",
},
childrenWithDefault: {
...linkedDataSchema,
badge: "warning",
badgeDescription: "Warning message",
default: {
id: "textField",
props: {
text: "Hello world",
},
},
},
},
};

Просмотреть файл

@ -0,0 +1,53 @@
import { linkedDataSchema } from "@microsoft/fast-tooling";
export default {
$schema: "http://json-schema.org/schema#",
title: "Category",
description: "A test component's schema definition.",
type: "object",
$id: "category",
id: "category",
properties: {
string: {
title: "Textarea",
type: "string",
},
boolean: {
title: "Checkbox",
type: "boolean",
},
enum: {
title: "Select",
type: "string",
enum: ["span", "button"],
},
number: {
title: "Numberfield",
type: "number",
},
object: {
title: "Object",
type: "object",
properties: {
number: {
type: "number",
},
string: {
type: "string",
},
},
},
array: {
title: "Array",
type: "array",
items: {
title: "String item",
type: "string",
},
},
children: {
title: "Linked data",
...linkedDataSchema,
},
},
};

Просмотреть файл

@ -0,0 +1,28 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with checkbox",
description: "A test component's schema definition.",
type: "object",
id: "checkbox",
properties: {
toggle: {
title: "Required Checkbox",
type: "boolean",
},
optionalToggle: {
title: "Optional Checkbox",
type: "boolean",
},
defaultToggle: {
title: "Default Checkbox",
type: "boolean",
default: true,
},
disabledToggle: {
title: "Disabled Checkbox",
type: "boolean",
disabled: true,
},
},
required: ["toggle"],
};

Просмотреть файл

@ -0,0 +1,44 @@
import { linkedDataSchema } from "@microsoft/fast-tooling";
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with custom properties ",
description: "A test component's schema definition.",
type: "object",
id: "children-with-react-props",
properties: {
boolean: {
title: "Boolean",
type: "boolean",
pluginId: "boolean-plugin-resolver",
},
array: {
title: "Array of strings",
type: "array",
pluginId: "array-plugin-resolver",
items: {
title: "String",
type: "string",
},
},
arrayObject: {
title: "Array of objects",
type: "array",
items: {
title: "Object",
type: "object",
properties: {
content: {
...linkedDataSchema,
pluginId: "children-plugin-resolver",
},
},
},
},
render: {
...linkedDataSchema,
pluginId: "children-plugin-resolver",
},
},
};

Просмотреть файл

@ -0,0 +1,54 @@
import { linkedDataSchema } from "@microsoft/fast-tooling";
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with children",
alias: "With Children",
description: "A test component's schema definition.",
type: "object",
id: "children",
properties: {
objectContainingNestedChildren: {
title: "Object with nested children",
type: "object",
properties: {
nestedObjectChildren: {
title: "Children in object",
...linkedDataSchema,
},
},
},
arrayContainingNestedChildren: {
title: "Array with nested children",
type: "array",
items: {
title: "Nested array item",
type: "object",
properties: {
nestedArrayChildren: {
title: "Children",
...linkedDataSchema,
},
},
},
},
children: {
title: "Children",
...linkedDataSchema,
formControlId: "react-children",
defaults: ["text"],
examples: ["Foo"],
},
restrictedWithChildren: {
title: "Restricted children with react defaults",
...linkedDataSchema,
ids: ["objects", "arrays"],
defaults: ["text"],
},
restrictedChildrenWithReactDefaults: {
title: "Restricted children without react defaults",
...linkedDataSchema,
ids: ["children"],
},
},
};

Просмотреть файл

@ -0,0 +1,8 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component interpreted by a plugin",
description: "A test component's schema definition.",
type: "string",
id: "component-with-plugin-interpretation",
pluginId: "string-plugin-resolver",
};

Просмотреть файл

@ -0,0 +1,26 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with const",
description: "A test component's schema definition.",
type: "object",
id: "constKeyword",
properties: {
foo: {
title: "Required with default",
type: "string",
const: "foo",
default: "default",
},
bar: {
title: "Required without default",
type: "number",
const: 40,
},
bat: {
title: "Optional without default",
type: "boolean",
const: true,
},
},
required: ["foo", "bar"],
};

Просмотреть файл

@ -0,0 +1,14 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with custom CSS controls with overrides",
description: "A test component's schema definition.",
type: "object",
id: "controlPluginCssWithOverrides",
properties: {
cssWithOverrides: {
title: "CSS with overrides",
type: "string",
formControlId: "custom-controls/css-with-overrides",
},
},
};

Просмотреть файл

@ -0,0 +1,31 @@
import { linkedDataSchema } from "@microsoft/fast-tooling";
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with custom CSS controls",
description: "A test component's schema definition.",
type: "object",
id: "controlPluginCss",
properties: {
css: {
title: "CSS",
type: "string",
formControlId: "custom-controls/css",
},
object: {
title: "Nested object",
type: "object",
properties: {
cssWithOverrides2: {
title: "CSS 2",
type: "string",
formControlId: "custom-controls/css",
},
},
},
children: {
title: "Children",
...linkedDataSchema,
},
},
};

Просмотреть файл

@ -0,0 +1,37 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with custom controls",
description: "A test component's schema definition.",
type: "object",
id: "customControl",
properties: {
file: {
title: "File",
type: "string",
formControlId: "custom-controls/file",
},
textAlign: {
title: "Text align",
type: "string",
enum: ["left", "center", "right", "justify"],
formControlId: "custom-controls/textAlign",
},
align: {
title: "Align",
type: "string",
enum: ["top", "center", "bottom"],
formControlId: "custom-controls/align",
},
fileUpload: {
title: "File upload",
type: "string",
formControlId: "custom-controls/fileUpload",
},
theme: {
title: "Theme",
type: "string",
enum: ["light", "dark"],
formControlId: "custom-controls/theme",
},
},
};

Просмотреть файл

@ -0,0 +1,97 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with defaults",
description: "A test component's schema definition.",
type: "object",
id: "defaults",
properties: {
a: {
title: "With default",
type: "string",
default: "correct",
},
b: {
title: "Without default",
type: "string",
},
c: {
title: "With default",
type: "object",
properties: {
alpha: {
title: "With default",
type: "string",
default: "correct",
},
beta: {
title: "Without default",
type: "string",
},
},
default: {
alpha: "incorrect",
beta: "correct",
},
},
d: {
title: "Without default",
type: "object",
properties: {
alpha: {
title: "With default",
type: "string",
default: "correct",
},
beta: {
title: "Without default",
type: "string",
},
},
},
e: {
title: "With default",
type: "array",
items: {
title: "With default",
type: "string",
default: "correct",
},
default: ["correct", "correct"],
},
f: {
title: "With default",
type: "string",
enum: ["foo", "bar"],
default: "bar",
},
g: {
title: "With default",
type: "boolean",
default: true,
},
h: {
title: "With default",
type: "string",
default: "foo",
const: "foo",
},
i: {
title: "With default",
type: "null",
default: null,
},
},
default: {
a: "incorrect",
b: "correct",
c: {
alpha: "incorrect",
beta: "incorrect",
},
d: {
alpha: "incorrect",
beta: "correct",
},
e: ["correct"],
},
};

Просмотреть файл

@ -0,0 +1,33 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with additional properties",
description: "A test component's schema definition.",
type: "object",
id: "dictionary",
propertyTitle: "Item key",
properties: {
additionalObjects: {
title: "A dictionary of objects",
type: "object",
additionalProperties: {
title: "An object",
type: "object",
properties: {
foo: {
type: "string",
},
},
},
},
additionalFalse: {
title: "A non-dictionary",
type: "object",
properties: {},
additionalProperties: false,
},
},
additionalProperties: {
title: "String item",
type: "string",
},
};

Просмотреть файл

@ -0,0 +1,61 @@
import { linkedDataSchema } from "@microsoft/fast-tooling";
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with all disabled types",
description: "A test component's schema definition.",
type: "object",
id: "disabled",
disabled: true,
properties: {
textarea: {
title: "textarea",
type: "string",
disabled: true,
},
"section-link": {
title: "section-link",
type: "object",
disabled: true,
},
display: {
title: "display",
const: "foobar",
disabled: true,
},
checkbox: {
title: "checkbox",
type: "boolean",
disabled: true,
},
button: {
title: "button",
type: "null",
disabled: true,
},
array: {
title: "array",
items: {
title: "array item",
type: "string",
disabled: true,
},
disabled: true,
},
"number-field": {
title: "number-field",
type: "number",
disabled: true,
},
select: {
title: "select",
type: "string",
enum: ["foo", "bar", "bat"],
disabled: true,
},
children: {
...linkedDataSchema,
disabled: true,
},
},
};

Просмотреть файл

@ -0,0 +1,124 @@
import { linkedDataSchema } from "@microsoft/fast-tooling";
export default {
$schema: "http://json-schema.org/schema#",
title: "General example",
description: "A test component's schema definition.",
type: "object",
id: "generalExample",
properties: {
title: {
title: "Title",
type: "string",
},
toggle: {
title: "Checkbox",
type: "boolean",
},
details: {
title: "Details",
type: "string",
},
toggle2: {
title: "Checkbox",
type: "boolean",
},
tag: {
title: "HTML tag",
type: "string",
enum: ["span", "button"],
default: "button",
},
level: {
title: "Level",
type: "number",
examples: [5],
},
text: {
title: "Text",
type: "string",
examples: ["Example text"],
},
alignHorizontal: {
title: "Align horizontal",
type: "string",
enum: ["left", "center", "right"],
},
alignVertical: {
title: "Align vertical",
type: "string",
enum: ["top", "bottom", "center"],
},
level2: {
title: "Level",
type: "number",
examples: [100],
},
objectNoRequired: {
title: "Object (no required items)",
type: "object",
properties: {
number: {
type: "number",
},
},
},
objectWithRequired: {
title: "Object (with required items)",
type: "object",
properties: {
boolean: {
type: "boolean",
},
},
required: ["boolean"],
},
optionalObjectWithRequired: {
title: "Optional object",
type: "object",
properties: {
string: {
type: "string",
},
},
required: ["string"],
},
optionalObjectNoRequired: {
title: "Object with no required items",
type: "object",
properties: {
boolean: {
type: "boolean",
},
},
},
strings: {
title: "Array",
type: "array",
items: {
type: "string",
},
minItems: 0,
maxItems: 6,
},
theme: {
title: "Theme",
type: "string",
enum: ["light", "dark"],
},
children: {
...linkedDataSchema,
},
},
required: [
"alignHorizontal",
"alignVertical",
"level",
"level2",
"title",
"details",
"tag",
"toggle",
"toggle2",
],
};

Просмотреть файл

@ -0,0 +1,124 @@
import { linkedDataSchema } from "@microsoft/fast-tooling";
export default {
$schema: "http://json-schema.org/schema#",
title: "General example",
description: "A test component's schema definition.",
type: "object",
id: "generalExample",
properties: {
title: {
title: "Title",
type: "string",
},
toggle: {
title: "Checkbox",
type: "boolean",
},
details: {
title: "Details",
type: "string",
},
toggle2: {
title: "Checkbox",
type: "boolean",
},
tag: {
title: "HTML tag",
type: "string",
enum: ["span", "button"],
default: "button",
},
level: {
title: "Level",
type: "number",
examples: [5],
},
text: {
title: "Text",
type: "string",
examples: ["Example text"],
},
alignHorizontal: {
title: "Align horizontal",
type: "string",
enum: ["left", "center", "right"],
},
alignVertical: {
title: "Align vertical",
type: "string",
enum: ["top", "bottom", "center"],
},
level2: {
title: "Level",
type: "number",
examples: [100],
},
objectNoRequired: {
title: "Object (no required items)",
type: "object",
properties: {
number: {
type: "number",
},
},
},
objectWithRequired: {
title: "Object (with required items)",
type: "object",
properties: {
boolean: {
type: "boolean",
},
},
required: ["boolean"],
},
optionalObjectWithRequired: {
title: "Optional object",
type: "object",
properties: {
string: {
type: "string",
},
},
required: ["string"],
},
optionalObjectNoRequired: {
title: "Object with no required items",
type: "object",
properties: {
boolean: {
type: "boolean",
},
},
},
strings: {
title: "Array",
type: "array",
items: {
type: "string",
},
minItems: 0,
maxItems: 6,
},
theme: {
title: "Theme",
type: "string",
enum: ["light", "dark"],
},
children: {
...linkedDataSchema,
},
},
required: [
"alignHorizontal",
"alignVertical",
"level",
"level2",
"title",
"details",
"tag",
"toggle",
"toggle2",
],
};

Просмотреть файл

@ -0,0 +1,65 @@
import alignHorizontalSchema from "./align-horizontal.schema";
import allControlTypesSchema from "./all-control-types.schema";
import anyOfSchema from "./any-of.schema";
import arraysSchema from "./arrays.schema";
import badgeSchema from "./badge.schema";
import categorySchema from "./category.schema";
import checkboxSchema from "./checkbox.schema";
import childrenPluginSchema from "./children.schema";
import childrenSchema from "./children.schema";
import componentPluginSchema from "./component-plugin.schema";
import constSchema from "./const.schema";
import controlPluginSchema from "./control-plugin.schema";
import controlPluginCssWithOverridesSchema from "./control-plugin.css-with-overrides.schema";
import controlPluginCssSchema from "./control-plugin.css.schema";
import defaultsSchema from "./defaults.schema";
import dictionarySchema from "./dictionary.schema";
import disabledSchema from "./disabled.schema";
import generalExampleSchema from "./general-example.schema";
import generalSchema from "./general.schema";
import invalidDataSchema from "./invalid-data.schema";
import mergedOneOfSchema from "./merged-one-of.schema";
import nestedOneOfSchema from "./nested-one-of.schema";
import nullSchema from "./null.schema";
import numberFieldSchema from "./number-field.schema";
import objectsSchema from "./objects.schema";
import oneOfDeeplyNestedSchema from "./one-of-deeply-nested.schema";
import oneOfSchema from "./one-of.schema";
import textFieldSchema from "./text-field.schema";
import textareaSchema from "./textarea.schema";
import textSchema from "./text.schema";
import tooltipSchema from "./tooltip.schema";
export {
alignHorizontalSchema,
allControlTypesSchema,
anyOfSchema,
arraysSchema,
badgeSchema,
categorySchema,
checkboxSchema,
childrenPluginSchema,
childrenSchema,
componentPluginSchema,
constSchema,
controlPluginSchema,
controlPluginCssWithOverridesSchema,
controlPluginCssSchema,
defaultsSchema,
dictionarySchema,
disabledSchema,
generalExampleSchema,
generalSchema,
invalidDataSchema,
mergedOneOfSchema,
nestedOneOfSchema,
nullSchema,
numberFieldSchema,
objectsSchema,
oneOfDeeplyNestedSchema,
oneOfSchema,
textFieldSchema,
textareaSchema,
textSchema,
tooltipSchema,
};

Просмотреть файл

@ -0,0 +1,84 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with invalid data",
description: "A test component's schema definition.",
type: "object",
id: "invalidData",
properties: {
invalidBooleanWrongType: {
title: "Invalid boolean wrong type",
type: "boolean",
},
invalidBooleanRequired: {
title: "Invalid boolean required",
type: "boolean",
},
invalidNullWrongType: {
title: "Invalid null wrong type",
type: "null",
},
invalidNullRequired: {
title: "Invalid null required",
type: "null",
},
invalidStringWrongType: {
title: "Invalid string wrong type",
type: "string",
},
invalidNumberWrongType: {
title: "Invalid number wrong type",
type: "number",
},
invalidEnumWrongType: {
title: "Invalid enum wrong type",
type: "string",
enum: ["foo", "bar", "bat"],
},
invalidArrayWrongType: {
title: "Invalid array wrong type",
type: "array",
items: {
title: "Invalid array wrong type item",
type: "string",
},
},
invalidObjectWrongType: {
title: "Invalid object wrong type",
type: "object",
},
invalidObjectMissingProperty: {
title: "Invalid object missing property",
type: "object",
properties: {
foo: {
title: "String",
type: "string",
},
bar: {
title: "Missing required property",
type: "string",
},
},
required: ["bar"],
},
objectExample: {
title: "Object",
type: "object",
properties: {
invalidBooleanWrongType: {
title: "Invalid boolean wrong type",
type: "boolean",
},
},
},
arrayExample: {
title: "Array",
type: "array",
items: {
title: "Invalid string wrong type",
type: "string",
},
},
},
required: ["invalidBooleanRequired", "invalidNullRequired"],
};

Просмотреть файл

@ -0,0 +1,43 @@
export default {
$schema: "http://json-schema.org/schema#",
type: "object",
id: "mergedOneOf",
title: "Merged oneOf",
properties: {
foo: {
title: "Foo",
type: "object",
required: ["a", "b"],
oneOf: [
{
title: "Overridden Foo 1",
description: "A is string",
properties: {
a: {
title: "A",
type: "string",
},
b: {
title: "B",
type: "number",
},
},
},
{
title: "Overridden Foo 2",
description: "B is string",
properties: {
a: {
title: "A",
type: "number",
},
b: {
title: "B",
type: "string",
},
},
},
],
},
},
};

Просмотреть файл

@ -0,0 +1,37 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with a nested oneOf",
description: "A test component's schema definition.",
type: "object",
id: "nestedOneOf",
properties: {
single: {
title: "Single oneOf",
type: "object",
oneOf: [
{
title: "Object",
description: "string",
type: "object",
properties: {
omega: {
title: "Omega",
type: "string",
},
alpha: {
title: "Alpha",
type: "object",
properties: {
beta: {
title: "string",
type: "string",
},
},
},
},
required: ["omega", "alpha"],
},
],
},
},
};

Просмотреть файл

@ -0,0 +1,18 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with null",
description: "A test component's schema definition.",
type: "object",
id: "nullKeyword",
properties: {
optionalNull: {
title: "optional null",
type: "null",
},
requiredNull: {
title: "required null",
type: "null",
},
},
required: ["requiredNull"],
};

Просмотреть файл

@ -0,0 +1,38 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with number-field",
description: "A test component's schema definition.",
type: "object",
id: "numberField",
properties: {
level: {
title: "Level",
type: "number",
enum: [1, 2, 3, 4],
default: 1,
},
quantity: {
title: "Quantity",
type: "number",
examples: [10],
minimum: 0,
maximum: 50,
multipleOf: 5,
},
defaultNumber: {
title: "Default number",
type: "number",
default: 5,
},
optionalNumber: {
title: "Optional number field",
type: "number",
},
disabledNumber: {
title: "Disabled number field",
type: "number",
disabled: true,
},
},
required: ["level", "quantity"],
};

Просмотреть файл

@ -0,0 +1,93 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with objects",
description: "A test component's schema definition.",
type: "object",
id: "objects",
properties: {
objectNoRequired: {
title: "object with no required items",
type: "object",
properties: {
number: {
type: "number",
},
},
},
objectWithRequired: {
title: "object with required items",
type: "object",
properties: {
boolean: {
type: "boolean",
},
},
required: ["boolean"],
},
optionalObjectWithRequired: {
title: "optional object",
type: "object",
properties: {
string: {
type: "string",
},
},
required: ["string"],
},
optionalObjectNoRequired: {
title: "object with no required items",
type: "object",
properties: {
boolean: {
type: "boolean",
},
},
},
optionalObjectWithNestedObject: {
title: "object with nested object",
type: "object",
properties: {
nestedObject: {
title: "Nested object",
type: "object",
properties: {
boolean: {
type: "boolean",
},
},
},
},
},
optionalObjectWithDefaultValue: {
title: "object with defaultValues",
type: "object",
properties: {
foo: {
type: "string",
},
},
default: {
foo: "bar",
},
},
optionalObjectDisabled: {
title: "object disabled",
type: "object",
disabled: true,
properties: {
foo: {
title: "Disabled textarea",
type: "string",
},
bar: {
title: "Disabled numberfield",
type: "number",
},
},
default: {
foo: "bar",
},
},
},
required: ["objectNoRequired", "objectWithRequired"],
};

Просмотреть файл

@ -0,0 +1,66 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Complex nesting arrays with oneOf",
type: "object",
id: "oneOfArrays",
properties: {
propertyKey: {
title: "Property key",
oneOf: [
{
title: "Containing object",
type: "object",
properties: {
propertyKey1: {
title: "propertyKey1 items",
type: "object",
properties: {
propertyKey2: {
title: "propertyKey2",
type: "object",
oneOf: [
{
title: "Alpha",
type: "object",
description: "Alpha",
properties: {
foo: {
title: "Foo",
type: "string",
enum: ["a"],
},
bar: {
title: "Bar",
type: "string",
enum: ["a"],
},
},
},
{
title: "Beta",
type: "object",
description: "Beta",
properties: {
foo: {
title: "Foo",
type: "string",
enum: ["b"],
},
bar: {
title: "Bar",
type: "string",
enum: ["b"],
},
},
},
],
},
},
},
},
required: ["propertyKey1"],
},
],
},
},
};

Просмотреть файл

@ -0,0 +1,124 @@
import { linkedDataSchema } from "@microsoft/fast-tooling";
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with oneOf",
description: "A test component's schema definition.",
type: "object",
id: "oneOf",
oneOf: [
{
description: "string",
type: "object",
additionalProperties: false,
properties: {
string: {
title: "string",
type: "string",
},
},
required: ["string"],
},
{
description: "number",
type: "object",
additionalProperties: false,
properties: {
number: {
title: "number",
type: "number",
},
},
required: ["number"],
},
{
title: "Number or string",
description: "number or string",
type: "object",
additionalProperties: false,
properties: {
numberOrString: {
title: "Number or string configuration",
oneOf: [
{
title: "number",
type: "number",
},
{
title: "string",
type: "string",
},
{
title: "object",
type: "object",
additionalProperties: false,
properties: {
object: {
title: "An object",
type: "object",
properties: {
number: {
title: "number",
type: "number",
},
},
required: ["number"],
},
},
required: ["object"],
},
{
title: "array",
type: "array",
items: {
title: "Array",
oneOf: [
{
title: "string item",
type: "string",
},
{
title: "number item",
type: "number",
},
],
},
},
{
title: "children object",
type: "object",
additionalProperties: false,
properties: {
children: {
...linkedDataSchema,
},
},
},
],
},
},
required: ["numberOrString"],
},
{
description: "category",
type: "object",
additionalProperties: false,
properties: {
foo: {
title: "string",
type: "string",
},
},
required: ["foo"],
formConfig: {
categories: [
{
title: "Category A",
expandable: true,
items: ["foo"],
},
],
},
},
],
};

Просмотреть файл

@ -0,0 +1,21 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with text-field",
description: "A test component's schema definition.",
type: "object",
id: "textField",
properties: {
tag: {
title: "Tag",
type: "string",
enum: ["span", "button"],
default: "button",
},
text: {
title: "Text",
type: "string",
examples: ["Example text"],
},
},
required: ["tag", "text"],
};

Просмотреть файл

@ -0,0 +1,8 @@
export default {
$schema: "http://json-schema.org/schema#",
description: "A test component's schema definition.",
id: "text",
title: "Text",
type: "string",
examples: ["Example text"],
};

Просмотреть файл

@ -0,0 +1,55 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with text-field",
description: "A test component's schema definition.",
type: "object",
id: "textField",
properties: {
tag: {
title: "Tag",
type: "string",
enum: ["span", "button"],
default: "button",
},
textWithExamples: {
title: "Text with examples",
type: "string",
examples: ["Example text"],
},
textWithDefault: {
title: "Text with default",
type: "string",
default: "Default value",
},
optionalTextWithExamples: {
title: "Optional text with examples",
type: "string",
examples: ["Example text"],
},
optionalTextWithDefault: {
title: "Optional text with default",
type: "string",
default: "Default value",
},
optionalTag: {
title: "Optional tag",
type: "string",
enum: ["span", "button"],
default: "button",
},
disabledTag: {
title: "Disabled tag",
type: "string",
enum: ["span", "button"],
default: "button",
disabled: true,
},
disabledText: {
title: "Disabled text area",
type: "string",
examples: ["Example text"],
disabled: true,
},
},
required: ["tag", "textWithDefault", "textWithExamples", "disabledText"],
};

Просмотреть файл

@ -0,0 +1,19 @@
export default {
$schema: "http://json-schema.org/schema#",
title: "Component with tooltips",
description: "A test component's schema definition.",
type: "object",
id: "tooltip",
properties: {
labelOnStandardControl: {
title: "My label 1",
description: "My label's tooltip 1",
type: "string",
},
labelOnSingleLineControl: {
title: "My label 2",
description: "My label's tooltip 2",
type: "boolean",
},
},
};

Просмотреть файл

@ -0,0 +1 @@
export * from "./mapping";

Просмотреть файл

@ -0,0 +1,316 @@
import React from "react";
import "../__tests__/mocks/match-media";
import {
DataType,
dictionaryLink,
linkedDataSchema,
mapDataDictionary,
pluginIdKeyword,
} from "@microsoft/fast-tooling";
import { ComponentDictionary, reactMapper, reactResolver } from "./mapping";
import Adapter from "enzyme-adapter-react-16";
import { configure, mount } from "enzyme";
/**
* Configure Enzyme
*/
configure({ adapter: new Adapter() });
class Foo extends React.Component<{}, {}> {
public render(): React.ReactNode {
return <div>{this.props.children}</div>;
}
}
class Bar extends React.Component<{}, {}> {
public render(): React.ReactNode {
return <div>{this.props.children}</div>;
}
}
const componentDictionary: ComponentDictionary = {
foo: Foo,
bar: Bar,
};
describe("reactMapper", () => {
test("should map data to a React component as props", () => {
const resolvedData: any = mapDataDictionary({
dataDictionary: [
{
foo: {
schemaId: "foo",
data: {
text: "Hello",
number: 42,
},
},
},
"foo",
],
mapper: reactMapper(componentDictionary),
resolver: reactResolver,
schemaDictionary: {
foo: {
id: "foo",
type: "object",
properties: {
text: {
type: "string",
},
number: {
type: "number",
},
},
},
},
});
const mappedData: any = mount(resolvedData);
const mappedComponent: any = mappedData.find("Foo");
expect(mappedComponent).toHaveLength(1);
expect(mappedComponent.prop("text")).toEqual("Hello");
expect(mappedComponent.prop("number")).toEqual(42);
});
test("should map data to a React component as children", () => {
const resolvedData: any = mapDataDictionary({
dataDictionary: [
{
foo: {
schemaId: "foo",
data: {
children: [
{
id: "bat",
},
{
id: "bar",
},
],
},
},
bar: {
schemaId: "bar",
parent: {
id: "foo",
dataLocation: "children",
},
data: "Hello world",
},
bat: {
schemaId: "bar",
parent: {
id: "foo",
dataLocation: "children",
},
data: "Foo",
},
},
"foo",
],
mapper: reactMapper(componentDictionary),
resolver: reactResolver,
schemaDictionary: {
foo: {
id: "foo",
type: "object",
properties: {
children: {
...linkedDataSchema,
},
},
},
bar: {
id: "bar",
type: "string",
},
},
});
const mappedData: any = mount(resolvedData);
expect(mappedData.find("Foo")).toHaveLength(1);
expect(mappedData.text()).toEqual("FooHello world");
});
test("should map data to nested React components", () => {
const mappedData: any = mount(
mapDataDictionary({
dataDictionary: [
{
foo: {
schemaId: "foo",
data: {
children: [
{
id: "bar",
dataLocation: "children",
},
],
},
},
bar: {
schemaId: "bar",
parent: {
id: "foo",
dataLocation: "children",
},
data: {
children: [
{
id: "bat",
},
],
},
},
bat: {
schemaId: "bat",
parent: {
id: "bar",
dataLocation: "children",
},
data: "Hello world",
},
},
"foo",
],
mapper: reactMapper(componentDictionary),
resolver: reactResolver,
schemaDictionary: {
foo: {
id: "foo",
type: "object",
properties: {
children: {
...linkedDataSchema,
},
},
},
bar: {
id: "bar",
type: "object",
properties: {
children: {
...linkedDataSchema,
},
},
},
bat: {
id: "bat",
type: DataType.string,
},
},
})
);
expect(mappedData.find("Foo")).toHaveLength(1);
expect(mappedData.find("Bar")).toHaveLength(1);
expect(mappedData.text()).toEqual("Hello world");
});
test("should map data with a plugin", () => {
const pluginId: string = "foobarbat";
function mapperPlugin(data: any): any {
return "Hello world, " + data;
}
const resolvedData: any = mapDataDictionary({
dataDictionary: [
{
foo: {
schemaId: "foo",
data: {
text: "!",
number: 42,
},
},
},
"foo",
],
mapper: reactMapper(componentDictionary),
resolver: reactResolver,
schemaDictionary: {
foo: {
id: "foo",
type: "object",
properties: {
text: {
[pluginIdKeyword]: pluginId,
type: "string",
},
number: {
type: "number",
},
},
},
},
plugins: [
{
ids: [pluginId],
mapper: mapperPlugin,
resolver: undefined,
},
],
});
const mappedData: any = mount(resolvedData);
const mappedComponent: any = mappedData.find("Foo");
expect(mappedComponent).toHaveLength(1);
expect(mappedComponent.prop("text")).toEqual("Hello world, !");
expect(mappedComponent.prop("number")).toEqual(42);
});
test("should resolve data with a plugin", () => {
const pluginId: string = "foobarbat";
function resolverPlugin(data: any): any {
return "Hello world";
}
const resolvedData: any = mapDataDictionary({
dataDictionary: [
{
foo: {
schemaId: "foo",
data: {
children: [
{
id: "bar",
dataLocation: "children",
},
],
},
},
bar: {
schemaId: "foo",
parent: {
id: "foo",
dataLocation: "children",
},
data: {},
},
},
"foo",
],
mapper: reactMapper(componentDictionary),
resolver: reactResolver,
schemaDictionary: {
foo: {
id: "foo",
type: "object",
properties: {
children: {
...linkedDataSchema,
[pluginIdKeyword]: pluginId,
},
},
},
},
plugins: [
{
ids: [pluginId],
mapper: undefined,
resolver: resolverPlugin,
},
],
});
const mappedData: any = mount(resolvedData);
const mappedComponent: any = mappedData.find("Foo");
expect(mappedComponent).toHaveLength(1);
expect(mappedComponent.prop("children")).toEqual(["Hello world"]);
});
});

Просмотреть файл

@ -0,0 +1,166 @@
import React, { ComponentClass, FunctionComponent } from "react";
import {
dictionaryLink,
MapperConfig,
pluginIdKeyword,
PropertyKeyword,
ResolverConfig,
} from "@microsoft/fast-tooling";
export interface ReactMapDataDictionaryPlugin {
/**
* The ids that map to the pluginIdKeyword in the JSON schema
*/
ids: string[];
/**
* The mapping function that returns the mapped values
*/
mapper: (data: any) => any;
/**
* The resolving function that returns the mapped values
*/
resolver: (data: any) => any;
}
export interface ComponentDictionary {
[key: string]: FunctionComponent<any> | ComponentClass<any> | string;
}
function getPluginResolver(
plugins: ReactMapDataDictionaryPlugin[],
id?: string
): ReactMapDataDictionaryPlugin | false {
if (typeof id !== "string") {
return false;
}
return plugins.find((plugin: ReactMapDataDictionaryPlugin) => {
return plugin.ids.includes(id);
});
}
function resolvePropertyWithPlugin(
plugin: ReactMapDataDictionaryPlugin,
value: any
): any {
return plugin.resolver(value);
}
function mapPropertyWithPlugin(plugin: ReactMapDataDictionaryPlugin, value: any): any {
return plugin.mapper(value);
}
/**
* A mapping function intended to be used with the
* `mapDataDictionary` export from the @microsoft/fast-tooling library
*/
export function reactMapper(
componentDictionary: ComponentDictionary
): (config: MapperConfig<JSX.Element>) => void {
return (config: MapperConfig<JSX.Element>): void => {
if (typeof config.dataDictionary[0][config.dictionaryId].data === "string") {
return;
}
const allAvailableProps = Object.keys(config.schema[PropertyKeyword.properties]);
config.dataDictionary[0][config.dictionaryId].data = {
component: componentDictionary[config.schema.id],
props: allAvailableProps
.filter(potentialProp => {
// remove slots from the attributes list
return !allAvailableProps
.filter((propName: string) => {
if (
config.schema[PropertyKeyword.properties][propName][
dictionaryLink
]
) {
return propName;
}
})
.includes(potentialProp);
})
.reduce((previousValue: {}, currentValue: string) => {
const plugin = getPluginResolver(
config.mapperPlugins,
config.schema[PropertyKeyword.properties][currentValue][
pluginIdKeyword
]
);
return {
...previousValue,
[currentValue]: plugin
? mapPropertyWithPlugin(
plugin,
config.dataDictionary[0][config.dictionaryId].data[
currentValue
]
)
: config.dataDictionary[0][config.dictionaryId].data[
currentValue
],
};
}, {}),
};
};
}
/**
* A resolver function intended to be used with the
* `mapDataDictionary` export from the @microsoft/fast-tooling library
*/
export function reactResolver(config: ResolverConfig<unknown>): any {
if (config.dataDictionary[1] !== config.dictionaryId) {
// the original data in the children location
const childrenAtLocation =
config.dataDictionary[0][
config.dataDictionary[0][config.dictionaryId].parent.id
].data.props[
config.dataDictionary[0][config.dictionaryId].parent.dataLocation
];
const childrenProps: any = {
...config.dataDictionary[0][config.dictionaryId].data.props,
key: Array.isArray(childrenAtLocation) ? childrenAtLocation.length : 0,
};
const pluginId: string =
config.schemaDictionary[
config.dataDictionary[0][
config.dataDictionary[0][config.dictionaryId].parent.id
].schemaId
][PropertyKeyword.properties][
config.dataDictionary[0][config.dictionaryId].parent.dataLocation
][pluginIdKeyword];
const plugin = getPluginResolver(config.resolverPlugins, pluginId);
// the child item being resolved to a react component
const newChildrenAtLocation = plugin
? resolvePropertyWithPlugin(plugin, childrenProps)
: typeof config.dataDictionary[0][config.dictionaryId].data === "string"
? config.dataDictionary[0][config.dictionaryId].data
: React.createElement(
config.dataDictionary[0][config.dictionaryId].data.component,
childrenProps
);
// re-assign this prop with the new child item
config.dataDictionary[0][
config.dataDictionary[0][config.dictionaryId].parent.id
].data.props[config.dataDictionary[0][config.dictionaryId].parent.dataLocation] =
childrenAtLocation === undefined
? [newChildrenAtLocation]
: [newChildrenAtLocation, ...childrenAtLocation];
}
if (typeof config.dataDictionary[0][config.dictionaryId].data === "string") {
return config.dataDictionary[0][config.dictionaryId].data;
}
return React.createElement(
config.dataDictionary[0][config.dictionaryId].data.component,
config.dataDictionary[0][config.dictionaryId].data.props
);
}

Просмотреть файл

@ -0,0 +1,5 @@
import { ArrayControlConfig, DragState } from "../templates";
export type ArrayControlProps = ArrayControlConfig;
export type ArrayControlState = DragState;

Просмотреть файл

@ -0,0 +1,429 @@
import React from "react";
import Adapter from "enzyme-adapter-react-16";
import "../../__tests__/mocks/match-media";
import { configure, mount } from "enzyme";
import ArrayControlStyled, { ArrayControl } from "./control.array";
import { ArrayControlProps } from "./control.array.props";
import { ArrayControlClassNameContract } from "./control.array.style";
import HTML5Backend from "react-dnd-html5-backend";
import { DndProvider } from "react-dnd";
import { ControlType } from "../templates";
import { DataType, ValidationError } from "@microsoft/fast-tooling";
import defaultStrings from "../form.strings";
const TestArrayControl: React.FC<any> = (
props: React.PropsWithChildren<any>
): React.ReactElement => {
return (
<DndProvider backend={HTML5Backend}>
<ArrayControl {...props} />
</DndProvider>
);
};
/*
* Configure Enzyme
*/
configure({ adapter: new Adapter() });
const arrayProps: ArrayControlProps = {
type: ControlType.array,
dataLocation: "",
navigationConfigId: "",
dictionaryId: "",
navigation: {
"": {
self: "",
parent: null,
relativeDataLocation: "",
schemaLocation: "",
schema: {},
disabled: false,
data: void 0,
text: "foo",
type: DataType.array,
items: [],
},
},
schemaLocation: "",
value: "",
schema: {},
minItems: 0,
maxItems: Infinity,
onChange: jest.fn(),
onAddExampleData: jest.fn(),
onUpdateSection: jest.fn(),
reportValidity: jest.fn(),
disabled: false,
elementRef: null,
updateValidity: jest.fn(),
validationErrors: [],
required: false,
messageSystem: void 0,
strings: defaultStrings,
messageSystemOptions: null,
};
const managedClasses: ArrayControlClassNameContract = {
arrayControl: "arrayControl-class",
arrayControl__disabled: "arrayControl__disabled-class",
arrayControl__invalid: "arrayControl__disabled-class",
arrayControl_invalidMessage: "arrayControl_invalidMessage-class",
arrayControl_existingItemListItem__invalid:
"arrayControl_existingItemListItem__invalid-class",
arrayControl_addItem: "arrayControl_addItem-class",
arrayControl_addItemLabel: "arrayControl_addItemLabel-class",
arrayControl_addItemButton: "arrayControl_addItemButton-class",
arrayControl_existingItemList: "arrayControl_existingItemList-class",
arrayControl_existingItemListItem: "arrayControl_existingItemListItem-class",
arrayControl_existingItemListItemLink: "arrayControl_existingItemListItemLink-class",
arrayControl_existingItemListItemLink__default:
"arrayControl_existingItemListItemLink__default-class",
arrayControl_existingItemRemoveButton: "arrayControl_existingItemRemoveButton-class",
};
describe("ArrayControl", () => {
test("should not throw", () => {
expect(() => {
mount(<ArrayControlStyled {...arrayProps} />);
}).not.toThrow();
});
test("should generate a button to add an array item if the maximum number of items has not been reached", () => {
const rendered: any = mount(
<TestArrayControl
{...arrayProps}
maxItems={2}
value={["foo"]}
managedClasses={managedClasses}
/>
);
expect(
rendered.find(`.${managedClasses.arrayControl_addItemButton}`).length
).toEqual(1);
});
test("should generate a button to add an array item if no maximum number of items has been specified", () => {
const rendered: any = mount(
<TestArrayControl
{...arrayProps}
value={["foo"]}
managedClasses={managedClasses}
/>
);
expect(
rendered.find(`.${managedClasses.arrayControl_addItemButton}`).length
).toEqual(1);
});
test("should not generate a button to add an array item if the maximum number of items has been reached", () => {
const rendered: any = mount(
<TestArrayControl
{...arrayProps}
maxItems={2}
value={["foo", "bar"]}
managedClasses={managedClasses}
/>
);
expect(
rendered.find(`.${managedClasses.arrayControl_addItemButton}`).length
).toEqual(0);
});
test("should add an item to the array if the add button has been clicked", () => {
const callback: any = jest.fn();
const rendered: any = mount(
<TestArrayControl
{...arrayProps}
value={["foo"]}
managedClasses={managedClasses}
onChange={callback}
/>
);
const addButton: any = rendered.find(
`.${managedClasses.arrayControl_addItemButton}`
);
addButton.simulate("click");
expect(callback).toHaveBeenCalledTimes(1);
expect(callback.mock.calls[0][0]).toEqual({ value: void 0, isArray: true });
});
test("should show a remove button on an existing array item if the minimum number of items has not been reached", () => {
const callback: any = jest.fn();
const rendered: any = mount(
<TestArrayControl
{...arrayProps}
value={["foo", "bar", "bat"]}
managedClasses={managedClasses}
onChange={callback}
/>
);
expect(
rendered.find(`.${managedClasses.arrayControl_existingItemRemoveButton}`)
.length
).toBe(3);
});
test("should show a remove button on an existing array item if the minimum number of items has not been specified", () => {
const callback: any = jest.fn();
const rendered: any = mount(
<TestArrayControl
{...arrayProps}
value={["foo", "bar"]}
managedClasses={managedClasses}
onChange={callback}
/>
);
expect(
rendered.find(`.${managedClasses.arrayControl_existingItemRemoveButton}`)
.length
).toBe(2);
});
test("should not show a remove button on existing array items if the minimum number of items has been reached", () => {
const callback: any = jest.fn();
const rendered: any = mount(
<TestArrayControl
{...arrayProps}
value={["foo", "bar"]}
minItems={2}
managedClasses={managedClasses}
onChange={callback}
/>
);
expect(
rendered.find(`.${managedClasses.arrayControl_existingItemRemoveButton}`)
.length
).toBe(0);
});
test("should remove an array item if the remove button has been clicked", () => {
const callback: any = jest.fn();
const rendered: any = mount(
<TestArrayControl
{...arrayProps}
value={["foo", "bar", "bat"]}
managedClasses={managedClasses}
onChange={callback}
/>
);
const removeButton: any = rendered
.find(`.${managedClasses.arrayControl_existingItemRemoveButton}`)
.at(1);
removeButton.simulate("click");
expect(callback).toHaveBeenCalledTimes(1);
expect(callback.mock.calls[0][0]).toEqual({
value: undefined,
isArray: true,
index: 1,
});
});
test("should not show default values if data exists", () => {
const arrayItem1: string = "foo";
const arrayItem2: string = "bar";
const defaultArrayItem1: string = "hello";
const defaultArrayItem2: string = "world";
const rendered: any = mount(
<TestArrayControl
{...arrayProps}
value={[arrayItem1, arrayItem2]}
default={[defaultArrayItem1, defaultArrayItem2]}
managedClasses={managedClasses}
/>
);
expect(rendered.html().includes(arrayItem1)).toBe(true);
expect(rendered.html().includes(arrayItem2)).toBe(true);
expect(rendered.html().includes(defaultArrayItem1)).toBe(false);
expect(rendered.html().includes(defaultArrayItem2)).toBe(false);
});
test("should fire an `onSectionUpdate` callback with a link is clicked", () => {
const handleSectionUpdate: any = jest.fn();
const arrayItem1: string = "foo";
const arrayItem2: string = "bar";
const rendered: any = mount(
<TestArrayControl
{...arrayProps}
value={[arrayItem1, arrayItem2]}
managedClasses={managedClasses}
onUpdateSection={handleSectionUpdate}
navigationConfigId={""}
navigation={{
"": {
self: "",
parent: null,
relativeDataLocation: "",
schemaLocation: "",
schema: {},
disabled: false,
data: void 0,
text: "foo",
type: DataType.array,
items: ["[0]", "[1]"],
},
"[0]": {
self: "[0]",
parent: "",
relativeDataLocation: "[0]",
schemaLocation: "items",
schema: {},
disabled: false,
data: void 0,
text: "foo",
type: DataType.string,
items: [],
},
"[1]": {
self: "[1]",
parent: "",
relativeDataLocation: "[1]",
schemaLocation: "items",
schema: {},
disabled: false,
data: void 0,
text: "bar",
type: DataType.string,
items: [],
},
}}
/>
);
rendered.find("a").at(0).simulate("click");
expect(handleSectionUpdate).toHaveBeenCalled();
expect(handleSectionUpdate.mock.calls[0][1]).toEqual("[0]");
const handleSectionUpdateWithTestLocations: any = jest.fn();
const renderedWithTestLocations: any = mount(
<TestArrayControl
{...arrayProps}
value={[arrayItem1, arrayItem2]}
managedClasses={managedClasses}
onUpdateSection={handleSectionUpdateWithTestLocations}
schemaLocation={"properties.test"}
dataLocation={"test"}
navigationConfigId={"test"}
navigation={{
"": {
self: "",
parent: null,
relativeDataLocation: "",
schemaLocation: "",
schema: {},
disabled: false,
data: void 0,
text: "foo",
type: DataType.object,
items: ["test"],
},
test: {
self: "test",
parent: "",
relativeDataLocation: "test",
schemaLocation: "properties.test",
schema: {},
disabled: false,
data: void 0,
text: "foo",
type: DataType.array,
items: ["test[0]", "test[1]"],
},
"test[0]": {
self: "test[0]",
parent: "test",
relativeDataLocation: "test[0]",
schemaLocation: "items",
schema: {},
disabled: false,
data: void 0,
text: "foo",
type: DataType.string,
items: [],
},
"test[1]": {
self: "test[1]",
parent: "test",
relativeDataLocation: "test[1]",
schemaLocation: "items",
schema: {},
disabled: false,
data: void 0,
text: "bar",
type: DataType.string,
items: [],
},
}}
/>
);
renderedWithTestLocations.find("a").at(0).simulate("click");
expect(handleSectionUpdateWithTestLocations).toHaveBeenCalled();
expect(handleSectionUpdateWithTestLocations.mock.calls[0][1]).toEqual("test[0]");
});
test("should add a disabled class if the disabled prop has been passed", () => {
const rendered: any = mount(
<TestArrayControl
{...arrayProps}
disabled={true}
managedClasses={managedClasses}
/>
);
expect(rendered.find(`.${managedClasses.arrayControl__disabled}`)).toBeTruthy();
});
test("should add an invalid class if the invalidMessage prop is not an empty string", () => {
const rendered: any = mount(
<TestArrayControl
{...arrayProps}
invalidMessage={"foo"}
managedClasses={managedClasses}
/>
);
expect(rendered.find(`.${managedClasses.arrayControl__invalid}`)).toBeTruthy();
});
test("should add an invalid class on each array item that is invalid", () => {
const rendered: any = mount(
<TestArrayControl
{...arrayProps}
invalidMessage={"foo"}
validationErrors={[
{ dataLocation: "foo.0", invalidMessage: "bar" } as ValidationError,
]}
managedClasses={managedClasses}
dataLocation={"foo"}
value={["foo", true]}
/>
);
expect(
rendered.find(`.${managedClasses.arrayControl_existingItemListItem__invalid}`)
).toHaveLength(1);
});
test("should show an invalid message on each invalid array item if the displayInlineValidation prop is passed", () => {
const invalidItemMessage: string = "foobarbat";
const rendered: any = mount(
<TestArrayControl
{...arrayProps}
invalidMessage={"foo"}
validationErrors={[
{
dataLocation: "foo.0",
invalidMessage: invalidItemMessage,
} as ValidationError,
]}
displayValidationInline={true}
managedClasses={managedClasses}
dataLocation={"foo"}
value={["foo", true]}
/>
);
const invalidItem: any = rendered.find(
`.${managedClasses.arrayControl_invalidMessage}`
);
expect(invalidItem.text()).toEqual(invalidItemMessage);
});
});

Просмотреть файл

@ -0,0 +1,119 @@
import { ellipsis } from "@microsoft/fast-jss-utilities";
import { ComponentStyles } from "@microsoft/fast-jss-manager-react";
import {
addItemStyle,
borderRadiusCSSProperty,
cleanListStyle,
defaultFontStyle,
errorColorCSSProperty,
gutterCSSProperty,
invalidMessageStyle,
L1CSSProperty,
labelRegionStyle,
labelStyle,
removeItemStyle,
} from "../../style";
/**
* Array class name contract
*/
export interface ArrayControlClassNameContract {
arrayControl?: string;
arrayControl__disabled?: string;
arrayControl__invalid?: string;
arrayControl_invalidMessage?: string;
arrayControl_addItem?: string;
arrayControl_addItemButton?: string;
arrayControl_addItemLabel?: string;
arrayControl_existingItemList?: string;
arrayControl_existingItemListItem?: string;
arrayControl_existingItemListItem__invalid?: string;
arrayControl_existingItemListItemLink?: string;
arrayControl_existingItemListItemLink__default?: string;
arrayControl_existingItemRemoveButton?: string;
}
const styles: ComponentStyles<ArrayControlClassNameContract, {}> = {
arrayControl: {},
arrayControl__disabled: {},
arrayControl__invalid: {
"& $arrayControl_existingItemList": {
"&::before": {
"border-color": errorColorCSSProperty,
},
},
},
arrayControl_invalidMessage: {
...invalidMessageStyle,
"padding-bottom": "5px",
"margin-top": "-5px",
},
arrayControl_addItem: {
...labelRegionStyle,
position: "relative",
},
arrayControl_addItemLabel: {
...labelStyle,
"max-width": "calc(100% - 30px)",
},
arrayControl_addItemButton: {
...addItemStyle,
},
arrayControl_existingItemList: {
...cleanListStyle,
position: "relative",
"&::before": {
content: "''",
display: "block",
width: "100%",
"border-bottom": "1px solid transparent",
position: "absolute",
top: "0",
bottom: "0",
},
},
arrayControl_existingItemListItem: {
position: "relative",
cursor: "pointer",
height: "30px",
"line-height": "30px",
paddingBottom: "5px",
"border-radius": borderRadiusCSSProperty,
"&::before": {
content: "''",
position: "absolute",
"border-radius": borderRadiusCSSProperty,
"pointer-events": "none",
height: "inherit",
width: `calc(100% - calc(${gutterCSSProperty} + 6px))`,
border: "1px solid transparent",
},
},
arrayControl_existingItemListItem__invalid: {
"&::before": {
"border-color": errorColorCSSProperty,
"box-sizing": "border-box",
},
},
arrayControl_existingItemListItemLink: {
...ellipsis(),
cursor: "pointer",
display: "block",
height: "30px",
"line-height": "30px",
width: "calc(100% - 40px)",
padding: "0 5px",
"background-color": L1CSSProperty,
"&$arrayControl_existingItemListItemLink__default": {
...defaultFontStyle,
cursor: "auto",
},
},
arrayControl_existingItemListItemLink__default: {},
arrayControl_existingItemRemoveButton: {
...removeItemStyle,
cursor: "pointer",
},
};
export default styles;

Просмотреть файл

@ -0,0 +1,326 @@
import { uniqueId } from "lodash-es";
import { ManagedClasses } from "@microsoft/fast-components-class-name-contracts-base";
import manageJss, { ManagedJSSProps } from "@microsoft/fast-jss-manager-react";
import React from "react";
import { getArrayLinks, isRootLocation } from "./utilities/form";
import styles, { ArrayControlClassNameContract } from "./control.array.style";
import { ArrayControlProps, ArrayControlState } from "./control.array.props";
import { DragItem } from "../templates";
import { ArrayAction } from "../templates/types";
import { classNames } from "@microsoft/fast-web-utilities";
/**
* Form control definition
*/
class ArrayControl extends React.Component<
ArrayControlProps & ManagedClasses<ArrayControlClassNameContract>,
ArrayControlState
> {
public static displayName: string = "ArrayControl";
public static defaultProps: Partial<
ArrayControlProps & ManagedClasses<ArrayControlClassNameContract>
> = {
managedClasses: {},
};
constructor(props: ArrayControlProps) {
super(props);
this.state = {
data: props.value,
isDragging: false,
};
}
public render(): React.ReactNode {
return (
<div
className={classNames(
this.props.managedClasses.arrayControl,
[
this.props.managedClasses.arrayControl__disabled,
this.props.disabled,
],
[
this.props.managedClasses.arrayControl__invalid,
this.props.invalidMessage !== "",
]
)}
>
{this.renderAddArrayItem()}
{this.renderExistingArrayItems()}
</div>
);
}
/**
* Render a button for adding an item to the array
*/
private renderAddArrayItemTrigger(): React.ReactNode {
return (
<button
className={this.props.managedClasses.arrayControl_addItemButton}
aria-label={this.props.strings.arrayAddItemTip}
onClick={this.arrayItemClickHandlerFactory(ArrayAction.add)}
/>
);
}
/**
* Render an add array item section
*/
private renderAddArrayItem(): React.ReactNode {
const existingItemLength: number = Array.isArray(this.props.value)
? this.props.value.length
: 0;
const {
arrayControl_addItem,
arrayControl_addItemLabel,
}: ArrayControlClassNameContract = this.props.managedClasses;
if (this.props.maxItems > existingItemLength) {
return (
<div className={arrayControl_addItem}>
<div className={arrayControl_addItemLabel}>
{this.props.strings.arrayAddItemLabel}
</div>
{this.renderAddArrayItemTrigger()}
</div>
);
}
}
/**
* Renders an default array link item
*/
private renderDefaultArrayLinkItem = (value: any, index: number): React.ReactNode => {
const {
arrayControl_existingItemListItem,
arrayControl_existingItemListItemLink,
arrayControl_existingItemListItemLink__default,
}: ArrayControlClassNameContract = this.props.managedClasses;
return (
<li
className={arrayControl_existingItemListItem}
key={`item-${index}`}
id={uniqueId(index.toString())}
>
<span
className={classNames(
arrayControl_existingItemListItemLink,
arrayControl_existingItemListItemLink__default
)}
>
{value}
</span>
</li>
);
};
/**
* Renders default array items
*/
private renderDefaultArrayLinkItems(): React.ReactNode {
return getArrayLinks(this.props.default).map(
(value: any, index: number): React.ReactNode => {
return this.renderDefaultArrayLinkItem(value, index);
}
);
}
/**
* Renders the links to an array section to be activated
*/
private renderExistingArrayItems(): React.ReactNode {
const hasData: boolean = Array.isArray(this.props.value);
const hasDefault: boolean = Array.isArray(this.props.default);
if (hasData) {
return (
<ul className={this.props.managedClasses.arrayControl_existingItemList}>
{this.renderArrayLinkItems()}
</ul>
);
}
if (hasDefault) {
return (
<ul className={this.props.managedClasses.arrayControl_existingItemList}>
{this.renderDefaultArrayLinkItems()}
</ul>
);
}
return (
<ul className={this.props.managedClasses.arrayControl_existingItemList}></ul>
);
}
/**
* Render UI for all items in an array
*/
private renderArrayLinkItems(): React.ReactNode {
const data: unknown[] = this.state.isDragging
? this.state.data
: this.props.value;
const {
arrayControl_existingItemRemoveButton,
arrayControl_existingItemListItem,
arrayControl_existingItemListItem__invalid,
arrayControl_existingItemListItemLink,
}: Partial<ArrayControlClassNameContract> = this.props.managedClasses;
return getArrayLinks(data).map(
(text: string, index: number): React.ReactNode => {
const invalidError: React.ReactNode = this.renderValidationError(index);
return (
<React.Fragment key={this.props.dataLocation + index}>
<DragItem
key={index}
index={index}
minItems={this.props.minItems}
itemLength={getArrayLinks(data).length}
itemRemoveClassName={arrayControl_existingItemRemoveButton}
itemClassName={classNames(arrayControl_existingItemListItem, [
arrayControl_existingItemListItem__invalid,
invalidError !== null,
])}
itemLinkClassName={arrayControl_existingItemListItemLink}
removeDragItem={this.arrayItemClickHandlerFactory}
onClick={this.arrayClickHandlerFactory}
moveDragItem={this.handleMoveDragItem}
dropDragItem={this.handleDropDragItem}
dragStart={this.handleDragStart}
dragEnd={this.handleDragEnd}
strings={this.props.strings}
>
{text}
</DragItem>
{!!this.props.displayValidationInline ? invalidError : null}
</React.Fragment>
);
}
);
}
private renderValidationError(index: number): React.ReactNode {
if (typeof this.props.validationErrors === "undefined") {
return null;
}
for (const error of this.props.validationErrors) {
if (error.dataLocation.startsWith(`${this.props.dataLocation}.${index}`)) {
return (
<div
className={this.props.managedClasses.arrayControl_invalidMessage}
>
{error.invalidMessage}
</div>
);
}
}
return null;
}
/**
* Array add/remove item click handler factory
*/
private arrayItemClickHandlerFactory = (
type: ArrayAction,
index?: number
): ((e: React.MouseEvent<HTMLButtonElement>) => void) => {
return (e: React.MouseEvent<HTMLButtonElement>): void => {
e.preventDefault();
type === ArrayAction.add
? this.handleAddArrayItem()
: this.handleRemoveArrayItem(index);
};
};
/**
* Array section link click handler factory
*/
private arrayClickHandlerFactory = (
index: number
): ((e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void) => {
return (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>): void => {
e.preventDefault();
this.props.onUpdateSection(
this.props.dictionaryId,
this.props.navigation[this.props.navigationConfigId].items[index]
);
};
};
/**
* Handles adding an array item
*/
private handleAddArrayItem(): void {
if (typeof this.props.value === "undefined") {
this.props.onChange({ value: [this.props.onAddExampleData("items")] });
} else {
this.props.onChange({
value: this.props.onAddExampleData("items"),
isArray: true,
});
}
}
/**
* Handles removing an array item
*/
private handleRemoveArrayItem(index: number): void {
this.props.onChange({ value: void 0, isArray: true, index });
}
/**
* Handle the start of the drag action
*/
private handleDragStart = (): void => {
this.setState({
isDragging: true,
data: [].concat(this.props.value || []),
});
};
/**
* Handle the end of the drag action
*/
private handleDragEnd = (): void => {
this.setState({
isDragging: false,
});
};
/**
* Handle moving the drag item
*/
private handleMoveDragItem = (sourceIndex: number, targetIndex: number): void => {
const currentData: unknown[] = [].concat(this.props.value);
if (sourceIndex !== targetIndex) {
currentData.splice(targetIndex, 0, currentData.splice(sourceIndex, 1)[0]);
}
this.setState({
data: currentData,
});
};
/**
* Handle dropping the drag item
* Triggers the onChange
*/
private handleDropDragItem = (): void => {
this.props.onChange({ value: this.state.data });
};
}
export { ArrayControl };
export default manageJss(styles)(ArrayControl);

Просмотреть файл

@ -0,0 +1,3 @@
import { CommonControlConfig } from "../templates";
export type ButtonControlProps = CommonControlConfig;

Просмотреть файл

@ -0,0 +1,116 @@
import React from "react";
import Adapter from "enzyme-adapter-react-16";
import "../../__tests__/mocks/match-media";
import { configure, mount, shallow } from "enzyme";
import { ButtonControl } from "./control.button";
import { ButtonControlProps } from "./control.button.props";
import { ButtonControlClassNameContract } from "./control.button.style";
import { ControlType } from "../templates";
import defaultStrings from "../form.strings";
/*
* Configure Enzyme
*/
configure({ adapter: new Adapter() });
const managedClasses: ButtonControlClassNameContract = {
buttonControl: "buttonControl-class",
buttonControl__disabled: "buttonControl__disabled-class",
buttonControl__default: "buttonControl__default-class",
};
const buttonProps: ButtonControlProps = {
type: ControlType.button,
dataLocation: "",
navigationConfigId: "",
dictionaryId: "",
navigation: {},
onChange: jest.fn(),
value: "",
schema: {},
disabled: false,
reportValidity: jest.fn(),
updateValidity: jest.fn(),
elementRef: null,
validationErrors: [],
required: false,
messageSystem: void 0,
strings: defaultStrings,
messageSystemOptions: null,
};
describe("ButtonControl", () => {
test("should not throw", () => {
expect(() => {
shallow(<ButtonControl {...buttonProps} managedClasses={managedClasses} />);
}).not.toThrow();
});
test("should generate an HTML button element", () => {
const rendered: any = mount(
<ButtonControl {...buttonProps} managedClasses={managedClasses} />
);
expect(rendered.find("button")).toHaveLength(1);
});
test("should be disabled when disabled props is passed", () => {
const rendered: any = mount(
<ButtonControl
{...buttonProps}
disabled={true}
managedClasses={managedClasses}
/>
);
expect(rendered.find(`.${managedClasses.buttonControl__disabled}`)).toHaveLength(
1
);
expect(rendered.find("button").prop("disabled")).toBeTruthy();
expect(rendered.find("input").prop("disabled")).toBeTruthy();
});
test("should have the default class when default prop is passed", () => {
const rendered: any = mount(
<ButtonControl
{...buttonProps}
value={undefined}
default={null}
managedClasses={managedClasses}
/>
);
expect(rendered.find(`.${managedClasses.buttonControl__default}`)).toHaveLength(
1
);
});
test("should fire the onChange event when the button is clicked", () => {
const callback: any = jest.fn();
const rendered: any = mount(
<ButtonControl
{...buttonProps}
managedClasses={managedClasses}
value={undefined}
onChange={callback}
/>
);
expect(callback).toHaveBeenCalledTimes(0);
rendered.find(`.${managedClasses.buttonControl}`).simulate("click");
expect(callback).toHaveBeenCalledTimes(1);
expect(callback.mock.calls[0][0]).toEqual({ value: null });
});
test("should NOT fire an onChange when the input value is updated", () => {
const callback: any = jest.fn();
const rendered: any = mount(
<ButtonControl
{...buttonProps}
onChange={callback}
managedClasses={managedClasses}
/>
);
rendered.find("input").simulate("change", { target: { value: "foo" } });
expect(callback).toHaveBeenCalledTimes(0);
});
});

Просмотреть файл

@ -0,0 +1,26 @@
import { ComponentStyles } from "@microsoft/fast-jss-manager-react";
import { defaultFontStyle, inputStyle } from "../../style";
/**
* Button class name contract
*/
export interface ButtonControlClassNameContract {
buttonControl?: string;
buttonControl__disabled?: string;
buttonControl__default?: string;
}
const styles: ComponentStyles<ButtonControlClassNameContract, {}> = {
buttonControl: {
...inputStyle,
width: "100%",
textAlign: "start",
"&$buttonControl__default": {
...defaultFontStyle,
},
},
buttonControl__disabled: {},
buttonControl__default: {},
};
export default styles;

Просмотреть файл

@ -0,0 +1,81 @@
import React from "react";
import manageJss, { ManagedJSSProps } from "@microsoft/fast-jss-manager-react";
import { ManagedClasses } from "@microsoft/fast-components-class-name-contracts-base";
import styles, { ButtonControlClassNameContract } from "./control.button.style";
import { ButtonControlProps } from "./control.button.props";
import { classNames } from "@microsoft/fast-web-utilities";
import { isDefault } from "./utilities/form";
/**
* Form control definition
* @extends React.Component
*/
class ButtonControl extends React.Component<
ButtonControlProps & ManagedClasses<ButtonControlClassNameContract>,
{}
> {
public static displayName: string = "ButtonControl";
public static defaultProps: Partial<
ButtonControlProps & ManagedClasses<ButtonControlClassNameContract>
> = {
managedClasses: {},
};
public render(): React.ReactNode {
return (
<React.Fragment>
<button
className={classNames(
this.props.managedClasses.buttonControl,
[
this.props.managedClasses.buttonControl__disabled,
this.props.disabled,
],
[
this.props.managedClasses.buttonControl__default,
isDefault(this.props.value, this.props.default),
]
)}
ref={this.props.elementRef as React.Ref<HTMLButtonElement>}
onBlur={this.props.updateValidity}
onFocus={this.props.reportValidity}
onClick={this.handleButtonClick()}
disabled={this.props.disabled}
>
Set to null
</button>
<input
id={this.props.dataLocation}
hidden={true}
value={this.props.value || ""}
onChange={this.handleInputChange}
disabled={this.props.disabled}
required={this.props.required}
/>
</React.Fragment>
);
}
private handleButtonClick = (): ((
e: React.MouseEvent<HTMLButtonElement>
) => void) => {
return (e: React.MouseEvent<HTMLButtonElement>): void => {
e.preventDefault();
this.props.onChange({ value: null });
};
};
/**
* Dummy onChange handler
*
* Form elements will not validate against read-only form items
* therefore a value and onChange handler must still be supplied
* even if there is no intention to update the value.
*/
private handleInputChange = (): void => {};
}
export { ButtonControl };
export default manageJss(styles)(ButtonControl);

Просмотреть файл

@ -0,0 +1,3 @@
import { CommonControlConfig } from "../templates";
export type CheckboxControlProps = CommonControlConfig;

Просмотреть файл

@ -0,0 +1,143 @@
import React from "react";
import Adapter from "enzyme-adapter-react-16";
import "../../__tests__/mocks/match-media";
import { configure, mount, shallow } from "enzyme";
import { CheckboxControl } from "./control.checkbox";
import { CheckboxControlProps } from "./control.checkbox.props";
import { CheckboxControlClassNameContract } from "./control.checkbox.style";
import { ControlType } from "../templates";
import defaultStrings from "../form.strings";
/*
* Configure Enzyme
*/
configure({ adapter: new Adapter() });
const managedClasses: CheckboxControlClassNameContract = {
checkboxControl: "checkboxControl-class",
checkboxControl__disabled: "checkboxControl__disabled-class",
checkboxControl__default: "checkboxControl__default-class",
};
const checkboxProps: CheckboxControlProps = {
type: ControlType.checkbox,
dataLocation: "",
navigationConfigId: "",
dictionaryId: "",
navigation: {},
onChange: jest.fn(),
value: false,
schema: {},
disabled: false,
elementRef: null,
reportValidity: jest.fn(),
updateValidity: jest.fn(),
validationErrors: [],
required: false,
messageSystem: void 0,
strings: defaultStrings,
messageSystemOptions: null,
};
describe("CheckboxControl", () => {
test("should not throw", () => {
expect(() => {
shallow(
<CheckboxControl {...checkboxProps} managedClasses={managedClasses} />
);
}).not.toThrow();
});
test("should generate an input HTML element", () => {
const rendered: any = mount(
<CheckboxControl {...checkboxProps} managedClasses={managedClasses} />
);
expect(rendered.find("input")).toHaveLength(1);
});
test("should have an `id` attribute on the HTML input element", () => {
const dataLocation: string = "foo";
const rendered: any = mount(
<CheckboxControl
{...checkboxProps}
dataLocation={dataLocation}
managedClasses={managedClasses}
/>
);
const input: any = rendered.find("input");
expect(dataLocation).toMatch(input.prop("id"));
});
test("should fire an `onChange` callback with the input is changed", () => {
const handleChange: any = jest.fn();
const rendered: any = mount(
<CheckboxControl
{...checkboxProps}
onChange={handleChange}
managedClasses={managedClasses}
/>
);
rendered
.find("input")
.at(0)
.simulate("change", { target: { checked: true } });
expect(handleChange).toHaveBeenCalled();
expect(handleChange.mock.calls[0][0]).toEqual({ value: true });
});
test("should be disabled when disabled props is passed", () => {
const rendered: any = mount(
<CheckboxControl
{...checkboxProps}
disabled={true}
managedClasses={managedClasses}
/>
);
const wrapper: any = rendered.find("div");
const input: any = rendered.find("input");
expect(input).toHaveLength(1);
expect(input.prop("disabled")).toBeTruthy();
expect(wrapper.find(`.${managedClasses.checkboxControl__disabled}`)).toHaveLength(
1
);
});
test("should have the default class when default prop is passed", () => {
const rendered: any = mount(
<CheckboxControl
{...checkboxProps}
value={undefined}
default={true}
managedClasses={managedClasses}
/>
);
expect(rendered.find(`.${managedClasses.checkboxControl__default}`)).toHaveLength(
1
);
});
test("should show default values if they exist and no data is available", () => {
const rendered: any = mount(
<CheckboxControl
{...checkboxProps}
managedClasses={managedClasses}
value={undefined}
default={true}
/>
);
expect(rendered.find("input").at(0).prop("value")).toBe(true.toString());
});
test("should not show default values if data exists", () => {
const rendered: any = mount(
<CheckboxControl
{...checkboxProps}
managedClasses={managedClasses}
value={false}
default={true}
/>
);
expect(rendered.find("input").at(0).prop("value")).toBe(false.toString());
});
});

Просмотреть файл

@ -0,0 +1,92 @@
import { ComponentStyles } from "@microsoft/fast-jss-manager-react";
import { applyFocusVisible } from "@microsoft/fast-jss-utilities";
import {
errorColorCSSProperty,
insetStrongBoxShadow,
L3FillColorProperty,
textColorCSSProperty,
} from "../../style";
/**
* Checkbox class name contract
*/
export interface CheckboxControlClassNameContract {
checkboxControl?: string;
checkboxControl__disabled?: string;
checkboxControl_input?: string;
checkboxControl_checkmark?: string;
checkboxControl__default?: string;
}
const styles: ComponentStyles<CheckboxControlClassNameContract, {}> = {
checkboxControl: {
position: "relative",
height: "14px",
width: "14px",
},
checkboxControl_input: {
position: "absolute",
appearance: "none",
minWidth: "14px",
height: "14px",
boxSizing: "border-box",
borderRadius: "2px",
border: "1px solid transparent",
zIndex: "1",
margin: "0",
"&:disabled": {
cursor: "not-allowed",
},
"&:hover": {
border: `1px solid ${textColorCSSProperty}`,
},
...applyFocusVisible({
outline: "none",
...insetStrongBoxShadow(textColorCSSProperty),
}),
"&:checked": {
"& + $checkboxControl_checkmark": {
"&::before": {
height: "3px",
left: "4px",
top: "7px",
transform: "rotate(-45deg)",
},
"&::after": {
height: "8px",
left: "8px",
top: "2px",
transform: "rotate(45deg)",
},
},
},
"&:invalid": {
borderColor: errorColorCSSProperty,
},
"&$checkboxControl__default": {
"& + span": {
"&::after, &::before": {
background: textColorCSSProperty,
},
},
},
},
checkboxControl_checkmark: {
position: "absolute",
left: "0",
width: "14px",
height: "14px",
background: L3FillColorProperty,
"&::after, &::before": {
position: "absolute",
display: "block",
content: "''",
width: "1px",
background: textColorCSSProperty,
},
},
checkboxControl__disabled: {},
checkboxControl__default: {},
};
export default styles;

Просмотреть файл

@ -0,0 +1,72 @@
import React from "react";
import manageJss, { ManagedJSSProps } from "@microsoft/fast-jss-manager-react";
import { ManagedClasses } from "@microsoft/fast-components-class-name-contracts-base";
import styles, { CheckboxControlClassNameContract } from "./control.checkbox.style";
import { CheckboxControlProps } from "./control.checkbox.props";
import { classNames } from "@microsoft/fast-web-utilities";
import { isDefault } from "./utilities/form";
/**
* Form control definition
* @extends React.Component
*/
class CheckboxControl extends React.Component<
CheckboxControlProps & ManagedClasses<CheckboxControlClassNameContract>,
{}
> {
public static displayName: string = "CheckboxControl";
public static defaultProps: Partial<
CheckboxControlProps & ManagedClasses<CheckboxControlClassNameContract>
> = {
managedClasses: {},
};
public render(): JSX.Element {
const value: boolean =
typeof this.props.value === "boolean"
? this.props.value
: typeof this.props.default === "boolean"
? this.props.default
: false;
return (
<div
className={classNames(
this.props.managedClasses.checkboxControl,
[
this.props.managedClasses.checkboxControl__disabled,
this.props.disabled,
],
[
this.props.managedClasses.checkboxControl__default,
isDefault(this.props.value, this.props.default),
]
)}
>
<input
className={this.props.managedClasses.checkboxControl_input}
id={this.props.dataLocation}
type={"checkbox"}
value={value.toString()}
onChange={this.handleChange()}
checked={value}
disabled={this.props.disabled}
ref={this.props.elementRef as React.Ref<HTMLInputElement>}
onFocus={this.props.reportValidity}
onBlur={this.props.updateValidity}
/>
<span className={this.props.managedClasses.checkboxControl_checkmark} />
</div>
);
}
private handleChange = (): ((e: React.ChangeEvent<HTMLInputElement>) => void) => {
return (e: React.ChangeEvent<HTMLInputElement>): void => {
this.props.onChange({ value: e.target.checked });
};
};
}
export { CheckboxControl };
export default manageJss(styles)(CheckboxControl);

Просмотреть файл

@ -0,0 +1,3 @@
import { CommonControlConfig } from "../templates";
export type DisplayControlProps = CommonControlConfig;

Просмотреть файл

@ -0,0 +1,142 @@
import React from "react";
import Adapter from "enzyme-adapter-react-16";
import "../../__tests__/mocks/match-media";
import { configure, mount, shallow } from "enzyme";
import { DisplayControl } from "./control.display";
import { DisplayControlProps } from "./control.display.props";
import { DisplayControlClassNameContract } from "./control.display.style";
import { ControlType } from "../templates";
import defaultStrings from "../form.strings";
/*
* Configure Enzyme
*/
configure({ adapter: new Adapter() });
const managedClasses: DisplayControlClassNameContract = {
displayControl: "displayControl",
displayControl__disabled: "displayControl__disabled",
displayControl__default: "displayControl__default",
};
const displayProps: DisplayControlProps = {
type: ControlType.display,
dataLocation: "",
navigationConfigId: "",
dictionaryId: "",
navigation: {},
value: "",
schema: {},
disabled: false,
reportValidity: jest.fn(),
updateValidity: jest.fn(),
onChange: jest.fn(),
elementRef: null,
validationErrors: [],
required: false,
messageSystem: void 0,
strings: defaultStrings,
messageSystemOptions: null,
};
describe("DisplayControl", () => {
test("should not throw", () => {
expect(() => {
shallow(<DisplayControl {...displayProps} managedClasses={managedClasses} />);
}).not.toThrow();
});
test("should generate an HTML input element", () => {
const rendered: any = mount(
<DisplayControl {...displayProps} managedClasses={managedClasses} />
);
expect(rendered.find("input")).toHaveLength(1);
});
test("should be disabled when disabled props is passed", () => {
const rendered: any = mount(
<DisplayControl
{...displayProps}
disabled={true}
managedClasses={managedClasses}
/>
);
expect(rendered.find(`.${managedClasses.displayControl__disabled}`)).toHaveLength(
1
);
expect(rendered.find("input").prop("disabled")).toBeTruthy();
});
test("should have the default class when default prop is passed", () => {
const rendered: any = mount(
<DisplayControl
{...displayProps}
value={undefined}
default={"foo"}
managedClasses={managedClasses}
/>
);
expect(rendered.find(`.${managedClasses.displayControl__default}`)).toHaveLength(
1
);
});
test("should show default values if they exist and no data is available", () => {
const defaultValue: string = "bar";
const rendered: any = mount(
<DisplayControl
{...displayProps}
managedClasses={managedClasses}
value={undefined}
default={defaultValue}
/>
);
expect(rendered.find(`.${managedClasses.displayControl}`).prop("value")).toBe(
defaultValue
);
});
test("should show non-string default values if they exist and no data is available", () => {
const defaultValue: number = 50;
const rendered: any = mount(
<DisplayControl
{...displayProps}
managedClasses={managedClasses}
value={undefined}
default={defaultValue}
/>
);
expect(rendered.find(`.${managedClasses.displayControl}`).prop("value")).toBe(
JSON.stringify(defaultValue, null, 2)
);
});
test("should not show default values if data exists", () => {
const value: string = "foo";
const defaultValue: string = "bar";
const rendered: any = mount(
<DisplayControl
{...displayProps}
managedClasses={managedClasses}
value={value}
default={defaultValue}
/>
);
expect(rendered.find(`.${managedClasses.displayControl}`).prop("value")).toBe(
value
);
});
test("should NOT fire an onChange when the input value is updated", () => {
const callback: any = jest.fn();
const rendered: any = mount(
<DisplayControl
{...displayProps}
onChange={callback}
managedClasses={managedClasses}
/>
);
rendered.find("input").simulate("change", { target: { value: "foo" } });
expect(callback).toHaveBeenCalledTimes(0);
});
});

Просмотреть файл

@ -0,0 +1,25 @@
import { ComponentStyles } from "@microsoft/fast-jss-manager-react";
import { defaultFontStyle, inputStyle } from "../../style";
/**
* Display class name contract
*/
export interface DisplayControlClassNameContract {
displayControl?: string;
displayControl__disabled?: string;
displayControl__default?: string;
}
const styles: ComponentStyles<DisplayControlClassNameContract, {}> = {
displayControl: {
...inputStyle,
width: "100%",
"&$displayControl__default": {
...defaultFontStyle,
},
},
displayControl__disabled: {},
displayControl__default: {},
};
export default styles;

Просмотреть файл

@ -0,0 +1,81 @@
import React from "react";
import manageJss, { ManagedJSSProps } from "@microsoft/fast-jss-manager-react";
import { ManagedClasses } from "@microsoft/fast-components-class-name-contracts-base";
import styles, { DisplayControlClassNameContract } from "./control.display.style";
import { DisplayControlProps } from "./control.display.props";
import { classNames } from "@microsoft/fast-web-utilities";
import { isDefault } from "./utilities/form";
/**
* Form control definition
*/
class DisplayControl extends React.Component<
DisplayControlProps & ManagedClasses<DisplayControlClassNameContract>,
{}
> {
public static displayName: string = "DisplayControl";
public static defaultProps: Partial<
DisplayControlProps & ManagedClasses<DisplayControlClassNameContract>
> = {
managedClasses: {},
};
public render(): React.ReactNode {
return (
<input
className={classNames(
this.props.managedClasses.displayControl,
[
this.props.managedClasses.displayControl__disabled,
this.props.disabled,
],
[
this.props.managedClasses.displayControl__default,
isDefault(this.props.value, this.props.default),
]
)}
type={"text"}
ref={this.props.elementRef as React.Ref<HTMLInputElement>}
onBlur={this.props.updateValidity}
onFocus={this.props.reportValidity}
value={this.getDisplayValue(this.props.value)}
onChange={this.handleInputChange}
disabled={this.props.disabled}
required={this.props.required}
/>
);
}
/**
* Dummy onChange handler
*
* Form elements will not validate against read-only form items
* therefore a value and onChange handler must still be supplied
* even if there is no intention to update the value.
*/
private handleInputChange = (): void => {};
private getDisplayValue(data: any): string {
const typeofData: string = typeof data;
const typeofDefault: string = typeof this.props.default;
if (typeofData === "undefined" && typeofDefault !== "undefined") {
if (typeofDefault === "string") {
return this.props.default;
}
return JSON.stringify(this.props.default, null, 2);
}
switch (typeofData) {
case "string":
return data;
default:
return JSON.stringify(data, null, 2);
}
}
}
export { DisplayControl };
export default manageJss(styles)(DisplayControl);

Просмотреть файл

@ -0,0 +1,23 @@
import { DragState, LinkedDataControlConfig } from "../templates";
export interface ChildComponentDataMapping {
[T: string]: any;
}
export type LinkedDataControlProps = LinkedDataControlConfig;
export enum Action {
add = "add",
edit = "edit",
delete = "delete",
}
/**
* State object for the LinkedDataControl component
*/
export interface LinkedDataControlState extends DragState {
/**
* The search term used to filter a list of linked data
*/
searchTerm: string;
}

Просмотреть файл

@ -0,0 +1,314 @@
import React from "react";
import Adapter from "enzyme-adapter-react-16";
import "../../__tests__/mocks/match-media";
import { configure, mount, ReactWrapper } from "enzyme";
import HTML5Backend from "react-dnd-html5-backend";
import { DndProvider } from "react-dnd";
import {
keyArrowDown,
keyArrowUp,
keyEnter,
keyTab,
} from "@microsoft/fast-web-utilities";
import { ControlType } from "../templates";
import { LinkedDataActionType } from "../templates/types";
import defaultStrings from "../form.strings";
import { LinkedDataControl } from "./control.linked-data";
import { LinkedDataControlProps } from "./control.linked-data.props";
import { LinkedDataControlClassNameContract } from "./control.linked-data.style";
const LinkedDataFormControlWithDragAndDrop: React.FC<any> = (
props: React.PropsWithChildren<any>
): React.ReactElement => {
return (
<DndProvider backend={HTML5Backend}>
<LinkedDataControl {...props} />
</DndProvider>
);
};
/*
* Configure Enzyme
*/
configure({ adapter: new Adapter() });
const managedClasses: LinkedDataControlClassNameContract = {
linkedDataControl: "linkedDataFormControl-class",
linkedDataControl_linkedDataListControl:
"linkedDataFormControl_linkedDataListControl-class",
linkedDataControl_linkedDataListInput:
"linkedDataFormControl_linkedDataListInput-class",
linkedDataControl_delete: "linkedDataFormControl_delete-class",
linkedDataControl_deleteButton: "linkedDataFormControl_deleteButton-class",
linkedDataControl_existingLinkedData:
"linkedDataFormControl_existingLinkedData-class",
linkedDataControl_existingLinkedDataItem:
"linkedDataFormControl_existingLinkedDataItem-class",
linkedDataControl_existingLinkedDataItemContent:
"linkedDataFormControl_existingLinkedDataItemContent-class",
linkedDataControl_existingLinkedDataItemLink:
"linkedDataFormControl_existingLinkedDataItemLink-class",
linkedDataControl_existingLinkedDataItemName:
"linkedDataFormControl_existingLinkedDataItemName-class",
};
const linkedDataProps: LinkedDataControlProps = {
type: ControlType.linkedData,
schemaDictionary: {
alpha: {
title: "alpha",
alias: "A",
id: "alpha",
type: "object",
properties: {},
},
beta: {
title: "beta",
id: "beta",
type: "object",
properties: {},
},
omega: {
title: "omega",
id: "omega",
type: "object",
properties: {},
},
},
required: false,
dataLocation: "locationOfLinkedData",
navigationConfigId: "",
dictionaryId: "",
dataDictionary: [
{
"": {
schemaId: "alpha",
data: {},
},
foo: {
schemaId: "beta",
data: {},
},
},
"",
],
navigation: {},
value: undefined,
schema: {},
onChange: jest.fn(),
onUpdateSection: jest.fn(),
reportValidity: jest.fn(),
updateValidity: jest.fn(),
disabled: false,
elementRef: null,
validationErrors: [],
messageSystem: void 0,
strings: defaultStrings,
messageSystemOptions: null,
};
describe("LinkedDataControl", () => {
test("should not throw", () => {
expect(() => {
mount(
<LinkedDataFormControlWithDragAndDrop
{...linkedDataProps}
managedClasses={managedClasses}
/>
);
}).not.toThrow();
});
test("should generate a text input", () => {
const rendered: any = mount(
<LinkedDataFormControlWithDragAndDrop
{...linkedDataProps}
managedClasses={managedClasses}
/>
);
expect(rendered.find("input")).toHaveLength(1);
});
test("should add a datalist", () => {
const rendered: any = mount(
<LinkedDataFormControlWithDragAndDrop
{...linkedDataProps}
managedClasses={managedClasses}
/>
);
expect(rendered.find("datalist")).toHaveLength(1);
});
test("should add an `aria-controls` on a text input with the same value as the id of the `listbox`", () => {
const rendered: any = mount(
<LinkedDataFormControlWithDragAndDrop
{...linkedDataProps}
managedClasses={managedClasses}
/>
);
const inputAriaControls: string = rendered.find("input").props()["aria-controls"];
const datalist: string = rendered.find("datalist").props()["id"];
expect(inputAriaControls).toEqual(datalist);
});
test("should add a `label` to an option if it's schema contains an `alias` property", () => {
const rendered: any = mount(
<LinkedDataFormControlWithDragAndDrop
{...linkedDataProps}
managedClasses={managedClasses}
/>
);
const listboxItems: any = rendered.find("datalist option");
expect(listboxItems.at(0).prop("label")).toEqual("A");
});
test("should generate options based on the schema items in the schema dictionary", () => {
const rendered: any = mount(
<LinkedDataFormControlWithDragAndDrop
{...linkedDataProps}
managedClasses={managedClasses}
/>
);
const listboxItems: any = rendered.find("datalist option");
expect(listboxItems).toHaveLength(3);
});
test("should show if linkedData are present in the data as a DragItem", () => {
const renderedWithOneChild: any = mount(
<LinkedDataFormControlWithDragAndDrop
{...linkedDataProps}
value={[{ id: "foo" }]}
managedClasses={managedClasses}
/>
);
expect(renderedWithOneChild.find("DragItem")).toHaveLength(1);
});
test("should not update the value to match when there are multiple matching values", () => {
const callback: any = jest.fn();
const rendered: any = mount(
<LinkedDataFormControlWithDragAndDrop
{...linkedDataProps}
onChange={callback}
managedClasses={managedClasses}
/>
);
const targetValue: any = { value: "a" };
const input: any = rendered.find("input");
input.simulate("change", { target: targetValue });
input.simulate("keydown", { key: keyTab });
expect(input.getDOMNode().value).toEqual("a");
expect(callback).not.toHaveBeenCalled();
});
test("should update the value to match when there is a single matching value", () => {
const callback: any = jest.fn();
const rendered: any = mount(
<LinkedDataFormControlWithDragAndDrop
{...linkedDataProps}
onChange={callback}
managedClasses={managedClasses}
/>
);
const targetValue: any = { value: "ome" };
const input: any = rendered.find("input");
input.simulate("change", { target: targetValue });
input.simulate("keydown", { key: keyTab });
expect(input.getDOMNode().value).toEqual("omega");
expect(callback).not.toHaveBeenCalled();
});
test("should fire a callback to update the data when the value is an exact match and enter has been pressed", () => {
const callback: any = jest.fn();
const rendered: any = mount(
<LinkedDataFormControlWithDragAndDrop
{...linkedDataProps}
onChange={callback}
managedClasses={managedClasses}
/>
);
const targetValue: any = { value: "omega" };
const input: any = rendered.find("input");
input.simulate("change", { target: targetValue });
input.simulate("keydown", { key: keyEnter });
expect(callback).toHaveBeenCalled();
expect(callback.mock.calls[0][0]).toEqual({
index: 0,
isLinkedData: true,
linkedDataAction: LinkedDataActionType.add,
value: [
{
data: {},
parent: {
dataLocation: "locationOfLinkedData",
id: "",
},
schemaId: "omega",
},
],
});
});
test("should update active section to the linked data item clicked", () => {
const callback: any = jest.fn();
const rendered: ReactWrapper = mount(
<LinkedDataFormControlWithDragAndDrop
{...linkedDataProps}
value={[{ id: "foo" }]}
onUpdateSection={callback}
managedClasses={managedClasses}
/>
);
rendered
.find(`li.${managedClasses.linkedDataControl_existingLinkedDataItem} > a`)
.simulate("click");
expect(callback).toHaveBeenCalled();
expect(callback.mock.calls[0][0]).toEqual("foo");
});
test("should not fire a callback to update the data when the value is changed but there is no match", () => {
const callback: any = jest.fn();
const rendered: any = mount(
<LinkedDataFormControlWithDragAndDrop
{...linkedDataProps}
onChange={callback}
managedClasses={managedClasses}
/>
);
const targetValue: any = { value: "no match" };
rendered
.find("input")
.simulate("change", { target: targetValue, currentTarget: targetValue });
rendered.find("input").simulate("keydown", { key: keyEnter });
expect(callback).not.toHaveBeenCalled();
});
test("should remove a child option from the data when the remove button has been clicked", () => {
const callback: any = jest.fn();
const renderedWithTwoLinkedData: any = mount(
<LinkedDataFormControlWithDragAndDrop
{...linkedDataProps}
managedClasses={managedClasses}
value={[
{
id: "foo",
},
]}
onChange={callback}
/>
);
renderedWithTwoLinkedData.find("DragItem").find("button").simulate("click");
expect(callback).toHaveBeenCalled();
expect(callback.mock.calls[0][0]).toEqual({
isLinkedData: true,
linkedDataAction: LinkedDataActionType.remove,
value: [
{
id: "foo",
},
],
});
});
});

Просмотреть файл

@ -0,0 +1,86 @@
import { ellipsis } from "@microsoft/fast-jss-utilities";
import { ComponentStyles } from "@microsoft/fast-jss-manager-react";
import {
cleanListStyle,
removeItemStyle,
selectInputStyle,
selectSpanStyle,
softRemoveStyle,
} from "../../style";
/**
* LinkedData class name contract
*/
export interface LinkedDataControlClassNameContract {
linkedDataControl?: string;
linkedDataControl_existingLinkedData?: string;
linkedDataControl_existingLinkedDataItem?: string;
linkedDataControl_existingLinkedDataItemLink?: string;
linkedDataControl_existingLinkedDataItemContent?: string;
linkedDataControl_existingLinkedDataItemName?: string;
linkedDataControl_linkedDataListControl?: string;
linkedDataControl_linkedDataListInput?: string;
linkedDataControl_linkedDataListInputRegion?: string;
linkedDataControl_delete?: string;
linkedDataControl_deleteButton?: string;
}
const styles: ComponentStyles<LinkedDataControlClassNameContract, {}> = {
linkedDataControl: {},
linkedDataControl_existingLinkedData: {
...cleanListStyle,
},
linkedDataControl_existingLinkedDataItem: {
position: "relative",
height: "30px",
marginLeft: "-10px",
paddingLeft: "10px",
cursor: "pointer",
display: "flex",
alignItems: "center",
"&:hover": {
backgroundColor: "rgba(255, 255, 255, 0.04)",
},
},
linkedDataControl_existingLinkedDataItemLink: {
width: "calc(100% - 36px)",
"&$linkedDataControl_existingLinkedDataItemName, &$linkedDataControl_existingLinkedDataItemContent": {
...ellipsis(),
width: "100%",
display: "inline-block",
verticalAlign: "bottom",
},
},
linkedDataControl_existingLinkedDataItemName: {
...ellipsis(),
display: "inline-block",
width: "100%",
whiteSpace: "nowrap",
},
linkedDataControl_existingLinkedDataItemContent: {},
linkedDataControl_linkedDataListControl: {
position: "relative",
width: "100%",
},
linkedDataControl_linkedDataListInput: {
...selectInputStyle,
"&::-webkit-calendar-picker-indicator": {
display: "none !important",
},
},
linkedDataControl_linkedDataListInputRegion: {
...selectSpanStyle,
},
linkedDataControl_delete: {
...softRemoveStyle,
cursor: "pointer",
position: "relative",
verticalAlign: "middle",
},
linkedDataControl_deleteButton: {
...removeItemStyle,
cursor: "pointer",
},
};
export default styles;

Просмотреть файл

@ -0,0 +1,338 @@
import { ManagedClasses } from "@microsoft/fast-components-class-name-contracts-base";
import React from "react";
import { keyEnter, keyTab } from "@microsoft/fast-web-utilities";
import { getDataFromSchema } from "@microsoft/fast-tooling";
import manageJss, { ManagedJSSProps } from "@microsoft/fast-jss-manager-react";
import { DragItem } from "../templates";
import { ArrayAction, LinkedDataActionType } from "../templates/types";
import styles, { LinkedDataControlClassNameContract } from "./control.linked-data.style";
import {
LinkedDataControlProps,
LinkedDataControlState,
} from "./control.linked-data.props";
/**
* Form control definition
*/
class LinkedDataControl extends React.Component<
LinkedDataControlProps & ManagedClasses<LinkedDataControlClassNameContract>,
LinkedDataControlState
> {
public static displayName: string = "LinkedDataControl";
public static defaultProps: Partial<
LinkedDataControlProps & ManagedClasses<LinkedDataControlClassNameContract>
> = {
managedClasses: {},
};
constructor(
props: LinkedDataControlProps & ManagedClasses<LinkedDataControlClassNameContract>
) {
super(props);
this.state = {
searchTerm: "",
isDragging: false,
data: [].concat(props.value || []),
};
}
public render(): React.ReactNode {
// Convert to search component when #3006 has been completed
return (
<div className={this.props.managedClasses.linkedDataControl}>
{this.renderExistingLinkedData()}
{this.renderAddLinkedData()}
</div>
);
}
/**
* Render the UI for adding linked data
*/
private renderAddLinkedData(): React.ReactNode {
return (
<div
className={
this.props.managedClasses.linkedDataControl_linkedDataListControl
}
>
<span
className={
this.props.managedClasses
.linkedDataControl_linkedDataListInputRegion
}
>
<input
className={
this.props.managedClasses
.linkedDataControl_linkedDataListInput
}
type={"text"}
aria-autocomplete={"list"}
list={this.getLinkedDataInputId()}
aria-controls={this.getLinkedDataInputId()}
value={this.state.searchTerm}
placeholder={this.props.strings.linkedDataPlaceholder}
onChange={this.handleSearchTermUpdate}
onKeyDown={this.handleLinkedDataKeydown}
/>
</span>
<datalist id={this.getLinkedDataInputId()} role={"listbox"}>
{this.renderFilteredLinkedDataOptions()}
</datalist>
</div>
);
}
private renderFilteredLinkedDataOptions(): React.ReactNode {
return Object.entries(this.props.schemaDictionary).map(
([key, value]: [string, any]): React.ReactNode => {
return (
<option
key={key}
value={value.title}
label={typeof value.alias === "string" ? value.alias : void 0}
/>
);
}
);
}
/**
* Render the list of existing linkedData for a component
*/
private renderExistingLinkedData(): React.ReactNode {
const childItems: React.ReactNode = this.renderExistingLinkedDataItem();
if (childItems) {
return (
<ul
className={
this.props.managedClasses.linkedDataControl_existingLinkedData
}
>
{childItems}
</ul>
);
}
}
private renderExistingLinkedDataItem(): React.ReactNode {
if (Array.isArray(this.props.value)) {
const {
linkedDataControl_existingLinkedDataItem,
linkedDataControl_existingLinkedDataItemLink,
linkedDataControl_deleteButton,
}: LinkedDataControlClassNameContract = this.props.managedClasses;
return (this.state.isDragging ? this.state.data : this.props.value).map(
(value: any, index: number) => {
return (
<DragItem
key={value + index}
itemClassName={linkedDataControl_existingLinkedDataItem}
itemLinkClassName={
linkedDataControl_existingLinkedDataItemLink
}
itemRemoveClassName={linkedDataControl_deleteButton}
minItems={0}
itemLength={1}
index={index}
onClick={this.handleItemClick(value.id)}
removeDragItem={this.handleRemoveItem}
moveDragItem={this.handleMoveItem}
dropDragItem={this.handleDropItem}
dragStart={this.handleDragStart}
dragEnd={this.handleDragEnd}
strings={this.props.strings}
>
{
this.props.schemaDictionary[
this.props.dataDictionary[0][value.id].schemaId
].title
}
</DragItem>
);
}
);
}
}
private handleDragStart = (): void => {
this.setState({
isDragging: true,
data: [].concat(this.props.value || []),
});
};
private handleDragEnd = (): void => {
this.setState({
isDragging: false,
});
};
private handleItemClick = (
id: string
): ((index: number) => (e: React.MouseEvent<HTMLAnchorElement>) => void) => {
return (index: number): ((e: React.MouseEvent<HTMLAnchorElement>) => void) => {
return (e: React.MouseEvent<HTMLAnchorElement>): void => {
this.props.onUpdateSection(id);
};
};
};
private handleRemoveItem = (
type: ArrayAction,
index: number
): ((e: React.MouseEvent<HTMLButtonElement>) => void) => {
return (e: React.MouseEvent<HTMLButtonElement>): void => {
this.props.onChange({
value: this.props.value.splice(index, 1),
isLinkedData: true,
linkedDataAction: LinkedDataActionType.remove,
});
};
};
private handleMoveItem = (sourceIndex: number, targetIndex: number): void => {
const currentData: unknown[] = [].concat(this.props.value);
if (sourceIndex !== targetIndex) {
currentData.splice(targetIndex, 0, currentData.splice(sourceIndex, 1)[0]);
}
this.setState({
data: currentData,
});
};
private handleDropItem = (): void => {
this.props.onChange({
value: this.state.data,
isLinkedData: true,
linkedDataAction: LinkedDataActionType.reorder,
});
};
private handleLinkedDataKeydown = (
e: React.KeyboardEvent<HTMLInputElement>
): void => {
if (e.target === e.currentTarget) {
// Enter adds linked data if the input value matches a schema lazily or exactly
if (e.key === keyEnter) {
e.preventDefault();
const normalizedValue = e.currentTarget.value.toLowerCase();
if (
this.lazyMatchValueWithASingleSchema(normalizedValue) ||
this.matchExactValueWithASingleSchema(e.currentTarget.value)
) {
this.addLinkedData(normalizedValue, e.currentTarget.value);
/**
* Adding items to the linked data causes the items to
* move the input down while the datalist remains in the same location,
* to prevent the datalist from overlapping the input
* the datalist is dismissed by defocusing and refocusing the input
*/
(e.target as HTMLElement).blur();
(e.target as HTMLElement).focus();
this.setState({
searchTerm: "",
});
}
// Tab performs an auto-complete if there is a single schema it can match to
} else if (e.key === keyTab) {
const normalizedValue = e.currentTarget.value.toLowerCase();
const matchedSchema = this.lazyMatchValueWithASingleSchema(
normalizedValue
);
if (typeof matchedSchema === "string") {
// prevent navigating away by tab when single schema matched
e.preventDefault();
this.setState({
searchTerm: this.props.schemaDictionary[matchedSchema].title,
});
}
}
}
};
private lazyMatchValueWithASingleSchema(value: string): string | void {
const matchingSchemas: string[] = Object.keys(this.props.schemaDictionary).reduce<
string[]
>((previousValue: string[], currentValue: string): string[] => {
if (
this.props.schemaDictionary[currentValue].title
.toLowerCase()
.includes(value)
) {
return previousValue.concat([currentValue]);
}
return previousValue;
}, []);
if (matchingSchemas.length === 1) {
return matchingSchemas[0];
}
}
private matchExactValueWithASingleSchema(value: string): string | void {
return Object.keys(this.props.schemaDictionary).find(
(schemaDictionaryKey: string) => {
return value === this.props.schemaDictionary[schemaDictionaryKey].title;
}
);
}
/**
* Change handler for editing the search term filter
*/
private handleSearchTermUpdate = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({
searchTerm: e.target.value,
});
};
private addLinkedData(normalizedValue: string, originalValue: string): void {
const matchedNormalizedValue:
| string
| void = this.lazyMatchValueWithASingleSchema(normalizedValue);
const matchedOriginalValue: string | void = this.matchExactValueWithASingleSchema(
originalValue
);
const schemaId: string | void = matchedNormalizedValue || matchedOriginalValue;
if (typeof schemaId !== "undefined") {
this.props.onChange({
value: [
{
schemaId,
parent: {
id: this.props.dictionaryId,
dataLocation: this.props.dataLocation,
},
data: getDataFromSchema(this.props.schemaDictionary[schemaId]),
},
],
isLinkedData: true,
linkedDataAction: LinkedDataActionType.add,
index: Array.isArray(this.props.value) ? this.props.value.length : 0,
});
}
}
private getLinkedDataInputId(): string {
return `${this.props.dataLocation}-input`;
}
}
export { LinkedDataControl };
export default manageJss(styles)(LinkedDataControl);

Просмотреть файл

@ -0,0 +1,3 @@
import { NumberFieldTypeControlConfig } from "../templates";
export type NumberFieldControlProps = NumberFieldTypeControlConfig;

Просмотреть файл

@ -0,0 +1,170 @@
import React from "react";
import Adapter from "enzyme-adapter-react-16";
import "../../__tests__/mocks/match-media";
import { configure, mount, shallow } from "enzyme";
import { NumberFieldControl } from "./control.number-field";
import { NumberFieldControlProps } from "./control.number-field.props";
import { NumberFieldControlClassNameContract } from "./control.number-field.style";
import { ControlType } from "../templates";
import defaultStrings from "../form.strings";
/*
* Configure Enzyme
*/
configure({ adapter: new Adapter() });
const managedClasses: NumberFieldControlClassNameContract = {
numberFieldControl: "numberFieldControl-class",
numberFieldControl__disabled: "numberFieldControl__disabled-class",
numberFieldControl__default: "numberFieldControl__default-class",
};
const numberFieldProps: NumberFieldControlProps = {
type: ControlType.numberField,
dataLocation: "",
navigationConfigId: "",
dictionaryId: "",
navigation: {},
onChange: jest.fn(),
min: 0,
max: Infinity,
step: 1,
value: 0,
schema: {},
disabled: false,
elementRef: null,
reportValidity: jest.fn(),
updateValidity: jest.fn(),
validationErrors: [],
required: false,
messageSystem: void 0,
strings: defaultStrings,
messageSystemOptions: null,
};
describe("NumberFieldControl", () => {
test("should not throw", () => {
expect(() => {
shallow(
<NumberFieldControl
{...numberFieldProps}
managedClasses={managedClasses}
/>
);
}).not.toThrow();
});
test("should generate an HTML input element", () => {
const rendered: any = mount(
<NumberFieldControl {...numberFieldProps} managedClasses={managedClasses} />
);
expect(rendered.find("input")).toHaveLength(1);
});
test("should be disabled when disabled props is passed", () => {
const rendered: any = mount(
<NumberFieldControl
{...numberFieldProps}
disabled={true}
managedClasses={managedClasses}
/>
);
expect(rendered.find("input")).toHaveLength(1);
expect(rendered.find("input").prop("disabled")).toBeTruthy();
expect(
rendered.find(`.${managedClasses.numberFieldControl__disabled}`)
).toHaveLength(1);
});
test("should have the default class when default prop is passed", () => {
const rendered: any = mount(
<NumberFieldControl
{...numberFieldProps}
value={undefined}
default={42}
managedClasses={managedClasses}
/>
);
expect(
rendered.find(`.${managedClasses.numberFieldControl__default}`)
).toHaveLength(1);
});
test("should fire an `onChange` callback with the input is changed", () => {
const handleChange: any = jest.fn();
const rendered: any = mount(
<NumberFieldControl
{...numberFieldProps}
onChange={handleChange}
managedClasses={managedClasses}
/>
);
rendered
.find("input")
.at(0)
.simulate("change", { target: { value: "1" } }); // The target.value from an input text box is always a string.
expect(handleChange).toHaveBeenCalled();
expect(handleChange.mock.calls[0][0]).toEqual({ value: 1 });
});
test("should fire an `onChange` callback with undefined value if the input is an empty string", () => {
const handleChange: any = jest.fn();
const rendered: any = mount(
<NumberFieldControl
{...numberFieldProps}
onChange={handleChange}
managedClasses={managedClasses}
/>
);
rendered
.find("input")
.at(0)
.simulate("change", { target: { value: "" } });
expect(handleChange).toHaveBeenCalled();
expect(handleChange.mock.calls[0][0]).toEqual({ value: undefined });
});
test("should not fire an `onChange` callback if the input is NaN", () => {
const handleChange: any = jest.fn();
const rendered: any = mount(
<NumberFieldControl
{...numberFieldProps}
onChange={handleChange}
managedClasses={managedClasses}
/>
);
rendered
.find("input")
.at(0)
.simulate("change", { target: { value: "foo" } });
expect(handleChange).not.toHaveBeenCalled();
});
test("should show default values if they exist and no data is available", () => {
const defaultValue: number = 5;
const rendered: any = mount(
<NumberFieldControl
{...numberFieldProps}
managedClasses={managedClasses}
value={undefined}
default={defaultValue}
/>
);
expect(rendered.find("input").prop("value")).toBe(defaultValue);
});
test("should not show default values if data exists", () => {
const value: number = 5;
const defaultValue: number = 10;
const rendered: any = mount(
<NumberFieldControl
{...numberFieldProps}
managedClasses={managedClasses}
value={value}
default={defaultValue}
/>
);
expect(rendered.find("input").at(0).prop("value")).toBe(value);
});
});

Просмотреть файл

@ -0,0 +1,25 @@
import { ComponentStyles } from "@microsoft/fast-jss-manager-react";
import { defaultFontStyle, inputStyle } from "../../style";
/**
* Number field class name contract
*/
export interface NumberFieldControlClassNameContract {
numberFieldControl?: string;
numberFieldControl__disabled?: string;
numberFieldControl__default?: string;
}
const styles: ComponentStyles<NumberFieldControlClassNameContract, {}> = {
numberFieldControl: {
...inputStyle,
width: "100%",
"&$numberFieldControl__default": {
...defaultFontStyle,
},
},
numberFieldControl__disabled: {},
numberFieldControl__default: {},
};
export default styles;

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше