diff --git a/.gitignore b/.gitignore index fd0c6bfac89eaa..337888cbd21739 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,13 @@ -# The best pattern to follow is ignoring editor files in a global .gitignore configuration file. -# However, in order to prevent issues, editor files are ignored here. +# It is best to ignoring editor and system files in a local .gitignore configuration file. +# However, in order to prevent issues, they are ignored here. +.DS_STORE .idea .vscode - -.DS_STORE *.log /.eslintcache /.nyc_output /coverage +/docs/.env.local /docs/.next /docs/export /examples/**/.cache diff --git a/README.md b/README.md index 550e6f95969479..7a0353233a1cc9 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Yes, it's really all you need to get started as you can see in this live and int ## Questions For _how-to_ questions and other non-issues, -please use [StackOverflow](https://stackoverflow.com/questions/tagged/material-ui) instead of Github issues. +please use [StackOverflow](https://stackoverflow.com/questions/tagged/material-ui) instead of GitHub issues. There is a StackOverflow tag called "material-ui" that you can use to tag your questions. ## Examples diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 20686dc6c17a4b..0c6f557b46b804 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -47,6 +47,15 @@ steps: yarn install displayName: 'install dependencies' + - script: | + yarn danger ci + displayName: 'prepare danger on PRs' + condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest')) + env: + AZURE_BUILD_ID: $(Build.BuildId) + DANGER_COMMAND: 'prepareBundleSizeReport' + DANGER_GITHUB_API_TOKEN: $(GITHUB_API_TOKEN) + - script: | yarn lerna run --ignore @material-ui/icons --parallel --scope "@material-ui/*" build displayName: 'build @material-ui packages' @@ -135,4 +144,5 @@ steps: condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest')) env: AZURE_BUILD_ID: $(Build.BuildId) + DANGER_COMMAND: 'reportBundleSize' DANGER_GITHUB_API_TOKEN: $(GITHUB_API_TOKEN) diff --git a/dangerfile.js b/dangerfile.js index d5014abbb43091..3a594bffd45507 100644 --- a/dangerfile.js +++ b/dangerfile.js @@ -4,6 +4,10 @@ const { danger, markdown } = require('danger'); const { exec } = require('child_process'); const { loadComparison } = require('./scripts/sizeSnapshot'); +const azureBuildId = process.env.AZURE_BUILD_ID; +const azureBuildUrl = `https://dev.azure.com/mui-org/Material-UI/_build/results?buildId=${azureBuildId}`; +const dangerCommand = process.env.DANGER_COMMAND; + const parsedSizeChangeThreshold = 300; const gzipSizeChangeThreshold = 100; @@ -30,7 +34,7 @@ const UPSTREAM_REMOTE = 'danger-upstream'; * scripts exit to avoid adding internal remotes to the local machine. This is * not an issue in CI. */ -async function cleanup() { +async function reportBundleSizeCleanup() { await git(`remote remove ${UPSTREAM_REMOTE}`); } @@ -103,7 +107,13 @@ function sieveResults(results) { return { all: results, main, pages }; } -async function run() { +function prepareBundleSizeReport() { + markdown( + `Bundle size will be reported once [Azure build #${azureBuildId}](${azureBuildUrl}) finishes.`, + ); +} + +async function reportBundleSize() { // Use git locally to grab the commit which represents the place // where the branches differ const upstreamRepo = danger.github.pr.base.repo.full_name; @@ -116,7 +126,7 @@ async function run() { await git(`fetch ${UPSTREAM_REMOTE}`); const mergeBaseCommit = await git(`merge-base HEAD ${UPSTREAM_REMOTE}/${upstreamRef}`); - const commitRange = `${mergeBaseCommit}...${danger.github.pr.head.sha}`; + const detailedComparisonUrl = `https://mui-dashboard.netlify.app/size-comparison?buildId=${azureBuildId}&baseRef=${danger.github.pr.base.ref}&baseCommit=${mergeBaseCommit}&prNumber=${danger.github.pr.number}`; const comparison = await loadComparison(mergeBaseCommit, upstreamRef); @@ -134,31 +144,32 @@ async function run() { markdown(importantChanges.join('\n')); } - const details = `[Details of bundle changes](https://mui-dashboard.netlify.app/size-comparison?buildId=${process.env.AZURE_BUILD_ID}&baseRef=${danger.github.pr.base.ref}&baseCommit=${mergeBaseCommit}&prNumber=${danger.github.pr.number})`; + const details = `[Details of bundle changes](${detailedComparisonUrl})`; markdown(details); } else { - // this can later be removed to reduce PR noise. It is kept for now for debug - // purposes only. DangerJS will swallow console.logs if it completes successfully - markdown(`No bundle size changes comparing ${commitRange}`); + markdown(`[No bundle size changes](${detailedComparisonUrl})`); } } -(async () => { - let exitCode = 0; - try { - await run(); - } catch (err) { - console.error(err); - exitCode = 1; - } - - try { - await cleanup(); - } catch (err) { - console.error(err); - exitCode = 1; +async function run() { + switch (dangerCommand) { + case 'prepareBundleSizeReport': + prepareBundleSizeReport(); + break; + case 'reportBundleSize': + try { + await reportBundleSize(); + } finally { + await reportBundleSizeCleanup(); + } + break; + default: + throw new TypeError(`Unrecognized danger command '${dangerCommand}'`); } +} - process.exit(exitCode); -})(); +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/docs/.env b/docs/.env new file mode 100644 index 00000000000000..9908147ec3fb12 --- /dev/null +++ b/docs/.env @@ -0,0 +1 @@ +FEEDBACK_URL=https://hgvi836wi8.execute-api.us-east-1.amazonaws.com \ No newline at end of file diff --git a/docs/next.config.js b/docs/next.config.js index 6b857de1bccf98..178ad045e1cffb 100644 --- a/docs/next.config.js +++ b/docs/next.config.js @@ -40,6 +40,7 @@ module.exports = { LIB_VERSION: JSON.stringify(pkg.version), PULL_REQUEST: JSON.stringify(process.env.PULL_REQUEST === 'true'), REACT_MODE: JSON.stringify(reactMode), + FEEDBACK_URL: JSON.stringify(process.env.FEEDBACK_URL), }, }), ]); diff --git a/docs/package.json b/docs/package.json index 887977570cdff2..ad2c46a1ff7f40 100644 --- a/docs/package.json +++ b/docs/package.json @@ -93,9 +93,9 @@ "prismjs": "^1.17.1", "prop-types": "^15.7.2", "raw-loader": "^1.0.0", - "react": "^16.13.0", + "react": "^16.14.0", "react-docgen": "^5.0.0-beta.1", - "react-dom": "^16.13.0", + "react-dom": "^16.14.0", "react-draggable": "^4.0.3", "react-final-form": "^6.3.0", "react-is": "^16.13.0", diff --git a/docs/packages/feedback/README.md b/docs/packages/feedback/README.md new file mode 100644 index 00000000000000..1330cfbf7eb40f --- /dev/null +++ b/docs/packages/feedback/README.md @@ -0,0 +1,108 @@ +# Rating + +This Lambda function stores and retrieves page feedback using DynamoDB. It is already deployed in the Material-UI AWS account. Request credentials if you need to update dev for testing, or to deploy a new prod version. + +If you wish to deploy your own instance for testing, follow the steps below. + +## Prerequisites + +Create an AWS profile in ~/.aws/credentials called "claudia" with credentials corresponding to an IAM user with AmazonAPIGatewayAdministrator, AWSLambdaFullAccess and IAMFullAccess policies. +You can do that with `aws configure --profile claudia`. + +Create a table in DynamoDB, with a `string` partition key called `id`, and a sort key called `page`. You can do that from the DynamoDB web console, or using the AWS CLI command line. Here is an example command that will create the `feedback-dev` table with the minimal provisioned throughput: + +```bash +aws dynamodb create-table --profile claudia --region us-east-1 \ + --attribute-definitions AttributeName=id,AttributeType=S AttributeName=page,AttributeType=S \ + --key-schema AttributeName=id,KeyType=HASH AttributeName=page,KeyType=RANGE \ + --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=1 \ + --query TableDescription.TableArn --output text \ + --table-name feedback-dev +``` + +You will need to repeat this command to create a table for production, for example `feedback-prod`. + +For on-demand throughput, replace: + +``` + --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=1 \ +``` + +with: + +``` + --billing-mode PAY_PER_REQUEST \ +``` + +The project includes an IAM access policy that will grant the lambda function access to the tables. You can edit the [policies/access-dynamodb.json](policies/access-dynamodb.json) file to change the access permissions. These are only applied on create (`yarn setup`). Alternatively, to avoid inadvetantly pushing changes, use the `--policies` flag with `yarn setup` to refer to a copy of this directory, and exclude it in your `~/.gitignore`. + +> ⚠️ You will need to update the "Resource" key in this file with the value returned after creating each table. + +## Get started + +> ⚠️ When setting up for the first time, you will need to delete the included `claudia.json` file that is specific to the MUI installation. Alternatively, if making changes to the function that you intend to submit back, then to avoid inadvetantly commiting changes to `claudia.json`, use `--config` with each command to create and use a local config file, and exclude this file in your `~/.gitignore`. + +To set this up, first [set up the credentials](https://claudiajs.com/tutorials/installing.html#configuring-access-credentials), then: + +1. run `yarn install` (from the root workspace) to install the dependencies +1. Navigate into the directory of this README, e.g. `cd docs/packages/feedback` +1. run `yarn setup` to create the lambda function on AWS under the default name. + This will also ask you for table names for development and production. + If you used the above AWS command, they will be `feedback-dev` and `feedback-dev` respectively. +1. Test the API using the [example requests below](#testing) + +For subsequent updates, use the `npm run deploy` command. + +## Stage variables + +The table name, stored in the API Gateway stage variables, is passed to each request processor in the `request.env` key-value map. Check out [index.js](index.js) to see it in use. + +The value is set during the first deployment, using `--configure-table-dev` & `--configure-table-prod`. This works using a post-deploy step (check out the last line of [index.js](index.js) for the actual setup, and [Configuring stage variables using post-deployment steps](https://github.com/claudiajs/claudia-api-builder/blob/master/docs/api.md#configuring-stage-variables-using-post-deployment-steps) for more information about the API). + +## The API + +- `POST` to `/feedback` - stores a new rating data object +- `GET` from `/feedback/{id}` - returns all ratings with id `{id}` +- `GET` from `/rating/average` - returns average ratings for all pages + +## Testing + +Claudia will print the API URL after it is created (typically something in the format `https://[API ID].execute-api.[REGION].amazonaws.com/`). Replace `` with that value in the examples below: + +You can test the API by using `curl` (or using a fancier client like [Postman](https://www.getpostman.com/)). Below are some examples with `curl`. + +### Create new feedback + +This will create a feedback entry from the data stored in [example.json](example.json). Change the data in the file to create ratings: + +```bash +curl -H "Content-Type: application/json" -X POST --data @example.json /feedback +``` + +Add the UUID returned to `example.json` with key `id` to store more feedback under the same id. + +### Retrieve feedback + +This will get the feedback stored for ID d6890562-3606-4c14-a765-da81919057d1 + +```bash +curl /feedback/d6890562-3606-4c14-a765-da81919057d1 +``` + +### Retrieve average ratings + +This will get the average feedback stored for all pages + +```bash +curl /feedback/average +``` + +### Testing with the documentation + +Create the file `docs/.env.local` containing an environment variable `FEEDBACK_URL` with your API URL without the version. For example: + +```sh +FEEDBACK_URL=https://abcd123ef4.execute-api.us-east-1.amazonaws.com +``` + +If already running, restart the local docs site. Feedback should now be posted to your deployment. diff --git a/docs/packages/feedback/claudia.json b/docs/packages/feedback/claudia.json new file mode 100644 index 00000000000000..8b578bbc83c6cc --- /dev/null +++ b/docs/packages/feedback/claudia.json @@ -0,0 +1,11 @@ +{ + "lambda": { + "role": "feedback-executor", + "name": "feedback", + "region": "us-east-1" + }, + "api": { + "id": "hgvi836wi8", + "module": "index" + } +} diff --git a/docs/packages/feedback/example.json b/docs/packages/feedback/example.json new file mode 100644 index 00000000000000..a8c44f332201d6 --- /dev/null +++ b/docs/packages/feedback/example.json @@ -0,0 +1,7 @@ +{ + "page": "/components/cards", + "version": "5.0.0", + "rating": 5, + "comment": "Yay!", + "language": "en" +} diff --git a/docs/packages/feedback/index.js b/docs/packages/feedback/index.js new file mode 100644 index 00000000000000..3c6aca1e1133aa --- /dev/null +++ b/docs/packages/feedback/index.js @@ -0,0 +1,149 @@ +/* eslint-disable no-console */ +const ApiBuilder = require('claudia-api-builder'); +const AWS = require('aws-sdk'); +const uuid = require('uuid/v4'); + +const api = new ApiBuilder(); +const dynamoDb = new AWS.DynamoDB.DocumentClient(); + +async function dbGet(request, id, page) { + const stage = request.context.stage; + const params = { + TableName: request.env[`${stage}TableName`], + Key: { + id, + page, + }, + ConsistentRead: true, + }; + + try { + console.log('dbGet()', params); + const response = await dynamoDb.get(params).promise(); + console.log({ response }); + return response.Item; + } catch (error) { + console.error(error); + throw error; + } +} + +async function dbPut(request, id, item = {}) { + const stage = request.context.stage; + const { page, rating, comment, version, language } = request.body; + + const params = { + TableName: request.env[`${stage}TableName`], + Item: { + id, + page, + rating, + comment, + version, + language, + dateTime: new Date().toString(), + }, + }; + + await Object.assign(params.Item, item); + + try { + console.log('dbPut()', params); + const response = await dynamoDb.put(params).promise(); + console.log({ response }); + return params.Item; + } catch (error) { + console.error(error); + throw error; + } +} + +async function dbQuery(request, id) { + const stage = request.context.stage; + const params = { + TableName: request.env[`${stage}TableName`], + KeyConditionExpression: 'id = :id', + ExpressionAttributeValues: { + ':id': id, + }, + ConsistentRead: true, + }; + + try { + console.log('dbQuery()', params); + const response = await dynamoDb.query(params).promise(); + console.log({ response }); + return response.Items; + } catch (error) { + console.error(error); + throw error; + } +} + +async function updateAverageRating(request, currentUserRating) { + console.log('updateAverageRating()'); + + const id = 'average'; + const pageAverage = await dbGet(request, id, request.body.page); + const average = (pageAverage && pageAverage.rating) || 0; + let count = (pageAverage && pageAverage.count) || 0; + + let rating; + if (currentUserRating !== null) { + rating = (average * count - currentUserRating + request.body.rating) / count; + } else { + rating = (average * count + request.body.rating) / (count + 1); + count += 1; + } + + const newAverageRating = { + rating, + count, + comment: undefined, + language: undefined, + }; + + return dbPut(request, id, newAverageRating); +} + +api.post( + '/feedback', + async (request) => { + console.log('POST /feedback', request.body); + const id = request.body.id || uuid(); + + let currentRating = null; + if (request.body.id) { + const userPage = await dbGet(request, id, request.body.page); + console.log({ userPage }); + currentRating = userPage && userPage.rating; + } + + try { + const result = await dbPut(request, id); + await updateAverageRating(request, currentRating); + return result; + } catch (error) { + console.log(error); + throw error; + } + }, + { success: 201 }, +); + +api.get('/feedback/{id}', (request) => { + console.log(`GET /feedback/${request.pathParams.id}`); + return (async () => { + const result = await dbQuery(request, request.pathParams.id); + return result.reduce((acc, curr) => { + const { rating, comment, count } = curr; + acc[curr.page] = { rating, comment, count }; + return acc; + }, {}); + })(); +}); + +api.addPostDeployConfig('devTableName', 'DynamoDB Development Table Name:', 'configure-table-dev'); +api.addPostDeployConfig('prodTableName', 'DynamoDB Production Table Name:', 'configure-table-prod'); + +module.exports = api; diff --git a/docs/packages/feedback/package.json b/docs/packages/feedback/package.json new file mode 100644 index 00000000000000..d41fdc0fc2122e --- /dev/null +++ b/docs/packages/feedback/package.json @@ -0,0 +1,30 @@ +{ + "name": "feedback", + "version": "1.0.0", + "description": "Store and retrieve page ratings and comments", + "main": "./index.js", + "license": "MIT", + "author": "Material-UI Team", + "private": true, + "files": [ + "*.js" + ], + "scripts": { + "claudia": "claudia --profile claudia", + "curl": "curl -H \"Content-Type: application/json\" -X POST --data @example.json https://hgvi836wi8.execute-api.us-east-1.amazonaws.com/dev/rating", + "setup": "claudia create --profile claudia --version dev --region us-east-1 --api-module index --policies policies --configure-table-dev --configure-table-prod --no-optional-dependencies", + "update": "claudia update --profile claudia --version dev --no-optional-dependencies", + "deploy": "claudia update --profile claudia --version dev --no-optional-dependencies && claudia --profile claudia set-version --version prod", + "reconfigure": "claudia update --profile claudia --configure-db" + }, + "dependencies": { + "claudia-api-builder": "^4.1.2", + "uuid": "^3.3.2" + }, + "devDependencies": { + "claudia": "^5.12.0" + }, + "optionalDependencies": { + "aws-sdk": "^2.766.0" + } +} diff --git a/docs/packages/feedback/policies/access-dynamodb.json b/docs/packages/feedback/policies/access-dynamodb.json new file mode 100644 index 00000000000000..13e30a4249a318 --- /dev/null +++ b/docs/packages/feedback/policies/access-dynamodb.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": ["dynamodb:DeleteItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query"], + "Effect": "Allow", + "Resource": "arn:aws:dynamodb:us-east-1:446171413463:table/feedback-dev" + }, + { + "Action": ["dynamodb:DeleteItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query"], + "Effect": "Allow", + "Resource": "arn:aws:dynamodb:us-east-1:446171413463:table/feedback-dev" + } + ] +} diff --git a/docs/pages/_app.js b/docs/pages/_app.js index 8fee303314c00d..7d6d0f0b195dda 100644 --- a/docs/pages/_app.js +++ b/docs/pages/_app.js @@ -213,7 +213,7 @@ async function registerServiceWorker() { if ( 'serviceWorker' in navigator && process.env.NODE_ENV === 'production' && - window.location.host.indexOf('material-ui.com') <= 0 + window.location.host.indexOf('material-ui.com') !== -1 ) { // register() automatically attempts to refresh the sw.js. const registration = await navigator.serviceWorker.register('/sw.js'); diff --git a/docs/pages/api-docs/accordion-actions.md b/docs/pages/api-docs/accordion-actions.md index 4a2fd25f04a9bf..67da434ce883f2 100644 --- a/docs/pages/api-docs/accordion-actions.md +++ b/docs/pages/api-docs/accordion-actions.md @@ -41,7 +41,7 @@ Any other props supplied will be provided to the root element (native element). | Rule name | Global class | Description | |:-----|:-------------|:------------| | root | .MuiAccordionActions-root | Styles applied to the root element. -| spacing | .MuiAccordionActions-spacing | Styles applied to the root element if `disableSpacing={false}`. +| spacing | .MuiAccordionActions-spacing | Styles applied to the root element unless `disableSpacing={true}`. You can override the style of the component thanks to one of these customization points: diff --git a/docs/pages/api-docs/accordion.md b/docs/pages/api-docs/accordion.md index 5d2637a2dfed15..325889d244dc1d 100644 --- a/docs/pages/api-docs/accordion.md +++ b/docs/pages/api-docs/accordion.md @@ -47,7 +47,7 @@ Any other props supplied will be provided to the root element ([Paper](/api/pape | Rule name | Global class | Description | |:-----|:-------------|:------------| | root | .MuiAccordion-root | Styles applied to the root element. -| rounded | .MuiAccordion-rounded | Styles applied to the root element if `square={false}`. +| rounded | .MuiAccordion-rounded | Styles applied to the root element unless `square={true}`. | expanded | .Mui-expanded | Pseudo-class applied to the root element if `expanded={true}`. | disabled | .Mui-disabled | Pseudo-class applied to the root element if `disabled={true}`. | region | .MuiAccordion-region | Styles applied to the region element, the container of the children. diff --git a/docs/pages/api-docs/avatar-group.md b/docs/pages/api-docs/avatar-group.md index 11428ea74f6e60..85f912a7ede5b4 100644 --- a/docs/pages/api-docs/avatar-group.md +++ b/docs/pages/api-docs/avatar-group.md @@ -1,5 +1,5 @@ --- -filename: /packages/material-ui-lab/src/AvatarGroup/AvatarGroup.js +filename: /packages/material-ui/src/AvatarGroup/AvatarGroup.js --- @@ -11,9 +11,9 @@ filename: /packages/material-ui-lab/src/AvatarGroup/AvatarGroup.js ## Import ```js -import AvatarGroup from '@material-ui/lab/AvatarGroup'; +import AvatarGroup from '@material-ui/core/AvatarGroup'; // or -import { AvatarGroup } from '@material-ui/lab'; +import { AvatarGroup } from '@material-ui/core'; ``` You can learn more about the difference by [reading this guide](/guides/minimizing-bundle-size/). @@ -51,7 +51,7 @@ You can override the style of the component thanks to one of these customization - With a [global class name](/customization/components/#overriding-styles-with-global-class-names). - With a theme and an [`overrides` property](/customization/globals/#css). -If that's not sufficient, you can check the [implementation of the component](https://github.com/mui-org/material-ui/blob/next/packages/material-ui-lab/src/AvatarGroup/AvatarGroup.js) for more detail. +If that's not sufficient, you can check the [implementation of the component](https://github.com/mui-org/material-ui/blob/next/packages/material-ui/src/AvatarGroup/AvatarGroup.js) for more detail. ## Demos diff --git a/docs/pages/api-docs/card-actions.md b/docs/pages/api-docs/card-actions.md index 1017a0c99869ed..e6946471fe8832 100644 --- a/docs/pages/api-docs/card-actions.md +++ b/docs/pages/api-docs/card-actions.md @@ -41,7 +41,7 @@ Any other props supplied will be provided to the root element (native element). | Rule name | Global class | Description | |:-----|:-------------|:------------| | root | .MuiCardActions-root | Styles applied to the root element. -| spacing | .MuiCardActions-spacing | Styles applied to the root element if `disableSpacing={false}`. +| spacing | .MuiCardActions-spacing | Styles applied to the root element unless `disableSpacing={true}`. You can override the style of the component thanks to one of these customization points: diff --git a/docs/pages/api-docs/dialog-actions.md b/docs/pages/api-docs/dialog-actions.md index 805be77bfb4ca5..c3f807ba491ade 100644 --- a/docs/pages/api-docs/dialog-actions.md +++ b/docs/pages/api-docs/dialog-actions.md @@ -41,7 +41,7 @@ Any other props supplied will be provided to the root element (native element). | Rule name | Global class | Description | |:-----|:-------------|:------------| | root | .MuiDialogActions-root | Styles applied to the root element. -| spacing | .MuiDialogActions-spacing | Styles applied to the root element if `disableSpacing={false}`. +| spacing | .MuiDialogActions-spacing | Styles applied to the root element unless `disableSpacing={true}`. You can override the style of the component thanks to one of these customization points: diff --git a/docs/pages/api-docs/filled-input.md b/docs/pages/api-docs/filled-input.md index ca4299265d2427..e6958f5c2f387f 100644 --- a/docs/pages/api-docs/filled-input.md +++ b/docs/pages/api-docs/filled-input.md @@ -66,7 +66,7 @@ Any other props supplied will be provided to the root element ([InputBase](/api/ |:-----|:-------------|:------------| | root | .MuiFilledInput-root | Styles applied to the root element. | colorSecondary | .MuiFilledInput-colorSecondary | Styles applied to the root element if color secondary. -| underline | .MuiFilledInput-underline | Styles applied to the root element if `disableUnderline={false}`. +| underline | .MuiFilledInput-underline | Styles applied to the root element unless `disableUnderline={true}`. | focused | .Mui-focused | Pseudo-class applied to the root element if the component is focused. | disabled | .Mui-disabled | Pseudo-class applied to the root element if `disabled={true}`. | adornedStart | .MuiFilledInput-adornedStart | Styles applied to the root element if `startAdornment` is provided. diff --git a/docs/pages/api-docs/input-label.md b/docs/pages/api-docs/input-label.md index 923b562f4dfb68..2a835e5f954264 100644 --- a/docs/pages/api-docs/input-label.md +++ b/docs/pages/api-docs/input-label.md @@ -57,7 +57,7 @@ Any other props supplied will be provided to the root element ([FormLabel](/api/ | formControl | .MuiInputLabel-formControl | Styles applied to the root element if the component is a descendant of `FormControl`. | marginDense | .MuiInputLabel-marginDense | Styles applied to the root element if `margin="dense"`. | shrink | .MuiInputLabel-shrink | Styles applied to the `input` element if `shrink={true}`. -| animated | .MuiInputLabel-animated | Styles applied to the `input` element if `disableAnimation={false}`. +| animated | .MuiInputLabel-animated | Styles applied to the `input` element unless `disableAnimation={true}`. | filled | .MuiInputLabel-filled | Styles applied to the root element if `variant="filled"`. | outlined | .MuiInputLabel-outlined | Styles applied to the root element if `variant="outlined"`. diff --git a/docs/pages/api-docs/input.md b/docs/pages/api-docs/input.md index ba1533007eb8ca..e3b6afa86d8873 100644 --- a/docs/pages/api-docs/input.md +++ b/docs/pages/api-docs/input.md @@ -69,7 +69,7 @@ Any other props supplied will be provided to the root element ([InputBase](/api/ | focused | .Mui-focused | Styles applied to the root element if the component is focused. | disabled | .Mui-disabled | Styles applied to the root element if `disabled={true}`. | colorSecondary | .MuiInput-colorSecondary | Styles applied to the root element if color secondary. -| underline | .MuiInput-underline | Styles applied to the root element if `disableUnderline={false}`. +| underline | .MuiInput-underline | Styles applied to the root element unless `disableUnderline={true}`. | error | .Mui-error | Pseudo-class applied to the root element if `error={true}`. | marginDense | .MuiInput-marginDense | Styles applied to the `input` element if `margin="dense"`. | multiline | .MuiInput-multiline | Styles applied to the root element if `multiline={true}`. diff --git a/docs/pages/api-docs/list-item.md b/docs/pages/api-docs/list-item.md index 052beaeb743fa1..b7efc0a89d0963 100644 --- a/docs/pages/api-docs/list-item.md +++ b/docs/pages/api-docs/list-item.md @@ -57,7 +57,7 @@ Any other props supplied will be provided to the root element (native element). | alignItemsFlexStart | .MuiListItem-alignItemsFlexStart | Styles applied to the `component` element if `alignItems="flex-start"`. | disabled | .Mui-disabled | Pseudo-class applied to the inner `component` element if `disabled={true}`. | divider | .MuiListItem-divider | Styles applied to the inner `component` element if `divider={true}`. -| gutters | .MuiListItem-gutters | Styles applied to the inner `component` element if `disableGutters={false}`. +| gutters | .MuiListItem-gutters | Styles applied to the inner `component` element unless `disableGutters={true}`. | button | .MuiListItem-button | Styles applied to the inner `component` element if `button={true}`. | secondaryAction | .MuiListItem-secondaryAction | Styles applied to the `component` element if `children` includes `ListItemSecondaryAction`. | selected | .Mui-selected | Pseudo-class applied to the root element if `selected={true}`. diff --git a/docs/pages/api-docs/list-subheader.md b/docs/pages/api-docs/list-subheader.md index b07944c23d5c6b..89d6c062b3d0a9 100644 --- a/docs/pages/api-docs/list-subheader.md +++ b/docs/pages/api-docs/list-subheader.md @@ -47,9 +47,9 @@ Any other props supplied will be provided to the root element (native element). | root | .MuiListSubheader-root | Styles applied to the root element. | colorPrimary | .MuiListSubheader-colorPrimary | Styles applied to the root element if `color="primary"`. | colorInherit | .MuiListSubheader-colorInherit | Styles applied to the root element if `color="inherit"`. -| gutters | .MuiListSubheader-gutters | Styles applied to the inner `component` element if `disableGutters={false}`. +| gutters | .MuiListSubheader-gutters | Styles applied to the inner `component` element unless `disableGutters={true}`. | inset | .MuiListSubheader-inset | Styles applied to the root element if `inset={true}`. -| sticky | .MuiListSubheader-sticky | Styles applied to the root element if `disableSticky={false}`. +| sticky | .MuiListSubheader-sticky | Styles applied to the root element unless `disableSticky={true}`. You can override the style of the component thanks to one of these customization points: diff --git a/docs/pages/api-docs/list.md b/docs/pages/api-docs/list.md index 0f75b54b4fd688..d44fc522ac8eb1 100644 --- a/docs/pages/api-docs/list.md +++ b/docs/pages/api-docs/list.md @@ -44,7 +44,7 @@ Any other props supplied will be provided to the root element (native element). | Rule name | Global class | Description | |:-----|:-------------|:------------| | root | .MuiList-root | Styles applied to the root element. -| padding | .MuiList-padding | Styles applied to the root element if `disablePadding={false}`. +| padding | .MuiList-padding | Styles applied to the root element unless `disablePadding={true}`. | dense | .MuiList-dense | Styles applied to the root element if dense. | subheader | .MuiList-subheader | Styles applied to the root element if a `subheader` is provided. diff --git a/docs/pages/api-docs/menu-item.md b/docs/pages/api-docs/menu-item.md index aa15201b74bb9b..efd42ab0f45ddd 100644 --- a/docs/pages/api-docs/menu-item.md +++ b/docs/pages/api-docs/menu-item.md @@ -44,7 +44,7 @@ Any other props supplied will be provided to the root element ([ListItem](/api/l | Rule name | Global class | Description | |:-----|:-------------|:------------| | root | .MuiMenuItem-root | Styles applied to the root element. -| gutters | .MuiMenuItem-gutters | Styles applied to the root element if `disableGutters={false}`. +| gutters | .MuiMenuItem-gutters | Styles applied to the root element unless `disableGutters={true}`. | selected | .Mui-selected | Styles applied to the root element if `selected={true}`. | dense | .MuiMenuItem-dense | Styles applied to the root element if dense. diff --git a/docs/pages/api-docs/paper.md b/docs/pages/api-docs/paper.md index ec57c8ed379489..06357b191ce5d9 100644 --- a/docs/pages/api-docs/paper.md +++ b/docs/pages/api-docs/paper.md @@ -44,7 +44,7 @@ Any other props supplied will be provided to the root element (native element). | Rule name | Global class | Description | |:-----|:-------------|:------------| | root | .MuiPaper-root | Styles applied to the root element. -| rounded | .MuiPaper-rounded | Styles applied to the root element if `square={false}`. +| rounded | .MuiPaper-rounded | Styles applied to the root element unless `square={true}`. | outlined | .MuiPaper-outlined | Styles applied to the root element if `variant="outlined"`. | elevation | .MuiPaper-elevation | Styles applied to the root element if `variant="elevation"`. | elevation0 | .MuiPaper-elevation0 | diff --git a/docs/pages/api-docs/toolbar.md b/docs/pages/api-docs/toolbar.md index 53aec7d9760424..a79ad0320c805f 100644 --- a/docs/pages/api-docs/toolbar.md +++ b/docs/pages/api-docs/toolbar.md @@ -43,7 +43,7 @@ Any other props supplied will be provided to the root element (native element). | Rule name | Global class | Description | |:-----|:-------------|:------------| | root | .MuiToolbar-root | Styles applied to the root element. -| gutters | .MuiToolbar-gutters | Styles applied to the root element if `disableGutters={false}`. +| gutters | .MuiToolbar-gutters | Styles applied to the root element unless `disableGutters={true}`. | regular | .MuiToolbar-regular | Styles applied to the root element if `variant="regular"`. | dense | .MuiToolbar-dense | Styles applied to the root element if `variant="dense"`. diff --git a/docs/pages/index.js b/docs/pages/index.js index 30afeb509a28bf..5cd9da36c8c216 100644 --- a/docs/pages/index.js +++ b/docs/pages/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import * as PropTypes from 'prop-types'; +import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; diff --git a/docs/src/modules/components/Demo.js b/docs/src/modules/components/Demo.js index 8ef4e8642e7191..84e25faa8423e8 100644 --- a/docs/src/modules/components/Demo.js +++ b/docs/src/modules/components/Demo.js @@ -1,49 +1,41 @@ import * as React from 'react'; -import LZString from 'lz-string'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import copy from 'clipboard-copy'; -import { useSelector, useDispatch } from 'react-redux'; -import { alpha, makeStyles, useTheme } from '@material-ui/core/styles'; +import { useSelector } from 'react-redux'; +import { alpha, makeStyles } from '@material-ui/core/styles'; import IconButton from '@material-ui/core/IconButton'; -import useMediaQuery from '@material-ui/core/useMediaQuery'; import Collapse from '@material-ui/core/Collapse'; -import Fade from '@material-ui/core/Fade'; -import ToggleButton from '@material-ui/core/ToggleButton'; -import ToggleButtonGroup from '@material-ui/core/ToggleButtonGroup'; -import { JavaScript as JavaScriptIcon, TypeScript as TypeScriptIcon } from '@material-ui/docs'; import NoSsr from '@material-ui/core/NoSsr'; -import EditIcon from '@material-ui/icons/Edit'; -import CodeIcon from '@material-ui/icons/Code'; -import FileCopyIcon from '@material-ui/icons/FileCopy'; -import Snackbar from '@material-ui/core/Snackbar'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; -import Tooltip from '@material-ui/core/Tooltip'; -import RefreshIcon from '@material-ui/icons/Refresh'; -import ResetFocusIcon from '@material-ui/icons/CenterFocusWeak'; import HighlightedCode from 'docs/src/modules/components/HighlightedCode'; import DemoSandboxed from 'docs/src/modules/components/DemoSandboxed'; import { AdCarbonInline } from 'docs/src/modules/components/AdCarbon'; -import getDemoConfig from 'docs/src/modules/utils/getDemoConfig'; import getJsxPreview from 'docs/src/modules/utils/getJsxPreview'; -import { getCookie } from 'docs/src/modules/utils/helpers'; -import { ACTION_TYPES, CODE_VARIANTS } from 'docs/src/modules/constants'; +import { CODE_VARIANTS } from 'docs/src/modules/constants'; -function compress(object) { - return LZString.compressToBase64(JSON.stringify(object)) - .replace(/\+/g, '-') // Convert '+' to '-' - .replace(/\//g, '_') // Convert '/' to '_' - .replace(/=+$/, ''); // Remove ending '=' -} +const DemoToolbar = React.lazy(() => import('./DemoToolbar')); +// Sync with styles from DemoToolbar +// Importing the styles results in no bundle size reduction +const useDemoToolbarFallbackStyles = makeStyles( + (theme) => { + return { + root: { + display: 'none', + [theme.breakpoints.up('sm')]: { + display: 'flex', + height: theme.spacing(6), + }, + }, + }; + }, + { name: 'DemoToolbar' }, +); +export function DemoToolbarFallback() { + const classes = useDemoToolbarFallbackStyles(); + const t = useSelector((state) => state.options.t); -function addHiddenInput(form, name, value) { - const input = document.createElement('input'); - input.type = 'hidden'; - input.name = name; - input.value = value; - form.appendChild(input); + return ( +
+ ); } function getDemoName(location) { @@ -87,551 +79,6 @@ function useUniqueId(prefix) { return id ? `${prefix}${id}` : id; } -const useDemoToolbarStyles = makeStyles( - (theme) => { - return { - root: { - display: 'none', - [theme.breakpoints.up('sm')]: { - display: 'flex', - flip: false, - top: 0, - right: theme.spacing(1), - height: theme.spacing(6), - }, - justifyContent: 'space-between', - }, - toggleButtonGroup: { - margin: '8px 0', - }, - toggleButton: { - padding: '4px 9px', - }, - tooltip: { - zIndex: theme.zIndex.appBar - 1, - }, - }; - }, - { name: 'DemoToolbar' }, -); - -const alwaysTrue = () => true; - -/** - * @param {React.Ref[]} controlRefs - * @param {object} [options] - * @param {(index: number) => boolean} [options.isFocusableControl] In case certain controls become unfocusable - * @param {number} [options.defaultActiveIndex] - */ -function useToolbar(controlRefs, options = {}) { - const { defaultActiveIndex = 0, isFocusableControl = alwaysTrue } = options; - const [activeControlIndex, setActiveControlIndex] = React.useState(defaultActiveIndex); - - // TODO: do we need to do this during layout practically? It's technically - // a bit too late since we allow user interaction between layout and passive effects - React.useEffect(() => { - setActiveControlIndex((currentActiveControlIndex) => { - if (!isFocusableControl(currentActiveControlIndex)) { - return defaultActiveIndex; - } - return currentActiveControlIndex; - }); - }, [defaultActiveIndex, isFocusableControl]); - - // controlRefs.findIndex(controlRef => controlRef.current = element) - function findControlIndex(element) { - let controlIndex = -1; - controlRefs.forEach((controlRef, index) => { - if (controlRef.current === element) { - controlIndex = index; - } - }); - return controlIndex; - } - - function handleControlFocus(event) { - const nextActiveControlIndex = findControlIndex(event.target); - if (nextActiveControlIndex !== -1) { - setActiveControlIndex(nextActiveControlIndex); - } else { - // make sure DCE works - // eslint-disable-next-line no-lonely-if - if (process.env.NODE_ENV !== 'production') { - console.error( - 'Material-UI: The toolbar contains a focusable element that is not controlled by the toolbar. ' + - 'Make sure you have attached `getControlProps(index)` to every focusable element within this toolbar.', - ); - } - } - } - - let handleToolbarFocus; - if (process.env.NODE_ENV !== 'production') { - handleToolbarFocus = (event) => { - if (findControlIndex(event.target) === -1) { - console.error( - 'Material-UI: The toolbar contains a focusable element that is not controlled by the toolbar. ' + - 'Make sure you have attached `getControlProps(index)` to every focusable element within this toolbar.', - ); - } - }; - } - - const { direction } = useTheme(); - - function handleToolbarKeyDown(event) { - // We handle toolbars where controls can be hidden temporarily. - // When a control is hidden we can't move focus to it and have to exclude - // it from the order. - let currentFocusableControlIndex = -1; - const focusableControls = []; - controlRefs.forEach((controlRef, index) => { - const { current: control } = controlRef; - if (index === activeControlIndex) { - currentFocusableControlIndex = focusableControls.length; - } - if (control !== null && isFocusableControl(index)) { - focusableControls.push(control); - } - }); - - const prevControlKey = direction === 'ltr' ? 'ArrowLeft' : 'ArrowRight'; - const nextControlKey = direction === 'ltr' ? 'ArrowRight' : 'ArrowLeft'; - - let nextFocusableIndex = -1; - switch (event.key) { - case prevControlKey: - nextFocusableIndex = - (currentFocusableControlIndex - 1 + focusableControls.length) % focusableControls.length; - break; - case nextControlKey: - nextFocusableIndex = (currentFocusableControlIndex + 1) % focusableControls.length; - break; - case 'Home': - nextFocusableIndex = 0; - break; - case 'End': - nextFocusableIndex = focusableControls.length - 1; - break; - default: - break; - } - - if (nextFocusableIndex !== -1) { - event.preventDefault(); - focusableControls[nextFocusableIndex].focus(); - } - } - - function getControlProps(index) { - return { - onFocus: handleControlFocus, - ref: controlRefs[index], - tabIndex: index === activeControlIndex ? 0 : -1, - }; - } - - return { - getControlProps, - toolbarProps: { - // TODO: good opportunity to warn on missing `aria-label` - onFocus: handleToolbarFocus, - onKeyDown: handleToolbarKeyDown, - role: 'toolbar', - }, - }; -} - -function DemoToolbar(props) { - const { - codeOpen, - codeVariant, - demo, - demoData, - demoId, - demoHovered, - demoName, - demoOptions, - demoSourceId, - initialFocusRef, - onCodeOpenChange, - onResetDemoClick, - openDemoSource, - showPreview, - } = props; - - const classes = useDemoToolbarStyles(); - - const dispatch = useDispatch(); - const t = useSelector((state) => state.options.t); - - const hasTSVariant = demo.rawTS; - const renderedCodeVariant = () => { - if (codeVariant === CODE_VARIANTS.TS && hasTSVariant) { - return CODE_VARIANTS.TS; - } - return CODE_VARIANTS.JS; - }; - - const handleCodeLanguageClick = (event, clickedCodeVariant) => { - if (codeVariant !== clickedCodeVariant) { - dispatch({ - type: ACTION_TYPES.OPTIONS_CHANGE, - payload: { - codeVariant: clickedCodeVariant, - }, - }); - } - }; - - const handleCodeSandboxClick = () => { - const demoConfig = getDemoConfig(demoData); - const parameters = compress({ - files: { - 'package.json': { - content: { - name: demoConfig.title, - description: demoConfig.description, - dependencies: demoConfig.dependencies, - devDependencies: { - 'react-scripts': 'latest', - ...demoConfig.devDependencies, - }, - main: demoConfig.main, - scripts: demoConfig.scripts, - // We used `title` previously but only inference from `name` is documented. - // TODO revisit once https://github.com/codesandbox/codesandbox-client/issues/4983 is resolved. - title: demoConfig.title, - }, - }, - ...Object.keys(demoConfig.files).reduce((files, name) => { - files[name] = { content: demoConfig.files[name] }; - return files; - }, {}), - }, - }); - - const form = document.createElement('form'); - form.method = 'POST'; - form.target = '_blank'; - form.action = 'https://codeSandbox.io/api/v1/sandboxes/define'; - addHiddenInput(form, 'parameters', parameters); - document.body.appendChild(form); - form.submit(); - document.body.removeChild(form); - }; - - const [anchorEl, setAnchorEl] = React.useState(null); - const handleMoreClick = (event) => { - setAnchorEl(event.currentTarget); - }; - const handleMoreClose = () => { - setAnchorEl(null); - }; - - const [snackbarOpen, setSnackbarOpen] = React.useState(false); - const [snackbarMessage, setSnackbarMessage] = React.useState(undefined); - - const handleSnackbarClose = () => { - setSnackbarOpen(false); - }; - const handleCopyClick = async () => { - try { - await copy(demoData.raw); - setSnackbarMessage(t('copiedSource')); - setSnackbarOpen(true); - } finally { - handleMoreClose(); - } - }; - - const handleStackBlitzClick = () => { - const demoConfig = getDemoConfig(demoData); - const form = document.createElement('form'); - form.method = 'POST'; - form.target = '_blank'; - form.action = 'https://stackblitz.com/run'; - addHiddenInput(form, 'project[template]', 'javascript'); - addHiddenInput(form, 'project[title]', demoConfig.title); - addHiddenInput(form, 'project[description]', demoConfig.description); - addHiddenInput(form, 'project[dependencies]', JSON.stringify(demoConfig.dependencies)); - addHiddenInput(form, 'project[devDependencies]', JSON.stringify(demoConfig.devDependencies)); - Object.keys(demoConfig.files).forEach((key) => { - const value = demoConfig.files[key]; - addHiddenInput(form, `project[files][${key}]`, value); - }); - document.body.appendChild(form); - form.submit(); - document.body.removeChild(form); - handleMoreClose(); - }; - - const createHandleCodeSourceLink = (anchor) => async () => { - try { - await copy(`${window.location.href.split('#')[0]}#${anchor}`); - setSnackbarMessage(t('copiedSourceLink')); - setSnackbarOpen(true); - } finally { - handleMoreClose(); - } - }; - - const [sourceHintSeen, setSourceHintSeen] = React.useState(false); - React.useEffect(() => { - setSourceHintSeen(getCookie('sourceHintSeen')); - }, []); - const handleCodeOpenClick = () => { - document.cookie = `sourceHintSeen=true;path=/;max-age=31536000`; - onCodeOpenChange(); - setSourceHintSeen(true); - }; - - function handleResetFocusClick() { - initialFocusRef.current.focusVisible(); - } - - const showSourceHint = demoHovered && !sourceHintSeen; - - let showCodeLabel; - if (codeOpen) { - showCodeLabel = showPreview ? t('hideFullSource') : t('hideSource'); - } else { - showCodeLabel = showPreview ? t('showFullSource') : t('showSource'); - } - - const atLeastSmallViewport = useMediaQuery((theme) => theme.breakpoints.up('sm')); - - const controlRefs = [ - React.useRef(null), - React.useRef(null), - React.useRef(null), - React.useRef(null), - React.useRef(null), - React.useRef(null), - React.useRef(null), - React.useRef(null), - ]; - // if the code is not open we hide the first two language controls - const isFocusableControl = React.useCallback((index) => (codeOpen ? true : index >= 2), [ - codeOpen, - ]); - const { getControlProps, toolbarProps } = useToolbar(controlRefs, { - defaultActiveIndex: 2, - isFocusableControl, - }); - - return ( - -
- - - - - - - - - - - -
- - - - - - {demoOptions.hideEditButton ? null : ( - - - - - - )} - - - - - - - - - - - - - - - - - - - - - {t('viewGitHub')} - - {demoOptions.hideEditButton ? null : ( - - {t('stackblitz')} - - )} - - {t('copySourceLinkJS')} - - - {t('copySourceLinkTS')} - - -
-
-
- -
- ); -} - -DemoToolbar.propTypes = { - codeOpen: PropTypes.bool.isRequired, - codeVariant: PropTypes.string.isRequired, - demo: PropTypes.object.isRequired, - demoData: PropTypes.object.isRequired, - demoHovered: PropTypes.bool.isRequired, - demoId: PropTypes.string, - demoName: PropTypes.string.isRequired, - demoOptions: PropTypes.object.isRequired, - demoSourceId: PropTypes.string, - initialFocusRef: PropTypes.shape({ current: PropTypes.object }).isRequired, - onCodeOpenChange: PropTypes.func.isRequired, - onResetDemoClick: PropTypes.func.isRequired, - openDemoSource: PropTypes.bool.isRequired, - showPreview: PropTypes.bool.isRequired, -}; - const useStyles = makeStyles( (theme) => ({ root: { @@ -805,25 +252,29 @@ export default function Demo(props) {
{demoOptions.hideToolbar ? null : ( - { - setCodeOpen((open) => !open); - setShowAd(true); - }} - onResetDemoClick={resetDemo} - openDemoSource={openDemoSource} - showPreview={showPreview} - /> + }> + }> + { + setCodeOpen((open) => !open); + setShowAd(true); + }} + onResetDemoClick={resetDemo} + openDemoSource={openDemoSource} + showPreview={showPreview} + /> + + )}
diff --git a/docs/src/modules/components/DemoToolbar.js b/docs/src/modules/components/DemoToolbar.js new file mode 100644 index 00000000000000..96eaafd5ae54e7 --- /dev/null +++ b/docs/src/modules/components/DemoToolbar.js @@ -0,0 +1,581 @@ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import copy from 'clipboard-copy'; +import LZString from 'lz-string'; +import { useDispatch, useSelector } from 'react-redux'; +import { makeStyles, useTheme } from '@material-ui/core/styles'; +import IconButton from '@material-ui/core/IconButton'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import Fade from '@material-ui/core/Fade'; +import ToggleButton from '@material-ui/core/ToggleButton'; +import ToggleButtonGroup from '@material-ui/core/ToggleButtonGroup'; +import { JavaScript as JavaScriptIcon, TypeScript as TypeScriptIcon } from '@material-ui/docs'; +import EditIcon from '@material-ui/icons/Edit'; +import CodeIcon from '@material-ui/icons/Code'; +import FileCopyIcon from '@material-ui/icons/FileCopy'; +import Snackbar from '@material-ui/core/Snackbar'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import Tooltip from '@material-ui/core/Tooltip'; +import RefreshIcon from '@material-ui/icons/Refresh'; +import ResetFocusIcon from '@material-ui/icons/CenterFocusWeak'; +import getDemoConfig from 'docs/src/modules/utils/getDemoConfig'; +import { getCookie } from 'docs/src/modules/utils/helpers'; +import { ACTION_TYPES, CODE_VARIANTS } from 'docs/src/modules/constants'; + +function compress(object) { + return LZString.compressToBase64(JSON.stringify(object)) + .replace(/\+/g, '-') // Convert '+' to '-' + .replace(/\//g, '_') // Convert '/' to '_' + .replace(/=+$/, ''); // Remove ending '=' +} + +function addHiddenInput(form, name, value) { + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = name; + input.value = value; + form.appendChild(input); +} + +const useDemoToolbarStyles = makeStyles( + (theme) => { + return { + // Sync with styles form DemoToolbarFallback. + root: { + display: 'none', + [theme.breakpoints.up('sm')]: { + display: 'flex', + flip: false, + top: 0, + right: theme.spacing(1), + height: theme.spacing(6), + }, + justifyContent: 'space-between', + }, + toggleButtonGroup: { + margin: '8px 0', + }, + toggleButton: { + padding: '4px 9px', + }, + tooltip: { + zIndex: theme.zIndex.appBar - 1, + }, + }; + }, + { name: 'DemoToolbar' }, +); + +export function DemoToolbarFallback() { + const classes = useDemoToolbarStyles(); + const t = useSelector((state) => state.options.t); + + return ( +
+ ); +} + +const alwaysTrue = () => true; + +/** + * @param {React.Ref[]} controlRefs + * @param {object} [options] + * @param {(index: number) => boolean} [options.isFocusableControl] In case certain controls become unfocusable + * @param {number} [options.defaultActiveIndex] + */ +function useToolbar(controlRefs, options = {}) { + const { defaultActiveIndex = 0, isFocusableControl = alwaysTrue } = options; + const [activeControlIndex, setActiveControlIndex] = React.useState(defaultActiveIndex); + + // TODO: do we need to do this during layout practically? It's technically + // a bit too late since we allow user interaction between layout and passive effects + React.useEffect(() => { + setActiveControlIndex((currentActiveControlIndex) => { + if (!isFocusableControl(currentActiveControlIndex)) { + return defaultActiveIndex; + } + return currentActiveControlIndex; + }); + }, [defaultActiveIndex, isFocusableControl]); + + // controlRefs.findIndex(controlRef => controlRef.current = element) + function findControlIndex(element) { + let controlIndex = -1; + controlRefs.forEach((controlRef, index) => { + if (controlRef.current === element) { + controlIndex = index; + } + }); + return controlIndex; + } + + function handleControlFocus(event) { + const nextActiveControlIndex = findControlIndex(event.target); + if (nextActiveControlIndex !== -1) { + setActiveControlIndex(nextActiveControlIndex); + } else { + // make sure DCE works + // eslint-disable-next-line no-lonely-if + if (process.env.NODE_ENV !== 'production') { + console.error( + 'Material-UI: The toolbar contains a focusable element that is not controlled by the toolbar. ' + + 'Make sure you have attached `getControlProps(index)` to every focusable element within this toolbar.', + ); + } + } + } + + let handleToolbarFocus; + if (process.env.NODE_ENV !== 'production') { + handleToolbarFocus = (event) => { + if (findControlIndex(event.target) === -1) { + console.error( + 'Material-UI: The toolbar contains a focusable element that is not controlled by the toolbar. ' + + 'Make sure you have attached `getControlProps(index)` to every focusable element within this toolbar.', + ); + } + }; + } + + const { direction } = useTheme(); + + function handleToolbarKeyDown(event) { + // We handle toolbars where controls can be hidden temporarily. + // When a control is hidden we can't move focus to it and have to exclude + // it from the order. + let currentFocusableControlIndex = -1; + const focusableControls = []; + controlRefs.forEach((controlRef, index) => { + const { current: control } = controlRef; + if (index === activeControlIndex) { + currentFocusableControlIndex = focusableControls.length; + } + if (control !== null && isFocusableControl(index)) { + focusableControls.push(control); + } + }); + + const prevControlKey = direction === 'ltr' ? 'ArrowLeft' : 'ArrowRight'; + const nextControlKey = direction === 'ltr' ? 'ArrowRight' : 'ArrowLeft'; + + let nextFocusableIndex = -1; + switch (event.key) { + case prevControlKey: + nextFocusableIndex = + (currentFocusableControlIndex - 1 + focusableControls.length) % focusableControls.length; + break; + case nextControlKey: + nextFocusableIndex = (currentFocusableControlIndex + 1) % focusableControls.length; + break; + case 'Home': + nextFocusableIndex = 0; + break; + case 'End': + nextFocusableIndex = focusableControls.length - 1; + break; + default: + break; + } + + if (nextFocusableIndex !== -1) { + event.preventDefault(); + focusableControls[nextFocusableIndex].focus(); + } + } + + function getControlProps(index) { + return { + onFocus: handleControlFocus, + ref: controlRefs[index], + tabIndex: index === activeControlIndex ? 0 : -1, + }; + } + + return { + getControlProps, + toolbarProps: { + // TODO: good opportunity to warn on missing `aria-label` + onFocus: handleToolbarFocus, + onKeyDown: handleToolbarKeyDown, + role: 'toolbar', + }, + }; +} + +export default function DemoToolbar(props) { + const { + codeOpen, + codeVariant, + demo, + demoData, + demoId, + demoHovered, + demoName, + demoOptions, + demoSourceId, + initialFocusRef, + onCodeOpenChange, + onResetDemoClick, + openDemoSource, + showPreview, + } = props; + + const classes = useDemoToolbarStyles(); + + const dispatch = useDispatch(); + const t = useSelector((state) => state.options.t); + + const hasTSVariant = demo.rawTS; + const renderedCodeVariant = () => { + if (codeVariant === CODE_VARIANTS.TS && hasTSVariant) { + return CODE_VARIANTS.TS; + } + return CODE_VARIANTS.JS; + }; + + const handleCodeLanguageClick = (event, clickedCodeVariant) => { + if (codeVariant !== clickedCodeVariant) { + dispatch({ + type: ACTION_TYPES.OPTIONS_CHANGE, + payload: { + codeVariant: clickedCodeVariant, + }, + }); + } + }; + + const handleCodeSandboxClick = () => { + const demoConfig = getDemoConfig(demoData); + const parameters = compress({ + files: { + 'package.json': { + content: { + name: demoConfig.title, + description: demoConfig.description, + dependencies: demoConfig.dependencies, + devDependencies: { + 'react-scripts': 'latest', + ...demoConfig.devDependencies, + }, + main: demoConfig.main, + scripts: demoConfig.scripts, + // We used `title` previously but only inference from `name` is documented. + // TODO revisit once https://github.com/codesandbox/codesandbox-client/issues/4983 is resolved. + title: demoConfig.title, + }, + }, + ...Object.keys(demoConfig.files).reduce((files, name) => { + files[name] = { content: demoConfig.files[name] }; + return files; + }, {}), + }, + }); + + const form = document.createElement('form'); + form.method = 'POST'; + form.target = '_blank'; + form.action = 'https://codeSandbox.io/api/v1/sandboxes/define'; + addHiddenInput(form, 'parameters', parameters); + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); + }; + + const [anchorEl, setAnchorEl] = React.useState(null); + const handleMoreClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleMoreClose = () => { + setAnchorEl(null); + }; + + const [snackbarOpen, setSnackbarOpen] = React.useState(false); + const [snackbarMessage, setSnackbarMessage] = React.useState(undefined); + + const handleSnackbarClose = () => { + setSnackbarOpen(false); + }; + const handleCopyClick = async () => { + try { + await copy(demoData.raw); + setSnackbarMessage(t('copiedSource')); + setSnackbarOpen(true); + } finally { + handleMoreClose(); + } + }; + + const handleStackBlitzClick = () => { + const demoConfig = getDemoConfig(demoData); + const form = document.createElement('form'); + form.method = 'POST'; + form.target = '_blank'; + form.action = 'https://stackblitz.com/run'; + addHiddenInput(form, 'project[template]', 'javascript'); + addHiddenInput(form, 'project[title]', demoConfig.title); + addHiddenInput(form, 'project[description]', demoConfig.description); + addHiddenInput(form, 'project[dependencies]', JSON.stringify(demoConfig.dependencies)); + addHiddenInput(form, 'project[devDependencies]', JSON.stringify(demoConfig.devDependencies)); + Object.keys(demoConfig.files).forEach((key) => { + const value = demoConfig.files[key]; + addHiddenInput(form, `project[files][${key}]`, value); + }); + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); + handleMoreClose(); + }; + + const createHandleCodeSourceLink = (anchor) => async () => { + try { + await copy(`${window.location.href.split('#')[0]}#${anchor}`); + setSnackbarMessage(t('copiedSourceLink')); + setSnackbarOpen(true); + } finally { + handleMoreClose(); + } + }; + + const [sourceHintSeen, setSourceHintSeen] = React.useState(false); + React.useEffect(() => { + setSourceHintSeen(getCookie('sourceHintSeen')); + }, []); + const handleCodeOpenClick = () => { + document.cookie = `sourceHintSeen=true;path=/;max-age=31536000`; + onCodeOpenChange(); + setSourceHintSeen(true); + }; + + function handleResetFocusClick() { + initialFocusRef.current.focusVisible(); + } + + const showSourceHint = demoHovered && !sourceHintSeen; + + let showCodeLabel; + if (codeOpen) { + showCodeLabel = showPreview ? t('hideFullSource') : t('hideSource'); + } else { + showCodeLabel = showPreview ? t('showFullSource') : t('showSource'); + } + + const atLeastSmallViewport = useMediaQuery((theme) => theme.breakpoints.up('sm')); + + const controlRefs = [ + React.useRef(null), + React.useRef(null), + React.useRef(null), + React.useRef(null), + React.useRef(null), + React.useRef(null), + React.useRef(null), + React.useRef(null), + ]; + // if the code is not open we hide the first two language controls + const isFocusableControl = React.useCallback((index) => (codeOpen ? true : index >= 2), [ + codeOpen, + ]); + const { getControlProps, toolbarProps } = useToolbar(controlRefs, { + defaultActiveIndex: 2, + isFocusableControl, + }); + + return ( + +
+ + + + + + + + + + +
+ + + + + + {demoOptions.hideEditButton ? null : ( + + + + + + )} + + + + + + + + + + + + + + + + + + + + + {t('viewGitHub')} + + {demoOptions.hideEditButton ? null : ( + + {t('stackblitz')} + + )} + + {t('copySourceLinkJS')} + + + {t('copySourceLinkTS')} + + +
+
+ +
+ ); +} + +DemoToolbar.propTypes = { + codeOpen: PropTypes.bool.isRequired, + codeVariant: PropTypes.string.isRequired, + demo: PropTypes.object.isRequired, + demoData: PropTypes.object.isRequired, + demoHovered: PropTypes.bool.isRequired, + demoId: PropTypes.string, + demoName: PropTypes.string.isRequired, + demoOptions: PropTypes.object.isRequired, + demoSourceId: PropTypes.string, + initialFocusRef: PropTypes.shape({ current: PropTypes.object }).isRequired, + onCodeOpenChange: PropTypes.func.isRequired, + onResetDemoClick: PropTypes.func.isRequired, + openDemoSource: PropTypes.bool.isRequired, + showPreview: PropTypes.bool.isRequired, +}; diff --git a/docs/src/modules/components/HighlightedCode.js b/docs/src/modules/components/HighlightedCode.js index 900ceb15d91c67..4799e1ad37cb2b 100644 --- a/docs/src/modules/components/HighlightedCode.js +++ b/docs/src/modules/components/HighlightedCode.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as PropTypes from 'prop-types'; +import PropTypes from 'prop-types'; import prism from 'docs/src/modules/utils/prism'; import MarkdownElement from './MarkdownElement'; diff --git a/docs/src/modules/components/MarkdownDocs.js b/docs/src/modules/components/MarkdownDocs.js index 3ffc59da2a6923..1b145c2d59c618 100644 --- a/docs/src/modules/components/MarkdownDocs.js +++ b/docs/src/modules/components/MarkdownDocs.js @@ -1,20 +1,14 @@ import * as React from 'react'; -import * as PropTypes from 'prop-types'; +import PropTypes from 'prop-types'; import clsx from 'clsx'; import { useSelector } from 'react-redux'; import { withStyles } from '@material-ui/core/styles'; -import ChevronRightIcon from '@material-ui/icons/ChevronRight'; -import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; -import Button from '@material-ui/core/Button'; -import Divider from '@material-ui/core/Divider'; +import NoSsr from '@material-ui/core/NoSsr'; +import { exactProp } from '@material-ui/utils'; import Head from 'docs/src/modules/components/Head'; import AppFrame from 'docs/src/modules/components/AppFrame'; import EditPage from 'docs/src/modules/components/EditPage'; import AppContainer from 'docs/src/modules/components/AppContainer'; -import PageContext from 'docs/src/modules/components/PageContext'; -import { pageToTitleI18n } from 'docs/src/modules/utils/helpers'; -import Link from 'docs/src/modules/components/Link'; -import { exactProp } from '@material-ui/utils'; import { SOURCE_CODE_ROOT_URL } from 'docs/src/modules/constants'; import Demo from 'docs/src/modules/components/Demo'; import AppTableOfContents from 'docs/src/modules/components/AppTableOfContents'; @@ -23,33 +17,12 @@ import Ad from 'docs/src/modules/components/Ad'; import AdManager from 'docs/src/modules/components/AdManager'; import AdGuest from 'docs/src/modules/components/AdGuest'; import ComponentLinkHeader from 'docs/src/modules/components/ComponentLinkHeader'; +import MarkdownDocsFooter from './MarkdownDocsFooter'; const markdownComponents = { 'modules/components/ComponentLinkHeader.js': ComponentLinkHeader, }; -function flattenPages(pages, current = []) { - return pages.reduce((items, item) => { - if (item.children && item.children.length > 1) { - items = flattenPages(item.children, items); - } else { - items.push(item.children && item.children.length === 1 ? item.children[0] : item); - } - return items; - }, current); -} - -// To replace with .findIndex() once we stop IE11 support. -function findIndex(array, comp) { - for (let i = 0; i < array.length; i += 1) { - if (comp(array[i])) { - return i; - } - } - - return -1; -} - const styles = (theme) => ({ root: { width: '100%', @@ -80,18 +53,6 @@ const styles = (theme) => ({ width: 'calc(100% - 175px - 240px)', }, }, - footer: { - marginTop: theme.spacing(12), - }, - pagination: { - margin: theme.spacing(3, 0, 4), - display: 'flex', - justifyContent: 'space-between', - }, - pageLinkButton: { - textTransform: 'none', - fontWeight: theme.typography.fontWeightRegular, - }, }); function MarkdownDocs(props) { @@ -104,13 +65,6 @@ function MarkdownDocs(props) { throw new Error('Missing description in the page'); } - const { activePage, pages } = React.useContext(PageContext); - const pageList = flattenPages(pages); - const currentPageNum = findIndex(pageList, (page) => page.pathname === activePage?.pathname); - const currentPage = pageList[currentPageNum]; - const prevPage = pageList[currentPageNum - 1]; - const nextPage = pageList[currentPageNum + 1]; - return ( @@ -184,43 +138,9 @@ function MarkdownDocs(props) { /> ); })} -
- {!currentPage || - currentPage.displayNav === false || - (nextPage.displayNav === false && !prevPage) ? null : ( - - -
- {prevPage ? ( - - ) : ( -
- )} - {nextPage.displayNav === false ? null : ( - - )} -
- - )} -
+ + +
{disableToc ? null : } diff --git a/docs/src/modules/components/MarkdownDocsFooter.js b/docs/src/modules/components/MarkdownDocsFooter.js new file mode 100644 index 00000000000000..e05abbc625c520 --- /dev/null +++ b/docs/src/modules/components/MarkdownDocsFooter.js @@ -0,0 +1,323 @@ +/* eslint-disable no-restricted-globals */ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { exactProp } from '@material-ui/utils'; +import { withStyles } from '@material-ui/core/styles'; +import DialogActions from '@material-ui/core/DialogActions'; +import TextField from '@material-ui/core/TextField'; +import Collapse from '@material-ui/core/Collapse'; +import Button from '@material-ui/core/Button'; +import Divider from '@material-ui/core/Divider'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import Tooltip from '@material-ui/core/Tooltip'; +import IconButton from '@material-ui/core/IconButton'; +import ThumbUpIcon from '@material-ui/icons/ThumbUpAlt'; +import ThumbDownIcon from '@material-ui/icons/ThumbDownAlt'; +import ChevronRightIcon from '@material-ui/icons/ChevronRight'; +import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; +import Snackbar from '@material-ui/core/Snackbar'; +import { getCookie, pageToTitleI18n } from 'docs/src/modules/utils/helpers'; +import PageContext from 'docs/src/modules/components/PageContext'; +import Link from 'docs/src/modules/components/Link'; + +const styles = (theme) => ({ + root: { + marginTop: theme.spacing(12), + }, + pagination: { + margin: theme.spacing(3, 0, 4), + display: 'flex', + justifyContent: 'space-between', + [theme.breakpoints.down('sm')]: { + flexWrap: 'wrap', + }, + }, + pageLinkButton: { + textTransform: 'none', + fontWeight: theme.typography.fontWeightRegular, + }, + feedbackMessage: { + margin: theme.spacing(0, 2), + }, + feedback: { + width: 'auto', + [theme.breakpoints.down('sm')]: { + order: 3, + width: '100%', + }, + }, + hidden: { + ariaHidden: 'true', + opacity: 0, + }, +}); + +function flattenPages(pages, current = []) { + return pages.reduce((items, item) => { + if (item.children && item.children.length > 1) { + items = flattenPages(item.children, items); + } else { + items.push(item.children && item.children.length === 1 ? item.children[0] : item); + } + return items; + }, current); +} + +// To replace with .findIndex() once we stop IE11 support. +function findIndex(array, comp) { + for (let i = 0; i < array.length; i += 1) { + if (comp(array[i])) { + return i; + } + } + + return -1; +} + +async function postFeedback(data) { + const env = window.location.host.indexOf('material-ui.com') !== -1 ? 'prod' : 'dev'; + try { + const response = await fetch(`${process.env.FEEDBACK_URL}/${env}/feedback`, { + method: 'POST', + referrerPolicy: 'origin', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + return response.json(); + } catch (error) { + console.error(error); + return null; + } +} + +async function getUserFeedback(id) { + const env = location.hostname === 'material-ui.com' ? 'prod' : 'dev'; + const URL = `${process.env.FEEDBACK_URL}/${env}/feedback/${id}`; + + try { + const response = await fetch(URL, { + method: 'GET', + cache: 'no-store', + referrerPolicy: 'origin', + }); + return response.json(); + } catch (error) { + console.error(error); + return null; + } +} + +async function submitFeedback(page, rating, comment, language) { + const data = { + id: getCookie('feedbackId'), + page, + rating, + comment, + version: process.env.LIB_VERSION, + language, + }; + + const result = await postFeedback(data); + if (result) { + document.cookie = `feedbackId=${result.id};path=/;max-age=31536000`; + setTimeout(async () => { + const userFeedback = await getUserFeedback(result.id); + if (userFeedback) { + document.cookie = `feedback=${JSON.stringify(userFeedback)};path=/;max-age=31536000`; + } + }); + } + return result; +} + +function getCurrentRating(pathname) { + let userFeedback; + if (process.browser) { + userFeedback = getCookie('feedback'); + userFeedback = userFeedback && JSON.parse(userFeedback); + } + return userFeedback && userFeedback[pathname] && userFeedback[pathname].rating; +} + +function MarkdownDocsFooter(props) { + const { classes } = props; + const t = useSelector((state) => state.options.t); + const userLanguage = useSelector((state) => state.options.userLanguage); + const { activePage, pages } = React.useContext(PageContext); + const [rating, setRating] = React.useState(); + const [comment, setComment] = React.useState(''); + const [commentOpen, setCommentOpen] = React.useState(false); + const [snackbarOpen, setSnackbarOpen] = React.useState(false); + const [snackbarMessage, setSnackbarMessage] = React.useState(false); + const inputRef = React.useRef(); + const bottomRef = React.useRef(); + const pageList = flattenPages(pages); + const currentPageNum = findIndex(pageList, (page) => page.pathname === activePage?.pathname); + const currentPage = pageList[currentPageNum]; + const prevPage = pageList[currentPageNum - 1]; + const nextPage = pageList[currentPageNum + 1]; + + const setCurrentRatingFromCookie = React.useCallback(() => { + setRating(getCurrentRating(currentPage.pathname)); + }, [currentPage.pathname]); + + React.useEffect(() => { + setCurrentRatingFromCookie(); + }, [setCurrentRatingFromCookie]); + + async function processFeedback() { + const result = await submitFeedback(currentPage.pathname, rating, comment, userLanguage); + if (result) { + setSnackbarMessage(t('feedbackSubmitted')); + } else { + setCurrentRatingFromCookie(); + setSnackbarMessage(t('feedbackFailed')); + } + setSnackbarOpen(true); + } + + const handleClickThumb = (vote) => async () => { + if (vote !== rating) { + setRating(vote); + // Focus a hidden element at the bottom of the page + // so that the texfield is visible when it opens. + bottomRef.current.focus(); + setCommentOpen(true); + } + }; + + const handleChangeTextfield = (event) => { + setComment(event.target.value); + }; + + const handleSubmitComment = () => { + setCommentOpen(false); + processFeedback(); + }; + + const handleCancelComment = () => { + setCommentOpen(false); + setCurrentRatingFromCookie(); + }; + + const handleEntered = () => { + inputRef.current.focus(); + }; + + const handleCloseSnackbar = () => { + setSnackbarOpen(false); + }; + + return ( + +
+ {!currentPage || + currentPage.displayNav === false || + (nextPage.displayNav === false && !prevPage) ? null : ( + + +
+ {prevPage ? ( + + ) : ( +
+ )} + + + {t('feedbackMessage')} + +
+ + + + + + + + + + +
+
+ {nextPage.displayNav === false ? null : ( + + )} +
+ + )} + +
+ + {t('feedbackTitle')} + +
+ + {rating === 1 ? t('feedbackMessageUp') : t('feedbackMessageDown')} + + +
+ + + + +
+
+
+ + +
+ ); +} + +MarkdownDocsFooter.propTypes = { + classes: PropTypes.object.isRequired, +}; + +if (process.env.NODE_ENV !== 'production') { + MarkdownDocsFooter.propTypes = exactProp(MarkdownDocsFooter.propTypes); +} + +export default withStyles(styles)(MarkdownDocsFooter); diff --git a/docs/src/modules/utils/helpers.js b/docs/src/modules/utils/helpers.js index d4dbb1c7e66eed..7bde646e578880 100644 --- a/docs/src/modules/utils/helpers.js +++ b/docs/src/modules/utils/helpers.js @@ -175,9 +175,17 @@ function getDependencies(raw, options = {}) { return deps; } +/** + * Get the value of a cookie + * Source: https://vanillajstoolkit.com/helpers/getcookie/ + * @param {String} name The name of the cookie + * @return {String} The cookie value + */ function getCookie(name) { - const regex = new RegExp(`(?:(?:^|.*;*)${name}*=*([^;]*).*$)|^.*$`); - return document.cookie.replace(regex, '$1'); + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + return undefined; } function pathnameToLanguage(pathname) { diff --git a/docs/src/pages/components/avatars/GroupAvatars.js b/docs/src/pages/components/avatars/GroupAvatars.js index 84e6b1f53bed9c..8c57f8953ca1e8 100644 --- a/docs/src/pages/components/avatars/GroupAvatars.js +++ b/docs/src/pages/components/avatars/GroupAvatars.js @@ -1,6 +1,6 @@ import * as React from 'react'; import Avatar from '@material-ui/core/Avatar'; -import AvatarGroup from '@material-ui/lab/AvatarGroup'; +import AvatarGroup from '@material-ui/core/AvatarGroup'; export default function GroupAvatars() { return ( diff --git a/docs/src/pages/components/avatars/GroupAvatars.tsx b/docs/src/pages/components/avatars/GroupAvatars.tsx index 84e6b1f53bed9c..8c57f8953ca1e8 100644 --- a/docs/src/pages/components/avatars/GroupAvatars.tsx +++ b/docs/src/pages/components/avatars/GroupAvatars.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import Avatar from '@material-ui/core/Avatar'; -import AvatarGroup from '@material-ui/lab/AvatarGroup'; +import AvatarGroup from '@material-ui/core/AvatarGroup'; export default function GroupAvatars() { return ( diff --git a/docs/src/pages/components/buttons/ButtonSizes.js b/docs/src/pages/components/buttons/ButtonSizes.js index 2e98517f054151..6b2a07184b6d05 100644 --- a/docs/src/pages/components/buttons/ButtonSizes.js +++ b/docs/src/pages/components/buttons/ButtonSizes.js @@ -9,9 +9,6 @@ const useStyles = makeStyles((theme) => ({ margin: { margin: theme.spacing(1), }, - extendedIcon: { - marginRight: theme.spacing(1), - }, })); export default function ButtonSizes() { diff --git a/docs/src/pages/components/buttons/ButtonSizes.tsx b/docs/src/pages/components/buttons/ButtonSizes.tsx index 4a3dad5b091bc8..eea201ba5ef3b6 100644 --- a/docs/src/pages/components/buttons/ButtonSizes.tsx +++ b/docs/src/pages/components/buttons/ButtonSizes.tsx @@ -10,9 +10,6 @@ const useStyles = makeStyles((theme: Theme) => margin: { margin: theme.spacing(1), }, - extendedIcon: { - marginRight: theme.spacing(1), - }, }), ); diff --git a/docs/src/pages/components/slider-styled/NonLinearSlider.js b/docs/src/pages/components/slider-styled/NonLinearSlider.js index 332d71420b0a81..66ca18f3c46239 100644 --- a/docs/src/pages/components/slider-styled/NonLinearSlider.js +++ b/docs/src/pages/components/slider-styled/NonLinearSlider.js @@ -1,33 +1,54 @@ import * as React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; -import Slider from '@material-ui/lab/SliderStyled'; +import Slider from '@material-ui/core/Slider'; + +const useStyles = makeStyles({ + root: { + width: 250, + }, +}); function valueLabelFormat(value) { - const [coefficient, exponent] = value - .toExponential() - .split('e') - .map((item) => Number(item)); - return `${Math.round(coefficient)}e^${exponent}`; + const units = ['KB', 'MB', 'GB', 'TB']; + + let unitIndex = 0; + let scaledValue = value; + + while (scaledValue >= 1024 && unitIndex < units.length - 1) { + unitIndex += 1; + scaledValue /= 1024; + } + + return `${scaledValue} ${units[unitIndex]}`; +} + +function calculateValue(value) { + return 2 ** value; } export default function NonLinearSlider() { - const [value, setValue] = React.useState(1); + const [value, setValue] = React.useState(10); const handleChange = (event, newValue) => { - setValue(newValue); + if (typeof newValue === 'number') { + setValue(newValue); + } }; + const classes = useStyles(); + return ( -
+
- Temperature range + Storage: {valueLabelFormat(calculateValue(value))} x ** 10} + min={5} + step={1} + max={30} + scale={calculateValue} getAriaValueText={valueLabelFormat} valueLabelFormat={valueLabelFormat} onChange={handleChange} diff --git a/docs/src/pages/components/slider-styled/NonLinearSlider.tsx b/docs/src/pages/components/slider-styled/NonLinearSlider.tsx index 5700e72cae4b3f..e257a6bef178ea 100644 --- a/docs/src/pages/components/slider-styled/NonLinearSlider.tsx +++ b/docs/src/pages/components/slider-styled/NonLinearSlider.tsx @@ -1,36 +1,57 @@ import * as React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; -import Slider from '@material-ui/lab/SliderStyled'; +import Slider from '@material-ui/core/Slider'; + +const useStyles = makeStyles({ + root: { + width: 250, + }, +}); function valueLabelFormat(value: number) { - const [coefficient, exponent] = value - .toExponential() - .split('e') - .map((item) => Number(item)); - return `${Math.round(coefficient)}e^${exponent}`; + const units = ['KB', 'MB', 'GB', 'TB']; + + let unitIndex = 0; + let scaledValue = value; + + while (scaledValue >= 1024 && unitIndex < units.length - 1) { + unitIndex += 1; + scaledValue /= 1024; + } + + return `${scaledValue} ${units[unitIndex]}`; +} + +function calculateValue(value: number) { + return 2 ** value; } export default function NonLinearSlider() { - const [value, setValue] = React.useState(1); + const [value, setValue] = React.useState(10); const handleChange = ( event: React.SyntheticEvent, newValue: number | number[], ) => { - setValue(newValue); + if (typeof newValue === 'number') { + setValue(newValue); + } }; + const classes = useStyles(); + return ( -
+
- Temperature range + Storage: {valueLabelFormat(calculateValue(value))} x ** 10} + min={5} + step={1} + max={30} + scale={calculateValue} getAriaValueText={valueLabelFormat} valueLabelFormat={valueLabelFormat} onChange={handleChange} diff --git a/docs/src/pages/components/slider-styled/slider-styled.md b/docs/src/pages/components/slider-styled/slider-styled.md index 2da24ea7f87212..e7affcbff6b6cd 100644 --- a/docs/src/pages/components/slider-styled/slider-styled.md +++ b/docs/src/pages/components/slider-styled/slider-styled.md @@ -96,7 +96,9 @@ The track can be inverted with `track="inverted"`. ## Non-linear scale You can use the `scale` prop to represent the `value` on a different scale. -For instance, in the following demo, the value _x_ represents the power of _10^x_. + +In the following demo, the value _x_ represents the value _2^x_. +Increasing _x_ by one increases the represented value by factor _2_. {{"demo": "pages/components/slider-styled/NonLinearSlider.js"}} diff --git a/docs/src/pages/components/steppers/steppers.md b/docs/src/pages/components/steppers/steppers.md index 8d7585c52f841b..34004243f6c090 100644 --- a/docs/src/pages/components/steppers/steppers.md +++ b/docs/src/pages/components/steppers/steppers.md @@ -72,7 +72,7 @@ Vertical steppers are designed for narrow screen sizes. They are ideal for mobil ## Mobile stepper -This component implements a compact stepper suitable for a mobile device. IT has more limited functionality than the vertical stepper. See [mobile steps](https://material.io/archive/guidelines/components/steppers.html#steppers-types-of-steps) for its inspiration. +This component implements a compact stepper suitable for a mobile device. It has more limited functionality than the vertical stepper. See [mobile steps](https://material.io/archive/guidelines/components/steppers.html#steppers-types-of-steps) for its inspiration. The mobile stepper supports three variants to display progress through the available steps: text, dots, and progress. diff --git a/docs/src/pages/discover-more/showcase/appList.js b/docs/src/pages/discover-more/showcase/appList.js index ad63178c7bb5ee..181b5041f89e19 100644 --- a/docs/src/pages/discover-more/showcase/appList.js +++ b/docs/src/pages/discover-more/showcase/appList.js @@ -529,7 +529,7 @@ const appList = [ }, { title: 'Swimmy', - description: 'An open source forum PWA. 🇯🇵 (Github docs are in English)', + description: 'An open source forum PWA. 🇯🇵 (GitHub docs are in English)', image: 'swimmy.jpg', link: 'https://swimmy.io/', source: 'https://github.com/swimmy/swimmy.io', diff --git a/docs/src/pages/getting-started/example-projects/example-projects.md b/docs/src/pages/getting-started/example-projects/example-projects.md index 2edc3cdeb34794..be60576c87fce4 100644 --- a/docs/src/pages/getting-started/example-projects/example-projects.md +++ b/docs/src/pages/getting-started/example-projects/example-projects.md @@ -14,7 +14,6 @@ You can find some example projects in the [GitHub repository](https://github.com - [CDN](https://github.com/mui-org/material-ui/tree/next/examples/cdn) - [Plain server-side](https://github.com/mui-org/material-ui/tree/next/examples/ssr) - [Use styled-components as style engine](https://github.com/mui-org/material-ui/tree/next/examples/create-react-app-with-styled-components) -- [Reason React](https://github.com/mui-org/material-ui/tree/next/examples/reason) Create React App is an awesome project for learning React. Have a look at [the alternatives available](https://github.com/facebook/create-react-app/blob/master/README.md#popular-alternatives) to see which project best fits your needs. diff --git a/docs/src/pages/getting-started/supported-platforms/supported-platforms.md b/docs/src/pages/getting-started/supported-platforms/supported-platforms.md index 95bc05e9eeccd5..aafe19b450d3a2 100644 --- a/docs/src/pages/getting-started/supported-platforms/supported-platforms.md +++ b/docs/src/pages/getting-started/supported-platforms/supported-platforms.md @@ -27,7 +27,7 @@ You can expect Material-UI's components to render without major issues. We support [Node.js](https://github.com/nodejs/node) starting with version 10 for server-side rendering. -Where possible, the [LTS versions that are in maintenance](https://github.com/nodejs/Release#lts-schedule1) are supported. +Where possible, the [LTS versions that are in maintenance](https://github.com/nodejs/Release#release-schedule) are supported. ### CSS prefixing diff --git a/docs/src/pages/guides/migration-v4/migration-v4.md b/docs/src/pages/guides/migration-v4/migration-v4.md index afe63aea2dd5c2..f2c54deab493f6 100644 --- a/docs/src/pages/guides/migration-v4/migration-v4.md +++ b/docs/src/pages/guides/migration-v4/migration-v4.md @@ -263,6 +263,13 @@ const classes = makeStyles(theme => ({ + ``` +- Move the AvatarGroup from the lab to the core. + + ```diff + -import AvatarGroup from '@material-ui/lab/AvatarGroup'; + +import AvatarGroup from '@material-ui/core/AvatarGroup'; + ``` + ### Badge - Rename `circle` to `circular` and `rectangle` to `rectangular` for consistency. The possible values should be adjectives, not nouns: diff --git a/docs/src/pages/guides/server-rendering/server-rendering.md b/docs/src/pages/guides/server-rendering/server-rendering.md index 674c4ddbb88ef9..99c8a98c74d907 100644 --- a/docs/src/pages/guides/server-rendering/server-rendering.md +++ b/docs/src/pages/guides/server-rendering/server-rendering.md @@ -92,6 +92,20 @@ inside a [`StylesProvider`](/styles/api/#stylesprovider) and [`ThemeProvider`](/ The key step in server-side rendering is to render the initial HTML of the component **before** we send it to the client side. To do this, we use [ReactDOMServer.renderToString()](https://reactjs.org/docs/react-dom-server.html). We then get the CSS from the `sheets` using `sheets.toString()`. +As we are also using emotion as our default styled engine, we need to extract the styles from the emotion instance as well. For this we need to share the same cache definition for both the client and server: + +`cache.js` + +```js +import createCache from '@emotion/cache'; + +const cache = createCache(); + +export default cache; +``` + +With this we are creating new Emotion server instance and using this to extract the critical styles for the html as well. + We will see how this is passed along in the `renderFullPage` function. ```jsx @@ -99,8 +113,12 @@ import express from 'express'; import * as React from 'react'; import ReactDOMServer from 'react-dom/server'; import { ServerStyleSheets, ThemeProvider } from '@material-ui/core/styles'; +import createEmotionServer from 'create-emotion-server'; import App from './App'; import theme from './theme'; +import cache from './cache'; + +const { extractCritical } = createEmotionServer(cache); function handleRender(req, res) { const sheets = new ServerStyleSheets(); @@ -108,17 +126,22 @@ function handleRender(req, res) { // Render the component to a string. const html = ReactDOMServer.renderToString( sheets.collect( - - - , + + + + + , ), ); // Grab the CSS from the sheets. const css = sheets.toString(); + // Grab the CSS from emotion + const styles = extractCritical(html); + // Send the rendered page back to the client. - res.send(renderFullPage(html, css)); + res.send(renderFullPage(html, `${css} ${styles.css}`)); } const app = express(); @@ -164,8 +187,10 @@ Let's take a look at the client file: import * as React from 'react'; import ReactDOM from 'react-dom'; import { ThemeProvider } from '@material-ui/core/styles'; +import { CacheProvider } from '@emotion/core'; import App from './App'; import theme from './theme'; +import cache from './cache'; function Main() { React.useEffect(() => { @@ -176,9 +201,11 @@ function Main() { }, []); return ( - - - + + + + + ); } diff --git a/docs/src/pages/landing/Sponsors.js b/docs/src/pages/landing/Sponsors.js index 429ec4af386b48..aecc73f86fc156 100644 --- a/docs/src/pages/landing/Sponsors.js +++ b/docs/src/pages/landing/Sponsors.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as PropTypes from 'prop-types'; +import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { makeStyles } from '@material-ui/core/styles'; import NoSsr from '@material-ui/core/NoSsr'; diff --git a/docs/translations/translations.json b/docs/translations/translations.json index 014401590d6d36..cf1a2ee036278e 100644 --- a/docs/translations/translations.json +++ b/docs/translations/translations.json @@ -7,6 +7,7 @@ "blogTitle": "Blog", "bundleSize": "Bundle size", "bundleSizeTooltip": "Scroll down to 'Exports Analysis' for a more detailed report.", + "cancel": "Cancel", "cdn": "or use a CDN.", "changeLanguage": "Change language", "checkoutDescr": "A step-by-step checkout page layout. Adapt the number of steps to suit your needs, or make steps optional.", @@ -28,6 +29,15 @@ "emojiLove": "Love", "emoojiWarning": "Warning", "expandAll": "Expand all", + "feedbackFailed": "Couldn't submit feedback. Please try again later.", + "feedbackGroupLabel": "give page feedback", + "feedbackMessage": "Was this page helpful?", + "feedbackMessageDown": "Please let us know what we could do to improve this page.", + "feedbackMessageUp": "Please let us know what we should keep doing more of.", + "feedbackNo": "No", + "feedbackSubmitted": "Feedback submitted", + "feedbackTitle": "Thanks for your feedback!", + "feedbackYes": "Yes", "footerCommunity": "Community", "footerCompany": "Company", "footerResources": "Resources", @@ -81,6 +91,7 @@ "stickyFooterDescr": "Attach a footer to the bottom of the viewport when page content is short.", "stickyFooterTitle": "Sticky footer", "strapline": "React components for faster and simpler web development. Build your own design system, or start with Material Design.", + "submit": "Submit", "tableOfContents": "Contents", "thanks": "Thank you!", "themes": "Premium themes", diff --git a/examples/cdn/index.html b/examples/cdn/index.html index 0d3cf789d198ba..350dcd0db3bdb3 100644 --- a/examples/cdn/index.html +++ b/examples/cdn/index.html @@ -6,7 +6,7 @@ - + @@ -95,13 +95,13 @@ function App() { return ( -
+ - CDN v4-beta example + CDN v5-alpha example -
+
); } diff --git a/examples/cdn/package.json b/examples/cdn/package.json index eadbd59e98d0df..7b2b45627e6aae 100644 --- a/examples/cdn/package.json +++ b/examples/cdn/package.json @@ -1,6 +1,6 @@ { "name": "cdn", - "version": "4.0.0", + "version": "5.0.0", "private": true, "dependencies": {}, "scripts": {} diff --git a/examples/nextjs-with-typescript/package.json b/examples/nextjs-with-typescript/package.json index df28d997fbf4ea..7ed0c2c52da713 100644 --- a/examples/nextjs-with-typescript/package.json +++ b/examples/nextjs-with-typescript/package.json @@ -1,10 +1,14 @@ { "name": "nextjs-with-typescript", - "version": "4.0.0", + "version": "5.0.0", "private": true, "dependencies": { - "@material-ui/core": "latest", + "@emotion/cache": "latest", + "@emotion/core": "latest", + "@emotion/styled": "latest", + "@material-ui/core": "next", "clsx": "latest", + "create-emotion-server": "latest", "next": "latest", "react": "latest", "react-dom": "latest" diff --git a/examples/nextjs-with-typescript/pages/_app.tsx b/examples/nextjs-with-typescript/pages/_app.tsx index ceda965f74ef29..47b3cce79281ec 100644 --- a/examples/nextjs-with-typescript/pages/_app.tsx +++ b/examples/nextjs-with-typescript/pages/_app.tsx @@ -2,9 +2,13 @@ import React from 'react'; import Head from 'next/head'; import { AppProps } from 'next/app'; import { ThemeProvider } from '@material-ui/core/styles'; +import { CacheProvider } from '@emotion/core'; import CssBaseline from '@material-ui/core/CssBaseline'; +import createCache from '@emotion/cache'; import theme from '../src/theme'; +export const cache = createCache(); + export default function MyApp(props: AppProps) { const { Component, pageProps } = props; @@ -17,7 +21,7 @@ export default function MyApp(props: AppProps) { }, []); return ( - + My page @@ -27,6 +31,6 @@ export default function MyApp(props: AppProps) { - + ); } diff --git a/examples/nextjs-with-typescript/pages/_document.tsx b/examples/nextjs-with-typescript/pages/_document.tsx index f9e3041c9129c4..0dbcf16597d1a0 100644 --- a/examples/nextjs-with-typescript/pages/_document.tsx +++ b/examples/nextjs-with-typescript/pages/_document.tsx @@ -1,7 +1,11 @@ import React from 'react'; import Document, { Html, Head, Main, NextScript } from 'next/document'; import { ServerStyleSheets } from '@material-ui/core/styles'; +import createEmotionServer from 'create-emotion-server'; import theme from '../src/theme'; +import { cache } from './_app'; + +const { extractCritical } = createEmotionServer(cache); export default class MyDocument extends Document { render() { @@ -59,10 +63,20 @@ MyDocument.getInitialProps = async (ctx) => { }); const initialProps = await Document.getInitialProps(ctx); + const styles = extractCritical(initialProps.html); return { ...initialProps, // Styles fragment is rendered after the app and page rendering finish. - styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()], + styles: [ + ...React.Children.toArray(initialProps.styles), + sheets.getStyleElement(), +